Skip to content

ContainerRecipe

A dict-like representation of container's recipe

The recipe file of default container '.main' will be '~/rezup.toml'

And for other container, it will be ~/rezup.{name}.toml for example: * ~/rezup.dev.toml -> container dev * ~/rezup.test.toml -> container test

Parameters:

Name Type Description Default
name str

Container name

None
Source code in rezup/recipe.py
class ContainerRecipe(BaseRecipe):
    """A dict-like representation of container's recipe

    The recipe file of default container '.main' will be '~/rezup.toml'

    And for other container, it will be `~/rezup.{name}.toml`
    for example:
        * `~/rezup.dev.toml`  -> container `dev`
        * `~/rezup.test.toml` -> container `test`

    Args:
        name (str, optional): Container name

    """
    REGEX = re.compile("rezup.?(.*).toml")
    RECIPES_DIR = DEFAULT_CONTAINER_RECIPES  #: `DEFAULT_CONTAINER_RECIPES`

    def __init__(self, name=None):
        super(ContainerRecipe, self).__init__(name)
        _name = self._name
        if _name and _name != DEFAULT_CONTAINER_NAME:
            self._file = "rezup.%s.toml" % _name
        else:
            self._file = "rezup.toml"

    @classmethod
    @contextmanager
    def provisional_recipes(cls, path):
        """Context for changing recipes root temporarily

        Container recipes should be in user's home directory by defaule, but
        that could be changed inside this context for the case when you need
        to operate on machines that have no recipe exists in home directory.

        ```
        with ContainerRecipe.provisional_recipes("/to/other/recipes"):
            ...
        ```

        Args:
            path (str or path-like): directory path where recipes located
        """
        default = cls.RECIPES_DIR
        try:
            cls.RECIPES_DIR = Path(path)
            yield
        finally:
            cls.RECIPES_DIR = default

    @classmethod
    def iter_recipes(cls):
        """Iter all recipe files found in `ContainerRecipe.RECIPES_DIR`
        Yields:
            `ContainerRecipe`
        """
        for item in cls.RECIPES_DIR.iterdir():
            if not item.is_file():
                continue
            match = cls.REGEX.search(item.name)
            if match:
                name = match.group(1)
                yield cls(name)

    def path(self):
        """Returns the file path of this recipe
        Returns:
            pathlib.Path: The file path of this recipe
        """
        if self._path is None:
            self._path = self.RECIPES_DIR / self._file
        return self._path

    def create(self, data=None):
        """Write out recipe content into a .toml file
        Args:
            data (dict, optional): Arbitrary data to write out
        """
        if not self.RECIPES_DIR.is_dir():
            self.RECIPES_DIR.mkdir(parents=True)
        path = self.path()

        if data:
            _data = toml.load(str(self.DEFAULT_RECIPE))
            deep_update(_data, data)
            with open(str(path), "w") as f:
                toml.dump(_data, f)
        else:
            # read & write as plaintext, so the comment can be preserved
            with open(str(self.DEFAULT_RECIPE), "r") as r:
                with open(str(path), "w") as w:
                    w.write(r.read())
        self._load()

create(self, data=None)

Write out recipe content into a .toml file

Parameters:

Name Type Description Default
data dict

Arbitrary data to write out

None
Source code in rezup/recipe.py
def create(self, data=None):
    """Write out recipe content into a .toml file
    Args:
        data (dict, optional): Arbitrary data to write out
    """
    if not self.RECIPES_DIR.is_dir():
        self.RECIPES_DIR.mkdir(parents=True)
    path = self.path()

    if data:
        _data = toml.load(str(self.DEFAULT_RECIPE))
        deep_update(_data, data)
        with open(str(path), "w") as f:
            toml.dump(_data, f)
    else:
        # read & write as plaintext, so the comment can be preserved
        with open(str(self.DEFAULT_RECIPE), "r") as r:
            with open(str(path), "w") as w:
                w.write(r.read())
    self._load()

iter_recipes() classmethod

Iter all recipe files found in ContainerRecipe.RECIPES_DIR

Yields:

Type Description

ContainerRecipe

Source code in rezup/recipe.py
@classmethod
def iter_recipes(cls):
    """Iter all recipe files found in `ContainerRecipe.RECIPES_DIR`
    Yields:
        `ContainerRecipe`
    """
    for item in cls.RECIPES_DIR.iterdir():
        if not item.is_file():
            continue
        match = cls.REGEX.search(item.name)
        if match:
            name = match.group(1)
            yield cls(name)

path(self)

Returns the file path of this recipe

Returns:

Type Description
pathlib.Path

The file path of this recipe

Source code in rezup/recipe.py
def path(self):
    """Returns the file path of this recipe
    Returns:
        pathlib.Path: The file path of this recipe
    """
    if self._path is None:
        self._path = self.RECIPES_DIR / self._file
    return self._path

provisional_recipes(cls, path) classmethod

Context for changing recipes root temporarily

Container recipes should be in user's home directory by defaule, but that could be changed inside this context for the case when you need to operate on machines that have no recipe exists in home directory.

with ContainerRecipe.provisional_recipes("/to/other/recipes"):
    ...

Parameters:

Name Type Description Default
path str or path-like

directory path where recipes located

