Skip to content

Other

Internal libraries

paasify.engines

Paasify Engine management

This class helps to deal with docker engine versions

EngineCompose

Bases: NodeMap, PaasifyObj

Generic docker-engine compose API

Source code in paasify/engines.py
class EngineCompose(NodeMap, PaasifyObj):
    "Generic docker-engine compose API"

    _node_parent_kind = ["Stack"]

    version = None
    docker_file_exists = False
    # docker_file_path = None
    arg_prefix = []

    conf_default = {
        "stack_name": None,
        "stack_path": None,
        "docker_file": "docker-compose.yml",
        # "docker_file_path": None,
    }

    ident = "default"

    compose_bin = "docker"
    # compose_pre = [
    #        "compose",
    #        "--file", "myfile.yml",
    #        "--project-name", "project-name",
    #        ]

    # compose_bin = "docker-compose"
    # compose_pre = [
    #        "--file", "myfile.yml",
    #        "--project-name", "project-name",
    #        ]

    def node_hook_init(self):
        "Create instance attributes"

        self.docker_file_path = None
        self.arg_prefix_full = []
        self.arg_prefix = []

    def node_hook_children(self):
        "Create stack context on start"

        # Get parents
        # stack = self._node_parent
        # prj = stack.prj

        # Init object
        # self.stack_name = self.stack_name
        # self.stack_path = self.stack_path
        # self.docker_file_path = self.docker_file_path or os.path.join(self.stack_path, self.docker_file)
        # pprint (self.__dict__)
        self.docker_file_path = os.path.join(self.stack_path, self.docker_file)
        # pprint (self.__dict__)

        # dsfsdf

        # Pre build args
        self.arg_prefix = [
            "compose",
            "--project-name",
            f"{self.stack_name}",
            # "--project-directory", f"{self.stack_path}",
        ]
        self.arg_prefix_full = [
            "compose",
            "--project-name",
            f"{self.stack_name}",
            "--file",
            f"{self.docker_file_path}",
        ]

    def run(self, cli_args=None, command=None, logger=None, **kwargs):
        "Wrapper to execute commands"

        command = command or self.compose_bin
        cli_args = cli_args or []

        self.log.exec(f"Run: {command} {' '.join(cli_args)}")
        # result = _exec(command, cli_args=cli_args, logger=self.log, **kwargs)
        result = _exec(command, cli_args=cli_args, **kwargs)

        if result:
            result = bin2utf8(result)
            log.notice(result.txtout)

        return result

    def require_stack(self):
        "Ensure stack context"

        if not self.stack_name:
            assert False, "Command not available for stacks!"

    def require_compose_file(self):
        "Raise an exception when compose file is absent"

        self.require_stack()

        if not os.path.isfile(self.docker_file_path):
            self.log.warning("Please build stack first")
            raise error.BuildStackFirstError("Docker file is not built yet !")

    def assemble(self, compose_files, env_file=None, env=None):
        "Generate docker-compose file"

        self.require_stack()

        cli_args = list(self.arg_prefix)

        if env_file:
            cli_args.extend(["--env-file", env_file])
        for file in compose_files:
            cli_args.extend(["--file", file])
        cli_args.extend(
            [
                "config",
                # "--no-interpolate",
                # "--no-normalize",
            ]
        )

        env_string = env or {}
        env_string = {
            k: cast_docker_compose(v)
            for k, v in env.items()
            if isinstance(
                v,
                (
                    str,
                    int,
                    bool,
                    #  type(None) # Do we want None as well ?
                ),
            )
        }

        out = self.run(cli_args=cli_args, _out=None, _env=env_string)
        return out

    # pylint: disable=invalid-name
    def up(self, **kwargs):
        "Start containers"

        self.require_compose_file()
        cli_args = self.arg_prefix_full + [
            "up",
            "--detach",
        ]
        out = self.run(cli_args=cli_args, **kwargs)
        return out

    def down(self, **kwargs):
        "Stop containers"

        self.require_stack()
        # cli_args = list(self.arg_prefix)
        cli_args = self.arg_prefix_full + [
            # "--project-name",
            # self.stack_name,
            "down",
            "--remove-orphans",
        ]

        try:
            out = self.run(cli_args=cli_args, **kwargs)
            # out = _exec("docker-compose", cli_args, **kwargs)
            # if out:
            #    bin2utf8(out)
            #    log.notice(out.txtout)

        # pylint: disable=no-member
        except sh.ErrorReturnCode_1 as err:
            bin2utf8(err)

            # This is U.G.L.Y
            if "has active endpoints" not in err.txterr:
                raise error.DockerCommandFailed(f"{err.txterr}")

        return out

    def logs(self, follow=False):
        "Return container logs"

        self.require_stack()
        sh_options = {}
        cli_args = self.arg_prefix + [
            # "--project-name",
            # self.stack_name,
            "logs",
        ]
        if follow:
            cli_args.append("-f")
            sh_options["_fg"] = True

        out = self.run(cli_args=cli_args, **sh_options)
        print(out)

    # pylint: disable=invalid-name
    def ps(self):
        "Return container processes"

        self.require_stack()

        # Prepare command
        cli_args = self.arg_prefix + [
            # "compose",
            # "--project-name",
            # self.stack_name,
            "ps",
            "--all",
            "--format",
            "json",
        ]
        result = self.run(cli_args=cli_args, _out=None)

        # Report output from json
        stdout = result.txtout
        payload = json.loads(stdout)
        for svc in payload:

            # Get and filter interesting ports
            published = svc["Publishers"] or []
            published = [x for x in published if x.get("PublishedPort") > 0]

            # Reduce duplicates
            for pub in published:
                if pub.get("URL") == "0.0.0.0":
                    pub["URL"] = "::"

            # Format port strings
            exposed = []
            for port in published:
                src_ip = port["URL"]
                src_port = port["PublishedPort"]
                dst_port = port["TargetPort"]
                prot = port["Protocol"]

                r = f"{src_ip}:{src_port}->{dst_port}/{prot}"
                exposed.append(r)

            # Remove duplicates ports and show
            exposed = list(set(exposed))
            print(
                f"  {svc['Project'] :<32} {svc['ID'][:12] :<12} {svc['Name'] :<40} {svc['Service'] :<16} {svc['State'] :<10} {', '.join(exposed)}"
            )
assemble(compose_files, env_file=None, env=None)

Generate docker-compose file

Source code in paasify/engines.py
def assemble(self, compose_files, env_file=None, env=None):
    "Generate docker-compose file"

    self.require_stack()

    cli_args = list(self.arg_prefix)

    if env_file:
        cli_args.extend(["--env-file", env_file])
    for file in compose_files:
        cli_args.extend(["--file", file])
    cli_args.extend(
        [
            "config",
            # "--no-interpolate",
            # "--no-normalize",
        ]
    )

    env_string = env or {}
    env_string = {
        k: cast_docker_compose(v)
        for k, v in env.items()
        if isinstance(
            v,
            (
                str,
                int,
                bool,
                #  type(None) # Do we want None as well ?
            ),
        )
    }

    out = self.run(cli_args=cli_args, _out=None, _env=env_string)
    return out
down(**kwargs)

Stop containers

Source code in paasify/engines.py
def down(self, **kwargs):
    "Stop containers"

    self.require_stack()
    # cli_args = list(self.arg_prefix)
    cli_args = self.arg_prefix_full + [
        # "--project-name",
        # self.stack_name,
        "down",
        "--remove-orphans",
    ]

    try:
        out = self.run(cli_args=cli_args, **kwargs)
        # out = _exec("docker-compose", cli_args, **kwargs)
        # if out:
        #    bin2utf8(out)
        #    log.notice(out.txtout)

    # pylint: disable=no-member
    except sh.ErrorReturnCode_1 as err:
        bin2utf8(err)

        # This is U.G.L.Y
        if "has active endpoints" not in err.txterr:
            raise error.DockerCommandFailed(f"{err.txterr}")

    return out
logs(follow=False)

Return container logs

Source code in paasify/engines.py
def logs(self, follow=False):
    "Return container logs"

    self.require_stack()
    sh_options = {}
    cli_args = self.arg_prefix + [
        # "--project-name",
        # self.stack_name,
        "logs",
    ]
    if follow:
        cli_args.append("-f")
        sh_options["_fg"] = True

    out = self.run(cli_args=cli_args, **sh_options)
    print(out)
node_hook_children()

Create stack context on start

Source code in paasify/engines.py
def node_hook_children(self):
    "Create stack context on start"

    # Get parents
    # stack = self._node_parent
    # prj = stack.prj

    # Init object
    # self.stack_name = self.stack_name
    # self.stack_path = self.stack_path
    # self.docker_file_path = self.docker_file_path or os.path.join(self.stack_path, self.docker_file)
    # pprint (self.__dict__)
    self.docker_file_path = os.path.join(self.stack_path, self.docker_file)
    # pprint (self.__dict__)

    # dsfsdf

    # Pre build args
    self.arg_prefix = [
        "compose",
        "--project-name",
        f"{self.stack_name}",
        # "--project-directory", f"{self.stack_path}",
    ]
    self.arg_prefix_full = [
        "compose",
        "--project-name",
        f"{self.stack_name}",
        "--file",
        f"{self.docker_file_path}",
    ]
