-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bump version, 0.2 -> 0.3. A significant refactoring, but similar API
- Loading branch information
t-silvers
committed
Jul 18, 2023
1 parent
131864b
commit f921714
Showing
34 changed files
with
1,615 additions
and
985 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ __pycache__/* | |
.vscode | ||
tags | ||
_dev | ||
tests/envs/test-env | ||
usr/envs/* | ||
usr/scripts/* | ||
*.db | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from enum import Enum | ||
|
||
class Cleanup(Enum): | ||
ENV = 'env' | ||
RSCRIPT = 'rscript' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from pyrty.env_managers.base_env import BaseEnvManager | ||
from pyrty.env_managers.conda import CondaEnvManager, MambaEnvManager | ||
# from pyrty.env_managers.packrat import PackratEnvCreator | ||
# from pyrty.env_managers.renv import RenvEnvCreator | ||
# from pyrty.env_managers.venv import VenvEnvCreator | ||
|
||
_env_managers = { | ||
'conda': CondaEnvManager, | ||
'mamba': MambaEnvManager, | ||
# 'packrat': PackratEnvCreator, | ||
# 'renv': RenvEnvCreator, | ||
# 'venv': VenvEnvCreator, | ||
} | ||
|
||
__all__ = [ | ||
'_env_creators', | ||
'BaseEnvManager', | ||
'CondaEnvManager', | ||
'MambaEnvManager', | ||
# 'PackratEnvCreator', | ||
# 'RenvEnvCreator', | ||
# 'VenvEnvCreator', | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import subprocess | ||
from abc import ABC, abstractmethod | ||
from pathlib import Path | ||
|
||
from pyrty.env_managers.utils import SHELL_EXE | ||
|
||
|
||
class BaseEnvManager(ABC): | ||
|
||
def __str__(self) -> str: | ||
return super().__str__() | ||
|
||
def get_run_cmd(self, cmd: str) -> str: | ||
return self.run_cmd_template.format(cmd=cmd) | ||
|
||
def create(self) -> None: | ||
subprocess.run([SHELL_EXE, str(self.deploy_script_path)], check=True) | ||
if self.postdeploy_script_path.exists(): | ||
subprocess.run([SHELL_EXE, str(self.postdeploy_script_path)], check=True) | ||
|
||
def remove(self) -> None: | ||
if self.exists: | ||
subprocess.run(self.remove_cmd, check=True, shell=True) | ||
|
||
# TODO: Something strange happening here on unregistering | ||
if self.deploy_script_path.exists(): | ||
self.deploy_script_path.unlink() | ||
|
||
if self.postdeploy_script_path.exists(): | ||
self.postdeploy_script_path.unlink() | ||
else: | ||
raise FileNotFoundError(f'Environment {self.prefix} does not exist.') | ||
|
||
@property | ||
@abstractmethod | ||
def deploy_script_path(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def env(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def name(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def postdeploy_script_path(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def prefix(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def remove_cmd(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def run_cmd_template(self): | ||
pass | ||
|
||
@property | ||
def exe(self) -> str: | ||
return self._exe | ||
|
||
@property | ||
def exists(self) -> bool: | ||
if self.prefix: | ||
return Path(self.prefix).is_dir() | ||
else: | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
import logging | ||
import shutil | ||
import subprocess | ||
from dataclasses import dataclass, field | ||
from pathlib import Path | ||
|
||
import yaml | ||
|
||
from pyrty.env_managers.base_env import BaseEnvManager | ||
from pyrty.env_managers.utils import ( | ||
DEFAULT_CHANNELS, | ||
default_env_name, | ||
default_env_prefix, | ||
write_conda_deploy_script, | ||
) | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass | ||
class CondaEnv: | ||
"""Represents a Conda environment.""" | ||
|
||
name: str | ||
dependencies: list | ||
channels: list = field(default_factory=lambda: DEFAULT_CHANNELS) | ||
|
||
def __str__(self): | ||
return self.to_yaml() | ||
|
||
def to_yaml(self) -> str: | ||
"""Convert the CondaEnv object to a YAML string. | ||
Returns: | ||
str: The YAML representation of the object. | ||
""" | ||
return yaml.safe_dump(self.__dict__, default_flow_style=False) | ||
|
||
def write(self, path: str) -> None: | ||
"""Write the CondaEnv object to a YAML file. | ||
Args: | ||
path (str): The path of the file to write to. | ||
""" | ||
yaml_string = self.to_yaml() | ||
with open(path, 'w') as file: | ||
file.write(yaml_string) | ||
|
||
@classmethod | ||
def from_yaml(cls, path: str): | ||
"""Create a CondaEnv instance from a YAML file. | ||
Args: | ||
path (str): The path of the YAML file. | ||
Returns: | ||
CondaEnv: The created CondaEnv instance. | ||
""" | ||
with open(path, 'r') as file: | ||
yaml_data = yaml.safe_load(file) | ||
|
||
return cls(yaml_data.get('name', ''), | ||
yaml_data.get('dependencies', []), | ||
yaml_data.get('channels', [])) | ||
|
||
@classmethod | ||
def from_existing(cls, exe: Path, prefix: Path, path: Path): | ||
"""Create a CondaEnv instance from an existing Conda env. | ||
Args: | ||
prefix (str): The path of the Conda env. | ||
Returns: | ||
CondaEnv: The created CondaEnv instance. | ||
""" | ||
subprocess.run(f'{str(exe)} run -p {str(prefix)} {str(exe)} env export > {str(path)}', shell=True, check=True) | ||
return cls.from_yaml(path) | ||
|
||
@property | ||
def r_packages(self) -> list: | ||
"""List of R packages (including CRAN and Bioconductor) in the environment. | ||
Returns: | ||
list: List of R packages. | ||
""" | ||
return self.cran_packages + self.bioc_packages | ||
|
||
@property | ||
def cran_packages(self) -> list: | ||
"""List of CRAN packages in the environment. | ||
Returns: | ||
list: List of CRAN packages. | ||
""" | ||
return [dep for dep in self.dependencies if dep.startswith('r-')] | ||
|
||
@property | ||
def bioc_packages(self) -> list: | ||
"""List of Bioconductor packages in the environment. | ||
Returns: | ||
list: List of Bioconductor packages. | ||
""" | ||
return [dep for dep in self.dependencies if dep.startswith('bioconductor-')] | ||
|
||
|
||
class CondaEnvManager(BaseEnvManager): | ||
def __init__( | ||
self, | ||
exe: Path = None, | ||
prefix: Path = None, | ||
envfile: Path = None, | ||
name: str = None, | ||
dependencies: list = None, | ||
channels: list = None, | ||
postdeploy_cmds: list[str] = None, | ||
): | ||
self._exe = exe if exe else shutil.which("conda") | ||
self._prefix = prefix | ||
self.envfile = envfile | ||
self._name = name | ||
self._dependencies = dependencies | ||
self.channels = channels or DEFAULT_CHANNELS | ||
self.postdeploy_cmds = postdeploy_cmds | ||
|
||
def create(self): | ||
# Overwrite | ||
self._process_env_specs(self.prefix, self.envfile, self.name, | ||
self.dependencies, self.channels) | ||
self._write_deploy_script() | ||
self._process_postdeploy_cmds(self.postdeploy_cmds) | ||
super().create() | ||
|
||
def _process_env_specs(self, prefix, envfile, name, dependencies, channels): | ||
if not envfile and name: | ||
envfile = Path(f"{name}.yaml") | ||
elif not envfile and not name: | ||
raise ValueError("Must provide either a path (to `envfile`) or name.") | ||
self.envfile = Path(envfile) | ||
|
||
# -- When environment already exists ... | ||
self._prefix = prefix # May be None | ||
if self.exists: | ||
_logger.info(f"Environment exists at {self.prefix}.") | ||
self._env = CondaEnv.from_existing(self.exe, self.prefix, self.envfile) | ||
|
||
else: | ||
if not self.prefix: # Now must be provided or generated | ||
if name: | ||
self._prefix = default_env_prefix(name) | ||
elif not name and envfile: | ||
# Parse name from envfile | ||
with open(self.envfile, 'r') as file: | ||
yaml_data = yaml.safe_load(file) | ||
name = yaml_data['name'] # Should error if name not in yaml_data | ||
self._prefix = name | ||
else: | ||
raise ValueError("Must provide either an envfile or name and dependencies.") | ||
|
||
# -- When environment file exists ... | ||
if self.envfile.exists(): | ||
_logger.info(f"Environment file {self.envfile} exists.") | ||
self._env = CondaEnv.from_yaml(self.envfile) | ||
|
||
# -- When environment dependencies are provided ... | ||
elif name and dependencies: | ||
name_ = default_env_name(name) | ||
_logger.info(f"Creating environment {name_} with dependencies {dependencies}.") | ||
self._env = CondaEnv(name_, dependencies, channels) | ||
self._env.write(self.envfile) | ||
|
||
# -- Else ... | ||
else: | ||
raise ValueError("Must provide either an envfile or name and dependencies.") | ||
|
||
self._name = self.env.name | ||
|
||
def add_channel(self, channel) -> None: | ||
if channel not in self.channels: | ||
self.channels.append(channel) | ||
|
||
# def add_dependency(self, dependency) -> None: | ||
# if dependency not in self.dependencies: | ||
# self.dependencies.append(dependency) | ||
|
||
@property | ||
def deploy_script_path(self) -> Path: | ||
return Path(self.prefix).parent / f"{self.name}.deploy.sh" | ||
|
||
@property | ||
def dependencies(self): | ||
return self._dependencies | ||
|
||
@dependencies.setter | ||
def add_dependency(self, dependency): | ||
if dependency not in self._dependencies: | ||
self._dependencies.append(dependency) | ||
|
||
@property | ||
def env(self): | ||
return self._env | ||
|
||
@property | ||
def name(self): | ||
return self._name | ||
|
||
@property | ||
def postdeploy_script_path(self) -> Path: | ||
return Path(self.prefix).parent / f"{self.name}.post-deploy.sh" | ||
|
||
@property | ||
def prefix(self): | ||
return self._prefix | ||
|
||
@property | ||
def remove_cmd(self): | ||
return f"{self.exe} env remove --prefix {self.prefix} -y" | ||
|
||
@property | ||
def run_cmd_template(self) -> str: | ||
return f"{self.exe} run -p {self.prefix} {{cmd}}" | ||
|
||
def _write_deploy_script(self) -> None: | ||
write_conda_deploy_script(self.deploy_script_path, self.exe, self.prefix, self.envfile) | ||
|
||
def _process_postdeploy_cmds(self, postdeploy_cmds): | ||
if postdeploy_cmds: | ||
self._write_postdeploy_script(postdeploy_cmds) | ||
|
||
def _write_postdeploy_script(self, cmds: list[str]) -> None: | ||
postdeploy_commands = "\n".join([self.get_run_cmd(cmd) for cmd in cmds]) | ||
self.postdeploy_script_path.write_text(postdeploy_commands) | ||
|
||
class MambaEnvManager(CondaEnvManager): | ||
def __init__( | ||
self, | ||
exe: Path = None, | ||
prefix: Path = None, | ||
envfile: Path = None, | ||
name: str = None, | ||
dependencies: list = None, | ||
channels: list = None, | ||
postdeploy_cmds: list[str] = None, | ||
): | ||
super().__init__(exe=exe or shutil.which("mamba"), prefix=prefix, | ||
envfile=envfile, name=name, dependencies=dependencies, | ||
channels=channels, postdeploy_cmds=postdeploy_cmds) |
Oops, something went wrong.