required
Source code in rezup/recipe.py
@classmethod
@contextmanager
def provisional_recipes(cls, path):
    """Context for changing recipes root temporarily

    Container recipes should be in user's home directory by defaule, but
    that could be changed inside this context for the case when you need
    to operate on machines that have no recipe exists in home directory.

    ```
    with ContainerRecipe.provisional_recipes("/to/other/recipes"):
        ...
    ```

    Args:
        path (str or path-like): directory path where recipes located
    """
    default = cls.RECIPES_DIR
    try:
        cls.RECIPES_DIR = Path(path)
        yield
    finally:
        cls.RECIPES_DIR = default

get_container_root

Internal use

Get container root path from recipe

# rezup.foo.toml
[root]
local = ""
remote = "/path/to/remote/containers"

If the recipe represents the above example recipe file, container named foo will have local root set to rezup.recipe.DEFAULT_CONTAINER_RECIPES, unless environ variable REZUP_ROOT_LOCAL has value. And the remote root will set to /path/to/remote/containers.

Parameters:

Name Type Description Default
recipe `ContainerRecipe`

The container recipe to lookup from.

required
remote bool

Fetch remote root if True, otherwise local.

False

Returns:

Type Description
`pathlib.Path`

Container root path, local or remote one.

Source code in rezup/container.py
def get_container_root(recipe, remote=False):
    """Get container root path from recipe

    ```toml
    # rezup.foo.toml
    [root]
    local = ""
    remote = "/path/to/remote/containers"

    ```

    If the `recipe` represents the above example recipe file, container named
    `foo` will have local root set to `rezup.recipe.DEFAULT_CONTAINER_RECIPES`,
    unless environ variable `REZUP_ROOT_LOCAL` has value. And the remote root
    will set to `/path/to/remote/containers`.

    Args:
        recipe (`ContainerRecipe`): The container recipe to lookup from.
        remote (bool): Fetch remote root if True, otherwise local.

    Returns:
        `pathlib.Path`: Container root path, local or remote one.

    """
    data = recipe.data()
    if remote:
        remote = \
            data["root"]["remote"] \
            or os.getenv("REZUP_ROOT_REMOTE")

        return Path(norm_path(remote)) if remote else None

    else:
        local = \
            data["root"]["local"] \
            or os.getenv("REZUP_ROOT_LOCAL") \
            or "~/.rezup"

        return Path(norm_path(local))

Container

Timestamp ordered virtual environment stack

A Rez venv provider, that allows updating venv continuously without affecting any existing consumer.

In filesystem, a container is a folder that has at least one Rez venv installation exists, and those Rez venv folders (Revision) are named by timestamp, so when the container is being asked for a venv to use, the current latest available one will be sorted out.

The location of the container can be set with env var REZUP_ROOT_LOCAL or ~/.rezup will be used by default.

For centralize management in production, one remote container can be defined with an env var REZUP_ROOT_REMOTE. The remote container only contains the venv installation manifest file, and when being asked, a venv will be created locally or re-used if same revision exists in local.

Parameters:

Name Type Description Default
name `str`

Container name, use Container.DEFAULT_NAME if not given.

None
recipe `ContainerRecipe`

A ContainerRecipe object to help constructing container, will look into ContainerRecipe.RECIPES_DIR if not given.

None
force_local `bool`

Default False. Ignore linking remote even if the remote path has set, always link to local if True.