node_hook_init()

Create instance attributes

Source code in paasify/engines.py
def node_hook_init(self):
    "Create instance attributes"

    self.docker_file_path = None
    self.arg_prefix_full = []
    self.arg_prefix = []
ps()

Return container processes

Source code in paasify/engines.py
def ps(self):
    "Return container processes"

    self.require_stack()

    # Prepare command
    cli_args = self.arg_prefix + [
        # "compose",
        # "--project-name",
        # self.stack_name,
        "ps",
        "--all",
        "--format",
        "json",
    ]
    result = self.run(cli_args=cli_args, _out=None)

    # Report output from json
    stdout = result.txtout
    payload = json.loads(stdout)
    for svc in payload:

        # Get and filter interesting ports
        published = svc["Publishers"] or []
        published = [x for x in published if x.get("PublishedPort") > 0]

        # Reduce duplicates
        for pub in published:
            if pub.get("URL") == "0.0.0.0":
                pub["URL"] = "::"

        # Format port strings
        exposed = []
        for port in published:
            src_ip = port["URL"]
            src_port = port["PublishedPort"]
            dst_port = port["TargetPort"]
            prot = port["Protocol"]

            r = f"{src_ip}:{src_port}->{dst_port}/{prot}"
            exposed.append(r)

        # Remove duplicates ports and show
        exposed = list(set(exposed))
        print(
            f"  {svc['Project'] :<32} {svc['ID'][:12] :<12} {svc['Name'] :<40} {svc['Service'] :<16} {svc['State'] :<10} {', '.join(exposed)}"
        )
require_compose_file()

Raise an exception when compose file is absent

Source code in paasify/engines.py
def require_compose_file(self):
    "Raise an exception when compose file is absent"

    self.require_stack()

    if not os.path.isfile(self.docker_file_path):
        self.log.warning("Please build stack first")
        raise error.BuildStackFirstError("Docker file is not built yet !")
require_stack()

Ensure stack context

Source code in paasify/engines.py
def require_stack(self):
    "Ensure stack context"

    if not self.stack_name:
        assert False, "Command not available for stacks!"
run(cli_args=None, command=None, logger=None, **kwargs)

Wrapper to execute commands

Source code in paasify/engines.py
def run(self, cli_args=None, command=None, logger=None, **kwargs):
    "Wrapper to execute commands"

    command = command or self.compose_bin
    cli_args = cli_args or []

    self.log.exec(f"Run: {command} {' '.join(cli_args)}")
    # result = _exec(command, cli_args=cli_args, logger=self.log, **kwargs)
    result = _exec(command, cli_args=cli_args, **kwargs)

    if result:
        result = bin2utf8(result)
        log.notice(result.txtout)

    return result
up(**kwargs)

Start containers

Source code in paasify/engines.py
def up(self, **kwargs):
    "Start containers"

    self.require_compose_file()
    cli_args = self.arg_prefix_full + [
        "up",
        "--detach",
    ]
    out = self.run(cli_args=cli_args, **kwargs)
    return out

EngineComposeV1

Bases: EngineCompose

Docker-engine: Support for version until 1.29

Source code in paasify/engines.py
class EngineComposeV1(EngineCompose):
    "Docker-engine: Support for version until 1.29"

    ident = "docker-compose 1"

    # pylint: disable=invalid-name
    def ps(self):
        cli_args = [
            "--file",
            self.docker_file_path,
            "ps",
            "--all",
        ]

        result = _exec("docker-compose", cli_args, _fg=True)

        return result

EngineComposeV2

Bases: EngineCompose

Docker-engine: Support for version until 2.6

Source code in paasify/engines.py
class EngineComposeV2(EngineCompose):
    "Docker-engine: Support for version until 2.6"

    ident = "docker compose 2"

EngineCompose_16

Bases: EngineCompose

Docker-engine: Support for version until 1.6

Source code in paasify/engines.py
class EngineCompose_16(EngineCompose):
    "Docker-engine: Support for version until 1.6"

    ident = "docker-compose-1.6"

EngineDetect

Class helper to retrieve the appropriate docker-engine class

Source code in paasify/engines.py
class EngineDetect:
    "Class helper to retrieve the appropriate docker-engine class"

    versions = {
        "docker": {
            "20.10.17": {},
        },
        "docker-compose": {
            "2.0.0": EngineComposeV2,
            "1.0.0": EngineComposeV1,
            # "2.6.1": EngineCompose_26,
            # "1.29.0": EngineCompose_129,
            # "1.6.3": EngineCompose_16,
        },
        "podman-compose": {},
    }

    def detect_docker_compose(self):
        "Detect current version of docker compose. Return a docker-engine class."

        # pylint: disable=no-member

        # Try docker-compose v1

        # Try docker compose v2
        out = "No output for command"
        patt = r"version v?(?P<version>(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+))"
        try:
            log.notice("This can take age when debugger is enabled...")
            out = _exec("docker", ["compose", "version"])
            # TOFIX: This takes ages in debugger, when above _log_msg is unset ?
            # out = cmd('--version')
            bin2utf8(out)
        except sh.ErrorReturnCode as err:
            # raise error.DockerUnsupportedVersion(
            #    f"Impossible to guess docker-compose version: {out}"
            # ) from err

            # pylint: disable=no-member
            try:

                # cmd = sh.Command("docker-compose", _log_msg='paasify')
                log.notice("This can take age when debugger is enabled...")
                out = _exec("docker-compose", ["--version"])
                # TOFIX: This takes ages in debugger, when above _log_msg is unset ?
                # out = cmd('--version')
                bin2utf8(out)
            except sh.ErrorReturnCode:
                raise error.DockerUnsupportedVersion(
                    f"Impossible to guess docker-compose version: {out}"
                ) from err

        # Scan version
        match = re.search(patt, out.txtout)
        if match:
            version = match.groupdict()
        else:
            msg = f"Output format of docker-compose is not recognised: {out.txtout}"
            raise error.DockerUnsupportedVersion(msg)
        curr_ver = semver.VersionInfo.parse(version["version"])

        # Scan available versions
        versions = sorted(
            list(map(semver.VersionInfo.parse, self.versions["docker-compose"].keys()))
        )
        versions.reverse()
        match = None
        for version in versions:
            works = curr_ver.match(f">={version}")
            if works:
                match = str(version)
                break

        if not match:
            raise error.DockerUnsupportedVersion(
                f"Version of docker-compose is not supported: {curr_ver}"
            )

        cls = self.versions["docker-compose"][match]
        cls.version = match
        cls.name = "docker-compose"
        cls.ident = match
        return cls

    def detect(self, engine=None):
        "Return the Engine class that match engine string"

        if not engine:
            log.info("Guessing best docker engine ...")
            obj = self.detect_docker_compose()
        else:

            if engine not in self.versions["docker-compose"]:
                versions = list(self.versions["docker-compose"].keys())
                log.warning(f"Please select engine one of: {versions}")
                raise error.DockerUnsupportedVersion(
                    f"Unknown docker-engine version: {engine}"
                )
            obj = self.versions["docker-compose"][engine]
        # if not result:
        #     raise error.DockerUnsupportedVersion(f"Can;t find docker-compose")

        log.debug(f"Detected docker-compose version: {obj.version}")

        return obj
detect(engine=None)

Return the Engine class that match engine string

Source code in paasify/engines.py
def detect(self, engine=None):
    "Return the Engine class that match engine string"

    if not engine:
        log.info("Guessing best docker engine ...")
        obj = self.detect_docker_compose()
    else:

        if engine not in self.versions["docker-compose"]:
            versions = list(self.versions["docker-compose"].keys())
            log.warning(f"Please select engine one of: {versions}")
            raise error.DockerUnsupportedVersion(
                f"Unknown docker-engine version: {engine}"
            )
        obj = self.versions["docker-compose"][engine]
    # if not result:
    #     raise error.DockerUnsupportedVersion(f"Can;t find docker-compose")

    log.debug(f"Detected docker-compose version: {obj.version}")

    return obj
detect_docker_compose()

Detect current version of docker compose. Return a docker-engine class.

