From a2f81319ea4aa0d1b0f9764b74b95ca2e11cc443 Mon Sep 17 00:00:00 2001 From: Yicheng Luo Date: Sat, 27 Apr 2024 16:32:01 +0100 Subject: [PATCH] Refactor * Refactor console logging * Make output from job service more readable * Fork sftp file system * Add new implementation for storage * Simplify artifact store creation * Skip image push for local filesystem * Rename Command to AppBundle * Fall back to default local configuration --- .pre-commit-config.yaml | 2 +- examples/jax_gpu/launcher_docker.py | 1 + lxm3/cli/clean.py | 171 ---- lxm3/cli/cli.py | 46 - lxm3/cli/shell.py | 4 +- lxm3/contrib/ucl.py | 1 + lxm3/experimental/image_cache.py | 4 +- lxm3/experimental/job_script_builder.py | 352 ------- lxm3/xm_cluster/__init__.py | 3 +- lxm3/xm_cluster/artifacts.py | 259 ++--- lxm3/xm_cluster/config.py | 46 +- lxm3/xm_cluster/console.py | 22 +- lxm3/xm_cluster/executables.py | 2 +- lxm3/xm_cluster/execution/gridengine.py | 80 +- .../execution/job_script_builder.py | 677 ++++++------- lxm3/xm_cluster/execution/local.py | 74 +- lxm3/xm_cluster/execution/slurm.py | 73 +- lxm3/xm_cluster/execution/utils.py | 33 - lxm3/xm_cluster/executors.py | 2 +- lxm3/xm_cluster/experiment.py | 14 +- lxm3/xm_cluster/packaging/archive_builder.py | 16 +- lxm3/xm_cluster/packaging/cluster.py | 100 +- lxm3/xm_cluster/packaging/local.py | 7 +- pdm.lock | 950 ++++++++++-------- pyproject.toml | 22 +- tests/artifact_test.py | 49 + tests/config_test.py | 18 +- tests/conftest.py | 4 +- tests/execution_test.py | 198 ++-- tests/experiment_test.py | 2 +- tests/experimental/script_gen_test.py | 84 -- tests/experimental/testdata/pkg/main.py | 8 - tests/packaging_test.py | 46 +- 33 files changed, 1372 insertions(+), 1998 deletions(-) delete mode 100644 lxm3/cli/clean.py delete mode 100644 lxm3/experimental/job_script_builder.py delete mode 100644 lxm3/xm_cluster/execution/utils.py create mode 100644 tests/artifact_test.py delete mode 100644 tests/experimental/script_gen_test.py delete mode 100755 tests/experimental/testdata/pkg/main.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 932ab97..22d45a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.4 + rev: v0.4.2 hooks: - id: ruff args: [ --fix ] diff --git a/examples/jax_gpu/launcher_docker.py b/examples/jax_gpu/launcher_docker.py index f278a43..e6b9847 100644 --- a/examples/jax_gpu/launcher_docker.py +++ b/examples/jax_gpu/launcher_docker.py @@ -2,6 +2,7 @@ """This showcases how to create a launcher script without ever requiring the user to manually build a singularity image. """ + import subprocess from absl import app diff --git a/lxm3/cli/clean.py b/lxm3/cli/clean.py deleted file mode 100644 index 6c73d04..0000000 --- a/lxm3/cli/clean.py +++ /dev/null @@ -1,171 +0,0 @@ -import datetime -import os -from typing import Optional - -import fsspec -from rich.prompt import Confirm - -from lxm3.xm_cluster import config as config_lib -from lxm3.xm_cluster.console import console - -_JOB = "job" -_ARCHIVE = "archive" -_CONTAINER = "container" -_VALID_TYPES = [_JOB, _ARCHIVE, _CONTAINER] - - -class ClusterJob: - def __init__(self, client, job_root) -> None: - self._client = client - self._job_root = job_root - - @property - def job_name(self): - return os.path.basename(self._job_root) - - def job_script(self): - return ( - self._client._fs.cat(os.path.join(self._job_root, "job.sh")) - .decode("utf-8") - .strip() - ) - - def job_id(self): - return ( - self._client._fs.cat(os.path.join(self._job_root, "job_id")) - .decode("utf-8") - .strip() - ) - - def logs(self): - logs = self._client._fs.ls(os.path.join(self._job_root, "logs")) - return logs - - def time_created(self): - return self._client._fs.info(os.path.join(self._job_root, "job_id"))["mtime"] - - -class JobClient: - def __init__(self, cluster_settings) -> None: - self._cluster_settings = cluster_settings - self._fs = fsspec.filesystem( - "sftp", - host=cluster_settings.hostname, - username=cluster_settings.user, - **cluster_settings.ssh_config, - ) - - @property - def filesystem(self): - return self._fs - - def list_projects(self): - project_root = os.path.join(self._cluster_settings.storage_root, "projects") - return [os.path.basename(p) for p in self._fs.ls(project_root)] - - def list_jobs(self, project): - job_root = os.path.join( - self._cluster_settings.storage_root, "projects", project, "jobs" - ) - return [os.path.basename(p) for p in self._fs.ls(job_root)] - - def list_containers(self, project): - container_root = os.path.join( - self._cluster_settings.storage_root, "projects", project, "containers" - ) - return [p for p in self._fs.ls(container_root)] - - def list_archives(self, project): - archive_root = os.path.join( - os.path.join( - self._cluster_settings.storage_root, "projects", project, "archives" - ) - ) - return [p for p in self._fs.ls(archive_root)] - - def get_job(self, project, job_name): - job_path = os.path.join( - self._cluster_settings.storage_root, "projects", project, "jobs", job_name - ) - - return ClusterJob(self, job_path) - - -def run_clean( - project: str, - days: Optional[int], - dry_run: bool = False, - force: bool = False, - type_: Optional[str] = None, -): - config = config_lib.default() - client = JobClient(config.cluster_settings()) - now = datetime.datetime.now(tz=datetime.timezone.utc) - - if type_ is None: - types = _VALID_TYPES - else: - types = type_.split(",") - for type_ in types: - if type_ not in _VALID_TYPES: - raise ValueError("Invalid type: {}".format(type_)) - - valid_projects = client.list_projects() - if project not in valid_projects: - raise ValueError( - "Invalid project: {}, available projects are {}".format( - project, valid_projects - ) - ) - items_to_remove = [] - if _JOB in types: - for job_name in client.list_jobs(project): - job = client.get_job(project, job_name) - job_root = job._job_root - time_created = client.filesystem.info(job_root)["mtime"] - if not days or (now - time_created > datetime.timedelta(days=days)): - items_to_remove.append(("job", job_root)) - - if _ARCHIVE in types: - for archive in client.list_archives(project): - time_created = client.filesystem.info(archive)["mtime"] - if not days or (now - time_created > datetime.timedelta(days=days)): - items_to_remove.append(("archive", archive)) - - if _CONTAINER in types: - for container in client.list_containers(project): - time_created = client.filesystem.info(container)["mtime"] - if not days or (now - time_created > datetime.timedelta(days=days)): - items_to_remove.append(("container", container)) - - if dry_run: - if len(items_to_remove) > 0: - for item_type, path in items_to_remove: - console.print("Removing {} {}".format(item_type, path)) - else: - console.print("No items to remove") - return - else: - remove = False - if not force: - if len(items_to_remove) == 0: - console.print("No items to remove") - return - for item_type, path in items_to_remove: - console.print("Would remove [bold]{}[bold] {}".format(item_type, path)) - try: - remove = Confirm.ask("Do you wish to continue?") - except KeyboardInterrupt: - console.print("Aborting") - return - else: - remove = True - - if remove: - for item_type, path in items_to_remove: - console.print("Removing [bold]{}[bold] {}".format(item_type, path)) - try: - client.filesystem.rm(path, recursive=True) - except: # noqa - console.print_exception() - console.print("Failed to remove {}".format(path)) diff --git a/lxm3/cli/cli.py b/lxm3/cli/cli.py index 523d47f..d7c7cdc 100644 --- a/lxm3/cli/cli.py +++ b/lxm3/cli/cli.py @@ -36,19 +36,6 @@ def launch(args): sys.path.pop(0) -def shell(args): - del args - from lxm3.cli.shell import main - - app.run(main) - - -def clean(args): - from lxm3.cli.clean import run_clean - - run_clean(args.project, args.days, args.dry_run, args.force, args.type) - - def register_launch_parser(parsers: argparse._SubParsersAction): launch_parser = parsers.add_parser( "launch", @@ -79,37 +66,6 @@ def register_launch_parser(parsers: argparse._SubParsersAction): launch_parser.set_defaults(command=launch) -def register_shell_parser(parsers: argparse._SubParsersAction): - launch_parser = parsers.add_parser( - "shell", - help="Open a shell.", - inherited_absl_flags=None, # type: ignore - ) - launch_parser.set_defaults(command=shell) - - -def register_clean_parser(parsers: argparse._SubParsersAction): - clean_parser = parsers.add_parser( - "clean", - help="Clean job artifacts.", - inherited_absl_flags=None, # type: ignore - ) - clean_parser.add_argument("--project", required=True) - clean_parser.add_argument( - "--dry_run", - default=False, - action="store_true", - ) - clean_parser.add_argument( - "--force", - default=False, - action="store_true", - ) - clean_parser.add_argument("--days", type=float) - clean_parser.add_argument("--type", default=None) - clean_parser.set_defaults(command=clean) - - def _parse_flags(argv): parser = argparse_flags.ArgumentParser(description="lxm3 experiment scheduler.") parser.set_defaults(command=lambda _: parser.print_help()) @@ -118,8 +74,6 @@ def _parse_flags(argv): register_version_parser(subparsers) register_launch_parser(subparsers) - register_shell_parser(subparsers) - register_clean_parser(subparsers) args = parser.parse_args(argv[1:]) return args diff --git a/lxm3/cli/shell.py b/lxm3/cli/shell.py index da50254..f91d30a 100644 --- a/lxm3/cli/shell.py +++ b/lxm3/cli/shell.py @@ -6,14 +6,14 @@ from lxm3.clusters import gridengine from lxm3.xm_cluster import config as config_lib -from lxm3.xm_cluster.console import console +from lxm3.xm_cluster import console def main(_): config = config_lib.default() cluster_settings = config.cluster_settings() - console.log( + console.info( "Creating a client to cluster " f"{cluster_settings.user}@{cluster_settings.hostname} ..." ) diff --git a/lxm3/contrib/ucl.py b/lxm3/contrib/ucl.py index 1699eb5..3e65783 100644 --- a/lxm3/contrib/ucl.py +++ b/lxm3/contrib/ucl.py @@ -51,6 +51,7 @@ To understand the rules for translating for the two clusters, refer to `:ucl_test`. """ + from typing import Optional from lxm3.xm_cluster import config diff --git a/lxm3/experimental/image_cache.py b/lxm3/experimental/image_cache.py index d2aade4..cdf5ad4 100644 --- a/lxm3/experimental/image_cache.py +++ b/lxm3/experimental/image_cache.py @@ -44,9 +44,9 @@ def _log(message): # Consider moving this out - from lxm3.xm_cluster.console import console # noqa + from lxm3.xm_cluster import console # noqa - console.log(message, style="dim") + console.info(message, style="dim") @dataclasses.dataclass(frozen=True) diff --git a/lxm3/experimental/job_script_builder.py b/lxm3/experimental/job_script_builder.py deleted file mode 100644 index 2bb87ae..0000000 --- a/lxm3/experimental/job_script_builder.py +++ /dev/null @@ -1,352 +0,0 @@ -import collections -import shlex -from typing import Any, Dict, List, Optional, Union - - -class JobScriptBuilder: - ARRAY_TASK_VAR_NAME = "SGE_TASK_ID" - ARRAY_TASK_OFFSET = 1 - DIRECTIVE_PREFIX = "#$" - JOB_ENV_PATTERN = "^(JOB_|SGE_|PE|NSLOTS|NHOSTS)" - - @classmethod - def _create_array_task_env_vars(cls, env_vars_list: List[Dict[str, str]]) -> str: - """Create the env_vars list.""" - lines = [] - # TODO(yl): Handle the case where each task has different env_vars - first_keys = set(env_vars_list[0].keys()) - if not first_keys: - return "" - for env_vars in env_vars_list: - if first_keys != set(env_vars.keys()): - raise ValueError( - "Expect all task environment variables to have the same keys" - "If you want to have different environment variables for each task, set them to ''" - ) - - # Find out keys that are common to all environment variables - var_to_values = collections.defaultdict(list) - for env in env_vars_list: - for k, v in env.items(): - var_to_values[k].append(v) - - common_keys = [] - for k, v in var_to_values.items(): - if len(set(v)) == 1: - common_keys.append(k) - common_keys = sorted(common_keys) - - # Generate shared environment variables - for k in sorted(common_keys): - lines.append( - 'export {key}="{value}"'.format(key=k, value=env_vars_list[0][k]) - ) - - for key in first_keys: - if key in common_keys: - continue - - for key in first_keys: - for task_id, env_vars in enumerate(env_vars_list): - lines.append( - "{key}_{task_id}={value}".format( - key=key, - task_id=task_id + cls.ARRAY_TASK_OFFSET, - value=env_vars[key], - ) - ) - lines.append( - '{key}=$(eval echo \\$"{key}_${task_id_env_var}")'.format( - key=key, task_id_env_var=cls.ARRAY_TASK_VAR_NAME - ) - ) - lines.append("export {key}".format(key=key)) - content = "\n".join(lines) - return content - - @classmethod - def _create_array_task_args(cls, args_list: List[List[str]]) -> str: - """Create the args list.""" - if not args_list: - return "\n".join(["TASK_CMD_ARGS=''"]) - lines = [] - for task_id, args in enumerate(args_list): - args_str = shlex.quote(" ".join(args)) - lines.append( - "TASK_CMD_ARGS_{task_id}={args_str}".format( - task_id=task_id + cls.ARRAY_TASK_OFFSET, args_str=args_str - ) - ) - lines.append( - 'TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_${task_id_env_var}")'.format( - task_id_env_var=cls.ARRAY_TASK_VAR_NAME - ) - ) - content = "\n".join(lines) - return content - - @classmethod - def create_array_task_script( - cls, - per_task_args: List[List[str]], - per_task_envs: Optional[List[Dict[str, str]]] = None, - ) -> str: - """Create a wrapper script to run an array of tasks. - Args: - per_task_args: A list of list[str] where each list contains the command line - arguments for a task. The arguments will NOT be quoted. If you need to evaluate - the arguments, use shlex if you need to quote individual arguments. - per_task_env: A list of dictionaries where each dictionary contains the - environment variables for a task. If None, then the environment variables - will be empty. - Raises: - ValueError: If the number of environment variables and arguments do not match. - ValueError: If the environment variables do not shared the same keys. - - Returns: - A string that represents the wrapper script. This script can be evaluated - via sh -c. Note that the a shebang is not included. - """ - if per_task_envs is None: - per_task_envs = [{} for _ in per_task_args] - - if len(per_task_envs) != len(per_task_args): - raise ValueError("Expect the same number of env_vars and args") - - env_vars = cls._create_array_task_env_vars(per_task_envs) - args = cls._create_array_task_args(per_task_args) - - return f"""\ -if [ -z ${{{cls.ARRAY_TASK_VAR_NAME}+x}} ]; -then -echo >&2 "ERROR[$0]: \\${cls.ARRAY_TASK_VAR_NAME} is not set." -exit 2 -fi - -# Prepare environment variables for task -{env_vars} - -# Prepare command line arguments for task -{args} - -# Evaluate the TASK_CMD_ARGS and set the positional arguments -# This is necessary to handle quoting in the arguments. -eval set -- $TASK_CMD_ARGS -""" - - @classmethod - def create_job_script( - cls, - *, - command: List[str], - singularity_image: Optional[str] = None, - singularity_binds: Optional[List[str]] = None, - singularity_options: Optional[List[str]] = None, - archives: Optional[Union[str, List[str]]] = None, - files: Optional[List[str]] = None, - per_task_args: Optional[List[List[str]]] = None, - per_task_envs: Optional[List[Dict[str, str]]] = None, - # SGE options - resources: Optional[Dict[str, Any]] = None, - parallel_environments: Optional[Dict[str, int]] = None, - log_directory: Optional[str] = None, - merge_log_output: bool = True, - task_concurrency: Optional[int] = None, - extra_directives: Optional[List[str]] = None, - # Additional script content - prologue: str = "", - epilogue: str = "", - verbose: bool = False, - ): - shebang = "#!/bin/sh" - script = [shebang] - - if verbose: - script.append("LXM_DEBUG=1") - - if not resources: - resources = {} - - for resource_name, resource_value in resources.items(): - script.append(f"{cls.DIRECTIVE_PREFIX} -l {resource_name}={resource_value}") - - if parallel_environments is None: - parallel_environments = {} - - for pe_name, pe_value in parallel_environments.items(): - script.append(f"{cls.DIRECTIVE_PREFIX} -pe {pe_name}={pe_value}") - - if log_directory is not None: - script.append(f"{cls.DIRECTIVE_PREFIX} -o {log_directory}") - script.append(f"{cls.DIRECTIVE_PREFIX} -e {log_directory}") - - if merge_log_output: - script.append(f"{cls.DIRECTIVE_PREFIX} -j y") - - if task_concurrency is not None: - if not task_concurrency > 0: - raise ValueError("Task concurrency must be positive") - - script.append(f"{cls.DIRECTIVE_PREFIX} -tc {task_concurrency}") - - if per_task_args is not None: - script.append(f"{cls.DIRECTIVE_PREFIX} -t 1-{len(per_task_args)}") - - if not extra_directives: - extra_directives = [] - for h in extra_directives: - script.append(f"{cls.DIRECTIVE_PREFIX} {h}") - - script.append( - """\ -LXM_WORKDIR="$(mktemp -d)" -if [ "${LXM_DEBUG:-}" = 1 ]; then - echo >& 2 "DEBUG[$(basename "$0")] Working directory: $LXM_WORKDIR" -fi -cleanup() { - if [ "${LXM_DEBUG:-}" = 1 ]; then - echo >& 2 "DEBUG[$(basename "$0")] Cleaning up $LXM_WORKDIR" - fi - rm -rf "$LXM_WORKDIR" -} -trap cleanup EXIT -cd "$LXM_WORKDIR" -""" - ) - - script.append(prologue) - if archives: - if isinstance(archives, str): - archives = [archives] - script.append(_extract_archive_cmds(archives, "$LXM_WORKDIR")) - if files: - for f in files: - script.append(f'cp {f} "$LXM_WORKDIR"/') - - job_script_file = "job-params.sh" - if per_task_args is not None: - script.append( - f"""\ -cat <<'EOF' > "$LXM_WORKDIR/{job_script_file}" -{cls.create_array_task_script(per_task_args, per_task_envs)} -EOF -chmod +x "$LXM_WORKDIR"/{job_script_file} -""" - ) - - if singularity_image is not None: - workdir_mount_path = "/run/task" - container_job_script_path = f"/etc/{job_script_file}" - env_file = ".env" - if singularity_binds is None: - singularity_binds = [] - binds = [ - *singularity_binds, - f'"$LXM_WORKDIR":{workdir_mount_path}', - ] - script.append( - f"""\ -# Save and pass environment variables from SGE to a file -printenv | grep -s -E "{cls.JOB_ENV_PATTERN}" > {env_file} -""" - ) - if per_task_args is not None: - binds.append( - f'"$LXM_WORKDIR"/{job_script_file}:{container_job_script_path}:ro', - ) - wrapped_cmd = [ - "sh", - "-c", - shlex.quote( - f'. {container_job_script_path}; {shlex.join(command)} "$@"' - ), - ] - else: - wrapped_cmd = ["sh", "-c", shlex.quote(f'{shlex.join(command)} "$@"')] - script.append( - _get_singularity_command( - singularity_image, - command=wrapped_cmd, - binds=binds, - env_file=env_file, - options=singularity_options, - pwd=workdir_mount_path, - ) - ) - else: - if per_task_args is not None: - script.append( - f'. "$LXM_WORKDIR"/{job_script_file}; {shlex.join(command)} "$@"' - ) - else: - script.append(shlex.join(command)) - - script.append(epilogue) - return "\n".join(script) - - -create_job_script = JobScriptBuilder.create_job_script - - -def _get_singularity_command( - image: str, - *, - command: List[str], - binds: Optional[List[str]] = None, - envs: Optional[Dict[str, str]] = None, - options: Optional[List[str]] = None, - env_file: Optional[str] = None, - pwd: Optional[str] = None, -): - if not binds: - binds = [] - - if not options: - options = [] - - if not envs: - envs = {} - - singularity_cmd = [] - singularity_cmd = [ - "singularity", - "exec", - *([f"--env-file={env_file}"] if env_file else []), - *[f"--env={key}={value}" for key, value in envs.items()], - *([f"--pwd={pwd}"] if pwd else []), - *options, - *[f"--bind={bind}" for bind in binds], - image, - *command, - ] - return " ".join(singularity_cmd) - - -def _extract_archive_cmds(archive: List[str], extract_dir: str): - return """\ -# Extract archives -extract_archive() {{ - case $ar in - *.zip) - unzip -q -u -d "$1" "$2" - ;; - *.tar) - tar -C "$1" -xf "$2" - ;; - *.tar.gz|*.tgz) - tar -C "$1" -xzf "$2" - ;; - *) - echo >& 2 "Unsupported archive format: $2" - exit 2 - ;; - esac -}} -ARCHIVES="{archive}" -for ar in $ARCHIVES; do - extract_archive "{extract_dir}" "$ar" -done - """.format( - archive=" ".join(archive), - extract_dir=extract_dir, - ) diff --git a/lxm3/xm_cluster/__init__.py b/lxm3/xm_cluster/__init__.py index 8841163..2bf4b2d 100644 --- a/lxm3/xm_cluster/__init__.py +++ b/lxm3/xm_cluster/__init__.py @@ -13,7 +13,7 @@ from lxm3.xm_cluster.executable_specs import PythonPackage from lxm3.xm_cluster.executable_specs import SingularityContainer from lxm3.xm_cluster.executable_specs import UniversalPackage -from lxm3.xm_cluster.executables import Command +from lxm3.xm_cluster.executables import AppBundle from lxm3.xm_cluster.executors import DockerOptions from lxm3.xm_cluster.executors import GridEngine from lxm3.xm_cluster.executors import Local @@ -22,6 +22,7 @@ from lxm3.xm_cluster.experiment import ClusterExperiment from lxm3.xm_cluster.experiment import ClusterWorkUnit from lxm3.xm_cluster.experiment import create_experiment +from lxm3.xm_cluster.experiment import get_current_experiment from lxm3.xm_cluster.requirements import JobRequirements logging.getLogger("paramiko").setLevel(logging.WARNING) diff --git a/lxm3/xm_cluster/artifacts.py b/lxm3/xm_cluster/artifacts.py index 02bcd89..1e72f43 100644 --- a/lxm3/xm_cluster/artifacts.py +++ b/lxm3/xm_cluster/artifacts.py @@ -1,17 +1,25 @@ -import abc import datetime +import functools import os -from typing import Any, Mapping, Optional +import shutil +from typing import Optional, Tuple +import attr import fsspec -import rich.progress -import rich.syntax -from fsspec.implementations.sftp import SFTPFileSystem +from fsspec.implementations import local +from fsspec.implementations import sftp -from lxm3.xm_cluster.console import console +from lxm3.xm_cluster import config as config_lib -class ArtifactStore(abc.ABC): +@attr.s(auto_attribs=True) +class FileInfo: + path: str + size: int + time_modified: datetime.datetime + + +class ArtifactStore: def __init__( self, filesystem: fsspec.AbstractFileSystem, @@ -22,184 +30,119 @@ def __init__( self._storage_root = os.path.join(staging_directory, "projects", project) else: self._storage_root = staging_directory - self._fs = filesystem + self._filesystem = filesystem self._initialize() - def _initialize(self): - pass + @property + def filesystem(self): + return self._filesystem - def job_path(self, job_name: str): - return os.path.join(self._storage_root, "jobs", job_name) + @property + def storage_root(self): + return self._storage_root - def job_script_path(self, job_name: str): - return os.path.join(self.job_path(job_name), "job.sh") + def _initialize(self): + self.ensure_dir(".") + if not self.exists(".gitignore"): + self.put_text("*\n", ".gitignore") - def job_log_path(self, job_name: str): - return os.path.join(os.path.join(self._storage_root, "logs", job_name)) + def get_file_info(self, path: str) -> FileInfo: + path = self.normalize_path(path) + info = self._filesystem.info(path) + size = info["size"] + mod_time = info["mtime"] + if not isinstance(mod_time, datetime.datetime): + mod_time = datetime.datetime.fromtimestamp( + info["mtime"], tz=datetime.timezone.utc + ) + return FileInfo(path, size, mod_time) - def singularity_image_path(self, image_name: str): - return os.path.join(self._storage_root, "containers", image_name) + def normalize_path(self, path: str) -> str: + assert not os.path.isabs(path) + return os.path.join(self._storage_root, path) - def archive_path(self, archive_name: str): - return os.path.join(self._storage_root, "archives", archive_name) + def exists(self, name: str) -> bool: + return self._filesystem.exists(self.normalize_path(name)) - def _should_update(self, src: str, dst: str) -> bool: - if not self._fs.exists(dst): - return True + def ensure_dir(self, directory: str): + directory = self.normalize_path(directory) + return self._filesystem.makedirs(directory, exist_ok=True) - local_stat = os.stat(src) - local_mtime = datetime.datetime.utcfromtimestamp( - local_stat.st_mtime - ).timestamp() - storage_stat = self._fs.info(dst) - storage_mtime = storage_stat["mtime"] + def put_file(self, lpath: str, rpath: str) -> str: + path = self.normalize_path(rpath) - if isinstance(storage_mtime, datetime.datetime): - storage_mtime = storage_mtime.timestamp() + self._filesystem.makedirs(os.path.dirname(path), exist_ok=True) + self._filesystem.put(lpath, path) - if local_stat.st_size != storage_stat["size"]: - return True + return path - if int(local_mtime) > int(storage_mtime): - return True + def put_text(self, text: str, rpath: str) -> str: + path = self.normalize_path(rpath) - return False + self._filesystem.makedirs(os.path.dirname(path), exist_ok=True) + self._filesystem.write_text(path, text) - def _put_content(self, dst, content): - self._fs.makedirs(os.path.dirname(dst), exist_ok=True) + return path - with self._fs.open(dst, "wt") as f: - f.write(content) + def put_fileobj(self, fileobj, rpath: str) -> str: + path = self.normalize_path(rpath) - def _put_file(self, local_filename, dst): - self._fs.makedirs(os.path.dirname(dst), exist_ok=True) - self._fs.put(local_filename, dst) + self._filesystem.makedirs(os.path.dirname(path), exist_ok=True) - def deploy_job_scripts(self, job_name, job_script): - job_path = self.job_path(job_name) - job_log_path = self.job_log_path(job_name) + filesystem = self._filesystem + if isinstance(filesystem, local.LocalFileSystem): + with filesystem.open(path, "wb") as fout: + shutil.copyfileobj(fileobj, fout) + elif isinstance(filesystem, sftp.SFTPFileSystem): + filesystem.ftp.putfo(fileobj, path) + else: + raise NotImplementedError() - self._fs.makedirs(job_path, exist_ok=True) - self._fs.makedirs(job_log_path, exist_ok=True) - self._put_content(self.job_script_path(job_name), job_script) - console.log(f"Created job script {self.job_script_path(job_name)}") + return path - def deploy_singularity_container(self, lpath, image_name): - assert "/" not in image_name - deploy_container_path = self.singularity_image_path(image_name) - should_update = self._should_update(lpath, deploy_container_path) - if should_update: - self._fs.makedirs(os.path.dirname(deploy_container_path), exist_ok=True) - self._put_file(lpath, deploy_container_path) - console.log(f"Deployed Singularity container to {deploy_container_path}") - else: - console.log(f"Container {deploy_container_path} exists, skip upload.") - return deploy_container_path + def should_update(self, lpath: str, rpath: str) -> Tuple[bool, str]: + if not self.exists(rpath): + return True, "file does not exist" - def deploy_resource_archive(self, lpath, archive_name): - assert "/" not in archive_name - deploy_archive_path = self.archive_path(archive_name) + local_stat = os.stat(lpath) + local_size = local_stat.st_size + local_mtime = datetime.datetime.fromtimestamp( + local_stat.st_atime, tz=datetime.timezone.utc + ) + remote_file_info = self.get_file_info(rpath) - should_update = self._should_update(lpath, deploy_archive_path) - if should_update: - self._put_file(lpath, deploy_archive_path) - console.log(f"Deployed archive to {deploy_archive_path}") - else: - console.log(f"Archive {deploy_archive_path} exists, skipping upload.") - return deploy_archive_path + if local_size != remote_file_info.size: + return True, "file size mismatch" + if local_mtime > remote_file_info.time_modified: + return True, "local file is newer" -class LocalArtifactStore(ArtifactStore): - def __init__(self, staging_directory: str, project=None): - staging_directory = os.path.abspath(os.path.expanduser(staging_directory)) - super().__init__(fsspec.filesystem("file"), staging_directory, project) + return False, "" - def _initialize(self): - if not os.path.exists(self._storage_root): - self._fs.makedirs(self._storage_root) - # Create a .gitignore file to prevent git from tracking the directory - self._fs.write_text(os.path.join(self._storage_root, ".gitignore"), "*\n") - - def deploy_singularity_container(self, lpath, image_name): - deploy_container_path = self.singularity_image_path(image_name) - should_update = self._should_update(lpath, deploy_container_path) - if should_update: - self._fs.makedirs(os.path.dirname(deploy_container_path), exist_ok=True) - if os.path.exists(deploy_container_path): - os.unlink(deploy_container_path) - self._put_file(lpath, deploy_container_path) - console.log(f"Deployed Singularity container to {deploy_container_path}") - else: - console.log(f"Container {deploy_container_path} exists, skip upload.") - return deploy_container_path +@functools.lru_cache(maxsize=None) +def get_local_artifact_store() -> ArtifactStore: + default = config_lib.default() + settings = default.local_settings() + project = default.project() + filesystem = fsspec.filesystem("file") + storage_root = os.path.abspath(os.path.expanduser(settings.storage_root)) + return ArtifactStore(filesystem, storage_root, project=project) -class RemoteArtifactStore(ArtifactStore): - _fs: SFTPFileSystem - def __init__( - self, - hostname: str, - user: Optional[str], - staging_directory: str, - project: Optional[str] = None, - connect_kwargs: Optional[Mapping[str, Any]] = None, - ): - if connect_kwargs is None: - connect_kwargs = {} - fs = SFTPFileSystem(host=hostname, username=user, **connect_kwargs) # type: ignore - # Normalize the storage root to an absolute path. - self._host = hostname - self._user = user - if not os.path.isabs(staging_directory): - staging_directory = fs.ftp.normalize(staging_directory) - super().__init__(fs, staging_directory, project) - - def deploy_singularity_container(self, lpath, image_name): - assert "/" not in image_name - deploy_container_path = self.singularity_image_path(image_name) - should_update = self._should_update(lpath, deploy_container_path) - if should_update: - with rich.progress.Progress( - rich.progress.TextColumn("[progress.description]{task.description}"), - rich.progress.BarColumn(), - rich.progress.TaskProgressColumn(), - rich.progress.TimeRemainingColumn(), - rich.progress.TransferSpeedColumn(), - console=console, - ) as progress: - self._fs.makedirs(os.path.dirname(deploy_container_path), exist_ok=True) - - task = progress.add_task(f"Uploading {os.path.basename(lpath)}") - - def callback(transferred_bytes: int, total_bytes: int): - progress.update( - task, completed=transferred_bytes, total=total_bytes - ) - - self._fs.ftp.put( - lpath, - deploy_container_path, - callback=callback, - confirm=True, - ) - progress.update(task, description="Done!") +@functools.lru_cache(maxsize=None) +def get_cluster_artifact_store() -> ArtifactStore: + default = config_lib.default() + settings = default.cluster_settings() + project = default.project() - else: - console.log(f"Container {deploy_container_path} exists, skip upload.") - return deploy_container_path - - -def create_artifact_store( - storage_root: str, - hostname: Optional[str] = None, - user: Optional[str] = None, - project: Optional[str] = None, - connect_kwargs=None, -) -> ArtifactStore: - if hostname is None: - return LocalArtifactStore(storage_root, project=project) + if settings.hostname is None: + filesystem = fsspec.filesystem("file") + storage_root = os.path.abspath(os.path.expanduser(settings.storage_root)) else: - return RemoteArtifactStore( - hostname, user, storage_root, project=project, connect_kwargs=connect_kwargs + filesystem = sftp.SFTPFileSystem( + host=settings.hostname, username=settings.user, **settings.ssh_config ) + storage_root = filesystem.ftp.normalize(settings.storage_root) + + return ArtifactStore(filesystem, staging_directory=storage_root, project=project) diff --git a/lxm3/xm_cluster/config.py b/lxm3/xm_cluster/config.py index d33bd6b..d2f288e 100644 --- a/lxm3/xm_cluster/config.py +++ b/lxm3/xm_cluster/config.py @@ -1,6 +1,6 @@ import functools import os -from typing import Any, Dict, Optional, Protocol +from typing import Any, Optional, Protocol import appdirs import tomlkit @@ -19,24 +19,10 @@ def __init__(self, data) -> None: def __repr__(self) -> str: return repr(self._data) - @property - def binds(self) -> Dict[str, str]: - binds = self._data.get("binds", []) - return {bind["src"]: bind["dest"] for bind in binds} - - @property - def env(self) -> Dict[str, str]: - return self._data.get("env", {}) - class ExecutionSettings(Protocol): @property - def env(self) -> Dict[str, str]: - ... - - @property - def singularity(self) -> SingularitySettings: - ... + def singularity(self) -> SingularitySettings: ... class LocalSettings(ExecutionSettings): @@ -50,10 +36,6 @@ def __repr__(self) -> str: def storage_root(self) -> str: return self._data["storage"]["staging"] - @property - def env(self) -> Dict[str, str]: - return self._data.get("env", {}) - @property def singularity(self) -> SingularitySettings: return SingularitySettings(self._data.get("singularity", {})) @@ -97,10 +79,6 @@ def ssh_config(self): return connect_kwargs - @property - def env(self) -> Dict[str, str]: - return self._data.get("env", {}) - @property def singularity(self) -> SingularitySettings: return SingularitySettings(self._data.get("singularity", {})) @@ -139,6 +117,11 @@ def local_settings(self) -> LocalSettings: def default_cluster(self) -> str: cluster = os.environ.get("LXM_CLUSTER", None) if cluster is None: + if not self._data.get("clusters", None): + raise ValueError( + "No cluster has been configured. You should create a configuration file " + "in order to use the cluster execution backends." + ) cluster = self._data["clusters"][0]["name"] return cluster @@ -150,6 +133,16 @@ def cluster_settings(self) -> ClusterSettings: cluster_config = clusters[location] return ClusterSettings(cluster_config) + @classmethod + def default(cls): + return cls( + { + "project": None, + "local": {"storage": {"staging": os.path.join(os.getcwd(), ".lxm")}}, + "clusters": [], + } + ) + @functools.lru_cache() def default() -> Config: @@ -174,4 +167,7 @@ def default() -> Config: logging.debug("Loading config from %s", cwd_path) return Config.from_file(user_config_path) else: - raise ValueError("Unable to load Config") + logging.info( + "Failed to load configuration. Creating a default configuration for local execution." + ) + return Config.default() diff --git a/lxm3/xm_cluster/console.py b/lxm3/xm_cluster/console.py index a9463af..826e3a5 100644 --- a/lxm3/xm_cluster/console.py +++ b/lxm3/xm_cluster/console.py @@ -1,3 +1,23 @@ +import contextlib + from rich.console import Console -console = Console() +console = Console(highlight=False, soft_wrap=True) + + +def info(message: str, **kwargs): + console.print(f"[cyan]INFO[/] {message}", **kwargs) + + +def error(message: str, **kwargs): + console.print(f"[red]ERROR[/] {message}", **kwargs) + + +@contextlib.contextmanager +def status(message: str): + status = console.status(message) + try: + status.start() + yield + finally: + status.stop() diff --git a/lxm3/xm_cluster/executables.py b/lxm3/xm_cluster/executables.py index 8a7abf4..d73306a 100644 --- a/lxm3/xm_cluster/executables.py +++ b/lxm3/xm_cluster/executables.py @@ -6,7 +6,7 @@ @attr.s(auto_attribs=True) -class Command(xm.Executable): +class AppBundle(xm.Executable): entrypoint_command: str resource_uri: str args: xm.SequentialArgs = attr.Factory(xm.SequentialArgs) diff --git a/lxm3/xm_cluster/execution/gridengine.py b/lxm3/xm_cluster/execution/gridengine.py index b75d7c9..b2d72b3 100644 --- a/lxm3/xm_cluster/execution/gridengine.py +++ b/lxm3/xm_cluster/execution/gridengine.py @@ -10,22 +10,22 @@ from lxm3.xm_cluster import array_job from lxm3.xm_cluster import artifacts from lxm3.xm_cluster import config as config_lib +from lxm3.xm_cluster import console from lxm3.xm_cluster import executables from lxm3.xm_cluster import executors from lxm3.xm_cluster import requirements as cluster_requirements -from lxm3.xm_cluster.console import console from lxm3.xm_cluster.execution import job_script_builder class GridEngineJobScriptBuilder( job_script_builder.JobScriptBuilder[executors.GridEngine] ): - TASK_OFFSET = 1 + ARRAY_TASK_ID = "SGE_TASK_ID" + ARRAY_TASK_OFFSET = 1 JOB_SCRIPT_SHEBANG = "#!/usr/bin/env bash" - TASK_ID_VAR_NAME = "SGE_TASK_ID" JOB_ENV_PATTERN = "^(JOB_|SGE_|PE|NSLOTS|NHOSTS)" - executable_cls = executables.Command + executable_cls = executables.AppBundle executor_cls = executors.GridEngine @classmethod @@ -38,27 +38,27 @@ def _is_gpu_requested(cls, executor: executors.GridEngine) -> bool: ) @classmethod - def _create_setup_cmds( - cls, executable: executables.Command, executor: executors.GridEngine + def _create_job_script_prologue( + cls, executable: executables.AppBundle, executor: executors.GridEngine ) -> str: - cmds = ["echo >&2 INFO[$(basename $0)]: Running on host $(hostname)"] + cmds = ['echo >&2 "INFO[$(basename "$0")]: Running on host $(hostname)"'] for module in executor.modules: cmds.append(f"module load {module}") if cls._is_gpu_requested(executor): cmds.append( - "echo >&2 INFO[$(basename $0)]: CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES" + 'echo >&2 "INFO[$(basename "$0")]: CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES"' ) cmds.append("nvidia-smi") if executable.singularity_image is not None: cmds.append( - "echo >&2 INFO[$(basename $0)]: Singularity version: $(singularity --version)" + 'echo >&2 "INFO[$(basename "$0")]: Singularity version: $(singularity --version)"' ) return "\n".join(cmds) @classmethod - def _create_job_header( + def _create_job_script_header( cls, executor: executors.GridEngine, num_array_tasks: Optional[int], @@ -71,7 +71,7 @@ def _create_job_header( return job_header def build( - self, job: job_script_builder.ClusterJob, job_name: str, job_log_dir: str + self, job: job_script_builder.JobType, job_name: str, job_log_dir: str ) -> str: assert isinstance(job.executor, self.executor_cls) assert isinstance(job.executable, self.executable_cls) @@ -104,38 +104,26 @@ class GridEngineClient: def __init__( self, - settings: Optional[config_lib.ClusterSettings] = None, - artifact_store: Optional[artifacts.ArtifactStore] = None, + settings: config_lib.ClusterSettings, + artifact_store: artifacts.ArtifactStore, ) -> None: - if settings is None: - settings = config_lib.default().cluster_settings() self._settings = settings - - if artifact_store is None: - project = config_lib.default().project() - artifact_store = artifacts.create_artifact_store( - settings.storage_root, - hostname=settings.hostname, - user=settings.user, - project=project, - connect_kwargs=settings.ssh_config, - ) self._artifact_store = artifact_store self._cluster = gridengine.GridEngineCluster( self._settings.hostname, self._settings.user ) - def launch(self, job_name: str, job: job_script_builder.ClusterJob): + def launch(self, job_name: str, job: job_script_builder.JobType): job_name = re.sub("\\W", "_", job_name) - job_script_dir = self._artifact_store.job_path(job_name) - job_log_dir = self._artifact_store.job_log_path(job_name) + job_log_dir = job_script_builder.job_log_path(job_name) + self._artifact_store.ensure_dir(job_log_dir) + job_log_dir = self._artifact_store.normalize_path(job_log_dir) + builder = self.builder_cls(self._settings) job_script_content = builder.build(job, job_name, job_log_dir) - - self._artifact_store.deploy_job_scripts(job_name, job_script_content) - job_script_path = os.path.join( - job_script_dir, job_script_builder.JOB_SCRIPT_NAME + job_script_path = self._artifact_store.put_text( + job_script_content, job_script_builder.job_script_path(job_name) ) if isinstance(job, array_job.ArrayJob): @@ -143,33 +131,39 @@ def launch(self, job_name: str, job: job_script_builder.ClusterJob): else: num_jobs = 1 - console.log(f"Launching {num_jobs} job on {self._settings.hostname}") - console.log(f"Launch with command:\n qsub {job_script_path}") + console.info( + f"Launching {num_jobs} job on {self._settings.hostname} with [cyan bold dim]qsub {job_script_path}[/]" + ) group = self._cluster.launch(job_script_path) - console.log(f"Successfully launched job {group.group(0)}") - console.log(f"Logs are saved in {job_log_dir}") handles = [ GridEngineHandle(job_id) for job_id in gridengine.split_job_ids(group) ] - self._save_job_id(job_script_path, group.group(0)) + self._save_job_id(job_name, group.group(0)) + + console.info( + f"""\ +Successfully launched job [green bold]{group.group(0)}[/] + - Saved job id in [dim]{os.path.dirname(job_script_path)}/job_id[/] + - Find job logs in [dim]{job_log_dir}""" + ) return handles - def _save_job_id(self, job_script_path: str, job_id: str): - self._artifact_store._fs.write_text( - os.path.join(os.path.dirname(job_script_path), "job_id"), f"{job_id}\n" - ) + def _save_job_id(self, job_name: str, job_id: str): + self._artifact_store.put_text(job_id, f"jobs/{job_name}/job_id") @functools.lru_cache() def client(): - return GridEngineClient() + settings = config_lib.default().cluster_settings() + artifact_store = artifacts.get_cluster_artifact_store() + return GridEngineClient(settings, artifact_store) async def launch( - job_name: str, job: job_script_builder.ClusterJob + job_name: str, job: job_script_builder.JobType ) -> List[GridEngineHandle]: if isinstance(job, array_job.ArrayJob): jobs = [job] # type: ignore diff --git a/lxm3/xm_cluster/execution/job_script_builder.py b/lxm3/xm_cluster/execution/job_script_builder.py index 8a51891..e817278 100644 --- a/lxm3/xm_cluster/execution/job_script_builder.py +++ b/lxm3/xm_cluster/execution/job_script_builder.py @@ -1,106 +1,32 @@ import abc import collections +import os import shlex -from typing import Dict, Generic, List, Optional, TypedDict, TypeVar, Union, cast +import textwrap +from typing import Dict, Generic, List, Optional, TypeVar, Union, cast + +import attr from lxm3 import xm -from lxm3._vendor.xmanager.xm import job_blocks from lxm3.xm_cluster import array_job from lxm3.xm_cluster import config as config_lib from lxm3.xm_cluster import executables from lxm3.xm_cluster import executors -JOB_SCRIPT_NAME = "job.sh" -CONTAINER_WORKDIR = "/run/task" - -JOB_PARAM_NAME = "job-param.sh" -CONTAINER_JOB_PARAM_PATH = f"/etc/{JOB_PARAM_NAME}" - -LXM_WORK_DIR = "LXM_WORKDIR" -LXM_TASK_ID = "LXM_TASK_ID" - -ClusterJob = Union[xm.Job, array_job.ArrayJob] - - -class _WorkItem(TypedDict): - args: List[str] - env_vars: Dict[str, str] - - -ExecutorClsType = TypeVar("ExecutorClsType") - - -def create_singularity_command( - *, - image: str, - args: List[str], - env_vars: Dict[str, str], - options: List[str], - binds: List[str], - use_gpu: bool, - pwd: str, -) -> List[str]: - cmd = ["singularity", "exec"] - - for bind in binds: - cmd.extend([f"--bind={bind}"]) - - for key, value in env_vars.items(): - cmd.extend(["--env", f"{key}={value}"]) - - cmd.extend(options) - - if use_gpu: - cmd.append("--nv") - - cmd.extend(["--pwd", pwd]) - - cmd.extend([image, *args]) - - return cmd - - -def create_docker_command( - image: str, - args: List[str], - env_vars: Dict[str, str], - binds: List[str], - options: List[str], - workdir: str, - use_gpu: bool, -) -> List[str]: - cmd = ["docker", "run", "--rm"] - - for bind in binds: - cmd.extend(["--mount", bind]) - - cmd.extend(list(options)) +JobType = Union[xm.Job, array_job.ArrayJob] +ExecutorType = TypeVar("ExecutorType") - for k, v in env_vars.items(): - cmd.extend(["--env", f"{k}={v}"]) - - if use_gpu: - cmd.extend( - [ - "--runtime=nvidia", - "--env", - '"NVIDIA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-all}"', - ] - ) - - cmd.extend(["--workdir", workdir]) - - cmd.extend([image, *args]) - return cmd - - -class JobScriptBuilder(abc.ABC, Generic[ExecutorClsType]): - TASK_OFFSET: int +class JobScriptBuilder(abc.ABC, Generic[ExecutorType]): + ARRAY_TASK_ID: str + ARRAY_TASK_OFFSET: int JOB_SCRIPT_SHEBANG: str = "#!/usr/bin/env bash" - TASK_ID_VAR_NAME: str JOB_ENV_PATTERN = None + CONTAINER_WORKDIR: str = "/run/lxm3/workdir" + JOB_PARAM_NAME: str = "job-param.sh" + CONTAINER_JOB_PARAM_PATH: str = f"/tmp/{JOB_PARAM_NAME}" + def __init__( self, settings: Optional[config_lib.ExecutionSettings] = None, @@ -109,9 +35,9 @@ def __init__( @classmethod @abc.abstractmethod - def _create_job_header( + def _create_job_script_header( cls, - executor: ExecutorClsType, + executor: ExecutorType, num_array_tasks: Optional[int], job_log_dir: str, job_name: str, @@ -120,304 +46,361 @@ def _create_job_header( @classmethod @abc.abstractmethod - def _is_gpu_requested(cls, executor: ExecutorClsType) -> bool: - """Is GPU requested?""" + def _is_gpu_requested(cls, executor: ExecutorType) -> bool: + """Infer GPU resource from executor""" @classmethod @abc.abstractmethod - def _create_setup_cmds(cls, executable, executor: ExecutorClsType) -> str: + def _create_job_script_prologue( + cls, + executable: executables.AppBundle, + executor: ExecutorType, + ) -> str: """Generate backend specific setup commands""" - @staticmethod - def _get_additional_env(env_vars, parent_env): - env = {} - for k, v in parent_env.items(): - if k not in env_vars: - env[k] = v - return env - - @staticmethod - def _get_additional_binds(bind, singularity_additional_binds): - updates = {} - for src, dst in singularity_additional_binds.items(): - if dst not in bind.values(): - updates[src] = dst - return updates - - @staticmethod - def _create_env_vars(env_vars_list: List[Dict[str, str]]) -> str: - """Create the env_vars list.""" - lines = [] - first_keys = set(env_vars_list[0].keys()) - if not first_keys: - return "" - - # Find out keys that are common to all environment variables - var_to_values = collections.defaultdict(list) - for env in env_vars_list: - for k, v in env.items(): - var_to_values[k].append(v) - - common_keys = [] - for k, v in var_to_values.items(): - if len(set(v)) == 1: - common_keys.append(k) - common_keys = sorted(common_keys) - - for env_vars in env_vars_list: - if first_keys != set(env_vars.keys()): - raise ValueError( - "Expect all environment variables to have the same keys" - ) - - # Generate shared environment variables - for k in sorted(common_keys): - lines.append( - 'export {key}="{value}"'.format(key=k, value=env_vars_list[0][k]) - ) + def _create_job_args_script(self, job: JobType) -> str: + executable = job.executable + assert isinstance(executable, executables.AppBundle) + if isinstance(job, xm.Job): + args = xm.merge_args(executable.args, job.args).to_list() + env = {**executable.env_vars, **job.env_vars} + return _create_job_args_script(args, env) + elif isinstance(job, array_job.ArrayJob): + args = [ + xm.merge_args(executable.args, per_task_args).to_list() + for per_task_args in job.args + ] + env = [ + {**executable.env_vars, **per_task_envs} + for per_task_envs in job.env_vars + ] - for key in first_keys: - if key in common_keys: - continue - for task_id, env_vars in enumerate(env_vars_list): - lines.append( - '{key}_{task_id}="{value}"'.format( - key=key, task_id=task_id, value=env_vars[key] - ) - ) - lines.append( - '{key}=$(eval echo \\$"{key}_${lxm_task_id}")'.format( - key=key, lxm_task_id=LXM_TASK_ID - ) + return _create_array_job_args_script( + args, env, self.ARRAY_TASK_ID, self.ARRAY_TASK_OFFSET ) - lines.append("export {key}".format(key=key)) - content = "\n".join(lines) - return content - - @staticmethod - def _create_args(args_list: List[List[str]]) -> str: - """Create the args list.""" - if not args_list: - return "" - lines = [] - for task_id, args in enumerate(args_list): - args_str = " ".join([a for a in args]) - lines.append( - 'TASK_CMD_ARGS_{task_id}="{args_str}"'.format( - task_id=task_id, args_str=args_str - ) - ) - lines.append(f'TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_${LXM_TASK_ID}")') - lines.append("eval set -- $TASK_CMD_ARGS") - content = "\n".join(lines) - return content - - @staticmethod - def _create_work_list(job: Union[xm.Job, array_job.ArrayJob]) -> List[_WorkItem]: - work_list: List[_WorkItem] = [] - executable = cast(executables.Command, job.executable) - if isinstance(job, array_job.ArrayJob): - jobs = array_job.flatten_array_job(job) else: - jobs = [job] - for job in jobs: - work_list.append( - { - "args": job_blocks.merge_args(executable.args, job.args).to_list( - xm.utils.ARG_ESCAPER - ), - "env_vars": {**executable.env_vars, **job.env_vars}, - } - ) - return work_list + raise ValueError(f"{type(job)} is not supported") - def _create_array_script(self, job) -> str: + def _create_install_commands(self, job: JobType, install_dir: str) -> str: executable = job.executable - singularity_image = executable.singularity_image - - if self._settings is not None: - env_overrides = self._settings.env - singularity_env_overrides = self._settings.env - else: - env_overrides = {} - singularity_env_overrides = {} - - work_list = self._create_work_list(job) - for work_item in work_list: - if singularity_image is not None: - work_item["env_vars"].update( - self._get_additional_env( - work_item["env_vars"], singularity_env_overrides - ) - ) - work_item["env_vars"].update( - self._get_additional_env(work_item["env_vars"], env_overrides) + assert isinstance(executable, executables.AppBundle) + job_args_script = self._create_job_args_script(job) + if self.JOB_ENV_PATTERN: + export_env_file_cmds = ( + f"printenv | {{ grep -E '{self.JOB_ENV_PATTERN}' || :; }} > " + f'"{install_dir}/.environment"' ) - return f""" -{self._create_env_vars([work["env_vars"] for work in work_list])} -{self._create_args([work["args"] for work in work_list])} -""" - - def build( - self, - job: Union[xm.Job, array_job.ArrayJob], - job_name: str, - job_log_dir: str, - ) -> str: - setup = self._create_setup_cmds(job.executable, job.executor) - - num_array_tasks = None - if isinstance(job, array_job.ArrayJob) and len(job.args) > 1: - num_array_tasks = len(job.args) - header = self._create_job_header( - job.executor, num_array_tasks, job_log_dir, job_name + else: + export_env_file_cmds = f'touch "{install_dir}/.environment"' + extract_pkg_cmds = _get_extract_command(executable.resource_uri, install_dir) + save_job_args_cmds = f"cat <<'EOF' > \"{install_dir}/{self.JOB_PARAM_NAME}\"\n{job_args_script}\nEOF" + return "\n".join( + [ + extract_pkg_cmds, + save_job_args_cmds, + export_env_file_cmds, + ] ) + def _create_entrypoint_commands(self, job: JobType, install_dir: str) -> str: executable = job.executable - if not isinstance(executable, executables.Command): + if not isinstance(executable, executables.AppBundle): raise ValueError("Only Command executable is supported") executor = cast( Union[executors.Local, executors.GridEngine, executors.Slurm], job.executor ) - num_tasks = len(job.args) if isinstance(job, array_job.ArrayJob) else 1 - array_script = self._create_array_script(job) - - def _rewrite_array_job_command(array_scrpt_path: str, cmd: str): - return ["sh", "-c", shlex.quote(f'. {array_scrpt_path}; {cmd} "$@"')] - if executable.singularity_image is not None: - if executor.singularity_options is not None: - binds = {**executor.singularity_options.bind} - options = [*executor.singularity_options.extra_options] - else: - binds = {} - options = [] - - if self._settings is not None: - binds.update( - self._get_additional_binds(binds, self._settings.singularity.binds) + if executable.singularity_image or executable.docker_image: + if executable.docker_image and executable.singularity_image: + raise ValueError("Only docker or singularity image should be used.") + if executable.singularity_image: + image = executable.singularity_image + get_container_cmd = create_singularity_command + singularity_options = ( + executor.singularity_options or executors.SingularityOptions() ) - - binds[f'"${LXM_WORK_DIR}"'] = CONTAINER_WORKDIR - binds[ - f'"${LXM_WORK_DIR}"/{JOB_PARAM_NAME}' - ] = f"{CONTAINER_JOB_PARAM_PATH}:ro" - env_vars = {LXM_TASK_ID: f'"${LXM_TASK_ID}"'} - - entrypoint = create_singularity_command( - image=executable.singularity_image, - args=_rewrite_array_job_command( - CONTAINER_JOB_PARAM_PATH, executable.entrypoint_command - ), - binds=[f"{k}:{v}" for k, v in binds.items()], - env_vars=env_vars, - options=options, - pwd=CONTAINER_WORKDIR, - use_gpu=self._is_gpu_requested(executor), - ) - - elif executable.docker_image is not None: - if executor.docker_options is not None: - binds = {**executor.docker_options.volumes} - options = [*executor.docker_options.extra_options] + bind_mounts = [ + BindMount(src, dst) for src, dst in singularity_options.bind + ] + runtime_options = [*singularity_options.extra_options] + elif executable.docker_image: + image = executable.docker_image + get_container_cmd = create_docker_command + docker_options = executor.docker_options or executors.DockerOptions() + bind_mounts = [ + BindMount(src, dst) for src, dst in docker_options.volumes + ] + runtime_options = [*docker_options.extra_options] else: - binds = {} - options = [] - - mounts = [f"type=bind,source={k},target={v}" for k, v in binds.items()] - mounts.append( - f'type=bind,source="${LXM_WORK_DIR}",target={CONTAINER_WORKDIR}' - ) - mounts.append( - f'type=bind,source="${LXM_WORK_DIR}"/{JOB_PARAM_NAME},target={CONTAINER_JOB_PARAM_PATH},readonly' + assert False + + bind_mounts.extend( + [ + BindMount(install_dir, self.CONTAINER_WORKDIR), + BindMount( + f"{install_dir}/{self.JOB_PARAM_NAME}", + f"{self.CONTAINER_JOB_PARAM_PATH}", + read_only=True, + ), + ] ) - env_vars = {f"{LXM_TASK_ID}": f'"${LXM_TASK_ID}"'} - entrypoint = create_docker_command( - image=executable.docker_image, + entrypoint = get_container_cmd( + image=image, args=_rewrite_array_job_command( - CONTAINER_JOB_PARAM_PATH, executable.entrypoint_command + self.CONTAINER_JOB_PARAM_PATH, executable.entrypoint_command ), - env_vars=env_vars, - binds=mounts, - options=options, - workdir=CONTAINER_WORKDIR, + bind_mounts=bind_mounts, + env_vars={}, + options=runtime_options, + working_dir=self.CONTAINER_WORKDIR, use_gpu=self._is_gpu_requested(executor), + env_file=f"{install_dir}/.environment", ) else: entrypoint = _rewrite_array_job_command( - f"./{JOB_PARAM_NAME}", executable.entrypoint_command + f"./{self.JOB_PARAM_NAME}", executable.entrypoint_command ) - cmds = """\ -TASK_OFFSET=%(task_offset)s -TASK_INDEX_NAME="%(task_index_name)s" -NUM_TASKS="%(num_tasks)s" - -if [ $NUM_TASKS -eq 1 ]; then - # If there is only one task, then we don't need to use the task index - %(lxm_task_id)s=0 -else - %(lxm_task_id)s=$(($(eval echo \\$"TASK_INDEX_NAME") - $TASK_OFFSET)) -fi -export LXM_TASK_ID -cat <<'EOF' > "$%(lxm_work_dir)s"/%(job_param_name)s -%(array_script)s -EOF -chmod +x "$%(lxm_work_dir)s"/%(job_param_name)s -%(entrypoint)s -""" % { - "task_offset": self.TASK_OFFSET, - "task_index_name": self.TASK_ID_VAR_NAME, - "num_tasks": num_tasks, - "entrypoint": " ".join(entrypoint), - "array_script": array_script, - "job_param_name": JOB_PARAM_NAME, - "lxm_work_dir": LXM_WORK_DIR, - "lxm_task_id": LXM_TASK_ID, + return " ".join(entrypoint) + + def build( + self, job: Union[xm.Job, array_job.ArrayJob], job_name: str, job_log_dir: str + ) -> str: + executable = job.executable + if not isinstance(executable, executables.AppBundle): + raise TypeError("Only AppBundle is supported") + executor = job.executor + + num_array_tasks = None + if isinstance(job, array_job.ArrayJob): + num_array_tasks = len(job.args) + + header = self._create_job_script_header( + executor, num_array_tasks, job_log_dir, job_name + ) + prologue = self._create_job_script_prologue(executable, job.executor) + install_dir = "$LXM_WORKDIR" + install_cmds = self._create_install_commands(job, install_dir) + entrypoint_cmds = self._create_entrypoint_commands(job, install_dir) + return _JOB_SCRIPT_TEMPLATE % { + "shebang": self.JOB_SCRIPT_SHEBANG, + "header": header, + "install": install_cmds, + "prologue": prologue, + "entrypoint": entrypoint_cmds, } - job_script_content = """\ + +_JOB_SCRIPT_TEMPLATE = """\ %(shebang)s %(header)s -%(setup)s set -e -%(lxm_work_dir)s=$(mktemp -d) -# Extract archives -ARCHIVES="%(archives)s" -for ar in $ARCHIVES; do - case $ar in - *.zip) - unzip -q -d $LXM_WORKDIR $ar - ;; - *.tar) - tar -C $LXM_WORKDIR -xf $ar - ;; - *.tar.gz|*.tgz) - tar -C $LXM_WORKDIR -xzf $ar - ;; - *) - _error "Unsupported archive format: $ar" - ;; - esac -done +LXM_WORKDIR="$(mktemp -d)" cleanup() { - echo >& 2 "DEBUG[$(basename $0)] Cleaning up $%(lxm_work_dir)s" - rm -rf $%(lxm_work_dir)s + echo >& 2 "DEBUG[$(basename "$0")] Cleaning up $LXM_WORKDIR" + rm -rf "$LXM_WORKDIR" } trap cleanup EXIT +cd "$LXM_WORKDIR" +%(install)s -cd $%(lxm_work_dir)s +%(prologue)s %(entrypoint)s -""" % { - "shebang": self.JOB_SCRIPT_SHEBANG, - "header": header, - "setup": setup, - "entrypoint": cmds, - "archives": " ".join([executable.resource_uri]), - "lxm_work_dir": LXM_WORK_DIR, - } - return job_script_content +""" + + +@attr.s(auto_attribs=True) +class BindMount: + path: str + mount_path: str + read_only: bool = False + + +def create_singularity_command( + *, + image: str, + args: List[str], + env_vars: Dict[str, str], + options: List[str], + bind_mounts: List[BindMount], + use_gpu: bool, + working_dir: str, + env_file: str, +) -> List[str]: + cmd = ["singularity", "exec"] + + for mount in bind_mounts: + bm = f"{mount.path}:{mount.mount_path}" + if mount.read_only: + bm += ":ro" + cmd.append(f'--bind="{bm}"') + + for key, value in env_vars.items(): + cmd.append(f'--env={key}="{value}"') + + cmd.extend(options) + + if use_gpu: + cmd.append("--nv") + + cmd.append(f'--pwd="{working_dir}"') + cmd.append(f'--env-file="{env_file}"') + + cmd.extend([image, *args]) + + return cmd + + +def create_docker_command( + *, + image: str, + args: List[str], + env_vars: Dict[str, str], + bind_mounts: List[BindMount], + options: List[str], + working_dir: str, + use_gpu: bool, + env_file: str, +) -> List[str]: + cmd = ["docker", "run", "--rm"] + + for mount in bind_mounts: + mount_spec = f"type=bind,source={mount.path},target={mount.mount_path}" + if mount.read_only: + mount_spec += ",readonly" + cmd.extend([f'--mount="{mount_spec}"']) + + for k, v in env_vars.items(): + cmd.append(f'--env={k}="{v}"') + + if use_gpu: + cmd.extend( + [ + "--runtime=nvidia", + '--env=NVIDIA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-all}"', + ] + ) + + cmd.extend(options) + cmd.append(f'--workdir="{working_dir}"') + cmd.append(f'--env-file="{env_file}"') + cmd.extend([image, *args]) + + return cmd + + +def _rewrite_array_job_command(array_script_path: str, cmd: str) -> List[str]: + return ["sh", "-c", shlex.quote(f'. {array_script_path}; {cmd} "$@"')] + + +def _get_extract_command(archive: str, directory: str) -> str: + if archive.endswith(".zip"): + return f'unzip -q -d "{directory}" "{archive}"' + elif archive.endswith(".tar"): + return f'tar -C "{directory}" -xf "{archive}"' + elif archive.endswith((".tar.gz", ".tgz")): + return f'tar -C "{directory}" -xzf "{archive}"' + else: + raise ValueError(archive) + + +def _create_job_args_script(args: List[str], env: Dict[str, str]) -> str: + return "\n".join( + [ + "export LXM_TASK_ID=0", + _create_env_vars([env], "LXM_TASK_ID", 0), + _create_args([args], "LXM_TASK_ID", 0), + ] + ) + + +def _create_array_job_args_script( + args: List[List[str]], + env: List[Dict[str, str]], + index_name: str, + index_offset: int, +) -> str: + return "\n".join( + [ + textwrap.dedent( + f"""\ + if [ -z ${{{index_name}+x}} ]; + then + echo >&2 "ERROR[$0]: \\${index_name} is not set." + exit 2 + fi""" + ), + _create_env_vars(env, index_name, index_offset), + _create_args(args, index_name, index_offset), + ] + ) + + +def _create_env_vars( + env_vars_list: List[Dict[str, str]], index_name: str, index_offset: int +) -> str: + """Create the env_vars list.""" + lines = [] + first_keys = set(env_vars_list[0].keys()) + if not first_keys: + return "" + + # Find out keys that are common to all environment variables + var_to_values = collections.defaultdict(list) + for env in env_vars_list: + for k, v in env.items(): + var_to_values[k].append(v) + + common_keys = [] + for k, v in var_to_values.items(): + if len(set(v)) == 1: + common_keys.append(k) + common_keys = sorted(common_keys) + + for env_vars in env_vars_list: + if first_keys != set(env_vars.keys()): + raise ValueError("Expect all environment variables to have the same keys") + + # Generate shared environment variables + for k in sorted(common_keys): + lines.append(f'export {k}="{env_vars_list[0][k]}"') + + for key in first_keys: + if key in common_keys: + continue + for task_id, env_vars in enumerate(env_vars_list, start=index_offset): + lines.append(f'{key}_{task_id}="{env_vars[key]}"') + lines.append(f'{key}=$(eval echo \\$"{key}_${index_name}")') + lines.append(f"export {key}") + content = "\n".join(lines) + return content + + +def _create_args(args_list: List[List[str]], index_name: str, index_offset: int) -> str: + """Create the args list.""" + if not args_list: + return "" + lines = [] + for task_id, args in enumerate(args_list, start=index_offset): + args_str = " ".join([a for a in args]) + lines.append(f'TASK_CMD_ARGS_{task_id}="{args_str}"') + lines.append(f'TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_${index_name}")') + lines.append("eval set -- $TASK_CMD_ARGS") + content = "\n".join(lines) + return content + + +def job_path(job_name: str): + return os.path.join("jobs", job_name) + + +def job_script_path(job_name: str): + return os.path.join(job_path(job_name), "job.sh") + + +def job_log_path(job_name: str): + return os.path.join("logs", job_name) diff --git a/lxm3/xm_cluster/execution/local.py b/lxm3/xm_cluster/execution/local.py index 95398a5..790bb25 100644 --- a/lxm3/xm_cluster/execution/local.py +++ b/lxm3/xm_cluster/execution/local.py @@ -14,9 +14,9 @@ from lxm3.xm_cluster import array_job as array_job_lib from lxm3.xm_cluster import artifacts from lxm3.xm_cluster import config as config_lib +from lxm3.xm_cluster import console from lxm3.xm_cluster import executables from lxm3.xm_cluster import executors -from lxm3.xm_cluster.console import console from lxm3.xm_cluster.execution import job_script_builder _LOCAL_EXECUTOR: Optional[concurrent.futures.ThreadPoolExecutor] = None @@ -27,7 +27,7 @@ def local_executor(): if _LOCAL_EXECUTOR is None: _LOCAL_EXECUTOR = concurrent.futures.ThreadPoolExecutor(1) - def shutdown(): + def shutdown(*args, **kwargs): if _LOCAL_EXECUTOR is not None: logging.debug("Shutting down local executor...") _LOCAL_EXECUTOR.shutdown() @@ -37,29 +37,30 @@ def shutdown(): class LocalJobScriptBuilder(job_script_builder.JobScriptBuilder[executors.Local]): - TASK_OFFSET = 1 + ARRAY_TASK_ID = "LOCAL_TASK_ID" + ARRAY_TASK_OFFSET = 1 JOB_SCRIPT_SHEBANG = "#!/usr/bin/env bash" - TASK_ID_VAR_NAME = "LOCAL_TASK_ID" + JOB_ENV_PATTERN = "^(LOCAL_TASK_ID)" @classmethod def _is_gpu_requested(cls, executor: executors.Local) -> bool: return shutil.which("nvidia-smi") is not None @classmethod - def _create_setup_cmds( - cls, executable: executables.Command, executor: executors.Local + def _create_job_script_prologue( + cls, executable: executables.AppBundle, executor: executors.Local ) -> str: del executor - cmds = ["echo >&2 INFO[$(basename $0)]: Running on host $(hostname)"] + cmds = ['echo >&2 "INFO[$(basename "$0")]: Running on host $(hostname)"'] if executable.singularity_image is not None: cmds.append( - "echo >&2 INFO[$(basename $0)]: Singularity version: $(singularity --version)" + 'echo >&2 "INFO[$(basename "$0")]: Singularity version: $(singularity --version)"' ) return "\n".join(cmds) @classmethod - def _create_job_header( + def _create_job_script_header( cls, executor: executors.Local, num_array_tasks: Optional[int], @@ -70,10 +71,10 @@ def _create_job_header( return "" def build( - self, job: job_script_builder.ClusterJob, job_name: str, job_log_dir: str + self, job: job_script_builder.JobType, job_name: str, job_log_dir: str ) -> str: assert isinstance(job.executor, executors.Local) - assert isinstance(job.executable, executables.Command) + assert isinstance(job.executable, executables.AppBundle) return super().build(job, job_name, job_log_dir) @@ -102,32 +103,23 @@ class LocalClient: def __init__( self, - settings: Optional[config_lib.LocalSettings] = None, - artifact_store: Optional[artifacts.LocalArtifactStore] = None, + settings: config_lib.LocalSettings, + artifact_store: artifacts.ArtifactStore, ) -> None: - if settings is None: - config = config_lib.default() - settings = config.local_settings() self._settings = settings - - if artifact_store is None: - config = config_lib.default() - artifact_store = artifacts.LocalArtifactStore( - self._settings.storage_root, project=config.project() - ) - self._artifact_store = artifact_store - def launch(self, job_name: str, job: job_script_builder.ClusterJob): + def launch(self, job_name: str, job: job_script_builder.JobType): job_name = re.sub("\\W", "_", job_name) - job_script_dir = self._artifact_store.job_path(job_name) - job_log_dir = self._artifact_store.job_log_path(job_name) + + job_log_dir = job_script_builder.job_log_path(job_name) + self._artifact_store.ensure_dir(job_log_dir) + job_log_dir = self._artifact_store.normalize_path(job_log_dir) + builder = self.builder_cls(self._settings) job_script_content = builder.build(job, job_name, job_log_dir) - - self._artifact_store.deploy_job_scripts(job_name, job_script_content) - job_script_path = os.path.join( - job_script_dir, job_script_builder.JOB_SCRIPT_NAME + job_script_path = self._artifact_store.put_text( + job_script_content, job_script_builder.job_script_path(job_name) ) if isinstance(job, array_job_lib.ArrayJob): @@ -135,19 +127,21 @@ def launch(self, job_name: str, job: job_script_builder.ClusterJob): else: num_jobs = 1 - console.print(f"Launching {num_jobs} jobs locally...") + console.info(f"Launching {num_jobs} jobs locally...") handles = [] for i in range(num_jobs): def task(i): + additional_env = {} + if isinstance(job, array_job_lib.ArrayJob): + additional_env = { + LocalJobScriptBuilder.ARRAY_TASK_ID: str( + i + LocalJobScriptBuilder.ARRAY_TASK_OFFSET + ), + } subprocess.run( ["bash", job_script_path], - env={ - **os.environ, - LocalJobScriptBuilder.TASK_ID_VAR_NAME: str( - i + LocalJobScriptBuilder.TASK_OFFSET - ), - }, + env={**os.environ, **additional_env}, ) future = local_executor().submit(task, i) @@ -158,10 +152,12 @@ def task(i): @functools.lru_cache() def client() -> LocalClient: - return LocalClient() + local_settings = config_lib.default().local_settings() + artifact_store = artifacts.get_local_artifact_store() + return LocalClient(local_settings, artifact_store) -async def launch(job_name: str, job: job_script_builder.ClusterJob): +async def launch(job_name: str, job: job_script_builder.JobType): if isinstance(job, array_job_lib.ArrayJob): jobs = [job] # type: ignore elif isinstance(job, xm.JobGroup): diff --git a/lxm3/xm_cluster/execution/slurm.py b/lxm3/xm_cluster/execution/slurm.py index 1380a05..08f6b10 100644 --- a/lxm3/xm_cluster/execution/slurm.py +++ b/lxm3/xm_cluster/execution/slurm.py @@ -9,16 +9,17 @@ from lxm3.xm_cluster import array_job from lxm3.xm_cluster import artifacts from lxm3.xm_cluster import config as config_lib +from lxm3.xm_cluster import console from lxm3.xm_cluster import executables from lxm3.xm_cluster import executors -from lxm3.xm_cluster.console import console from lxm3.xm_cluster.execution import job_script_builder class SlurmJobScriptBuilder(job_script_builder.JobScriptBuilder[executors.Slurm]): - TASK_OFFSET = 1 + ARRAY_TASK_ID = "SLURM_ARRAY_TASK_ID" + ARRAY_TASK_OFFSET = 1 JOB_SCRIPT_SHEBANG = "#!/usr/bin/bash -l" - TASK_ID_VAR_NAME = "SLURM_ARRAY_TASK_ID" + JOB_ENV_PATTERN = "^(SLURM_)" @classmethod def _is_gpu_requested(cls, executor: executors.Slurm) -> bool: @@ -26,25 +27,25 @@ def _is_gpu_requested(cls, executor: executors.Slurm) -> bool: return True # TODO @classmethod - def _create_setup_cmds(cls, executable, executor: executors.Slurm) -> str: - cmds = ["echo >&2 INFO[$(basename $0)]: Running on host $(hostname)"] + def _create_job_script_prologue(cls, executable, executor: executors.Slurm) -> str: + cmds = ['echo >&2 "INFO[$(basename "$0")]: Running on host $(hostname)"'] for module in executor.modules: cmds.append(f"module load {module}") if cls._is_gpu_requested(executor): cmds.append( - "echo >&2 INFO[$(basename $0)]: CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES" + 'echo >&2 "INFO[$(basename "$0")]: CUDA_VISIBLE_DEVICES=$CUDA_VISIBLE_DEVICES"' ) # cmds.append("nvidia-smi") if executable.singularity_image is not None: cmds.append( - "echo >&2 INFO[$(basename $0)]: Singularity version: $(singularity --version)" + 'echo >&2 "INFO[$(basename "$0")]: Singularity version: $(singularity --version)"' ) return "\n".join(cmds) @classmethod - def _create_job_header( + def _create_job_script_header( cls, executor: executors.Slurm, num_array_tasks: Optional[int], @@ -57,10 +58,10 @@ def _create_job_header( return job_header def build( - self, job: job_script_builder.ClusterJob, job_name: str, job_log_dir: str + self, job: job_script_builder.JobType, job_name: str, job_log_dir: str ) -> str: assert isinstance(job.executor, executors.Slurm) - assert isinstance(job.executable, executables.Command) + assert isinstance(job.executable, executables.AppBundle) return super().build(job, job_name, job_log_dir) @@ -89,72 +90,54 @@ class SlurmClient: def __init__( self, - settings: Optional[config_lib.ClusterSettings] = None, - artifact_store: Optional[artifacts.ArtifactStore] = None, + settings: config_lib.ClusterSettings, + artifact_store: artifacts.ArtifactStore, ) -> None: - if settings is None: - settings = config_lib.default().cluster_settings() self._settings = settings - - if artifact_store is None: - project = config_lib.default().project() - artifact_store = artifacts.create_artifact_store( - settings.storage_root, - hostname=settings.hostname, - user=settings.user, - project=project, - connect_kwargs=settings.ssh_config, - ) self._artifact_store = artifact_store self._cluster = slurm.SlurmCluster( - hostname=self._settings.hostname, - username=self._settings.user, + hostname=self._settings.hostname, username=self._settings.user ) - def launch(self, job_name: str, job: job_script_builder.ClusterJob): + def launch(self, job_name: str, job: job_script_builder.JobType): job_name = re.sub("\\W", "_", job_name) - job_script_dir = self._artifact_store.job_path(job_name) - job_log_dir = self._artifact_store.job_log_path(job_name) + job_log_dir = job_script_builder.job_log_path(job_name) + self._artifact_store.ensure_dir(job_log_dir) + job_log_dir = self._artifact_store.normalize_path(job_log_dir) + builder = self.builder_cls(self._settings) job_script_content = builder.build(job, job_name, job_log_dir) - - self._artifact_store.deploy_job_scripts(job_name, job_script_content) - job_script_path = os.path.join( - job_script_dir, job_script_builder.JOB_SCRIPT_NAME + job_script_path = self._artifact_store.put_text( + job_script_content, job_script_builder.job_script_path(job_name) ) if isinstance(job, array_job.ArrayJob): num_jobs = len(job.env_vars) else: num_jobs = 1 - console.log(f"Launching {num_jobs} job on {self._settings.hostname}") + console.info(f"Launching {num_jobs} job on {self._settings.hostname}") job_id = self._cluster.launch(job_script_path) - console.log(f"Successfully launched job {job_id}") + console.info(f"Successfully launched job {job_id}") + self._artifact_store.put_text(str(job_id), f"jobs/{job_name}/job_id") if num_jobs > 1: job_ids = [f"{job_id}_{i}" for i in range(num_jobs)] else: job_ids = [f"{job_id}"] handles = [SlurmHandle(j) for j in job_ids] - self._save_job_id(job_script_path, str(job_id)) return handles - def _save_job_id(self, job_script_path: str, job_id: str): - self._artifact_store._fs.write_text( - os.path.join(os.path.dirname(job_script_path), "job_id"), f"{job_id}\n" - ) - @functools.lru_cache() def client() -> SlurmClient: - return SlurmClient() + settings = config_lib.default().cluster_settings() + artifact_store = artifacts.get_cluster_artifact_store() + return SlurmClient(settings, artifact_store) -async def launch( - job_name: str, job: job_script_builder.ClusterJob -) -> List[SlurmHandle]: +async def launch(job_name: str, job: job_script_builder.JobType) -> List[SlurmHandle]: if isinstance(job, array_job.ArrayJob): jobs = [job] # type: ignore elif isinstance(job, xm.JobGroup): diff --git a/lxm3/xm_cluster/execution/utils.py b/lxm3/xm_cluster/execution/utils.py deleted file mode 100644 index b25dde0..0000000 --- a/lxm3/xm_cluster/execution/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import subprocess -from typing import List, Optional, Sequence - - -def rsync( - src: str, - dst: str, - opt: List[str], - host: Optional[str] = None, - excludes: Optional[Sequence[str]] = None, - filters: Optional[Sequence[str]] = None, - mkdirs: bool = False, -): - if excludes is None: - excludes = [] - if filters is None: - filters = [] - opt = list(opt) - for exclude in excludes: - opt.append(f"--exclude={exclude}") - for filter in filters: - opt.append(f"--filter=:- {filter}") - if not host: - os.makedirs(os.path.dirname(dst), exist_ok=True) - sync_cmd = ["rsync"] + opt + [src, dst] - subprocess.check_call(sync_cmd) - else: - if mkdirs: - subprocess.check_output(["ssh", host, "mkdir", "-p", os.path.dirname(dst)]) - dst = f"{host}:{dst}" - sync_cmd = ["rsync"] + opt + [src, dst] - subprocess.check_call(sync_cmd) diff --git a/lxm3/xm_cluster/executors.py b/lxm3/xm_cluster/executors.py index fe4750e..16d7b91 100644 --- a/lxm3/xm_cluster/executors.py +++ b/lxm3/xm_cluster/executors.py @@ -8,7 +8,7 @@ def _convert_time( - time: Optional[Union[str, datetime.datetime, int]] + time: Optional[Union[str, datetime.datetime, int]], ) -> Optional[datetime.timedelta]: if time is None: return None diff --git a/lxm3/xm_cluster/experiment.py b/lxm3/xm_cluster/experiment.py index eb735f2..50f8803 100644 --- a/lxm3/xm_cluster/experiment.py +++ b/lxm3/xm_cluster/experiment.py @@ -10,14 +10,15 @@ from lxm3._vendor.xmanager import xm from lxm3._vendor.xmanager.xm import async_packager +from lxm3._vendor.xmanager.xm import core from lxm3._vendor.xmanager.xm import id_predictor from lxm3._vendor.xmanager.xm import job_blocks from lxm3._vendor.xmanager.xm import pattern_matching as pm from lxm3.xm_cluster import array_job as array_job_lib from lxm3.xm_cluster import config as config_lib +from lxm3.xm_cluster import console from lxm3.xm_cluster import metadata from lxm3.xm_cluster import packaging -from lxm3.xm_cluster.console import console from lxm3.xm_cluster.execution import gridengine as gridengine_execution from lxm3.xm_cluster.execution import local as local_execution from lxm3.xm_cluster.execution import slurm as slurm_execution @@ -175,7 +176,7 @@ def _unsupported_aux_units(_): def _wait_for_local_jobs(self, is_exit_abrupt: bool): if self._work_units: if any([wu._local_handles for wu in self._work_units]): - console.print( + console.info( "Waiting for local jobs to complete. " "Press Ctrl+C to terminate them and exit" ) @@ -239,3 +240,12 @@ def create_experiment(experiment_title: str) -> ClusterExperiment: config.set_project(vcs.name) return ClusterExperiment(experiment_title, vcs=vcs) + + +def get_current_experiment(): + try: + return core._current_experiment.get() + except LookupError as e: + raise RuntimeError( + "get_current_experiment requires an experiment context" + ) from e diff --git a/lxm3/xm_cluster/packaging/archive_builder.py b/lxm3/xm_cluster/packaging/archive_builder.py index 562047f..1fb7c99 100644 --- a/lxm3/xm_cluster/packaging/archive_builder.py +++ b/lxm3/xm_cluster/packaging/archive_builder.py @@ -6,8 +6,8 @@ import subprocess import tempfile +from lxm3.xm_cluster import console from lxm3.xm_cluster import executable_specs as cluster_executable_specs -from lxm3.xm_cluster.console import console ENTRYPOINT_SCRIPT = "./entrypoint.sh" @@ -40,9 +40,7 @@ def create_python_archive( resources = py_package.resources with tempfile.TemporaryDirectory() as tmpdir: - with console.status( - "Creating python package archive for {}".format(package_dir) - ): + with console.status(f"Building python package [dim]{package_dir}[/]"): try: subprocess.run( [ @@ -60,8 +58,8 @@ def create_python_archive( ) except subprocess.CalledProcessError as e: if e.stderr: - console.log("Error during packaging, stderr:", style="bold red") - console.log(e.stderr, style="bold red") + console.error("Error during packaging, stderr:") + console.error(e.stderr) raise PackagingError( f"Failed to create python package from {package_dir}" ) from e @@ -84,7 +82,7 @@ def create_python_archive( ) if os.path.exists(os.path.join(tmpdir, "bin")): - console.log('Removing "bin/" directory as these are not yet portable.') + console.info('Removing "bin/" directory as these are not yet portable.') shutil.rmtree(os.path.join(tmpdir, "bin")) for f in glob.glob(os.path.join(tmpdir, "*.dist-info")): @@ -105,9 +103,7 @@ def create_python_archive( verbose=True, ) - console.log( - f"Created archive: [repr.path]{os.path.basename(archive_name)}[repr.path]" - ) + console.info(f"Created archive: {os.path.basename(archive_name)}") return os.path.basename(archive_name) diff --git a/lxm3/xm_cluster/packaging/cluster.py b/lxm3/xm_cluster/packaging/cluster.py index 61a9a59..478b533 100644 --- a/lxm3/xm_cluster/packaging/cluster.py +++ b/lxm3/xm_cluster/packaging/cluster.py @@ -6,28 +6,59 @@ from typing import Any import appdirs +import fsspec +import fsspec.implementations +import fsspec.implementations.local +import rich.progress from lxm3 import singularity from lxm3 import xm from lxm3._vendor.xmanager.xm import pattern_matching from lxm3.experimental import image_cache from lxm3.xm_cluster import artifacts -from lxm3.xm_cluster import config as config_lib +from lxm3.xm_cluster import console from lxm3.xm_cluster import executable_specs as cluster_executable_specs from lxm3.xm_cluster import executables as cluster_executables -from lxm3.xm_cluster.console import console from lxm3.xm_cluster.packaging import archive_builder from lxm3.xm_cluster.packaging import container_builder -# from lxm3.xm_cluster.packaging import digest_util - _IMAGE_CACHE_DIR = os.path.join(appdirs.user_cache_dir("lxm3"), "image_cache") -# def _get_push_image_name(image_path: str, digest: Optional[str] = None) -> str: -# if digest is None: -# digest = digest_util.sha256_digest(image_path) -# path = pathlib.Path(image_path) -# return path.with_stem(path.stem + "@" + digest.replace(":", ".")).name + +def singularity_image_path(image_name: str): + return os.path.join("containers", image_name) + + +def archive_path(archive_name: str): + return os.path.join("archives", archive_name) + + +def _transfer_file_with_progress( + artifact_store: artifacts.ArtifactStore, + lpath: str, + rpath: str, +) -> str: + should_update, reason = artifact_store.should_update(lpath, rpath) + + basename = os.path.basename(lpath) + if should_update: + console.info(f"Transferring {basename} ({reason})") + with ( + rich.progress.Progress( + rich.progress.TextColumn("[progress.description]{task.description}"), + rich.progress.BarColumn(), + rich.progress.TaskProgressColumn(), + rich.progress.TimeRemainingColumn(), + rich.progress.TransferSpeedColumn(), + console=console.console, + ) as progress, + progress.open(lpath, mode="rb", description=os.path.basename(lpath)) as fin, + ): + put_path = artifact_store.put_fileobj(fin, rpath) + return put_path + else: + console.info(f"Skipped {basename}") + return artifact_store.get_file_info(rpath).path def _package_python_package( @@ -40,11 +71,11 @@ def _package_python_package( local_archive_path = os.path.join(staging, archive_name) entrypoint_cmd = archive_builder.ENTRYPOINT_SCRIPT push_archive_name = os.path.basename(local_archive_path) - deployed_archive_path = artifact_store.deploy_resource_archive( - local_archive_path, push_archive_name + deployed_archive_path = _transfer_file_with_progress( + artifact_store, local_archive_path, archive_path(push_archive_name) ) - return cluster_executables.Command( + return cluster_executables.AppBundle( entrypoint_command=entrypoint_cmd, resource_uri=deployed_archive_path, name=py_package.name, @@ -84,7 +115,7 @@ def _package_pex_binary( pex_options.extend(["--runtime-pex-root=./.pex"]) with console.status(f"Creating pex {pex_name}"): pex_cmd = [pex_executable, "-o", pex_path, *pex_options] - console.log(f"Running pex command: {' '.join(pex_cmd)}") + console.info(f"Running pex command: {' '.join(pex_cmd)}") subprocess.run(pex_cmd, check=True) # Add resources to the archive @@ -104,11 +135,11 @@ def _package_pex_binary( os.path.join(staging, spec.name), "zip", install_dir ) push_archive_name = os.path.basename(local_archive_path) - deployed_archive_path = artifact_store.deploy_resource_archive( - local_archive_path, push_archive_name + deployed_archive_path = _transfer_file_with_progress( + artifact_store, local_archive_path, archive_path(push_archive_name) ) - return cluster_executables.Command( + return cluster_executables.AppBundle( entrypoint_command=f"./{pex_name}", resource_uri=deployed_archive_path, name=spec.name, @@ -128,11 +159,11 @@ def _package_universal_package( ) local_archive_path = os.path.join(staging, os.path.basename(archive_name)) push_archive_name = os.path.basename(local_archive_path) - deployed_archive_path = artifact_store.deploy_resource_archive( - local_archive_path, push_archive_name + deployed_archive_path = _transfer_file_with_progress( + artifact_store, local_archive_path, archive_path(push_archive_name) ) - return cluster_executables.Command( + return cluster_executables.AppBundle( entrypoint_command=" ".join(universal_package.entrypoint), resource_uri=deployed_archive_path, name=universal_package.name, @@ -196,16 +227,26 @@ def _package_singularity_container( # TODO(yl): think about keeping multiple versions of the container in the storage. if not transport: push_image_name = os.path.basename(singularity_image) - deploy_container_path = artifact_store.singularity_image_path(push_image_name) - artifact_store.deploy_singularity_container(singularity_image, push_image_name) + if isinstance( + artifact_store.filesystem, fsspec.implementations.local.LocalFileSystem + ): + # Do not copy SIF image if executor is local + deploy_container_path = os.path.realpath(singularity_image) + else: + deploy_container_path = _transfer_file_with_progress( + artifact_store, + singularity_image, + singularity_image_path(push_image_name), + ) elif transport == "docker-daemon": cache_image_info = image_cache.get_cached_image( singularity_image, cache_dir=_IMAGE_CACHE_DIR ) push_image_name = singularity.uri.filename(singularity_image, "sif") - deploy_container_path = artifact_store.singularity_image_path(push_image_name) - artifact_store.deploy_singularity_container( - cache_image_info.blob_path, push_image_name + deploy_container_path = _transfer_file_with_progress( + artifact_store, + cache_image_info.blob_path, + singularity_image_path(push_image_name), ) else: # For other transports, just use the image as is for now. @@ -252,14 +293,5 @@ def _throw_on_unknown_executable( def package_for_cluster_executor(packageable: xm.Packageable): - config = config_lib.default() - cluster_settings = config.cluster_settings() - - artifact_store = artifacts.create_artifact_store( - cluster_settings.storage_root, - hostname=cluster_settings.hostname, - user=cluster_settings.user, - project=config.project(), - connect_kwargs=cluster_settings.ssh_config, - ) + artifact_store = artifacts.get_cluster_artifact_store() return _PACKAGING_ROUTER(packageable.executable_spec, packageable, artifact_store) diff --git a/lxm3/xm_cluster/packaging/local.py b/lxm3/xm_cluster/packaging/local.py index a3587c4..2f1d9a3 100644 --- a/lxm3/xm_cluster/packaging/local.py +++ b/lxm3/xm_cluster/packaging/local.py @@ -1,15 +1,10 @@ from lxm3 import xm from lxm3.xm_cluster import artifacts -from lxm3.xm_cluster import config as config_lib from lxm3.xm_cluster.packaging import cluster def package_for_local_executor(packageable: xm.Packageable): - config = config_lib.default() - local_settings = config_lib.default().local_settings() - artifact_store = artifacts.LocalArtifactStore( - local_settings.storage_root, project=config.project() - ) + artifact_store = artifacts.get_local_artifact_store() return cluster._PACKAGING_ROUTER( packageable.executable_spec, packageable, artifact_store ) diff --git a/pdm.lock b/pdm.lock index 87c94f4..2f4541d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,26 +5,26 @@ groups = ["default", "dev", "pex"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:8c2c652cf891884a2108380317082c3ab5e1329ec8227547b809d44ecd8397ad" +content_hash = "sha256:d70f4bd036f0caf5fb1bc2ba661c1852595c01cd42291e6c405e319bb11fe63a" [[package]] name = "absl-py" -version = "2.0.0" +version = "2.1.0" requires_python = ">=3.7" summary = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py." files = [ - {file = "absl-py-2.0.0.tar.gz", hash = "sha256:d9690211c5fcfefcdd1a45470ac2b5c5acd45241c3af71eed96bc5441746c0d5"}, - {file = "absl_py-2.0.0-py3-none-any.whl", hash = "sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3"}, + {file = "absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff"}, + {file = "absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308"}, ] [[package]] name = "alabaster" -version = "0.7.13" -requires_python = ">=3.6" -summary = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +requires_python = ">=3.9" +summary = "A light, configurable Sphinx theme" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -46,61 +46,73 @@ files = [ {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, ] +[[package]] +name = "atomicwrites" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Atomic file writes." +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" requires_python = ">=3.7" summary = "Classes Without Boilerplate" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" requires_python = ">=3.7" summary = "Internationalization utilities" -dependencies = [ - "setuptools; python_version >= \"3.12\"", -] files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [[package]] name = "bcrypt" -version = "4.0.1" -requires_python = ">=3.6" +version = "4.1.2" +requires_python = ">=3.7" summary = "Modern password hashing for your software and your servers" files = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, ] [[package]] name = "black" -version = "23.10.1" +version = "24.4.2" requires_python = ">=3.8" summary = "The uncompromising code formatter." dependencies = [ @@ -113,40 +125,44 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [[package]] name = "cachetools" -version = "5.3.2" +version = "5.3.3" requires_python = ">=3.7" summary = "Extensible memoizing collections and decorators" files = [ - {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, - {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, ] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -319,141 +335,150 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.5.0" requires_python = ">=3.8" summary = "Code coverage measurement for Python" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [[package]] name = "coverage" -version = "7.3.2" +version = "7.5.0" extras = ["toml"] requires_python = ">=3.8" summary = "Code coverage measurement for Python" dependencies = [ - "coverage==7.3.2", + "coverage==7.5.0", "tomli; python_full_version <= \"3.11.0a6\"", ] files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [[package]] name = "cryptography" -version = "41.0.5" +version = "42.0.5" requires_python = ">=3.7" summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." dependencies = [ - "cffi>=1.12", -] -files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, + {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, + {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, + {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, + {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, + {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, + {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, ] [[package]] @@ -481,48 +506,47 @@ files = [ [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" summary = "Distribution utilities" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "docker" -version = "6.1.3" -requires_python = ">=3.7" +version = "7.0.0" +requires_python = ">=3.8" summary = "A Python library for the Docker Engine API." dependencies = [ "packaging>=14.0", "pywin32>=304; sys_platform == \"win32\"", "requests>=2.26.0", "urllib3>=1.26.0", - "websocket-client>=0.32.0", ] files = [ - {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, - {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, + {file = "docker-7.0.0-py3-none-any.whl", hash = "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b"}, + {file = "docker-7.0.0.tar.gz", hash = "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3"}, ] [[package]] name = "docutils" -version = "0.20.1" -requires_python = ">=3.7" +version = "0.21.2" +requires_python = ">=3.9" summary = "Docutils -- Python Documentation Utilities" files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.1" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [[package]] @@ -542,22 +566,22 @@ files = [ [[package]] name = "filelock" -version = "3.13.1" +version = "3.14.0" requires_python = ">=3.8" summary = "A platform independent file lock." files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, ] [[package]] name = "fsspec" -version = "2023.10.0" +version = "2024.3.1" requires_python = ">=3.8" summary = "File-system specification" files = [ - {file = "fsspec-2023.10.0-py3-none-any.whl", hash = "sha256:346a8f024efeb749d2a5fca7ba8854474b1ff9af7c3faaf636a4548781136529"}, - {file = "fsspec-2023.10.0.tar.gz", hash = "sha256:330c66757591df346ad3091a53bd907e15348c2ba17d63fd54f5c39c4457d2a5"}, + {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, + {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, ] [[package]] @@ -575,35 +599,35 @@ files = [ [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.43" requires_python = ">=3.7" summary = "GitPython is a Python library used to interact with Git repositories" dependencies = [ "gitdb<5,>=4.0.1", ] files = [ - {file = "GitPython-3.1.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [[package]] name = "identify" -version = "2.5.31" +version = "2.5.36" requires_python = ">=3.8" summary = "File identification library for Python" files = [ - {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, - {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] [[package]] name = "idna" -version = "3.4" +version = "3.7" requires_python = ">=3.5" summary = "Internationalized Domain Names in Applications (IDNA)" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -618,25 +642,25 @@ files = [ [[package]] name = "immutabledict" -version = "3.0.0" +version = "4.2.0" requires_python = ">=3.8,<4.0" summary = "Immutable wrapper around dictionaries (a fork of frozendict)" files = [ - {file = "immutabledict-3.0.0-py3-none-any.whl", hash = "sha256:034bacc6c6872707c4ec0ea9515de6bbe0dcf0fcabd97ae19fd4e4c338f05798"}, - {file = "immutabledict-3.0.0.tar.gz", hash = "sha256:5a23cd369a6187f76a8c29d7d687980b092538eb9800e58964603f1b973c56fe"}, + {file = "immutabledict-4.2.0-py3-none-any.whl", hash = "sha256:d728b2c2410d698d95e6200237feb50a695584d20289ad3379a439aa3d90baba"}, + {file = "immutabledict-4.2.0.tar.gz", hash = "sha256:e003fd81aad2377a5a758bf7e1086cf3b70b63e9a5cc2f46bce8d0a2b4727c5f"}, ] [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "7.1.0" requires_python = ">=3.8" summary = "Read metadata from Python packages" dependencies = [ "zipp>=0.5", ] files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [[package]] @@ -661,15 +685,15 @@ files = [ [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" requires_python = ">=3.7" summary = "A very fast and expressive template engine." dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [[package]] @@ -687,51 +711,51 @@ files = [ [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" requires_python = ">=3.7" summary = "Safely add untrusted strings to HTML/XML markup." files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -784,11 +808,11 @@ files = [ [[package]] name = "myst-parser" -version = "2.0.0" +version = "3.0.1" requires_python = ">=3.8" summary = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," dependencies = [ - "docutils<0.21,>=0.16", + "docutils<0.22,>=0.18", "jinja2", "markdown-it-py~=3.0", "mdit-py-plugins~=0.4", @@ -796,8 +820,8 @@ dependencies = [ "sphinx<8,>=6", ] files = [ - {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, - {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, + {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, + {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, ] [[package]] @@ -815,17 +839,17 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" requires_python = ">=3.7" summary = "Core utilities for Python packages" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "paramiko" -version = "3.3.1" +version = "3.4.0" requires_python = ">=3.6" summary = "SSH2 protocol library" dependencies = [ @@ -834,64 +858,64 @@ dependencies = [ "pynacl>=1.5", ] files = [ - {file = "paramiko-3.3.1-py3-none-any.whl", hash = "sha256:b7bc5340a43de4287bbe22fe6de728aa2c22468b2a849615498dd944c2f275eb"}, - {file = "paramiko-3.3.1.tar.gz", hash = "sha256:6a3777a961ac86dbef375c5f5b8d50014a1a96d0fd7f054a43bc880134b0ff77"}, + {file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"}, + {file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"}, ] [[package]] name = "pathspec" -version = "0.11.2" -requires_python = ">=3.7" +version = "0.12.1" +requires_python = ">=3.8" summary = "Utility library for gitignore style pattern matching of file paths." files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "pex" -version = "2.2.1" +version = "2.3.1" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<3.13,>=2.7" summary = "The PEX packaging toolchain." files = [ - {file = "pex-2.2.1-py2.py3-none-any.whl", hash = "sha256:cde6756dc1ace8b4e0175afcd62da29f6635abe5516671717dffacb512502630"}, - {file = "pex-2.2.1.tar.gz", hash = "sha256:23adde5fd0439fd4468ad105662ba5b23118540b26632bd2362dfedad22b1aff"}, + {file = "pex-2.3.1-py2.py3-none-any.whl", hash = "sha256:64692a5bf6f298403aab930d22f0d836ae4736c5bc820e262e9092fe8c56f830"}, + {file = "pex-2.3.1.tar.gz", hash = "sha256:d1264c91161c21139b454744c8053e25b8aad2d15da89232181b4f38f3f54575"}, ] [[package]] name = "pip" -version = "23.3.1" +version = "24.0" requires_python = ">=3.7" summary = "The PyPA recommended tool for installing Python packages." files = [ - {file = "pip-23.3.1-py3-none-any.whl", hash = "sha256:55eb67bb6171d37447e82213be585b75fe2b12b359e993773aca4de9247a052b"}, - {file = "pip-23.3.1.tar.gz", hash = "sha256:1fcaa041308d01f14575f6d0d2ea4b75a3e2871fe4f9c694976f908768e14174"}, + {file = "pip-24.0-py3-none-any.whl", hash = "sha256:ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc"}, + {file = "pip-24.0.tar.gz", hash = "sha256:ea9bd1a847e8c5774a5777bb398c19e80bcd4e2aa16a4b301b718fe6f593aba2"}, ] [[package]] name = "platformdirs" -version = "3.11.0" -requires_python = ">=3.7" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.5.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [[package]] name = "pre-commit" -version = "3.5.0" -requires_python = ">=3.8" +version = "3.7.0" +requires_python = ">=3.9" summary = "A framework for managing and maintaining multi-language pre-commit hooks." dependencies = [ "cfgv>=2.0.0", @@ -901,28 +925,28 @@ dependencies = [ "virtualenv>=20.10.0", ] files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, ] [[package]] name = "pycparser" -version = "2.21" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.22" +requires_python = ">=3.8" summary = "C parser in Python" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" requires_python = ">=3.7" summary = "Pygments is a syntax highlighting package written in Python." files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [[package]] @@ -948,34 +972,50 @@ files = [ [[package]] name = "pytest" -version = "7.4.3" -requires_python = ">=3.7" +version = "8.2.0" +requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" dependencies = [ "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", - "pluggy<2.0,>=0.12", - "tomli>=1.0.0; python_version < \"3.11\"", + "pluggy<2.0,>=1.5", + "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [[package]] name = "pytest-cov" -version = "4.1.0" -requires_python = ">=3.7" +version = "5.0.0" +requires_python = ">=3.8" summary = "Pytest plugin for measuring coverage." dependencies = [ "coverage[toml]>=5.2.1", "pytest>=4.6", ] files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[[package]] +name = "pytest-golden" +version = "0.2.2" +requires_python = ">=3.6,<4.0" +summary = "Plugin for pytest that offloads expected outputs to data files" +dependencies = [ + "atomicwrites<2.0.0,>=1.4.0", + "pytest>=6.1.2", + "ruamel-yaml<1.0,>=0.16.12", + "testfixtures<7.0.0,>=6.15.0", +] +files = [ + {file = "pytest-golden-0.2.2.tar.gz", hash = "sha256:54e6f317a533758e6dcc96e6ef9457c610ae1c9db53575686a303f3ef7ad1e35"}, + {file = "pytest_golden-0.2.2-py3-none-any.whl", hash = "sha256:2e43a45244d16ab5ac8bbd72e26f28d2c9a24440f2fbdb26940e46fed505e5c0"}, ] [[package]] @@ -1019,6 +1059,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1052,7 +1093,7 @@ files = [ [[package]] name = "rich" -version = "13.6.0" +version = "13.7.1" requires_python = ">=3.7.0" summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" dependencies = [ @@ -1060,43 +1101,97 @@ dependencies = [ "pygments<3.0.0,>=2.13.0", ] files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +requires_python = ">=3.7" +summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +dependencies = [ + "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", +] +files = [ + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +requires_python = ">=3.6" +summary = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +files = [ + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, ] [[package]] name = "ruff" -version = "0.1.4" +version = "0.4.2" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.1.4-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:864958706b669cce31d629902175138ad8a069d99ca53514611521f532d91495"}, - {file = "ruff-0.1.4-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9fdd61883bb34317c788af87f4cd75dfee3a73f5ded714b77ba928e418d6e39e"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4eaca8c9cc39aa7f0f0d7b8fe24ecb51232d1bb620fc4441a61161be4a17539"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9a1301dc43cbf633fb603242bccd0aaa34834750a14a4c1817e2e5c8d60de17"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e8db8ab6f100f02e28b3d713270c857d370b8d61871d5c7d1702ae411df683"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:80fea754eaae06335784b8ea053d6eb8e9aac75359ebddd6fee0858e87c8d510"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bc02a480d4bfffd163a723698da15d1a9aec2fced4c06f2a753f87f4ce6969c"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862811b403063765b03e716dac0fda8fdbe78b675cd947ed5873506448acea4"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58826efb8b3efbb59bb306f4b19640b7e366967a31c049d49311d9eb3a4c60cb"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdfd453fc91d9d86d6aaa33b1bafa69d114cf7421057868f0b79104079d3e66e"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e8791482d508bd0b36c76481ad3117987301b86072158bdb69d796503e1c84a8"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01206e361021426e3c1b7fba06ddcb20dbc5037d64f6841e5f2b21084dc51800"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:645591a613a42cb7e5c2b667cbefd3877b21e0252b59272ba7212c3d35a5819f"}, - {file = "ruff-0.1.4-py3-none-win32.whl", hash = "sha256:99908ca2b3b85bffe7e1414275d004917d1e0dfc99d497ccd2ecd19ad115fd0d"}, - {file = "ruff-0.1.4-py3-none-win_amd64.whl", hash = "sha256:1dfd6bf8f6ad0a4ac99333f437e0ec168989adc5d837ecd38ddb2cc4a2e3db8a"}, - {file = "ruff-0.1.4-py3-none-win_arm64.whl", hash = "sha256:d98ae9ebf56444e18a3e3652b3383204748f73e247dea6caaf8b52d37e6b32da"}, - {file = "ruff-0.1.4.tar.gz", hash = "sha256:21520ecca4cc555162068d87c747b8f95e1e95f8ecfcbbe59e8dd00710586315"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] [[package]] name = "setuptools" -version = "68.2.2" +version = "69.5.1" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [[package]] @@ -1143,16 +1238,16 @@ files = [ [[package]] name = "sphinx" -version = "7.2.6" +version = "7.3.7" requires_python = ">=3.9" summary = "Python documentation generator" dependencies = [ "Jinja2>=3.0", "Pygments>=2.14", - "alabaster<0.8,>=0.7", + "alabaster~=0.7.14", "babel>=2.9", "colorama>=0.4.5; sys_platform == \"win32\"", - "docutils<0.21,>=0.18.1", + "docutils<0.22,>=0.18.1", "imagesize>=1.3", "importlib-metadata>=4.8; python_version < \"3.10\"", "packaging>=21.0", @@ -1164,10 +1259,11 @@ dependencies = [ "sphinxcontrib-jsmath", "sphinxcontrib-qthelp", "sphinxcontrib-serializinghtml>=1.1.9", + "tomli>=2; python_version < \"3.11\"", ] files = [ - {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, - {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [[package]] @@ -1183,41 +1279,32 @@ files = [ [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.7" +version = "1.0.8" requires_python = ">=3.9" summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -dependencies = [ - "Sphinx>=5", -] files = [ - {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, - {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.5" +version = "1.0.6" requires_python = ">=3.9" summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -dependencies = [ - "Sphinx>=5", -] files = [ - {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, - {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.4" +version = "2.0.5" requires_python = ">=3.9" summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -dependencies = [ - "Sphinx>=5", -] files = [ - {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, - {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] [[package]] @@ -1232,28 +1319,31 @@ files = [ [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.6" +version = "1.0.7" requires_python = ">=3.9" summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -dependencies = [ - "Sphinx>=5", -] files = [ - {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, - {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.9" +version = "1.1.10" requires_python = ">=3.9" summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -dependencies = [ - "Sphinx>=5", +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] + +[[package]] +name = "testfixtures" +version = "6.18.5" +summary = "A collection of helpers and mock objects for unit tests and doc tests." files = [ - {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, - {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, + {file = "testfixtures-6.18.5-py2.py3-none-any.whl", hash = "sha256:7de200e24f50a4a5d6da7019fb1197aaf5abd475efb2ec2422fdcf2f2eb98c1d"}, + {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"}, ] [[package]] @@ -1268,118 +1358,118 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.2" +version = "0.12.4" requires_python = ">=3.7" summary = "Style preserving TOML library" files = [ - {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, - {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.11.0" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "urllib3" -version = "2.0.7" -requires_python = ">=3.7" +version = "2.2.1" +requires_python = ">=3.8" summary = "HTTP library with thread-safe connection pooling, file post, and more." files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [[package]] name = "vcsinfo" -version = "2.1.105" +version = "2.1.110" summary = "Utilities to normalize working with different Version Control Systems" dependencies = [ - "GitPython<4", - "gitdb<5", + "GitPython>=3", + "gitdb>=4", ] files = [ - {file = "vcsinfo-2.1.105-py3-none-any.whl", hash = "sha256:847a587260d98c1979283c95b691f98fa0d311dd9ae8a965ffb4a14bffed0509"}, - {file = "vcsinfo-2.1.105.tar.gz", hash = "sha256:8db396c9693016e6bf263f5212a114a1483140816751329ff1d7a770c88ca4db"}, + {file = "vcsinfo-2.1.110-py3-none-any.whl", hash = "sha256:05356fbf597c780573bccb994a4df8f4aa3d6e9623876a41b306370c594dbaaa"}, + {file = "vcsinfo-2.1.110.tar.gz", hash = "sha256:a91833fee288657ce9f9e2b3bec481f1f22bd69efbd423d48d989cd9e20cd703"}, ] [[package]] name = "virtualenv" -version = "20.24.6" +version = "20.26.1" requires_python = ">=3.7" summary = "Virtual Python Environment builder" dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", - "platformdirs<4,>=3.9.1", + "platformdirs<5,>=3.9.1", ] files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, -] - -[[package]] -name = "websocket-client" -version = "1.6.4" -requires_python = ">=3.8" -summary = "WebSocket client for Python with low level API options" -files = [ - {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, - {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, + {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, + {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, ] [[package]] name = "wrapt" -version = "1.15.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.16.0" +requires_python = ">=3.6" summary = "Module for decorators, wrappers and monkey patching." files = [ - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" requires_python = ">=3.8" summary = "Backport of pathlib-compatible object wrapper for zip files" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] diff --git a/pyproject.toml b/pyproject.toml index 3c4ae2d..b5a5b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ dev = [ "sphinx_nameko_theme", "black>=23.1.0", "ruff>=0.0.243", + "pytest-golden>=0.2.2", ] [tool.pdm.version] @@ -80,27 +81,29 @@ test = "pytest lxm3 tests" test-cov = "pytest --cov=lxm3 --cov-report=xml tests" cov = {composite = ["test-cov", "coverage report"]} docs = "sphinx-build -b html docs docs/build/html -j auto" -lint.shell = "ruff ." +lint.shell = "ruff check ." fmt.shell = "ruff format ." [tool.ruff] +exclude = ["_vendor", "xm", ".venv"] + +[tool.ruff.lint] select = [ "E", # pycodestyle "F", # pyflakes "I", # isort ] -exclude = ["_vendor", "xm", ".venv"] ignore = ["E501"] -[tool.ruff.extend-per-file-ignores] +[tool.ruff.lint.extend-per-file-ignores] "__init__.py" = ["F401"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["lxm3"] force-single-line = true single-line-exclusions = ["typing"] -[tool.ruff.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" [tool.coverage.paths] @@ -120,6 +123,10 @@ exclude_lines = [ "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] +omit = [ + "lxm3/experimental/*", + "lxm3/xm_cluster/packaging/digest_util.py" +] [tool.pyright] include = ["lxm3"] @@ -145,6 +152,11 @@ testpaths = [ "tests", "lxm3/_vendor", ] +markers = [ + "integration", +] +addopts = "--import-mode=importlib" +enable_assertion_pass_hook = true filterwarnings = [ # xm/core_test.py "ignore:cannot collect test class 'TestError':pytest.PytestCollectionWarning", diff --git a/tests/artifact_test.py b/tests/artifact_test.py new file mode 100644 index 0000000..e992a3e --- /dev/null +++ b/tests/artifact_test.py @@ -0,0 +1,49 @@ +import os +import pathlib + +import fsspec +from absl.testing import absltest +from absl.testing import parameterized + +from lxm3.xm_cluster import artifacts + + +class LocalArtifactsTest(parameterized.TestCase): + def _create_store(self): + fs = fsspec.filesystem("local") + staging = self.create_tempdir() + project = "test" + store = artifacts.ArtifactStore(fs, os.fspath(staging), project) + return store, staging, project + + def test_put_text(self): + store, staging, project = self._create_store() + content = "foo" + dst = "nested/one" + expected_dst = pathlib.Path(staging, f"projects/{project}/{dst}") + store.put_text(content, dst) + self.assertEqual(content, expected_dst.read_text()) + self.assertEqual(store.get_file_info(dst).size, len(content)) + + def test_put_file(self): + store, staging, project = self._create_store() + content = "foo" + src = self.create_tempfile("test.py", content=content) + dst = "nested/one" + expected_dst = pathlib.Path(staging, f"projects/{project}/{dst}") + store.put_file(src.full_path, dst) + + self.assertEqual(content, expected_dst.read_text()) + self.assertEqual(store.get_file_info(dst).size, len(content)) + + def test_ensure_dir(self): + store, staging, project = self._create_store() + dst = "nested/one" + store.ensure_dir(dst) + expected_path = pathlib.Path(staging, f"projects/{project}", dst) + self.assertTrue(expected_path.exists()) + self.assertTrue(expected_path.is_dir()) + + +if __name__ == "__main__": + absltest.main() diff --git a/tests/config_test.py b/tests/config_test.py index 374f826..513cd44 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -57,17 +57,13 @@ def test_config_project(self): with unittest.mock.patch.dict("os.environ", {"LXM_PROJECT": "test"}): self.assertEqual(config.project(), "test") - def test_local_settings(self): - settings = config_lib.LocalSettings() - self.assertEqual(settings.env, {}) - self.assertEqual(settings.singularity.env, {}) - self.assertEqual(settings.singularity.binds, {}) - - def test_cluster_settings(self): - settings = config_lib.ClusterSettings() - self.assertEqual(settings.env, {}) - self.assertEqual(settings.singularity.env, {}) - self.assertEqual(settings.singularity.binds, {}) + def test_default_config(self): + config = config_lib.Config.default() + local = config.local_settings() + assert local.storage_root + assert config.project + with self.assertRaises(ValueError): + config.cluster_settings() if __name__ == "__main__": diff --git a/tests/conftest.py b/tests/conftest.py index b2eca1c..4a02db0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,6 @@ def pytest_configure(config): flags.FLAGS.mark_as_parsed() -def pytest_ignore_collect(path, config): - if path.isfile() and path.islink(): +def pytest_ignore_collect(collection_path, config): + if collection_path.is_file() and collection_path.is_symlink(): return True diff --git a/tests/execution_test.py b/tests/execution_test.py index 47ec236..3a3a109 100644 --- a/tests/execution_test.py +++ b/tests/execution_test.py @@ -2,11 +2,14 @@ import shutil import subprocess import sys +import textwrap import unittest import zipfile from unittest import mock from unittest.mock import patch +import fsspec +import pytest from absl.testing import absltest from absl.testing import parameterized @@ -35,75 +38,59 @@ def is_docker_installed(): class JobScriptBuilderTest(parameterized.TestCase): def test_env_vars(self): - env_var_str = job_script.JobScriptBuilder._create_env_vars( - [{"FOO": "BAR1"}, {"FOO": "BAR2"}] + env_var_str = job_script._create_env_vars( + [{"FOO": "BAR1"}, {"FOO": "BAR2"}], "LXM_TASK_ID", 0 ) - expected = """\ -FOO_0="BAR1" -FOO_1="BAR2" -FOO=$(eval echo \\$"FOO_$LXM_TASK_ID") -export FOO""" + expected = textwrap.dedent("""\ + FOO_0="BAR1" + FOO_1="BAR2" + FOO=$(eval echo \\$"FOO_$LXM_TASK_ID") + export FOO""") self.assertEqual(env_var_str, expected) def test_empty_env_vars(self): - self.assertEqual(job_script.JobScriptBuilder._create_env_vars([{}]), "") + self.assertEqual(job_script._create_env_vars([{}], "LXM_TASK_ID", 0), "") def test_common_values(self): - env_var_str = job_script.JobScriptBuilder._create_env_vars( - [{"FOO": "BAR", "BAR": "1"}, {"FOO": "BAR", "BAR": "2"}] + env_var_str = job_script._create_env_vars( + [{"FOO": "BAR", "BAR": "1"}, {"FOO": "BAR", "BAR": "2"}], "LXM_TASK_ID", 0 ) - expected = """\ -export FOO="BAR" -BAR_0="1" -BAR_1="2" -BAR=$(eval echo \\$"BAR_$LXM_TASK_ID") -export BAR""" + expected = textwrap.dedent("""\ + export FOO="BAR" + BAR_0="1" + BAR_1="2" + BAR=$(eval echo \\$"BAR_$LXM_TASK_ID") + export BAR""") self.assertEqual(env_var_str, expected) def test_different_keys(self): with self.assertRaises(ValueError): - job_script.JobScriptBuilder._create_env_vars( - [{"FOO": "BAR1"}, {"BAR": "BAR2"}] + job_script._create_env_vars( + [{"FOO": "BAR1"}, {"BAR": "BAR2"}], "LXM_TASK_ID", 0 ) def test_args(self): - args_str = job_script.JobScriptBuilder._create_args( - [["--seed=1", "--task=1"], ["--seed=2", "--task=2"]] + args_str = job_script._create_args( + [["--seed=1", "--task=1"], ["--seed=2", "--task=2"]], "LXM_TASK_ID", 0 ) - expected = """\ -TASK_CMD_ARGS_0="--seed=1 --task=1" -TASK_CMD_ARGS_1="--seed=2 --task=2" -TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_$LXM_TASK_ID") -eval set -- $TASK_CMD_ARGS""" + expected = textwrap.dedent("""\ + TASK_CMD_ARGS_0="--seed=1 --task=1" + TASK_CMD_ARGS_1="--seed=2 --task=2" + TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_$LXM_TASK_ID") + eval set -- $TASK_CMD_ARGS""") self.assertEqual(args_str, expected) def test_empty_args(self): - self.assertEqual(job_script.JobScriptBuilder._create_args([]), "") + self.assertEqual(job_script._create_args([], "LXM_TASK_ID", 0), "") self.assertEqual( - job_script.JobScriptBuilder._create_args([[]]), - """\ -TASK_CMD_ARGS_0="" -TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_$LXM_TASK_ID") -eval set -- $TASK_CMD_ARGS""", - ) - - def test_get_additional_env(self): - job_env = {"FOO": "FOO_0", "OVERRIDE": "OVERRIDE"} - - self.assertEqual( - job_script.JobScriptBuilder._get_additional_env( - job_env, {"FOO": "FOO_HOST", "BAR": "BAR"} + job_script._create_args([[]], "LXM_TASK_ID", 0), + textwrap.dedent( + """\ + TASK_CMD_ARGS_0="" + TASK_CMD_ARGS=$(eval echo \\$"TASK_CMD_ARGS_$LXM_TASK_ID") + eval set -- $TASK_CMD_ARGS""", ), - {"BAR": "BAR"}, - ) - - def test_additional_bindings(self): - job_binds = {"/c": "/b", "/d": "/e"} - overrides = {"/a": "/b", "/foo": "/bar"} - additional_binds = job_script.JobScriptBuilder._get_additional_binds( - job_binds, overrides ) - self.assertEqual(additional_binds, {"/foo": "/bar"}) class LocalExecutionTest(parameterized.TestCase): @@ -126,7 +113,7 @@ def test_local_launch(self): container = staging_dir.create_file(container_name) deploy_dir = self.create_tempdir(name="deploy") - executable = executables.Command( + executable = executables.AppBundle( name="test", entrypoint_command="echo hello", resource_uri=archive.full_path, @@ -134,7 +121,9 @@ def test_local_launch(self): ) executor = executors.Local() job = xm.Job(executable, executor, name="test") - artifact = artifacts.LocalArtifactStore(deploy_dir.full_path) + artifact = artifacts.ArtifactStore( + fsspec.filesystem("local"), deploy_dir.full_path + ) settings = config.LocalSettings() client = local.LocalClient(settings, artifact) with mock.patch.object(subprocess, "run"): @@ -167,11 +156,9 @@ def test_job_script_run_single_job(self): info.external_attr = 0o777 << 16 # give full access to included file z.writestr( info, - """\ -#!/usr/bin/env bash -echo $@ $FOO""", + "#!/usr/bin/env bash\necho $@ $FOO", ) - executable = xm_cluster.Command( + executable = xm_cluster.AppBundle( "foo", "./entrypoint.sh", resource_uri=tmpf.full_path ) job = xm.Job( @@ -192,11 +179,9 @@ def test_job_script_run_array_job(self): info.external_attr = 0o777 << 16 # give full access to included file z.writestr( info, - """\ -#!/usr/bin/env bash -echo $@ $FOO""", + "#!/usr/bin/env bash\necho $@ $FOO", ) - executable = xm_cluster.Command( + executable = xm_cluster.AppBundle( "foo", "./entrypoint.sh", resource_uri=tmpf.full_path ) job = xm_cluster.ArrayJob( @@ -208,11 +193,11 @@ def test_job_script_run_array_job(self): builder = local.LocalJobScriptBuilder() job_script_content = builder.build(job, "foo", "/tmp") process = self._run_job_script( - job_script_content, env={builder.TASK_ID_VAR_NAME: "1"} + job_script_content, env={builder.ARRAY_TASK_ID: "1"} ) self.assertEqual(process.stdout.decode("utf-8").strip(), "--seed=1 FOO_0") process = self._run_job_script( - job_script_content, env={builder.TASK_ID_VAR_NAME: "2"} + job_script_content, env={builder.ARRAY_TASK_ID: "2"} ) self.assertEqual(process.stdout.decode("utf-8").strip(), "--seed=2 FOO_1") @@ -223,38 +208,31 @@ def test_job_script_handles_ml_collections_quoting(self): info.external_attr = 0o777 << 16 # give full access to included file z.writestr( "run.py", - """\ -from absl import app -from ml_collections import config_dict -from ml_collections import config_flags - -def _get_config(): - config = config_dict.ConfigDict() - config.name = "" - return config - + textwrap.dedent("""\ + from absl import app + from ml_collections import config_dict + from ml_collections import config_flags -_CONFIG = config_flags.DEFINE_config_dict("config", _get_config()) + def _get_config(): + config = config_dict.ConfigDict() + config.name = "" + return config + _CONFIG = config_flags.DEFINE_config_dict("config", _get_config()) -def main(_): - config = _CONFIG.value - print(config.name) + def main(_): + config = _CONFIG.value + print(config.name) -if __name__ == "__main__": - config = _get_config() - app.run(main) - -""", + if __name__ == "__main__": + config = _get_config() + app.run(main)"""), ) z.writestr( info, - """\ -#!/usr/bin/env bash -{} run.py $@ -""".format(sys.executable), + "#!/usr/bin/env bash\n{} run.py $@\n".format(sys.executable), ) - executable = xm_cluster.Command( + executable = xm_cluster.AppBundle( "foo", "./entrypoint.sh", resource_uri=tmpf.full_path ) job = xm_cluster.ArrayJob( @@ -265,9 +243,10 @@ def main(_): ) builder = local.LocalJobScriptBuilder() job_script_content = builder.build(job, "foo", "/tmp") - process = self._run_job_script(job_script_content, env={"SGE_TASK_ID": "1"}) + process = self._run_job_script(job_script_content, env={"LOCAL_TASK_ID": "1"}) self.assertEqual(process.stdout.decode("utf-8").strip(), "train[:90%]") + @pytest.mark.integration @unittest.skipIf(not is_singularity_installed(), "Singularity is not installed") def test_singularity(self): tmpf = self.create_tempfile("test.zip") @@ -276,26 +255,31 @@ def test_singularity(self): info.external_attr = 0o777 << 16 # give full access to included file z.writestr( info, - """\ -#!/usr/bin/env bash -echo $FOO""", + "#!/usr/bin/env bash\necho $FOO", ) - executable = xm_cluster.Command( + executable = xm_cluster.AppBundle( "test", "./entrypoint.sh", resource_uri=tmpf.full_path, singularity_image="docker://python:3.10-slim", ) job = xm_cluster.ArrayJob( - executable, executor=xm_cluster.Local(), env_vars=[{"FOO": "FOO_0"}] + executable, + executor=xm_cluster.Local( + singularity_options=xm_cluster.SingularityOptions( + extra_options=["--containall"] + ) + ), + env_vars=[{"FOO": "FOO_0"}], ) job_script_content = local.LocalJobScriptBuilder().build(job, "foo", "/tmp") process = self._run_job_script( - job_script_content, env={local.LocalJobScriptBuilder.TASK_ID_VAR_NAME: "1"} + job_script_content, env={local.LocalJobScriptBuilder.ARRAY_TASK_ID: "1"} ) self.assertEqual(process.stdout.decode("utf-8").strip(), "FOO_0") + @pytest.mark.integration @unittest.skipIf(not is_docker_installed(), "Docker is not installed") def test_docker_image(self): tmpf = self.create_tempfile("test.zip") @@ -304,12 +288,10 @@ def test_docker_image(self): info.external_attr = 0o777 << 16 # give full access to included file z.writestr( info, - """\ -#!/usr/bin/env bash -echo $FOO""", + "#!/usr/bin/env bash\necho $FOO\n", ) - executable = xm_cluster.Command( + executable = xm_cluster.AppBundle( "test", "./entrypoint.sh", resource_uri=tmpf.full_path, @@ -320,7 +302,7 @@ def test_docker_image(self): ) job_script_content = local.LocalJobScriptBuilder().build(job, "foo", "/tmp") process = self._run_job_script( - job_script_content, env={local.LocalJobScriptBuilder.TASK_ID_VAR_NAME: "1"} + job_script_content, env={local.LocalJobScriptBuilder.ARRAY_TASK_ID: "1"} ) self.assertEqual(process.stdout.decode("utf-8").strip(), "FOO_0") @@ -360,7 +342,7 @@ def test_gridengine_header(self): self.assertIn("#$ -tc 2", header) def test_setup_cmds(self): - executable = executables.Command( + executable = executables.AppBundle( name="test", entrypoint_command="echo hello", resource_uri="", @@ -372,8 +354,10 @@ def test_setup_cmds(self): "_is_gpu_requested", return_value=True, ): - setup_cmds = gridengine.GridEngineJobScriptBuilder._create_setup_cmds( - executable, executor + setup_cmds = ( + gridengine.GridEngineJobScriptBuilder._create_job_script_prologue( + executable, executor + ) ) self.assertIn("nvidia-smi", setup_cmds) self.assertIn("module load module1", setup_cmds) @@ -388,7 +372,7 @@ def test_launch(self): container = staging_dir.create_file(container_name) deploy_dir = self.create_tempdir(name="deploy") - executable = executables.Command( + executable = executables.AppBundle( name="test", entrypoint_command="echo hello", resource_uri=archive.full_path, @@ -396,7 +380,9 @@ def test_launch(self): ) executor = executors.GridEngine() job = xm.Job(executable, executor, name="test") - artifact = artifacts.LocalArtifactStore(deploy_dir.full_path) + artifact = artifacts.ArtifactStore( + fsspec.filesystem("local"), deploy_dir.full_path + ) settings = config.ClusterSettings() client = gridengine.GridEngineClient(settings, artifact) with mock.patch.object( @@ -427,7 +413,7 @@ def test_slurm_header(self): self.assertIn("#SBATCH --array=1-10", header) def test_setup_cmds(self): - executable = executables.Command( + executable = executables.AppBundle( name="test", entrypoint_command="echo hello", resource_uri="", @@ -439,7 +425,7 @@ def test_setup_cmds(self): "_is_gpu_requested", return_value=True, ): - setup_cmds = slurm.SlurmJobScriptBuilder._create_setup_cmds( + setup_cmds = slurm.SlurmJobScriptBuilder._create_job_script_prologue( executable, executor ) self.assertIn("module load module1", setup_cmds) @@ -454,7 +440,7 @@ def test_slurm_launch(self): container = staging_dir.create_file(container_name) deploy_dir = self.create_tempdir(name="deploy") - executable = executables.Command( + executable = executables.AppBundle( name="test", entrypoint_command="echo hello", resource_uri=archive.full_path, @@ -462,7 +448,9 @@ def test_slurm_launch(self): ) executor = executors.Slurm() job = xm.Job(executable, executor, name="test") - artifact = artifacts.LocalArtifactStore(deploy_dir.full_path) + artifact = artifacts.ArtifactStore( + fsspec.filesystem("local"), deploy_dir.full_path + ) settings = config.ClusterSettings() client = slurm.SlurmClient(settings, artifact) with mock.patch.object(slurm_cluster.SlurmCluster, "launch") as mock_launch: diff --git a/tests/experiment_test.py b/tests/experiment_test.py index 8ca5fcb..ce50287 100644 --- a/tests/experiment_test.py +++ b/tests/experiment_test.py @@ -47,7 +47,7 @@ def setUp(self): super().setUp() staging = self.create_tempdir().full_path self._config = config_lib.Config.from_string(_TEST_CONFIG.format(staging)) - self._executable = xm_cluster.Command( + self._executable = xm_cluster.AppBundle( name="dummy", entrypoint_command="./entrypint.sh", resource_uri="dummy", diff --git a/tests/experimental/script_gen_test.py b/tests/experimental/script_gen_test.py deleted file mode 100644 index 8020876..0000000 --- a/tests/experimental/script_gen_test.py +++ /dev/null @@ -1,84 +0,0 @@ -import os -import shlex -import shutil -import subprocess -import unittest - -from absl.testing import absltest -from absl.testing import parameterized - -from lxm3.experimental import job_script_builder - -HERE = os.path.dirname(__file__) - - -singularity_image = "examples/basic/python_3.10-slim.sif" - - -@unittest.skip("Skip until we have a better way to test this") -class ScriptGenTest(parameterized.TestCase): - @parameterized.parameters((False,), (True,)) - def test_array_script(self, use_singularity): - tmpdir = self.create_tempdir() - f = shutil.make_archive( - os.path.join(tmpdir, "test"), "zip", os.path.join(HERE, "testdata") - ) - per_task_envs = [{"BAR": "0"}, {"BAR": "1"}] - per_task_args = [ - ["--foo", "1", "--notes", shlex.quote("space space")], - ["--foo", "2", "--notes", shlex.quote("space space2")], - ] - - script = job_script_builder.create_job_script( - command=["./pkg/main.py"], - archives=f, - singularity_image=singularity_image if use_singularity else None, # type: ignore - singularity_options=["--compat"], - per_task_args=per_task_args, - per_task_envs=per_task_envs, - ) - job_script = tmpdir.create_file("jobscript.sh", content=script) - - proc = subprocess.run( - ["sh", job_script.full_path], - check=True, - capture_output=True, - text=True, - env={**os.environ, "SGE_TASK_ID": "2"}, - ) - self.assertIn("['--foo', '2', '--notes', 'space space2']", proc.stdout) - with self.assertRaises(subprocess.CalledProcessError): - print( - subprocess.run( - ["sh", job_script.full_path], - check=True, - capture_output=True, - text=True, - env={**os.environ}, - ).stderr - ) - - @parameterized.parameters((True,), (False,)) - def test_job_script(self, use_singularity): - tmpdir = self.create_tempdir() - f = shutil.make_archive( - os.path.join(tmpdir, "test"), "zip", os.path.join(HERE, "testdata") - ) - - script = job_script_builder.create_job_script( - command=["pkg/main.py", "arg0", "arg1"], - archives=f, - singularity_options=["--compat"], - singularity_image=singularity_image if use_singularity else None, # type: ignore - ) - - job_script = tmpdir.create_file("jobscript.sh", content=script) - - proc = subprocess.run( - ["sh", job_script.full_path], capture_output=True, check=True, text=True - ) - self.assertIn("['arg0', 'arg1']", proc.stdout) - - -if __name__ == "__main__": - absltest.main() diff --git a/tests/experimental/testdata/pkg/main.py b/tests/experimental/testdata/pkg/main.py deleted file mode 100755 index cf9c0e0..0000000 --- a/tests/experimental/testdata/pkg/main.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys - -for k in os.environ: - if k.startswith("SGE") or k.startswith("JOB"): - print(k, os.environ[k]) -print(sys.argv[1:]) diff --git a/tests/packaging_test.py b/tests/packaging_test.py index dd6b7bf..71b0b06 100644 --- a/tests/packaging_test.py +++ b/tests/packaging_test.py @@ -1,7 +1,10 @@ import os +import sys +import unittest import unittest.mock import zipfile +import fsspec from absl.testing import absltest from absl.testing import parameterized @@ -13,6 +16,10 @@ _HERE = os.path.abspath(os.path.dirname(__file__)) +def _create_artifact_store(staging, project): + return artifacts.ArtifactStore(fsspec.filesystem("local"), staging, project) + + class PackagingTest(parameterized.TestCase): @parameterized.parameters( (cluster._package_python_package,), @@ -25,7 +32,7 @@ def test_package_python(self, pkg_fun): with unittest.mock.patch("subprocess.run"): tmpdir = self.create_tempdir().full_path - store = artifacts.LocalArtifactStore(tmpdir, "test") + store = _create_artifact_store(tmpdir, "test") executable = pkg_fun( spec, xm.Packageable( @@ -34,33 +41,7 @@ def test_package_python(self, pkg_fun): ), store, ) - self.assertIsInstance(executable, xm_cluster.Command) - - # @parameterized.parameters((cluster._package_python_package,)) - # def test_package_python_resource_overwrites(self, pkg_fun): - # resource = xm_cluster.Fileset( - # files={ - # os.path.join( - # _HERE, "testdata/test_pkg/py_package/main.py" - # ): "py_package/main.py", - # } - # ) - # spec = xm_cluster.PythonPackage( - # entrypoint=xm_cluster.ModuleName("py_package.main"), - # path=os.path.join(_HERE, "testdata/test_pkg"), - # resources=[resource], - # ) - - # tmpdir = self.create_tempdir().full_path - # store = artifacts.LocalArtifactStore(tmpdir, "test") - # pkg_fun( - # spec, - # xm.Packageable( - # spec, - # xm_cluster.Local().Spec(), - # ), - # store, - # ) + self.assertIsInstance(executable, xm_cluster.AppBundle) def test_package_default_pip_args(self): spec = xm_cluster.PythonPackage( @@ -75,6 +56,7 @@ def test_package_default_pip_args(self): @parameterized.parameters( (cluster._package_universal_package,), ) + @absltest.skipIf("darwin" in sys.platform, "Not working on MacOS") def test_package_universal(self, pkg_fun): spec = xm_cluster.UniversalPackage( entrypoint=["python3", "main.py"], @@ -82,7 +64,7 @@ def test_package_universal(self, pkg_fun): build_script="build.sh", ) tmpdir = self.create_tempdir().full_path - store = artifacts.LocalArtifactStore(tmpdir, "test") + store = _create_artifact_store(tmpdir, "test") executable = pkg_fun( spec, xm.Packageable( @@ -91,10 +73,10 @@ def test_package_universal(self, pkg_fun): ), store, ) - self.assertIsInstance(executable, xm_cluster.Command) + self.assertIsInstance(executable, xm_cluster.AppBundle) # Check that archive exists - self.assertTrue(store._fs.exists(executable.resource_uri)) - with store._fs.open(executable.resource_uri, "rb") as f: + self.assertTrue(store.filesystem.exists(executable.resource_uri)) + with store.filesystem.open(executable.resource_uri, "rb") as f: archive = zipfile.ZipFile(f) # type: ignore self.assertEqual(set(archive.namelist()), set(["main.py"]))