False
Source code in rezup/container.py
class Container:
    """Timestamp ordered virtual environment stack

    A Rez venv provider, that allows updating venv continuously without
    affecting any existing consumer.

    In filesystem, a container is a folder that has at least one Rez venv
    installation exists, and those Rez venv folders (`Revision`) are named
    by timestamp, so when the container is being asked for a venv to use,
    the current latest available one will be sorted out.

    The location of the container can be set with env var `REZUP_ROOT_LOCAL`
    or `~/.rezup` will be used by default.

    For centralize management in production, one remote container can be
    defined with an env var `REZUP_ROOT_REMOTE`. The remote container only
    contains the venv installation manifest file, and when being asked, a
    venv will be created locally or re-used if same revision exists in local.

    Args:
        name (`str`, optional): Container name, use `Container.DEFAULT_NAME`
            if not given.
        recipe (`ContainerRecipe`, optional): A `ContainerRecipe` object to
            help constructing container, will look into `ContainerRecipe.RECIPES_DIR`
            if not given.
        force_local (`bool`, optional): Default `False`. Ignore linking remote
            even if the remote path has set, always link to local if `True`.

    """
    DEFAULT_NAME = DEFAULT_CONTAINER_NAME  #: `rezup.recipe.DEFAULT_CONTAINER_NAME`

    def __init__(self, name=None, recipe=None, force_local=False):
        name = name or self.DEFAULT_NAME
        recipe = recipe or ContainerRecipe(name)

        local_root = get_container_root(recipe, remote=False)
        remote_root = get_container_root(recipe, remote=True)
        if force_local:
            root = local_root
        else:
            root = remote_root or local_root

        self._remote = root == remote_root
        self._recipe = recipe
        self._name = name
        self._root = root
        self._path = root / name

    def __repr__(self):
        return "%s(remote=%d, name=%r, path=%r)" % (
            self.__class__.__name__,
            int(self.is_remote()),
            self.name(),
            self._path.resolve(),
        )

    @classmethod
    def create(cls, name, force_local=False):
        """Create container from recipe.

        Args:
            name (str): The name of container.
            force_local (bool): Create as local if True, or try remote.

        Returns:
            `Container`: A container instance.
        """
        recipe = ContainerRecipe(name)
        _log.debug("Sourcing recipe from: %s" % recipe)
        if not recipe.is_file():
            _log.debug("Recipe file not exists, creating default..")
            recipe.create()
        return Container(name, recipe, force_local)

    def root(self):
        """
        Returns:
            `pathlib.Path`: Path to where this container is located.
        """
        return self._root

    def name(self):
        """
        Returns:
            str: Container name.
        """
        return self._name

    def path(self):
        """
        Returns:
            `pathlib.Path`: Full path of this container.
        """
        return self._path

    def recipe(self):
        """
        Returns:
            `ContainerRecipe`: The recipe instance that binds to this container.
        """
        return self._recipe

    def libs(self):
        """
        Returns:
            `pathlib.Path`: Path to this container's shared libraries.
        """
        return self._path / "libs"

    def revisions(self):
        """
        Returns:
            `pathlib.Path`: Path to this container's revisions.
        """
        return self._path / "revisions"

    def is_exists(self):
        """
        Returns:
            bool: Return `True` if this container exists.
        """
        return self._path.is_dir()

    def is_empty(self):
        """
        Returns:
            bool: Return `True` if this container has no valid revisions.
        """
        return not bool(next(self.iter_revision(), None))

    def is_remote(self):
        """
        Returns:
            bool: Return `True` if this container is linked to remote side.
        """
        return self._remote

    def purge(self):
        if self.is_exists():
            revision = self.get_latest_revision(only_ready=False)
            if not revision:
                # TODO: don't remove it immediately, mark as purged and
                #   remove it when $REZUP_CLEAN_AFTER meet
                rmtree(self._path)
            else:
                # TODO: should have better exception type
                # TODO: need to check revision is in use
                raise Exception("Revision is creating.")

        # keep tidy, try remove the root of containers if it's now empty
        root = self.root()
        if root.is_dir() and not next(root.iterdir(), None):
            rmtree(root)

    def iter_revision(self, validate=True, latest_first=True):
        """Iterating revisions from this container.

        Args:
            validate (bool, optional): Default `True`. Only yield revisions
                that are valid if `True`.
            latest_first (bool): Default `True`. Yield revisions based on
                it's directory name (timestamp string) in descending order,
                or ascending if `False`.

        Yields:
            Revision: `Revision` instances that match the condition.

        """
        _log.debug("Iterating revisions in container %s.." % self)

        if not self.is_exists():
            _log.debug("Container %r not exists." % self.name())
            return

        revisions_root = Revision.compose_path(container=self)
        if not revisions_root.is_dir():
            _log.debug("Revision root is not a directory: %s" % revisions_root)
            return

        revisions_root = str(revisions_root)
        for entry in sorted(os.listdir(revisions_root), reverse=latest_first):
            revision = Revision(container=self, dirname=entry)
            _log.debug("... %s" % revision)
            if not validate or revision.is_valid():
                yield revision

    def get_latest_revision(self, only_ready=True):
        """Get latest revision from this container.

        Args:
            only_ready (bool): Default `True`. Include revisions that are not
                in ready state if `False`.

        Returns:
            Revision: An instance of `Revision` if found, or `None`.

        """
        for revision in self.iter_revision():
            if not only_ready or revision.is_ready():
                _log.debug("Found latest revision.")
                return revision

    def get_revision_by_time(self, timestamp, fallback=False, only_ready=True):
        """Returns a revision that match the timestamp

        Args:
            timestamp (datetime.datetime): a time for matching revision
            fallback (bool): If True, accept earlier revision when no exact matched
            only_ready (bool): Include revisions that are not in ready state if False

        Returns:
            Revision: An instance of `Revision` if found, or `None`.

        """
        for revision in self.iter_revision():
            if not only_ready or revision.is_ready():
                if (revision.timestamp() == timestamp
                        or (fallback and revision.timestamp() <= timestamp)):

                    _log.debug("Found time matched revision.")
                    return revision

        _log.debug("No time matched revision found.")

    def new_revision(self):
        """Create a new revision

        Returns:
            Revision: An instance of `Revision` that just created.

        """
        return Revision.create(self)

create(name, force_local=False) classmethod

Create container from recipe.

Parameters:

Name Type Description Default
name str

The name of container.

required
force_local bool

Create as local if True, or try remote.

False

Returns:

Type Description
`Container`

A container instance.

Source code in rezup/container.py
@classmethod
def create(cls, name, force_local=False):
    """Create container from recipe.

    Args:
        name (str): The name of container.
        force_local (bool): Create as local if True, or try remote.

    Returns:
        `Container`: A container instance.
    """
    recipe = ContainerRecipe(name)
    _log.debug("Sourcing recipe from: %s" % recipe)
    if not recipe.is_file():
        _log.debug("Recipe file not exists, creating default..")
        recipe.create()
    return Container(name, recipe, force_local)

get_latest_revision(self, only_ready=True)

Get latest revision from this container.