Source code in paasify/engines.py
def detect_docker_compose(self):
    "Detect current version of docker compose. Return a docker-engine class."

    # pylint: disable=no-member

    # Try docker-compose v1

    # Try docker compose v2
    out = "No output for command"
    patt = r"version v?(?P<version>(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+))"
    try:
        log.notice("This can take age when debugger is enabled...")
        out = _exec("docker", ["compose", "version"])
        # TOFIX: This takes ages in debugger, when above _log_msg is unset ?
        # out = cmd('--version')
        bin2utf8(out)
    except sh.ErrorReturnCode as err:
        # raise error.DockerUnsupportedVersion(
        #    f"Impossible to guess docker-compose version: {out}"
        # ) from err

        # pylint: disable=no-member
        try:

            # cmd = sh.Command("docker-compose", _log_msg='paasify')
            log.notice("This can take age when debugger is enabled...")
            out = _exec("docker-compose", ["--version"])
            # TOFIX: This takes ages in debugger, when above _log_msg is unset ?
            # out = cmd('--version')
            bin2utf8(out)
        except sh.ErrorReturnCode:
            raise error.DockerUnsupportedVersion(
                f"Impossible to guess docker-compose version: {out}"
            ) from err

    # Scan version
    match = re.search(patt, out.txtout)
    if match:
        version = match.groupdict()
    else:
        msg = f"Output format of docker-compose is not recognised: {out.txtout}"
        raise error.DockerUnsupportedVersion(msg)
    curr_ver = semver.VersionInfo.parse(version["version"])

    # Scan available versions
    versions = sorted(
        list(map(semver.VersionInfo.parse, self.versions["docker-compose"].keys()))
    )
    versions.reverse()
    match = None
    for version in versions:
        works = curr_ver.match(f">={version}")
        if works:
            match = str(version)
            break

    if not match:
        raise error.DockerUnsupportedVersion(
            f"Version of docker-compose is not supported: {curr_ver}"
        )

    cls = self.versions["docker-compose"][match]
    cls.version = match
    cls.name = "docker-compose"
    cls.ident = match
    return cls

bin2utf8(obj)

Transform sh output bin to utf-8

Source code in paasify/engines.py
def bin2utf8(obj):
    "Transform sh output bin to utf-8"

    if hasattr(obj, "stdout"):
        obj.txtout = obj.stdout.decode("utf-8").rstrip("\n")
    else:
        obj.txtout = None
    if hasattr(obj, "stderr"):
        obj.txterr = obj.stderr.decode("utf-8").rstrip("\n")
    else:
        obj.txterr = None
    return obj

paasify.sources

Paasify Source management libray

Source

Bases: NodeMap, PaasifyObj

A Source instance

Source code in paasify/sources.py
class Source(NodeMap, PaasifyObj):
    """A Source instance"""

    conf_default = {
        "remote": None,
        "name": None,
        "prefix": None,
        "dir": None,
        "install": None,
        # "prefix": "https://github.com/%s.git",
    }

    schema_def = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "Paasify Source configuration",
        "oneOf": [
            {
                "type": "string",
            },
            {
                "type": "object",
                "additionalProperties": False,
                "properties": {
                    "name": {
                        "type": "string",
                    },
                    "remote": {
                        "type": "string",
                    },
                    "ref": {
                        "type": "string",
                    },
                    "prefix": {
                        "type": "string",
                    },
                    "aliases": {
                        "type": "array",
                    },
                },
            },
        ],
    }

    def extract_short(self, short):
        "Guess data from source short forms"
        ret = None

        # Try if remote
        remote = self.remote or short
        if remote:
            out = gitparse(remote)
            if hasattr(out, "url"):
                # Guess from valid URL
                ret = {
                    "remote": out.url,
                    "name": out.repo,
                    "dir": f"{out.owner}-{out.repo}",
                    "install": "clone",
                    "guess": f"a git repo: {out.url}",
                }
            else:
                self.log.trace(f"Source is not a git repo: {self._node_conf_raw}")

        # Try if dir
        value_ = self.dir or self.remote or self.name or short
        if not ret and value_:
            # So is it a path? Relative, absolute?
            install_ = self.install or "none"

            # Look for direct path
            path = FileReference(value_, root=self.runtime.root_path, keep=True)
            if os.path.isdir(path.path()):
                path_ = path.path()
                ret = {
                    "remote": None,
                    "name": str(self.name or os.path.basename(path.path_abs())),
                    "path": path_,
                    "install": install_,
                    "guess": f"path: {path_}",
                }
            else:
                self.log.trace(
                    f"Source is not found in path '{path}' for {self._node_conf_raw}"
                )

            # Look into <prj>.paasify/collections
            if not ret:
                path_ = FileReference(
                    value_, self.runtime.project_collection_dir, keep=True
                ).path()
                if os.path.isdir(path_):
                    ret = {
                        "remote": None,
                        "name": str(self.name or os.path.basename(path_)),
                        "path": path_,
                        "install": install_,
                        "guess": f"inside the stack: {path_}",
                    }
                else:
                    self.log.trace(
                        f"Source is not found in collection dir '{path}' for {self._node_conf_raw}"
                    )

        # Try default gh repo
        value_ = self.dir or self.remote or short
        if not ret and value_:
            # Last change to guess from pattern
            owner = None
            repo = value_
            if "/" in value_:
                parts = short.split("/", 2)
                owner = parts[0]
                repo = parts[1]

            if not owner:
                owner = "paasify"

            remote_ = f"https://github.com/{owner}/{repo}.git"
            ret = {
                "remote": remote_,
                "name": f"{owner}/{repo}",
                "dir": f"{owner}/{repo}",
                "install": "clone",
                "guess": "Refer to github repository ({remote_})",
            }

        if not ret:
            msg = f"Invalid configuration for source: {self._node_conf_raw}"
            raise error.InvalidSourceConfig(msg)

        assert ret["install"]
        return ret

    def node_hook_init(self, **kwargs):
        "Setup object vars from parents"
        self.obj_prj = self._node_parent._node_parent
        self.collection_dir = self.obj_prj.runtime.project_collection_dir

    def node_hook_transform(self, payload):
        "Transform short form into dict"

        if isinstance(payload, str):
            payload = {"short": payload}
        return payload

    def node_hook_final(self):
        "Init source correctly from available elements"

        self.runtime = self._node_parent._node_parent.runtime
        self.relative = self.runtime.relative

        # TODO: this is very awful
        self.short = getattr(self, "short", None)
        self.name = getattr(self, "name", None)
        self.dir = getattr(self, "dir", None)
        self.aliases = getattr(self, "aliases", [])
        self.remote = getattr(self, "remote", None)
        self.install = getattr(self, "install", None)
        self.path = None

        extracted = None
        extracted = self.extract_short(self.short)
        for key, value in extracted.items():
            setattr(self, key, getattr(self, key, None) or value)
        self.log.debug(f"Source '{self.name}' is {extracted['guess']}")

        # The actual thing we all want !
        if not self.path:
            self.path = FileReference(
                self.dir, self.runtime.project_collection_dir, keep=False
            ).path()

        assert self.name
        assert self.path

    def is_git(self):
        "Return true if git repo"
        test_path = os.path.join(self.path, ".git")
        return os.path.isdir(test_path)

    def is_installed(self):
        "Return true if installed"
        return os.path.isdir(self.path)

    def cmd_install(self):
        "Install from remote"

        # Check if install dir is already present
        remote = self.remote
        install = self.install

        # Skip non installable sources
        if install == "none":
            self.log.notice(f"Source '{self.name}' is a path, no need to install")
            return

        # Ensure parent directory exists before install
        dest = os.path.join(self.collection_dir, self.dir)
        parent_dir = os.path.dirname(dest)
        if not os.path.isdir(parent_dir):
            self.log.info(f"Creating parent directories: {parent_dir}")
            os.makedirs(parent_dir)

        # Install source
        if install.startswith("symlink"):
            self.log.notice(f"Installing '{self.name}' via {install}: {remote}")

            # Ensure nothing exists first
            if os.path.exists(dest):

                if not os.path.exists(os.readlink(dest)):
                    self.log.notice(f"Removing broken link in: {dest}")
                    os.unlink(dest)
                else:
                    assert False, "Found a bug !"

            # Create the symlink
            self.log.notice(f"Collection symlinked in: {dest}")
            os.symlink(remote, dest)

        elif install == "clone":
            # Git clone that stuff
            if os.path.isdir(self.path):
                self.log.notice(
                    f"Source '{self.name}' is already installed in {self.path}"
                )
                return
            self.log.notice(f"Installing '{self.name}' git: {remote} in {self.path}")
            cli_args = ["clone", remote, self.path]
            _exec("git", cli_args, _fg=True)
        else:
            raise Exception(f"Unsupported methods: {install}")

    def cmd_update(self):
        "Update from remote"

        # Check if install dir is already present
        if not os.path.isdir(self.path):
            self.log.info("This source is not installed yet")
            return

        # Git clone that stuff
        git_url = self.git_url()
        self.log.info(f"Updating git repo: {git_url}")
        cli_args = [
            "-C",
            self.path,
            "pull",
        ]
        _exec("git", cli_args, _fg=True)
cmd_install()

Install from remote

Source code in paasify/sources.py
def cmd_install(self):
    "Install from remote"

    # Check if install dir is already present
    remote = self.remote
    install = self.install

    # Skip non installable sources
    if install == "none":
        self.log.notice(f"Source '{self.name}' is a path, no need to install")
        return

    # Ensure parent directory exists before install
    dest = os.path.join(self.collection_dir, self.dir)
    parent_dir = os.path.dirname(dest)
    if not os.path.isdir(parent_dir):
        self.log.info(f"Creating parent directories: {parent_dir}")
        os.makedirs(parent_dir)

    # Install source
    if install.startswith("symlink"):
        self.log.notice(f"Installing '{self.name}' via {install}: {remote}")

        # Ensure nothing exists first
        if os.path.exists(dest):

            if not os.path.exists(os.readlink(dest)):
                self.log.notice(f"Removing broken link in: {dest}")
                os.unlink(dest)
            else:
                assert False, "Found a bug !"

        # Create the symlink
        self.log.notice(f"Collection symlinked in: {dest}")
        os.symlink(remote, dest)

    elif install == "clone":
        # Git clone that stuff
        if os.path.isdir(self.path):
            self.log.notice(
                f"Source '{self.name}' is already installed in {self.path}"
            )
            return
        self.log.notice(f"Installing '{self.name}' git: {remote} in {self.path}")
        cli_args = ["clone", remote, self.path]
        _exec("git", cli_args, _fg=True)
    else:
        raise Exception(f"Unsupported methods: {install}")
cmd_update()

Update from remote

Source code in paasify/sources.py
def cmd_update(self):
    "Update from remote"

    # Check if install dir is already present
    if not os.path.isdir(self.path):
        self.log.info("This source is not installed yet")
        return

    # Git clone that stuff
    git_url = self.git_url()
    self.log.info(f"Updating git repo: {git_url}")
    cli_args = [
        "-C",
        self.path,
        "pull",
    ]
    _exec("git", cli_args, _fg=True)
extract_short(short)

Guess data from source short forms

Source code in paasify/sources.py
def extract_short(self, short):
    "Guess data from source short forms"
    ret = None

    # Try if remote
    remote = self.remote or short
    if remote:
        out = gitparse(remote)
        if hasattr(out, "url"):
            # Guess from valid URL
            ret = {
                "remote": out.url,
                "name": out.repo,
                "dir": f"{out.owner}-{out.repo}",
                "install": "clone",
                "guess": f"a git repo: {out.url}",
            }
        else:
            self.log.trace(f"Source is not a git repo: {self._node_conf_raw}")

    # Try if dir
    value_ = self.dir or self.remote or self.name or short
    if not ret and value_:
        # So is it a path? Relative, absolute?
        install_ = self.install or "none"

        # Look for direct path
        path = FileReference(value_, root=self.runtime.root_path, keep=True)
        if os.path.isdir(path.path()):
            path_ = path.path()
            ret = {
                "remote": None,
                "name": str(self.name or os.path.basename(path.path_abs())),
                "path": path_,
                "install": install_,
                "guess": f"path: {path_}",
            }
        else:
            self.log.trace(
                f"Source is not found in path '{path}' for {self._node_conf_raw}"
            )

        # Look into <prj>.paasify/collections
        if not ret:
            path_ = FileReference(
                value_, self.runtime.project_collection_dir, keep=True
            ).path()
            if os.path.isdir(path_):
                ret = {
                    "remote": None,
                    "name": str(self.name or os.path.basename(path_)),
                    "path": path_,
                    "install": install_,
                    "guess": f"inside the stack: {path_}",
                }
            else:
                self.log.trace(
                    f"Source is not found in collection dir '{path}' for {self._node_conf_raw}"
                )

    # Try default gh repo
    value_ = self.dir or self.remote or short
    if not ret and value_:
        # Last change to guess from pattern
        owner = None
        repo = value_
        if "/" in value_:
            parts = short.split("/", 2)
            owner = parts[0]
            repo = parts[1]

        if not owner:
            owner = "paasify"

        remote_ = f"https://github.com/{owner}/{repo}.git"
        ret = {
            "remote": remote_,
            "name": f"{owner}/{repo}",
            "dir": f"{owner}/{repo}",
            "install": "clone",
            "guess": "Refer to github repository ({remote_})",
        }

    if not ret:
        msg = f"Invalid configuration for source: {self._node_conf_raw}"
        raise error.InvalidSourceConfig(msg)

    assert ret["install"]
    return ret
is_git()

Return true if git repo

Source code in paasify/sources.py
def is_git(self):
    "Return true if git repo"
    test_path = os.path.join(self.path, ".git")
    return os.path.isdir(test_path)
is_installed()

Return true if installed

Source code in paasify/sources.py
def is_installed(self):
    "Return true if installed"
    return os.path.isdir(self.path)
node_hook_final()

Init source correctly from available elements

Source code in paasify/sources.py
def node_hook_final(self):
    "Init source correctly from available elements"

    self.runtime = self._node_parent._node_parent.runtime
    self.relative = self.runtime.relative

    # TODO: this is very awful
    self.short = getattr(self, "short", None)
    self.name = getattr(self, "name", None)
    self.dir = getattr(self, "dir", None)
    self.aliases = getattr(self, "aliases", [])
    self.remote = getattr(self, "remote", None)
    self.install = getattr(self, "install", None)
    self.path = None

    extracted = None
    extracted = self.extract_short(self.short)
    for key, value in extracted.items():
        setattr(self, key, getattr(self, key, None) or value)
    self.log.debug(f"Source '{self.name}' is {extracted['guess']}")

    # The actual thing we all want !
    if not self.path:
        self.path = FileReference(
            self.dir, self.runtime.project_collection_dir, keep=False
        ).path()

    assert self.name
    assert self.path
node_hook_init(**kwargs)

Setup object vars from parents

Source code in paasify/sources.py
def node_hook_init(self, **kwargs):
    "Setup object vars from parents"
    self.obj_prj = self._node_parent._node_parent
    self.collection_dir = self.obj_prj.runtime.project_collection_dir
node_hook_transform(payload)

Transform short form into dict

Source code in paasify/sources.py
def node_hook_transform(self, payload):
    "Transform short form into dict"

    if isinstance(payload, str):
        payload = {"short": payload}
    return payload

SourcesManager

Bases: NodeList, PaasifyObj

Source manager

Source code in paasify/sources.py
class SourcesManager(NodeList, PaasifyObj):
    "Source manager"

    conf_schema = {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "Paasify Source configuration",
        "description": "Configure a list of collections to install",
        "oneOf": [
            {
                "type": "null",
                "title": "Unset sources",
                "description": "If null or empty, it does not use any source",
            },
            {
                "title": "List of sources",
                "description": "Each source define a git repository and a name for stack references",
                "type": "array",
                "items": Source.schema_def,
            },
        ],
    }

    conf_children = Source

    def node_hook_transform(self, payload):

        # Always inject the paasify core source at first
        payload = payload or []
        payload.insert(
            0,
            {
                "name": "paasify",
                "dir": PAASIFY_COLLECTION_PATH,
            },
        )

        return payload

    def node_hook_final(self):
        "Prepare sources"

        # Look for duplicate names
        known_names = []
        for src in self.get_children():

            if src.name in known_names:
                dups = [dup._node_conf_raw for dup in self.find_by_name_alias(src.name)]
                dups = ", ".join(dups)
                msg = f"Two stacks have the same name: {dups}"
                raise error.DuplicateSourceName(msg)
            known_names.append(src.name)

            for alias in src.aliases:
                if alias in known_names:
                    dups = [
                        dup._node_conf_raw for dup in self.find_by_name_alias(alias)
                    ]
                    dups = ", ".join(dups)
                    msg = f"An alias have duplicate assignments: {dups}"
                    raise error.DuplicateSourceName(msg)
                known_names.append(alias)

        # Assign a default node:
        default = self.find_by_name_alias("default")
        if len(default) < 1:
            first_node = first(self.get_children())
            if first_node:
                first_node.aliases.append("default")
                self.log.info(
                    f"First source has been set as default: {first_node.name}"
                )
            else:
                self.log.info("No source found for this project")

    def get_all(self):
        "Return the list of all sources"
        return list(self.get_children())

    def list_all_names(self) -> list:
        "Return a list of valid string names"

        sources = self.get_children()
        return [src.name for src in sources]

    def get_app_source(self, app_name, source=None):
        "Return the source of an app_name"

        if source:
            sources = self.find_by_name_alias(source)
        else:
            sources = self.get_all()

        checked_dirs = []
        for src in sources:
            check_dir = os.path.join(src.path, app_name)
            if os.path.isdir(check_dir):
                return src
            checked_dirs.append(check_dir)

        # Fail and explain why
        sources = [src.name for src in self.get_all()]
        src_str = ",".join(sources)
        msg_dirs = " ".join(checked_dirs)
        msg = f"Impossible to find app '{app_name}' in sources '{src_str}' in paths: {msg_dirs}"

        if len(sources) != checked_dirs:
            hint = "Are you sure you have installed sources before? If not, run: paasify src install"
            self.log.warning(hint)
        raise error.MissingApp(msg)

    def get_search_paths(self):
        "Get search paths"

        return [{"path": src.path, "src": src} for src in self.get_children()]

    def find_by_name_alias(self, string, name=None, alias=None):
        "Return all src that match name or alias"

        name = name or string
        alias = alias or string

        sources = self.get_children()
        return [src for src in sources if name == src.name or alias in src.aliases]

    # Commands
    # ======================

    def cmd_tree(self) -> list:
        "Show a tree of all sources"

        print("Not implemented yet")

    def cmd_ls(self, explain=False) -> list:
        "List all stacks names"

        sources = self.get_children()
        for src in [None] + sources:

            if not src:
                remote = "REMOTE"
                name = "NAME"
                is_installed = "INSTALLED"
                is_git = "GIT"
                path = "PATH"
            else:
                is_installed = "True" if src.is_installed() else "False"
                is_git = "True" if src.is_git() else "False"
                remote = src.remote or ""
                name = src.name
                path = src.path

            print(
                f"  {name :<20} {is_installed :<10} {is_git :<8} {remote :<50} {path}"
            )

    def cmd_install(self):
        "Ensure all sources are installed"

        sources = self.get_children()
        for src in sources:
            log.notice(f"Installing source: {src.name}")
            src.cmd_install()

    def cmd_update(self):
        "Update all source"

        sources = self.get_children()
        for src in sources:
            log.notice(f"Installing source: {src.name}")
            src.cmd_update()