Parameters:

Name Type Description Default
only_ready bool

Default True. Include revisions that are not in ready state if False.

True

Returns:

Type Description
Revision

An instance of Revision if found, or None.

Source code in rezup/container.py
def get_latest_revision(self, only_ready=True):
    """Get latest revision from this container.

    Args:
        only_ready (bool): Default `True`. Include revisions that are not
            in ready state if `False`.

    Returns:
        Revision: An instance of `Revision` if found, or `None`.

    """
    for revision in self.iter_revision():
        if not only_ready or revision.is_ready():
            _log.debug("Found latest revision.")
            return revision

get_revision_by_time(self, timestamp, fallback=False, only_ready=True)

Returns a revision that match the timestamp

Parameters:

Name Type Description Default
timestamp datetime.datetime

a time for matching revision

required
fallback bool

If True, accept earlier revision when no exact matched

False
only_ready bool

Include revisions that are not in ready state if False

True

Returns:

Type Description
Revision

An instance of Revision if found, or None.

Source code in rezup/container.py
def get_revision_by_time(self, timestamp, fallback=False, only_ready=True):
    """Returns a revision that match the timestamp

    Args:
        timestamp (datetime.datetime): a time for matching revision
        fallback (bool): If True, accept earlier revision when no exact matched
        only_ready (bool): Include revisions that are not in ready state if False

    Returns:
        Revision: An instance of `Revision` if found, or `None`.

    """
    for revision in self.iter_revision():
        if not only_ready or revision.is_ready():
            if (revision.timestamp() == timestamp
                    or (fallback and revision.timestamp() <= timestamp)):

                _log.debug("Found time matched revision.")
                return revision

    _log.debug("No time matched revision found.")

is_empty(self)

Returns:

Type Description
bool

Return True if this container has no valid revisions.

Source code in rezup/container.py
def is_empty(self):
    """
    Returns:
        bool: Return `True` if this container has no valid revisions.
    """
    return not bool(next(self.iter_revision(), None))

is_exists(self)

Returns:

Type Description
bool

Return True if this container exists.

Source code in rezup/container.py
def is_exists(self):
    """
    Returns:
        bool: Return `True` if this container exists.
    """
    return self._path.is_dir()

is_remote(self)

Returns:

Type Description
bool

Return True if this container is linked to remote side.

Source code in rezup/container.py
def is_remote(self):
    """
    Returns:
        bool: Return `True` if this container is linked to remote side.
    """
    return self._remote

iter_revision(self, validate=True, latest_first=True)

Iterating revisions from this container.

Parameters:

Name Type Description Default
validate bool

Default True. Only yield revisions that are valid if True.

True
latest_first bool

Default True. Yield revisions based on it's directory name (timestamp string) in descending order, or ascending if False.

True

Yields:

Type Description
Revision

Revision instances that match the condition.

Source code in rezup/container.py
def iter_revision(self, validate=True, latest_first=True):
    """Iterating revisions from this container.

    Args:
        validate (bool, optional): Default `True`. Only yield revisions
            that are valid if `True`.
        latest_first (bool): Default `True`. Yield revisions based on
            it's directory name (timestamp string) in descending order,
            or ascending if `False`.

    Yields:
        Revision: `Revision` instances that match the condition.

    """
    _log.debug("Iterating revisions in container %s.." % self)

    if not self.is_exists():
        _log.debug("Container %r not exists." % self.name())
        return

    revisions_root = Revision.compose_path(container=self)
    if not revisions_root.is_dir():
        _log.debug("Revision root is not a directory: %s" % revisions_root)
        return

    revisions_root = str(revisions_root)
    for entry in sorted(os.listdir(revisions_root), reverse=latest_first):
        revision = Revision(container=self, dirname=entry)
        _log.debug("... %s" % revision)
        if not validate or revision.is_valid():
            yield revision

libs(self)

Returns:

Type Description
`pathlib.Path`

Path to this container's shared libraries.

Source code in rezup/container.py
def libs(self):
    """
    Returns:
        `pathlib.Path`: Path to this container's shared libraries.
    """
    return self._path / "libs"

name(self)

Returns:

Type Description
str

Container name.

Source code in rezup/container.py
def name(self):
    """
    Returns:
        str: Container name.
    """
    return self._name

new_revision(self)

Create a new revision

Returns:

Type Description
Revision

An instance of Revision that just created.

Source code in rezup/container.py
def new_revision(self):
    """Create a new revision

    Returns:
        Revision: An instance of `Revision` that just created.

    """
    return Revision.create(self)

path(self)

Returns:

Type Description
`pathlib.Path`

Full path of this container.

Source code in rezup/container.py
def path(self):
    """
    Returns:
        `pathlib.Path`: Full path of this container.
    """
    return self._path

recipe(self)

Returns:

Type Description
`ContainerRecipe`

The recipe instance that binds to this container.

Source code in rezup/container.py
def recipe(self):
    """
    Returns:
        `ContainerRecipe`: The recipe instance that binds to this container.
    """
    return self._recipe

revisions(self)

Returns:

Type Description
`pathlib.Path`

Path to this container's revisions.

Source code in rezup/container.py
def revisions(self):
    """
    Returns:
        `pathlib.Path`: Path to this container's revisions.
    """
    return self._path / "revisions"

root(self)

Returns:

Type Description
`pathlib.Path`

Path to where this container is located.

Source code in rezup/container.py
def root(self):
    """
    Returns:
        `pathlib.Path`: Path to where this container is located.
    """
    return self._root

Revision

Parameters:

Name Type Description Default
container Container

The container that holds this revision

required
dirname str

Directory name of this revision, should be a timestamp string if given.

None
Source code in rezup/container.py
class Revision:
    """
    Args:
        container (Container): The container that holds this revision
        dirname (str, optional): Directory name of this revision, should be
            a timestamp string if given.
    """

    def __init__(self, container, dirname=None):
        dirname = str(dirname or time.time())

        self._container = container
        self._dirname = dirname
        self._path = self.compose_path(container, dirname)
        self._timestamp = None
        self._is_valid = None
        self._metadata = None
        self._recipe = RevisionRecipe(self)
        self._metadata_path = self._path / "revision.json"
        self._is_pulled = False

    def __repr__(self):
        return "%s(valid=%d, ready=%d, remote=%d, time=%s, path=%r)" % (
            self.__class__.__name__,
            int(self.is_valid()),
            int(self.is_ready()),
            int(self.is_remote()),
            self.time_str() or "?",
            self._path.resolve(),
        )

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return False
        return self._container.name() == other._container.name() \
            and self.timestamp() == other.timestamp()

    @classmethod
    def compose_path(cls, container, dirname=None):
        path = container.revisions()
        if dirname:
            path /= dirname
        return path

    @classmethod
    def create(cls, container):
        revision = cls(container=container)
        revision._write()
        return revision

    def _write(self, pulling=None):
        """Sourcing recipe and create a revision

        Args:
            pulling (Revision, optional): If given, pulling recipe from that
                revision, usually a remote one.
        """
        _log.info("Creating revision..")

        recipe = self._recipe
        makedirs(self._path)

        # write revision recipe
        if pulling is None:
            _log.info("Recipe sourced from: %s" % self._container.recipe())
            recipe.create()
        else:
            _log.debug("Pulling recipe from: %s" % pulling)
            recipe.pull(pulling)

        if not self.is_valid():
            raise Exception("Invalid new revision, this is a bug.")

        # manifest
        rez_ = Tool(recipe["rez"])
        extensions = [Tool(d) for d in recipe.get("extension", []) if d]
        shared_lib = recipe.get("shared")
        _log.info("Recipe loaded.")
        _log.debug("       Rez: %s" % rez_.url)
        _log.debug(" Extension: " + ", ".join([e.name for e in extensions]))
        _log.debug("Shared-lib: %s" % shared_lib)

        # install, if at local
        if not self._container.is_remote():
            self._install(rez_, extensions, shared_lib)

        # save metadata, mark revision as ready
        with open(str(self._metadata_path), "w") as f:
            # metadata
            f.write(json.dumps({
                "rezup_version": __version__,
                "creator": getpass.getuser(),
                "hostname": socket.gethostname(),
                "revision_path": str(self._path),
                "venvs": ["rez"] + [t.name for t in extensions if t.isolation],
            }, indent=4))

        _log.info("Revision created: %s" % self)

    def _install(self, rez_, extensions=None, shared_lib=None):
        """Construct Rez virtual environment by recipe
        """
        _log.debug("Installing..")

        pip_entry = self._recipe.get("pip")
        extensions = extensions or []
        installer = Installer(
            self,
            pip_opt=pip_entry.get("options"),
            pip_env=pip_entry.get("env"),
        )

        installer.install_rez(rez_)

        if shared_lib:
            warnings.warn("Shared-lib section is about to be deprecated, "
                          "use 'rez.lib' or 'extension.lib' section instead. "
                          "See https://github.com/davidlatwe/rezup/issues/62",
                          DeprecationWarning)
            installer.create_shared_lib(name=shared_lib["name"],
                                        requires=shared_lib["requires"])

        for ext in extensions:
            installer.install_extension(ext)

    def validate(self):
        is_valid = True
        seconds = float(self._dirname)
        timestamp = datetime.fromtimestamp(seconds)
        if self._recipe.is_file():
            self._timestamp = timestamp
        else:
            is_valid = False

        return is_valid

    def is_valid(self):
        if self._is_valid is None:
            try:
                self._is_valid = self.validate()
            except (OSError, ValueError):
                self._is_valid = False

        return self._is_valid

    def is_ready(self):
        return self._metadata_path.is_file()

    def is_remote(self):
        return self._container.is_remote()

    def dirname(self):
        return self._dirname

    def path(self):
        return self._path

    def timestamp(self):
        return self._timestamp

    def time_str(self):
        return self._timestamp.strftime("%d.%b.%y %H:%M") \
            if self._timestamp else ""

    def container(self):
        return self._container

    def metadata(self):
        if self.is_ready():
            if self._metadata is None:
                with open(str(self._metadata_path), "r") as f:
                    self._metadata = json.load(f)
            return self._metadata

    def recipe(self):
        if self.is_valid():
            return self._recipe

    def recipe_env(self):
        _platform = platform.system().lower()
        recipe = self.recipe() or {}
        env = {}

        def load_env(**kwargs):
            return {
                k: v for k, v in dotenv_values(**kwargs).items()
                if v is not None  # exclude config section line
            }

        def file_loader(d):
            return [d[k] for k in sorted([
                k for k, v in d.items() if isinstance(v, string_types)])]

        dot_env = recipe.get("dotenv")
        if dot_env:
            # non platform specific dotenv
            env_files = file_loader(dot_env)
            if isinstance(dot_env.get(_platform), dict):
                # platform specific dotenv
                env_files += file_loader(dot_env[_platform])

            for file in env_files:
                env.update(load_env(dotenv_path=file))

        recipe_env = recipe.get("env")
        if recipe_env:
            stream = StringIO()

            _parser = ConfigParser()
            _parser.optionxform = str  # to prevent turning keys into lowercase
            _parser.read_dict({"env": recipe_env})
            _parser.write(stream)

            stream.seek(0)  # must reset buffer
            env.update(load_env(stream=stream))

        env.update({
            "REZUP_CONTAINER": self._container.name(),
            "REZUP_USING_REMOTE": "yes" if self._is_pulled else "",
        })

        return env

    def purge(self):
        if self.is_valid():
            # TODO: don't remove it immediately, mark as purged and
            #   remove it when $REZUP_CLEAN_AFTER meet
            rmtree(self._path)

    def iter_backward(self):
        for revision in self._container.iter_revision(latest_first=True):
            if revision.timestamp() < self._timestamp:
                yield revision

    def iter_forward(self):
        for revision in self._container.iter_revision(latest_first=False):
            if revision.timestamp() > self._timestamp:
                yield revision

    def pull(self, check_out=True, fallback=False):
        """Return corresponding local side revision

        If the revision is from remote container, calling this method will
        find a timestamp matched local revision and create one if not found
        by default.

        If the revision is from local container, return `self`.

        Args:
            check_out (bool, optional): When no matched local revision,
                create one if True or just return None at the end.
                Default is True.
            fallback (bool, optional)

        Returns:
            Revision or None

        """
        if not self.is_remote():
            return self

        # get local
        _con_name = self._container.name()
        _con_recipe = self._container.recipe()  # careful, this affect's root

        local = Container(_con_name, recipe=_con_recipe, force_local=True)
        rev = local.get_revision_by_time(self._timestamp,
                                         fallback=fallback,
                                         only_ready=True)

        _allow_create = rev is None and check_out
        _did_fallback = rev.timestamp() != self._timestamp if rev else False

        if not fallback and _allow_create:
            _log.info("Pulling from remote container: %s" % self._container)
            rev = Revision(container=local, dirname=self._dirname)
            rev._write(pulling=self)

        if fallback and _did_fallback:
            _log.warning(
                "Local revision at exact time (%s) is not ready, "
                "fallback to %s" % (self.time_str(), rev)
            )

        if rev is not None:
            rev._is_pulled = True

        return rev

    def spawn_shell(self, command=None):
        """Spawn a sub-shell

        Args:
            command (list, optional): Shell script file with args or commands.
                If given, the sub-shell will not be interactive.

        Returns:
            subprocess.Popen

        Raises:
            ContainerError

        """
        if not self.is_valid():
            raise ContainerError("Cannot use invalid revision.")
        if not self.is_ready():
            raise ContainerError("Revision is not ready to be used.")

        if self.is_remote():
            # use local
            revision = self.pull()
            if revision is None:
                raise ContainerError("No revision pulled.")
            if not revision.is_ready():
                raise ContainerError("Revision is not ready to be used.")

            return revision.spawn_shell(command=command)

        else:
            # Launch subprocess
            environment = self._compose_env()
            shell_name, shell_exec = self._get_shell()

            if command:
                # run command and exit
                if command[0] == ".":
                    cmd = command
                else:
                    exe = command[0]
                    exe = shell.which(exe, env=environment) or exe
                    cmd = [exe] + command[1:]

            else:
                # interactive shell
                _con_name = self._container.name()
                _con_from = "remote" if self._is_pulled else "local"
                prompt = "rezup (%s/%s) " % (_con_name, _con_from)
                prompt = shell.format_prompt_code(prompt, shell_name)
                environment.update({
                    "REZUP_PROMPT": os.getenv("REZUP_PROMPT", prompt),
                })

                cmd = shell.get_launch_cmd(
                    shell_name,
                    shell_exec,
                    interactive=True,
                )

            popen = subprocess.Popen(cmd, env=environment)

            return popen

    def use(self, command=None, wait=True):
        """Run a sub-shell

        Args:
            command (list, optional): Shell script with args or commands. If
                given, the sub-shell will not be interactive.
            wait (bool, optional): Whether to wait `command` finish or not,
                default True.

        Returns:
            int: subprocess return code, will always return 0 if `command`
                is given and `wait` is False.

        """
        block = not command
        popen = self.spawn_shell(command=command)

        if block or wait:
            stdout, stderr = popen.communicate()
            return popen.returncode
        else:
            return 0

    def _compose_env(self):
        env = os.environ.copy()
        env.update(self.recipe_env() or {})
        env["PATH"] = os.pathsep.join([
            os.pathsep.join([str(p) for p in self.production_bin_dirs()]),
            env["PATH"]
        ])
        # use `pythonfinder` package if need to exclude python from PATH

        return env

    def _get_shell(self):
        shell_name = os.getenv("REZUP_DEFAULT_SHELL")
        if shell_name:
            shell_exec = shell_name
            shell_name, ext = os.path.splitext(os.path.basename(shell_name))
            shell_name = shell_name.lower()
        else:
            shell_name, shell_exec = shell.get_current_shell()
            shell_exec = shell_exec or shell_name

        return shell_name, shell_exec

    def _require_local(method):  # noqa
        """Decorator for ensuring local revision exists before action
        """
        @functools.wraps(method)  # noqa
        def wrapper(self, *args, **kwargs):
            if not self.is_remote():
                return method(self, *args, **kwargs)  # noqa

            check_out = bool(os.getenv("REZUP_ALWAYS_CHECKOUT"))
            revision = self.pull(check_out=check_out)
            if revision is None:
                raise ContainerError(
                    "This revision is from remote container, no matched found "
                    "in local. Possible not been pulled into local yet."
                )

            return getattr(revision, method.__name__)(self, *args, **kwargs)

        return wrapper

    @_require_local  # noqa
    def locate_rez_lib(self, venv_session=None):
        """Returns rez module location in this revision

        Returns:
            pathlib.Path or None if not found.

        """
        if venv_session is None:
            venv_path = self.path() / "venv" / "rez"
            venv_session = virtualenv.session_via_cli(args=[str(venv_path)])
        venv_lib = venv_session.creator.purelib

        # rez may get installed in edit mode, try short route first
        egg_link = venv_lib / "rez.egg-link"
        if egg_link.is_file():
            with open(str(egg_link), "r") as f:
                package_location = f.readline().strip()
            if os.path.isdir(package_location):
                return Path(package_location)

        for importer, modname, pkg in pkgutil.walk_packages([str(venv_lib)]):
            if pkg and modname == "rez":
                loader = importer.find_module(modname)
                try:
                    path = loader.path  # SourceFileLoader
                    return Path(path).parent.parent
                except AttributeError:
                    path = loader.filename  # ImpLoader, py2
                    return Path(path).parent

    @_require_local  # noqa
    def get_rez_version(self, venv_session=None):
        """Returns rez version installed in this revision

        Returns:
            str or None if not found.

        """
        rez_location = self.locate_rez_lib(venv_session)
        if rez_location is None:
            return

        version_py = rez_location / "rez" / "utils" / "_version.py"
        if version_py.is_file():
            _locals = {"_rez_version": ""}
            with open(str(version_py)) as f:
                exec(f.read(), globals(), _locals)
            return _locals["_rez_version"]

    @_require_local  # noqa
    def production_bin_dir(self, venv_name):
        """Returns production bin scripts dir in this revision

        Returns:
            pathlib.Path, but the directory may not exists.

        """
        bin_dirname = "Scripts" if platform.system() == "Windows" else "bin"
        venv_bin_dir = self.path() / "venv" / venv_name / bin_dirname
        return venv_bin_dir / "rez"

    @_require_local  # noqa
    def production_bin_dirs(self):
        bin_dirs = []

        metadata = self.metadata()
        if metadata and not metadata.get("rezup_version"):
            # rezup-1.x
            _log.debug("Is a 'rezup-1.x' styled revision.")
            bin_dirs.append(self._path / "bin")
        else:
            # rezup-2.x
            _log.debug("Is a 'rezup-2.x' styled revision.")
            for venv_name in metadata.get("venvs", []):
                bin_dirs.append(self.production_bin_dir(venv_name))

        return bin_dirs