cmd_install()

Ensure all sources are installed

Source code in paasify/sources.py
def cmd_install(self):
    "Ensure all sources are installed"

    sources = self.get_children()
    for src in sources:
        log.notice(f"Installing source: {src.name}")
        src.cmd_install()
cmd_ls(explain=False)

List all stacks names

Source code in paasify/sources.py
def cmd_ls(self, explain=False) -> list:
    "List all stacks names"

    sources = self.get_children()
    for src in [None] + sources:

        if not src:
            remote = "REMOTE"
            name = "NAME"
            is_installed = "INSTALLED"
            is_git = "GIT"
            path = "PATH"
        else:
            is_installed = "True" if src.is_installed() else "False"
            is_git = "True" if src.is_git() else "False"
            remote = src.remote or ""
            name = src.name
            path = src.path

        print(
            f"  {name :<20} {is_installed :<10} {is_git :<8} {remote :<50} {path}"
        )
cmd_tree()

Show a tree of all sources

Source code in paasify/sources.py
def cmd_tree(self) -> list:
    "Show a tree of all sources"

    print("Not implemented yet")
cmd_update()

Update all source

Source code in paasify/sources.py
def cmd_update(self):
    "Update all source"

    sources = self.get_children()
    for src in sources:
        log.notice(f"Installing source: {src.name}")
        src.cmd_update()
find_by_name_alias(string, name=None, alias=None)

Return all src that match name or alias

Source code in paasify/sources.py
def find_by_name_alias(self, string, name=None, alias=None):
    "Return all src that match name or alias"

    name = name or string
    alias = alias or string

    sources = self.get_children()
    return [src for src in sources if name == src.name or alias in src.aliases]
get_all()

Return the list of all sources

Source code in paasify/sources.py
def get_all(self):
    "Return the list of all sources"
    return list(self.get_children())
get_app_source(app_name, source=None)

Return the source of an app_name

Source code in paasify/sources.py
def get_app_source(self, app_name, source=None):
    "Return the source of an app_name"

    if source:
        sources = self.find_by_name_alias(source)
    else:
        sources = self.get_all()

    checked_dirs = []
    for src in sources:
        check_dir = os.path.join(src.path, app_name)
        if os.path.isdir(check_dir):
            return src
        checked_dirs.append(check_dir)

    # Fail and explain why
    sources = [src.name for src in self.get_all()]
    src_str = ",".join(sources)
    msg_dirs = " ".join(checked_dirs)
    msg = f"Impossible to find app '{app_name}' in sources '{src_str}' in paths: {msg_dirs}"

    if len(sources) != checked_dirs:
        hint = "Are you sure you have installed sources before? If not, run: paasify src install"
        self.log.warning(hint)
    raise error.MissingApp(msg)
get_search_paths()

Get search paths

Source code in paasify/sources.py
def get_search_paths(self):
    "Get search paths"

    return [{"path": src.path, "src": src} for src in self.get_children()]
list_all_names()

Return a list of valid string names

Source code in paasify/sources.py
def list_all_names(self) -> list:
    "Return a list of valid string names"

    sources = self.get_children()
    return [src.name for src in sources]
node_hook_final()

Prepare sources

Source code in paasify/sources.py
def node_hook_final(self):
    "Prepare sources"

    # Look for duplicate names
    known_names = []
    for src in self.get_children():

        if src.name in known_names:
            dups = [dup._node_conf_raw for dup in self.find_by_name_alias(src.name)]
            dups = ", ".join(dups)
            msg = f"Two stacks have the same name: {dups}"
            raise error.DuplicateSourceName(msg)
        known_names.append(src.name)

        for alias in src.aliases:
            if alias in known_names:
                dups = [
                    dup._node_conf_raw for dup in self.find_by_name_alias(alias)
                ]
                dups = ", ".join(dups)
                msg = f"An alias have duplicate assignments: {dups}"
                raise error.DuplicateSourceName(msg)
            known_names.append(alias)

    # Assign a default node:
    default = self.find_by_name_alias("default")
    if len(default) < 1:
        first_node = first(self.get_children())
        if first_node:
            first_node.aliases.append("default")
            self.log.info(
                f"First source has been set as default: {first_node.name}"
            )
        else:
            self.log.info("No source found for this project")

Common libraries

paasify.common

Paasify common library

Holds common pieces of code

OutputFormat

Bases: str, Enum

Available paasify format outputs

Source code in paasify/common.py
class OutputFormat(str, Enum):
    "Available paasify format outputs"

    # pylint: disable=invalid-name

    yaml = "yaml"
    json = "json"

SchemaTarget

Bases: str, Enum

Available schema items

Source code in paasify/common.py
class SchemaTarget(str, Enum):
    "Available schema items"

    app = "app"
    prj = "prj"
    prj_config = "prj_config"
    prj_stacks = "prj_stacks"
    prj_sources = "prj_sources"

StringTemplate

Bases: Template

String Template class override to support version of python below 3.11

Source code: Source: https://github.com/python/cpython/commit/dce642f24418c58e67fa31a686575c980c31dd37

Source code in paasify/common.py
class StringTemplate(Template):
    """
    String Template class override to support version of python below 3.11

    Source code: Source: https://github.com/python/cpython/commit/dce642f24418c58e67fa31a686575c980c31dd37
    """

    def get_identifiers(self):
        """Returns a list of the valid identifiers in the template, in the order
        they first appear, ignoring any invalid identifiers."""

        ids = []
        for mo in self.pattern.finditer(self.template):
            named = mo.group("named") or mo.group("braced")
            if named is not None and named not in ids:
                # add a named group only the first time it appears
                ids.append(named)
            elif (
                named is None
                and mo.group("invalid") is None
                and mo.group("escaped") is None
            ):
                # If all the groups are None, there must be
                # another group we're not expecting
                raise ValueError("Unrecognized named group in pattern", self.pattern)
        return ids

    def is_valid(self):
        """Returns false if the template has invalid placeholders that will cause
        :meth:`substitute` to raise :exc:`ValueError`.
        """

        for mo in self.pattern.finditer(self.template):
            if mo.group("invalid") is not None:
                return False
            if (
                mo.group("named") is None
                and mo.group("braced") is None
                and mo.group("escaped") is None
            ):
                # If all the groups are None, there must be
                # another group we're not expecting
                raise ValueError("Unrecognized named group in pattern", self.pattern)
        return True
get_identifiers()

Returns a list of the valid identifiers in the template, in the order they first appear, ignoring any invalid identifiers.

Source code in paasify/common.py
def get_identifiers(self):
    """Returns a list of the valid identifiers in the template, in the order
    they first appear, ignoring any invalid identifiers."""

    ids = []
    for mo in self.pattern.finditer(self.template):
        named = mo.group("named") or mo.group("braced")
        if named is not None and named not in ids:
            # add a named group only the first time it appears
            ids.append(named)
        elif (
            named is None
            and mo.group("invalid") is None
            and mo.group("escaped") is None
        ):
            # If all the groups are None, there must be
            # another group we're not expecting
            raise ValueError("Unrecognized named group in pattern", self.pattern)
    return ids
is_valid()

Returns false if the template has invalid placeholders that will cause :meth:substitute to raise :exc:ValueError.