get_rez_version(self, venv_session=None)

Returns rez version installed in this revision

Returns:

Type Description

str or None if not found.

Source code in rezup/container.py
@_require_local  # noqa
def get_rez_version(self, venv_session=None):
    """Returns rez version installed in this revision

    Returns:
        str or None if not found.

    """
    rez_location = self.locate_rez_lib(venv_session)
    if rez_location is None:
        return

    version_py = rez_location / "rez" / "utils" / "_version.py"
    if version_py.is_file():
        _locals = {"_rez_version": ""}
        with open(str(version_py)) as f:
            exec(f.read(), globals(), _locals)
        return _locals["_rez_version"]

locate_rez_lib(self, venv_session=None)

Returns rez module location in this revision

Returns:

Type Description

pathlib.Path or None if not found.

Source code in rezup/container.py
@_require_local  # noqa
def locate_rez_lib(self, venv_session=None):
    """Returns rez module location in this revision

    Returns:
        pathlib.Path or None if not found.

    """
    if venv_session is None:
        venv_path = self.path() / "venv" / "rez"
        venv_session = virtualenv.session_via_cli(args=[str(venv_path)])
    venv_lib = venv_session.creator.purelib

    # rez may get installed in edit mode, try short route first
    egg_link = venv_lib / "rez.egg-link"
    if egg_link.is_file():
        with open(str(egg_link), "r") as f:
            package_location = f.readline().strip()
        if os.path.isdir(package_location):
            return Path(package_location)

    for importer, modname, pkg in pkgutil.walk_packages([str(venv_lib)]):
        if pkg and modname == "rez":
            loader = importer.find_module(modname)
            try:
                path = loader.path  # SourceFileLoader
                return Path(path).parent.parent
            except AttributeError:
                path = loader.filename  # ImpLoader, py2
                return Path(path).parent