Source code in paasify/common.py
def is_valid(self):
    """Returns false if the template has invalid placeholders that will cause
    :meth:`substitute` to raise :exc:`ValueError`.
    """

    for mo in self.pattern.finditer(self.template):
        if mo.group("invalid") is not None:
            return False
        if (
            mo.group("named") is None
            and mo.group("braced") is None
            and mo.group("escaped") is None
        ):
            # If all the groups are None, there must be
            # another group we're not expecting
            raise ValueError("Unrecognized named group in pattern", self.pattern)
    return True

cast_docker_compose(var)

Convert any types to strings

Source code in paasify/common.py
def cast_docker_compose(var):
    "Convert any types to strings"

    if var is None:
        result = ""
    elif isinstance(var, (bool)):
        result = "true" if var else "false"
    elif isinstance(var, (str, int)):
        result = str(var)
    elif isinstance(var, list):
        result = ",".join(var)
    elif isinstance(var, dict):
        result = ",".join([f"{key}={str(val)}" for key, val in var.items()])
    else:
        raise Exception(f"Impossible to cast value: {var}")

    return result

ensure_dir_exists(path)

Ensure directories exist for a given path

Source code in paasify/common.py
def ensure_dir_exists(path):
    """Ensure directories exist for a given path"""
    if not os.path.isdir(path):
        log.info(f"Create directory: {path}")
        os.makedirs(path)
        return True
    return False

ensure_parent_dir_exists(path)

Ensure parent directories exist for a given path

Source code in paasify/common.py
def ensure_parent_dir_exists(path):
    """Ensure parent directories exist for a given path"""
    parent = os.path.dirname(os.path.normpath(path))
    return ensure_dir_exists(parent)

filter_existing_files(root_path, candidates)

Return only existing files

Source code in paasify/common.py
def filter_existing_files(root_path, candidates):
    """Return only existing files"""
    result = [
        os.path.join(root_path, cand)
        for cand in candidates
        if os.path.isfile(os.path.join(root_path, cand))
    ]
    return list(set(result))

find_file_up(names, paths)

Find every files names in names list in every listed paths

Source code in paasify/common.py
def find_file_up(names, paths):
    """
    Find every files names in names list in
    every listed paths
    """
    assert isinstance(names, list), f"Names must be array, not: {type(names)}"
    assert isinstance(paths, list), f"Paths must be array, not: {type(names)}"

    result = []
    for path in paths:
        for name in names:
            file_path = os.path.join(path, name)
            if os.access(file_path, os.R_OK):
                result.append(file_path)

    return result

get_paasify_pkg_dir()

Return the dir where the actual paasify source code lives

Source code in paasify/common.py
def get_paasify_pkg_dir():
    """Return the dir where the actual paasify source code lives"""

    # pylint: disable=import-outside-toplevel
    import paasify as _

    return os.path.dirname(_.__file__)

list_parent_dirs(path)

Return a list of the parents paths path treated as strings, must be absolute path

Source code in paasify/common.py
def list_parent_dirs(path):
    """
    Return a list of the parents paths
    path treated as strings, must be absolute path
    """
    result = [path]
    val = path
    while val and val != os.sep:
        val = os.path.split(val)[0]
        result.append(val)
    return result

merge_env_vars(obj)

Transform all keys of a dict starting by _ to their equivalent wihtout _

Source code in paasify/common.py
def merge_env_vars(obj):
    "Transform all keys of a dict starting by _ to their equivalent wihtout _"

    override_keys = [key.lstrip("_") for key in obj.keys() if key.startswith("_")]
    for key in override_keys:
        old_key = "_" + key
        obj[key] = obj[old_key]
        obj.pop(old_key)

    return obj, override_keys

to_bool(string)

Return a boolean

Source code in paasify/common.py
def to_bool(string):
    "Return a boolean"
    if isinstance(string, bool):
        return string
    return string.lower() in ["true", "1", "t", "y", "yes"]

uniq(seq)

Remove duplicate duplicates items in a list while preserving order

Source code in paasify/common.py
def uniq(seq):
    """Remove duplicate duplicates items in a list while preserving order"""
    return list(dict.fromkeys(seq))

update_dict(dict1, dict2, strict=False)

Update dict1 keys with null value from dict2

Source code in paasify/common.py
def update_dict(dict1, dict2, strict=False):
    """Update dict1 keys with null value from dict2"""
    result = dict1.copy()
    for key, new_value in dict2.items():
        value = result.get(key)
        if not strict and not value:
            result[key] = new_value
        elif strict and value is None:
            result[key] = new_value

    return result

paasify.framework

Paasify Framework Libary

FileLookup

Bases: PaasifyObj

A FileLookup Object

Useful for identifing available files in differents hierachies

Source code in paasify/framework.py
class FileLookup(PaasifyObj):
    """A FileLookup Object

    Useful for identifing available files in differents hierachies"""

    def __init__(self, path=None, pattern=None, **kwargs):
        self._lookups = []
        self.log = logging.getLogger("paasify.cli.FileLookup")

        if path and pattern:
            self.insert(path, pattern, **kwargs)

    def _parse(self, path, pattern, **kwargs):
        "Ensure lookup is correctly formed"
        data = dict(kwargs)
        data.update(
            {
                "path": path,
                "pattern": pattern if isinstance(pattern, list) else [pattern],
            }
        )
        return data

    def insert(self, path, pattern, **kwargs):
        "Insert first a path/pattern to the lookup object"
        data = self._parse(path, pattern, **kwargs)
        self._lookups.insert(0, data)

    def append(self, path, pattern, **kwargs):
        "Append a path/pattern to the lookup object"
        data = self._parse(path, pattern, **kwargs)
        self._lookups.append(data)

    def get_lookups(self):
        "Return object lookups"
        return self._lookups

    def lookup_candidates(self):
        "List all available candidates of files for given folders, low level"

        result = []
        for lookup in self._lookups:
            path = lookup["path"]
            cand = filter_existing_files(path, lookup["pattern"])
            lookup["matches"] = cand
            result.append(lookup)
        return result

    def paths(self, first=False):
        "All matched files"

        vars_cand = self.lookup_candidates()
        ret = []
        for cand in vars_cand:
            for match in cand["matches"]:
                ret.append(match)

        if first:
            return _first(ret) if len(ret) > 0 else None
        return ret

    def match(self, fail_on_missing=False, first=False):
        "Match all candidates, and built a list result with object inside"

        vars_cand = self.lookup_candidates()
        result = []
        missing = []
        for cand in vars_cand:
            matches = cand["matches"]
            for match in matches:
                payload = dict(cand)
                payload.update({"match": match})
                result.append(payload)
            if len(matches) == 0:
                # Still add unlucky candidates
                missing.append(cand)

        # Report errors if missing
        if fail_on_missing and len(missing) > 0:
            missing_paths = [
                os.path.join(lookup["path"], _first(lookup["pattern"]))
                for lookup in missing
            ]
            for missed in missing_paths:
                self.log.info(f"Missing file: {missed}")
            missed_str = ",".join(missing_paths)
            raise error.MissingFile(
                f"Can't load {len(missing_paths)} vars files: {missed_str}"
            )

        if first:
            return _first(result) if len(result) > 0 else None

        return result
append(path, pattern, **kwargs)

Append a path/pattern to the lookup object

Source code in paasify/framework.py
def append(self, path, pattern, **kwargs):
    "Append a path/pattern to the lookup object"
    data = self._parse(path, pattern, **kwargs)
    self._lookups.append(data)
get_lookups()

Return object lookups

Source code in paasify/framework.py
def get_lookups(self):
    "Return object lookups"
    return self._lookups
insert(path, pattern, **kwargs)

Insert first a path/pattern to the lookup object

Source code in paasify/framework.py
def insert(self, path, pattern, **kwargs):
    "Insert first a path/pattern to the lookup object"
    data = self._parse(path, pattern, **kwargs)
    self._lookups.insert(0, data)
lookup_candidates()

List all available candidates of files for given folders, low level

Source code in paasify/framework.py
def lookup_candidates(self):
    "List all available candidates of files for given folders, low level"

    result = []
    for lookup in self._lookups:
        path = lookup["path"]
        cand = filter_existing_files(path, lookup["pattern"])
        lookup["matches"] = cand
        result.append(lookup)
    return result
match(fail_on_missing=False, first=False)

Match all candidates, and built a list result with object inside

Source code in paasify/framework.py
def match(self, fail_on_missing=False, first=False):
    "Match all candidates, and built a list result with object inside"

    vars_cand = self.lookup_candidates()
    result = []
    missing = []
    for cand in vars_cand:
        matches = cand["matches"]
        for match in matches:
            payload = dict(cand)
            payload.update({"match": match})
            result.append(payload)
        if len(matches) == 0:
            # Still add unlucky candidates
            missing.append(cand)

    # Report errors if missing
    if fail_on_missing and len(missing) > 0:
        missing_paths = [
            os.path.join(lookup["path"], _first(lookup["pattern"]))
            for lookup in missing
        ]
        for missed in missing_paths:
            self.log.info(f"Missing file: {missed}")
        missed_str = ",".join(missing_paths)
        raise error.MissingFile(
            f"Can't load {len(missing_paths)} vars files: {missed_str}"
        )

    if first:
        return _first(result) if len(result) > 0 else None

    return result
paths(first=False)

All matched files

Source code in paasify/framework.py
def paths(self, first=False):
    "All matched files"

    vars_cand = self.lookup_candidates()
    ret = []
    for cand in vars_cand:
        for match in cand["matches"]:
            ret.append(match)

    if first:
        return _first(ret) if len(ret) > 0 else None
    return ret

FileReference

A FileReference Object

Useful for managing project paths

The path, once created is immutable, you choose how it behave one time and done forever. They act a immutable local variable.

path: The path you want to manage root: The root of your project, CWD else

Returned path will be returned as absolute

True: Return default path from origin abs/rel False: Return default path from root_path abs/rel

Source code in paasify/framework.py
class FileReference:
    """A FileReference Object

    Useful for managing project paths

    The path, once created is immutable, you choose how it behave one time
    and done forever. They act a immutable local variable.


    path: The path you want to manage
    root: The root of your project, CWD else
    keep: Returned path will be returned as absolute
        True: Return default path from origin abs/rel
        False: Return default path from root_path abs/rel

    """

    def __init__(self, path, root=None, keep=False):

        assert isinstance(path, str), f"Got: {type(path)}"
        root = root or os.getcwd()
        self.raw = path
        self.root = root
        self.keep = keep

    def __str__(self):
        return self.path()

    def is_abs(self):
        "Return true if the path is absolute"
        return os.path.isabs(self.raw)

    def is_root_abs(self):
        "Return true if the root path is absolute"
        return os.path.isabs(self.root)

    def path(self, start=None):
        "Return the absolute or relative path from root depending if root is absolute or not"

        if self.keep:
            if self.is_abs():
                return self.path_abs(start=start)
            return self.path_rel(start=start)
        else:
            if self.is_root_abs():
                return self.path_abs(start=start)
            return self.path_rel(start=start)

    def path_abs(self, start=None):
        "Return the absolute path from root"

        if self.is_abs():
            result = self.raw
        else:
            start = start or self.root
            real_path = os.path.join(start, self.raw)
            result = os.path.abspath(real_path) or "."
        return result

    def path_rel(self, start=None):
        "Return the relative path from root"

        if self.is_abs():
            start = start or self.root
            result = os.path.relpath(self.raw, start=start)
        else:
            start = start or self.root
            real_path = os.path.join(start, self.raw)
            result = os.path.relpath(real_path) or "."
        return result
is_abs()

Return true if the path is absolute

Source code in paasify/framework.py
def is_abs(self):
    "Return true if the path is absolute"
    return os.path.isabs(self.raw)
is_root_abs()

Return true if the root path is absolute

Source code in paasify/framework.py
def is_root_abs(self):
    "Return true if the root path is absolute"
    return os.path.isabs(self.root)
path(start=None)

Return the absolute or relative path from root depending if root is absolute or not

Source code in paasify/framework.py
def path(self, start=None):
    "Return the absolute or relative path from root depending if root is absolute or not"

    if self.keep:
        if self.is_abs():
            return self.path_abs(start=start)
        return self.path_rel(start=start)
    else:
        if self.is_root_abs():
            return self.path_abs(start=start)
        return self.path_rel(start=start)
path_abs(start=None)

Return the absolute path from root

Source code in paasify/framework.py
def path_abs(self, start=None):
    "Return the absolute path from root"

    if self.is_abs():
        result = self.raw
    else:
        start = start or self.root
        real_path = os.path.join(start, self.raw)
        result = os.path.abspath(real_path) or "."
    return result
path_rel(start=None)

Return the relative path from root

Source code in paasify/framework.py
def path_rel(self, start=None):
    "Return the relative path from root"

    if self.is_abs():
        start = start or self.root
        result = os.path.relpath(self.raw, start=start)
    else:
        start = start or self.root
        real_path = os.path.join(start, self.raw)
        result = os.path.relpath(real_path) or "."
    return result

PaasifyConfigVar

Bases: NodeMap, PaasifyObj

Simple Paaisfy Configuration Var Dict

Source code in paasify/framework.py
class PaasifyConfigVar(NodeMap, PaasifyObj):
    "Simple Paaisfy Configuration Var Dict"

    conf_ident = "{self.name}={self.value}"
    conf_default = {
        "name": None,
        "value": None,
    }

    # conf_logger = "paasify.cli.ConfigVar"

    conf_schema = {
        "title": "Variable definition as key/value",
        "description": "Simple key value variable declaration, under the form of: {KEY: VALUE}. This does preserve value type.",
        "type": "object",
        "propertyNames": {"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"},
        "minProperties": 1,
        "maxProperties": 1,
        # "additionalProperties": False,
        "patternProperties": {
            "^[A-Za-z_][A-Za-z0-9_]*$": {
                # ".*": {
                "title": "Environment Key value",
                "description": "Value must be serializable type",
                # "oneOf": [
                #    {"title": "As string", "type": "string"},
                #    {"title": "As boolean", "type": "boolean"},
                #    {"title": "As integer", "type": "integer"},
                #    {
                #        "title": "As null",
                #        "description": "If set to null, this will remove variable",
                #        "type": "null",
                #    },
                # ],
            }
        },
    }

    def node_hook_transform(self, payload):

        result = None
        if isinstance(payload, str):
            value = payload.split("=", 2)
            result = {
                "name": value[0],
                "value": value[1],
            }
        if isinstance(payload, dict):
            if "name" in payload and "value" in payload and len(payload.keys()) == 2:
                result = {
                    "name": payload["name"],
                    "value": payload["value"],
                }

            elif len(payload.keys()) == 1:
                for key, value in payload.items():
                    result = {
                        "name": key,
                        "value": value,
                    }

        if result is None:
            raise Exception(f"Unsupported type {type(payload)}: {payload}")

        return result

PaasifyConfigVars

Bases: NodeList, PaasifyObj

Paasify Project configuration object

Source code in paasify/framework.py
class PaasifyConfigVars(NodeList, PaasifyObj):
    "Paasify Project configuration object"

    conf_children = PaasifyConfigVar

    conf_schema = {
        # "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "Environment configuration",
        "description": (
            "Environment configuration. Paasify leave two choices for the "
            "configuration, either use the native dict configuration or use the "
            "docker-compatible format"
        ),
        "oneOf": [
            {
                "title": "Env configuration as list",
                "description": (
                    "Configure variables as a list. This is the recommended way as"
                    "it preserves the variable parsing order, useful for templating. This format "
                    "allow multiple configuration format."
                ),
                "type": "array",
                "default": [],
                "additionalProperties": vardef_schema_complex,
                "examples": [
                    {
                        "env": [
                            # "MYSQL_ADMIN_DB=MyDB",
                            {"MYSQL_ADMIN_USER": "MyUser"},
                            {"MYSQL_ADMIN_DB": "MyDB"},
                            {"MYSQL_ENABLE_BACKUP": True},
                            {"MYSQL_BACKUPS_NODES": 3},
                            {"MYSQL_NODE_REPLICA": None},
                            "MYSQL_WELCOME_STRING=Is alway a string",
                        ],
                    },
                ],
            },
            {
                "title": "Env configuration as dict (Compat)",
                "description": (
                    "Configure variables as a dict. This option is only proposed for "
                    "compatibility reasons. It does not preserve the order of the variables."
                ),
                "type": "object",
                "default": {},
                # "patternProperties": {
                #    #".*": { "properties": PaasifyConfigVar.conf_schema, }
                #    ".*": { "properties": vardef_schema_complex , }
                # },
                "propertyNames": {"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"},
                # "additionalProperties": PaasifyConfigVar.conf_schema,
                "examples": [
                    {
                        "env": {
                            "MYSQL_ADMIN_USER": "MyUser",
                            "MYSQL_ADMIN_DB": "MyDB",
                            "MYSQL_ENABLE_BACKUP": True,
                            "MYSQL_BACKUPS_NODES": 3,
                            "MYSQL_NODE_REPLICA": None,
                        }
                    },
                ],
            },
            {
                "title": "Unset",
                "description": ("Do not define any vars"),
                "type": "null",
                "default": None,
                "examples": [
                    {
                        "env": None,
                    },
                    {
                        "env": [],
                    },
                    {
                        "env": {},
                    },
                ],
            },
        ],
    }

    def node_hook_transform(self, payload):

        result = []
        if not payload:
            pass
        elif isinstance(payload, dict):
            for key, value in payload.items():
                var_def = {key: value}
                result.append(var_def)
        elif isinstance(payload, list):
            result = payload

        # elif isinstance(payload, str):
        #         value = payload.split("=", 2)
        #         result = {
        #             "name": value[0],
        #             "value": value[1],
        #         }
        else:
            raise error.InvalidConfig(f"Unsupported type: {payload}")

        return result

    def get_vars(self, current=None):
        "Parse vars and interpolate strings"

        result = dict(current or {})

        for var in self._nodes:
            value = var.value
            result[var.name] = value
        return result

    def get_vars_list(self, current=None):
        "Parse vars and interpolate strings"

        assert isinstance(current, list) or current is None

        result = list(current or [])
        for var in self._nodes:
            result.append(var)
        return result