production_bin_dir(self, venv_name)

Returns production bin scripts dir in this revision

Returns:

Type Description

pathlib.Path, but the directory may not exists.

Source code in rezup/container.py
@_require_local  # noqa
def production_bin_dir(self, venv_name):
    """Returns production bin scripts dir in this revision

    Returns:
        pathlib.Path, but the directory may not exists.

    """
    bin_dirname = "Scripts" if platform.system() == "Windows" else "bin"
    venv_bin_dir = self.path() / "venv" / venv_name / bin_dirname
    return venv_bin_dir / "rez"

pull(self, check_out=True, fallback=False)

Return corresponding local side revision

If the revision is from remote container, calling this method will find a timestamp matched local revision and create one if not found by default.

If the revision is from local container, return self.

Parameters:

Name Type Description Default
check_out bool

When no matched local revision, create one if True or just return None at the end. Default is True.

True

Returns:

Type Description

Revision or None

Source code in rezup/container.py
def pull(self, check_out=True, fallback=False):
    """Return corresponding local side revision

    If the revision is from remote container, calling this method will
    find a timestamp matched local revision and create one if not found
    by default.

    If the revision is from local container, return `self`.

    Args:
        check_out (bool, optional): When no matched local revision,
            create one if True or just return None at the end.
            Default is True.
        fallback (bool, optional)

    Returns:
        Revision or None

    """
    if not self.is_remote():
        return self

    # get local
    _con_name = self._container.name()
    _con_recipe = self._container.recipe()  # careful, this affect's root

    local = Container(_con_name, recipe=_con_recipe, force_local=True)
    rev = local.get_revision_by_time(self._timestamp,
                                     fallback=fallback,
                                     only_ready=True)

    _allow_create = rev is None and check_out
    _did_fallback = rev.timestamp() != self._timestamp if rev else False

    if not fallback and _allow_create:
        _log.info("Pulling from remote container: %s" % self._container)
        rev = Revision(container=local, dirname=self._dirname)
        rev._write(pulling=self)

    if fallback and _did_fallback:
        _log.warning(
            "Local revision at exact time (%s) is not ready, "
            "fallback to %s" % (self.time_str(), rev)
        )

    if rev is not None:
        rev._is_pulled = True

    return rev

spawn_shell(self, command=None)

Spawn a sub-shell

Parameters:

Name Type Description Default
command list

Shell script file with args or commands. If given, the sub-shell will not be interactive.

None

Returns:

Type Description

subprocess.Popen

Source code in rezup/container.py
def spawn_shell(self, command=None):
    """Spawn a sub-shell

    Args:
        command (list, optional): Shell script file with args or commands.
            If given, the sub-shell will not be interactive.

    Returns:
        subprocess.Popen

    Raises:
        ContainerError

    """
    if not self.is_valid():
        raise ContainerError("Cannot use invalid revision.")
    if not self.is_ready():
        raise ContainerError("Revision is not ready to be used.")

    if self.is_remote():
        # use local
        revision = self.pull()
        if revision is None:
            raise ContainerError("No revision pulled.")
        if not revision.is_ready():
            raise ContainerError("Revision is not ready to be used.")

        return revision.spawn_shell(command=command)

    else:
        # Launch subprocess
        environment = self._compose_env()
        shell_name, shell_exec = self._get_shell()

        if command:
            # run command and exit
            if command[0] == ".":
                cmd = command
            else:
                exe = command[0]
                exe = shell.which(exe, env=environment) or exe
                cmd = [exe] + command[1:]

        else:
            # interactive shell
            _con_name = self._container.name()
            _con_from = "remote" if self._is_pulled else "local"
            prompt = "rezup (%s/%s) " % (_con_name, _con_from)
            prompt = shell.format_prompt_code(prompt, shell_name)
            environment.update({
                "REZUP_PROMPT": os.getenv("REZUP_PROMPT", prompt),
            })

            cmd = shell.get_launch_cmd(
                shell_name,
                shell_exec,
                interactive=True,
            )

        popen = subprocess.Popen(cmd, env=environment)

        return popen

use(self, command=None, wait=True)

Run a sub-shell

Parameters:

Name Type Description Default
command list

Shell script with args or commands. If given, the sub-shell will not be interactive.

None
wait bool

Whether to wait command finish or not, default True.

True

Returns:

Type Description
int

subprocess return code, will always return 0 if command is given and wait is False.

Source code in rezup/container.py
def use(self, command=None, wait=True):
    """Run a sub-shell

    Args:
        command (list, optional): Shell script with args or commands. If
            given, the sub-shell will not be interactive.
        wait (bool, optional): Whether to wait `command` finish or not,
            default True.

    Returns:
        int: subprocess return code, will always return 0 if `command`
            is given and `wait` is False.

    """
    block = not command
    popen = self.spawn_shell(command=command)

    if block or wait:
        stdout, stderr = popen.communicate()
        return popen.returncode
    else:
        return 0

ContainerError

Any error that related to the container

Source code in rezup/exceptions.py
class ContainerError(Exception):
    """Any error that related to the container"""
    pass
Back to top