get_vars(current=None)

Parse vars and interpolate strings

Source code in paasify/framework.py
def get_vars(self, current=None):
    "Parse vars and interpolate strings"

    result = dict(current or {})

    for var in self._nodes:
        value = var.value
        result[var.name] = value
    return result
get_vars_list(current=None)

Parse vars and interpolate strings

Source code in paasify/framework.py
def get_vars_list(self, current=None):
    "Parse vars and interpolate strings"

    assert isinstance(current, list) or current is None

    result = list(current or [])
    for var in self._nodes:
        result.append(var)
    return result

PaasifyObj

Bases: Base, MixInLog

Default Paasify base object

Source code in paasify/framework.py
class PaasifyObj(Base, MixInLog):
    "Default Paasify base object"

    module = "paasify.cli"
    conf_logger = None
    log = _log

    def __init__(self, *args, **kwargs):

        # Manually load classe
        Base.__init__(self, *args, **kwargs)
        MixInLog.__init__(self, *args, **kwargs)

PaasifySimpleDict

Bases: NodeMap, PaasifyObj

Simple Paaisfy Configuration Dict

Source code in paasify/framework.py
class PaasifySimpleDict(NodeMap, PaasifyObj):
    "Simple Paaisfy Configuration Dict"

    conf_default = {}

PaasifySource

Bases: NodeDict, PaasifyObj

Paasify source configuration

Source code in paasify/framework.py
class PaasifySource(NodeDict, PaasifyObj):
    "Paasify source configuration"

    def install(self, update=False):
        "Install a source if not updated"

        # Check if the source if installed or install latest

        prj = self.get_parent().get_parent()
        coll_dir = prj.runtime.project_collection_dir
        src_dir = os.path.join(coll_dir, self.ident)
        git_dir = os.path.join(src_dir, ".git")

        if os.path.isdir(git_dir) and not update:
            self.log.debug(
                f"Collection '{self.ident}' is already installed in: {git_dir}"
            )
            return

        self.log.info(f"Install source '{self.ident}' in: {git_dir}")
        raise NotImplementedError
install(update=False)

Install a source if not updated

Source code in paasify/framework.py
def install(self, update=False):
    "Install a source if not updated"

    # Check if the source if installed or install latest

    prj = self.get_parent().get_parent()
    coll_dir = prj.runtime.project_collection_dir
    src_dir = os.path.join(coll_dir, self.ident)
    git_dir = os.path.join(src_dir, ".git")

    if os.path.isdir(git_dir) and not update:
        self.log.debug(
            f"Collection '{self.ident}' is already installed in: {git_dir}"
        )
        return

    self.log.info(f"Install source '{self.ident}' in: {git_dir}")
    raise NotImplementedError

PaasifySources

Bases: NodeDict, PaasifyObj

Sources manager

Source code in paasify/framework.py
class PaasifySources(NodeDict, PaasifyObj):
    "Sources manager"
    conf_children = PaasifySource

paasify.errors

Paasify errors

BuildStackFirstError

Bases: PaasifyError

Raised when a trying to interact with stack but docker-compose.yml is missing

Source code in paasify/errors.py
class BuildStackFirstError(PaasifyError):
    """Raised when a trying to interact with stack but docker-compose.yml is missing"""

    rc = 39

ConfigBackendError

Bases: PaasifyError

Raised when could not work with cafram

Source code in paasify/errors.py
class ConfigBackendError(PaasifyError):
    """Raised when could not work with cafram"""

    rc = 45

DockerBuildConfig

Bases: PaasifyError

Raised when docker-config failed

Source code in paasify/errors.py
class DockerBuildConfig(PaasifyError):
    "Raised when docker-config failed"
    rc = 30

DockerCommandFailed

Bases: PaasifyError

Raised when docker-config failed

Source code in paasify/errors.py
class DockerCommandFailed(PaasifyError):
    "Raised when docker-config failed"
    rc = 32

DockerUnsupportedVersion

Bases: PaasifyError

Raised when docker-config failed

Source code in paasify/errors.py
class DockerUnsupportedVersion(PaasifyError):
    "Raised when docker-config failed"
    rc = 33

DuplicateSourceName

Bases: PaasifyError

Raised when two sources have the same name

Source code in paasify/errors.py
class DuplicateSourceName(PaasifyError):
    """Raised when two sources have the same name"""

    rc = 49

InvalidConfig

Bases: PaasifyError

Raised when invalid syntax for config

Source code in paasify/errors.py
class InvalidConfig(PaasifyError):
    "Raised when invalid syntax for config"
    rc = 36

InvalidSourceConfig

Bases: PaasifyError

Raised when a source is not configured properly

Source code in paasify/errors.py
class InvalidSourceConfig(PaasifyError):
    """Raised when a source is not configured properly"""

    rc = 46

JsonnetBuildFailed

Bases: PaasifyError

Raised when jsonnet failed

Source code in paasify/errors.py
class JsonnetBuildFailed(PaasifyError):
    "Raised when jsonnet failed"
    rc = 31

JsonnetProcessError

Bases: PaasifyError

Raised when jsonnet file can't be executed

Source code in paasify/errors.py
class JsonnetProcessError(PaasifyError):
    "Raised when jsonnet file can't be executed"
    rc = 34

MissingApp

Bases: PaasifyError

Raised when referencing unknown app

Source code in paasify/errors.py
class MissingApp(PaasifyError):
    """Raised when referencing unknown app"""

    rc = 44

MissingFile

Bases: PaasifyError

Raised when referencing unexisting file

Source code in paasify/errors.py
class MissingFile(PaasifyError):
    """Raised when referencing unexisting file"""

    rc = 48

MissingTag

Bases: PaasifyError

Raised when referencing unexistant tag

Source code in paasify/errors.py
class MissingTag(PaasifyError):
    """Raised when referencing unexistant tag"""

    rc = 43

OnlyOneStackAllowed

Bases: PaasifyError

Raised when trying to apply command one more than one stack

Source code in paasify/errors.py
class OnlyOneStackAllowed(PaasifyError):
    """Raised when trying to apply command one more than one stack"""

    rc = 41

PaasifyError

Bases: Exception

Base class for other exceptions

Source code in paasify/errors.py
class PaasifyError(Exception):
    """Base class for other exceptions"""

    paasify = True
    rc = 1

    def __init__(self, message, rc=None, advice=None):
        # self.paasify = True
        self.advice = advice

        # pylint: disable=invalid-name
        self.rc = rc or self.rc
        super().__init__(message)

PaasifyNestedProject

Bases: PaasifyError

Raised when a project is created into an existing project

Source code in paasify/errors.py
class PaasifyNestedProject(PaasifyError):
    "Raised when a project is created into an existing project"
    rc = 35

ProjectInvalidConfig

Bases: PaasifyError

Raised when project config contains errors

Source code in paasify/errors.py
class ProjectInvalidConfig(PaasifyError):
    """Raised when project config contains errors"""

    rc = 18

ProjectNotFound

Bases: PaasifyError

Raised when project is not found

Source code in paasify/errors.py
class ProjectNotFound(PaasifyError):
    """Raised when project is not found"""

    rc = 17

ShellCommandFailed

Bases: PaasifyError

Raised when project config contains errors

Source code in paasify/errors.py
class ShellCommandFailed(PaasifyError):
    """Raised when project config contains errors"""

    rc = 18

StackMissingDockerComposeFile

Bases: PaasifyError

Raised when a stack can't find a docker-compose.yml

Source code in paasify/errors.py
class StackMissingDockerComposeFile(PaasifyError):
    """Raised when a stack can't find a docker-compose.yml"""

    rc = 38

StackMissingOrigin

Bases: PaasifyError

Raised when a stack origin is not determined

Source code in paasify/errors.py
class StackMissingOrigin(PaasifyError):
    """Raised when a stack origin is not determined"""

    rc = 20

StackNotFound

Bases: PaasifyError

Raised when stack is not found

Source code in paasify/errors.py
class StackNotFound(PaasifyError):
    """Raised when stack is not found"""

    rc = 19

UndeclaredVariable

Bases: PaasifyError

Raised when a a variable is called but not defined

Source code in paasify/errors.py
class UndeclaredVariable(PaasifyError):
    """Raised when a a variable is called but not defined"""

    rc = 47

YAMLError

Bases: PaasifyError

Raised when having issues with YAML file

Source code in paasify/errors.py
class YAMLError(PaasifyError):
    """Raised when having issues with YAML file"""

    rc = 42