From be01c39f0cb77dca754a93a57015c80c47a8b526 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 16 Feb 2023 07:11:25 -0500 Subject: [PATCH 01/47] build: removed exclude src in pre-commit-config --- .pre-commit-config.yaml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 407fb9a01..e6e0dd0c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,25 +7,21 @@ repos: exclude: .bumpversion.cfg - id: check-yaml - id: check-json - exclude: src/ - id: mixed-line-ending - args: ['--fix=lf'] + args: ["--fix=lf"] description: Forces to replace line ending by the UNIX 'lf' character. - id: pretty-format-json - exclude: src/ - args: ['--no-sort-keys'] + args: ["--no-sort-keys"] - id: check-added-large-files - args: ['--maxkb=500'] + args: ["--maxkb=500"] - repo: https://github.com/psf/black rev: 23.1.0 hooks: - id: black - exclude: src/ - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 - exclude: src/ additional_dependencies: ["flake8-docstrings==1.7.0"] - repo: https://github.com/myint/autoflake rev: v2.0.1 @@ -36,7 +32,6 @@ repos: rev: 5.12.0 hooks: - id: isort - exclude: src - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: @@ -46,10 +41,9 @@ repos: rev: v1.0.0 hooks: - id: mypy - exclude: src/ additional_dependencies: [types-requests==2.28.11.8] - repo: https://github.com/python-poetry/poetry - rev: '1.3.0' # add version here + rev: "1.3.0" # add version here hooks: - id: poetry-check - id: poetry-lock From 79fd1177b3d6eff1350fb8e0ab7f235077d1cdb7 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Sun, 26 Feb 2023 22:42:34 -0500 Subject: [PATCH 02/47] refactor: added docstrings to top of scripts --- setup.py | 6 +- src/polus/plugins/__init__.py | 7 +- src/polus/plugins/__main__.py | 6 + .../_plugins/classes/plugin_classes.py | 67 ++++++++--- .../_plugins/classes/plugin_methods.py | 5 +- src/polus/plugins/_plugins/update.py | 108 ++++++++++++++++++ src/polus/plugins/plugins.py | 2 - 7 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 src/polus/plugins/_plugins/update.py diff --git a/setup.py b/setup.py index fabdacab4..ae258fb31 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,11 @@ -from setuptools import setup, find_packages +"""polus-plugins setup.py.""" + +from setuptools import find_packages, setup # with open("README.md", "r") as fh: # long_description = fh.read() -with open("./polus/_plugins/VERSION", "r") as fh: +with open("./polus/_plugins/VERSION") as fh: version = fh.read() with open("./polus/_plugins/VERSION", "w") as fw: fw.write(version) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 9a12d0f9b..8f880f1dc 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -1,2 +1,7 @@ -# This file should not be changed +""" +Polus-plugins. + +Package to configure and run polus-plugins. +""" + from . import * # noqa: F403,F401 diff --git a/src/polus/plugins/__main__.py b/src/polus/plugins/__main__.py index 9b9b48b32..36b67e430 100644 --- a/src/polus/plugins/__main__.py +++ b/src/polus/plugins/__main__.py @@ -1,3 +1,9 @@ +""" +polus-plugins main.py. + +main.py +""" + import click diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 3ff133803..61f46bff7 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -10,10 +10,9 @@ from ..io import DuplicateVersionFound, Version, _in_old_to_new, _ui_old_to_new from ..manifests.manifest_utils import _load_manifest, validate_manifest -from ..models import (ComputeSchema, PluginUIInput, PluginUIOutput, - WIPPPluginManifest) +from ..models import ComputeSchema, PluginUIInput, PluginUIOutput, WIPPPluginManifest from ..utils import cast_version, name_cleaner -from .plugin_methods import PluginMethods +from .plugin_methods import _PluginMethods logger = logging.getLogger("polus.plugins") PLUGINS = {} @@ -53,7 +52,7 @@ def list(self): @classmethod def get_plugin(cls, name: str, version: typing.Optional[str] = None): - """Returns a plugin object. + """Get a plugin with option to specify version. Return a plugin object with the option to specify a version. The specified version's manifest must exist in manifests folder. @@ -94,12 +93,11 @@ def load_config(self, config: typing.Union[dict, pathlib.Path]): @classmethod def refresh(cls): - """Refresh the plugin list + """Refresh the plugin list. This should be optimized, since it will become noticeably slow when there are many plugins. """ - organizations = [ x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" ] # ignore __pycache__ @@ -126,16 +124,28 @@ def refresh(cls): PLUGINS[key][plugin.version] = file -class Plugin(WIPPPluginManifest, PluginMethods): - """Required until json schema is fixed""" +class Plugin(WIPPPluginManifest, _PluginMethods): + """WIPP Plugin Class. + + Contains methods to configure, run, and save plugins. + + Attributes: + versions: A list of local available versions for this plugin. + + Methods: + save_manifest(path): save plugin manifest to specified path + """ id: uuid.UUID class Config: + """Config class for Pydantic Model.""" + extra = Extra.allow allow_mutation = False def __init__(self, _uuid: bool = True, **data): + """Init a plugin object from manifest.""" if _uuid: data["id"] = uuid.uuid4() else: @@ -155,10 +165,11 @@ def __init__(self, _uuid: bool = True, **data): @property def versions(plugin): # cannot be in PluginMethods because PLUGINS lives here - """Return list of versions of a Plugin""" + """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(plugin.name)]) def to_compute(self, hardware_requirements: typing.Optional[dict] = None): + """Convert WIPP Plugin object to Compute Plugin object.""" data = deepcopy(self.manifest) return ComputePlugin( hardware_requirements=hardware_requirements, _from_old=True, **data @@ -170,6 +181,7 @@ def save_manifest( hardware_requirements: typing.Optional[dict] = None, compute: bool = False, ): + """Save plugin manifest to specified path.""" if compute: with open(path, "w") as fw: self.to_compute( @@ -187,7 +199,8 @@ def save_manifest( logger.debug("Saved manifest to %s" % (path)) def __setattr__(self, name, value): - PluginMethods.__setattr__(self, name, value) + """Set I/O parameters as attributes.""" + _PluginMethods.__setattr__(self, name, value) @property def _config_file(self): @@ -196,16 +209,31 @@ def _config_file(self): return m def save_config(self, path: typing.Union[str, pathlib.Path]): + """Save manifest with configured I/O parameters to specified path.""" with open(path, "w") as fw: json.dump(self._config_file, fw, indent=4, default=str) logger.debug("Saved config to %s" % (path)) def __repr__(self) -> str: - return PluginMethods.__repr__(self) + """Print plugin name and version.""" + return _PluginMethods.__repr__(self) -class ComputePlugin(ComputeSchema, PluginMethods): +class ComputePlugin(ComputeSchema, _PluginMethods): + """Compute Plugin Class. + + Contains methods to configure, run, and save plugins. + + Attributes: + versions: A list of local available versions for this plugin. + + Methods: + save_manifest(path): save plugin manifest to specified path + """ + class Config: + """Config class for Pydantic Model.""" + extra = Extra.allow allow_mutation = False @@ -216,6 +244,7 @@ def __init__( _uuid: bool = True, **data, ): + """Init a plugin object from manifest.""" if _uuid: data["id"] = uuid.uuid4() else: @@ -275,7 +304,7 @@ def _ui_out(d: dict): @property def versions(plugin): # cannot be in PluginMethods because PLUGINS lives here - """Return list of versions of a Plugin""" + """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(plugin.name)]) @property @@ -285,26 +314,30 @@ def _config_file(self): return m def __setattr__(self, name, value): - PluginMethods.__setattr__(self, name, value) + """Set I/O parameters as attributes.""" + _PluginMethods.__setattr__(self, name, value) def save_config(self, path: typing.Union[str, pathlib.Path]): + """Save configured manifest with I/O parameters to specified path.""" with open(path, "w") as fw: json.dump(self._config_file, fw, indent=4) logger.debug("Saved config to %s" % (path)) def save_manifest(self, path: typing.Union[str, pathlib.Path]): + """Save plugin manifest to specified path.""" with open(path, "w") as fw: json.dump(self.manifest, fw, indent=4) logger.debug("Saved manifest to %s" % (path)) def __repr__(self) -> str: - return PluginMethods.__repr__(self) + """Print plugin name and version.""" + return _PluginMethods.__repr__(self) def load_plugin( manifest: typing.Union[str, dict, pathlib.Path] ) -> typing.Union[Plugin, ComputePlugin]: - """Parses a manifest and returns one of Plugin or ComputePlugin""" + """Parse a manifest and return one of Plugin or ComputePlugin.""" manifest = _load_manifest(manifest) if "pluginHardwareRequirements" in manifest: # Parse the manifest @@ -319,7 +352,7 @@ def submit_plugin( manifest: typing.Union[str, dict, pathlib.Path], refresh: bool = False, ): - """Parses a plugin and creates a local copy of it. + """Parse a plugin and create a local copy of it. This function accepts a plugin manifest as a string, a dictionary (parsed json), or a pathlib.Path object pointed at a plugin manifest. diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index f31fce2ec..89b7f8fae 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -1,3 +1,4 @@ +"""Methods for all plugin objects.""" import enum import json import logging @@ -13,10 +14,12 @@ class IOKeyError(Exception): + """Raised when trying to set invalid I/O parameter.""" + pass -class PluginMethods: +class _PluginMethods: @property def organization(self): return self.containerId.split("/")[0] diff --git a/src/polus/plugins/_plugins/update.py b/src/polus/plugins/_plugins/update.py new file mode 100644 index 000000000..af95f51a2 --- /dev/null +++ b/src/polus/plugins/_plugins/update.py @@ -0,0 +1,108 @@ +import json +import logging +import re +import typing + +from pydantic import ValidationError +from tqdm import tqdm + +from polus.plugins._plugins.classes import submit_plugin +from polus.plugins._plugins.gh import _init_github +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.manifests.manifest_utils import (_error_log, + _scrape_manifests) + +logger = logging.getLogger("polus.plugins") + +def update_polus_plugins( + gh_auth: typing.Optional[str] = None, min_depth: int = 2, max_depth: int = 3 +): + logger.info("Updating polus plugins.") + # Get all manifests + valid, invalid = _scrape_manifests( + "polusai/polus-plugins", _init_github(gh_auth), min_depth, max_depth, True + ) + manifests = valid.copy() + manifests.extend(invalid) + logger.info("Submitting %s plugins." % len(manifests)) + + for manifest in manifests: + try: + plugin = submit_plugin(manifest) + + """ Parsing checks specific to polus-plugins """ + error_list = [] + + # Check that plugin version matches container version tag + container_name, version = tuple(plugin.containerId.split(":")) + version = Version(version=version) + organization, container_name = tuple(container_name.split("/")) + try: + assert ( + plugin.version == version + ), f"containerId version ({version}) does not match plugin version ({plugin.version})" + except AssertionError as err: + error_list.append(err) + + # Check to see that the plugin is registered to Labshare + try: + assert organization in [ + "polusai", + "labshare", + ], "All polus plugin containers must be under the Labshare organization." + except AssertionError as err: + error_list.append(err) + + # Checks for container name, they are somewhat related to our + # Jenkins build + try: + assert container_name.startswith( + "polus" + ), "containerId name must begin with polus-" + except AssertionError as err: + error_list.append(err) + + try: + assert container_name.endswith( + "plugin" + ), "containerId name must end with -plugin" + except AssertionError as err: + error_list.append(err) + + if len(error_list) > 0: + raise ValidationError(error_list, plugin.__class__) + + except ValidationError as val_err: + try: + _error_log(val_err, manifest, "update_polus_plugins") + except BaseException as e: + # logger.debug(f"There was an error {e} in {plugin.name}") + logger.exception(f"In {plugin.name}: {e}") + except BaseException as e: + # logger.debug(f"There was an error {e} in {plugin.name}") + logger.exception(f"In {plugin.name}: {e}") + + +def update_nist_plugins(gh_auth: typing.Optional[str] = None): + # Parse README links + gh = _init_github(gh_auth) + repo = gh.get_repo("usnistgov/WIPP") + contents = repo.get_contents("plugins") + readme = [r for r in contents if r.name == "README.md"][0] + pattern = re.compile( + r"\[manifest\]\((https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))\)" + ) + matches = pattern.findall(str(readme.decoded_content)) + logger.info("Updating NIST plugins.") + for match in tqdm(matches, desc="NIST Manifests"): + url_parts = match[0].split("/")[3:] + plugin_repo = gh.get_repo("/".join(url_parts[:2])) + manifest = json.loads( + plugin_repo.get_contents("/".join(url_parts[4:])).decoded_content + ) + + try: + submit_plugin(manifest) + + except ValidationError as val_err: + _error_log(val_err, manifest, "update_nist_plugins") \ No newline at end of file diff --git a/src/polus/plugins/plugins.py b/src/polus/plugins/plugins.py index e1ff0a963..390bff29b 100644 --- a/src/polus/plugins/plugins.py +++ b/src/polus/plugins/plugins.py @@ -10,7 +10,6 @@ from ._plugins.gh import _init_github from ._plugins.io import Version from ._plugins.manifests.manifest_utils import _error_log, _scrape_manifests -from ._plugins.registry import WippPluginRegistry """ Set up logging for the module @@ -24,7 +23,6 @@ plugins = _Plugins() get_plugin = plugins.get_plugin load_config = plugins.load_config -plugins.WippPluginRegistry = WippPluginRegistry plugins.refresh() # calls the refresh method when library is imported From f726f806095f6c62acc30a30909170b00082710b Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 2 Mar 2023 11:09:30 -0500 Subject: [PATCH 03/47] refactor: addressed most of flake8 --- src/polus/plugins/__init__.py | 20 ++++++++++++-- .../plugins/_plugins/classes/__init__.py | 10 +++++-- .../_plugins/classes/plugin_classes.py | 11 ++++---- src/polus/plugins/_plugins/gh.py | 3 ++- src/polus/plugins/_plugins/io.py | 1 + .../_plugins/manifests/manifest_utils.py | 5 ++-- src/polus/plugins/_plugins/models/__init__.py | 7 ++--- src/polus/plugins/_plugins/models/compute.py | 27 +++++++++++++++---- src/polus/plugins/_plugins/models/wipp.py | 10 +++++-- src/polus/plugins/_plugins/registry.py | 9 ++++--- src/polus/plugins/_plugins/registry_utils.py | 1 + .../{plugins.py => _plugins/update.py} | 27 ++----------------- src/polus/plugins/_plugins/utils.py | 3 ++- 13 files changed, 82 insertions(+), 52 deletions(-) rename src/polus/plugins/{plugins.py => _plugins/update.py} (83%) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 9a12d0f9b..e6fa6f96b 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -1,2 +1,18 @@ -# This file should not be changed -from . import * # noqa: F403,F401 +import logging + +# TODO try to get rid of _Plugins +from polus.plugins._plugins.classes import _Plugins as plugins, submit_plugin + +""" +Set up logging for the module +""" +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger("polus.plugins") +plugins.refresh() # calls the refresh method when library is imported + +list = plugins.list + +__all__ = ["plugins", "submit_plugin"] diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 5df72bf9e..d1b203162 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,4 +1,10 @@ -from .plugin_classes import (ComputePlugin, Plugin, _Plugins, load_plugin, - submit_plugin) +"""Plugin classes and functions.""" +from polus.plugins._plugins.classes.plugin_classes import ( + ComputePlugin, + Plugin, + _Plugins, + load_plugin, + submit_plugin, +) __all__ = ["Plugin", "ComputePlugin", "load_plugin", "submit_plugin", "_Plugins"] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 3ff133803..b6e7fe144 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -1,3 +1,4 @@ +"""Classes for Plugin objects containing methods to configure, run, and save.""" import json import logging import pathlib @@ -8,12 +9,12 @@ from pydantic import Extra -from ..io import DuplicateVersionFound, Version, _in_old_to_new, _ui_old_to_new -from ..manifests.manifest_utils import _load_manifest, validate_manifest -from ..models import (ComputeSchema, PluginUIInput, PluginUIOutput, +from polus.plugins._plugins.io import DuplicateVersionFound, Version, _in_old_to_new, _ui_old_to_new +from polus.plugins._plugins.manifests.manifest_utils import _load_manifest, validate_manifest +from polus.plugins._plugins.models import (ComputeSchema, PluginUIInput, PluginUIOutput, WIPPPluginManifest) -from ..utils import cast_version, name_cleaner -from .plugin_methods import PluginMethods +from polus.plugins._plugins.utils import cast_version, name_cleaner +from polus.plugins._plugins.classes.plugin_methods import PluginMethods logger = logging.getLogger("polus.plugins") PLUGINS = {} diff --git a/src/polus/plugins/_plugins/gh.py b/src/polus/plugins/_plugins/gh.py index de3938c69..2d7ea858b 100644 --- a/src/polus/plugins/_plugins/gh.py +++ b/src/polus/plugins/_plugins/gh.py @@ -1,10 +1,11 @@ +"""GitHub utilties.""" import logging import os from urllib.parse import urljoin import github -from .classes.plugin_classes import submit_plugin +from polus.plugins._plugins.classes import submit_plugin logger = logging.getLogger("polus.plugins") diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 92a35b1d2..c0c52848e 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -1,3 +1,4 @@ +"""Plugins I/O utilities.""" import enum import logging import pathlib diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py index 609adae4f..fad38649b 100644 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ b/src/polus/plugins/_plugins/manifests/manifest_utils.py @@ -1,3 +1,4 @@ +"""Utilities for manifest parsing and validation.""" import json import logging import pathlib @@ -9,8 +10,8 @@ from pydantic import ValidationError, errors from tqdm import tqdm -from ..models import ComputeSchema, WIPPPluginManifest -from ..utils import cast_version +from polus.plugins._plugins.models import ComputeSchema, WIPPPluginManifest +from polus.plugins._plugins.utils import cast_version logger = logging.getLogger("polus.plugins") diff --git a/src/polus/plugins/_plugins/models/__init__.py b/src/polus/plugins/_plugins/models/__init__.py index 5fcaeb86b..29e362f49 100644 --- a/src/polus/plugins/_plugins/models/__init__.py +++ b/src/polus/plugins/_plugins/models/__init__.py @@ -1,6 +1,7 @@ -from .compute import PluginSchema as ComputeSchema -from .compute import PluginUIInput, PluginUIOutput -from .wipp import WIPPPluginManifest +"""Pydantic Models based on JSON schemas.""" +from polus.plugins._plugins.models.compute import PluginSchema as ComputeSchema +from polus.plugins._plugins.models.compute import PluginUIInput, PluginUIOutput +from polus.plugins._plugins.models.wipp import WIPPPluginManifest __all__ = [ "WIPPPluginManifest", diff --git a/src/polus/plugins/_plugins/models/compute.py b/src/polus/plugins/_plugins/models/compute.py index 8dff1196c..ef0874a46 100644 --- a/src/polus/plugins/_plugins/models/compute.py +++ b/src/polus/plugins/_plugins/models/compute.py @@ -1,10 +1,27 @@ +"""Extending automatically generated compute model. + +This file modifies and extend certain fields and +functions of PolusComputeSchema.py which is automatically +generated by datamodel-codegen from JSON schema. +""" from typing import List -from ..io import IOBase, Version -from .PolusComputeSchema import ( # CustomUIType, ????; CLTSchema ???? - ConditionEntry, GpuVendor, PluginHardwareRequirements, PluginInput, - PluginInputType, PluginOutput, PluginOutputType, PluginSchema, - PluginUIInput, PluginUIOutput, PluginUIType, ThenEntry, Validator) +from polus.plugins._plugins.io import IOBase, Version +from polus.plugins._plugins.models.PolusComputeSchema import ( # CustomUIType, ????; CLTSchema ???? + ConditionEntry, + GpuVendor, + PluginHardwareRequirements, + PluginInput, + PluginInputType, + PluginOutput, + PluginOutputType, + PluginSchema, + PluginUIInput, + PluginUIOutput, + PluginUIType, + ThenEntry, + Validator, +) class PluginInput(PluginInput, IOBase): diff --git a/src/polus/plugins/_plugins/models/wipp.py b/src/polus/plugins/_plugins/models/wipp.py index a68946d11..5139f1f9c 100644 --- a/src/polus/plugins/_plugins/models/wipp.py +++ b/src/polus/plugins/_plugins/models/wipp.py @@ -1,9 +1,15 @@ +"""Extending automatically generated wipp model. + +This file modifies and extend certain fields and +functions of WIPPPluginSchema.py which is automatically +generated by datamodel-codegen from JSON schema. +""" from typing import List, Literal, Optional, Union from pydantic import BaseModel, Field -from ..io import Input, Output, Version -from .WIPPPluginSchema import UiItem, WippPluginManifest # type: ignore +from polus.plugins._plugins.io import Input, Output, Version +from polus.plugins._plugins.models.WIPPPluginSchema import UiItem, WippPluginManifest # type: ignore class ui1(BaseModel): diff --git a/src/polus/plugins/_plugins/registry.py b/src/polus/plugins/_plugins/registry.py index bbe154819..ebca77157 100644 --- a/src/polus/plugins/_plugins/registry.py +++ b/src/polus/plugins/_plugins/registry.py @@ -1,3 +1,4 @@ +"""Methods to interact with REST API of WIPP Plugin Registry.""" import json import logging import typing @@ -8,10 +9,10 @@ import xmltodict from tqdm import tqdm -from .classes.plugin_classes import ComputePlugin, Plugin -from .classes.plugin_classes import _Plugins as plugins -from .classes.plugin_classes import submit_plugin -from .registry_utils import _generate_query, _to_xml +from polus.plugins._plugins.classes.plugin_classes import ComputePlugin, Plugin +from polus.plugins._plugins.classes.plugin_classes import _Plugins as plugins +from polus.plugins._plugins.classes.plugin_classes import submit_plugin +from polus.plugins._plugins.registry_utils import _generate_query, _to_xml logger = logging.getLogger("polus.plugins") diff --git a/src/polus/plugins/_plugins/registry_utils.py b/src/polus/plugins/_plugins/registry_utils.py index 8e68da005..8c2bd5163 100644 --- a/src/polus/plugins/_plugins/registry_utils.py +++ b/src/polus/plugins/_plugins/registry_utils.py @@ -1,3 +1,4 @@ +"""Utilities for WIPP Registry Module.""" import re import typing diff --git a/src/polus/plugins/plugins.py b/src/polus/plugins/_plugins/update.py similarity index 83% rename from src/polus/plugins/plugins.py rename to src/polus/plugins/_plugins/update.py index e1ff0a963..4115f1555 100644 --- a/src/polus/plugins/plugins.py +++ b/src/polus/plugins/_plugins/update.py @@ -1,31 +1,8 @@ -import json import logging -import re -import typing +from polus.plugins._plugins.gh import _init_github -from pydantic import ValidationError -from tqdm import tqdm - -from ._plugins.classes import _Plugins, submit_plugin -from ._plugins.gh import _init_github -from ._plugins.io import Version -from ._plugins.manifests.manifest_utils import _error_log, _scrape_manifests -from ._plugins.registry import WippPluginRegistry - -""" -Set up logging for the module -""" -logging.basicConfig( - format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", - datefmt="%d-%b-%y %H:%M:%S", -) logger = logging.getLogger("polus.plugins") logger.setLevel(logging.INFO) -plugins = _Plugins() -get_plugin = plugins.get_plugin -load_config = plugins.load_config -plugins.WippPluginRegistry = WippPluginRegistry -plugins.refresh() # calls the refresh method when library is imported def update_polus_plugins( @@ -119,4 +96,4 @@ def update_nist_plugins(gh_auth: typing.Optional[str] = None): submit_plugin(manifest) except ValidationError as val_err: - _error_log(val_err, manifest, "update_nist_plugins") + _error_log(val_err, manifest, "update_nist_plugins") \ No newline at end of file diff --git a/src/polus/plugins/_plugins/utils.py b/src/polus/plugins/_plugins/utils.py index 4467c3fcf..192fc02e3 100644 --- a/src/polus/plugins/_plugins/utils.py +++ b/src/polus/plugins/_plugins/utils.py @@ -1,4 +1,5 @@ -from .io import Version +"""General utilities for polus-plugins.""" +from polus.plugins._plugins.io import Version def name_cleaner(name: str) -> str: From b32c4a5a3795534994935f52ba545cb2e0cacc2c Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 3 Mar 2023 08:38:54 -0500 Subject: [PATCH 04/47] refactor: refactored to pass flake8 and remove plugins class --- .pre-commit-config.yaml | 14 ++ src/polus/plugins/__init__.py | 30 ++- .../plugins/_plugins/classes/__init__.py | 23 ++- .../_plugins/classes/plugin_classes.py | 182 ++++++++---------- .../_plugins/classes/plugin_methods.py | 2 - src/polus/plugins/_plugins/gh.py | 4 +- src/polus/plugins/_plugins/io.py | 33 +++- .../_plugins/manifests/manifest_utils.py | 7 +- src/polus/plugins/_plugins/models/__init__.py | 2 +- src/polus/plugins/_plugins/models/compute.py | 24 +-- src/polus/plugins/_plugins/models/wipp.py | 10 +- src/polus/plugins/_plugins/registry.py | 24 +-- src/polus/plugins/_plugins/update.py | 8 +- src/polus/plugins/_plugins/utils.py | 11 +- 14 files changed, 203 insertions(+), 171 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6e0dd0c9..e7ca1c341 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,10 +18,13 @@ repos: rev: 23.1.0 hooks: - id: black + exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py + - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 + exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py additional_dependencies: ["flake8-docstrings==1.7.0"] - repo: https://github.com/myint/autoflake rev: v2.0.1 @@ -32,6 +35,7 @@ repos: rev: 5.12.0 hooks: - id: isort + exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: @@ -41,6 +45,16 @@ repos: rev: v1.0.0 hooks: - id: mypy + args: + [ + "--disable-error-code", + "no-redef", + "--disable-error-code", + "valid-type", + "--disable-error-code", + "import", + ] + exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py additional_dependencies: [types-requests==2.28.11.8] - repo: https://github.com/python-poetry/poetry rev: "1.3.0" # add version here diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index e6fa6f96b..a3e83ec10 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -1,7 +1,11 @@ +"""Initialize polus-plugins module.""" + import logging -# TODO try to get rid of _Plugins -from polus.plugins._plugins.classes import _Plugins as plugins, submit_plugin +from polus.plugins._plugins.classes import ( # noqa # pylint: disable=unused-import + get_plugin, list_plugins, load_plugin, refresh) +from polus.plugins._plugins.update import ( # noqa # pylint: disable=unused-import + update_nist_plugins, update_polus_plugins) """ Set up logging for the module @@ -11,8 +15,24 @@ datefmt="%d-%b-%y %H:%M:%S", ) logger = logging.getLogger("polus.plugins") -plugins.refresh() # calls the refresh method when library is imported +refresh() # calls the refresh method when library is imported +__plugins = list_plugins() + +for _p in __plugins: + # make each plugin available as polus.plugins.PluginName + globals()[_p] = get_plugin(_p) + +plugin_list = list_plugins() -list = plugins.list +_export_list = [ + "plugin_list", + "refresh", + "submit_plugin", + "get_plugin", + "load_plugin", + "list_plugins", + "update_polus_plugins", + "update_nist_plugins", +] + __plugins -__all__ = ["plugins", "submit_plugin"] +__all__ = _export_list diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index d1b203162..7e2740277 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,10 +1,17 @@ """Plugin classes and functions.""" -from polus.plugins._plugins.classes.plugin_classes import ( - ComputePlugin, - Plugin, - _Plugins, - load_plugin, - submit_plugin, -) +from polus.plugins._plugins.classes.plugin_classes import (ComputePlugin, + Plugin, get_plugin, + list_plugins, + load_plugin, + refresh, + submit_plugin) -__all__ = ["Plugin", "ComputePlugin", "load_plugin", "submit_plugin", "_Plugins"] +__all__ = [ + "Plugin", + "ComputePlugin", + "load_plugin", + "submit_plugin", + "get_plugin", + "refresh", + "list_plugins", +] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 4e391851e..05fc31310 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -5,18 +5,20 @@ import typing import uuid from copy import deepcopy -from pprint import pformat from pydantic import Extra -from ..io import DuplicateVersionFound, Version, _in_old_to_new, _ui_old_to_new -from ..manifests.manifest_utils import _load_manifest, validate_manifest -from ..models import ComputeSchema, PluginUIInput, PluginUIOutput, WIPPPluginManifest -from ..utils import cast_version, name_cleaner -from .plugin_methods import _PluginMethods +from polus.plugins._plugins.classes.plugin_methods import _PluginMethods +from polus.plugins._plugins.io import (DuplicateVersionFound, Version, + _in_old_to_new, _ui_old_to_new) +from polus.plugins._plugins.manifests.manifest_utils import (_load_manifest, + validate_manifest) +from polus.plugins._plugins.models import (ComputeSchema, PluginUIInput, + PluginUIOutput, WIPPPluginManifest) +from polus.plugins._plugins.utils import cast_version, name_cleaner logger = logging.getLogger("polus.plugins") -PLUGINS = {} +PLUGINS: typing.Dict[str, typing.Dict] = {} # PLUGINS = {"BasicFlatfieldCorrectionPlugin": # {Version('0.1.4'): Path(<...>), Version('0.1.5'): Path(<...>)}. # "VectorToLabel": {Version(...)}} @@ -28,101 +30,85 @@ PLUGIN_DIR = pathlib.Path(__file__).parent.parent.joinpath("manifests") -""" -Plugin Fetcher Class -""" +def load_config(config: typing.Union[dict, pathlib.Path]): + """Load configured plugin from config file/dict.""" + if isinstance(config, pathlib.Path): + with open(config) as fr: + m = json.load(fr) + elif isinstance(config, dict): + m = config + else: + raise TypeError("config must be a dict or a path") + _io = m["_io_keys"] + cl = m["class"] + m.pop("class", None) + if cl == "Compute": + pl = ComputePlugin(_uuid=False, **m) + elif cl == "WIPP": + pl = Plugin(_uuid=False, **m) + else: + raise ValueError("Invalid value of class") + for k, v in _io.items(): + val = v["value"] + if val is not None: # exclude those values not set + setattr(pl, k, val) + return pl -class _Plugins: - def __getattribute__(self, name): - if name in PLUGINS: - return self.get_plugin(name) - return super().__getattribute__(name) +def get_plugin(name: str, version: typing.Optional[str] = None): + """Get a plugin with option to specify version. - def __len__(self): - return len(self.list) + Return a plugin object with the option to specify a version. The specified version's manifest must exist in manifests folder. - def __repr__(self): - return pformat(self.list) + Args: + name: Name of the plugin. + version: Optional version of the plugin, must follow semver. - @property - def list(self): - output = list(PLUGINS.keys()) - output.sort() - return output - - @classmethod - def get_plugin(cls, name: str, version: typing.Optional[str] = None): - """Get a plugin with option to specify version. - - Return a plugin object with the option to specify a version. The specified version's manifest must exist in manifests folder. - - Args: - name: Name of the plugin. - version: Optional version of the plugin, must follow semver. - - Returns: - Plugin object - """ - if version is None: - return load_plugin(PLUGINS[name][max(PLUGINS[name])]) - else: - return load_plugin(PLUGINS[name][Version(**{"version": version})]) - - def load_config(self, config: typing.Union[dict, pathlib.Path]): - if isinstance(config, pathlib.Path): - with open(config) as fr: - m = json.load(fr) - elif isinstance(config, dict): - m = config - else: - raise TypeError("config must be a dict or a path") - _io = m["_io_keys"] - cl = m["class"] - m.pop("class", None) - if cl == "Compute": - pl = ComputePlugin(_uuid=False, **m) - elif cl == "WIPP": - pl = Plugin(_uuid=False, **m) - else: - raise ValueError("Invalid value of class") - for k, v in _io.items(): - val = v["value"] - if val is not None: # exclude those values not set - setattr(pl, k, val) - return pl - - @classmethod - def refresh(cls): - """Refresh the plugin list. - - This should be optimized, since it will become noticeably slow when - there are many plugins. - """ - organizations = [ - x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" - ] # ignore __pycache__ - - for org in organizations: - if org.is_file(): + Returns: + Plugin object + """ + if version is None: + return load_plugin(PLUGINS[name][max(PLUGINS[name])]) + else: + return load_plugin(PLUGINS[name][Version(**{"version": version})]) + + +def refresh(): + """Refresh the plugin list.""" + organizations = [ + x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" + ] # ignore __pycache__ + + for org in organizations: + if org.is_file(): + continue + + for file in org.iterdir(): + if file.suffix == ".py": continue - for file in org.iterdir(): - if file.suffix == ".py": - continue + plugin = validate_manifest(file) + key = name_cleaner(plugin.name) + # Add version and path to VERSIONS + if key not in PLUGINS: + PLUGINS[key] = {} + if plugin.version in PLUGINS[key]: + if not file == PLUGINS[key][plugin.version]: + raise DuplicateVersionFound( + "Found duplicate version of plugin %s in %s" + % (plugin.name, PLUGIN_DIR) + ) + PLUGINS[key][plugin.version] = file + + +_r = refresh + - plugin = validate_manifest(file) - key = name_cleaner(plugin.name) - # Add version and path to VERSIONS - if key not in PLUGINS: - PLUGINS[key] = {} - if plugin.version in PLUGINS[key]: - if not file == PLUGINS[key][plugin.version]: - raise DuplicateVersionFound( - "Found duplicate version of plugin %s in %s" - % (plugin.name, PLUGIN_DIR) - ) - PLUGINS[key][plugin.version] = file +def list_plugins(): + """List all local plugins.""" + output = list(PLUGINS.keys()) + output.sort() + return output class Plugin(WIPPPluginManifest, _PluginMethods): @@ -340,12 +326,12 @@ def load_plugin( ) -> typing.Union[Plugin, ComputePlugin]: """Parse a manifest and return one of Plugin or ComputePlugin.""" manifest = _load_manifest(manifest) - if "pluginHardwareRequirements" in manifest: + if "pluginHardwareRequirements" in manifest: # type: ignore[operator] # Parse the manifest - plugin = ComputePlugin(**manifest) # New Schema + plugin = ComputePlugin(**manifest) # type: ignore[arg-type] else: # Parse the manifest - plugin = Plugin(**manifest) # Old Schema + plugin = Plugin(**manifest) # type: ignore[arg-type] return plugin @@ -385,5 +371,5 @@ def submit_plugin( # Refresh plugins list if refresh = True if refresh: - _Plugins.refresh() + _r() return plugin diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index 89b7f8fae..2455ed1e8 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -16,8 +16,6 @@ class IOKeyError(Exception): """Raised when trying to set invalid I/O parameter.""" - pass - class _PluginMethods: @property diff --git a/src/polus/plugins/_plugins/gh.py b/src/polus/plugins/_plugins/gh.py index 2d7ea858b..80290b392 100644 --- a/src/polus/plugins/_plugins/gh.py +++ b/src/polus/plugins/_plugins/gh.py @@ -58,8 +58,8 @@ def add_plugin_from_gh( Returns: A Plugin object populated with information from the plugin manifest. """ - l = [user, repo, branch, plugin, manifest_name] - u = "/".join(l) + l1 = [user, repo, branch, plugin, manifest_name] + u = "/".join(l1) url = urljoin("https://raw.githubusercontent.com", u) logger.info("Adding %s" % url) return submit_plugin(url, refresh=True) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index c0c52848e..bb4139dac 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -11,7 +11,7 @@ logger = logging.getLogger("polus.plugins") """ -Enums for validating plugin input, output, and ui components +Enums for validating plugin input, output, and ui components. """ WIPP_TYPES = { "collection": pathlib.Path, @@ -34,7 +34,7 @@ class InputTypes(str, enum.Enum): # wipp schema - """This is needed until the json schema is updated""" + """Enum of Inpyt Types for WIPP schema.""" collection = "collection" pyramid = "pyramid" @@ -54,11 +54,12 @@ class InputTypes(str, enum.Enum): # wipp schema @classmethod def list(cls): + """List Input Types.""" return list(map(lambda c: c.value, cls)) class OutputTypes(str, enum.Enum): # wipp schema - """This is needed until the json schema is updated""" + """Enum for Output Types for WIPP schema.""" collection = "collection" pyramid = "pyramid" @@ -72,11 +73,12 @@ class OutputTypes(str, enum.Enum): # wipp schema @classmethod def list(cls): + """List Output Types.""" return list(map(lambda c: c.value, cls)) def _in_old_to_new(old: str) -> str: # map wipp InputType to compute schema's InputType - """Map an InputType from wipp schema to one of compute schema""" + """Map an InputType from wipp schema to one of compute schema.""" d = {"integer": "number", "enum": "string"} if old in ["string", "array", "number", "boolean"]: return old @@ -87,7 +89,7 @@ def _in_old_to_new(old: str) -> str: # map wipp InputType to compute schema's I def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's UIType - """Map an InputType from wipp schema to a UIType of compute schema""" + """Map an InputType from wipp schema to a UIType of compute schema.""" type_dict = { "string": "text", "boolean": "checkbox", @@ -102,6 +104,8 @@ def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's U class IOBase(BaseModel): + """Base Class for I/O arguments.""" + type: typing.Any options: typing.Optional[dict] = None value: typing.Optional[typing.Any] = None @@ -158,6 +162,7 @@ def _validate(self): super().__setattr__("value", value) def __setattr__(self, name, value): + """Set I/O attributes.""" if name not in ["value", "id", "_fs"]: # Don't permit any other values to be changed raise TypeError(f"Cannot set property: {name}") @@ -172,7 +177,7 @@ def __setattr__(self, name, value): class Output(IOBase): - """Required until JSON schema is fixed""" + """Required until JSON schema is fixed.""" name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( # noqa: F722 ..., examples=["outputCollection"], title="Output name" @@ -186,7 +191,7 @@ class Output(IOBase): class Input(IOBase): - """Required until JSON schema is fixed""" + """Required until JSON schema is fixed.""" name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( # noqa: F722 ..., @@ -206,6 +211,7 @@ class Input(IOBase): ) def __init__(self, **data): + """Initialize input.""" super().__init__(**data) if self.description is None: @@ -226,13 +232,17 @@ def _check_version_number(value: str | int) -> bool: class Version(BaseModel): + """SemVer object.""" + version: str def __init__(self, version): + """Initialize Version object.""" super().__init__(version=version) @validator("version") def semantic_version(cls, value): + """Pydantic Validator to check semver.""" version = value.split(".") assert ( @@ -252,17 +262,21 @@ def semantic_version(cls, value): @property def major(self): + """Return x from x.y.z .""" return self.version.split(".")[0] @property def minor(self): + """Return y from x.y.z .""" return self.version.split(".")[1] @property def patch(self): + """Return z from x.y.z .""" return self.version.split(".")[2] def __lt__(self, other): + """Compare if Version is less than other Version object.""" assert isinstance(other, Version), "Can only compare version objects." if other.major > self.major: @@ -281,9 +295,11 @@ def __lt__(self, other): return False def __gt__(self, other): + """Compare if Version is greater than other Version object.""" return other < self def __eq__(self, other): + """Compare if two Version objects are equal.""" return ( other.major == self.major and other.minor == self.minor @@ -291,8 +307,9 @@ def __eq__(self, other): ) def __hash__(self): + """Needed to use Version objects as dict keys.""" return hash(self.version) class DuplicateVersionFound(Exception): - pass + """Raise when two equal versions found.""" diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py index fad38649b..e51188b0b 100644 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ b/src/polus/plugins/_plugins/manifests/manifest_utils.py @@ -29,7 +29,7 @@ def is_valid_manifest(plugin: dict) -> bool: - """Validates basic attributes of a plugin manifest. + """Validate basic attributes of a plugin manifest. Args: plugin: A parsed plugin json file @@ -37,7 +37,6 @@ def is_valid_manifest(plugin: dict) -> bool: Returns: True if the plugin has the minimal json fields """ - fields = list(plugin.keys()) try: @@ -50,7 +49,7 @@ def is_valid_manifest(plugin: dict) -> bool: def _load_manifest(m: typing.Union[str, dict, pathlib.Path]) -> dict: - """Convert to dictionary if pathlib.Path or str""" + """Convert to dictionary if pathlib.Path or str.""" if isinstance(m, dict): return m elif isinstance(m, pathlib.Path): @@ -74,7 +73,7 @@ def _load_manifest(m: typing.Union[str, dict, pathlib.Path]) -> dict: def validate_manifest( manifest: typing.Union[str, dict, pathlib.Path] ) -> typing.Union[WIPPPluginManifest, ComputeSchema]: - """Validates a plugin manifest against schema""" + """Validate a plugin manifest against schema.""" manifest = _load_manifest(manifest) manifest["version"] = cast_version( manifest["version"] diff --git a/src/polus/plugins/_plugins/models/__init__.py b/src/polus/plugins/_plugins/models/__init__.py index 29e362f49..0c5df0ced 100644 --- a/src/polus/plugins/_plugins/models/__init__.py +++ b/src/polus/plugins/_plugins/models/__init__.py @@ -1,6 +1,6 @@ """Pydantic Models based on JSON schemas.""" from polus.plugins._plugins.models.compute import PluginSchema as ComputeSchema -from polus.plugins._plugins.models.compute import PluginUIInput, PluginUIOutput +from polus.plugins._plugins.models.PolusComputeSchema import PluginUIInput, PluginUIOutput from polus.plugins._plugins.models.wipp import WIPPPluginManifest __all__ = [ diff --git a/src/polus/plugins/_plugins/models/compute.py b/src/polus/plugins/_plugins/models/compute.py index ef0874a46..8e0ab3eb7 100644 --- a/src/polus/plugins/_plugins/models/compute.py +++ b/src/polus/plugins/_plugins/models/compute.py @@ -7,32 +7,22 @@ from typing import List from polus.plugins._plugins.io import IOBase, Version -from polus.plugins._plugins.models.PolusComputeSchema import ( # CustomUIType, ????; CLTSchema ???? - ConditionEntry, - GpuVendor, - PluginHardwareRequirements, - PluginInput, - PluginInputType, - PluginOutput, - PluginOutputType, - PluginSchema, - PluginUIInput, - PluginUIOutput, - PluginUIType, - ThenEntry, - Validator, -) +from polus.plugins._plugins.models.PolusComputeSchema import (PluginInput, + PluginOutput, + PluginSchema) class PluginInput(PluginInput, IOBase): - pass + """Base Class for Input Args.""" class PluginOutput(PluginOutput, IOBase): - pass + """Base Class for Output Args.""" class PluginSchema(PluginSchema): + """Extended Compute Plugin Schema with extended IO defs.""" + inputs: List[PluginInput] outputs: List[PluginOutput] version: Version diff --git a/src/polus/plugins/_plugins/models/wipp.py b/src/polus/plugins/_plugins/models/wipp.py index 5139f1f9c..619d615fb 100644 --- a/src/polus/plugins/_plugins/models/wipp.py +++ b/src/polus/plugins/_plugins/models/wipp.py @@ -9,10 +9,12 @@ from pydantic import BaseModel, Field from polus.plugins._plugins.io import Input, Output, Version -from polus.plugins._plugins.models.WIPPPluginSchema import UiItem, WippPluginManifest # type: ignore +from polus.plugins._plugins.models.WIPPPluginSchema import WippPluginManifest class ui1(BaseModel): + """Base class for UI items.""" + key: str = Field(constr=r"^inputs.[a-zA-Z0-9][-a-zA-Z0-9]*$") title: str description: Optional[str] @@ -23,16 +25,22 @@ class ui1(BaseModel): class FieldSet(BaseModel): + """Base class for FieldSet.""" + title: str fields: List[str] = Field(min_items=1, unique_items=True) class ui2(BaseModel): + """UI items class for fieldsets.""" + key: Literal["fieldsets"] fieldsets: List[FieldSet] = Field(min_items=1, unique_items=True) class WIPPPluginManifest(WippPluginManifest): + """Extended WIPP Plugin Schema.""" + inputs: List[Input] = Field( ..., description="Defines inputs to the plugin", title="List of Inputs" ) diff --git a/src/polus/plugins/_plugins/registry.py b/src/polus/plugins/_plugins/registry.py index ebca77157..18e17f29c 100644 --- a/src/polus/plugins/_plugins/registry.py +++ b/src/polus/plugins/_plugins/registry.py @@ -9,20 +9,19 @@ import xmltodict from tqdm import tqdm -from polus.plugins._plugins.classes.plugin_classes import ComputePlugin, Plugin -from polus.plugins._plugins.classes.plugin_classes import _Plugins as plugins -from polus.plugins._plugins.classes.plugin_classes import submit_plugin +from polus.plugins._plugins.classes import (ComputePlugin, Plugin, refresh, + submit_plugin) from polus.plugins._plugins.registry_utils import _generate_query, _to_xml logger = logging.getLogger("polus.plugins") class FailedToPublish(Exception): - pass + """Raised when there is an error publishing a resource.""" class MissingUserInfo(Exception): - pass + """Raised when necessary user info is not provided for authentication.""" class WippPluginRegistry: @@ -35,6 +34,7 @@ def __init__( registry_url: str = "https://wipp-registry.ci.ncats.io", verify: bool = True, # verify SSL? ): + """Initialize WippPluginRegistry from username, password, registry url.""" self.registry_url = registry_url self.username = username self.password = password @@ -42,13 +42,13 @@ def __init__( @classmethod def _parse_xml(cls, xml: str): - """Returns dictionary of Plugin Manifest. If error, returns None.""" + """Return dictionary of Plugin Manifest. If error, return None.""" d = xmltodict.parse(xml)["Resource"]["role"]["PluginManifest"][ "PluginManifestContent" ]["#text"] try: return json.loads(d) - except: + except BaseException: e = eval(d) if isinstance(e, dict): return e @@ -56,6 +56,7 @@ def _parse_xml(cls, xml: str): return None def update_plugins(self): + """Update plugins from WIPP Registry.""" url = self.registry_url + "/rest/data/query/" headers = {"Content-type": "application/json"} data = '{"query": {"$or":[{"Resource.role.type":"Plugin"},{"Resource.role.type.#text":"Plugin"}]}}' @@ -74,7 +75,7 @@ def update_plugins(self): for r in tqdm(r.json()["results"], desc="Updating Plugins from WIPP"): try: manifest = WippPluginRegistry._parse_xml(r["xml_content"]) - plugin = submit_plugin(manifest) + submit_plugin(manifest) valid += 1 except BaseException as err: invalid.update({r["title"]: err.args[0]}) @@ -87,7 +88,7 @@ def update_plugins(self): % (valid) ) logger.debug("Submitted %s plugins successfully." % (valid)) - plugins.refresh() + refresh() def query( self, @@ -126,7 +127,6 @@ def query( Returns: An array of the manifests of the Plugins returned by the query. """ - url = self.registry_url + "/rest/data/query/" headers = {"Content-type": "application/json"} query = _generate_query( @@ -151,7 +151,7 @@ def query( ] def get_current_schema(self): - """Return current schema in WIPP""" + """Return current schema in WIPP.""" r = requests.get( urljoin( self.registry_url, @@ -257,10 +257,10 @@ def patch_resource( pid, version, ): + """Patch resource in registry.""" if self.username is None or self.password is None: raise MissingUserInfo("The registry connection must be authenticated.") - """Patch resource.""" # Get current version of the resource data = self.get_resource_by_pid(pid) diff --git a/src/polus/plugins/_plugins/update.py b/src/polus/plugins/_plugins/update.py index 8c6925ed1..5effe4b54 100644 --- a/src/polus/plugins/_plugins/update.py +++ b/src/polus/plugins/_plugins/update.py @@ -9,10 +9,8 @@ from polus.plugins._plugins.classes import submit_plugin from polus.plugins._plugins.gh import _init_github from polus.plugins._plugins.io import Version -from polus.plugins._plugins.manifests.manifest_utils import ( - _error_log, - _scrape_manifests, -) +from polus.plugins._plugins.manifests.manifest_utils import (_error_log, + _scrape_manifests) logger = logging.getLogger("polus.plugins") @@ -20,6 +18,7 @@ def update_polus_plugins( gh_auth: typing.Optional[str] = None, min_depth: int = 2, max_depth: int = 3 ): + """Scrape PolusAI GitHub repo and create local versions of Plugins.""" logger.info("Updating polus plugins.") # Get all manifests valid, invalid = _scrape_manifests( @@ -87,6 +86,7 @@ def update_polus_plugins( def update_nist_plugins(gh_auth: typing.Optional[str] = None): + """Scrape NIST GitHub repo and create local versions of Plugins.""" # Parse README links gh = _init_github(gh_auth) repo = gh.get_repo("usnistgov/WIPP") diff --git a/src/polus/plugins/_plugins/utils.py b/src/polus/plugins/_plugins/utils.py index 192fc02e3..6d9b19ddb 100644 --- a/src/polus/plugins/_plugins/utils.py +++ b/src/polus/plugins/_plugins/utils.py @@ -3,6 +3,7 @@ def name_cleaner(name: str) -> str: + """Generate Plugin Class Name from Plugin name in manifest.""" replace_chars = "()<>-_" for char in replace_chars: name = name.replace(char, " ") @@ -10,15 +11,7 @@ def name_cleaner(name: str) -> str: def cast_version(value): + """Return Version object from version str or dict.""" if isinstance(value, dict): # if init from a Version object value = value["version"] return Version(version=value) - - -# input_types = { -# "path": "Directory", # always Dir? -# "string": "string", -# "number": "double", -# "boolean": "boolean" -# # not yet implemented: array -# } From cd70c77c50f0e1d4812354819fb72dfc103d7908 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Mar 2023 09:15:16 -0500 Subject: [PATCH 05/47] refactor: fixed isort and black conflicts --- .pre-commit-config.yaml | 32 ++++--------- pyproject.toml | 35 +++++++------- setup.py | 46 ------------------- src/polus/plugins/__init__.py | 36 ++++++++++----- src/polus/plugins/__main__.py | 18 -------- .../plugins/_plugins/classes/__init__.py | 15 +++--- .../_plugins/classes/plugin_classes.py | 24 +++++++--- src/polus/plugins/_plugins/models/__init__.py | 5 +- src/polus/plugins/_plugins/models/compute.py | 14 +++--- src/polus/plugins/_plugins/registry.py | 3 +- src/polus/plugins/_plugins/update.py | 6 ++- 11 files changed, 95 insertions(+), 139 deletions(-) delete mode 100644 setup.py delete mode 100644 src/polus/plugins/__main__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7ca1c341..f39790700 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,17 +14,11 @@ repos: args: ["--no-sort-keys"] - id: check-added-large-files args: ["--maxkb=500"] - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py - - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 - exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py + exclude: ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ additional_dependencies: ["flake8-docstrings==1.7.0"] - repo: https://github.com/myint/autoflake rev: v2.0.1 @@ -35,7 +29,8 @@ repos: rev: 5.12.0 hooks: - id: isort - exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py + exclude: ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ + args: ["--profile", "black", "--filter-files"] - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: @@ -45,21 +40,10 @@ repos: rev: v1.0.0 hooks: - id: mypy - args: - [ - "--disable-error-code", - "no-redef", - "--disable-error-code", - "valid-type", - "--disable-error-code", - "import", - ] - exclude: src/polus/plugins/_plugins/models/PolusComputeSchema.py|src/polus/plugins/_plugins/models/WIPPPluginSchema.py + exclude: ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ additional_dependencies: [types-requests==2.28.11.8] - - repo: https://github.com/python-poetry/poetry - rev: "1.3.0" # add version here + - repo: https://github.com/psf/black + rev: 23.1.0 hooks: - - id: poetry-check - - id: poetry-lock - - id: poetry-export - args: ["-f", "requirements.txt", "-o", "requirements.txt"] + - id: black + exclude: ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ diff --git a/pyproject.toml b/pyproject.toml index 7ce326eb5..da9fb1775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,36 @@ [tool.poetry] +authors = ["Nicholas Schaub "] +description = "Python API to configure and run Polus Plugins." +license = "License :: OSI Approved :: MIT License" +maintainers = ["Camilo Velez "] -readme = "README.md" packages = [{include = "polus", from = "src"}] - -[tool.mypy] -exclude = "src/polus" +readme = "README.md" +repository = "https://github.com/PolusAI/polus-plugins" +version = "0.1.0" [tool.poetry.dependencies] +click = "^8.1.3" +fsspec = "^2023.1.0" +pydantic = "^1.10.4" +pygithub = "^1.57" python = "^3.9" python-on-whales = "^0.57.0" -pygithub = "^1.57" -pydantic = "^1.10.4" tqdm = "^4.64.1" xmltodict = "^0.13.0" -fsspec = "^2023.1.0" -click = "^8.1.3" [tool.poetry.group.dev.dependencies] -nox = "^2022.11.21" -pre-commit = "^3.0.4" -pydantic-to-typescript = "^1.0.10" -datamodel-code-generator = "^0.17.1" black = "^23.1.0" +datamodel-code-generator = "^0.17.1" flake8 = "^6.0.0" +nox = "^2022.11.21" poetry = "^1.3.2" +pre-commit = "^3.0.4" +pydantic-to-typescript = "^1.0.10" [build-system] -requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +requires = ["poetry-core"] + +[tool.isort] +profile = "black" diff --git a/setup.py b/setup.py deleted file mode 100644 index ae258fb31..000000000 --- a/setup.py +++ /dev/null @@ -1,46 +0,0 @@ -"""polus-plugins setup.py.""" - -from setuptools import find_packages, setup - -# with open("README.md", "r") as fh: -# long_description = fh.read() - -with open("./polus/_plugins/VERSION") as fh: - version = fh.read() - with open("./polus/_plugins/VERSION", "w") as fw: - fw.write(version) - -package_data = ["_plugins/VERSION", "_plugins/manifests/*"] - -setup( - name="polus-plugins", - version=version, - author="Nick Schaub", - author_email="nick.schaub@nih.gov", - description="API for Polus Plugins.", - # long_description=long_description, - # long_description_content_type="text/markdown", - # project_urls={ - # 'Documentation': 'https://bfio.readthedocs.io/en/latest/', - # 'Source': 'https://github.com/polusai/polus-data' - # }, - # entry_points={'napari.plugin': 'bfio = bfio.bfio'}, - packages=find_packages(), - package_data={"polus": package_data}, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires=">=3.7", - install_requires=[ - "pygithub>=1.55", - "docker>=5.0.3", - "pydantic>=1.8.2", - "python_on_whales>=0.34.0", - "tqdm>=4.64.0", - "xmltodict>=0.12.0", - "fsspec>=2021.11.0", - ], -) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index a3e83ec10..888b47303 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -3,9 +3,16 @@ import logging from polus.plugins._plugins.classes import ( # noqa # pylint: disable=unused-import - get_plugin, list_plugins, load_plugin, refresh) + get_plugin, + list_plugins, + load_plugin, + refresh, + submit_plugin, +) from polus.plugins._plugins.update import ( # noqa # pylint: disable=unused-import - update_nist_plugins, update_polus_plugins) + update_nist_plugins, + update_polus_plugins, +) """ Set up logging for the module @@ -18,14 +25,23 @@ refresh() # calls the refresh method when library is imported __plugins = list_plugins() -for _p in __plugins: - # make each plugin available as polus.plugins.PluginName - globals()[_p] = get_plugin(_p) +# for _p in __plugins: +# # make each plugin available as polus.plugins.PluginName +# globals()[_p] = get_plugin(_p) + +# plugin_list = list_plugins() -plugin_list = list_plugins() -_export_list = [ - "plugin_list", +def __getattr__(name): + if name == "list": + return __plugins + if name in __plugins: + return get_plugin(name) + else: + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +__all__ = [ "refresh", "submit_plugin", "get_plugin", @@ -33,6 +49,4 @@ "list_plugins", "update_polus_plugins", "update_nist_plugins", -] + __plugins - -__all__ = _export_list +] diff --git a/src/polus/plugins/__main__.py b/src/polus/plugins/__main__.py deleted file mode 100644 index 36b67e430..000000000 --- a/src/polus/plugins/__main__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -polus-plugins main.py. - -main.py -""" - -import click - - -@click.command() -@click.option("--name", "-r", default="world", type=str) -def main(name): - """Parse a timezone and greet a location a number of times.""" - print(f"Hello {name}!") - - -if __name__ == "__main__": - main() diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 7e2740277..4d399675d 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,10 +1,13 @@ """Plugin classes and functions.""" -from polus.plugins._plugins.classes.plugin_classes import (ComputePlugin, - Plugin, get_plugin, - list_plugins, - load_plugin, - refresh, - submit_plugin) +from polus.plugins._plugins.classes.plugin_classes import ( + ComputePlugin, + Plugin, + get_plugin, + list_plugins, + load_plugin, + refresh, + submit_plugin, +) __all__ = [ "Plugin", diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 05fc31310..2093b3f6f 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -9,12 +9,22 @@ from pydantic import Extra from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import (DuplicateVersionFound, Version, - _in_old_to_new, _ui_old_to_new) -from polus.plugins._plugins.manifests.manifest_utils import (_load_manifest, - validate_manifest) -from polus.plugins._plugins.models import (ComputeSchema, PluginUIInput, - PluginUIOutput, WIPPPluginManifest) +from polus.plugins._plugins.io import ( + DuplicateVersionFound, + Version, + _in_old_to_new, + _ui_old_to_new, +) +from polus.plugins._plugins.manifests.manifest_utils import ( + _load_manifest, + validate_manifest, +) +from polus.plugins._plugins.models import ( + ComputeSchema, + PluginUIInput, + PluginUIOutput, + WIPPPluginManifest, +) from polus.plugins._plugins.utils import cast_version, name_cleaner logger = logging.getLogger("polus.plugins") @@ -326,7 +336,7 @@ def load_plugin( ) -> typing.Union[Plugin, ComputePlugin]: """Parse a manifest and return one of Plugin or ComputePlugin.""" manifest = _load_manifest(manifest) - if "pluginHardwareRequirements" in manifest: # type: ignore[operator] + if "pluginHardwareRequirements" in manifest: # type: ignore[operator] # Parse the manifest plugin = ComputePlugin(**manifest) # type: ignore[arg-type] else: diff --git a/src/polus/plugins/_plugins/models/__init__.py b/src/polus/plugins/_plugins/models/__init__.py index 0c5df0ced..ce1d984dd 100644 --- a/src/polus/plugins/_plugins/models/__init__.py +++ b/src/polus/plugins/_plugins/models/__init__.py @@ -1,6 +1,9 @@ """Pydantic Models based on JSON schemas.""" from polus.plugins._plugins.models.compute import PluginSchema as ComputeSchema -from polus.plugins._plugins.models.PolusComputeSchema import PluginUIInput, PluginUIOutput +from polus.plugins._plugins.models.PolusComputeSchema import ( + PluginUIInput, + PluginUIOutput, +) from polus.plugins._plugins.models.wipp import WIPPPluginManifest __all__ = [ diff --git a/src/polus/plugins/_plugins/models/compute.py b/src/polus/plugins/_plugins/models/compute.py index 8e0ab3eb7..8584143c3 100644 --- a/src/polus/plugins/_plugins/models/compute.py +++ b/src/polus/plugins/_plugins/models/compute.py @@ -7,20 +7,22 @@ from typing import List from polus.plugins._plugins.io import IOBase, Version -from polus.plugins._plugins.models.PolusComputeSchema import (PluginInput, - PluginOutput, - PluginSchema) +from polus.plugins._plugins.models.PolusComputeSchema import ( + PluginInput, + PluginOutput, + PluginSchema, +) -class PluginInput(PluginInput, IOBase): +class PluginInput(PluginInput, IOBase): # type: ignore """Base Class for Input Args.""" -class PluginOutput(PluginOutput, IOBase): +class PluginOutput(PluginOutput, IOBase): # type: ignore """Base Class for Output Args.""" -class PluginSchema(PluginSchema): +class PluginSchema(PluginSchema): # type: ignore """Extended Compute Plugin Schema with extended IO defs.""" inputs: List[PluginInput] diff --git a/src/polus/plugins/_plugins/registry.py b/src/polus/plugins/_plugins/registry.py index 18e17f29c..e09f9a1c3 100644 --- a/src/polus/plugins/_plugins/registry.py +++ b/src/polus/plugins/_plugins/registry.py @@ -9,8 +9,7 @@ import xmltodict from tqdm import tqdm -from polus.plugins._plugins.classes import (ComputePlugin, Plugin, refresh, - submit_plugin) +from polus.plugins._plugins.classes import ComputePlugin, Plugin, refresh, submit_plugin from polus.plugins._plugins.registry_utils import _generate_query, _to_xml logger = logging.getLogger("polus.plugins") diff --git a/src/polus/plugins/_plugins/update.py b/src/polus/plugins/_plugins/update.py index 5effe4b54..87fdcfd0d 100644 --- a/src/polus/plugins/_plugins/update.py +++ b/src/polus/plugins/_plugins/update.py @@ -9,8 +9,10 @@ from polus.plugins._plugins.classes import submit_plugin from polus.plugins._plugins.gh import _init_github from polus.plugins._plugins.io import Version -from polus.plugins._plugins.manifests.manifest_utils import (_error_log, - _scrape_manifests) +from polus.plugins._plugins.manifests.manifest_utils import ( + _error_log, + _scrape_manifests, +) logger = logging.getLogger("polus.plugins") From 77c09b764a165e832eb193dd852545471f9e5d19 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Mar 2023 17:02:08 -0500 Subject: [PATCH 06/47] refactor: removed unused code in __init__.py --- src/polus/plugins/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 888b47303..681a3f837 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -25,12 +25,6 @@ refresh() # calls the refresh method when library is imported __plugins = list_plugins() -# for _p in __plugins: -# # make each plugin available as polus.plugins.PluginName -# globals()[_p] = get_plugin(_p) - -# plugin_list = list_plugins() - def __getattr__(name): if name == "list": From d4088b19f3eb79bc642e2605e1d62c7e38e275a3 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Mar 2023 17:04:48 -0500 Subject: [PATCH 07/47] refactor: removed extra comments --- src/polus/plugins/_plugins/manifests/manifest_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py index e51188b0b..e22b4985b 100644 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ b/src/polus/plugins/_plugins/manifests/manifest_utils.py @@ -81,7 +81,7 @@ def validate_manifest( if "pluginHardwareRequirements" in manifest: # Parse the manifest try: - plugin = ComputeSchema(**manifest) # New Schema + plugin = ComputeSchema(**manifest) except ValidationError as err: raise err except BaseException as e: @@ -89,7 +89,7 @@ def validate_manifest( else: # Parse the manifest try: - plugin = WIPPPluginManifest(**manifest) # New Schema + plugin = WIPPPluginManifest(**manifest) except ValidationError as err: logger.info(manifest) raise err From 5aea47e40ac7a06bb65ce755af9d08532c715dab Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Mar 2023 21:22:00 -0500 Subject: [PATCH 08/47] fix: fixed missing character in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index da9fb1775..922c0e39f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ authors = ["Nicholas Schaub "] description = "Python API to configure and run Polus Plugins." license = "License :: OSI Approved :: MIT License" -maintainers = ["Camilo Velez "] name = "polus-plugins" packages = [{include = "polus", from = "src"}] readme = "README.md" From 4f0910116c98d971d84dbaef83bf0f76656acfe3 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Mar 2023 23:30:19 -0500 Subject: [PATCH 09/47] feat: improved error handling of manifests' validation errors --- .gitignore | 5 +++ src/polus/plugins/__init__.py | 2 + .../plugins/_plugins/classes/__init__.py | 2 + .../_plugins/classes/plugin_classes.py | 44 ++++++++++++++----- .../_plugins/manifests/manifest_utils.py | 21 ++++++--- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index c6f865e76..aecb69a90 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,8 @@ cython_debug/ # test data directory data + +# local manifests +src/polus/plugins/_plugins/manifests/* +# allow python scripts insied manifests dir +!src/polus/plugins/_plugins/manifests/*.py diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 681a3f837..15bae7f5f 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -6,6 +6,7 @@ get_plugin, list_plugins, load_plugin, + print_invalid, refresh, submit_plugin, ) @@ -41,6 +42,7 @@ def __getattr__(name): "get_plugin", "load_plugin", "list_plugins", + "print_invalid", "update_polus_plugins", "update_nist_plugins", ] diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 4d399675d..2b4478c00 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -5,6 +5,7 @@ get_plugin, list_plugins, load_plugin, + print_invalid, refresh, submit_plugin, ) @@ -17,4 +18,5 @@ "get_plugin", "refresh", "list_plugins", + "print_invalid", ] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 2093b3f6f..87d47410f 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -16,6 +16,7 @@ _ui_old_to_new, ) from polus.plugins._plugins.manifests.manifest_utils import ( + InvalidManifest, _load_manifest, validate_manifest, ) @@ -89,6 +90,9 @@ def refresh(): x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" ] # ignore __pycache__ + global _invalid + _invalid = {} + for org in organizations: if org.is_file(): continue @@ -97,18 +101,34 @@ def refresh(): if file.suffix == ".py": continue - plugin = validate_manifest(file) - key = name_cleaner(plugin.name) - # Add version and path to VERSIONS - if key not in PLUGINS: - PLUGINS[key] = {} - if plugin.version in PLUGINS[key]: - if not file == PLUGINS[key][plugin.version]: - raise DuplicateVersionFound( - "Found duplicate version of plugin %s in %s" - % (plugin.name, PLUGIN_DIR) - ) - PLUGINS[key][plugin.version] = file + try: + plugin = validate_manifest(file) + except InvalidManifest as e: + _invalid[_load_manifest(file)["name"]] = str(e.__cause__) + else: + key = name_cleaner(plugin.name) + # Add version and path to VERSIONS + if key not in PLUGINS: + PLUGINS[key] = {} + if plugin.version in PLUGINS[key]: + if not file == PLUGINS[key][plugin.version]: + raise DuplicateVersionFound( + "Found duplicate version of plugin %s in %s" + % (plugin.name, PLUGIN_DIR) + ) + PLUGINS[key][plugin.version] = file + + if len(_invalid) > 0: + logger.warning( + f"local manifests {[str(x) for x in _invalid.keys()]} are invalid. Run polus.plugins.print_invalid() for more details." + ) + + +def print_invalid(): + """Print invalid manifests with respective validation errors.""" + for x, y in _invalid.items(): + print(x) + print(y + "\n") _r = refresh diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py index e22b4985b..311ab67ba 100644 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ b/src/polus/plugins/_plugins/manifests/manifest_utils.py @@ -28,6 +28,10 @@ ] +class InvalidManifest(Exception): + """Raised when manifest has validation errors.""" + + def is_valid_manifest(plugin: dict) -> bool: """Validate basic attributes of a plugin manifest. @@ -78,23 +82,28 @@ def validate_manifest( manifest["version"] = cast_version( manifest["version"] ) # cast version to semver object + if "name" in manifest: + name = manifest["name"] + else: + raise InvalidManifest(f"{manifest} has no value for name") + if "pluginHardwareRequirements" in manifest: # Parse the manifest try: plugin = ComputeSchema(**manifest) - except ValidationError as err: - raise err + except ValidationError as e: + raise InvalidManifest(f"{name} does not conform to schema") from e except BaseException as e: raise e else: # Parse the manifest try: plugin = WIPPPluginManifest(**manifest) - except ValidationError as err: - logger.info(manifest) - raise err + except ValidationError as e: + raise InvalidManifest( + f"{manifest['name']} does not conform to schema" + ) from e except BaseException as e: - logger.info(manifest) raise e return plugin From 6c62833d238b2233e8e0406b19562727dfa22e18 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 10 Mar 2023 11:49:56 -0500 Subject: [PATCH 10/47] refactor: removed logger config --- pyproject.toml | 7 ++++++- src/polus/plugins/__init__.py | 4 ---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 922c0e39f..b414c3f26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -authors = ["Nicholas Schaub "] +authors = ["Nicholas Schaub ", "Camilo Velez "] description = "Python API to configure and run Polus Plugins." license = "License :: OSI Approved :: MIT License" maintainers = ["Camilo Velez "] @@ -34,3 +34,8 @@ requires = ["poetry-core"] [tool.isort] profile = "black" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 681a3f837..44f35664e 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -17,10 +17,6 @@ """ Set up logging for the module """ -logging.basicConfig( - format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", - datefmt="%d-%b-%y %H:%M:%S", -) logger = logging.getLogger("polus.plugins") refresh() # calls the refresh method when library is imported __plugins = list_plugins() From 604b5300b64875cdd245ab56df9fbc25063c1a02 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Wed, 15 Mar 2023 01:16:28 -0400 Subject: [PATCH 11/47] feat: added cwl methods --- poetry.lock | 4 +- pyproject.toml | 1 + .../_plugins/classes/plugin_methods.py | 103 ++++++++++++++++++ src/polus/plugins/_plugins/cwl/__init__.py | 3 + src/polus/plugins/_plugins/cwl/base.cwl | 17 +++ src/polus/plugins/_plugins/cwl/cwl.py | 7 ++ src/polus/plugins/_plugins/io.py | 91 +++++++++++++++- 7 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/polus/plugins/_plugins/cwl/__init__.py create mode 100644 src/polus/plugins/_plugins/cwl/base.cwl create mode 100644 src/polus/plugins/_plugins/cwl/cwl.py diff --git a/poetry.lock b/poetry.lock index b9486ac60..92a95c241 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1704,7 +1704,7 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2418,4 +2418,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "41b354db5107c27e95db6ff1adee8ba1aebf89511a275d0ac6f0ecb4315854fe" +content-hash = "762706d491fa3eacac75871a6bc89cfe83a698ebad17dfc86380bc7c0a35da06" diff --git a/pyproject.toml b/pyproject.toml index 922c0e39f..5d63bd8ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ python = "^3.9" python-on-whales = "^0.57.0" tqdm = "^4.64.1" xmltodict = "^0.13.0" +pyyaml = "^6.0" [tool.poetry.group.dev.dependencies] black = "^23.1.0" diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index 2455ed1e8..7e9643055 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -6,10 +6,23 @@ import random import signal import typing +from os.path import relpath import fsspec +import yaml # type: ignore +from cwltool.context import RuntimeContext +from cwltool.factory import Factory from python_on_whales import docker +from polus.plugins._plugins.cwl import CWL_BASE_DICT +from polus.plugins._plugins.io import ( + input_to_cwl, + io_to_yml, + output_to_cwl, + outputs_cwl, +) +from polus.plugins._plugins.utils import name_cleaner + logger = logging.getLogger("polus.plugins") @@ -17,7 +30,19 @@ class IOKeyError(Exception): """Raised when trying to set invalid I/O parameter.""" +class MissingInputValues(Exception): + """Raised when there are required input values that have not been set.""" + + class _PluginMethods: + def _check_inputs(self): + """Check if all required inputs have been set.""" + _in = [x for x in self.inputs if x.required and not x.value] # type: ignore + if len(_in) > 0: + raise MissingInputValues( + f"{[x.name for x in _in]} are required inputs but have not been set" # type: ignore + ) + @property def organization(self): return self.containerId.split("/")[0] @@ -40,6 +65,7 @@ def run( gpus: typing.Union[None, str, int] = "all", **kwargs, ): + self._check_inputs() inp_dirs = [] out_dirs = [] @@ -187,6 +213,83 @@ def __setattr__(self, name, value): super().__setattr__(name, value) + def _to_cwl(self): + """Return CWL yml as dict.""" + cwl_dict = CWL_BASE_DICT + cwl_dict["inputs"] = {} + cwl_dict["outputs"] = {} + inputs = [input_to_cwl(x) for x in self.inputs] + inputs = inputs + [output_to_cwl(x) for x in self.outputs] + for inp in inputs: + cwl_dict["inputs"].update(inp) + outputs = [outputs_cwl(x) for x in self.outputs] + for out in outputs: + cwl_dict["outputs"].update(out) + cwl_dict["requirements"]["DockerRequirement"]["dockerPull"] = self.containerId + return cwl_dict + + def save_cwl(self, path: typing.Union[str, pathlib.Path]): + """Save plugin as CWL command line tool.""" + assert str(path).split(".")[-1] == "cwl", "Path must end in .cwl" + with open(path, "w") as file: + yaml.dump(self._to_cwl(), file) + return path + + @property + def _cwl_io(self) -> dict: + """Dict of I/O for CWL.""" + return { + x.name: io_to_yml(x) for x in self._io_keys.values() if x.value is not None + } + + def save_cwl_io(self, path): + """Save plugin's I/O values to yml file to be used with CWL command line tool.""" + self._check_inputs() + assert str(path).split(".")[-1] == "yml", "Path must end in .yml" + with open(path, "w") as file: + yaml.dump(self._cwl_io, file) + return path + + def run_cwl( + self, + cwl_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + io_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + ): + """Run configured plugin in CWL. + + Run plugin as a CWL command line tool after setting I/O values. + Two files will be generated: a CWL (`.cwl`) command line tool + and an I/O file (`.yml`). They will be generated in + current working directory if no paths are specified. Optional paths + for these files can be specified with arguments `cwl_path`, + and `io_path` respectively. + + Args: + cwl_path: [Optional] target path for `.cwl` file + io_path: [Optional] target path for `.yml` file + + """ + if not self.outDir: + raise ValueError("") + + if not cwl_path: + _p = pathlib.Path.cwd().joinpath(name_cleaner(self.name) + ".cwl") + _cwl = self.save_cwl(_p) + else: + _cwl = self.save_cwl(cwl_path) + + if not io_path: + _p = pathlib.Path.cwd().joinpath(name_cleaner(self.name) + ".yml") + self.save_cwl_io(_p) # saves io to make it visible to user + else: + self.save_cwl_io(io_path) # saves io to make it visible to user + + outdir_path = relpath(self.outDir.parent) # type: ignore + rc = RuntimeContext({"outdir": outdir_path}) + fac = Factory(runtime_context=rc) + cwl = fac.make(str(_cwl)) + return cwl(**self._cwl_io) # object's io dict is used instead of .yml file + def __lt__(self, other): return self.version < other.version diff --git a/src/polus/plugins/_plugins/cwl/__init__.py b/src/polus/plugins/_plugins/cwl/__init__.py new file mode 100644 index 000000000..966ef2d46 --- /dev/null +++ b/src/polus/plugins/_plugins/cwl/__init__.py @@ -0,0 +1,3 @@ +from .cwl import CWL_BASE_DICT + +__all__ = ["CWL_BASE_DICT"] diff --git a/src/polus/plugins/_plugins/cwl/base.cwl b/src/polus/plugins/_plugins/cwl/base.cwl new file mode 100644 index 000000000..7a869228e --- /dev/null +++ b/src/polus/plugins/_plugins/cwl/base.cwl @@ -0,0 +1,17 @@ +#!/usr/bin/env cwl-runner + +cwlVersion: v1.2 +class: CommandLineTool + +requirements: + DockerRequirement: + dockerPull: + InitialWorkDirRequirement: + listing: + - writable: true + entry: $(inputs.outDir) + InlineJavascriptRequirement: {} + +inputs: + +outputs: diff --git a/src/polus/plugins/_plugins/cwl/cwl.py b/src/polus/plugins/_plugins/cwl/cwl.py new file mode 100644 index 000000000..59a1163d9 --- /dev/null +++ b/src/polus/plugins/_plugins/cwl/cwl.py @@ -0,0 +1,7 @@ +from pathlib import Path + +import yaml # type: ignore + +PATH = Path(__file__) +with open(PATH.with_name("base.cwl"), "rb") as cwl_file: + CWL_BASE_DICT = yaml.full_load(cwl_file) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index bb4139dac..917424e64 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -1,9 +1,11 @@ +# type: ignore """Plugins I/O utilities.""" import enum import logging import pathlib import re import typing +from functools import singledispatch import fsspec from pydantic import BaseModel, Field, PrivateAttr, constr, validator @@ -34,7 +36,7 @@ class InputTypes(str, enum.Enum): # wipp schema - """Enum of Inpyt Types for WIPP schema.""" + """Enum of Input Types for WIPP schema.""" collection = "collection" pyramid = "pyramid" @@ -313,3 +315,90 @@ def __hash__(self): class DuplicateVersionFound(Exception): """Raise when two equal versions found.""" + + +"""CWL""" + +cwl_input_types = { + "path": "Directory", # always Dir? Yes + "string": "string", + "number": "double", + "boolean": "boolean", + "genericData": "Directory" + # not yet implemented: array +} + + +def _type_in(input: Input): + """Return appropriate value for `type` based on input type.""" + val = input.type.value + req = "" if input.required else "?" + if val == "enum": + if input.required: + s = [{"type": "enum", "symbols": input.options["values"]}] + else: + s = ["null", {"type": "enum", "symbols": input.options["values"]}] + + elif val in cwl_input_types: + s = cwl_input_types[val] + req + else: + s = "string" + req # defaults to string + return s + + +def input_to_cwl(input): + """Return dict of inputs for cwl.""" + r = { + f"{input.name}": { + "type": _type_in(input), + "inputBinding": {"prefix": f"--{input.name}"}, + } + } + return r + + +def output_to_cwl(o): + """Return dict of output args for cwl for input section.""" + r = { + f"{o.name}": { + "type": "Directory", + "inputBinding": {"prefix": f"--{o.name}"}, + } + } + return r + + +def outputs_cwl(o): + """Return dict of output for `outputs` in cwl.""" + r = { + f"{o.name}": { + "type": {"type": "array", "items": ["File", "Directory"]}, + "outputBinding": {"glob": f"$(inputs.{o.name}.basename)"}, + } + } + return r + + +# -- I/O as arguments in .yml + + +@singledispatch +def _io_value_to_yml(io) -> str | dict: + return str(io) + + +@_io_value_to_yml.register +def _(io: pathlib.Path): + r = {"class": "Directory", "location": str(io)} + return r + + +@_io_value_to_yml.register +def _(io: enum.Enum): + r = io.name + return r + + +def io_to_yml(io): + """Return IO entry for yml file.""" + return _io_value_to_yml(io.value) From 5f8761068b2664890357538791aff7dc17db61f5 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Wed, 15 Mar 2023 01:43:20 -0400 Subject: [PATCH 12/47] build: modified authors in pyproject.toml --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 922c0e39f..b414c3f26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -authors = ["Nicholas Schaub "] +authors = ["Nicholas Schaub ", "Camilo Velez "] description = "Python API to configure and run Polus Plugins." license = "License :: OSI Approved :: MIT License" maintainers = ["Camilo Velez "] @@ -34,3 +34,8 @@ requires = ["poetry-core"] [tool.isort] profile = "black" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] From 6cae5c697b01ab237bcf267a8512005e494cf3d9 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Wed, 15 Mar 2023 01:44:24 -0400 Subject: [PATCH 13/47] build: modified authors in pyproject.toml --- pyproject.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d63bd8ed..b414c3f26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -authors = ["Nicholas Schaub "] +authors = ["Nicholas Schaub ", "Camilo Velez "] description = "Python API to configure and run Polus Plugins." license = "License :: OSI Approved :: MIT License" maintainers = ["Camilo Velez "] @@ -18,7 +18,6 @@ python = "^3.9" python-on-whales = "^0.57.0" tqdm = "^4.64.1" xmltodict = "^0.13.0" -pyyaml = "^6.0" [tool.poetry.group.dev.dependencies] black = "^23.1.0" @@ -35,3 +34,8 @@ requires = ["poetry-core"] [tool.isort] profile = "black" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] From 08ef8f597b57ecdf05aadd54a59337ed0550f7cc Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 14 Apr 2023 10:24:21 -0400 Subject: [PATCH 14/47] refactor: removed 'files' from outputbinding --- src/polus/plugins/_plugins/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 917424e64..0d17d1ab2 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -372,7 +372,7 @@ def outputs_cwl(o): """Return dict of output for `outputs` in cwl.""" r = { f"{o.name}": { - "type": {"type": "array", "items": ["File", "Directory"]}, + "type": "directory", "outputBinding": {"glob": f"$(inputs.{o.name}.basename)"}, } } From 1f14b0ab9cab752a493b59369c88e9473e9c0947 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Sun, 23 Apr 2023 23:19:04 -0400 Subject: [PATCH 15/47] fix: fixed Union operator for Python <3.10 --- src/polus/plugins/_plugins/io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index bb4139dac..d08911ed6 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -1,3 +1,4 @@ +# type: ignore """Plugins I/O utilities.""" import enum import logging @@ -220,7 +221,7 @@ def __init__(self, **data): ) -def _check_version_number(value: str | int) -> bool: +def _check_version_number(value: typing.Union[str, int]) -> bool: if isinstance(value, int): value = str(value) if "-" in value: From 95ec314ef1fb3344c080097afe07210e53162855 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 24 Apr 2023 00:45:29 -0400 Subject: [PATCH 16/47] refactor: removed global var invalid_manifest --- src/polus/plugins/__init__.py | 4 ++-- .../plugins/_plugins/classes/__init__.py | 2 -- .../_plugins/classes/plugin_classes.py | 23 ++++--------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index c3a1e0439..3307b4be0 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -6,7 +6,6 @@ get_plugin, list_plugins, load_plugin, - print_invalid, refresh, submit_plugin, ) @@ -19,6 +18,8 @@ Set up logging for the module """ logger = logging.getLogger("polus.plugins") + + refresh() # calls the refresh method when library is imported __plugins = list_plugins() @@ -38,7 +39,6 @@ def __getattr__(name): "get_plugin", "load_plugin", "list_plugins", - "print_invalid", "update_polus_plugins", "update_nist_plugins", ] diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 2b4478c00..4d399675d 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -5,7 +5,6 @@ get_plugin, list_plugins, load_plugin, - print_invalid, refresh, submit_plugin, ) @@ -18,5 +17,4 @@ "get_plugin", "refresh", "list_plugins", - "print_invalid", ] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 87d47410f..d2dd99f73 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -90,9 +90,6 @@ def refresh(): x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" ] # ignore __pycache__ - global _invalid - _invalid = {} - for org in organizations: if org.is_file(): continue @@ -103,8 +100,10 @@ def refresh(): try: plugin = validate_manifest(file) - except InvalidManifest as e: - _invalid[_load_manifest(file)["name"]] = str(e.__cause__) + except InvalidManifest: + logger.warning("Validation error in %s" % (str(file))) + except BaseException as e: + logger.warning(f"Unexpected error {e} with {str(file)}") else: key = name_cleaner(plugin.name) # Add version and path to VERSIONS @@ -118,20 +117,8 @@ def refresh(): ) PLUGINS[key][plugin.version] = file - if len(_invalid) > 0: - logger.warning( - f"local manifests {[str(x) for x in _invalid.keys()]} are invalid. Run polus.plugins.print_invalid() for more details." - ) - - -def print_invalid(): - """Print invalid manifests with respective validation errors.""" - for x, y in _invalid.items(): - print(x) - print(y + "\n") - -_r = refresh +_r = refresh # to use in submit_plugin since refresh is a named arg def list_plugins(): From 30a96d18ca72ec80a116e1d0630b7a8c46a7cb07 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 25 Apr 2023 00:42:34 -0400 Subject: [PATCH 17/47] fix: fixed collection not as directory type --- src/polus/plugins/_plugins/io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 0d17d1ab2..9e27db7c8 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -324,7 +324,8 @@ class DuplicateVersionFound(Exception): "string": "string", "number": "double", "boolean": "boolean", - "genericData": "Directory" + "genericData": "Directory", + "collection": "Directory" # not yet implemented: array } From e4e6cd4c2b2f13226987cf2ac082c3d10eddc0cd Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 25 Apr 2023 00:46:26 -0400 Subject: [PATCH 18/47] fix: fixed lower case d in Directory output --- src/polus/plugins/_plugins/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 9e27db7c8..18d760df0 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -373,7 +373,7 @@ def outputs_cwl(o): """Return dict of output for `outputs` in cwl.""" r = { f"{o.name}": { - "type": "directory", + "type": "Directory", "outputBinding": {"glob": f"$(inputs.{o.name}.basename)"}, } } From 51f3814dedcd990a5fea0f45a09e7ea479e67aa9 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 25 Apr 2023 00:53:34 -0400 Subject: [PATCH 19/47] refactor: refactored enums in CLT to str for compat with workflows --- src/polus/plugins/_plugins/io.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 18d760df0..d89ca7722 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -325,7 +325,8 @@ class DuplicateVersionFound(Exception): "number": "double", "boolean": "boolean", "genericData": "Directory", - "collection": "Directory" + "collection": "Directory", + "enum": "string" # for compatibility with workflows # not yet implemented: array } @@ -334,13 +335,15 @@ def _type_in(input: Input): """Return appropriate value for `type` based on input type.""" val = input.type.value req = "" if input.required else "?" - if val == "enum": - if input.required: - s = [{"type": "enum", "symbols": input.options["values"]}] - else: - s = ["null", {"type": "enum", "symbols": input.options["values"]}] - elif val in cwl_input_types: + # NOT compatible with CWL workflows, ok in CLT + # if val == "enum": + # if input.required: + # s = [{"type": "enum", "symbols": input.options["values"]}] + # else: + # s = ["null", {"type": "enum", "symbols": input.options["values"]}] + + if val in cwl_input_types: s = cwl_input_types[val] + req else: s = "string" + req # defaults to string From 49f76cc099443581cfa8c72076b70693773791b8 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 15 May 2023 15:27:03 -0400 Subject: [PATCH 20/47] build: added cwltool to pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b414c3f26..48848463c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,10 +14,11 @@ click = "^8.1.3" fsspec = "^2023.1.0" pydantic = "^1.10.4" pygithub = "^1.57" -python = "^3.9" +python = ">=3.9, <3.12" python-on-whales = "^0.57.0" tqdm = "^4.64.1" xmltodict = "^0.13.0" +cwltool = "^3.1.20230513155734" [tool.poetry.group.dev.dependencies] black = "^23.1.0" From f2ec33ad6eef61daa43dad372e0ad88c1868a8a5 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 15 May 2023 15:34:07 -0400 Subject: [PATCH 21/47] fix: removed | for unions --- src/polus/plugins/_plugins/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 53f1b372b..89ea69165 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -387,7 +387,7 @@ def outputs_cwl(o): @singledispatch -def _io_value_to_yml(io) -> str | dict: +def _io_value_to_yml(io) -> typing.Union[str, dict]: return str(io) From 11282b5154549aa72fa317a4e7101089a84439fb Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 15 May 2023 15:57:11 -0400 Subject: [PATCH 22/47] feat: added methods to remove plugins --- src/polus/plugins/__init__.py | 9 ++-- .../plugins/_plugins/classes/__init__.py | 4 ++ .../_plugins/classes/plugin_classes.py | 54 +++++++++++++------ .../_plugins/classes/plugin_methods.py | 2 +- .../_plugins/manifests/manifest_utils.py | 28 +++++----- 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 3307b4be0..bb9e66474 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -7,6 +7,8 @@ list_plugins, load_plugin, refresh, + remove_all, + remove_plugin, submit_plugin, ) from polus.plugins._plugins.update import ( # noqa # pylint: disable=unused-import @@ -21,13 +23,12 @@ refresh() # calls the refresh method when library is imported -__plugins = list_plugins() def __getattr__(name): if name == "list": - return __plugins - if name in __plugins: + return list_plugins() + if name in list_plugins(): return get_plugin(name) else: raise AttributeError(f"module '{__name__}' has no attribute '{name}'") @@ -41,4 +42,6 @@ def __getattr__(name): "list_plugins", "update_polus_plugins", "update_nist_plugins", + "remove_all", + "remove_plugin", ] diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 4d399675d..128208753 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -6,6 +6,8 @@ list_plugins, load_plugin, refresh, + remove_all, + remove_plugin, submit_plugin, ) @@ -17,4 +19,6 @@ "get_plugin", "refresh", "list_plugins", + "remove_plugin", + "remove_all", ] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index d2dd99f73..196ccfc29 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -1,7 +1,9 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" import json import logging +import os import pathlib +import shutil import typing import uuid from copy import deepcopy @@ -38,7 +40,7 @@ Paths and Fields """ # Location to store any discovered plugin manifests -PLUGIN_DIR = pathlib.Path(__file__).parent.parent.joinpath("manifests") +_PLUGIN_DIR = pathlib.Path(__file__).parent.parent.joinpath("manifests") def load_config(config: typing.Union[dict, pathlib.Path]): @@ -87,13 +89,12 @@ def get_plugin(name: str, version: typing.Optional[str] = None): def refresh(): """Refresh the plugin list.""" organizations = [ - x for x in PLUGIN_DIR.iterdir() if x.name != "__pycache__" + x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() ] # ignore __pycache__ - for org in organizations: - if org.is_file(): - continue + PLUGINS.clear() + for org in organizations: for file in org.iterdir(): if file.suffix == ".py": continue @@ -113,14 +114,11 @@ def refresh(): if not file == PLUGINS[key][plugin.version]: raise DuplicateVersionFound( "Found duplicate version of plugin %s in %s" - % (plugin.name, PLUGIN_DIR) + % (plugin.name, _PLUGIN_DIR) ) PLUGINS[key][plugin.version] = file -_r = refresh # to use in submit_plugin since refresh is a named arg - - def list_plugins(): """List all local plugins.""" output = list(PLUGINS.keys()) @@ -354,7 +352,6 @@ def load_plugin( def submit_plugin( manifest: typing.Union[str, dict, pathlib.Path], - refresh: bool = False, ): """Parse a plugin and create a local copy of it. @@ -380,13 +377,40 @@ def submit_plugin( # Save the manifest if it doesn't already exist in the database organization = plugin.containerId.split("/")[0] - org_path = PLUGIN_DIR.joinpath(organization.lower()) + org_path = _PLUGIN_DIR.joinpath(organization.lower()) org_path.mkdir(exist_ok=True, parents=True) if not org_path.joinpath(out_name).exists(): with open(org_path.joinpath(out_name), "w") as fw: - json.dump(plugin.dict(), fw, indent=4) + m = plugin.dict() + m["version"] = m["version"]["version"] + json.dump(m, fw, indent=4) - # Refresh plugins list if refresh = True - if refresh: - _r() + # Refresh plugins list + refresh() return plugin + + +def remove_plugin(plugin: str, version: typing.Optional[str] = None): + """Remove plugin from the local database.""" + if not version: + for plugin_version in PLUGINS[plugin]: + remove_plugin(plugin, plugin_version) + else: + if not isinstance(version, Version): + version_ = cast_version(version) + else: + version_ = version + path = PLUGINS[plugin][version_] + os.remove(path) + refresh() + + +def remove_all(): + """Remove all plugins from the local database.""" + organizations = [ + x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() + ] # ignore __pycache__ + logger.warning("Removing all plugins from local database") + for org in organizations: + shutil.rmtree(org) + refresh() diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index 7e9643055..7d6da40a0 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -172,7 +172,7 @@ def _config(self): @property def manifest(self): - m = json.loads(self.json(exclude={"_io_keys", "versions"})) + m = json.loads(self.json(exclude={"_io_keys", "versions", "id"})) m["version"] = m["version"]["version"] return m diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py index 311ab67ba..ebc9d0556 100644 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ b/src/polus/plugins/_plugins/manifests/manifest_utils.py @@ -70,17 +70,18 @@ def _load_manifest(m: typing.Union[str, dict, pathlib.Path]) -> dict: else: manifest = requests.get(m).json() else: - raise ValueError("invalid manifest") + msg = "invalid manifest" + raise ValueError(msg) return manifest def validate_manifest( - manifest: typing.Union[str, dict, pathlib.Path] + manifest: typing.Union[str, dict, pathlib.Path], ) -> typing.Union[WIPPPluginManifest, ComputeSchema]: """Validate a plugin manifest against schema.""" manifest = _load_manifest(manifest) manifest["version"] = cast_version( - manifest["version"] + manifest["version"], ) # cast version to semver object if "name" in manifest: name = manifest["name"] @@ -114,7 +115,7 @@ def _scrape_manifests( min_depth: int = 1, max_depth: typing.Optional[int] = None, return_invalid: bool = False, -) -> typing.Union[list, typing.Tuple[list, list]]: +) -> typing.Union[list, tuple[list, list]]: if max_depth is None: max_depth = min_depth min_depth = 0 @@ -160,15 +161,17 @@ def _error_log(val_err, manifest, fct): if isinstance(err, AssertionError): report.append( "The plugin ({}) failed an assertion check: {}".format( - manifest["name"], err.args[0] - ) + manifest["name"], + err.args[0], + ), ) logger.critical(f"{fct}: {report[-1]}") elif isinstance(err.exc, errors.MissingError): report.append( "The plugin ({}) is missing fields: {}".format( - manifest["name"], err.loc_tuple() - ) + manifest["name"], + err.loc_tuple(), + ), ) logger.critical(f"{fct}: {report[-1]}") elif errors.ExtraError: @@ -179,13 +182,14 @@ def _error_log(val_err, manifest, fct): err.loc_tuple()[0], manifest[err.loc_tuple()[0]][err.loc_tuple()[1]]["name"], err.exc.args[0][0].loc_tuple(), - ) + ), ) else: report.append( "The plugin ({}) had an error: {}".format( - manifest["name"], err.exc.args[0][0] - ) + manifest["name"], + err.exc.args[0][0], + ), ) logger.critical(f"{fct}: {report[-1]}") else: @@ -194,5 +198,5 @@ def _error_log(val_err, manifest, fct): fct, manifest["name"], str(val_err).replace("\n", ", ").replace(" ", " "), - ) + ), ) From 3bf67b72c091785ad9c269260351199a080a4d88 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 15 May 2023 16:19:04 -0400 Subject: [PATCH 23/47] feat: added refresh to update functions --- src/polus/plugins/_plugins/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/update.py b/src/polus/plugins/_plugins/update.py index 87fdcfd0d..8651fb92d 100644 --- a/src/polus/plugins/_plugins/update.py +++ b/src/polus/plugins/_plugins/update.py @@ -6,7 +6,7 @@ from pydantic import ValidationError from tqdm import tqdm -from polus.plugins._plugins.classes import submit_plugin +from polus.plugins._plugins.classes import refresh, submit_plugin from polus.plugins._plugins.gh import _init_github from polus.plugins._plugins.io import Version from polus.plugins._plugins.manifests.manifest_utils import ( @@ -85,6 +85,7 @@ def update_polus_plugins( except BaseException as e: # logger.debug(f"There was an error {e} in {plugin.name}") logger.exception(f"In {plugin.name}: {e}") + refresh() def update_nist_plugins(gh_auth: typing.Optional[str] = None): @@ -111,3 +112,4 @@ def update_nist_plugins(gh_auth: typing.Optional[str] = None): except ValidationError as val_err: _error_log(val_err, manifest, "update_nist_plugins") + refresh() From 17ff5719a5b2ee70480bc3b18eb77041fac1b77b Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 30 May 2023 09:34:20 -0400 Subject: [PATCH 24/47] refactor: general refactoring --- .../_plugins/classes/plugin_methods.py | 18 ++++++----- src/polus/plugins/_plugins/io.py | 32 +++++++++---------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index 7d6da40a0..8d2b2d16a 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -6,12 +6,12 @@ import random import signal import typing -from os.path import relpath import fsspec import yaml # type: ignore from cwltool.context import RuntimeContext from cwltool.factory import Factory +from cwltool.utils import CWLObjectType from python_on_whales import docker from polus.plugins._plugins.cwl import CWL_BASE_DICT @@ -25,6 +25,8 @@ logger = logging.getLogger("polus.plugins") +StrPath = typing.TypeVar("StrPath", str, pathlib.Path) + class IOKeyError(Exception): """Raised when trying to set invalid I/O parameter.""" @@ -47,7 +49,7 @@ def _check_inputs(self): def organization(self): return self.containerId.split("/")[0] - def load_config(self, path: typing.Union[str, pathlib.Path]): + def load_config(self, path: StrPath): with open(path) as fw: config = json.load(fw) inp = config["inputs"] @@ -228,7 +230,7 @@ def _to_cwl(self): cwl_dict["requirements"]["DockerRequirement"]["dockerPull"] = self.containerId return cwl_dict - def save_cwl(self, path: typing.Union[str, pathlib.Path]): + def save_cwl(self, path: StrPath): """Save plugin as CWL command line tool.""" assert str(path).split(".")[-1] == "cwl", "Path must end in .cwl" with open(path, "w") as file: @@ -252,9 +254,9 @@ def save_cwl_io(self, path): def run_cwl( self, - cwl_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, - io_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, - ): + cwl_path: typing.Optional[StrPath] = None, + io_path: typing.Optional[StrPath] = None, + ) -> typing.Union[CWLObjectType, str, None]: """Run configured plugin in CWL. Run plugin as a CWL command line tool after setting I/O values. @@ -284,8 +286,8 @@ def run_cwl( else: self.save_cwl_io(io_path) # saves io to make it visible to user - outdir_path = relpath(self.outDir.parent) # type: ignore - rc = RuntimeContext({"outdir": outdir_path}) + outdir_path = self.outDir.parent.relative_to(pathlib.Path.cwd()) + rc = RuntimeContext({"outdir": str(outdir_path)}) fac = Factory(runtime_context=rc) cwl = fac.make(str(_cwl)) return cwl(**self._cwl_io) # object's io dict is used instead of .yml file diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 89ea69165..fcebc3327 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -319,7 +319,7 @@ class DuplicateVersionFound(Exception): """CWL""" -cwl_input_types = { +CWL_INPUT_TYPES = { "path": "Directory", # always Dir? Yes "string": "string", "number": "double", @@ -331,10 +331,10 @@ class DuplicateVersionFound(Exception): } -def _type_in(input: Input): +def _type_in(inp: Input): """Return appropriate value for `type` based on input type.""" - val = input.type.value - req = "" if input.required else "?" + val = inp.type.value + req = "" if inp.required else "?" # NOT compatible with CWL workflows, ok in CLT # if val == "enum": @@ -343,41 +343,41 @@ def _type_in(input: Input): # else: # s = ["null", {"type": "enum", "symbols": input.options["values"]}] - if val in cwl_input_types: - s = cwl_input_types[val] + req + if val in CWL_INPUT_TYPES: + s = CWL_INPUT_TYPES[val] + req else: s = "string" + req # defaults to string return s -def input_to_cwl(input): +def input_to_cwl(inp: Input): """Return dict of inputs for cwl.""" r = { - f"{input.name}": { - "type": _type_in(input), - "inputBinding": {"prefix": f"--{input.name}"}, + f"{inp.name}": { + "type": _type_in(inp), + "inputBinding": {"prefix": f"--{inp.name}"}, } } return r -def output_to_cwl(o): +def output_to_cwl(out: Output): """Return dict of output args for cwl for input section.""" r = { - f"{o.name}": { + f"{out.name}": { "type": "Directory", - "inputBinding": {"prefix": f"--{o.name}"}, + "inputBinding": {"prefix": f"--{out.name}"}, } } return r -def outputs_cwl(o): +def outputs_cwl(out: Output): """Return dict of output for `outputs` in cwl.""" r = { - f"{o.name}": { + f"{out.name}": { "type": "Directory", - "outputBinding": {"glob": f"$(inputs.{o.name}.basename)"}, + "outputBinding": {"glob": f"$(inputs.{out.name}.basename)"}, } } return r From e9924f1048c8a5e37f867fef2197829844343b1a Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 30 May 2023 09:37:12 -0400 Subject: [PATCH 25/47] refactor: added missing type hint in plugin_methods --- src/polus/plugins/_plugins/classes/plugin_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index 8d2b2d16a..cbec4b42b 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -230,12 +230,12 @@ def _to_cwl(self): cwl_dict["requirements"]["DockerRequirement"]["dockerPull"] = self.containerId return cwl_dict - def save_cwl(self, path: StrPath): + def save_cwl(self, path: StrPath) -> pathlib.Path: """Save plugin as CWL command line tool.""" assert str(path).split(".")[-1] == "cwl", "Path must end in .cwl" with open(path, "w") as file: yaml.dump(self._to_cwl(), file) - return path + return pathlib.Path(path) @property def _cwl_io(self) -> dict: From 42cbf86f1780c53c4d7cec040832c55db9195aa0 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 7 Jul 2023 12:17:14 -0400 Subject: [PATCH 26/47] style: addressed review comments --- .../_plugins/classes/plugin_classes.py | 89 +++++++++---------- .../_plugins/classes/plugin_methods.py | 79 ++++++++-------- 2 files changed, 83 insertions(+), 85 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 196ccfc29..7397dbab9 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -1,4 +1,5 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" +# pylint: disable=W1203, enable=W1201 import json import logging import os @@ -46,26 +47,26 @@ def load_config(config: typing.Union[dict, pathlib.Path]): """Load configured plugin from config file/dict.""" if isinstance(config, pathlib.Path): - with open(config) as fr: - m = json.load(fr) + with open(config, encoding="utf-8") as file: + manifest_ = json.load(file) elif isinstance(config, dict): - m = config + manifest_ = config else: raise TypeError("config must be a dict or a path") - _io = m["_io_keys"] - cl = m["class"] - m.pop("class", None) - if cl == "Compute": - pl = ComputePlugin(_uuid=False, **m) - elif cl == "WIPP": - pl = Plugin(_uuid=False, **m) + _io = manifest_["_io_keys"] + cl_ = manifest_["class"] + manifest_.pop("class", None) + if cl_ == "Compute": + pl_ = ComputePlugin(_uuid=False, **manifest_) + elif cl_ == "WIPP": + pl_ = Plugin(_uuid=False, **manifest_) else: raise ValueError("Invalid value of class") - for k, v in _io.items(): - val = v["value"] + for k, value_ in _io.items(): + val = value_["value"] if val is not None: # exclude those values not set - setattr(pl, k, val) - return pl + setattr(pl_, k, val) + return pl_ def get_plugin(name: str, version: typing.Optional[str] = None): @@ -82,8 +83,7 @@ def get_plugin(name: str, version: typing.Optional[str] = None): """ if version is None: return load_plugin(PLUGINS[name][max(PLUGINS[name])]) - else: - return load_plugin(PLUGINS[name][Version(**{"version": version})]) + return load_plugin(PLUGINS[name][Version(**{"version": version})]) def refresh(): @@ -102,9 +102,9 @@ def refresh(): try: plugin = validate_manifest(file) except InvalidManifest: - logger.warning("Validation error in %s" % (str(file))) - except BaseException as e: - logger.warning(f"Unexpected error {e} with {str(file)}") + logger.warning(f"Validation error in {str(file)}") + except BaseException as exc: # pylint: disable=W0718 + logger.warning(f"Unexpected error {exc} with {str(file)}") else: key = name_cleaner(plugin.name) # Add version and path to VERSIONS @@ -113,8 +113,7 @@ def refresh(): if plugin.version in PLUGINS[key]: if not file == PLUGINS[key][plugin.version]: raise DuplicateVersionFound( - "Found duplicate version of plugin %s in %s" - % (plugin.name, _PLUGIN_DIR) + f"Found duplicate version of plugin {plugin.name} in {_PLUGIN_DIR}" ) PLUGINS[key][plugin.version] = file @@ -166,9 +165,9 @@ def __init__(self, _uuid: bool = True, **data): ) @property - def versions(plugin): # cannot be in PluginMethods because PLUGINS lives here + def versions(self): # cannot be in PluginMethods because PLUGINS lives here """Return list of local versions of a Plugin.""" - return list(PLUGINS[name_cleaner(plugin.name)]) + return list(PLUGINS[name_cleaner(self.name)]) def to_compute(self, hardware_requirements: typing.Optional[dict] = None): """Convert WIPP Plugin object to Compute Plugin object.""" @@ -185,20 +184,20 @@ def save_manifest( ): """Save plugin manifest to specified path.""" if compute: - with open(path, "w") as fw: + with open(path, "w", encoding="utf=8") as file: self.to_compute( hardware_requirements=hardware_requirements ).save_manifest(path) else: - with open(path, "w") as fw: + with open(path, "w", encoding="utf=8") as file: d = self.manifest json.dump( d, - fw, + file, indent=4, ) - logger.debug("Saved manifest to %s" % (path)) + logger.debug(f"Saved manifest to {path}") def __setattr__(self, name, value): """Set I/O parameters as attributes.""" @@ -212,9 +211,9 @@ def _config_file(self): def save_config(self, path: typing.Union[str, pathlib.Path]): """Save manifest with configured I/O parameters to specified path.""" - with open(path, "w") as fw: - json.dump(self._config_file, fw, indent=4, default=str) - logger.debug("Saved config to %s" % (path)) + with open(path, "w", encoding="utf-8") as file: + json.dump(self._config_file, file, indent=4, default=str) + logger.debug(f"Saved config to {path}") def __repr__(self) -> str: """Print plugin name and version.""" @@ -271,8 +270,8 @@ def _ui_in(d: dict): # assuming old all ui input ] # get type from i/o except IndexError: tp = "string" # default to string - except BaseException: - raise + except BaseException as exc: + raise exc d["type"] = _ui_old_to_new(tp) return PluginUIInput(**d) @@ -305,9 +304,9 @@ def _ui_out(d: dict): ) @property - def versions(plugin): # cannot be in PluginMethods because PLUGINS lives here + def versions(self): # cannot be in PluginMethods because PLUGINS lives here """Return list of local versions of a Plugin.""" - return list(PLUGINS[name_cleaner(plugin.name)]) + return list(PLUGINS[name_cleaner(self.name)]) @property def _config_file(self): @@ -321,15 +320,15 @@ def __setattr__(self, name, value): def save_config(self, path: typing.Union[str, pathlib.Path]): """Save configured manifest with I/O parameters to specified path.""" - with open(path, "w") as fw: - json.dump(self._config_file, fw, indent=4) - logger.debug("Saved config to %s" % (path)) + with open(path, "w", encoding="utf-8") as file: + json.dump(self._config_file, file, indent=4) + logger.debug(f"Saved config to {path}") def save_manifest(self, path: typing.Union[str, pathlib.Path]): """Save plugin manifest to specified path.""" - with open(path, "w") as fw: - json.dump(self.manifest, fw, indent=4) - logger.debug("Saved manifest to %s" % (path)) + with open(path, "w", encoding="utf-8") as file: + json.dump(self.manifest, file, indent=4) + logger.debug(f"Saved manifest to {path}") def __repr__(self) -> str: """Print plugin name and version.""" @@ -380,10 +379,10 @@ def submit_plugin( org_path = _PLUGIN_DIR.joinpath(organization.lower()) org_path.mkdir(exist_ok=True, parents=True) if not org_path.joinpath(out_name).exists(): - with open(org_path.joinpath(out_name), "w") as fw: - m = plugin.dict() - m["version"] = m["version"]["version"] - json.dump(m, fw, indent=4) + with open(org_path.joinpath(out_name), "w", encoding="utf-8") as file: + manifest_ = plugin.dict() + manifest_["version"] = manifest_["version"]["version"] + json.dump(manifest_, file, indent=4) # Refresh plugins list refresh() @@ -392,7 +391,7 @@ def submit_plugin( def remove_plugin(plugin: str, version: typing.Optional[str] = None): """Remove plugin from the local database.""" - if not version: + if version is not None: for plugin_version in PLUGINS[plugin]: remove_plugin(plugin, plugin_version) else: diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_methods.py index cbec4b42b..28e9127b7 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_methods.py @@ -1,4 +1,5 @@ """Methods for all plugin objects.""" +# pylint: disable=W1203, W0212, enable=W1201 import enum import json import logging @@ -47,10 +48,12 @@ def _check_inputs(self): @property def organization(self): + """Plugin container's organization.""" return self.containerId.split("/")[0] def load_config(self, path: StrPath): - with open(path) as fw: + """Load configured plugin from file.""" + with open(path, encoding="utf=8") as fw: config = json.load(fw) inp = config["inputs"] out = config["outputs"] @@ -60,7 +63,7 @@ def load_config(self, path: StrPath): for k, v in out.items(): if k in self._io_keys: setattr(self, k, v) - logger.debug("Loaded config from %s" % (path)) + logger.debug(f"Loaded config from {path}") def run( self, @@ -127,7 +130,7 @@ def run( container_name = f"polus{random.randint(10, 99)}" def sig( - signal, frame + signal, frame # pylint: disable=W0613, W0621 ): # signal handler to kill container when KeyboardInterrupt print(f"Exiting container {container_name}") docker.kill(container_name) @@ -137,10 +140,9 @@ def sig( ) # make of sig the handler for KeyboardInterrupt if gpus is None: logger.info( - "Running container without GPU. %s version %s" - % (self.__class__.__name__, self.version.version) + f"Running container without GPU. {self.__class__.__name__} version {self.version.version}" ) - d = docker.run( + docker_ = docker.run( self.containerId, args, name=container_name, @@ -148,13 +150,12 @@ def sig( mounts=mnts, **kwargs, ) - print(d) + print(docker_) else: logger.info( - "Running container with GPU: --gpus %s. %s version %s" - % (gpus, self.__class__.__name__, self.version.version) + f"Running container with GPU: --gpus {gpus}. {self.__class__.__name__} version {self.version.version}" ) - d = docker.run( + docker_ = docker.run( self.containerId, args, gpus=gpus, @@ -163,20 +164,21 @@ def sig( mounts=mnts, **kwargs, ) - print(d) + print(docker_) @property def _config(self): - m = self.dict() - for x in m["inputs"]: - x["value"] = None - return m + model_ = self.dict() + for inp in model_["inputs"]: + inp["value"] = None + return model_ @property def manifest(self): - m = json.loads(self.json(exclude={"_io_keys", "versions", "id"})) - m["version"] = m["version"]["version"] - return m + """Plugin manifest.""" + manifest_ = json.loads(self.json(exclude={"_io_keys", "versions", "id"})) + manifest_["version"] = manifest_["version"]["version"] + return manifest_ def __getattribute__(self, name): if name != "_io_keys" and hasattr(self, "_io_keys"): @@ -192,26 +194,23 @@ def __setattr__(self, name, value): if name == "_fs": if not issubclass(type(value), fsspec.spec.AbstractFileSystem): raise ValueError("_fs must be an fsspec FileSystem") - else: - for i in self.inputs: - i._fs = value - for o in self.outputs: - o._fs = value - return + for i in self.inputs: + i._fs = value + for o in self.outputs: + o._fs = value + return - elif name != "_io_keys" and hasattr(self, "_io_keys"): + if name != "_io_keys" and hasattr(self, "_io_keys"): if name in self._io_keys: logger.debug( - "Value of %s in %s set to %s" - % (name, self.__class__.__name__, value) + f"Value of {name} in {self.__class__.__name__} set to {value}" ) self._io_keys[name].value = value return - else: - raise IOKeyError( - "Attempting to set %s in %s but %s is not a valid I/O parameter" - % (name, self.__class__.__name__, name) - ) + raise IOKeyError( + f"Attempting to set {name} in {self.__class__.__name__} but" + "{name} is not a valid I/O parameter" + ) super().__setattr__(name, value) @@ -232,8 +231,8 @@ def _to_cwl(self): def save_cwl(self, path: StrPath) -> pathlib.Path: """Save plugin as CWL command line tool.""" - assert str(path).split(".")[-1] == "cwl", "Path must end in .cwl" - with open(path, "w") as file: + assert str(path).rsplit(".", maxsplit=1)[-1] == "cwl", "Path must end in .cwl" + with open(path, "w", encoding="utf-8") as file: yaml.dump(self._to_cwl(), file) return pathlib.Path(path) @@ -244,13 +243,13 @@ def _cwl_io(self) -> dict: x.name: io_to_yml(x) for x in self._io_keys.values() if x.value is not None } - def save_cwl_io(self, path): + def save_cwl_io(self, path) -> pathlib.Path: """Save plugin's I/O values to yml file to be used with CWL command line tool.""" self._check_inputs() - assert str(path).split(".")[-1] == "yml", "Path must end in .yml" - with open(path, "w") as file: + assert str(path).rsplit(".", maxsplit=1)[-1] == "yml", "Path must end in .yml" + with open(path, "w", encoding="utf-8") as file: yaml.dump(self._cwl_io, file) - return path + return pathlib.Path(path) def run_cwl( self, @@ -287,8 +286,8 @@ def run_cwl( self.save_cwl_io(io_path) # saves io to make it visible to user outdir_path = self.outDir.parent.relative_to(pathlib.Path.cwd()) - rc = RuntimeContext({"outdir": str(outdir_path)}) - fac = Factory(runtime_context=rc) + r_c = RuntimeContext({"outdir": str(outdir_path)}) + fac = Factory(runtime_context=r_c) cwl = fac.make(str(_cwl)) return cwl(**self._cwl_io) # object's io dict is used instead of .yml file From eb3b63b77a5e952ab21b1c1fab94580ec190857b Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 7 Jul 2023 12:19:17 -0400 Subject: [PATCH 27/47] chore: removed poetry.lock --- poetry.lock | 2421 --------------------------------------------------- 1 file changed, 2421 deletions(-) delete mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 92a95c241..000000000 --- a/poetry.lock +++ /dev/null @@ -1,2421 +0,0 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "argcomplete" -version = "2.0.0" -description = "Bash tab completion for argparse" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, - {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, -] - -[package.extras] -test = ["coverage", "flake8", "pexpect", "wheel"] - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "23.1.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cachecontrol" -version = "0.12.11" -description = "httplib2 caching for requests" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, - {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, -] - -[package.dependencies] -lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} -msgpack = ">=0.5.2" -requests = "*" - -[package.extras] -filecache = ["lockfile (>=0.9)"] -redis = ["redis (>=2.10.5)"] - -[[package]] -name = "certifi" -version = "2022.12.7" -description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] - -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.3.1" -description = "Validate configuration and produce human readable error messages." -category = "dev" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] - -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.0.1" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, -] - -[[package]] -name = "cleo" -version = "2.0.1" -description = "Cleo allows you to create beautiful and testable command-line interfaces." -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "cleo-2.0.1-py3-none-any.whl", hash = "sha256:6eb133670a3ed1f3b052d53789017b6e50fca66d1287e6e6696285f4cb8ea448"}, - {file = "cleo-2.0.1.tar.gz", hash = "sha256:eb4b2e1f3063c11085cebe489a6e9124163c226575a3c3be69b2e51af4a15ec5"}, -] - -[package.dependencies] -crashtest = ">=0.4.1,<0.5.0" -rapidfuzz = ">=2.2.0,<3.0.0" - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "colorlog" -version = "6.7.0" -description = "Add colours to the output of Python's logging module." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, - {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -development = ["black", "flake8", "mypy", "pytest", "types-colorama"] - -[[package]] -name = "crashtest" -version = "0.4.1" -description = "Manage Python errors with ease" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, - {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, -] - -[[package]] -name = "cryptography" -version = "39.0.1" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"}, - {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"}, - {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"}, - {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"}, - {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"}, - {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, - {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"}, - {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"}, - {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"}, - {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"}, -] - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] -sdist = ["setuptools-rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] -test-randomorder = ["pytest-randomly"] -tox = ["tox"] - -[[package]] -name = "datamodel-code-generator" -version = "0.17.1" -description = "Datamodel Code Generator" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "datamodel_code_generator-0.17.1-py3-none-any.whl", hash = "sha256:8e3bf374dff69ce65e07feed3322f5b83432cc7d3974169865ede69c4b98bf92"}, - {file = "datamodel_code_generator-0.17.1.tar.gz", hash = "sha256:a22e1f71b76c149a0fbb8214f7b1e4926c01bf5bc94ce5264a932d65599fb4a9"}, -] - -[package.dependencies] -argcomplete = ">=1.10,<3.0" -black = ">=19.10b0" -genson = ">=1.2.1,<2.0" -inflect = ">=4.1.0,<6.0" -isort = ">=4.3.21,<6.0" -jinja2 = ">=2.10.1,<4.0" -openapi-spec-validator = ">=0.2.8,<=0.5.1" -packaging = "*" -prance = ">=0.18.2,<1.0" -pydantic = [ - {version = ">=1.10.0,<2.0", extras = ["email"], markers = "python_version >= \"3.11\""}, - {version = ">=1.5.1,<2.0", extras = ["email"], markers = "python_version < \"3.10\""}, - {version = ">=1.9.0,<2.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, -] -PySnooper = ">=0.4.1,<2.0.0" -toml = ">=0.10.0,<1.0.0" -typed-ast = [ - {version = ">=1.5.0", markers = "python_full_version >= \"3.9.8\""}, - {version = ">=1.4.2", markers = "python_full_version < \"3.9.8\""}, -] - -[package.extras] -http = ["httpx"] - -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] - -[[package]] -name = "dnspython" -version = "2.3.0" -description = "DNS toolkit" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, - {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, -] - -[package.extras] -curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] -dnssec = ["cryptography (>=2.6,<40.0)"] -doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] -doq = ["aioquic (>=0.9.20)"] -idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.23)"] -wmi = ["wmi (>=1.5.1,<2.0.0)"] - -[[package]] -name = "dulwich" -version = "0.20.50" -description = "Python Git Library" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "dulwich-0.20.50-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:97f02f8d500d4af08dc022d697c56e8539171acc3f575c2fe9acf3b078e5c8c9"}, - {file = "dulwich-0.20.50-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7301773e5cc16d521bc6490e73772a86a4d1d0263de506f08b54678cc4e2f061"}, - {file = "dulwich-0.20.50-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b70106580ed11f45f4c32d2831d0c9c9f359bc2415fff4a6be443e3a36811398"}, - {file = "dulwich-0.20.50-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f9c4f2455f966cad94648278fa9972e4695b35d04f82792fa58e1ea15dd83f0"}, - {file = "dulwich-0.20.50-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9163fbb021a8ad9c35a0814a5eedf45a8eb3a0b764b865d7016d901fc5a947fc"}, - {file = "dulwich-0.20.50-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:322ff8ff6aa4d6d36294cd36de1c84767eb1903c7db3e7b4475ad091febf5363"}, - {file = "dulwich-0.20.50-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d3290a45651c8e534f8e83ae2e30322aefdd162f0f338bae2e79a6ee5a87513"}, - {file = "dulwich-0.20.50-cp310-cp310-win32.whl", hash = "sha256:80ab07131a6e68594441f5c4767e9e44e87fceafc3e347e541c928a18c679bd8"}, - {file = "dulwich-0.20.50-cp310-cp310-win_amd64.whl", hash = "sha256:eefe786a6010f8546baac4912113eeed4e397ddb8c433a345b548a04d4176496"}, - {file = "dulwich-0.20.50-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df3562dde3079d57287c233d45b790bc967c5aae975c9a7b07ca30e60e055512"}, - {file = "dulwich-0.20.50-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e1ae18d5805f0c0c5dac65795f8d48660437166b12ee2c0ffea95bfdbf9c1051"}, - {file = "dulwich-0.20.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2f7df39bd1378d3b0bfb3e7fc930fd0191924af1f0ef587bcd9946afe076c06"}, - {file = "dulwich-0.20.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:731e7f319b34251fadeb362ada1d52cc932369d9cdfa25c0e41150cda28773d0"}, - {file = "dulwich-0.20.50-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d11d44176e5d2fa8271fc86ad1e0a8731b9ad8f77df64c12846b30e16135eb"}, - {file = "dulwich-0.20.50-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7aaabb8e4beadd53f75f853a981caaadef3ef130e5645c902705704eaf136daa"}, - {file = "dulwich-0.20.50-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3dc9f97ec8d3db08d9723b9fd06f3e52c15b84c800d153cfb59b0a3dc8b8d40"}, - {file = "dulwich-0.20.50-cp311-cp311-win32.whl", hash = "sha256:3b1964fa80cafd5a1fd71615b0313daf6f3295c6ab05656ea0c1d2423539904a"}, - {file = "dulwich-0.20.50-cp311-cp311-win_amd64.whl", hash = "sha256:a24a3893108f3b97beb958670d5f3f2a3bec73a1fe18637a572a85abd949a1c4"}, - {file = "dulwich-0.20.50-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6d409a282f8848fd6c8d7c7545ad2f75c16de5d5977de202642f1d50fdaac554"}, - {file = "dulwich-0.20.50-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5411d0f1092152e1c0bb916ae490fe181953ae1b8d13f4e68661253e10b78dbb"}, - {file = "dulwich-0.20.50-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6343569f998ce429e2a5d813c56768ac51b496522401db950f0aa44240bfa901"}, - {file = "dulwich-0.20.50-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a405cd236766060894411614a272cfb86fe86cde5ca73ef264fc4fa5a715fff4"}, - {file = "dulwich-0.20.50-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ee0f9b02019c0ea84cdd31c00a0c283669b771c85612997a911715cf84e33d99"}, - {file = "dulwich-0.20.50-cp36-cp36m-win32.whl", hash = "sha256:2644466270267270f2157ea6f1c0aa224f6f3bf06a307fc39954e6b4b3d82bae"}, - {file = "dulwich-0.20.50-cp36-cp36m-win_amd64.whl", hash = "sha256:d4629635a97e3af1b5da48071e00c8e70fad85f3266fadabe1f5a8f49172c507"}, - {file = "dulwich-0.20.50-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0e4862f318d99cc8a500e3622a89613a88c07d957a0f628cdc2ed86addff790f"}, - {file = "dulwich-0.20.50-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c96e3fb9d48c0454dc242c7accc7819780c9a7f29e441a9eff12361ed0fa35f9"}, - {file = "dulwich-0.20.50-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc6092a4f0bbbff2e553e87a9c6325955b64ea43fca21297c8182e19ae8a43c"}, - {file = "dulwich-0.20.50-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:519b627d49d273e2fd01c79d09e578675ca6cd05193c1787e9ef165c9a1d66ea"}, - {file = "dulwich-0.20.50-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a75cab01b909c4c683c2083e060e378bc01701b7366b5a7d9846ef6d3b9e3d5"}, - {file = "dulwich-0.20.50-cp37-cp37m-win32.whl", hash = "sha256:ea8ffe26d91dbcd5580dbd5a07270a12ea57b091604d77184da0a0d9fad50ed3"}, - {file = "dulwich-0.20.50-cp37-cp37m-win_amd64.whl", hash = "sha256:8f3af857f94021cae1322d86925bfc0dd31e501e885ab5db275473bfac0bb39d"}, - {file = "dulwich-0.20.50-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fb35cedb1243bc420d885ef5b4afd642c6ac8f07ddfc7fdbca1becf9948bf7e"}, - {file = "dulwich-0.20.50-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4bb23a9cec63e16c0e432335f068169b73dd44fa9318dd7cd7a4ca83607ff367"}, - {file = "dulwich-0.20.50-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5267619b34ddaf8d9a6b841492cd17a971fd25bf9a5657f2de928385c3a08b94"}, - {file = "dulwich-0.20.50-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9091f1d53a3c0747cbf0bd127c64e7f09b770264d8fb53e284383fcdf69154e7"}, - {file = "dulwich-0.20.50-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ec7c8fea2b44187a3b545e6c11ab9947ffb122647b07abcdb7cc3aaa770c0e"}, - {file = "dulwich-0.20.50-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:11b180b80363b4fc70664197028181a17ae4c52df9965a29b62a6c52e40c2dbe"}, - {file = "dulwich-0.20.50-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c83e7840d9d0a94d7033bc109efe0c22dfcdcd816bcd4469085e42809e3bf5ba"}, - {file = "dulwich-0.20.50-cp38-cp38-win32.whl", hash = "sha256:c075f69c2de19d9fd97e3b70832d2b42c6a4a5d909b3ffd1963b67d86029f95f"}, - {file = "dulwich-0.20.50-cp38-cp38-win_amd64.whl", hash = "sha256:06775c5713cfeda778c7c67d4422b5e7554d3a7f644f1dde646cdf486a30285a"}, - {file = "dulwich-0.20.50-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:49f66f1c057c18d7d60363f461f4ab8329320fbe1f02a7a33c255864a7d3c942"}, - {file = "dulwich-0.20.50-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e541cd690a5e3d55082ed51732d755917e933cddeb4b0204f2a5ec5d5d7b60b"}, - {file = "dulwich-0.20.50-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:80e8750ee2fa0ab2784a095956077758e5f6107de27f637c4b9d18406652c22c"}, - {file = "dulwich-0.20.50-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbb6368f18451dc44c95c55e1a609d1a01d3821f7ed480b22b2aea1baca0f4a7"}, - {file = "dulwich-0.20.50-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3ee45001411b638641819b7b3b33f31f13467c84066e432256580fcab7d8815"}, - {file = "dulwich-0.20.50-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4842e22ed863a776b36ef8ffe9ed7b772eb452b42c8d02975c29d27e3bc50ab4"}, - {file = "dulwich-0.20.50-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:790e4a641284a7fb4d56ebdaf8b324a5826fbbb9c54307c06f586f9f6a5e56db"}, - {file = "dulwich-0.20.50-cp39-cp39-win32.whl", hash = "sha256:f08406b6b789dea5c95ba1130a0801d8748a67f18be940fe7486a8b481fde875"}, - {file = "dulwich-0.20.50-cp39-cp39-win_amd64.whl", hash = "sha256:78c388ad421199000fb7b5ed5f0c7b509b3e31bd7cad303786a4d0bf89b82f60"}, - {file = "dulwich-0.20.50-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cb194c53109131bcbcd1ca430fcd437cdaf2d33e204e45fbe121c47eaa43e9af"}, - {file = "dulwich-0.20.50-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7542a72c5640dd0620862d6df8688f02a6c336359b5af9b3fcfe11b7fa6652f"}, - {file = "dulwich-0.20.50-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa1d0861517ebbbe0e0084cc9ab4f7ab720624a3eda2bd10e45f774ab858db8"}, - {file = "dulwich-0.20.50-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:583c6bbc27f13fe2e41a19f6987a42681c6e4f6959beae0a6e5bb033b8b081a8"}, - {file = "dulwich-0.20.50-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0c61c193d02c0e1e0d758cdd57ae76685c368d09a01f00d704ba88bd96767cfe"}, - {file = "dulwich-0.20.50-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2edbff3053251985f10702adfafbee118298d383ef5b5b432a5f22d1f1915df"}, - {file = "dulwich-0.20.50-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a344230cadfc5d315752add6ce9d4cfcfc6c85e36bbf57fce9444bcc7c6ea8fb"}, - {file = "dulwich-0.20.50-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bff9bde0b6b05b00c6acbb1a94357caddb2908ed7026a48c715ff50d220335"}, - {file = "dulwich-0.20.50-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e29a3c2037761fa816aa556e78364dfc8e3f44b873db2d17aed96f9b06ac83a3"}, - {file = "dulwich-0.20.50-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2aa2a4a84029625bf9c63771f8a628db1f3be2d2ea3cb8b17942cd4317797152"}, - {file = "dulwich-0.20.50-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd9fa00971ecf059bb358085a942ecac5be4ff71acdf299f44c8cbc45c18659f"}, - {file = "dulwich-0.20.50-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4adac92fb95671ea3a24f2f8e5e5e8f638711ce9c33a3ca6cd68bf1ff7d99f"}, - {file = "dulwich-0.20.50.tar.gz", hash = "sha256:50a941796b2c675be39be728d540c16b5b7ce77eb9e1b3f855650ece6832d2be"}, -] - -[package.dependencies] -urllib3 = ">=1.25" - -[package.extras] -fastimport = ["fastimport"] -https = ["urllib3 (>=1.24.1)"] -paramiko = ["paramiko"] -pgp = ["gpg"] - -[[package]] -name = "email-validator" -version = "1.3.1" -description = "A robust email address syntax and deliverability validation library." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"}, - {file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"}, -] - -[package.dependencies] -dnspython = ">=1.15.0" -idna = ">=2.0.0" - -[[package]] -name = "filelock" -version = "3.9.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, - {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, -] - -[package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "flake8" -version = "6.0.0" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" - -[[package]] -name = "fsspec" -version = "2023.1.0" -description = "File-system specification" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "fsspec-2023.1.0-py3-none-any.whl", hash = "sha256:b833e2e541e9e8cde0ab549414187871243177feb3d344f9d27b25a93f5d8139"}, - {file = "fsspec-2023.1.0.tar.gz", hash = "sha256:fbae7f20ff801eb5f7d0bedf81f25c787c0dfac5e982d98fa3884a9cde2b5411"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -entrypoints = ["importlib-metadata"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -tqdm = ["tqdm"] - -[[package]] -name = "genson" -version = "1.2.2" -description = "GenSON is a powerful, user-friendly JSON Schema generator." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "genson-1.2.2.tar.gz", hash = "sha256:8caf69aa10af7aee0e1a1351d1d06801f4696e005f06cedef438635384346a16"}, -] - -[[package]] -name = "html5lib" -version = "1.1" -description = "HTML parser based on the WHATWG HTML specification" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, - {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, -] - -[package.dependencies] -six = ">=1.9" -webencodings = "*" - -[package.extras] -all = ["chardet (>=2.2)", "genshi", "lxml"] -chardet = ["chardet (>=2.2)"] -genshi = ["genshi"] -lxml = ["lxml"] - -[[package]] -name = "identify" -version = "2.5.18" -description = "File identification library for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, - {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "importlib-metadata" -version = "4.13.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, - {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] - -[[package]] -name = "importlib-resources" -version = "5.10.2" -description = "Read resources from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"}, - {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "inflect" -version = "5.6.2" -description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "inflect-5.6.2-py3-none-any.whl", hash = "sha256:b45d91a4a28a4e617ff1821117439b06eaa86e2a4573154af0149e9be6687238"}, - {file = "inflect-5.6.2.tar.gz", hash = "sha256:aadc7ed73928f5e014129794bbac03058cca35d0a973a5fc4eb45c7fa26005f9"}, -] - -[package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pygments", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "jaraco-classes" -version = "3.2.3" -description = "Utility functions for Python class constructs" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, -] - -[package.dependencies] -more-itertools = "*" - -[package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "jeepney" -version = "0.8.0" -description = "Low-level, pure Python DBus protocol wrapper." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] - -[package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["async_generator", "trio"] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jsonschema" -version = "4.17.3" -description = "An implementation of JSON Schema validation for Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, - {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, -] - -[package.dependencies] -attrs = ">=17.4.0" -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-spec" -version = "0.1.3" -description = "JSONSchema Spec with object-oriented paths" -category = "dev" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "jsonschema_spec-0.1.3-py3-none-any.whl", hash = "sha256:b3cde007ad65c2e631e2f8653cf187124a2c714d02d9fafbab68ad64bf5745d6"}, - {file = "jsonschema_spec-0.1.3.tar.gz", hash = "sha256:8d8db7c255e524fab1016a952a9143e5b6e3c074f4ed25d1878f8e97806caec0"}, -] - -[package.dependencies] -jsonschema = ">=4.0.0,<5.0.0" -pathable = ">=0.4.1,<0.5.0" -PyYAML = ">=5.1" -typing-extensions = ">=4.3.0,<5.0.0" - -[[package]] -name = "keyring" -version = "23.13.1" -description = "Store and access your passwords safely." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, - {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} -"jaraco.classes" = "*" -jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} - -[package.extras] -completion = ["shtab"] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, -] - -[[package]] -name = "lockfile" -version = "0.12.2" -description = "Platform-independent file locking module" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, - {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, -] - -[[package]] -name = "markupsafe" -version = "2.1.2" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "more-itertools" -version = "9.0.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, - {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, -] - -[[package]] -name = "msgpack" -version = "1.0.4" -description = "MessagePack serializer" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88"}, - {file = "msgpack-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa"}, - {file = "msgpack-1.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e"}, - {file = "msgpack-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db"}, - {file = "msgpack-1.0.4-cp310-cp310-win32.whl", hash = "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef"}, - {file = "msgpack-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075"}, - {file = "msgpack-1.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9"}, - {file = "msgpack-1.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6"}, - {file = "msgpack-1.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae"}, - {file = "msgpack-1.0.4-cp36-cp36m-win32.whl", hash = "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6"}, - {file = "msgpack-1.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661"}, - {file = "msgpack-1.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227"}, - {file = "msgpack-1.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e"}, - {file = "msgpack-1.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236"}, - {file = "msgpack-1.0.4-cp37-cp37m-win32.whl", hash = "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44"}, - {file = "msgpack-1.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab"}, - {file = "msgpack-1.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e"}, - {file = "msgpack-1.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43"}, - {file = "msgpack-1.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243"}, - {file = "msgpack-1.0.4-cp38-cp38-win32.whl", hash = "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2"}, - {file = "msgpack-1.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55"}, - {file = "msgpack-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92"}, - {file = "msgpack-1.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8"}, - {file = "msgpack-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae"}, - {file = "msgpack-1.0.4-cp39-cp39-win32.whl", hash = "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c"}, - {file = "msgpack-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce"}, - {file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.7.0" -description = "Node.js virtual environment builder" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "nox" -version = "2022.11.21" -description = "Flexible test automation." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "nox-2022.11.21-py3-none-any.whl", hash = "sha256:0e41a990e290e274cb205a976c4c97ee3c5234441a8132c8c3fd9ea3c22149eb"}, - {file = "nox-2022.11.21.tar.gz", hash = "sha256:e21c31de0711d1274ca585a2c5fde36b1aa962005ba8e9322bf5eeed16dcd684"}, -] - -[package.dependencies] -argcomplete = ">=1.9.4,<3.0" -colorlog = ">=2.6.1,<7.0.0" -packaging = ">=20.9" -virtualenv = ">=14" - -[package.extras] -tox-to-nox = ["jinja2", "tox"] - -[[package]] -name = "openapi-schema-validator" -version = "0.3.4" -description = "OpenAPI schema validation for Python" -category = "dev" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "openapi-schema-validator-0.3.4.tar.gz", hash = "sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd"}, - {file = "openapi_schema_validator-0.3.4-py3-none-any.whl", hash = "sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -jsonschema = ">=4.0.0,<5.0.0" - -[package.extras] -isodate = ["isodate"] -rfc3339-validator = ["rfc3339-validator"] -strict-rfc3339 = ["strict-rfc3339"] - -[[package]] -name = "openapi-spec-validator" -version = "0.5.1" -description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" -category = "dev" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "openapi-spec-validator-0.5.1.tar.gz", hash = "sha256:8248634bad1f23cac5d5a34e193ab36e23914057ca69e91a1ede5af75552c465"}, - {file = "openapi_spec_validator-0.5.1-py3-none-any.whl", hash = "sha256:4a8aee1e45b1ac868e07ab25e18828fe9837baddd29a8e20fdb3d3c61c8eea3d"}, -] - -[package.dependencies] -importlib-resources = ">=5.8.0,<6.0.0" -jsonschema = ">=4.0.0,<5.0.0" -jsonschema-spec = ">=0.1.1,<0.2.0" -lazy-object-proxy = ">=1.7.1,<2.0.0" -openapi-schema-validator = ">=0.3.2,<0.4.0" -PyYAML = ">=5.1" - -[package.extras] -requests = ["requests"] - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "pathable" -version = "0.4.3" -description = "Object-oriented paths" -category = "dev" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, - {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, -] - -[[package]] -name = "pathspec" -version = "0.11.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, - {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, -] - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pkginfo" -version = "1.9.6" -description = "Query metadata from sdists / bdists / installed packages." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, - {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, -] - -[package.extras] -testing = ["pytest", "pytest-cov"] - -[[package]] -name = "platformdirs" -version = "2.6.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, - {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, -] - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - -[[package]] -name = "poetry" -version = "1.3.2" -description = "Python dependency management and packaging made easy." -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "poetry-1.3.2-py3-none-any.whl", hash = "sha256:41980d557954b1418fa503de7a8fb25f19c03c0223a171666b305f05a45fc206"}, - {file = "poetry-1.3.2.tar.gz", hash = "sha256:26ded25f0cf67943243ca4f0aafd47ec4668bdb62845dbb8c2b0e3d9cd280bf4"}, -] - -[package.dependencies] -cachecontrol = {version = ">=0.12.9,<0.13.0", extras = ["filecache"]} -cleo = ">=2.0.0,<3.0.0" -crashtest = ">=0.4.1,<0.5.0" -dulwich = ">=0.20.46,<0.21.0" -filelock = ">=3.8.0,<4.0.0" -html5lib = ">=1.0,<2.0" -importlib-metadata = {version = ">=4.4,<5.0", markers = "python_version < \"3.10\""} -jsonschema = ">=4.10.0,<5.0.0" -keyring = ">=23.9.0,<24.0.0" -lockfile = ">=0.12.2,<0.13.0" -packaging = ">=20.4" -pexpect = ">=4.7.0,<5.0.0" -pkginfo = ">=1.5,<2.0" -platformdirs = ">=2.5.2,<3.0.0" -poetry-core = "1.4.0" -poetry-plugin-export = ">=1.2.0,<2.0.0" -requests = ">=2.18,<3.0" -requests-toolbelt = ">=0.9.1,<0.11.0" -shellingham = ">=1.5,<2.0" -tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.11.1,<0.11.2 || >0.11.2,<0.11.3 || >0.11.3,<1.0.0" -trove-classifiers = ">=2022.5.19" -urllib3 = ">=1.26.0,<2.0.0" -virtualenv = [ - {version = ">=20.4.3,<20.4.5 || >20.4.5,<20.4.6 || >20.4.6,<21.0.0", markers = "sys_platform != \"win32\" or python_version != \"3.9\""}, - {version = ">=20.4.3,<20.4.5 || >20.4.5,<20.4.6 || >20.4.6,<20.16.6", markers = "sys_platform == \"win32\" and python_version == \"3.9\""}, -] -xattr = {version = ">=0.10.0,<0.11.0", markers = "sys_platform == \"darwin\""} - -[[package]] -name = "poetry-core" -version = "1.4.0" -description = "Poetry PEP 517 Build Backend" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "poetry_core-1.4.0-py3-none-any.whl", hash = "sha256:5559ab80384ac021db329ef317086417e140ee1176bcfcb3a3838b544e213c8e"}, - {file = "poetry_core-1.4.0.tar.gz", hash = "sha256:514bd33c30e0bf56b0ed44ee15e120d7e47b61ad908b2b1011da68c48a84ada9"}, -] - -[[package]] -name = "poetry-plugin-export" -version = "1.3.0" -description = "Poetry plugin to export the dependencies to various formats" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "poetry_plugin_export-1.3.0-py3-none-any.whl", hash = "sha256:6e5919bf84afcb08cdd419a03f909f490d8671f00633a3c6df8ba09b0820dc2f"}, - {file = "poetry_plugin_export-1.3.0.tar.gz", hash = "sha256:61ae5ec1db233aba947a48e1ce54c6ff66afd0e1c87195d6bce64c73a5ae658c"}, -] - -[package.dependencies] -poetry = ">=1.3.0,<2.0.0" -poetry-core = ">=1.3.0,<2.0.0" - -[[package]] -name = "prance" -version = "0.21.8.0" -description = "Resolving Swagger/OpenAPI 2.0 and 3.0.0 Parser" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "prance-0.21.8.0-py3-none-any.whl", hash = "sha256:51ec41d10b317bf5d4e74782a7f7f0c0488c6042433b5b4fde2a988cd069d235"}, - {file = "prance-0.21.8.0.tar.gz", hash = "sha256:ce06feef8814c3436645f3b094e91067b1a111bc860a51f239f93437a8d4b00e"}, -] - -[package.dependencies] -chardet = ">=3.0,<5.0" -requests = ">=2.25,<3.0" -"ruamel.yaml" = ">=0.17.10,<0.18.0" -semver = ">=2.13,<3.0" -six = ">=1.15,<2.0" - -[package.extras] -cli = ["click (>=7.0,<8.0)"] -dev = ["bumpversion (>=0.6)", "pytest (>=6.1)", "pytest-cov (>=2.11)", "sphinx (>=3.4)", "towncrier (>=19.2)", "tox (>=3.4)"] -flex = ["flex (>=6.13,<7.0)"] -icu = ["PyICU (>=2.4,<3.0)"] -osv = ["openapi-spec-validator (>=0.2.1)"] -ssv = ["swagger-spec-validator (>=2.4,<3.0)"] - -[[package]] -name = "pre-commit" -version = "3.0.4" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.0.4-py2.py3-none-any.whl", hash = "sha256:9e3255edb0c9e7fe9b4f328cb3dc86069f8fdc38026f1bf521018a05eaf4d67b"}, - {file = "pre_commit-3.0.4.tar.gz", hash = "sha256:bc4687478d55578c4ac37272fe96df66f73d9b5cf81be6f28627d4e712e752d5"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pycodestyle" -version = "2.10.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, -] - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydantic" -version = "1.10.4" -description = "Data validation and settings management using python type hints" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"}, - {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"}, - {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"}, - {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"}, - {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"}, - {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"}, - {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"}, - {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"}, - {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"}, - {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"}, - {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"}, - {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"}, - {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"}, - {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"}, - {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"}, - {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"}, - {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"}, - {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"}, - {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"}, - {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"}, - {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"}, - {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"}, - {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"}, - {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"}, - {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"}, - {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"}, - {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"}, - {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"}, - {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"}, - {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"}, - {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"}, - {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"}, - {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"}, - {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"}, - {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"}, - {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"}, -] - -[package.dependencies] -email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pydantic-to-typescript" -version = "1.0.10" -description = "Convert pydantic models to typescript interfaces" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pydantic-to-typescript-1.0.10.tar.gz", hash = "sha256:c0d030f213b9b225381fc59a5ad7544a0e20bfb6856323f324fd3cf73e4fd447"}, - {file = "pydantic_to_typescript-1.0.10-py3-none-any.whl", hash = "sha256:b2b3954fd4aa55f367aa0513ee3c21cd327221936c0cfbeb8d46b51542eceea7"}, -] - -[package.dependencies] -pydantic = "*" - -[package.extras] -dev = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "pyflakes" -version = "3.0.1" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, -] - -[[package]] -name = "pygithub" -version = "1.57" -description = "Use the full Github API v3" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyGithub-1.57-py3-none-any.whl", hash = "sha256:5822febeac2391f1306c55a99af2bc8f86c8bf82ded000030cd02c18f31b731f"}, - {file = "PyGithub-1.57.tar.gz", hash = "sha256:c273f252b278fb81f1769505cc6921bdb6791e1cebd6ac850cc97dad13c31ff3"}, -] - -[package.dependencies] -deprecated = "*" -pyjwt = ">=2.4.0" -pynacl = ">=1.4.0" -requests = ">=2.14.0" - -[package.extras] -integrations = ["cryptography"] - -[[package]] -name = "pyjwt" -version = "2.6.0" -description = "JSON Web Token implementation in Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, - {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - -[[package]] -name = "pyrsistent" -version = "0.19.3" -description = "Persistent/Functional/Immutable data structures" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, - {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, - {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, - {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, - {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, - {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, -] - -[[package]] -name = "pysnooper" -version = "1.1.1" -description = "A poor man's debugger for Python." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "PySnooper-1.1.1-py2.py3-none-any.whl", hash = "sha256:378f13d731a3e04d3d0350e5f295bdd0f1b49fc8a8b8bf2067fe1e5290bd20be"}, - {file = "PySnooper-1.1.1.tar.gz", hash = "sha256:d17dc91cca1593c10230dce45e46b1d3ff0f8910f0c38e941edf6ba1260b3820"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "python-on-whales" -version = "0.57.0" -description = "A Docker client for Python, designed to be fun and intuitive!" -category = "main" -optional = false -python-versions = ">=3.7, <4" -files = [ - {file = "python-on-whales-0.57.0.tar.gz", hash = "sha256:b92296ab45b2990a8a18a561fd481b6dce8e21330ca34ba7988d6bd6b1bf9f0e"}, - {file = "python_on_whales-0.57.0-py3-none-any.whl", hash = "sha256:c99d5af3d93f5f7c182cc84453543f4a2301dfb1f674f110a22daa5378532d24"}, -] - -[package.dependencies] -pydantic = ">=1.5" -requests = "*" -tqdm = "*" -typer = ">=0.4.1" -typing-extensions = "*" - -[[package]] -name = "pywin32-ctypes" -version = "0.2.0" -description = "" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] - -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] - -[[package]] -name = "rapidfuzz" -version = "2.13.7" -description = "rapid fuzzy string matching" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b75dd0928ce8e216f88660ab3d5c5ffe990f4dd682fd1709dba29d5dafdde6de"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24d3fea10680d085fd0a4d76e581bfb2b1074e66e78fd5964d4559e1fcd2a2d4"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8109e0324d21993d5b2d111742bf5958f3516bf8c59f297c5d1cc25a2342eb66"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f705652360d520c2de52bee11100c92f59b3e3daca308ebb150cbc58aecdad"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7496e8779905b02abc0ab4ba2a848e802ab99a6e20756ffc967a0de4900bd3da"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24eb6b843492bdc63c79ee4b2f104059b7a2201fef17f25177f585d3be03405a"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:467c1505362823a5af12b10234cb1c4771ccf124c00e3fc9a43696512bd52293"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53dcae85956853b787c27c1cb06f18bb450e22cf57a4ad3444cf03b8ff31724a"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46b9b8aa09998bc48dd800854e8d9b74bc534d7922c1d6e1bbf783e7fa6ac29c"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1fbad8fb28d98980f5bff33c7842efef0315d42f0cd59082108482a7e6b61410"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:43fb8cb030f888c3f076d40d428ed5eb4331f5dd6cf1796cfa39c67bf0f0fc1e"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b6bad92de071cbffa2acd4239c1779f66851b60ffbbda0e4f4e8a2e9b17e7eef"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d00df2e4a81ffa56a6b1ec4d2bc29afdcb7f565e0b8cd3092fece2290c4c7a79"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-win32.whl", hash = "sha256:2c836f0f2d33d4614c3fbaf9a1eb5407c0fe23f8876f47fd15b90f78daa64c34"}, - {file = "rapidfuzz-2.13.7-cp310-cp310-win_amd64.whl", hash = "sha256:c36fd260084bb636b9400bb92016c6bd81fd80e59ed47f2466f85eda1fc9f782"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b34e8c0e492949ecdd5da46a1cfc856a342e2f0389b379b1a45a3cdcd3176a6e"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:875d51b3497439a72e2d76183e1cb5468f3f979ab2ddfc1d1f7dde3b1ecfb42f"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae33a72336059213996fe4baca4e0e4860913905c2efb7c991eab33b95a98a0a"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5585189b3d90d81ccd62d4f18530d5ac8972021f0aaaa1ffc6af387ff1dce75"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42085d4b154a8232767de8296ac39c8af5bccee6b823b0507de35f51c9cbc2d7"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:585206112c294e335d84de5d5f179c0f932837752d7420e3de21db7fdc476278"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f891b98f8bc6c9d521785816085e9657212621e93f223917fb8e32f318b2957e"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08590905a95ccfa43f4df353dcc5d28c15d70664299c64abcad8721d89adce4f"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5dd713a1734574c2850c566ac4286594bacbc2d60b9170b795bee4b68656625"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:988f8f6abfba7ee79449f8b50687c174733b079521c3cc121d65ad2d38831846"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b3210869161a864f3831635bb13d24f4708c0aa7208ef5baac1ac4d46e9b4208"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f6fe570e20e293eb50491ae14ddeef71a6a7e5f59d7e791393ffa99b13f1f8c2"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6120f2995f5154057454c5de99d86b4ef3b38397899b5da1265467e8980b2f60"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-win32.whl", hash = "sha256:b20141fa6cee041917801de0bab503447196d372d4c7ee9a03721b0a8edf5337"}, - {file = "rapidfuzz-2.13.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec55a81ac2b0f41b8d6fb29aad16e55417036c7563bad5568686931aa4ff08f7"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d005e058d86f2a968a8d28ca6f2052fab1f124a39035aa0523261d6baf21e1f"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe59a0c21a032024edb0c8e43f5dee5623fef0b65a1e3c1281836d9ce199af3b"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfc04f7647c29fb48da7a04082c34cdb16f878d3c6d098d62d5715c0ad3000c"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68a89bb06d5a331511961f4d3fa7606f8e21237467ba9997cae6f67a1c2c2b9e"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:effe182767d102cb65dfbbf74192237dbd22d4191928d59415aa7d7c861d8c88"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25b4cedf2aa19fb7212894ce5f5219010cce611b60350e9a0a4d492122e7b351"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3a9bd02e1679c0fd2ecf69b72d0652dbe2a9844eaf04a36ddf4adfbd70010e95"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5e2b3d020219baa75f82a4e24b7c8adcb598c62f0e54e763c39361a9e5bad510"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:cf62dacb3f9234f3fddd74e178e6d25c68f2067fde765f1d95f87b1381248f58"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:fa263135b892686e11d5b84f6a1892523123a00b7e5882eff4fbdabb38667347"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa4c598ed77f74ec973247ca776341200b0f93ec3883e34c222907ce72cb92a4"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-win32.whl", hash = "sha256:c2523f8180ebd9796c18d809e9a19075a1060b1a170fde3799e83db940c1b6d5"}, - {file = "rapidfuzz-2.13.7-cp37-cp37m-win_amd64.whl", hash = "sha256:5ada0a14c67452358c1ee52ad14b80517a87b944897aaec3e875279371a9cb96"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ca8a23097c1f50e0fdb4de9e427537ca122a18df2eead06ed39c3a0bef6d9d3a"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9be02162af0376d64b840f2fc8ee3366794fc149f1e06d095a6a1d42447d97c5"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af4f7c3c904ca709493eb66ca9080b44190c38e9ecb3b48b96d38825d5672559"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f50d1227e6e2a0e3ae1fb1c9a2e1c59577d3051af72c7cab2bcc430cb5e18da"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c71d9d512b76f05fa00282227c2ae884abb60e09f08b5ca3132b7e7431ac7f0d"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b52ac2626945cd21a2487aeefed794c14ee31514c8ae69b7599170418211e6f6"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca00fafd2756bc9649bf80f1cf72c647dce38635f0695d7ce804bc0f759aa756"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d248a109699ce9992304e79c1f8735c82cc4c1386cd8e27027329c0549f248a2"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c88adbcb933f6b8612f6c593384bf824e562bb35fc8a0f55fac690ab5b3486e5"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c8601a66fbfc0052bb7860d2eacd303fcde3c14e87fdde409eceff516d659e77"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:27be9c63215d302ede7d654142a2e21f0d34ea6acba512a4ae4cfd52bbaa5b59"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3dcffe1f3cbda0dc32133a2ae2255526561ca594f15f9644384549037b355245"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8450d15f7765482e86ef9be2ad1a05683cd826f59ad236ef7b9fb606464a56aa"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-win32.whl", hash = "sha256:460853983ab88f873173e27cc601c5276d469388e6ad6e08c4fd57b2a86f1064"}, - {file = "rapidfuzz-2.13.7-cp38-cp38-win_amd64.whl", hash = "sha256:424f82c35dbe4f83bdc3b490d7d696a1dc6423b3d911460f5493b7ffae999fd2"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c3fbe449d869ea4d0909fc9d862007fb39a584fb0b73349a6aab336f0d90eaed"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:16080c05a63d6042643ae9b6cfec1aefd3e61cef53d0abe0df3069b9d4b72077"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dbcf5371ea704759fcce772c66a07647751d1f5dbdec7818331c9b31ae996c77"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114810491efb25464016fd554fdf1e20d390309cecef62587494fc474d4b926f"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a84ab9ac9a823e7e93b4414f86344052a5f3e23b23aa365cda01393ad895bd"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81642a24798851b118f82884205fc1bd9ff70b655c04018c467824b6ecc1fabc"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3741cb0bf9794783028e8b0cf23dab917fa5e37a6093b94c4c2f805f8e36b9f"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:759a3361711586a29bc753d3d1bdb862983bd9b9f37fbd7f6216c24f7c972554"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1333fb3d603d6b1040e365dca4892ba72c7e896df77a54eae27dc07db90906e3"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:916bc2e6cf492c77ad6deb7bcd088f0ce9c607aaeabc543edeb703e1fbc43e31"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:23524635840500ce6f4d25005c9529a97621689c85d2f727c52eed1782839a6a"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ebe303cd9839af69dd1f7942acaa80b1ba90bacef2e7ded9347fbed4f1654672"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fe56659ccadbee97908132135de4b875543353351e0c92e736b7c57aee298b5a"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-win32.whl", hash = "sha256:3f11a7eff7bc6301cd6a5d43f309e22a815af07e1f08eeb2182892fca04c86cb"}, - {file = "rapidfuzz-2.13.7-cp39-cp39-win_amd64.whl", hash = "sha256:e8914dad106dacb0775718e54bf15e528055c4e92fb2677842996f2d52da5069"}, - {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f7930adf84301797c3f09c94b9c5a9ed90a9e8b8ed19b41d2384937e0f9f5bd"}, - {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31022d9970177f6affc6d5dd757ed22e44a10890212032fabab903fdee3bfe7"}, - {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f42b82f268689f429def9ecfb86fa65ceea0eaf3fed408b570fe113311bf5ce7"}, - {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b477b43ced896301665183a5e0faec0f5aea2373005648da8bdcb3c4b73f280"}, - {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d63def9bbc6b35aef4d76dc740301a4185867e8870cbb8719ec9de672212fca8"}, - {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c66546e30addb04a16cd864f10f5821272a1bfe6462ee5605613b4f1cb6f7b48"}, - {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f799d1d6c33d81e983d3682571cc7d993ae7ff772c19b3aabb767039c33f6d1e"}, - {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82f20c0060ffdaadaf642b88ab0aa52365b56dffae812e188e5bdb998043588"}, - {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042644133244bfa7b20de635d500eb9f46af7097f3d90b1724f94866f17cb55e"}, - {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75c45dcd595f8178412367e302fd022860ea025dc4a78b197b35428081ed33d5"}, - {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d8b081988d0a49c486e4e845a547565fee7c6e7ad8be57ff29c3d7c14c6894c"}, - {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ffad751f43ab61001187b3fb4a9447ec2d1aedeff7c5bac86d3b95f9980cc3"}, - {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:020858dd89b60ce38811cd6e37875c4c3c8d7fcd8bc20a0ad2ed1f464b34dc4e"}, - {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cda1e2f66bb4ba7261a0f4c2d052d5d909798fca557cbff68f8a79a87d66a18f"}, - {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b6389c50d8d214c9cd11a77f6d501529cb23279a9c9cafe519a3a4b503b5f72a"}, - {file = "rapidfuzz-2.13.7.tar.gz", hash = "sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c"}, -] - -[package.extras] -full = ["numpy"] - -[[package]] -name = "requests" -version = "2.28.2" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" -files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-toolbelt" -version = "0.10.1" -description = "A utility belt for advanced users of python-requests" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, - {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "ruamel-yaml" -version = "0.17.21" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "dev" -optional = false -python-versions = ">=3" -files = [ - {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, - {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} - -[package.extras] -docs = ["ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.7" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, - {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -description = "Python bindings to FreeDesktop.org Secret Service API" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] - -[package.dependencies] -cryptography = ">=2.0" -jeepney = ">=0.6" - -[[package]] -name = "semver" -version = "2.13.0" -description = "Python helper for Semantic Versioning (http://semver.org/)" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, - {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, -] - -[[package]] -name = "setuptools" -version = "67.3.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-67.3.1-py3-none-any.whl", hash = "sha256:23c86b4e44432bfd8899384afc08872ec166a24f48a3f99f293b0a557e6a6b5d"}, - {file = "setuptools-67.3.1.tar.gz", hash = "sha256:daec07fd848d80676694d6bf69c009d28910aeece68a38dbe88b7e1bb6dba12e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "shellingham" -version = "1.5.1" -description = "Tool to Detect Surrounding Shell" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.1-py2.py3-none-any.whl", hash = "sha256:ce5f6d0062c719deabf696bccfab5273e09a6ea1b0ed32867aff03392adbdc51"}, - {file = "shellingham-1.5.1.tar.gz", hash = "sha256:41bc81fa8d74afb04338e0398f9732ee2217407ade778ae1e2709bde89d85c45"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tomlkit" -version = "0.11.6" -description = "Style preserving TOML library" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, -] - -[[package]] -name = "tqdm" -version = "4.64.1" -description = "Fast, Extensible Progress Meter" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, - {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "trove-classifiers" -version = "2023.2.8" -description = "Canonical source for classifiers on PyPI (pypi.org)." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "trove-classifiers-2023.2.8.tar.gz", hash = "sha256:3b6960fb96c1d4cc9988bdc1b90bcd65fcf5d9843d884dfc86bd674ff81a4dea"}, - {file = "trove_classifiers-2023.2.8-py3-none-any.whl", hash = "sha256:3aff899dd9792c4d095740980a5967eb98094ff881faf0b29afc775b75aaaac6"}, -] - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] - -[[package]] -name = "typer" -version = "0.7.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, - {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, -] - -[package.dependencies] -click = ">=7.1.1,<9.0.0" - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] - -[[package]] -name = "urllib3" -version = "1.26.14" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.16.5" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, -] - -[package.dependencies] -distlib = ">=0.3.5,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<3" - -[package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "virtualenv" -version = "20.19.0" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, - {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, -] - -[package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -platformdirs = ">=2.4,<4" - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "wrapt" -version = "1.14.1" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] - -[[package]] -name = "xattr" -version = "0.10.1" -description = "Python wrapper for extended filesystem attributes" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "xattr-0.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:16a660a883e703b311d1bbbcafc74fa877585ec081cd96e8dd9302c028408ab1"}, - {file = "xattr-0.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1e2973e72faa87ca29d61c23b58c3c89fe102d1b68e091848b0e21a104123503"}, - {file = "xattr-0.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:13279fe8f7982e3cdb0e088d5cb340ce9cbe5ef92504b1fd80a0d3591d662f68"}, - {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1dc9b9f580ef4b8ac5e2c04c16b4d5086a611889ac14ecb2e7e87170623a0b75"}, - {file = "xattr-0.10.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:485539262c2b1f5acd6b6ea56e0da2bc281a51f74335c351ea609c23d82c9a79"}, - {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:295b3ab335fcd06ca0a9114439b34120968732e3f5e9d16f456d5ec4fa47a0a2"}, - {file = "xattr-0.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a126eb38e14a2f273d584a692fe36cff760395bf7fc061ef059224efdb4eb62c"}, - {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b0e919c24f5b74428afa91507b15e7d2ef63aba98e704ad13d33bed1288dca81"}, - {file = "xattr-0.10.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e31d062cfe1aaeab6ba3db6bd255f012d105271018e647645941d6609376af18"}, - {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:209fb84c09b41c2e4cf16dd2f481bb4a6e2e81f659a47a60091b9bcb2e388840"}, - {file = "xattr-0.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4120090dac33eddffc27e487f9c8f16b29ff3f3f8bcb2251b2c6c3f974ca1e1"}, - {file = "xattr-0.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e739d624491267ec5bb740f4eada93491de429d38d2fcdfb97b25efe1288eca"}, - {file = "xattr-0.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2677d40b95636f3482bdaf64ed9138fb4d8376fb7933f434614744780e46e42d"}, - {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40039f1532c4456fd0f4c54e9d4e01eb8201248c321c6c6856262d87e9a99593"}, - {file = "xattr-0.10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:148466e5bb168aba98f80850cf976e931469a3c6eb11e9880d9f6f8b1e66bd06"}, - {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0aedf55b116beb6427e6f7958ccd80a8cbc80e82f87a4cd975ccb61a8d27b2ee"}, - {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3024a9ff157247c8190dd0eb54db4a64277f21361b2f756319d9d3cf20e475f"}, - {file = "xattr-0.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f1be6e733e9698f645dbb98565bb8df9b75e80e15a21eb52787d7d96800e823b"}, - {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7880c8a54c18bc091a4ce0adc5c6d81da1c748aec2fe7ac586d204d6ec7eca5b"}, - {file = "xattr-0.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:89c93b42c3ba8aedbc29da759f152731196c2492a2154371c0aae3ef8ba8301b"}, - {file = "xattr-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b905e808df61b677eb972f915f8a751960284358b520d0601c8cbc476ba2df6"}, - {file = "xattr-0.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ef954d0655f93a34d07d0cc7e02765ec779ff0b59dc898ee08c6326ad614d5"}, - {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:199b20301b6acc9022661412346714ce764d322068ef387c4de38062474db76c"}, - {file = "xattr-0.10.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec0956a8ab0f0d3f9011ba480f1e1271b703d11542375ef73eb8695a6bd4b78b"}, - {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffcb57ca1be338d69edad93cf59aac7c6bb4dbb92fd7bf8d456c69ea42f7e6d2"}, - {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f0563196ee54756fe2047627d316977dc77d11acd7a07970336e1a711e934db"}, - {file = "xattr-0.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc354f086f926a1c7f04886f97880fed1a26d20e3bc338d0d965fd161dbdb8ab"}, - {file = "xattr-0.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0cd2d02ef2fb45ecf2b0da066a58472d54682c6d4f0452dfe7ae2f3a76a42ea"}, - {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49626096ddd72dcc1654aadd84b103577d8424f26524a48d199847b5d55612d0"}, - {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceaa26bef8fcb17eb59d92a7481c2d15d20211e217772fb43c08c859b01afc6a"}, - {file = "xattr-0.10.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c014c371391f28f8cd27d73ea59f42b30772cd640b5a2538ad4f440fd9190b"}, - {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:46c32cd605673606b9388a313b0050ee7877a0640d7561eea243ace4fa2cc5a6"}, - {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:772b22c4ff791fe5816a7c2a1c9fcba83f9ab9bea138eb44d4d70f34676232b4"}, - {file = "xattr-0.10.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:183ad611a2d70b5a3f5f7aadef0fcef604ea33dcf508228765fd4ddac2c7321d"}, - {file = "xattr-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8068df3ebdfa9411e58d5ae4a05d807ec5994645bb01af66ec9f6da718b65c5b"}, - {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bc40570155beb85e963ae45300a530223d9822edfdf09991b880e69625ba38a"}, - {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:436e1aaf23c07e15bed63115f1712d2097e207214fc6bcde147c1efede37e2c5"}, - {file = "xattr-0.10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7298455ccf3a922d403339781b10299b858bb5ec76435445f2da46fb768e31a5"}, - {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:986c2305c6c1a08f78611eb38ef9f1f47682774ce954efb5a4f3715e8da00d5f"}, - {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5dc6099e76e33fa3082a905fe59df766b196534c705cf7a2e3ad9bed2b8a180e"}, - {file = "xattr-0.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:042ad818cda6013162c0bfd3816f6b74b7700e73c908cde6768da824686885f8"}, - {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9d4c306828a45b41b76ca17adc26ac3dc00a80e01a5ba85d71df2a3e948828f2"}, - {file = "xattr-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a606280b0c9071ef52572434ecd3648407b20df3d27af02c6592e84486b05894"}, - {file = "xattr-0.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5b49d591cf34cda2079fd7a5cb2a7a1519f54dc2e62abe3e0720036f6ed41a85"}, - {file = "xattr-0.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8705ac6791426559c1a5c2b88bb2f0e83dc5616a09b4500899bfff6a929302"}, - {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5ea974930e876bc5c146f54ac0f85bb39b7b5de2b6fc63f90364712ae368ebe"}, - {file = "xattr-0.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f55a2dd73a12a1ae5113c5d9cd4b4ab6bf7950f4d76d0a1a0c0c4264d50da61d"}, - {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:475c38da0d3614cc5564467c4efece1e38bd0705a4dbecf8deeb0564a86fb010"}, - {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:925284a4a28e369459b2b7481ea22840eed3e0573a4a4c06b6b0614ecd27d0a7"}, - {file = "xattr-0.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa32f1b45fed9122bed911de0fcc654da349e1f04fa4a9c8ef9b53e1cc98b91e"}, - {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c5d3d0e728bace64b74c475eb4da6148cd172b2d23021a1dcd055d92f17619ac"}, - {file = "xattr-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8faaacf311e2b5cc67c030c999167a78a9906073e6abf08eaa8cf05b0416515c"}, - {file = "xattr-0.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6b8d5ca452674e1a96e246a3d2db5f477aecbc7c945c73f890f56323e75203"}, - {file = "xattr-0.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3725746a6502f40f72ef27e0c7bfc31052a239503ff3eefa807d6b02a249be22"}, - {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789bd406d1aad6735e97b20c6d6a1701e1c0661136be9be862e6a04564da771f"}, - {file = "xattr-0.10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a7a807ab538210ff8532220d8fc5e2d51c212681f63dbd4e7ede32543b070f"}, - {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e5825b5fc99ecdd493b0cc09ec35391e7a451394fdf623a88b24726011c950d"}, - {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80638d1ce7189dc52f26c234cee3522f060fadab6a8bc3562fe0ddcbe11ba5a4"}, - {file = "xattr-0.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ff0dbe4a6ce2ce065c6de08f415bcb270ecfd7bf1655a633ddeac695ce8b250"}, - {file = "xattr-0.10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5267e5f9435c840d2674194150b511bef929fa7d3bc942a4a75b9eddef18d8d8"}, - {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b27dfc13b193cb290d5d9e62f806bb9a99b00cd73bb6370d556116ad7bb5dc12"}, - {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:636ebdde0277bce4d12d2ef2550885804834418fee0eb456b69be928e604ecc4"}, - {file = "xattr-0.10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d60c27922ec80310b45574351f71e0dd3a139c5295e8f8b19d19c0010196544f"}, - {file = "xattr-0.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b34df5aad035d0343bd740a95ca30db99b776e2630dca9cc1ba8e682c9cc25ea"}, - {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f24a7c04ff666d0fe905dfee0a84bc899d624aeb6dccd1ea86b5c347f15c20c1"}, - {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3878e1aff8eca64badad8f6d896cb98c52984b1e9cd9668a3ab70294d1ef92d"}, - {file = "xattr-0.10.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4abef557028c551d59cf2fb3bf63f2a0c89f00d77e54c1c15282ecdd56943496"}, - {file = "xattr-0.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0e14bd5965d3db173d6983abdc1241c22219385c22df8b0eb8f1846c15ce1fee"}, - {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9be588a4b6043b03777d50654c6079af3da60cc37527dbb80d36ec98842b1e"}, - {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bc4ae264aa679aacf964abf3ea88e147eb4a22aea6af8c6d03ebdebd64cfd6"}, - {file = "xattr-0.10.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827b5a97673b9997067fde383a7f7dc67342403093b94ea3c24ae0f4f1fec649"}, - {file = "xattr-0.10.1.tar.gz", hash = "sha256:c12e7d81ffaa0605b3ac8c22c2994a8e18a9cf1c59287a1b7722a2289c952ec5"}, -] - -[package.dependencies] -cffi = ">=1.0" - -[[package]] -name = "xmltodict" -version = "0.13.0" -description = "Makes working with XML feel like you are working with JSON" -category = "main" -optional = false -python-versions = ">=3.4" -files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, -] - -[[package]] -name = "zipp" -version = "3.13.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"}, - {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "762706d491fa3eacac75871a6bc89cfe83a698ebad17dfc86380bc7c0a35da06" From b637530dc52f0f2ba9f8849df31c244b065e63af Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Wed, 25 Oct 2023 20:41:55 -0400 Subject: [PATCH 28/47] fix: fixed rm all --- src/polus/plugins/__init__.py | 29 +- .../plugins/_plugins/classes/__init__.py | 22 +- .../_plugins/classes/plugin_classes.py | 332 ++++++++++-------- 3 files changed, 206 insertions(+), 177 deletions(-) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index bb9e66474..f0663d4df 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -1,18 +1,26 @@ """Initialize polus-plugins module.""" import logging +from typing import Union -from polus.plugins._plugins.classes import ( # noqa # pylint: disable=unused-import - get_plugin, - list_plugins, - load_plugin, - refresh, - remove_all, +from polus.plugins._plugins.classes import ( + ComputePlugin, # pylint: disable=unused-import +) +from polus.plugins._plugins.classes import Plugin # pylint: disable=unused-import +from polus.plugins._plugins.classes import get_plugin # pylint: disable=unused-import +from polus.plugins._plugins.classes import list_plugins # pylint: disable=unused-import +from polus.plugins._plugins.classes import refresh # pylint: disable=unused-import +from polus.plugins._plugins.classes import remove_all # pylint: disable=unused-import +from polus.plugins._plugins.classes import ( # pylint: disable=unused-import remove_plugin, +) +from polus.plugins._plugins.classes import ( # pylint: disable=unused-import submit_plugin, ) -from polus.plugins._plugins.update import ( # noqa # pylint: disable=unused-import +from polus.plugins._plugins.update import ( # pylint: disable=unused-import update_nist_plugins, +) +from polus.plugins._plugins.update import ( # pylint: disable=unused-import update_polus_plugins, ) @@ -25,20 +33,19 @@ refresh() # calls the refresh method when library is imported -def __getattr__(name): +def __getattr__(name: str) -> Union[Plugin, ComputePlugin, list]: if name == "list": return list_plugins() if name in list_plugins(): return get_plugin(name) - else: - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) __all__ = [ "refresh", "submit_plugin", "get_plugin", - "load_plugin", "list_plugins", "update_polus_plugins", "update_nist_plugins", diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 128208753..5e71e8ae6 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,20 +1,18 @@ """Plugin classes and functions.""" -from polus.plugins._plugins.classes.plugin_classes import ( - ComputePlugin, - Plugin, - get_plugin, - list_plugins, - load_plugin, - refresh, - remove_all, - remove_plugin, - submit_plugin, -) +from polus.plugins._plugins.classes.plugin_classes import ComputePlugin +from polus.plugins._plugins.classes.plugin_classes import Plugin +from polus.plugins._plugins.classes.plugin_classes import _load_plugin +from polus.plugins._plugins.classes.plugin_classes import get_plugin +from polus.plugins._plugins.classes.plugin_classes import list_plugins +from polus.plugins._plugins.classes.plugin_classes import refresh +from polus.plugins._plugins.classes.plugin_classes import remove_all +from polus.plugins._plugins.classes.plugin_classes import remove_plugin +from polus.plugins._plugins.classes.plugin_classes import submit_plugin __all__ = [ "Plugin", "ComputePlugin", - "load_plugin", + "_load_plugin", "submit_plugin", "get_plugin", "refresh", diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index b6a7a6d09..8b1f85fa3 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -2,37 +2,32 @@ # pylint: disable=W1203, enable=W1201 import json import logging -import os -import pathlib import shutil -import typing import uuid from copy import deepcopy - -from pydantic import Extra +from pathlib import Path +from typing import Any +from typing import Optional +from typing import Union from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import ( - DuplicateVersionFound, - Version, - _in_old_to_new, - _ui_old_to_new, -) -from polus.plugins._plugins.manifests.manifest_utils import ( - InvalidManifest, - _load_manifest, - validate_manifest, -) -from polus.plugins._plugins.models import ( - ComputeSchema, - PluginUIInput, - PluginUIOutput, - WIPPPluginManifest, -) -from polus.plugins._plugins.utils import cast_version, name_cleaner +from polus.plugins._plugins.io import DuplicateVersionFound +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.io import _in_old_to_new +from polus.plugins._plugins.io import _ui_old_to_new +from polus.plugins._plugins.manifests.manifest_utils import InvalidManifest +from polus.plugins._plugins.manifests.manifest_utils import _load_manifest +from polus.plugins._plugins.manifests.manifest_utils import validate_manifest +from polus.plugins._plugins.models import ComputeSchema +from polus.plugins._plugins.models import PluginUIInput +from polus.plugins._plugins.models import PluginUIOutput +from polus.plugins._plugins.models import WIPPPluginManifest +from polus.plugins._plugins.utils import cast_version +from polus.plugins._plugins.utils import name_cleaner +from pydantic import Extra logger = logging.getLogger("polus.plugins") -PLUGINS: typing.Dict[str, typing.Dict] = {} +PLUGINS: dict[str, dict] = {} # PLUGINS = {"BasicFlatfieldCorrectionPlugin": # {Version('0.1.4'): Path(<...>), Version('0.1.5'): Path(<...>)}. # "VectorToLabel": {Version(...)}} @@ -41,52 +36,10 @@ Paths and Fields """ # Location to store any discovered plugin manifests -_PLUGIN_DIR = pathlib.Path(__file__).parent.parent.joinpath("manifests") - - -def load_config(config: typing.Union[dict, pathlib.Path]): - """Load configured plugin from config file/dict.""" - if isinstance(config, pathlib.Path): - with open(config, encoding="utf-8") as file: - manifest_ = json.load(file) - elif isinstance(config, dict): - manifest_ = config - else: - raise TypeError("config must be a dict or a path") - _io = manifest_["_io_keys"] - cl_ = manifest_["class"] - manifest_.pop("class", None) - if cl_ == "Compute": - pl_ = ComputePlugin(_uuid=False, **manifest_) - elif cl_ == "WIPP": - pl_ = Plugin(_uuid=False, **manifest_) - else: - raise ValueError("Invalid value of class") - for k, value_ in _io.items(): - val = value_["value"] - if val is not None: # exclude those values not set - setattr(pl_, k, val) - return pl_ +_PLUGIN_DIR = Path(__file__).parent.parent.joinpath("manifests") -def get_plugin(name: str, version: typing.Optional[str] = None): - """Get a plugin with option to specify version. - - Return a plugin object with the option to specify a version. The specified version's manifest must exist in manifests folder. - - Args: - name: Name of the plugin. - version: Optional version of the plugin, must follow semver. - - Returns: - Plugin object - """ - if version is None: - return load_plugin(PLUGINS[name][max(PLUGINS[name])]) - return load_plugin(PLUGINS[name][Version(**{"version": version})]) - - -def refresh(): +def refresh() -> None: """Refresh the plugin list.""" organizations = [ x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() @@ -102,25 +55,30 @@ def refresh(): try: plugin = validate_manifest(file) except InvalidManifest: - logger.warning(f"Validation error in {str(file)}") - except BaseException as exc: # pylint: disable=W0718 - logger.warning(f"Unexpected error {exc} with {str(file)}") + logger.warning(f"Validation error in {file!s}") + except BaseException as exc: # pylint: disable=W0718 # noqa: BLE001 + logger.warning(f"Unexpected error {exc} with {file!s}") else: key = name_cleaner(plugin.name) # Add version and path to VERSIONS if key not in PLUGINS: PLUGINS[key] = {} - if plugin.version in PLUGINS[key]: - if not file == PLUGINS[key][plugin.version]: - raise DuplicateVersionFound( - f"Found duplicate version of plugin {plugin.name} in {_PLUGIN_DIR}" - ) + if ( + plugin.version in PLUGINS[key] + and file != PLUGINS[key][plugin.version] + ): + msg = ( + "Found duplicate version of plugin" + f"{plugin.name} in {_PLUGIN_DIR}" + ) + raise DuplicateVersionFound( + msg, + ) PLUGINS[key][plugin.version] = file - -def list_plugins(): +def list_plugins() -> list: """List all local plugins.""" output = list(PLUGINS.keys()) output.sort() @@ -139,7 +97,7 @@ class Plugin(WIPPPluginManifest, _PluginMethods): save_manifest(path): save plugin manifest to specified path """ - id: uuid.UUID + id: uuid.UUID # noqa: A003 class Config: """Config class for Pydantic Model.""" @@ -147,12 +105,12 @@ class Config: extra = Extra.allow allow_mutation = False - def __init__(self, _uuid: bool = True, **data): + def __init__(self, _uuid: bool = True, **data: dict) -> None: """Init a plugin object from manifest.""" if _uuid: - data["id"] = uuid.uuid4() + data["id"] = uuid.uuid4() # type: ignore else: - data["id"] = uuid.UUID(str(data["id"])) + data["id"] = uuid.UUID(str(data["id"])) # type: ignore data["version"] = cast_version(data["version"]) super().__init__(**data) @@ -161,59 +119,66 @@ def __init__(self, _uuid: bool = True, **data): self._io_keys = {i.name: i for i in self.inputs} self._io_keys.update({o.name: o for o in self.outputs}) - if self.author == "": - logger.warning( - f"The plugin ({self.name}) is missing the author field. This field is not required but should be filled in." + if not self.author: + warn_msg = ( + f"The plugin ({self.name}) is missing the author field. " + "This field is not required but should be filled in." ) + logger.warning(warn_msg) @property - def versions(self): # cannot be in PluginMethods because PLUGINS lives here + def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives here """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(self.name)]) - def to_compute(self, hardware_requirements: typing.Optional[dict] = None): + def to_compute( + self, + hardware_requirements: Optional[dict] = None, + ) -> type[ComputeSchema]: """Convert WIPP Plugin object to Compute Plugin object.""" data = deepcopy(self.manifest) return ComputePlugin( - hardware_requirements=hardware_requirements, _from_old=True, **data + hardware_requirements=hardware_requirements, + _from_old=True, + **data, ) def save_manifest( self, - path: typing.Union[str, pathlib.Path], - hardware_requirements: typing.Optional[dict] = None, + path: Union[str, Path], + hardware_requirements: Optional[dict] = None, compute: bool = False, - ): + ) -> None: """Save plugin manifest to specified path.""" if compute: - with open(path, "w", encoding="utf=8") as file: + with Path(path).open("w", encoding="utf-8") as file: self.to_compute( - hardware_requirements=hardware_requirements + hardware_requirements=hardware_requirements, ).save_manifest(path) else: - with open(path, "w", encoding="utf=8") as file: - d = self.manifest + with Path(path).open("w", encoding="utf-8") as file: + dict_ = self.manifest json.dump( - d, + dict_, file, indent=4, ) logger.debug(f"Saved manifest to {path}") - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" _PluginMethods.__setattr__(self, name, value) @property - def _config_file(self): - m = self._config - m["class"] = "WIPP" - return m + def _config_file(self) -> dict: + config_ = self._config + config_["class"] = "WIPP" + return config_ - def save_config(self, path: typing.Union[str, pathlib.Path]): + def save_config(self, path: Union[str, Path]) -> None: """Save manifest with configured I/O parameters to specified path.""" - with open(path, "w", encoding="utf-8") as file: + with Path(path).open("w", encoding="utf-8") as file: json.dump(self._config_file, file, indent=4, default=str) logger.debug(f"Saved config to {path}") @@ -242,53 +207,57 @@ class Config: def __init__( self, - hardware_requirements: typing.Optional[dict] = None, + hardware_requirements: Optional[dict] = None, _from_old: bool = False, _uuid: bool = True, - **data, - ): + **data: dict, + ) -> None: """Init a plugin object from manifest.""" if _uuid: - data["id"] = uuid.uuid4() + data["id"] = uuid.uuid4() # type: ignore else: - data["id"] = uuid.UUID(str(data["id"])) + data["id"] = uuid.UUID(str(data["id"])) # type: ignore if _from_old: - def _convert_input(d: dict): - d["type"] = _in_old_to_new(d["type"]) - return d + def _convert_input(dict_: dict) -> dict: + dict_["type"] = _in_old_to_new(dict_["type"]) + return dict_ - def _convert_output(d: dict): - d["type"] = "path" - return d + def _convert_output(dict_: dict) -> dict: + dict_["type"] = "path" + return dict_ - def _ui_in(d: dict): # assuming old all ui input + def _ui_in(dict_: dict) -> PluginUIInput: # assuming old all ui input # assuming format inputs. ___ - inp = d["key"].split(".")[-1] # e.g inpDir + inp = dict_["key"].split(".")[-1] # e.g inpDir try: - tp = [x["type"] for x in data["inputs"] if x["name"] == inp][ + type_ = [x["type"] for x in data["inputs"] if x["name"] == inp][ 0 ] # get type from i/o except IndexError: - tp = "string" # default to string + type_ = "string" # default to string except BaseException as exc: raise exc - d["type"] = _ui_old_to_new(tp) - return PluginUIInput(**d) + dict_["type"] = _ui_old_to_new(type_) + return PluginUIInput(**dict_) - def _ui_out(d: dict): - nd = deepcopy(d) - nd["name"] = "outputs." + nd["name"] - nd["type"] = _ui_old_to_new(nd["type"]) - return PluginUIOutput(**nd) + def _ui_out(dict_: dict) -> PluginUIOutput: + new_dict_ = deepcopy(dict_) + new_dict_["name"] = "outputs." + new_dict_["name"] + new_dict_["type"] = _ui_old_to_new(new_dict_["type"]) + return PluginUIOutput(**new_dict_) - data["inputs"] = [_convert_input(x) for x in data["inputs"]] - data["outputs"] = [_convert_output(x) for x in data["outputs"]] + data["inputs"] = [_convert_input(x) for x in data["inputs"]] # type: ignore + data["outputs"] = [ + _convert_output(x) for x in data["outputs"] + ] # type: ignore data["pluginHardwareRequirements"] = {} - data["ui"] = [_ui_in(x) for x in data["ui"]] # inputs - data["ui"].extend([_ui_out(x) for x in data["outputs"]]) # type: ignore # outputs + data["ui"] = [_ui_in(x) for x in data["ui"]] # type: ignore + data["ui"].extend( # type: ignore[attr-defined] + [_ui_out(x) for x in data["outputs"]], + ) if hardware_requirements: for k, v in hardware_requirements.items(): @@ -300,35 +269,37 @@ def _ui_out(d: dict): self._io_keys = {i.name: i for i in self.inputs} self._io_keys.update({o.name: o for o in self.outputs}) # type: ignore - if self.author == "": - logger.warning( - f"The plugin ({self.name}) is missing the author field. This field is not required but should be filled in." + if not self.author: + warn_msg = ( + f"The plugin ({self.name}) is missing the author field. " + "This field is not required but should be filled in." ) + logger.warning(warn_msg) @property - def versions(self): # cannot be in PluginMethods because PLUGINS lives here + def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives here """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(self.name)]) @property - def _config_file(self): - m = self._config - m["class"] = "Compute" - return m + def _config_file(self) -> dict: + config_ = self._config + config_["class"] = "Compute" + return config_ - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" _PluginMethods.__setattr__(self, name, value) - def save_config(self, path: typing.Union[str, pathlib.Path]): + def save_config(self, path: Union[str, Path]) -> None: """Save configured manifest with I/O parameters to specified path.""" - with open(path, "w", encoding="utf-8") as file: + with Path(path).open("w", encoding="utf-8") as file: json.dump(self._config_file, file, indent=4) logger.debug(f"Saved config to {path}") - def save_manifest(self, path: typing.Union[str, pathlib.Path]): + def save_manifest(self, path: Union[str, Path]) -> None: """Save plugin manifest to specified path.""" - with open(path, "w", encoding="utf-8") as file: + with Path(path).open("w", encoding="utf-8") as file: json.dump(self.manifest, file, indent=4) logger.debug(f"Saved manifest to {path}") @@ -337,9 +308,9 @@ def __repr__(self) -> str: return _PluginMethods.__repr__(self) -def load_plugin( - manifest: typing.Union[str, dict, pathlib.Path] -) -> typing.Union[Plugin, ComputePlugin]: +def _load_plugin( + manifest: Union[str, dict, Path], +) -> Union[Plugin, ComputePlugin]: """Parse a manifest and return one of Plugin or ComputePlugin.""" manifest = _load_manifest(manifest) if "pluginHardwareRequirements" in manifest: # type: ignore[operator] @@ -352,8 +323,8 @@ def load_plugin( def submit_plugin( - manifest: typing.Union[str, dict, pathlib.Path], -): + manifest: Union[str, dict, Path], +) -> Union[Plugin, ComputePlugin]: """Parse a plugin and create a local copy of it. This function accepts a plugin manifest as a string, a dictionary (parsed @@ -361,8 +332,8 @@ def submit_plugin( Args: manifest: - A plugin manifest. It can be a url, a dictionary, - a path to a JSON file or a string that can be parsed as a dictionary + A plugin manifest. It can be a url, a dictionary, + a path to a JSON file or a string that can be parsed as a dictionary Returns: A Plugin object populated with information from the plugin manifest. @@ -381,7 +352,7 @@ def submit_plugin( org_path = _PLUGIN_DIR.joinpath(organization.lower()) org_path.mkdir(exist_ok=True, parents=True) if not org_path.joinpath(out_name).exists(): - with open(org_path.joinpath(out_name), "w", encoding="utf-8") as file: + with org_path.joinpath(out_name).open("w", encoding="utf-8") as file: manifest_ = plugin.dict() manifest_["version"] = manifest_["version"]["version"] json.dump(manifest_, file, indent=4) @@ -391,22 +362,75 @@ def submit_plugin( return plugin -def remove_plugin(plugin: str, version: typing.Optional[str] = None): +def get_plugin( + name: str, + version: Optional[str] = None, +) -> Union[Plugin, ComputePlugin]: + """Get a plugin with option to specify version. + + Return a plugin object with the option to specify a version. + The specified version's manifest must exist in manifests folder. + + Args: + name: Name of the plugin. + version: Optional version of the plugin, must follow semver. + + Returns: + Plugin object + """ + if version is None: + return _load_plugin(PLUGINS[name][max(PLUGINS[name])]) + return _load_plugin(PLUGINS[name][Version(**{"version": version})]) + + +def load_config(config: Union[dict, Path]) -> Union[Plugin, ComputePlugin]: + """Load configured plugin from config file/dict.""" + if isinstance(config, Path): + with config.open("r", encoding="utf-8") as file: + manifest_ = json.load(file) + elif isinstance(config, dict): + manifest_ = config + else: + msg = "config must be a dict or a path" + raise TypeError(msg) + io_keys_ = manifest_["_io_keys"] + class_ = manifest_["class"] + manifest_.pop("class", None) + if class_ == "Compute": + plugin_ = ComputePlugin(_uuid=False, **manifest_) + elif class_ == "WIPP": + plugin_ = Plugin(_uuid=False, **manifest_) + else: + msg = "Invalid value of class" + raise ValueError(msg) + for key, value_ in io_keys_.items(): + val = value_["value"] + if val is not None: # exclude those values not set + setattr(plugin_, key, val) + return plugin_ + + +def remove_plugin(plugin: str, version: Optional[Union[str, list[str]]] = None) -> None: """Remove plugin from the local database.""" - if version is not None: + if version is None: for plugin_version in PLUGINS[plugin]: remove_plugin(plugin, plugin_version) + return else: + if isinstance(version, list): + for version_ in version: + remove_plugin(plugin, version_) + return if not isinstance(version, Version): version_ = cast_version(version) else: version_ = version path = PLUGINS[plugin][version_] - os.remove(path) + path.unlink() refresh() -def remove_all(): +def remove_all() -> None: """Remove all plugins from the local database.""" organizations = [ x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() From 6391d452bda66bc2738682428386982e6d383a72 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 16 May 2023 21:47:18 -0400 Subject: [PATCH 29/47] fix: fixed unexported load_config function --- src/polus/plugins/__init__.py | 2 ++ src/polus/plugins/_plugins/classes/__init__.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index f0663d4df..0319cb364 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -9,6 +9,7 @@ from polus.plugins._plugins.classes import Plugin # pylint: disable=unused-import from polus.plugins._plugins.classes import get_plugin # pylint: disable=unused-import from polus.plugins._plugins.classes import list_plugins # pylint: disable=unused-import +from polus.plugins._plugins.classes import load_config # pylint: disable=unused-import from polus.plugins._plugins.classes import refresh # pylint: disable=unused-import from polus.plugins._plugins.classes import remove_all # pylint: disable=unused-import from polus.plugins._plugins.classes import ( # pylint: disable=unused-import @@ -46,6 +47,7 @@ def __getattr__(name: str) -> Union[Plugin, ComputePlugin, list]: "refresh", "submit_plugin", "get_plugin", + "load_config", "list_plugins", "update_polus_plugins", "update_nist_plugins", diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 5e71e8ae6..f5f852c85 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,4 +1,5 @@ """Plugin classes and functions.""" +<<<<<<< rm1 from polus.plugins._plugins.classes.plugin_classes import ComputePlugin from polus.plugins._plugins.classes.plugin_classes import Plugin from polus.plugins._plugins.classes.plugin_classes import _load_plugin @@ -8,6 +9,20 @@ from polus.plugins._plugins.classes.plugin_classes import remove_all from polus.plugins._plugins.classes.plugin_classes import remove_plugin from polus.plugins._plugins.classes.plugin_classes import submit_plugin +======= +from polus.plugins._plugins.classes.plugin_classes import ( + ComputePlugin, + Plugin, + get_plugin, + list_plugins, + load_config, + load_plugin, + refresh, + remove_all, + remove_plugin, + submit_plugin, +) +>>>>>>> fix: fixed unexported load_config function __all__ = [ "Plugin", @@ -19,4 +34,5 @@ "list_plugins", "remove_plugin", "remove_all", + "load_config", ] From 90f8335eaa723b8af0960e336fd3d190ad6e7d85 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 26 Oct 2023 15:36:17 -0400 Subject: [PATCH 30/47] fix: fixed last rebase --- src/polus/plugins/_plugins/classes/__init__.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index f5f852c85..91200c48e 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,33 +1,17 @@ """Plugin classes and functions.""" -<<<<<<< rm1 from polus.plugins._plugins.classes.plugin_classes import ComputePlugin from polus.plugins._plugins.classes.plugin_classes import Plugin -from polus.plugins._plugins.classes.plugin_classes import _load_plugin from polus.plugins._plugins.classes.plugin_classes import get_plugin from polus.plugins._plugins.classes.plugin_classes import list_plugins +from polus.plugins._plugins.classes.plugin_classes import load_config from polus.plugins._plugins.classes.plugin_classes import refresh from polus.plugins._plugins.classes.plugin_classes import remove_all from polus.plugins._plugins.classes.plugin_classes import remove_plugin from polus.plugins._plugins.classes.plugin_classes import submit_plugin -======= -from polus.plugins._plugins.classes.plugin_classes import ( - ComputePlugin, - Plugin, - get_plugin, - list_plugins, - load_config, - load_plugin, - refresh, - remove_all, - remove_plugin, - submit_plugin, -) ->>>>>>> fix: fixed unexported load_config function __all__ = [ "Plugin", "ComputePlugin", - "_load_plugin", "submit_plugin", "get_plugin", "refresh", From 1ac68084389f538f76c3de12a84c65cdc515d221 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Sun, 23 Apr 2023 23:02:38 -0400 Subject: [PATCH 31/47] chore: edited gitignore --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e2a8abbb7..e3ca00b58 100644 --- a/.gitignore +++ b/.gitignore @@ -167,5 +167,8 @@ data # local manifests src/polus/plugins/_plugins/manifests/* -# allow python scripts insied manifests dir -!src/polus/plugins/_plugins/manifests/*.py \ No newline at end of file +# allow python scripts inside manifests dir +!src/polus/plugins/_plugins/manifests/*.py + +#macOS +*.DS_Store From 1acab6fa84416df49f8fc460e0d94cb87fbc7f27 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 16 May 2023 18:58:34 -0400 Subject: [PATCH 32/47] chore: updated gitignore to include node_modules --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e3ca00b58..36805ca94 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,6 @@ src/polus/plugins/_plugins/manifests/* #macOS *.DS_Store + +#husky +node_modules From af67ab506cace1f685bd41f29c0647a490b430d1 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Wed, 17 May 2023 19:44:08 -0400 Subject: [PATCH 33/47] test: added initial tests --- pyproject.toml | 16 ++ tests/resources/b1.json | 77 +++++++++ tests/resources/b2.json | 76 +++++++++ tests/resources/b3.json | 76 +++++++++ tests/resources/g1.json | 78 +++++++++ tests/resources/g2.json | 77 +++++++++ tests/resources/g3.json | 77 +++++++++ tests/resources/omeconverter.json | 45 +++++ tests/resources/omeconverter030.json | 70 ++++++++ tests/resources/target1.cwl | 32 ++++ tests/test_cwl.py | 88 ++++++++++ tests/test_io.py | 64 +++++++ tests/test_manifests.py | 242 +++++++++++++++++++++++++++ tests/test_plugins.py | 167 ++++++++++++++++++ tests/test_version.py | 85 ++++++++++ 15 files changed, 1270 insertions(+) create mode 100644 tests/resources/b1.json create mode 100644 tests/resources/b2.json create mode 100644 tests/resources/b3.json create mode 100644 tests/resources/g1.json create mode 100644 tests/resources/g2.json create mode 100644 tests/resources/g3.json create mode 100644 tests/resources/omeconverter.json create mode 100644 tests/resources/omeconverter030.json create mode 100644 tests/resources/target1.cwl create mode 100644 tests/test_cwl.py create mode 100644 tests/test_io.py create mode 100644 tests/test_manifests.py create mode 100644 tests/test_plugins.py create mode 100644 tests/test_version.py diff --git a/pyproject.toml b/pyproject.toml index e71100aaf..098d1fa98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,12 @@ version = "0.1.1" [tool.poetry.dependencies] click = "^8.1.3" +cwltool = "^3.1.20230513155734" fsspec = "^2023.6.0" pydantic = ">=1.10.9, <2.0" pygithub = "^1.58.2" +fsspec = "^2023.1.0" +pydantic = "^1.10.4" python = ">=3.9, <3.12" python-on-whales = "^0.57.0" tqdm = "^4.65.0" @@ -33,8 +36,18 @@ pytest-sugar = "^0.9.7" pytest-cov = "^4.1.0" pytest-benchmark = "^4.0.0" datamodel-code-generator = "^0.17.1" +flake8 = "^6.0.0" +fsspec = "^2023.1.0" nox = "^2022.11.21" +poetry = "^1.3.2" +pre-commit = "^3.0.4" +pydantic = "^1.10.4" pydantic-to-typescript = "^1.0.10" +python = ">=3.9, <3.12" +python-on-whales = "^0.57.0" +tqdm = "^4.64.1" +xmltodict = "^0.13.0" +pyyaml = "^6.0" [build-system] build-backend = "poetry.core.masonry.api" @@ -47,3 +60,6 @@ profile = "black" addopts = [ "--import-mode=importlib", ] +markers = [ + "repo: marks tests that validate plugin.json files in local repo", +] diff --git a/tests/resources/b1.json b/tests/resources/b1.json new file mode 100644 index 000000000..a385c3baf --- /dev/null +++ b/tests/resources/b1.json @@ -0,0 +1,77 @@ +{ + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "Peng et al. \"A BaSiC tool for background and shading correction of optical microscopy images\" Nature Communications (2017)", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/b2.json b/tests/resources/b2.json new file mode 100644 index 000000000..3e28f5b1c --- /dev/null +++ b/tests/resources/b2.json @@ -0,0 +1,76 @@ +{ + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "citation": "Peng et al. \"A BaSiC tool for background and shading correction of optical microscopy images\" Nature Communications (2017)", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/b3.json b/tests/resources/b3.json new file mode 100644 index 000000000..f161974fa --- /dev/null +++ b/tests/resources/b3.json @@ -0,0 +1,76 @@ +{ + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "citation": "Peng et al. \"A BaSiC tool for background and shading correction of optical microscopy images\" Nature Communications (2017)", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/g1.json b/tests/resources/g1.json new file mode 100644 index 000000000..ca32f1905 --- /dev/null +++ b/tests/resources/g1.json @@ -0,0 +1,78 @@ +{ + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "Peng et al. \"A BaSiC tool for background and shading correction of optical microscopy images\" Nature Communications (2017)", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/g2.json b/tests/resources/g2.json new file mode 100644 index 000000000..24d32be1a --- /dev/null +++ b/tests/resources/g2.json @@ -0,0 +1,77 @@ +{ + "version": "1.2.7", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "name": "BaSiC Flatfield Correction Plugin", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "repository": "https://github.com/polusai/polus-plugins", + "title": "Flatfield correction using BaSiC algorithm.", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/g3.json b/tests/resources/g3.json new file mode 100644 index 000000000..e589644e4 --- /dev/null +++ b/tests/resources/g3.json @@ -0,0 +1,77 @@ +{ + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "Peng et al. \"A BaSiC tool for background and shading correction of optical microscopy images\" Nature Communications (2017)", + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": true + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": true + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": true + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection..." + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets" + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values." + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image" + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image" + } + ] +} diff --git a/tests/resources/omeconverter.json b/tests/resources/omeconverter.json new file mode 100644 index 000000000..b696f46e6 --- /dev/null +++ b/tests/resources/omeconverter.json @@ -0,0 +1,45 @@ +{ + "name": "OME Converter", + "version": "0.2.2", + "title": "OME Converter", + "description": "Convert Bioformats supported format to OME Zarr.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/labshare/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/ome-converter-plugin:0.2.2", + "inputs": [ + { + "name": "inpDir", + "type": "genericData", + "description": "Input generic data collection to be processed by this plugin", + "required": true + }, + { + "name": "filePatter", + "type": "string", + "description": "A filepattern, used to select data to be converted", + "required": true + } + ], + "outputs": [ + { + "name": "outDir", + "type": "genericData", + "description": "Output collection" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input generic collection", + "description": "Input generic data collection to be processed by this plugin" + }, + { + "key": "inputs.filePattern", + "title": "Filepattern", + "description": "A filepattern, used to select data for conversion" + } + ] +} diff --git a/tests/resources/omeconverter030.json b/tests/resources/omeconverter030.json new file mode 100644 index 000000000..7613dcf3c --- /dev/null +++ b/tests/resources/omeconverter030.json @@ -0,0 +1,70 @@ +{ + "name": "OME Converter", + "version": "0.3.0", + "title": "OME Converter", + "description": "Convert Bioformats supported format to OME Zarr or OME TIF", + "author": "Nick Schaub (nick.schaub@nih.gov), Hamdah Shafqat Abbasi (hamdahshafqat.abbasi@nih.gov)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/PolusAI/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/ome-converter-plugin:0.3.0", + "baseCommand": [ + "python3", + "-m", + "polus.plugins.formats.ome_converter" + ], + "inputs": [ + { + "name": "inpDir", + "type": "genericData", + "description": "Input generic data collection to be processed by this plugin", + "required": true + }, + { + "name": "filePattern", + "type": "string", + "description": "A filepattern, used to select data to be converted", + "required": true + }, + { + "name": "fileExtension", + "type": "enum", + "description": "Type of data conversion", + "default": "default", + "options": { + "values": [ + ".ome.tif", + ".ome.zarr", + "default" + ] + }, + "required": true + } + ], + "outputs": [ + { + "name": "outDir", + "type": "genericData", + "description": "Output collection" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input generic collection", + "description": "Input generic data collection to be processed by this plugin" + }, + { + "key": "inputs.filePattern", + "title": "Filepattern", + "description": "A filepattern, used to select data for conversion" + }, + { + "key": "inputs.fileExtension", + "title": "fileExtension", + "description": "Type of data conversion", + "default": ".ome.tif" + } + ] +} diff --git a/tests/resources/target1.cwl b/tests/resources/target1.cwl new file mode 100644 index 000000000..322dfb093 --- /dev/null +++ b/tests/resources/target1.cwl @@ -0,0 +1,32 @@ +class: CommandLineTool +cwlVersion: v1.2 +inputs: + fileExtension: + inputBinding: + prefix: --fileExtension + type: string + filePattern: + inputBinding: + prefix: --filePattern + type: string + inpDir: + inputBinding: + prefix: --inpDir + type: Directory + outDir: + inputBinding: + prefix: --outDir + type: Directory +outputs: + outDir: + outputBinding: + glob: $(inputs.outDir.basename) + type: Directory +requirements: + DockerRequirement: + dockerPull: polusai/ome-converter-plugin:0.3.0 + InitialWorkDirRequirement: + listing: + - entry: $(inputs.outDir) + writable: true + InlineJavascriptRequirement: {} diff --git a/tests/test_cwl.py b/tests/test_cwl.py new file mode 100644 index 000000000..e5a7a84ae --- /dev/null +++ b/tests/test_cwl.py @@ -0,0 +1,88 @@ +# type: ignore +"""Tests for CWL utils.""" +from pathlib import Path + +import pytest +import yaml + +import polus.plugins as pp +from polus.plugins._plugins.classes.plugin_methods import MissingInputValues + +OMECONVERTER = Path("./tests/resources/omeconverter030.json") + + +class TestCWL: + """Testing CWL utilities.""" + + @classmethod + def setup_class(cls): + """Configure the class.""" + pp.submit_plugin(OMECONVERTER) + + def test_save_cwl(self): + """Test save_cwl.""" + plug = pp.get_plugin("OmeConverter", "0.3.0") + plug.save_cwl(Path("./tests/resources/omeconverter.cwl")) + assert Path("./tests/resources/omeconverter.cwl").exists() + + def test_read_saved_cwl(self): + """Test saved cwl.""" + with open("./tests/resources/omeconverter.cwl", encoding="utf-8") as file: + src_cwl = file.read() + with open("./tests/resources/target1.cwl", encoding="utf-8") as file: + target_cwl = file.read() + assert src_cwl == target_cwl + + def test_save_cwl_io(self): + """Test save_cwl IO.""" + plug = pp.get_plugin("OmeConverter", "0.3.0") + plug.inpDir = Path("./tests/resources").absolute() + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + plug.fileExtension = ".ome.zarr" + plug.outDir = Path("./tests/resources").absolute() + plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) + + def test_save_cwl_io_not_inp(self): + """Test save_cwl IO.""" + plug = pp.get_plugin("OmeConverter", "0.3.0") + with pytest.raises(MissingInputValues): + plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) + + def test_save_cwl_io_not_inp2(self): + """Test save_cwl IO.""" + plug = pp.get_plugin("OmeConverter", "0.3.0") + plug.inpDir = Path("./tests/resources").absolute() + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + with pytest.raises(MissingInputValues): + plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) + + def test_save_cwl_io_not_yml(self): + """Test save_cwl IO.""" + plug = pp.get_plugin("OmeConverter", "0.3.0") + plug.inpDir = Path("./tests/resources").absolute() + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + plug.fileExtension = ".ome.zarr" + plug.outDir = Path("./tests/resources").absolute() + with pytest.raises(AssertionError): + plug.save_cwl_io(Path("./tests/resources/omeconverter_io.cwl")) + + def test_read_cwl_io(self): + """Test read_cwl_io.""" + with open("./tests/resources/omeconverter_io.yml", encoding="utf-8") as file: + src_io = yaml.safe_load(file) + assert src_io["inpDir"] == { + "class": "Directory", + "location": str(Path("./tests/resources").absolute()), + } + assert src_io["outDir"] == { + "class": "Directory", + "location": str(Path("./tests/resources").absolute()), + } + assert src_io["filePattern"] == "img_r{rrr}_c{ccc}.tif" + assert src_io["fileExtension"] == ".ome.zarr" + + @classmethod + def teardown_class(cls): + """Teardown.""" + Path("./tests/resources/omeconverter.cwl").unlink() + Path("./tests/resources/omeconverter_io.yml").unlink() diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 000000000..2594cb736 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,64 @@ +"""IO Tests.""" +from pathlib import Path + +import pytest +from fsspec.implementations.local import LocalFileSystem + +from polus.plugins._plugins.classes import load_plugin +from polus.plugins._plugins.classes.plugin_methods import IOKeyError +from polus.plugins._plugins.io import Input, IOBase + +path = Path(".") + +io1 = { + "type": "collection", + "name": "input1", + "required": True, + "description": "Test IO", +} +io2 = {"type": "boolean", "name": "input2", "required": True, "description": "Test IO"} +iob1 = { + "type": "collection", +} +plugin = load_plugin(path.joinpath("tests/resources/g1.json")) + + +class TestIO: + """Tests for I/O operations.""" + + def test_iobase(self): + """Test IOBase.""" + IOBase(**iob1) + + @pytest.mark.parametrize("io", [io1, io2], ids=["io1", "io2"]) + def test_input(self, io): + """Test Input.""" + Input(**io) + + def test_set_attr_invalid1(self): + """Test setting invalid attribute.""" + with pytest.raises(TypeError): + plugin.inputs[0].examples = [2, 5] + + def test_set_attr_invalid2(self): + """Test setting invalid attribute.""" + with pytest.raises(IOKeyError): + plugin.invalid = False + + def test_set_attr_valid1(self): + """Test setting valid attribute.""" + i = [x for x in plugin.inputs if x.name == "darkfield"] + i[0].value = True + + def test_set_attr_valid2(self): + """Test setting valid attribute.""" + plugin.darkfield = True + + def test_set_fsspec(self): + """Test setting fs valid attribute.""" + plugin._fs = LocalFileSystem() # pylint: disable=protected-access + + def test_set_fsspec2(self): + """Test setting fs invalid attribute.""" + with pytest.raises(ValueError): + plugin._fs = "./" # pylint: disable=protected-access diff --git a/tests/test_manifests.py b/tests/test_manifests.py new file mode 100644 index 000000000..7f2c6cbd3 --- /dev/null +++ b/tests/test_manifests.py @@ -0,0 +1,242 @@ +"""Test manifests utils.""" +from collections import OrderedDict +from pathlib import Path + +import pytest + +from polus.plugins._plugins.classes import list_plugins +from polus.plugins._plugins.classes.plugin_classes import PLUGINS +from polus.plugins._plugins.manifests.manifest_utils import ( + InvalidManifest, + _load_manifest, + validate_manifest, +) +from polus.plugins._plugins.models import ComputeSchema, WIPPPluginManifest + +path_resources = Path("./tests/resources") + +d_val = { + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": 'Peng et al. "A BaSiC tool for background and shading correction of optical microscopy images" Nature Communications (2017)', + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": True, + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": True, + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": True, + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": True, + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": False, + }, + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin", + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection...", + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets", + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values.", + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image", + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image", + }, + ], +} + +test_dict_load = OrderedDict( + { + "dictionary": { + "name": "BaSiC Flatfield Correction Plugin", + "version": "1.2.7", + "title": "Flatfield correction using BaSiC algorithm.", + "description": "Generates images used for flatfield correction using the BaSiC algorithm.", + "author": "Nick Schaub (nick.schaub@nih.gov)", + "institution": "National Center for the Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": 'Peng et al. "A BaSiC tool for background and shading correction of optical microscopy images" Nature Communications (2017)', + "containerId": "polusai/basic-flatfield-correction-plugin:1.2.7", + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection.", + "required": True, + }, + { + "name": "filePattern", + "type": "string", + "description": "Filename pattern used to separate images by channel, timepoint, and replicate.", + "required": True, + }, + { + "name": "darkfield", + "type": "boolean", + "description": "Calculate darkfield image.", + "required": True, + }, + { + "name": "photobleach", + "type": "boolean", + "description": "Calculate photobleaching offsets.", + "required": True, + }, + { + "name": "groupBy", + "type": "string", + "description": "Group images together for flatfield by variable.", + "required": False, + }, + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output data for the plugin", + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input image collection: ", + "description": "Image collection...", + }, + { + "key": "inputs.filePattern", + "title": "Filename pattern: ", + "description": "Use a filename pattern to calculate flatfield information by subsets", + }, + { + "key": "inputs.groupBy", + "title": "Grouping Variables: ", + "description": "Group data together with varying variable values.", + }, + { + "key": "inputs.darkfield", + "title": "Calculate darkfield: ", + "description": "If selected, will generate a darkfield image", + }, + { + "key": "inputs.photobleach", + "title": "Calclate photobleaching offset: ", + "description": "If selected, will generate an offset scalar for each image", + }, + ], + }, + "path": path_resources.joinpath("g1.json"), + } +) + +path = Path(".") +local_manifests = list(path.rglob("*plugin.json")) +local_manifests = [ + x for x in local_manifests if "cookiecutter.project" not in str(x) +] # filter cookiecutter templates +local_manifest_names = [str(x) for x in local_manifests] + + +def _get_path(manifest): + """Return path of local plugin manifest.""" + return PLUGINS[manifest][max(PLUGINS[manifest])] + + +class TestManifests: + """Test plugin manifests.""" + + @pytest.mark.repo + @pytest.mark.parametrize("manifest", local_manifests, ids=local_manifest_names) + def test_manifests_local(self, manifest): + """Test local (repo) manifests.""" + assert isinstance( + validate_manifest(manifest), (WIPPPluginManifest, ComputeSchema) + ) + + def test_list_plugins(self): + """Test `list_plugins()`.""" + o = list(PLUGINS.keys()) + o.sort() + assert o == list_plugins() + + @pytest.mark.parametrize("manifest", list_plugins(), ids=list_plugins()) + def test_manifests_plugindir(self, manifest): + """Test manifests available in polus-plugins installation dir.""" + p = _get_path(manifest) + assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) + + @pytest.mark.parametrize( + "type_", test_dict_load.values(), ids=test_dict_load.keys() + ) + def test_load_manifest(self, type_): # test path and dict + """Test _load_manifest() for types path and dict.""" + assert _load_manifest(type_) == d_val + + def test_load_manifest_str(self): + """Test _load_manifest() for str.""" + st_ = """{"a": 2, "b": "Polus"}""" + assert _load_manifest(st_) == {"a": 2, "b": "Polus"} + + bad = [f"b{x}.json" for x in [1, 2, 3]] + good = [f"g{x}.json" for x in [1, 2, 3]] + + @pytest.mark.parametrize("manifest", bad, ids=bad) + def test_bad_manifest(self, manifest): + """Test bad manifests raise InvalidManifest error.""" + with pytest.raises(InvalidManifest): + validate_manifest(path.joinpath("tests", "resources", manifest)) + + @pytest.mark.parametrize("manifest", good, ids=good) + def test_good_manifest(self, manifest): + """Test different manifests that all should pass validation.""" + p = path.joinpath("tests", "resources", manifest) + assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 000000000..ae4f85fd7 --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,167 @@ +# type: ignore +# pylint: disable=C0116, W0621 +"""Plugin Object Tests.""" +from pathlib import Path + +import polus.plugins as pp +from polus.plugins._plugins.classes.plugin_classes import Plugin, load_plugin + +OMECONVERTER = Path("./tests/resources/omeconverter.json") +BASIC_131 = ( + "https://raw.githubusercontent.com/PolusAI/polus-plugins/master" + "/regression/polus-basic-flatfield-correction-plugin/plugin.json" +) +BASIC_127 = ( + "https://raw.githubusercontent.com/PolusAI/polus-plugins/" + "440e64a51a578e21b574009424a75c848ebbbb03/regression/polus-basic" + "-flatfield-correction-plugin/plugin.json" +) + + +class TestPlugins: + """Test Plugin objects.""" + + @classmethod + def setup_class(cls): + """Configure the class.""" + pp.remove_all() + + def test_empty_list(self): + """Test empty list.""" + assert pp.list == [] + + def test_submit_plugin(self): + """Test submit_plugin.""" + pp.submit_plugin(OMECONVERTER) + # assert "OmeConverter" in pp.list + assert pp.list == ["OmeConverter"] + + def test_get_plugin(self): + """Test get_plugin.""" + assert isinstance(pp.get_plugin("OmeConverter"), Plugin) + + def test_url1(self): + """Test url submit.""" + pp.submit_plugin(BASIC_131) + assert ( + pp.list.sort() == ["OmeConverter", "BasicFlatfieldCorrectionPlugin"].sort() + ) + + def test_url2(self): + """Test url submit.""" + pp.submit_plugin(BASIC_127) + assert ( + pp.list.sort() == ["OmeConverter", "BasicFlatfieldCorrectionPlugin"].sort() + ) + + def test_load_plugin(self): + """Test load_plugin.""" + assert isinstance(load_plugin(OMECONVERTER), Plugin) + + def test_load_plugin2(self): + """Test load_plugin.""" + assert isinstance(load_plugin(BASIC_131), Plugin) + + def test_attr1(self): + """Test attributes.""" + p_attr = pp.OmeConverter + p_get = pp.get_plugin("OmeConverter") + for attr in ["name", "version", "inputs", "outputs"]: + assert getattr(p_attr, attr) == getattr(p_get, attr) + + def test_attr2(self): + """Test attributes.""" + p_attr = pp.BasicFlatfieldCorrectionPlugin + p_get = pp.get_plugin("BasicFlatfieldCorrectionPlugin") + for attr in ["name", "version", "inputs", "outputs"]: + assert getattr(p_attr, attr) == getattr(p_get, attr) + + def test_versions(self): + """Test versions.""" + assert [ + x.version for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions + ].sort() == [ + "1.3.1", + "1.2.7", + ].sort() + + def test_get_max_version1(self): + """Test get max version.""" + plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin") + assert plug.version.version == "1.3.1" + + def test_get_max_version2(self): + """Test get max version.""" + plug = pp.BasicFlatfieldCorrectionPlugin + assert plug.version.version == "1.3.1" + + def test_get_specific_version(self): + """Test get specific version.""" + plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") + assert plug.version.version == "1.2.7" + + def test_remove_version(self): + """Test remove version.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") + assert [x.version for x in pp.BasicFlatfieldCorrectionPlugin.versions] == [ + "1.3.1" + ] + + def test_resubmit_plugin(self): + """Test resubmit plugin.""" + pp.submit_plugin(BASIC_127) + + def test_remove_all_versions_plugin(self): + """Test remove all versions plugin.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin") + assert pp.list == ["OmeConverter"] + + def test_resubmit_plugin2(self): + """Test resubmit plugin.""" + pp.submit_plugin(BASIC_131) + + +class TestConfig: + """Test Plugin objects.""" + + @classmethod + def setup_class(cls): + """Configure the class.""" + if "BasicFlatfieldCorrectionPlugin" not in pp.list: + pp.submit_plugin(BASIC_131) + cls.plug1 = pp.BasicFlatfieldCorrectionPlugin + cls.plug1.inpDir = Path("./tests/resources").absolute() + cls.plug1.outDir = str(Path("./tests/").absolute()) + cls.plug1.filePattern = "*.ome.tif" + cls.plug1.darkfield = True + cls.plug1.photobleach = False + + def test_save_config(self): + """Test save_config file.""" + self.plug1.save_config("./tests/resources/config1.json") + assert Path("./tests/resources/config1.json").exists() + + def test_load_config(self): + """Test load_config from config file.""" + plug2 = pp.load_config(Path("./tests/resources/config1.json")) + for i_o in ["inpDir", "outDir", "filePattern"]: + assert getattr(plug2, i_o) == getattr(self.plug1, i_o) + assert plug2.id == self.plug1.id + + def test_load_config_no_plugin(self): + """Test load_config after removing plugin.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin") + assert pp.list == ["OmeConverter"] + plug2 = pp.load_config(Path("./tests/resources/config1.json")) + assert isinstance(plug2, Plugin) + assert plug2.id == self.plug1.id + + def test_remove_all(self): + """Test remove_all.""" + pp.remove_all() + assert pp.list == [] + + @classmethod + def teardown_class(cls): + """Teardown class.""" + Path("./tests/resources/config1.json").unlink() diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 000000000..be2e70fba --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,85 @@ +"""Test Version object and cast_version utility function.""" +import pytest +from pydantic import ValidationError + +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.utils import cast_version + +g = [ + "1.2.3", + "1.4.7-rc1", + "4.1.5", + "12.8.3", + "10.2.0", + "2.2.3-dev5", + "0.3.4", + "0.2.34-rc23", +] +b = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] + + +class TestVersion: + """Test Version object.""" + + @pytest.mark.parametrize("ver", g, ids=g) + def test_version(self, ver): + """Test Version pydantic model.""" + Version(version=ver) + + @pytest.mark.parametrize("ver", g, ids=g) + def test_cast_version(self, ver): + """Test cast_version utility function.""" + cast_version(ver) + + @pytest.mark.parametrize("ver", b, ids=b) + def test_bad_version1(self, ver): + """Test ValidationError is raised for invalid versions.""" + with pytest.raises(ValidationError): + cast_version(ver) + + mj = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] + mn = ["1.3.3", "7.3.4", "98.3.12", "23.3.0", "1.3.5"] + pt = ["12.2.7", "2.3.7", "1.7.7", "7.7.7", "7.29.7"] + + @pytest.mark.parametrize("ver", mj, ids=mj) + def test_major(self, ver): + """Test major version.""" + assert cast_version(ver).major == "2" + + @pytest.mark.parametrize("ver", mn, ids=mn) + def test_minor(self, ver): + """Test minor version.""" + assert cast_version(ver).minor == "3" + + @pytest.mark.parametrize("ver", pt, ids=pt) + def test_patch(self, ver): + """Test patch version.""" + assert cast_version(ver).patch == "7" + + def test_gt1(self): + """Test greater than operator.""" + assert cast_version("1.2.3") > cast_version("1.2.1") + + def test_gt2(self): + """Test greater than operator.""" + assert cast_version("5.7.3") > cast_version("5.6.3") + + def test_st1(self): + """Test less than operator.""" + assert cast_version("5.7.3") < cast_version("5.7.31") + + def test_st2(self): + """Test less than operator.""" + assert cast_version("1.0.2") < cast_version("2.0.2") + + def test_eq1(self): + """Test equality operator.""" + assert Version(version="1.3.3") == cast_version("1.3.3") + + def test_eq2(self): + """Test equality operator.""" + assert Version(version="5.4.3") == cast_version("5.4.3") + + def test_eq3(self): + """Test equality operator.""" + assert Version(version="1.3.3") != cast_version("1.3.8") From dc02e086284130c43fa70a862ff556a438730a37 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 22 May 2023 19:10:35 -0400 Subject: [PATCH 34/47] tests: refactored test_cwl --- tests/test_cwl.py | 166 ++++++++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/tests/test_cwl.py b/tests/test_cwl.py index e5a7a84ae..4e4b26047 100644 --- a/tests/test_cwl.py +++ b/tests/test_cwl.py @@ -1,4 +1,5 @@ # type: ignore +# pylint: disable=W0621, W0613 """Tests for CWL utils.""" from pathlib import Path @@ -8,81 +9,100 @@ import polus.plugins as pp from polus.plugins._plugins.classes.plugin_methods import MissingInputValues -OMECONVERTER = Path("./tests/resources/omeconverter030.json") +TEST_PATH = Path(__file__).parent +RSRC_PATH = TEST_PATH.joinpath("resources") +OMECONVERTER = RSRC_PATH.joinpath("omeconverter030.json") -class TestCWL: - """Testing CWL utilities.""" - @classmethod - def setup_class(cls): - """Configure the class.""" +@pytest.fixture +def submit_plugin(): + """Submit OmeConverter plugin.""" + if "OmeConverter" not in pp.list: pp.submit_plugin(OMECONVERTER) + else: + if "0.3.0" not in [x.version for x in pp.OmeConverter.versions]: + pp.submit_plugin(OMECONVERTER) - def test_save_cwl(self): - """Test save_cwl.""" - plug = pp.get_plugin("OmeConverter", "0.3.0") - plug.save_cwl(Path("./tests/resources/omeconverter.cwl")) - assert Path("./tests/resources/omeconverter.cwl").exists() - - def test_read_saved_cwl(self): - """Test saved cwl.""" - with open("./tests/resources/omeconverter.cwl", encoding="utf-8") as file: - src_cwl = file.read() - with open("./tests/resources/target1.cwl", encoding="utf-8") as file: - target_cwl = file.read() - assert src_cwl == target_cwl - - def test_save_cwl_io(self): - """Test save_cwl IO.""" - plug = pp.get_plugin("OmeConverter", "0.3.0") - plug.inpDir = Path("./tests/resources").absolute() - plug.filePattern = "img_r{rrr}_c{ccc}.tif" - plug.fileExtension = ".ome.zarr" - plug.outDir = Path("./tests/resources").absolute() - plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) - - def test_save_cwl_io_not_inp(self): - """Test save_cwl IO.""" - plug = pp.get_plugin("OmeConverter", "0.3.0") - with pytest.raises(MissingInputValues): - plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) - - def test_save_cwl_io_not_inp2(self): - """Test save_cwl IO.""" - plug = pp.get_plugin("OmeConverter", "0.3.0") - plug.inpDir = Path("./tests/resources").absolute() - plug.filePattern = "img_r{rrr}_c{ccc}.tif" - with pytest.raises(MissingInputValues): - plug.save_cwl_io(Path("./tests/resources/omeconverter_io.yml")) - - def test_save_cwl_io_not_yml(self): - """Test save_cwl IO.""" - plug = pp.get_plugin("OmeConverter", "0.3.0") - plug.inpDir = Path("./tests/resources").absolute() - plug.filePattern = "img_r{rrr}_c{ccc}.tif" - plug.fileExtension = ".ome.zarr" - plug.outDir = Path("./tests/resources").absolute() - with pytest.raises(AssertionError): - plug.save_cwl_io(Path("./tests/resources/omeconverter_io.cwl")) - - def test_read_cwl_io(self): - """Test read_cwl_io.""" - with open("./tests/resources/omeconverter_io.yml", encoding="utf-8") as file: - src_io = yaml.safe_load(file) - assert src_io["inpDir"] == { - "class": "Directory", - "location": str(Path("./tests/resources").absolute()), - } - assert src_io["outDir"] == { - "class": "Directory", - "location": str(Path("./tests/resources").absolute()), - } - assert src_io["filePattern"] == "img_r{rrr}_c{ccc}.tif" - assert src_io["fileExtension"] == ".ome.zarr" - - @classmethod - def teardown_class(cls): - """Teardown.""" - Path("./tests/resources/omeconverter.cwl").unlink() - Path("./tests/resources/omeconverter_io.yml").unlink() + +@pytest.fixture +def plug(submit_plugin): + """Get OmeConverter plugin.""" + return pp.get_plugin("OmeConverter", "0.3.0") + + +@pytest.fixture(scope="session") +def cwl_io_path(tmp_path_factory): + """Temp CWL IO path.""" + return tmp_path_factory.mktemp("io") / "omeconverter_io.yml" + + +@pytest.fixture(scope="session") +def cwl_path(tmp_path_factory): + """Temp CWL IO path.""" + return tmp_path_factory.mktemp("cwl") / "omeconverter.cwl" + + +def test_save_cwl(plug, cwl_path): + """Test save_cwl.""" + plug.save_cwl(cwl_path) + assert cwl_path.exists() + + +def test_read_saved_cwl(cwl_path): + """Test saved cwl.""" + with open(cwl_path, encoding="utf-8") as file: + src_cwl = file.read() + with open(RSRC_PATH.joinpath("target1.cwl"), encoding="utf-8") as file: + target_cwl = file.read() + assert src_cwl == target_cwl + + +def test_save_cwl_io(plug, cwl_io_path): + """Test save_cwl IO.""" + rs_path = RSRC_PATH.absolute() + plug.inpDir = rs_path + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + plug.fileExtension = ".ome.zarr" + plug.outDir = rs_path + plug.save_cwl_io(cwl_io_path) + + +def test_save_cwl_io_not_inp(plug, cwl_io_path): + """Test save_cwl IO.""" + with pytest.raises(MissingInputValues): + plug.save_cwl_io(cwl_io_path) + + +def test_save_cwl_io_not_inp2(plug, cwl_io_path): + """Test save_cwl IO.""" + plug.inpDir = RSRC_PATH.absolute() + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + with pytest.raises(MissingInputValues): + plug.save_cwl_io(cwl_io_path) + + +def test_save_cwl_io_not_yml(plug, cwl_io_path): + """Test save_cwl IO.""" + plug.inpDir = RSRC_PATH.absolute() + plug.filePattern = "img_r{rrr}_c{ccc}.tif" + plug.fileExtension = ".ome.zarr" + plug.outDir = RSRC_PATH.absolute() + with pytest.raises(AssertionError): + plug.save_cwl_io(cwl_io_path.with_suffix(".txt")) + + +def test_read_cwl_io(cwl_io_path): + """Test read_cwl_io.""" + with open(cwl_io_path, encoding="utf-8") as file: + src_io = yaml.safe_load(file) + assert src_io["inpDir"] == { + "class": "Directory", + "location": str(RSRC_PATH.absolute()), + } + assert src_io["outDir"] == { + "class": "Directory", + "location": str(RSRC_PATH.absolute()), + } + assert src_io["filePattern"] == "img_r{rrr}_c{ccc}.tif" + assert src_io["fileExtension"] == ".ome.zarr" From 4cb0e8c1e05be85e4b5cd885fc9bcd700dc6876a Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 22 May 2023 22:45:44 -0400 Subject: [PATCH 35/47] tests: refactored tests --- tests/test_cwl.py | 3 +- tests/test_io.py | 71 +++++----- tests/test_manifests.py | 21 +-- tests/test_plugins.py | 300 +++++++++++++++++++++------------------- tests/test_version.py | 136 +++++++++--------- 5 files changed, 282 insertions(+), 249 deletions(-) diff --git a/tests/test_cwl.py b/tests/test_cwl.py index 4e4b26047..de713f45f 100644 --- a/tests/test_cwl.py +++ b/tests/test_cwl.py @@ -9,8 +9,7 @@ import polus.plugins as pp from polus.plugins._plugins.classes.plugin_methods import MissingInputValues -TEST_PATH = Path(__file__).parent -RSRC_PATH = TEST_PATH.joinpath("resources") +RSRC_PATH = Path(__file__).parent.joinpath("resources") OMECONVERTER = RSRC_PATH.joinpath("omeconverter030.json") diff --git a/tests/test_io.py b/tests/test_io.py index 2594cb736..885c15320 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,3 +1,4 @@ +# pylint: disable=C0103 """IO Tests.""" from pathlib import Path @@ -8,7 +9,7 @@ from polus.plugins._plugins.classes.plugin_methods import IOKeyError from polus.plugins._plugins.io import Input, IOBase -path = Path(".") +RSRC_PATH = Path(__file__).parent.joinpath("resources") io1 = { "type": "collection", @@ -20,45 +21,49 @@ iob1 = { "type": "collection", } -plugin = load_plugin(path.joinpath("tests/resources/g1.json")) +plugin = load_plugin(RSRC_PATH.joinpath("g1.json")) -class TestIO: - """Tests for I/O operations.""" +def test_iobase(): + """Test IOBase.""" + IOBase(**iob1) - def test_iobase(self): - """Test IOBase.""" - IOBase(**iob1) - @pytest.mark.parametrize("io", [io1, io2], ids=["io1", "io2"]) - def test_input(self, io): - """Test Input.""" - Input(**io) +@pytest.mark.parametrize("io", [io1, io2], ids=["io1", "io2"]) +def test_input(io): + """Test Input.""" + Input(**io) - def test_set_attr_invalid1(self): - """Test setting invalid attribute.""" - with pytest.raises(TypeError): - plugin.inputs[0].examples = [2, 5] - def test_set_attr_invalid2(self): - """Test setting invalid attribute.""" - with pytest.raises(IOKeyError): - plugin.invalid = False +def test_set_attr_invalid1(): + """Test setting invalid attribute.""" + with pytest.raises(TypeError): + plugin.inputs[0].examples = [2, 5] - def test_set_attr_valid1(self): - """Test setting valid attribute.""" - i = [x for x in plugin.inputs if x.name == "darkfield"] - i[0].value = True - def test_set_attr_valid2(self): - """Test setting valid attribute.""" - plugin.darkfield = True +def test_set_attr_invalid2(): + """Test setting invalid attribute.""" + with pytest.raises(IOKeyError): + plugin.invalid = False - def test_set_fsspec(self): - """Test setting fs valid attribute.""" - plugin._fs = LocalFileSystem() # pylint: disable=protected-access - def test_set_fsspec2(self): - """Test setting fs invalid attribute.""" - with pytest.raises(ValueError): - plugin._fs = "./" # pylint: disable=protected-access +def test_set_attr_valid1(): + """Test setting valid attribute.""" + i = [x for x in plugin.inputs if x.name == "darkfield"] + i[0].value = True + + +def test_set_attr_valid2(): + """Test setting valid attribute.""" + plugin.darkfield = True + + +def test_set_fsspec(): + """Test setting fs valid attribute.""" + plugin._fs = LocalFileSystem() # pylint: disable=protected-access + + +def test_set_fsspec2(): + """Test setting fs invalid attribute.""" + with pytest.raises(ValueError): + plugin._fs = "./" # pylint: disable=protected-access diff --git a/tests/test_manifests.py b/tests/test_manifests.py index 7f2c6cbd3..6238e28f2 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -1,3 +1,4 @@ +# pylint: disable=C0103 """Test manifests utils.""" from collections import OrderedDict from pathlib import Path @@ -13,7 +14,7 @@ ) from polus.plugins._plugins.models import ComputeSchema, WIPPPluginManifest -path_resources = Path("./tests/resources") +RSRC_PATH = Path(__file__).parent.joinpath("resources") d_val = { "name": "BaSiC Flatfield Correction Plugin", @@ -174,16 +175,16 @@ }, ], }, - "path": path_resources.joinpath("g1.json"), + "path": RSRC_PATH.joinpath("g1.json"), } ) -path = Path(".") -local_manifests = list(path.rglob("*plugin.json")) -local_manifests = [ - x for x in local_manifests if "cookiecutter.project" not in str(x) +REPO_PATH = RSRC_PATH.parent.parent +LOCAL_MANIFESTS = list(REPO_PATH.rglob("*plugin.json")) +LOCAL_MANIFESTS = [ + x for x in LOCAL_MANIFESTS if "cookiecutter.project" not in str(x) ] # filter cookiecutter templates -local_manifest_names = [str(x) for x in local_manifests] +LOCAL_MANIFEST_NAMES = [str(x) for x in LOCAL_MANIFESTS] def _get_path(manifest): @@ -195,7 +196,7 @@ class TestManifests: """Test plugin manifests.""" @pytest.mark.repo - @pytest.mark.parametrize("manifest", local_manifests, ids=local_manifest_names) + @pytest.mark.parametrize("manifest", LOCAL_MANIFESTS, ids=LOCAL_MANIFEST_NAMES) def test_manifests_local(self, manifest): """Test local (repo) manifests.""" assert isinstance( @@ -233,10 +234,10 @@ def test_load_manifest_str(self): def test_bad_manifest(self, manifest): """Test bad manifests raise InvalidManifest error.""" with pytest.raises(InvalidManifest): - validate_manifest(path.joinpath("tests", "resources", manifest)) + validate_manifest(REPO_PATH.joinpath("tests", "resources", manifest)) @pytest.mark.parametrize("manifest", good, ids=good) def test_good_manifest(self, manifest): """Test different manifests that all should pass validation.""" - p = path.joinpath("tests", "resources", manifest) + p = RSRC_PATH.joinpath(manifest) assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index ae4f85fd7..9c7d48f9c 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,12 +1,15 @@ # type: ignore -# pylint: disable=C0116, W0621 +# pylint: disable=C0116, W0621, W0613 """Plugin Object Tests.""" from pathlib import Path +import pytest + import polus.plugins as pp from polus.plugins._plugins.classes.plugin_classes import Plugin, load_plugin -OMECONVERTER = Path("./tests/resources/omeconverter.json") +RSRC_PATH = Path(__file__).parent.joinpath("resources") +OMECONVERTER = RSRC_PATH.joinpath("omeconverter.json") BASIC_131 = ( "https://raw.githubusercontent.com/PolusAI/polus-plugins/master" "/regression/polus-basic-flatfield-correction-plugin/plugin.json" @@ -18,150 +21,165 @@ ) -class TestPlugins: - """Test Plugin objects.""" +@pytest.fixture +def remove_all(): + """Remove all plugins.""" + pp.remove_all() - @classmethod - def setup_class(cls): - """Configure the class.""" - pp.remove_all() - def test_empty_list(self): - """Test empty list.""" - assert pp.list == [] +def test_empty_list(remove_all): + """Test empty list.""" + assert pp.list == [] - def test_submit_plugin(self): - """Test submit_plugin.""" - pp.submit_plugin(OMECONVERTER) - # assert "OmeConverter" in pp.list - assert pp.list == ["OmeConverter"] - def test_get_plugin(self): - """Test get_plugin.""" - assert isinstance(pp.get_plugin("OmeConverter"), Plugin) +def test_submit_plugin(): + """Test submit_plugin.""" + pp.submit_plugin(OMECONVERTER) + assert pp.list == ["OmeConverter"] - def test_url1(self): - """Test url submit.""" - pp.submit_plugin(BASIC_131) - assert ( - pp.list.sort() == ["OmeConverter", "BasicFlatfieldCorrectionPlugin"].sort() - ) - - def test_url2(self): - """Test url submit.""" - pp.submit_plugin(BASIC_127) - assert ( - pp.list.sort() == ["OmeConverter", "BasicFlatfieldCorrectionPlugin"].sort() - ) - - def test_load_plugin(self): - """Test load_plugin.""" - assert isinstance(load_plugin(OMECONVERTER), Plugin) - - def test_load_plugin2(self): - """Test load_plugin.""" - assert isinstance(load_plugin(BASIC_131), Plugin) - - def test_attr1(self): - """Test attributes.""" - p_attr = pp.OmeConverter - p_get = pp.get_plugin("OmeConverter") - for attr in ["name", "version", "inputs", "outputs"]: - assert getattr(p_attr, attr) == getattr(p_get, attr) - - def test_attr2(self): - """Test attributes.""" - p_attr = pp.BasicFlatfieldCorrectionPlugin - p_get = pp.get_plugin("BasicFlatfieldCorrectionPlugin") - for attr in ["name", "version", "inputs", "outputs"]: - assert getattr(p_attr, attr) == getattr(p_get, attr) - - def test_versions(self): - """Test versions.""" - assert [ - x.version for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions - ].sort() == [ - "1.3.1", - "1.2.7", - ].sort() - - def test_get_max_version1(self): - """Test get max version.""" - plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin") - assert plug.version.version == "1.3.1" - - def test_get_max_version2(self): - """Test get max version.""" - plug = pp.BasicFlatfieldCorrectionPlugin - assert plug.version.version == "1.3.1" - - def test_get_specific_version(self): - """Test get specific version.""" - plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") - assert plug.version.version == "1.2.7" - - def test_remove_version(self): - """Test remove version.""" - pp.remove_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") - assert [x.version for x in pp.BasicFlatfieldCorrectionPlugin.versions] == [ - "1.3.1" - ] - - def test_resubmit_plugin(self): - """Test resubmit plugin.""" - pp.submit_plugin(BASIC_127) - - def test_remove_all_versions_plugin(self): - """Test remove all versions plugin.""" - pp.remove_plugin("BasicFlatfieldCorrectionPlugin") - assert pp.list == ["OmeConverter"] - - def test_resubmit_plugin2(self): - """Test resubmit plugin.""" + +def test_get_plugin(): + """Test get_plugin.""" + assert isinstance(pp.get_plugin("OmeConverter"), Plugin) + + +def test_url1(): + """Test url submit.""" + pp.submit_plugin(BASIC_131) + assert sorted(pp.list) == ["BasicFlatfieldCorrectionPlugin", "OmeConverter"] + + +def test_url2(): + """Test url submit.""" + pp.submit_plugin(BASIC_127) + assert sorted(pp.list) == ["BasicFlatfieldCorrectionPlugin", "OmeConverter"] + + +def test_load_plugin(): + """Test load_plugin.""" + assert isinstance(load_plugin(OMECONVERTER), Plugin) + + +def test_load_plugin2(): + """Test load_plugin.""" + assert isinstance(load_plugin(BASIC_131), Plugin) + + +def test_attr1(): + """Test attributes.""" + p_attr = pp.OmeConverter + p_get = pp.get_plugin("OmeConverter") + for attr in ["name", "version", "inputs", "outputs"]: + assert getattr(p_attr, attr) == getattr(p_get, attr) + + +def test_attr2(): + """Test attributes.""" + p_attr = pp.BasicFlatfieldCorrectionPlugin + p_get = pp.get_plugin("BasicFlatfieldCorrectionPlugin") + for attr in ["name", "version", "inputs", "outputs"]: + assert getattr(p_attr, attr) == getattr(p_get, attr) + + +def test_versions(): + """Test versions.""" + assert sorted( + [x.version for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions] + ) == [ + "1.2.7", + "1.3.1", + ] + + +def test_get_max_version1(): + """Test get max version.""" + plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin") + assert plug.version.version == "1.3.1" + + +def test_get_max_version2(): + """Test get max version.""" + plug = pp.BasicFlatfieldCorrectionPlugin + assert plug.version.version == "1.3.1" + + +def test_get_specific_version(): + """Test get specific version.""" + plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") + assert plug.version.version == "1.2.7" + + +def test_remove_version(): + """Test remove version.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") + assert [x.version for x in pp.BasicFlatfieldCorrectionPlugin.versions] == ["1.3.1"] + + +def test_resubmit_plugin(): + """Test resubmit plugin.""" + pp.submit_plugin(BASIC_127) + + +def test_remove_all_versions_plugin(): + """Test remove all versions plugin.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin") + assert pp.list == ["OmeConverter"] + + +def test_resubmit_plugin2(): + """Test resubmit plugin.""" + pp.submit_plugin(BASIC_131) + + +@pytest.fixture(scope="session") +def plug0(): + """Fixture to submit plugin.""" + if "BasicFlatfieldCorrectionPlugin" not in pp.list: pp.submit_plugin(BASIC_131) -class TestConfig: - """Test Plugin objects.""" - - @classmethod - def setup_class(cls): - """Configure the class.""" - if "BasicFlatfieldCorrectionPlugin" not in pp.list: - pp.submit_plugin(BASIC_131) - cls.plug1 = pp.BasicFlatfieldCorrectionPlugin - cls.plug1.inpDir = Path("./tests/resources").absolute() - cls.plug1.outDir = str(Path("./tests/").absolute()) - cls.plug1.filePattern = "*.ome.tif" - cls.plug1.darkfield = True - cls.plug1.photobleach = False - - def test_save_config(self): - """Test save_config file.""" - self.plug1.save_config("./tests/resources/config1.json") - assert Path("./tests/resources/config1.json").exists() - - def test_load_config(self): - """Test load_config from config file.""" - plug2 = pp.load_config(Path("./tests/resources/config1.json")) - for i_o in ["inpDir", "outDir", "filePattern"]: - assert getattr(plug2, i_o) == getattr(self.plug1, i_o) - assert plug2.id == self.plug1.id - - def test_load_config_no_plugin(self): - """Test load_config after removing plugin.""" - pp.remove_plugin("BasicFlatfieldCorrectionPlugin") - assert pp.list == ["OmeConverter"] - plug2 = pp.load_config(Path("./tests/resources/config1.json")) - assert isinstance(plug2, Plugin) - assert plug2.id == self.plug1.id - - def test_remove_all(self): - """Test remove_all.""" - pp.remove_all() - assert pp.list == [] - - @classmethod - def teardown_class(cls): - """Teardown class.""" - Path("./tests/resources/config1.json").unlink() +@pytest.fixture(scope="session") +def plug1(plug0): + """Configure the class.""" + plug1 = pp.BasicFlatfieldCorrectionPlugin + plug1.inpDir = RSRC_PATH.absolute() + plug1.outDir = RSRC_PATH.absolute() + plug1.filePattern = "*.ome.tif" + plug1.darkfield = True + plug1.photobleach = False + return plug1 + + +@pytest.fixture(scope="session") +def config_path(tmp_path_factory): + """Temp config path.""" + return tmp_path_factory.mktemp("config") / "config1.json" + + +def test_save_config(plug1, config_path): + """Test save_config file.""" + plug1.save_config(config_path) + assert Path(config_path).exists() + + +def test_load_config(plug1, config_path): + """Test load_config from config file.""" + plug2 = pp.load_config(config_path) + for i_o in ["inpDir", "outDir", "filePattern"]: + assert getattr(plug2, i_o) == getattr(plug1, i_o) + assert plug2.id == plug1.id + + +def test_load_config_no_plugin(plug1, config_path): + """Test load_config after removing plugin.""" + pp.remove_plugin("BasicFlatfieldCorrectionPlugin") + assert pp.list == ["OmeConverter"] + plug2 = pp.load_config(config_path) + assert isinstance(plug2, Plugin) + assert plug2.id == plug1.id + + +def test_remove_all(): + """Test remove_all.""" + pp.remove_all() + assert pp.list == [] diff --git a/tests/test_version.py b/tests/test_version.py index be2e70fba..5bad7cbae 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,7 +5,7 @@ from polus.plugins._plugins.io import Version from polus.plugins._plugins.utils import cast_version -g = [ +G = [ "1.2.3", "1.4.7-rc1", "4.1.5", @@ -15,71 +15,81 @@ "0.3.4", "0.2.34-rc23", ] -b = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] +B = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] -class TestVersion: - """Test Version object.""" +@pytest.mark.parametrize("ver", G, ids=G) +def test_version(ver): + """Test Version pydantic model.""" + Version(version=ver) - @pytest.mark.parametrize("ver", g, ids=g) - def test_version(self, ver): - """Test Version pydantic model.""" - Version(version=ver) - @pytest.mark.parametrize("ver", g, ids=g) - def test_cast_version(self, ver): - """Test cast_version utility function.""" +@pytest.mark.parametrize("ver", G, ids=G) +def test_cast_version(ver): + """Test cast_version utility function.""" + cast_version(ver) + + +@pytest.mark.parametrize("ver", B, ids=B) +def test_bad_version1(ver): + """Test ValidationError is raised for invalid versions.""" + with pytest.raises(ValidationError): cast_version(ver) - @pytest.mark.parametrize("ver", b, ids=b) - def test_bad_version1(self, ver): - """Test ValidationError is raised for invalid versions.""" - with pytest.raises(ValidationError): - cast_version(ver) - - mj = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] - mn = ["1.3.3", "7.3.4", "98.3.12", "23.3.0", "1.3.5"] - pt = ["12.2.7", "2.3.7", "1.7.7", "7.7.7", "7.29.7"] - - @pytest.mark.parametrize("ver", mj, ids=mj) - def test_major(self, ver): - """Test major version.""" - assert cast_version(ver).major == "2" - - @pytest.mark.parametrize("ver", mn, ids=mn) - def test_minor(self, ver): - """Test minor version.""" - assert cast_version(ver).minor == "3" - - @pytest.mark.parametrize("ver", pt, ids=pt) - def test_patch(self, ver): - """Test patch version.""" - assert cast_version(ver).patch == "7" - - def test_gt1(self): - """Test greater than operator.""" - assert cast_version("1.2.3") > cast_version("1.2.1") - - def test_gt2(self): - """Test greater than operator.""" - assert cast_version("5.7.3") > cast_version("5.6.3") - - def test_st1(self): - """Test less than operator.""" - assert cast_version("5.7.3") < cast_version("5.7.31") - - def test_st2(self): - """Test less than operator.""" - assert cast_version("1.0.2") < cast_version("2.0.2") - - def test_eq1(self): - """Test equality operator.""" - assert Version(version="1.3.3") == cast_version("1.3.3") - - def test_eq2(self): - """Test equality operator.""" - assert Version(version="5.4.3") == cast_version("5.4.3") - - def test_eq3(self): - """Test equality operator.""" - assert Version(version="1.3.3") != cast_version("1.3.8") + +mj = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] +mn = ["1.3.3", "7.3.4", "98.3.12", "23.3.0", "1.3.5"] +pt = ["12.2.7", "2.3.7", "1.7.7", "7.7.7", "7.29.7"] + + +@pytest.mark.parametrize("ver", mj, ids=mj) +def test_major(ver): + """Test major version.""" + assert cast_version(ver).major == "2" + + +@pytest.mark.parametrize("ver", mn, ids=mn) +def test_minor(ver): + """Test minor version.""" + assert cast_version(ver).minor == "3" + + +@pytest.mark.parametrize("ver", pt, ids=pt) +def test_patch(ver): + """Test patch version.""" + assert cast_version(ver).patch == "7" + + +def test_gt1(): + """Test greater than operator.""" + assert cast_version("1.2.3") > cast_version("1.2.1") + + +def test_gt2(): + """Test greater than operator.""" + assert cast_version("5.7.3") > cast_version("5.6.3") + + +def test_st1(): + """Test less than operator.""" + assert cast_version("5.7.3") < cast_version("5.7.31") + + +def test_st2(): + """Test less than operator.""" + assert cast_version("1.0.2") < cast_version("2.0.2") + + +def test_eq1(): + """Test equality operator.""" + assert Version(version="1.3.3") == cast_version("1.3.3") + + +def test_eq2(): + """Test equality operator.""" + assert Version(version="5.4.3") == cast_version("5.4.3") + + +def test_eq3(): + """Test equality operator.""" + assert Version(version="1.3.3") != cast_version("1.3.8") From 0c3828b9ba32ed3ec983a8798e1b45aa515e48d5 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 30 May 2023 09:41:42 -0400 Subject: [PATCH 36/47] refactor: renamed omeconverter.json --- tests/resources/{omeconverter.json => omeconverter022.json} | 0 tests/test_plugins.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/resources/{omeconverter.json => omeconverter022.json} (100%) diff --git a/tests/resources/omeconverter.json b/tests/resources/omeconverter022.json similarity index 100% rename from tests/resources/omeconverter.json rename to tests/resources/omeconverter022.json diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 9c7d48f9c..c5dc7a6c0 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -9,7 +9,7 @@ from polus.plugins._plugins.classes.plugin_classes import Plugin, load_plugin RSRC_PATH = Path(__file__).parent.joinpath("resources") -OMECONVERTER = RSRC_PATH.joinpath("omeconverter.json") +OMECONVERTER = RSRC_PATH.joinpath("omeconverter022.json") BASIC_131 = ( "https://raw.githubusercontent.com/PolusAI/polus-plugins/master" "/regression/polus-basic-flatfield-correction-plugin/plugin.json" From 6311575ad2e4b3317fa882a904f7dca95bf21f78 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Mon, 5 Jun 2023 08:20:17 -0400 Subject: [PATCH 37/47] refactor: refactored test_plugins test_manifests --- tests/test_manifests.py | 98 ++++++++++++++++++++--------------------- tests/test_plugins.py | 12 +++-- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/tests/test_manifests.py b/tests/test_manifests.py index 6238e28f2..dd4cccb7e 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -192,52 +192,52 @@ def _get_path(manifest): return PLUGINS[manifest][max(PLUGINS[manifest])] -class TestManifests: - """Test plugin manifests.""" - - @pytest.mark.repo - @pytest.mark.parametrize("manifest", LOCAL_MANIFESTS, ids=LOCAL_MANIFEST_NAMES) - def test_manifests_local(self, manifest): - """Test local (repo) manifests.""" - assert isinstance( - validate_manifest(manifest), (WIPPPluginManifest, ComputeSchema) - ) - - def test_list_plugins(self): - """Test `list_plugins()`.""" - o = list(PLUGINS.keys()) - o.sort() - assert o == list_plugins() - - @pytest.mark.parametrize("manifest", list_plugins(), ids=list_plugins()) - def test_manifests_plugindir(self, manifest): - """Test manifests available in polus-plugins installation dir.""" - p = _get_path(manifest) - assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) - - @pytest.mark.parametrize( - "type_", test_dict_load.values(), ids=test_dict_load.keys() - ) - def test_load_manifest(self, type_): # test path and dict - """Test _load_manifest() for types path and dict.""" - assert _load_manifest(type_) == d_val - - def test_load_manifest_str(self): - """Test _load_manifest() for str.""" - st_ = """{"a": 2, "b": "Polus"}""" - assert _load_manifest(st_) == {"a": 2, "b": "Polus"} - - bad = [f"b{x}.json" for x in [1, 2, 3]] - good = [f"g{x}.json" for x in [1, 2, 3]] - - @pytest.mark.parametrize("manifest", bad, ids=bad) - def test_bad_manifest(self, manifest): - """Test bad manifests raise InvalidManifest error.""" - with pytest.raises(InvalidManifest): - validate_manifest(REPO_PATH.joinpath("tests", "resources", manifest)) - - @pytest.mark.parametrize("manifest", good, ids=good) - def test_good_manifest(self, manifest): - """Test different manifests that all should pass validation.""" - p = RSRC_PATH.joinpath(manifest) - assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) +@pytest.mark.repo +@pytest.mark.parametrize("manifest", LOCAL_MANIFESTS, ids=LOCAL_MANIFEST_NAMES) +def test_manifests_local(manifest): + """Test local (repo) manifests.""" + assert isinstance(validate_manifest(manifest), (WIPPPluginManifest, ComputeSchema)) + + +def test_list_plugins(): + """Test `list_plugins()`.""" + o = list(PLUGINS.keys()) + o.sort() + assert o == list_plugins() + + +@pytest.mark.parametrize("manifest", list_plugins(), ids=list_plugins()) +def test_manifests_plugindir(manifest): + """Test manifests available in polus-plugins installation dir.""" + p = _get_path(manifest) + assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) + + +@pytest.mark.parametrize("type_", test_dict_load.values(), ids=test_dict_load.keys()) +def test_load_manifest(type_): # test path and dict + """Test _load_manifest() for types path and dict.""" + assert _load_manifest(type_) == d_val + + +def test_load_manifest_str(): + """Test _load_manifest() for str.""" + st_ = """{"a": 2, "b": "Polus"}""" + assert _load_manifest(st_) == {"a": 2, "b": "Polus"} + + +bad = [f"b{x}.json" for x in [1, 2, 3]] +good = [f"g{x}.json" for x in [1, 2, 3]] + + +@pytest.mark.parametrize("manifest", bad, ids=bad) +def test_bad_manifest(manifest): + """Test bad manifests raise InvalidManifest error.""" + with pytest.raises(InvalidManifest): + validate_manifest(REPO_PATH.joinpath("tests", "resources", manifest)) + + +@pytest.mark.parametrize("manifest", good, ids=good) +def test_good_manifest(manifest): + """Test different manifests that all should pass validation.""" + p = RSRC_PATH.joinpath(manifest) + assert isinstance(validate_manifest(p), (WIPPPluginManifest, ComputeSchema)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c5dc7a6c0..1b05c05ff 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -57,19 +57,21 @@ def test_url2(): def test_load_plugin(): """Test load_plugin.""" - assert isinstance(load_plugin(OMECONVERTER), Plugin) + assert load_plugin(OMECONVERTER).name == "OME Converter" def test_load_plugin2(): """Test load_plugin.""" - assert isinstance(load_plugin(BASIC_131), Plugin) + assert load_plugin(BASIC_131).name == "BaSiC Flatfield Correction Plugin" def test_attr1(): """Test attributes.""" p_attr = pp.OmeConverter p_get = pp.get_plugin("OmeConverter") - for attr in ["name", "version", "inputs", "outputs"]: + for attr in p_get.__dict__: + if attr == "id": + continue assert getattr(p_attr, attr) == getattr(p_get, attr) @@ -77,7 +79,9 @@ def test_attr2(): """Test attributes.""" p_attr = pp.BasicFlatfieldCorrectionPlugin p_get = pp.get_plugin("BasicFlatfieldCorrectionPlugin") - for attr in ["name", "version", "inputs", "outputs"]: + for attr in p_get.__dict__: + if attr == "id": + continue assert getattr(p_attr, attr) == getattr(p_get, attr) From 5bd377f9e66c93faa56b764c3724f13225fe0013 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 26 Oct 2023 12:24:35 -0400 Subject: [PATCH 38/47] fix: fixed pyproject and all tests passing --- pyproject.toml | 27 +++--- .../_plugins/classes/plugin_classes.py | 3 +- tests/test_cwl.py | 30 +++--- tests/test_io.py | 4 +- tests/test_plugins.py | 95 +++++++++---------- 5 files changed, 74 insertions(+), 85 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 098d1fa98..aef450ad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,44 +10,41 @@ repository = "https://github.com/PolusAI/polus-plugins" version = "0.1.1" [tool.poetry.dependencies] + click = "^8.1.3" cwltool = "^3.1.20230513155734" fsspec = "^2023.6.0" pydantic = ">=1.10.9, <2.0" pygithub = "^1.58.2" -fsspec = "^2023.1.0" -pydantic = "^1.10.4" python = ">=3.9, <3.12" python-on-whales = "^0.57.0" +pyyaml = "^6.0" tqdm = "^4.65.0" xmltodict = "^0.13.0" -cwltool = "^3.1.20230513155734" -pyyaml = "^6.0" [tool.poetry.group.dev.dependencies] -bump2version = "^1.0.1" -pre-commit = "^3.3.3" black = "^23.3.0" -ruff = "^0.0.274" -mypy = "^1.4.0" -pytest = "^7.3.2" -pytest-xdist = "^3.3.1" -pytest-sugar = "^0.9.7" -pytest-cov = "^4.1.0" -pytest-benchmark = "^4.0.0" +bump2version = "^1.0.1" datamodel-code-generator = "^0.17.1" flake8 = "^6.0.0" fsspec = "^2023.1.0" +mypy = "^1.4.0" nox = "^2022.11.21" poetry = "^1.3.2" -pre-commit = "^3.0.4" +pre-commit = "^3.3.3" pydantic = "^1.10.4" pydantic-to-typescript = "^1.0.10" +pytest = "^7.3.2" +pytest-benchmark = "^4.0.0" +pytest-cov = "^4.1.0" +pytest-sugar = "^0.9.7" +pytest-xdist = "^3.3.1" python = ">=3.9, <3.12" python-on-whales = "^0.57.0" +pyyaml = "^6.0" +ruff = "^0.0.274" tqdm = "^4.64.1" xmltodict = "^0.13.0" -pyyaml = "^6.0" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 8b1f85fa3..fc53f17d7 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -415,7 +415,6 @@ def remove_plugin(plugin: str, version: Optional[Union[str, list[str]]] = None) if version is None: for plugin_version in PLUGINS[plugin]: remove_plugin(plugin, plugin_version) - return else: if isinstance(version, list): for version_ in version: @@ -427,7 +426,7 @@ def remove_plugin(plugin: str, version: Optional[Union[str, list[str]]] = None) version_ = version path = PLUGINS[plugin][version_] path.unlink() - refresh() + refresh() def remove_all() -> None: diff --git a/tests/test_cwl.py b/tests/test_cwl.py index de713f45f..94fce2a4f 100644 --- a/tests/test_cwl.py +++ b/tests/test_cwl.py @@ -42,22 +42,8 @@ def cwl_path(tmp_path_factory): return tmp_path_factory.mktemp("cwl") / "omeconverter.cwl" -def test_save_cwl(plug, cwl_path): - """Test save_cwl.""" - plug.save_cwl(cwl_path) - assert cwl_path.exists() - - -def test_read_saved_cwl(cwl_path): - """Test saved cwl.""" - with open(cwl_path, encoding="utf-8") as file: - src_cwl = file.read() - with open(RSRC_PATH.joinpath("target1.cwl"), encoding="utf-8") as file: - target_cwl = file.read() - assert src_cwl == target_cwl - - -def test_save_cwl_io(plug, cwl_io_path): +@pytest.fixture +def cwl_io(plug, cwl_io_path): """Test save_cwl IO.""" rs_path = RSRC_PATH.absolute() plug.inpDir = rs_path @@ -67,6 +53,16 @@ def test_save_cwl_io(plug, cwl_io_path): plug.save_cwl_io(cwl_io_path) +def test_save_read_cwl(plug, cwl_path): + """Test save and read cwl.""" + plug.save_cwl(cwl_path) + with open(cwl_path, encoding="utf-8") as file: + src_cwl = file.read() + with open(RSRC_PATH.joinpath("target1.cwl"), encoding="utf-8") as file: + target_cwl = file.read() + assert src_cwl == target_cwl + + def test_save_cwl_io_not_inp(plug, cwl_io_path): """Test save_cwl IO.""" with pytest.raises(MissingInputValues): @@ -91,7 +87,7 @@ def test_save_cwl_io_not_yml(plug, cwl_io_path): plug.save_cwl_io(cwl_io_path.with_suffix(".txt")) -def test_read_cwl_io(cwl_io_path): +def test_read_cwl_io(cwl_io, cwl_io_path): """Test read_cwl_io.""" with open(cwl_io_path, encoding="utf-8") as file: src_io = yaml.safe_load(file) diff --git a/tests/test_io.py b/tests/test_io.py index 885c15320..f1135e504 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -5,7 +5,7 @@ import pytest from fsspec.implementations.local import LocalFileSystem -from polus.plugins._plugins.classes import load_plugin +from polus.plugins._plugins.classes.plugin_classes import _load_plugin from polus.plugins._plugins.classes.plugin_methods import IOKeyError from polus.plugins._plugins.io import Input, IOBase @@ -21,7 +21,7 @@ iob1 = { "type": "collection", } -plugin = load_plugin(RSRC_PATH.joinpath("g1.json")) +plugin = _load_plugin(RSRC_PATH.joinpath("g1.json")) def test_iobase(): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1b05c05ff..51bdcdb1e 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -6,13 +6,14 @@ import pytest import polus.plugins as pp -from polus.plugins._plugins.classes.plugin_classes import Plugin, load_plugin +from polus.plugins._plugins.classes.plugin_classes import Plugin, _load_plugin RSRC_PATH = Path(__file__).parent.joinpath("resources") OMECONVERTER = RSRC_PATH.joinpath("omeconverter022.json") BASIC_131 = ( - "https://raw.githubusercontent.com/PolusAI/polus-plugins/master" - "/regression/polus-basic-flatfield-correction-plugin/plugin.json" + "https://raw.githubusercontent.com/PolusAI/polus-plugins/" + "e8f23a3661e3e5f7ad7dc92f4b0d9c31e7076589/regression/" + "polus-basic-flatfield-correction-plugin/plugin.json" ) BASIC_127 = ( "https://raw.githubusercontent.com/PolusAI/polus-plugins/" @@ -32,40 +33,53 @@ def test_empty_list(remove_all): assert pp.list == [] -def test_submit_plugin(): +def test_submit_plugin(remove_all): """Test submit_plugin.""" pp.submit_plugin(OMECONVERTER) assert pp.list == ["OmeConverter"] -def test_get_plugin(): +@pytest.fixture +def submit_omeconverter(): + pp.submit_plugin(OMECONVERTER) + + +@pytest.fixture +def submit_basic131(): + pp.submit_plugin(BASIC_131) + + +@pytest.fixture +def submit_basic127(): + pp.submit_plugin(BASIC_127) + + +def test_get_plugin(submit_omeconverter): """Test get_plugin.""" assert isinstance(pp.get_plugin("OmeConverter"), Plugin) -def test_url1(): +def test_url1(submit_omeconverter, submit_basic131): """Test url submit.""" - pp.submit_plugin(BASIC_131) assert sorted(pp.list) == ["BasicFlatfieldCorrectionPlugin", "OmeConverter"] -def test_url2(): +def test_url2(submit_omeconverter, submit_basic131, submit_basic127): """Test url submit.""" - pp.submit_plugin(BASIC_127) assert sorted(pp.list) == ["BasicFlatfieldCorrectionPlugin", "OmeConverter"] -def test_load_plugin(): +def test_load_plugin(submit_omeconverter): """Test load_plugin.""" - assert load_plugin(OMECONVERTER).name == "OME Converter" + assert _load_plugin(OMECONVERTER).name == "OME Converter" -def test_load_plugin2(): +def test_load_plugin2(submit_basic131): """Test load_plugin.""" - assert load_plugin(BASIC_131).name == "BaSiC Flatfield Correction Plugin" + assert _load_plugin(BASIC_131).name == "BaSiC Flatfield Correction Plugin" -def test_attr1(): +def test_attr1(submit_omeconverter): """Test attributes.""" p_attr = pp.OmeConverter p_get = pp.get_plugin("OmeConverter") @@ -75,7 +89,7 @@ def test_attr1(): assert getattr(p_attr, attr) == getattr(p_get, attr) -def test_attr2(): +def test_attr2(submit_basic131): """Test attributes.""" p_attr = pp.BasicFlatfieldCorrectionPlugin p_get = pp.get_plugin("BasicFlatfieldCorrectionPlugin") @@ -85,7 +99,7 @@ def test_attr2(): assert getattr(p_attr, attr) == getattr(p_get, attr) -def test_versions(): +def test_versions(submit_basic131, submit_basic127): """Test versions.""" assert sorted( [x.version for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions] @@ -95,56 +109,42 @@ def test_versions(): ] -def test_get_max_version1(): +def test_get_max_version1(submit_basic131, submit_basic127): """Test get max version.""" plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin") assert plug.version.version == "1.3.1" -def test_get_max_version2(): +def test_get_max_version2(submit_basic131, submit_basic127): """Test get max version.""" plug = pp.BasicFlatfieldCorrectionPlugin assert plug.version.version == "1.3.1" -def test_get_specific_version(): +def test_get_specific_version(submit_basic131, submit_basic127): """Test get specific version.""" plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") assert plug.version.version == "1.2.7" -def test_remove_version(): +def test_remove_version(submit_basic131, submit_basic127): """Test remove version.""" pp.remove_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") assert [x.version for x in pp.BasicFlatfieldCorrectionPlugin.versions] == ["1.3.1"] -def test_resubmit_plugin(): - """Test resubmit plugin.""" - pp.submit_plugin(BASIC_127) - - -def test_remove_all_versions_plugin(): +def test_remove_all_versions_plugin( + submit_basic131, submit_basic127, submit_omeconverter +): """Test remove all versions plugin.""" pp.remove_plugin("BasicFlatfieldCorrectionPlugin") assert pp.list == ["OmeConverter"] -def test_resubmit_plugin2(): - """Test resubmit plugin.""" - pp.submit_plugin(BASIC_131) - - -@pytest.fixture(scope="session") -def plug0(): - """Fixture to submit plugin.""" - if "BasicFlatfieldCorrectionPlugin" not in pp.list: - pp.submit_plugin(BASIC_131) - - -@pytest.fixture(scope="session") -def plug1(plug0): +@pytest.fixture +def plug1(): """Configure the class.""" + pp.submit_plugin(BASIC_131) plug1 = pp.BasicFlatfieldCorrectionPlugin plug1.inpDir = RSRC_PATH.absolute() plug1.outDir = RSRC_PATH.absolute() @@ -160,14 +160,9 @@ def config_path(tmp_path_factory): return tmp_path_factory.mktemp("config") / "config1.json" -def test_save_config(plug1, config_path): - """Test save_config file.""" +def test_save_load_config(plug1, config_path): + """Test save_config, load_config from config file.""" plug1.save_config(config_path) - assert Path(config_path).exists() - - -def test_load_config(plug1, config_path): - """Test load_config from config file.""" plug2 = pp.load_config(config_path) for i_o in ["inpDir", "outDir", "filePattern"]: assert getattr(plug2, i_o) == getattr(plug1, i_o) @@ -176,14 +171,16 @@ def test_load_config(plug1, config_path): def test_load_config_no_plugin(plug1, config_path): """Test load_config after removing plugin.""" + plug1.save_config(config_path) + plug1_id = plug1.id pp.remove_plugin("BasicFlatfieldCorrectionPlugin") assert pp.list == ["OmeConverter"] plug2 = pp.load_config(config_path) assert isinstance(plug2, Plugin) - assert plug2.id == plug1.id + assert plug2.id == plug1_id -def test_remove_all(): +def test_remove_all(submit_basic131, submit_basic127, submit_omeconverter): """Test remove_all.""" pp.remove_all() assert pp.list == [] From 1d3285a2634d62ad6ada597bf296c7d0c3aa76cc Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 27 Jul 2023 02:00:14 -0400 Subject: [PATCH 39/47] feat: improved version comparison --- .../_plugins/classes/plugin_classes.py | 49 +++ src/polus/plugins/_plugins/io.py | 320 ++++++++++-------- tests/test_plugins.py | 10 +- 3 files changed, 235 insertions(+), 144 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index fc53f17d7..1281abb51 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -1,5 +1,6 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" # pylint: disable=W1203, enable=W1201 +# ruff: noqa: BLE001, A003, PTH123 import json import logging import shutil @@ -438,3 +439,51 @@ def remove_all() -> None: for org in organizations: shutil.rmtree(org) refresh() + + +def load_config(config: Union[dict, pathlib.Path]) -> Union[ComputePlugin, Plugin]: + """Load configured plugin from config file/dict.""" + if isinstance(config, pathlib.Path): + with open(config, encoding="utf-8") as file: + manifest_ = json.load(file) + elif isinstance(config, dict): + manifest_ = config + else: + msg = "config must be a dict or a path" + raise TypeError(msg) + _io = manifest_["_io_keys"] + cl_ = manifest_["class"] + manifest_.pop("class", None) + if cl_ == "Compute": + pl_ = ComputePlugin(_uuid=False, **manifest_) + elif cl_ == "WIPP": + pl_ = Plugin(_uuid=False, **manifest_) + else: + msg = "Invalid value of class" + raise ValueError(msg) + for k, value_ in _io.items(): + val = value_["value"] + if val is not None: # exclude those values not set + setattr(pl_, k, val) + return pl_ + + +def get_plugin( + name: str, + version: Optional[str] = None, +) -> Union[ComputePlugin, Plugin]: + """Get a plugin with option to specify version. + + Return a plugin object with the option to specify a version. + The specified version's manifest must exist in manifests folder. + + Args: + name: Name of the plugin. + version: Optional version of the plugin, must follow semver. + + Returns: + Plugin object + """ + if version is None: + return load_plugin(PLUGINS[name][max(PLUGINS[name])]) + return load_plugin(PLUGINS[name][Version(**{"version": version})]) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 41016bfb0..1f55a0318 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -1,14 +1,23 @@ # type: ignore +# ruff: noqa: S101, A003 +# pylint: disable=no-self-argument """Plugins I/O utilities.""" import enum import logging import pathlib import re -import typing from functools import singledispatch +from functools import singledispatchmethod +from typing import Any +from typing import Optional +from typing import Union import fsspec -from pydantic import BaseModel, Field, PrivateAttr, constr, validator +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr +from pydantic import constr +from pydantic import validator logger = logging.getLogger("polus.plugins") @@ -38,45 +47,35 @@ class InputTypes(str, enum.Enum): # wipp schema """Enum of Input Types for WIPP schema.""" - collection = "collection" - pyramid = "pyramid" - csvCollection = "csvCollection" - genericData = "genericData" - stitchingVector = "stitchingVector" - notebook = "notebook" - tensorflowModel = "tensorflowModel" - tensorboardLogs = "tensorboardLogs" - pyramidAnnotation = "pyramidAnnotation" - integer = "integer" - number = "number" - string = "string" - boolean = "boolean" - array = "array" - enum = "enum" - - @classmethod - def list(cls): - """List Input Types.""" - return list(map(lambda c: c.value, cls)) + COLLECTION = "collection" + PYRAMID = "pyramid" + CSVCOLLECTION = "csvCollection" + GENERICDATA = "genericData" + STITCHINGVECTOR = "stitchingVector" + NOTEBOOK = "notebook" + TENSORFLOWMODEL = "tensorflowModel" + TENSORBOARDLOGS = "tensorboardLogs" + PYRAMIDANNOTATION = "pyramidAnnotation" + INTEGER = "integer" + NUMBER = "number" + STRING = "string" + BOOLEAN = "boolean" + ARRAY = "array" + ENUM = "enum" class OutputTypes(str, enum.Enum): # wipp schema """Enum for Output Types for WIPP schema.""" - collection = "collection" - pyramid = "pyramid" - csvCollection = "csvCollection" - genericData = "genericData" - stitchingVector = "stitchingVector" - notebook = "notebook" - tensorflowModel = "tensorflowModel" - tensorboardLogs = "tensorboardLogs" - pyramidAnnotation = "pyramidAnnotation" - - @classmethod - def list(cls): - """List Output Types.""" - return list(map(lambda c: c.value, cls)) + COLLECTION = "collection" + PYRAMID = "pyramid" + CSVCOLLECTION = "csvCollection" + GENERICDATA = "genericData" + STITCHINGVECTOR = "stitchingVector" + NOTEBOOK = "notebook" + TENSORFLOWMODEL = "tensorflowModel" + TENSORBOARDLOGS = "tensorboardLogs" + PYRAMIDANNOTATION = "pyramidAnnotation" def _in_old_to_new(old: str) -> str: # map wipp InputType to compute schema's InputType @@ -84,10 +83,9 @@ def _in_old_to_new(old: str) -> str: # map wipp InputType to compute schema's I d = {"integer": "number", "enum": "string"} if old in ["string", "array", "number", "boolean"]: return old - elif old in d: + if old in d: return d[old] # integer or enum - else: - return "path" # everything else + return "path" # everything else def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's UIType @@ -101,34 +99,35 @@ def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's U } if old in type_dict: return type_dict[old] - else: - return "text" + return "text" -class IOBase(BaseModel): +class IOBase(BaseModel): # pylint: disable=R0903 """Base Class for I/O arguments.""" - type: typing.Any - options: typing.Optional[dict] = None - value: typing.Optional[typing.Any] = None - id: typing.Optional[typing.Any] = None - _fs: typing.Optional[typing.Type[fsspec.spec.AbstractFileSystem]] = PrivateAttr( - default=None + type: Any + options: Optional[dict] = None + value: Optional[Any] = None + id_: Optional[Any] = None + _fs: Optional[type[fsspec.spec.AbstractFileSystem]] = PrivateAttr( + default=None, ) # type checking is done at plugin level - def _validate(self): + def _validate(self) -> None: # pylint: disable=R0912 value = self.value if value is None: if self.required: + msg = f""" + The input value ({self.name}) is required, + but the value was not set.""" raise TypeError( - f"The input value ({self.name}) is required, but the value was not set." + msg, ) - else: - return + return - if self.type == InputTypes.enum: + if self.type == InputTypes.ENUM: try: if isinstance(value, str): value = enum.Enum(self.name, self.options["values"])[value] @@ -137,7 +136,11 @@ def _validate(self): except KeyError: logging.error( - f"Value ({value}) is not a valid value for the enum input ({self.name}). Must be one of {self.options['values']}." + f""" + Value ({value}) is not a valid value + for the enum input ({self.name}). + Must be one of {self.options['values']}. + """, ) raise else: @@ -145,17 +148,17 @@ def _validate(self): value = WIPP_TYPES[self.type](value) else: value = WIPP_TYPES[self.type.value]( - value + value, ) # compute, type does not inherit from str if isinstance(value, pathlib.Path): value = value.absolute() if self._fs: assert self._fs.exists( - str(value) + str(value), ), f"{value} is invalid or does not exist" assert self._fs.isdir( - str(value) + str(value), ), f"{value} is not a valid directory" else: assert value.exists(), f"{value} is invalid or does not exist" @@ -163,11 +166,12 @@ def _validate(self): super().__setattr__("value", value) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: # ruff: noqa: ANN401 """Set I/O attributes.""" if name not in ["value", "id", "_fs"]: # Don't permit any other values to be changed - raise TypeError(f"Cannot set property: {name}") + msg = f"Cannot set property: {name}" + raise TypeError(msg) super().__setattr__(name, value) @@ -175,61 +179,68 @@ def __setattr__(self, name, value): self._validate() -""" Plugin and Input/Output Classes """ - - -class Output(IOBase): +class Output(IOBase): # pylint: disable=R0903 """Required until JSON schema is fixed.""" - name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( # noqa: F722 - ..., examples=["outputCollection"], title="Output name" + name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( + ..., + examples=["outputCollection"], + title="Output name", ) type: OutputTypes = Field( - ..., examples=["stitchingVector", "collection"], title="Output type" + ..., + examples=["stitchingVector", "collection"], + title="Output type", ) - description: constr(regex=r"^(.*)$") = Field( # noqa: F722 - ..., examples=["Output collection"], title="Output description" + description: constr(regex=r"^(.*)$") = Field( + ..., + examples=["Output collection"], + title="Output description", ) -class Input(IOBase): +class Input(IOBase): # pylint: disable=R0903 """Required until JSON schema is fixed.""" - name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( # noqa: F722 + name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( ..., description="Input name as expected by the plugin CLI", examples=["inputImages", "fileNamePattern", "thresholdValue"], title="Input name", ) type: InputTypes - description: constr(regex=r"^(.*)$") = Field( # noqa: F722 - ..., examples=["Input Images"], title="Input description" + description: constr(regex=r"^(.*)$") = Field( + ..., + examples=["Input Images"], + title="Input description", ) - required: typing.Optional[bool] = Field( + required: Optional[bool] = Field( True, description="Whether an input is required or not", examples=[True], title="Required input", ) - def __init__(self, **data): + def __init__(self, **data) -> None: # ruff: noqa: ANN003 """Initialize input.""" super().__init__(**data) if self.description is None: logger.warning( - f"The input ({self.name}) is missing the description field. This field is not required but should be filled in." + f""" + The input ({self.name}) is missing the description field. + This field is not required but should be filled in. + """, ) -def _check_version_number(value: typing.Union[str, int]) -> bool: +def _check_version_number(value: Union[str, int]) -> bool: if isinstance(value, int): value = str(value) if "-" in value: value = value.split("-")[0] - if len(value) > 1: - if value[0] == "0": - return False + if len(value) > 1 and value[0] == "0": + return False return bool(re.match(r"^\d+$", value)) @@ -238,28 +249,35 @@ class Version(BaseModel): version: str - def __init__(self, version): + def __init__(self, version: str) -> None: """Initialize Version object.""" super().__init__(version=version) @validator("version") - def semantic_version(cls, value): + def semantic_version( + cls, + value, + ): # ruff: noqa: ANN202, N805, ANN001 """Pydantic Validator to check semver.""" version = value.split(".") assert ( - len(version) == 3 - ), f"Invalid version ({value}). Version must follow semantic versioning (see semver.org)" + len(version) == 3 # ruff: noqa: PLR2004 + ), f""" + Invalid version ({value}). Version must follow + semantic versioning (see semver.org)""" if "-" in version[-1]: # with hyphen idn = version[-1].split("-")[-1] id_reg = re.compile("[0-9A-Za-z-]+") assert bool( - id_reg.match(idn) - ), f"Invalid version ({value}). Version must follow semantic versioning (see semver.org)" + id_reg.match(idn), + ), f"""Invalid version ({value}). + Version must follow semantic versioning (see semver.org)""" assert all( - map(_check_version_number, version) - ), f"Invalid version ({value}). Version must follow semantic versioning (see semver.org)" + map(_check_version_number, version), + ), f"""Invalid version ({value}). + Version must follow semantic versioning (see semver.org)""" return value @property @@ -277,47 +295,82 @@ def patch(self): """Return z from x.y.z .""" return self.version.split(".")[2] - def __lt__(self, other): - """Compare if Version is less than other Version object.""" - assert isinstance(other, Version), "Can only compare version objects." + def __str__(self) -> str: + """Return string representation of Version object.""" + return self.version - if other.major > self.major: - return True - elif other.major == self.major: - if other.minor > self.minor: - return True - elif other.minor == self.minor: - if other.patch > self.patch: - return True - else: - return False - else: - return False - else: - return False + @singledispatchmethod + def __lt__(self, other: Any) -> bool: + """Compare if Version is less than other object.""" + msg = "invalid type for comparison." + raise TypeError(msg) - def __gt__(self, other): - """Compare if Version is greater than other Version object.""" - return other < self + @singledispatchmethod + def __gt__(self, other: Any) -> bool: + """Compare if Version is less than other object.""" + msg = "invalid type for comparison." + raise TypeError(msg) - def __eq__(self, other): + @singledispatchmethod + def __eq__(self, other: Any) -> bool: """Compare if two Version objects are equal.""" - return ( - other.major == self.major - and other.minor == self.minor - and other.patch == self.patch - ) + msg = "invalid type for comparison." + raise TypeError(msg) - def __hash__(self): + def __hash__(self) -> int: """Needed to use Version objects as dict keys.""" return hash(self.version) -class DuplicateVersionFound(Exception): - """Raise when two equal versions found.""" +@Version.__eq__.register(Version) # pylint: disable=no-member +def _(self, other): + return ( + other.major == self.major + and other.minor == self.minor + and other.patch == self.patch + ) + + +@Version.__eq__.register(str) # pylint: disable=no-member +def _(self, other): + return self == Version(**{"version": other}) + + +@Version.__lt__.register(Version) # pylint: disable=no-member +def _(self, other): + if other.major > self.major: + return True + if other.major == self.major: + if other.minor > self.minor: + return True + if other.minor == self.minor: + if other.patch > self.patch: + return True + return False + return False + return False -"""CWL""" +@Version.__lt__.register(str) # pylint: disable=no-member +def _(self, other): + v = Version(**{"version": other}) + return self < v + + +@Version.__gt__.register(Version) # pylint: disable=no-member +def _(self, other): + return other < self + + +@Version.__gt__.register(str) # pylint: disable=no-member +def _(self, other): + v = Version(**{"version": other}) + return self > v + + +class DuplicateVersionFoundError(Exception): + """Raise when two equal versions found.""" + CWL_INPUT_TYPES = { "path": "Directory", # always Dir? Yes @@ -340,68 +393,57 @@ def _type_in(inp: Input): # NOT compatible with CWL workflows, ok in CLT # if val == "enum": # if input.required: - # s = [{"type": "enum", "symbols": input.options["values"]}] - # else: - # s = ["null", {"type": "enum", "symbols": input.options["values"]}] - if val in CWL_INPUT_TYPES: - s = CWL_INPUT_TYPES[val] + req - else: - s = "string" + req # defaults to string - return s + # if val in CWL_INPUT_TYPES: + return CWL_INPUT_TYPES[val] + req if val in CWL_INPUT_TYPES else "string" + req def input_to_cwl(inp: Input): """Return dict of inputs for cwl.""" - r = { + return { f"{inp.name}": { "type": _type_in(inp), "inputBinding": {"prefix": f"--{inp.name}"}, - } + }, } - return r def output_to_cwl(out: Output): """Return dict of output args for cwl for input section.""" - r = { + return { f"{out.name}": { "type": "Directory", "inputBinding": {"prefix": f"--{out.name}"}, - } + }, } - return r def outputs_cwl(out: Output): """Return dict of output for `outputs` in cwl.""" - r = { + return { f"{out.name}": { "type": "Directory", "outputBinding": {"glob": f"$(inputs.{out.name}.basename)"}, - } + }, } - return r # -- I/O as arguments in .yml @singledispatch -def _io_value_to_yml(io) -> typing.Union[str, dict]: +def _io_value_to_yml(io) -> Union[str, dict]: return str(io) @_io_value_to_yml.register def _(io: pathlib.Path): - r = {"class": "Directory", "location": str(io)} - return r + return {"class": "Directory", "location": str(io)} @_io_value_to_yml.register def _(io: enum.Enum): - r = io.name - return r + return io.name def io_to_yml(io): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 51bdcdb1e..302759940 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -102,7 +102,7 @@ def test_attr2(submit_basic131): def test_versions(submit_basic131, submit_basic127): """Test versions.""" assert sorted( - [x.version for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions] + [x for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions] ) == [ "1.2.7", "1.3.1", @@ -112,25 +112,25 @@ def test_versions(submit_basic131, submit_basic127): def test_get_max_version1(submit_basic131, submit_basic127): """Test get max version.""" plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin") - assert plug.version.version == "1.3.1" + assert plug.version == "1.3.1" def test_get_max_version2(submit_basic131, submit_basic127): """Test get max version.""" plug = pp.BasicFlatfieldCorrectionPlugin - assert plug.version.version == "1.3.1" + assert plug.version == "1.3.1" def test_get_specific_version(submit_basic131, submit_basic127): """Test get specific version.""" plug = pp.get_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") - assert plug.version.version == "1.2.7" + assert plug.version == "1.2.7" def test_remove_version(submit_basic131, submit_basic127): """Test remove version.""" pp.remove_plugin("BasicFlatfieldCorrectionPlugin", "1.2.7") - assert [x.version for x in pp.BasicFlatfieldCorrectionPlugin.versions] == ["1.3.1"] + assert pp.BasicFlatfieldCorrectionPlugin.versions == ["1.3.1"] def test_remove_all_versions_plugin( From de915a2d9c83200faf1bb094014da6e9a46bebe9 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 27 Jul 2023 02:09:50 -0400 Subject: [PATCH 40/47] fix: fixed wrong imports --- src/polus/plugins/_plugins/classes/plugin_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index 1281abb51..ca1715194 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -12,7 +12,7 @@ from typing import Union from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import DuplicateVersionFound +from polus.plugins._plugins.io import DuplicateVersionFoundError from polus.plugins._plugins.io import Version from polus.plugins._plugins.io import _in_old_to_new from polus.plugins._plugins.io import _ui_old_to_new From 344696732f29583316c7aef43aae7dfd7bc8b6e2 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 26 Oct 2023 16:12:53 -0400 Subject: [PATCH 41/47] fix: restored plugin_classes from test2 --- .../_plugins/classes/plugin_classes.py | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index ca1715194..fc53f17d7 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -1,6 +1,5 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" # pylint: disable=W1203, enable=W1201 -# ruff: noqa: BLE001, A003, PTH123 import json import logging import shutil @@ -12,7 +11,7 @@ from typing import Union from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import DuplicateVersionFoundError +from polus.plugins._plugins.io import DuplicateVersionFound from polus.plugins._plugins.io import Version from polus.plugins._plugins.io import _in_old_to_new from polus.plugins._plugins.io import _ui_old_to_new @@ -439,51 +438,3 @@ def remove_all() -> None: for org in organizations: shutil.rmtree(org) refresh() - - -def load_config(config: Union[dict, pathlib.Path]) -> Union[ComputePlugin, Plugin]: - """Load configured plugin from config file/dict.""" - if isinstance(config, pathlib.Path): - with open(config, encoding="utf-8") as file: - manifest_ = json.load(file) - elif isinstance(config, dict): - manifest_ = config - else: - msg = "config must be a dict or a path" - raise TypeError(msg) - _io = manifest_["_io_keys"] - cl_ = manifest_["class"] - manifest_.pop("class", None) - if cl_ == "Compute": - pl_ = ComputePlugin(_uuid=False, **manifest_) - elif cl_ == "WIPP": - pl_ = Plugin(_uuid=False, **manifest_) - else: - msg = "Invalid value of class" - raise ValueError(msg) - for k, value_ in _io.items(): - val = value_["value"] - if val is not None: # exclude those values not set - setattr(pl_, k, val) - return pl_ - - -def get_plugin( - name: str, - version: Optional[str] = None, -) -> Union[ComputePlugin, Plugin]: - """Get a plugin with option to specify version. - - Return a plugin object with the option to specify a version. - The specified version's manifest must exist in manifests folder. - - Args: - name: Name of the plugin. - version: Optional version of the plugin, must follow semver. - - Returns: - Plugin object - """ - if version is None: - return load_plugin(PLUGINS[name][max(PLUGINS[name])]) - return load_plugin(PLUGINS[name][Version(**{"version": version})]) From 7a55c534352218aeb6f70df37b8dd30b6a05ae87 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 26 Oct 2023 16:14:14 -0400 Subject: [PATCH 42/47] fix: fix DuplicateVersionFound in plugin_classes --- src/polus/plugins/_plugins/classes/plugin_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes.py index fc53f17d7..9c4d54c05 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes.py @@ -11,7 +11,7 @@ from typing import Union from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import DuplicateVersionFound +from polus.plugins._plugins.io import DuplicateVersionFoundError from polus.plugins._plugins.io import Version from polus.plugins._plugins.io import _in_old_to_new from polus.plugins._plugins.io import _ui_old_to_new @@ -72,7 +72,7 @@ def refresh() -> None: "Found duplicate version of plugin" f"{plugin.name} in {_PLUGIN_DIR}" ) - raise DuplicateVersionFound( + raise DuplicateVersionFoundError( msg, ) PLUGINS[key][plugin.version] = file From 506a94b168d7160943cabb507e26a7a69bc77481 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Thu, 26 Oct 2023 17:25:43 -0400 Subject: [PATCH 43/47] feat: improved version comparison --- src/polus/plugins/_plugins/io.py | 12 +++++--- tests/test_version.py | 53 ++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io.py index 1f55a0318..fe6f27c7a 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io.py @@ -283,17 +283,21 @@ def semantic_version( @property def major(self): """Return x from x.y.z .""" - return self.version.split(".")[0] + return int(self.version.split(".")[0]) @property def minor(self): """Return y from x.y.z .""" - return self.version.split(".")[1] + return int(self.version.split(".")[1]) @property def patch(self): """Return z from x.y.z .""" - return self.version.split(".")[2] + if not self.version.split(".")[2].isdigit(): + msg = "Patch version is not a digit, comparison may not be accurate." + logger.warning(msg) + return self.version.split(".")[2] + return int(self.version.split(".")[2]) def __str__(self) -> str: """Return string representation of Version object.""" @@ -380,7 +384,7 @@ class DuplicateVersionFoundError(Exception): "genericData": "Directory", "collection": "Directory", "enum": "string", # for compatibility with workflows - "stitchingVector": "Directory" + "stitchingVector": "Directory", # not yet implemented: array } diff --git a/tests/test_version.py b/tests/test_version.py index 5bad7cbae..74cfdf258 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,7 +5,7 @@ from polus.plugins._plugins.io import Version from polus.plugins._plugins.utils import cast_version -G = [ +GOOD_VERSIONS = [ "1.2.3", "1.4.7-rc1", "4.1.5", @@ -15,49 +15,49 @@ "0.3.4", "0.2.34-rc23", ] -B = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] +BAD_VERSIONS = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] -@pytest.mark.parametrize("ver", G, ids=G) +@pytest.mark.parametrize("ver", GOOD_VERSIONS, ids=GOOD_VERSIONS) def test_version(ver): """Test Version pydantic model.""" Version(version=ver) -@pytest.mark.parametrize("ver", G, ids=G) +@pytest.mark.parametrize("ver", GOOD_VERSIONS, ids=GOOD_VERSIONS) def test_cast_version(ver): """Test cast_version utility function.""" - cast_version(ver) + assert isinstance(cast_version(ver), Version) -@pytest.mark.parametrize("ver", B, ids=B) +@pytest.mark.parametrize("ver", BAD_VERSIONS, ids=BAD_VERSIONS) def test_bad_version1(ver): """Test ValidationError is raised for invalid versions.""" with pytest.raises(ValidationError): - cast_version(ver) + assert isinstance(cast_version(ver), Version) -mj = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] -mn = ["1.3.3", "7.3.4", "98.3.12", "23.3.0", "1.3.5"] -pt = ["12.2.7", "2.3.7", "1.7.7", "7.7.7", "7.29.7"] +MAJOR_VERSION_EQUAL = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] +MINOR_VERSION_EQUAL = ["1.3.3", "7.3.4", "98.3.12", "23.3.0", "1.3.5"] +PATCH_EQUAL = ["12.2.7", "2.3.7", "1.7.7", "7.7.7", "7.29.7"] -@pytest.mark.parametrize("ver", mj, ids=mj) +@pytest.mark.parametrize("ver", MAJOR_VERSION_EQUAL, ids=MAJOR_VERSION_EQUAL) def test_major(ver): """Test major version.""" - assert cast_version(ver).major == "2" + assert cast_version(ver).major == 2 -@pytest.mark.parametrize("ver", mn, ids=mn) +@pytest.mark.parametrize("ver", MINOR_VERSION_EQUAL, ids=MINOR_VERSION_EQUAL) def test_minor(ver): """Test minor version.""" - assert cast_version(ver).minor == "3" + assert cast_version(ver).minor == 3 -@pytest.mark.parametrize("ver", pt, ids=pt) +@pytest.mark.parametrize("ver", PATCH_EQUAL, ids=PATCH_EQUAL) def test_patch(ver): """Test patch version.""" - assert cast_version(ver).patch == "7" + assert cast_version(ver).patch == 7 def test_gt1(): @@ -93,3 +93,24 @@ def test_eq2(): def test_eq3(): """Test equality operator.""" assert Version(version="1.3.3") != cast_version("1.3.8") + + +def test_eq_str1(): + """Test equality with str.""" + assert Version(version="1.3.3") == "1.3.3" + + +def test_lt_str1(): + """Test equality with str.""" + assert Version(version="1.3.3") < "1.5.3" + + +def test_gt_str1(): + """Test equality with str.""" + assert Version(version="4.5.10") > "4.5.9" + + +def test_eq_no_str(): + """Test equality with non-string.""" + with pytest.raises(TypeError): + assert Version(version="1.3.3") == 1.3 From 56c31d9ff9e0d8a0d3e8887bb3d44959eeb99728 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Tue, 7 Nov 2023 14:13:23 -0500 Subject: [PATCH 44/47] fix: fixed dependencies order --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aef450ad7..843878315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,19 +10,20 @@ repository = "https://github.com/PolusAI/polus-plugins" version = "0.1.1" [tool.poetry.dependencies] +python = ">=3.9, <3.12" click = "^8.1.3" cwltool = "^3.1.20230513155734" fsspec = "^2023.6.0" pydantic = ">=1.10.9, <2.0" pygithub = "^1.58.2" -python = ">=3.9, <3.12" python-on-whales = "^0.57.0" pyyaml = "^6.0" tqdm = "^4.65.0" xmltodict = "^0.13.0" [tool.poetry.group.dev.dependencies] +python = ">=3.9, <3.12" black = "^23.3.0" bump2version = "^1.0.1" datamodel-code-generator = "^0.17.1" @@ -39,7 +40,6 @@ pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" pytest-sugar = "^0.9.7" pytest-xdist = "^3.3.1" -python = ">=3.9, <3.12" python-on-whales = "^0.57.0" pyyaml = "^6.0" ruff = "^0.0.274" From d0b1e59ab03a10b344f6059f7d1f4c4af2d658a9 Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Fri, 24 Nov 2023 11:57:21 -0500 Subject: [PATCH 45/47] feat: added pydantic v2 support --- .pre-commit-config.yaml | 23 +- pyproject.toml | 11 +- src/polus/plugins/__init__.py | 4 + .../plugins/_plugins/classes/__init__.py | 39 +- .../{plugin_methods.py => plugin_base.py} | 172 ++++--- ...plugin_classes.py => plugin_classes_v1.py} | 30 +- .../_plugins/classes/plugin_classes_v2.py | 429 ++++++++++++++++ src/polus/plugins/_plugins/io/__init__.py | 35 ++ .../plugins/_plugins/{io.py => io/io_v1.py} | 6 +- src/polus/plugins/_plugins/io/io_v2.py | 464 ++++++++++++++++++ .../plugins/_plugins/manifests/__init__.py | 26 + .../_plugins/manifests/manifest_utils.py | 202 -------- .../_plugins/manifests/manifest_utils_v1.py | 206 ++++++++ .../_plugins/manifests/manifest_utils_v2.py | 202 ++++++++ .../_plugins/models/PolusComputeSchema.py | 136 ----- src/polus/plugins/_plugins/models/__init__.py | 33 +- .../models/pydanticv1/PolusComputeSchema.py | 137 ++++++ .../{ => pydanticv1}/WIPPPluginSchema.py | 100 ++-- .../_plugins/models/pydanticv1/__init__.py | 0 .../models/{ => pydanticv1}/compute.py | 16 +- .../_plugins/models/pydanticv1/wipp.py | 79 +++ .../models/pydanticv2/PolusComputeSchema.py | 136 +++++ .../models/pydanticv2/WIPPPluginSchema.py | 241 +++++++++ .../_plugins/models/pydanticv2/__init__.py | 0 .../_plugins/models/pydanticv2/compute.py | 28 ++ .../_plugins/models/pydanticv2/wipp.py | 79 +++ src/polus/plugins/_plugins/models/wipp.py | 51 -- src/polus/plugins/_plugins/update/__init__.py | 14 + .../{update.py => update/update_v1.py} | 90 ++-- .../plugins/_plugins/update/update_v2.py | 115 +++++ 30 files changed, 2500 insertions(+), 604 deletions(-) rename src/polus/plugins/_plugins/classes/{plugin_methods.py => plugin_base.py} (62%) rename src/polus/plugins/_plugins/classes/{plugin_classes.py => plugin_classes_v1.py} (94%) create mode 100644 src/polus/plugins/_plugins/classes/plugin_classes_v2.py create mode 100644 src/polus/plugins/_plugins/io/__init__.py rename src/polus/plugins/_plugins/{io.py => io/io_v1.py} (98%) create mode 100644 src/polus/plugins/_plugins/io/io_v2.py create mode 100644 src/polus/plugins/_plugins/manifests/__init__.py delete mode 100644 src/polus/plugins/_plugins/manifests/manifest_utils.py create mode 100644 src/polus/plugins/_plugins/manifests/manifest_utils_v1.py create mode 100644 src/polus/plugins/_plugins/manifests/manifest_utils_v2.py delete mode 100644 src/polus/plugins/_plugins/models/PolusComputeSchema.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv1/PolusComputeSchema.py rename src/polus/plugins/_plugins/models/{ => pydanticv1}/WIPPPluginSchema.py (69%) create mode 100644 src/polus/plugins/_plugins/models/pydanticv1/__init__.py rename src/polus/plugins/_plugins/models/{ => pydanticv1}/compute.py (57%) create mode 100644 src/polus/plugins/_plugins/models/pydanticv1/wipp.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv2/PolusComputeSchema.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv2/WIPPPluginSchema.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv2/__init__.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv2/compute.py create mode 100644 src/polus/plugins/_plugins/models/pydanticv2/wipp.py delete mode 100644 src/polus/plugins/_plugins/models/wipp.py create mode 100644 src/polus/plugins/_plugins/update/__init__.py rename src/polus/plugins/_plugins/{update.py => update/update_v1.py} (57%) create mode 100644 src/polus/plugins/_plugins/update/update_v2.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc7aa6b99..fb3993a6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,6 @@ fail_fast: true repos: - - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: @@ -21,38 +20,44 @@ repos: - id: detect-private-key - id: end-of-file-fixer - id: mixed-line-ending - args: ['--fix=lf'] + args: ["--fix=lf"] description: Forces to replace line ending by the UNIX 'lf' character. - id: trailing-whitespace - exclude: '.bumpversion.cfg' + exclude: ".bumpversion.cfg" - id: check-merge-conflict - repo: https://github.com/psf/black - rev: '23.3.0' + rev: "23.3.0" hooks: - id: black language_version: python3.9 - exclude: ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ + exclude: | + (?x)( + ^src\/polus\/plugins\/_plugins\/models\/pydanticv1\/\w*Schema.py$| + ^src\/polus\/plugins\/_plugins\/models\/pydanticv2\/\w*Schema.py$ + ) - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.274' + rev: "v0.0.274" hooks: - id: ruff exclude: | (?x)( test_[a-zA-Z0-9]+.py$| - ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ + ^src\/polus\/plugins\/_plugins\/models\/pydanticv1\/\w*Schema.py$| + ^src\/polus\/plugins\/_plugins\/models\/pydanticv2\/\w*Schema.py$ ) args: [--fix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.4.0' + rev: "v1.4.0" hooks: - id: mypy exclude: | (?x)( test_[a-zA-Z0-9]+.py$| - ^src\/polus\/plugins\/_plugins\/models\/\w*Schema.py$ + ^src\/polus\/plugins\/_plugins\/models\/pydanticv1\/\w*Schema.py$| + ^src\/polus\/plugins\/_plugins\/models\/pydanticv2\/\w*Schema.py$ ) additional_dependencies: [types-requests==2.31.0.1] diff --git a/pyproject.toml b/pyproject.toml index 3ad3fa419..19152f7c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,12 @@ python = ">=3.9, <3.12" click = "^8.1.3" cwltool = "^3.1.20230513155734" fsspec = "^2023.6.0" -pydantic = ">=1.10.9, <2.0" +pydantic = ">=1.10.0" pygithub = "^1.58.2" -python-on-whales = "^0.57.0" +python-on-whales = "^0.66.0" pyyaml = "^6.0" tqdm = "^4.65.0" +validators = "^0.22.0" xmltodict = "^0.13.0" [tool.poetry.group.dev.dependencies] @@ -27,21 +28,19 @@ python = ">=3.9, <3.12" black = "^23.3.0" bump2version = "^1.0.1" -datamodel-code-generator = "^0.17.1" +datamodel-code-generator = "^0.23.0" flake8 = "^6.0.0" fsspec = "^2023.1.0" mypy = "^1.4.0" nox = "^2022.11.21" poetry = "^1.3.2" pre-commit = "^3.3.3" -pydantic = "^1.10.4" -pydantic-to-typescript = "^1.0.10" pytest = "^7.3.2" pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" pytest-sugar = "^0.9.7" pytest-xdist = "^3.3.1" -python-on-whales = "^0.57.0" +python = ">=3.9, <3.12" pyyaml = "^6.0" ruff = "^0.0.274" tqdm = "^4.64.1" diff --git a/src/polus/plugins/__init__.py b/src/polus/plugins/__init__.py index 0319cb364..a69b802fa 100644 --- a/src/polus/plugins/__init__.py +++ b/src/polus/plugins/__init__.py @@ -30,6 +30,8 @@ """ logger = logging.getLogger("polus.plugins") +VERSION = "0.1.1" + refresh() # calls the refresh method when library is imported @@ -39,6 +41,8 @@ def __getattr__(name: str) -> Union[Plugin, ComputePlugin, list]: return list_plugins() if name in list_plugins(): return get_plugin(name) + if name == "__version__": + return VERSION msg = f"module '{__name__}' has no attribute '{name}'" raise AttributeError(msg) diff --git a/src/polus/plugins/_plugins/classes/__init__.py b/src/polus/plugins/_plugins/classes/__init__.py index 91200c48e..200b2c821 100644 --- a/src/polus/plugins/_plugins/classes/__init__.py +++ b/src/polus/plugins/_plugins/classes/__init__.py @@ -1,13 +1,32 @@ """Plugin classes and functions.""" -from polus.plugins._plugins.classes.plugin_classes import ComputePlugin -from polus.plugins._plugins.classes.plugin_classes import Plugin -from polus.plugins._plugins.classes.plugin_classes import get_plugin -from polus.plugins._plugins.classes.plugin_classes import list_plugins -from polus.plugins._plugins.classes.plugin_classes import load_config -from polus.plugins._plugins.classes.plugin_classes import refresh -from polus.plugins._plugins.classes.plugin_classes import remove_all -from polus.plugins._plugins.classes.plugin_classes import remove_plugin -from polus.plugins._plugins.classes.plugin_classes import submit_plugin +import pydantic + +PYDANTIC_VERSION = pydantic.__version__ + +if PYDANTIC_VERSION.split(".")[0] == "1": + from polus.plugins._plugins.classes.plugin_classes_v1 import PLUGINS + from polus.plugins._plugins.classes.plugin_classes_v1 import ComputePlugin + from polus.plugins._plugins.classes.plugin_classes_v1 import Plugin + from polus.plugins._plugins.classes.plugin_classes_v1 import _load_plugin + from polus.plugins._plugins.classes.plugin_classes_v1 import get_plugin + from polus.plugins._plugins.classes.plugin_classes_v1 import list_plugins + from polus.plugins._plugins.classes.plugin_classes_v1 import load_config + from polus.plugins._plugins.classes.plugin_classes_v1 import refresh + from polus.plugins._plugins.classes.plugin_classes_v1 import remove_all + from polus.plugins._plugins.classes.plugin_classes_v1 import remove_plugin + from polus.plugins._plugins.classes.plugin_classes_v1 import submit_plugin +elif PYDANTIC_VERSION.split(".")[0] == "2": + from polus.plugins._plugins.classes.plugin_classes_v2 import PLUGINS + from polus.plugins._plugins.classes.plugin_classes_v2 import ComputePlugin + from polus.plugins._plugins.classes.plugin_classes_v2 import Plugin + from polus.plugins._plugins.classes.plugin_classes_v2 import _load_plugin + from polus.plugins._plugins.classes.plugin_classes_v2 import get_plugin + from polus.plugins._plugins.classes.plugin_classes_v2 import list_plugins + from polus.plugins._plugins.classes.plugin_classes_v2 import load_config + from polus.plugins._plugins.classes.plugin_classes_v2 import refresh + from polus.plugins._plugins.classes.plugin_classes_v2 import remove_all + from polus.plugins._plugins.classes.plugin_classes_v2 import remove_plugin + from polus.plugins._plugins.classes.plugin_classes_v2 import submit_plugin __all__ = [ "Plugin", @@ -19,4 +38,6 @@ "remove_plugin", "remove_all", "load_config", + "_load_plugin", + "PLUGINS", ] diff --git a/src/polus/plugins/_plugins/classes/plugin_methods.py b/src/polus/plugins/_plugins/classes/plugin_base.py similarity index 62% rename from src/polus/plugins/_plugins/classes/plugin_methods.py rename to src/polus/plugins/_plugins/classes/plugin_base.py index 91b254a8c..ec9a18fe9 100644 --- a/src/polus/plugins/_plugins/classes/plugin_methods.py +++ b/src/polus/plugins/_plugins/classes/plugin_base.py @@ -3,58 +3,60 @@ import enum import json import logging -import pathlib import random import signal -import typing -from os.path import relpath +from pathlib import Path +from typing import Any +from typing import Optional +from typing import TypeVar +from typing import Union import fsspec import yaml # type: ignore from cwltool.context import RuntimeContext from cwltool.factory import Factory from cwltool.utils import CWLObjectType -from python_on_whales import docker - from polus.plugins._plugins.cwl import CWL_BASE_DICT -from polus.plugins._plugins.io import ( - input_to_cwl, - io_to_yml, - output_to_cwl, - outputs_cwl, -) +from polus.plugins._plugins.io import input_to_cwl +from polus.plugins._plugins.io import io_to_yml +from polus.plugins._plugins.io import output_to_cwl +from polus.plugins._plugins.io import outputs_cwl from polus.plugins._plugins.utils import name_cleaner +from python_on_whales import docker logger = logging.getLogger("polus.plugins") -StrPath = typing.TypeVar("StrPath", str, pathlib.Path) +StrPath = TypeVar("StrPath", str, Path) class IOKeyError(Exception): """Raised when trying to set invalid I/O parameter.""" -class MissingInputValues(Exception): +class MissingInputValuesError(Exception): """Raised when there are required input values that have not been set.""" -class _PluginMethods: - def _check_inputs(self): +class BasePlugin: + """Base Class for Plugins.""" + + def _check_inputs(self) -> None: """Check if all required inputs have been set.""" _in = [x for x in self.inputs if x.required and not x.value] # type: ignore if len(_in) > 0: - raise MissingInputValues( - f"{[x.name for x in _in]} are required inputs but have not been set" # type: ignore + msg = f"{[x.name for x in _in]} are required inputs but have not been set" + raise MissingInputValuesError( + msg, # type: ignore ) @property - def organization(self): + def organization(self) -> str: """Plugin container's organization.""" return self.containerId.split("/")[0] - def load_config(self, path: StrPath): + def load_config(self, path: StrPath) -> None: """Load configured plugin from file.""" - with open(path, encoding="utf=8") as fw: + with Path(path).open(encoding="utf=8") as fw: config = json.load(fw) inp = config["inputs"] out = config["outputs"] @@ -68,20 +70,13 @@ def load_config(self, path: StrPath): def run( self, - gpus: typing.Union[None, str, int] = "all", - **kwargs, - ): + gpus: Union[None, str, int] = "all", + **kwargs: Union[None, str, int], + ) -> None: + """Run plugin in Docker container.""" self._check_inputs() - inp_dirs = [] - out_dirs = [] - - for i in self.inputs: - if isinstance(i.value, pathlib.Path): - inp_dirs.append(str(i.value)) - - for o in self.outputs: - if isinstance(o.value, pathlib.Path): - out_dirs.append(str(o.value)) + inp_dirs = [x for x in self.inputs if isinstance(x.value, Path)] + out_dirs = [x for x in self.outputs if isinstance(x.value, Path)] inp_dirs_dict = {x: f"/data/inputs/input{n}" for (n, x) in enumerate(inp_dirs)} out_dirs_dict = { @@ -105,7 +100,7 @@ def run( i._validate() args.append(f"--{i.name}") - if isinstance(i.value, pathlib.Path): + if isinstance(i.value, Path): args.append(inp_dirs_dict[str(i.value)]) elif isinstance(i.value, enum.Enum): @@ -119,7 +114,7 @@ def run( o._validate() args.append(f"--{o.name}") - if isinstance(o.value, pathlib.Path): + if isinstance(o.value, Path): args.append(out_dirs_dict[str(o.value)]) elif isinstance(o.value, enum.Enum): @@ -128,20 +123,24 @@ def run( else: args.append(str(o.value)) - container_name = f"polus{random.randint(10, 99)}" + random_int = random.randint(10, 99) # noqa: S311 # only for naming + container_name = f"polus{random_int}" def sig( - signal, frame # pylint: disable=W0613, W0621 - ): # signal handler to kill container when KeyboardInterrupt - print(f"Exiting container {container_name}") + signal, # noqa # pylint: disable=W0613, W0621 + frame, # noqa # pylint: disable=W0613, W0621 + ) -> None: # signal handler to kill container when KeyboardInterrupt + logger.info(f"Exiting container {container_name}") docker.kill(container_name) signal.signal( - signal.SIGINT, sig + signal.SIGINT, + sig, ) # make of sig the handler for KeyboardInterrupt if gpus is None: logger.info( - f"Running container without GPU. {self.__class__.__name__} version {self.version.version}" + f"""Running container without GPU. {self.__class__.__name__} + version {self.version!s}""", ) docker_ = docker.run( self.containerId, @@ -151,10 +150,11 @@ def sig( mounts=mnts, **kwargs, ) - print(docker_) + print(docker_) # noqa else: logger.info( - f"Running container with GPU: --gpus {gpus}. {self.__class__.__name__} version {self.version.version}" + f"""Running container with GPU: --gpus {gpus}. + {self.__class__.__name__} version {self.version!s}""", ) docker_ = docker.run( self.containerId, @@ -165,36 +165,38 @@ def sig( mounts=mnts, **kwargs, ) - print(docker_) + print(docker_) # noqa @property - def _config(self): + def _config(self) -> dict: model_ = self.dict() for inp in model_["inputs"]: inp["value"] = None return model_ @property - def manifest(self): + def manifest(self) -> dict: """Plugin manifest.""" manifest_ = json.loads(self.json(exclude={"_io_keys", "versions", "id"})) manifest_["version"] = manifest_["version"]["version"] return manifest_ - def __getattribute__(self, name): - if name != "_io_keys" and hasattr(self, "_io_keys"): - if name in self._io_keys: - value = self._io_keys[name].value - if isinstance(value, enum.Enum): - value = value.name - return value + def __getattribute__(self, name: str) -> Any: # noqa + if name == "__class__": # pydantic v2 change + return super().__getattribute__(name) + if name != "_io_keys" and hasattr(self, "_io_keys") and name in self._io_keys: + value = self._io_keys[name].value + if isinstance(value, enum.Enum): + value = value.name + return value return super().__getattribute__(name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any) -> None: # noqa if name == "_fs": if not issubclass(type(value), fsspec.spec.AbstractFileSystem): - raise ValueError("_fs must be an fsspec FileSystem") + msg = "_fs must be an fsspec FileSystem" + raise ValueError(msg) for i in self.inputs: i._fs = value for o in self.outputs: @@ -204,18 +206,22 @@ def __setattr__(self, name, value): if name != "_io_keys" and hasattr(self, "_io_keys"): if name in self._io_keys: logger.debug( - f"Value of {name} in {self.__class__.__name__} set to {value}" + f"Value of {name} in {self.__class__.__name__} set to {value}", ) self._io_keys[name].value = value return + msg = ( + f"Attempting to set {name} in " + "{self.__class__.__name__} but " + "{{name}} is not a valid I/O parameter" + ) raise IOKeyError( - f"Attempting to set {name} in {self.__class__.__name__} but" - "{name} is not a valid I/O parameter" + msg, ) super().__setattr__(name, value) - def _to_cwl(self): + def _to_cwl(self) -> dict: """Return CWL yml as dict.""" cwl_dict = CWL_BASE_DICT cwl_dict["inputs"] = {} @@ -230,12 +236,14 @@ def _to_cwl(self): cwl_dict["requirements"]["DockerRequirement"]["dockerPull"] = self.containerId return cwl_dict - def save_cwl(self, path: StrPath) -> pathlib.Path: + def save_cwl(self, path: StrPath) -> Path: """Save plugin as CWL command line tool.""" - assert str(path).rsplit(".", maxsplit=1)[-1] == "cwl", "Path must end in .cwl" - with open(path, "w", encoding="utf-8") as file: + if str(path).rsplit(".", maxsplit=1)[-1] != "cwl": + msg = "path must end in .cwl" + raise ValueError(msg) + with Path(path).open("w", encoding="utf-8") as file: yaml.dump(self._to_cwl(), file) - return pathlib.Path(path) + return Path(path) @property def _cwl_io(self) -> dict: @@ -244,19 +252,24 @@ def _cwl_io(self) -> dict: x.name: io_to_yml(x) for x in self._io_keys.values() if x.value is not None } - def save_cwl_io(self, path) -> pathlib.Path: - """Save plugin's I/O values to yml file to be used with CWL command line tool.""" + def save_cwl_io(self, path: StrPath) -> Path: + """Save plugin's I/O values to yml file. + + To be used with CWL Command Line Tool. + """ self._check_inputs() - assert str(path).rsplit(".", maxsplit=1)[-1] == "yml", "Path must end in .yml" - with open(path, "w", encoding="utf-8") as file: + if str(path).rsplit(".", maxsplit=1)[-1] != "yml": + msg = "path must end in .yml" + raise ValueError(msg) + with Path(path).open("w", encoding="utf-8") as file: yaml.dump(self._cwl_io, file) - return pathlib.Path(path) + return Path(path) def run_cwl( self, - cwl_path: typing.Optional[StrPath] = None, - io_path: typing.Optional[StrPath] = None, - ) -> typing.Union[CWLObjectType, str, None]: + cwl_path: Optional[StrPath] = None, + io_path: Optional[StrPath] = None, + ) -> Union[CWLObjectType, str, None]: """Run configured plugin in CWL. Run plugin as a CWL command line tool after setting I/O values. @@ -272,31 +285,34 @@ def run_cwl( """ if not self.outDir: - raise ValueError("") + msg = "" + raise ValueError(msg) if not cwl_path: - _p = pathlib.Path.cwd().joinpath(name_cleaner(self.name) + ".cwl") + _p = Path.cwd().joinpath(name_cleaner(self.name) + ".cwl") _cwl = self.save_cwl(_p) else: _cwl = self.save_cwl(cwl_path) if not io_path: - _p = pathlib.Path.cwd().joinpath(name_cleaner(self.name) + ".yml") + _p = Path.cwd().joinpath(name_cleaner(self.name) + ".yml") self.save_cwl_io(_p) # saves io to make it visible to user else: self.save_cwl_io(io_path) # saves io to make it visible to user - outdir_path = self.outDir.parent.relative_to(pathlib.Path.cwd()) + outdir_path = self.outDir.parent.relative_to(Path.cwd()) r_c = RuntimeContext({"outdir": str(outdir_path)}) fac = Factory(runtime_context=r_c) cwl = fac.make(str(_cwl)) return cwl(**self._cwl_io) # object's io dict is used instead of .yml file - def __lt__(self, other): + def __lt__(self, other: "BasePlugin") -> bool: return self.version < other.version - def __gt__(self, other): + def __gt__(self, other: "BasePlugin") -> bool: return other.version < self.version def __repr__(self) -> str: - return f"{self.__class__.__name__}(name='{self.name}', version={self.version.version})" + return ( + f"{self.__class__.__name__}(name='{self.name}', version={self.version!s})" + ) diff --git a/src/polus/plugins/_plugins/classes/plugin_classes.py b/src/polus/plugins/_plugins/classes/plugin_classes_v1.py similarity index 94% rename from src/polus/plugins/_plugins/classes/plugin_classes.py rename to src/polus/plugins/_plugins/classes/plugin_classes_v1.py index 9c4d54c05..c2808fadd 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes_v1.py @@ -10,14 +10,14 @@ from typing import Optional from typing import Union -from polus.plugins._plugins.classes.plugin_methods import _PluginMethods -from polus.plugins._plugins.io import DuplicateVersionFoundError -from polus.plugins._plugins.io import Version -from polus.plugins._plugins.io import _in_old_to_new -from polus.plugins._plugins.io import _ui_old_to_new -from polus.plugins._plugins.manifests.manifest_utils import InvalidManifest -from polus.plugins._plugins.manifests.manifest_utils import _load_manifest -from polus.plugins._plugins.manifests.manifest_utils import validate_manifest +from polus.plugins._plugins.classes.plugin_base import BasePlugin +from polus.plugins._plugins.io.io_v1 import DuplicateVersionFoundError +from polus.plugins._plugins.io.io_v1 import Version +from polus.plugins._plugins.io.io_v1 import _in_old_to_new +from polus.plugins._plugins.io.io_v1 import _ui_old_to_new +from polus.plugins._plugins.manifests import InvalidManifestError +from polus.plugins._plugins.manifests import _load_manifest +from polus.plugins._plugins.manifests import validate_manifest from polus.plugins._plugins.models import ComputeSchema from polus.plugins._plugins.models import PluginUIInput from polus.plugins._plugins.models import PluginUIOutput @@ -54,7 +54,7 @@ def refresh() -> None: try: plugin = validate_manifest(file) - except InvalidManifest: + except InvalidManifestError: logger.warning(f"Validation error in {file!s}") except BaseException as exc: # pylint: disable=W0718 # noqa: BLE001 logger.warning(f"Unexpected error {exc} with {file!s}") @@ -85,7 +85,7 @@ def list_plugins() -> list: return output -class Plugin(WIPPPluginManifest, _PluginMethods): +class Plugin(WIPPPluginManifest, BasePlugin): """WIPP Plugin Class. Contains methods to configure, run, and save plugins. @@ -168,7 +168,7 @@ def save_manifest( def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" - _PluginMethods.__setattr__(self, name, value) + BasePlugin.__setattr__(self, name, value) @property def _config_file(self) -> dict: @@ -184,10 +184,10 @@ def save_config(self, path: Union[str, Path]) -> None: def __repr__(self) -> str: """Print plugin name and version.""" - return _PluginMethods.__repr__(self) + return BasePlugin.__repr__(self) -class ComputePlugin(ComputeSchema, _PluginMethods): +class ComputePlugin(ComputeSchema, BasePlugin): """Compute Plugin Class. Contains methods to configure, run, and save plugins. @@ -289,7 +289,7 @@ def _config_file(self) -> dict: def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" - _PluginMethods.__setattr__(self, name, value) + BasePlugin.__setattr__(self, name, value) def save_config(self, path: Union[str, Path]) -> None: """Save configured manifest with I/O parameters to specified path.""" @@ -305,7 +305,7 @@ def save_manifest(self, path: Union[str, Path]) -> None: def __repr__(self) -> str: """Print plugin name and version.""" - return _PluginMethods.__repr__(self) + return BasePlugin.__repr__(self) def _load_plugin( diff --git a/src/polus/plugins/_plugins/classes/plugin_classes_v2.py b/src/polus/plugins/_plugins/classes/plugin_classes_v2.py new file mode 100644 index 000000000..e59352f24 --- /dev/null +++ b/src/polus/plugins/_plugins/classes/plugin_classes_v2.py @@ -0,0 +1,429 @@ +"""Classes for Plugin objects containing methods to configure, run, and save.""" +# pylint: disable=W1203, enable=W1201 +import json +import logging +import shutil +import uuid +from copy import deepcopy +from pathlib import Path +from typing import Any +from typing import Optional +from typing import Union + +from polus.plugins._plugins.classes.plugin_base import BasePlugin +from polus.plugins._plugins.io.io_v2 import DuplicateVersionFoundError +from polus.plugins._plugins.io.io_v2 import Version +from polus.plugins._plugins.io.io_v2 import _in_old_to_new +from polus.plugins._plugins.io.io_v2 import _ui_old_to_new +from polus.plugins._plugins.manifests import InvalidManifestError +from polus.plugins._plugins.manifests import _load_manifest +from polus.plugins._plugins.manifests import validate_manifest +from polus.plugins._plugins.models import ComputeSchema +from polus.plugins._plugins.models import PluginUIInput +from polus.plugins._plugins.models import PluginUIOutput +from polus.plugins._plugins.models import WIPPPluginManifest +from polus.plugins._plugins.utils import cast_version +from polus.plugins._plugins.utils import name_cleaner +from pydantic import ConfigDict + +logger = logging.getLogger("polus.plugins") +PLUGINS: dict[str, dict] = {} +# PLUGINS = {"BasicFlatfieldCorrectionPlugin": +# {Version('0.1.4'): Path(<...>), Version('0.1.5'): Path(<...>)}. +# "VectorToLabel": {Version(...)}} + +""" +Paths and Fields +""" +# Location to store any discovered plugin manifests +_PLUGIN_DIR = Path(__file__).parent.parent.joinpath("manifests") + + +def refresh() -> None: + """Refresh the plugin list.""" + organizations = [ + x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() + ] # ignore __pycache__ + + PLUGINS.clear() + + for org in organizations: + for file in org.iterdir(): + if file.suffix == ".py": + continue + + try: + plugin = validate_manifest(file) + except InvalidManifestError: + logger.warning(f"Validation error in {file!s}") + except BaseException as exc: # pylint: disable=W0718 + logger.warning(f"Unexpected error {exc} with {file!s}") + raise exc + + else: + key = name_cleaner(plugin.name) + # Add version and path to VERSIONS + if key not in PLUGINS: + PLUGINS[key] = {} + if ( + plugin.version in PLUGINS[key] + and file != PLUGINS[key][plugin.version] + ): + msg = ( + "Found duplicate version of plugin" + f"{plugin.name} in {_PLUGIN_DIR}" + ) + raise DuplicateVersionFoundError( + msg, + ) + PLUGINS[key][plugin.version] = file + + +def list_plugins() -> list: + """List all local plugins.""" + output = list(PLUGINS.keys()) + output.sort() + return output + + +class Plugin(WIPPPluginManifest, BasePlugin): + """WIPP Plugin Class. + + Contains methods to configure, run, and save plugins. + + Attributes: + versions: A list of local available versions for this plugin. + + Methods: + save_manifest(path): save plugin manifest to specified path + """ + + id: uuid.UUID # noqa: A003 + model_config = ConfigDict(extra="allow", frozen=True) + + def __init__(self, _uuid: bool = True, **data: dict) -> None: + """Init a plugin object from manifest.""" + if _uuid: + data["id"] = uuid.uuid4() # type: ignore + else: + data["id"] = uuid.UUID(str(data["id"])) # type: ignore + + super().__init__(**data) + + self._io_keys = {i.name: i for i in self.inputs} + self._io_keys.update({o.name: o for o in self.outputs}) + + if not self.author: + warn_msg = ( + f"The plugin ({self.name}) is missing the author field. " + "This field is not required but should be filled in." + ) + logger.warning(warn_msg) + + @property + def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives here + """Return list of local versions of a Plugin.""" + return list(PLUGINS[name_cleaner(self.name)]) + + def to_compute( + self, + hardware_requirements: Optional[dict] = None, + ) -> type[ComputeSchema]: + """Convert WIPP Plugin object to Compute Plugin object.""" + data = deepcopy(self.manifest) + return ComputePlugin( + hardware_requirements=hardware_requirements, + _from_old=True, + **data, + ) + + def save_manifest( + self, + path: Union[str, Path], + hardware_requirements: Optional[dict] = None, + compute: bool = False, + ) -> None: + """Save plugin manifest to specified path.""" + if compute: + with Path(path).open("w", encoding="utf-8") as file: + self.to_compute( + hardware_requirements=hardware_requirements, + ).save_manifest(path) + else: + with Path(path).open("w", encoding="utf-8") as file: + dict_ = self.manifest + json.dump( + dict_, + file, + indent=4, + ) + + logger.debug(f"Saved manifest to {path}") + + def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 + """Set I/O parameters as attributes.""" + BasePlugin.__setattr__(self, name, value) + + @property + def _config_file(self) -> dict: + config_ = self._config + config_["class"] = "WIPP" + return config_ + + def save_config(self, path: Union[str, Path]) -> None: + """Save manifest with configured I/O parameters to specified path.""" + with Path(path).open("w", encoding="utf-8") as file: + json.dump(self._config_file, file, indent=4, default=str) + logger.debug(f"Saved config to {path}") + + def __repr__(self) -> str: + """Print plugin name and version.""" + return BasePlugin.__repr__(self) + + +class ComputePlugin(ComputeSchema, BasePlugin): + """Compute Plugin Class. + + Contains methods to configure, run, and save plugins. + + Attributes: + versions: A list of local available versions for this plugin. + + Methods: + save_manifest(path): save plugin manifest to specified path + """ + + model_config = ConfigDict(extra="allow", frozen=True) + + def __init__( + self, + hardware_requirements: Optional[dict] = None, + _from_old: bool = False, + _uuid: bool = True, + **data: dict, + ) -> None: + """Init a plugin object from manifest.""" + if _uuid: + data["id"] = uuid.uuid4() # type: ignore + else: + data["id"] = uuid.UUID(str(data["id"])) # type: ignore + + if _from_old: + + def _convert_input(dict_: dict) -> dict: + dict_["type"] = _in_old_to_new(dict_["type"]) + return dict_ + + def _convert_output(dict_: dict) -> dict: + dict_["type"] = "path" + return dict_ + + def _ui_in(dict_: dict) -> PluginUIInput: # assuming old all ui input + # assuming format inputs. ___ + inp = dict_["key"].split(".")[-1] # e.g inpDir + try: + type_ = [x["type"] for x in data["inputs"] if x["name"] == inp][ + 0 + ] # get type from i/o + except IndexError: + type_ = "string" # default to string + except BaseException as exc: + raise exc + + dict_["type"] = _ui_old_to_new(type_) + return PluginUIInput(**dict_) + + def _ui_out(dict_: dict) -> PluginUIOutput: + new_dict_ = deepcopy(dict_) + new_dict_["name"] = "outputs." + new_dict_["name"] + new_dict_["type"] = _ui_old_to_new(new_dict_["type"]) + return PluginUIOutput(**new_dict_) + + data["inputs"] = [_convert_input(x) for x in data["inputs"]] # type: ignore + data["outputs"] = [ + _convert_output(x) for x in data["outputs"] + ] # type: ignore + data["pluginHardwareRequirements"] = {} + data["ui"] = [_ui_in(x) for x in data["ui"]] # type: ignore + data["ui"].extend( # type: ignore[attr-defined] + [_ui_out(x) for x in data["outputs"]], + ) + + if hardware_requirements: + for k, v in hardware_requirements.items(): + data["pluginHardwareRequirements"][k] = v + + data["version"] = cast_version(data["version"]) + super().__init__(**data) + self.Config.allow_mutation = True + self._io_keys = {i.name: i for i in self.inputs} + self._io_keys.update({o.name: o for o in self.outputs}) # type: ignore + + if not self.author: + warn_msg = ( + f"The plugin ({self.name}) is missing the author field. " + "This field is not required but should be filled in." + ) + logger.warning(warn_msg) + + @property + def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives here + """Return list of local versions of a Plugin.""" + return list(PLUGINS[name_cleaner(self.name)]) + + @property + def _config_file(self) -> dict: + config_ = self._config + config_["class"] = "Compute" + return config_ + + def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 + """Set I/O parameters as attributes.""" + BasePlugin.__setattr__(self, name, value) + + def save_config(self, path: Union[str, Path]) -> None: + """Save configured manifest with I/O parameters to specified path.""" + with Path(path).open("w", encoding="utf-8") as file: + json.dump(self._config_file, file, indent=4) + logger.debug(f"Saved config to {path}") + + def save_manifest(self, path: Union[str, Path]) -> None: + """Save plugin manifest to specified path.""" + with Path(path).open("w", encoding="utf-8") as file: + json.dump(self.manifest, file, indent=4) + logger.debug(f"Saved manifest to {path}") + + def __repr__(self) -> str: + """Print plugin name and version.""" + return BasePlugin.__repr__(self) + + +def _load_plugin( + manifest: Union[str, dict, Path], +) -> Union[Plugin, ComputePlugin]: + """Parse a manifest and return one of Plugin or ComputePlugin.""" + manifest = _load_manifest(manifest) + if "pluginHardwareRequirements" in manifest: # type: ignore[operator] + # Parse the manifest + plugin = ComputePlugin(**manifest) # type: ignore[arg-type] + else: + # Parse the manifest + plugin = Plugin(**manifest) # type: ignore[arg-type] + return plugin + + +def submit_plugin( + manifest: Union[str, dict, Path], +) -> Union[Plugin, ComputePlugin]: + """Parse a plugin and create a local copy of it. + + This function accepts a plugin manifest as a string, a dictionary (parsed + json), or a pathlib.Path object pointed at a plugin manifest. + + Args: + manifest: + A plugin manifest. It can be a url, a dictionary, + a path to a JSON file or a string that can be parsed as a dictionary + + Returns: + A Plugin object populated with information from the plugin manifest. + """ + plugin = validate_manifest(manifest) + plugin_name = name_cleaner(plugin.name) + + # Get Major/Minor/Patch versions + out_name = ( + plugin_name + + f"_M{plugin.version.major}m{plugin.version.minor}p{plugin.version.patch}.json" + ) + + # Save the manifest if it doesn't already exist in the database + organization = plugin.containerId.split("/")[0] + org_path = _PLUGIN_DIR.joinpath(organization.lower()) + org_path.mkdir(exist_ok=True, parents=True) + if not org_path.joinpath(out_name).exists(): + with org_path.joinpath(out_name).open("w", encoding="utf-8") as file: + manifest_ = json.loads(plugin.model_dump_json()) + json.dump(manifest_, file, indent=4) + + # Refresh plugins list + refresh() + return plugin + + +def get_plugin( + name: str, + version: Optional[str] = None, +) -> Union[Plugin, ComputePlugin]: + """Get a plugin with option to specify version. + + Return a plugin object with the option to specify a version. + The specified version's manifest must exist in manifests folder. + + Args: + name: Name of the plugin. + version: Optional version of the plugin, must follow semver. + + Returns: + Plugin object + """ + if version is None: + return _load_plugin(PLUGINS[name][max(PLUGINS[name])]) + return _load_plugin(PLUGINS[name][Version(**{"version": version})]) + + +def load_config(config: Union[dict, Path]) -> Union[Plugin, ComputePlugin]: + """Load configured plugin from config file/dict.""" + if isinstance(config, Path): + with config.open("r", encoding="utf-8") as file: + manifest_ = json.load(file) + elif isinstance(config, dict): + manifest_ = config + else: + msg = "config must be a dict or a path" + raise TypeError(msg) + io_keys_ = manifest_["_io_keys"] + class_ = manifest_["class"] + manifest_.pop("class", None) + if class_ == "Compute": + plugin_ = ComputePlugin(_uuid=False, **manifest_) + elif class_ == "WIPP": + plugin_ = Plugin(_uuid=False, **manifest_) + else: + msg = "Invalid value of class" + raise ValueError(msg) + for key, value_ in io_keys_.items(): + val = value_["value"] + if val is not None: # exclude those values not set + setattr(plugin_, key, val) + return plugin_ + + +def remove_plugin(plugin: str, version: Optional[Union[str, list[str]]] = None) -> None: + """Remove plugin from the local database.""" + if version is None: + for plugin_version in PLUGINS[plugin]: + remove_plugin(plugin, plugin_version) + else: + if isinstance(version, list): + for version_ in version: + remove_plugin(plugin, version_) + return + if not isinstance(version, Version): + version_ = cast_version(version) + else: + version_ = version + path = PLUGINS[plugin][version_] + path.unlink() + refresh() + + +def remove_all() -> None: + """Remove all plugins from the local database.""" + organizations = [ + x for x in _PLUGIN_DIR.iterdir() if x.name != "__pycache__" and x.is_dir() + ] # ignore __pycache__ + logger.warning("Removing all plugins from local database") + for org in organizations: + shutil.rmtree(org) + refresh() diff --git a/src/polus/plugins/_plugins/io/__init__.py b/src/polus/plugins/_plugins/io/__init__.py new file mode 100644 index 000000000..1e10418b7 --- /dev/null +++ b/src/polus/plugins/_plugins/io/__init__.py @@ -0,0 +1,35 @@ +"""Init IO module.""" + +import pydantic + +PYDANTIC_VERSION = pydantic.__version__ + +if PYDANTIC_VERSION.split(".")[0] == "1": + from polus.plugins._plugins.io.io_v1 import Input + from polus.plugins._plugins.io.io_v1 import IOBase + from polus.plugins._plugins.io.io_v1 import Output + from polus.plugins._plugins.io.io_v1 import Version + from polus.plugins._plugins.io.io_v1 import input_to_cwl + from polus.plugins._plugins.io.io_v1 import io_to_yml + from polus.plugins._plugins.io.io_v1 import output_to_cwl + from polus.plugins._plugins.io.io_v1 import outputs_cwl +elif PYDANTIC_VERSION.split(".")[0] == "2": + from polus.plugins._plugins.io.io_v2 import Input + from polus.plugins._plugins.io.io_v2 import IOBase + from polus.plugins._plugins.io.io_v2 import Output + from polus.plugins._plugins.io.io_v2 import Version + from polus.plugins._plugins.io.io_v2 import input_to_cwl + from polus.plugins._plugins.io.io_v2 import io_to_yml + from polus.plugins._plugins.io.io_v2 import output_to_cwl + from polus.plugins._plugins.io.io_v2 import outputs_cwl + +__all__ = [ + "Input", + "Output", + "IOBase", + "Version", + "io_to_yml", + "outputs_cwl", + "input_to_cwl", + "output_to_cwl", +] diff --git a/src/polus/plugins/_plugins/io.py b/src/polus/plugins/_plugins/io/io_v1.py similarity index 98% rename from src/polus/plugins/_plugins/io.py rename to src/polus/plugins/_plugins/io/io_v1.py index fe6f27c7a..248e1af15 100644 --- a/src/polus/plugins/_plugins/io.py +++ b/src/polus/plugins/_plugins/io/io_v1.py @@ -10,6 +10,7 @@ from functools import singledispatchmethod from typing import Any from typing import Optional +from typing import TypeVar from typing import Union import fsspec @@ -102,6 +103,9 @@ def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's U return "text" +FileSystem = TypeVar("FileSystem", bound=fsspec.spec.AbstractFileSystem) + + class IOBase(BaseModel): # pylint: disable=R0903 """Base Class for I/O arguments.""" @@ -109,7 +113,7 @@ class IOBase(BaseModel): # pylint: disable=R0903 options: Optional[dict] = None value: Optional[Any] = None id_: Optional[Any] = None - _fs: Optional[type[fsspec.spec.AbstractFileSystem]] = PrivateAttr( + _fs: Optional[FileSystem] = PrivateAttr( default=None, ) # type checking is done at plugin level diff --git a/src/polus/plugins/_plugins/io/io_v2.py b/src/polus/plugins/_plugins/io/io_v2.py new file mode 100644 index 000000000..846a8cd3e --- /dev/null +++ b/src/polus/plugins/_plugins/io/io_v2.py @@ -0,0 +1,464 @@ +# type: ignore +# ruff: noqa: S101, A003 +# pylint: disable=no-self-argument +"""Plugins I/O utilities.""" +import enum +import logging +import pathlib +import re +from functools import singledispatch +from functools import singledispatchmethod +from typing import Annotated +from typing import Any +from typing import Optional +from typing import TypeVar +from typing import Union + +import fsspec +from pydantic import BaseModel +from pydantic import Field +from pydantic import PrivateAttr +from pydantic import RootModel +from pydantic import StringConstraints +from pydantic import field_validator + +logger = logging.getLogger("polus.plugins") + +""" +Enums for validating plugin input, output, and ui components. +""" +WIPP_TYPES = { + "collection": pathlib.Path, + "pyramid": pathlib.Path, + "csvCollection": pathlib.Path, + "genericData": pathlib.Path, + "stitchingVector": pathlib.Path, + "notebook": pathlib.Path, + "tensorflowModel": pathlib.Path, + "tensorboardLogs": pathlib.Path, + "pyramidAnnotation": pathlib.Path, + "integer": int, + "number": float, + "string": str, + "boolean": bool, + "array": str, + "enum": enum.Enum, + "path": pathlib.Path, +} + + +class InputTypes(str, enum.Enum): # wipp schema + """Enum of Input Types for WIPP schema.""" + + COLLECTION = "collection" + PYRAMID = "pyramid" + CSVCOLLECTION = "csvCollection" + GENERICDATA = "genericData" + STITCHINGVECTOR = "stitchingVector" + NOTEBOOK = "notebook" + TENSORFLOWMODEL = "tensorflowModel" + TENSORBOARDLOGS = "tensorboardLogs" + PYRAMIDANNOTATION = "pyramidAnnotation" + INTEGER = "integer" + NUMBER = "number" + STRING = "string" + BOOLEAN = "boolean" + ARRAY = "array" + ENUM = "enum" + + +class OutputTypes(str, enum.Enum): # wipp schema + """Enum for Output Types for WIPP schema.""" + + COLLECTION = "collection" + PYRAMID = "pyramid" + CSVCOLLECTION = "csvCollection" + GENERICDATA = "genericData" + STITCHINGVECTOR = "stitchingVector" + NOTEBOOK = "notebook" + TENSORFLOWMODEL = "tensorflowModel" + TENSORBOARDLOGS = "tensorboardLogs" + PYRAMIDANNOTATION = "pyramidAnnotation" + + +def _in_old_to_new(old: str) -> str: # map wipp InputType to compute schema's InputType + """Map an InputType from wipp schema to one of compute schema.""" + d = {"integer": "number", "enum": "string"} + if old in ["string", "array", "number", "boolean"]: + return old + if old in d: + return d[old] # integer or enum + return "path" # everything else + + +def _ui_old_to_new(old: str) -> str: # map wipp InputType to compute schema's UIType + """Map an InputType from wipp schema to a UIType of compute schema.""" + type_dict = { + "string": "text", + "boolean": "checkbox", + "number": "number", + "array": "text", + "integer": "number", + } + if old in type_dict: + return type_dict[old] + return "text" + + +FileSystem = TypeVar("FileSystem", bound=fsspec.spec.AbstractFileSystem) + + +class IOBase(BaseModel): # pylint: disable=R0903 + """Base Class for I/O arguments.""" + + type: Any = None + options: Optional[dict] = None + value: Optional[Any] = None + id_: Optional[Any] = None + _fs: Optional[FileSystem] = PrivateAttr( + default=None, + ) # type checking is done at plugin level + + def _validate(self) -> None: # pylint: disable=R0912 + value = self.value + + if value is None: + if self.required: + msg = f""" + The input value ({self.name}) is required, + but the value was not set.""" + raise TypeError( + msg, + ) + + return + + if self.type == InputTypes.ENUM: + try: + if isinstance(value, str): + value = enum.Enum(self.name, self.options["values"])[value] + elif not isinstance(value, enum.Enum): + raise ValueError + + except KeyError: + logging.error( + f""" + Value ({value}) is not a valid value + for the enum input ({self.name}). + Must be one of {self.options['values']}. + """, + ) + raise + else: + if isinstance(self.type, (InputTypes, OutputTypes)): # wipp + value = WIPP_TYPES[self.type](value) + else: + value = WIPP_TYPES[self.type.value]( + value, + ) # compute, type does not inherit from str + + if isinstance(value, pathlib.Path): + value = value.absolute() + if self._fs: + assert self._fs.exists( + str(value), + ), f"{value} is invalid or does not exist" + assert self._fs.isdir( + str(value), + ), f"{value} is not a valid directory" + else: + assert value.exists(), f"{value} is invalid or does not exist" + assert value.is_dir(), f"{value} is not a valid directory" + + super().__setattr__("value", value) + + def __setattr__(self, name: str, value: Any) -> None: # ruff: noqa: ANN401 + """Set I/O attributes.""" + if name not in ["value", "id", "_fs"]: + # Don't permit any other values to be changed + msg = f"Cannot set property: {name}" + raise TypeError(msg) + + super().__setattr__(name, value) + + if name == "value": + self._validate() + + +class Output(IOBase): # pylint: disable=R0903 + """Required until JSON schema is fixed.""" + + name: Annotated[ + str, + StringConstraints(pattern=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$"), + ] = Field( + ..., + examples=["outputCollection"], + title="Output name", + ) + type: OutputTypes = Field( + ..., + examples=["stitchingVector", "collection"], + title="Output type", + ) + description: Annotated[str, StringConstraints(pattern=r"^(.*)$")] = Field( + ..., + examples=["Output collection"], + title="Output description", + ) + + +class Input(IOBase): # pylint: disable=R0903 + """Required until JSON schema is fixed.""" + + name: Annotated[ + str, + StringConstraints(pattern=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$"), + ] = Field( + ..., + description="Input name as expected by the plugin CLI", + examples=["inputImages", "fileNamePattern", "thresholdValue"], + title="Input name", + ) + type: InputTypes + description: Annotated[str, StringConstraints(pattern=r"^(.*)$")] = Field( + ..., + examples=["Input Images"], + title="Input description", + ) + required: Optional[bool] = Field( + True, + description="Whether an input is required or not", + examples=[True], + title="Required input", + ) + + def __init__(self, **data) -> None: # ruff: noqa: ANN003 + """Initialize input.""" + super().__init__(**data) + + if self.description is None: + logger.warning( + f""" + The input ({self.name}) is missing the description field. + This field is not required but should be filled in. + """, + ) + + +def _check_version_number(value: Union[str, int]) -> bool: + if isinstance(value, int): + value = str(value) + if "-" in value: + value = value.split("-")[0] + if len(value) > 1 and value[0] == "0": + return False + return bool(re.match(r"^\d+$", value)) + + +class Version(RootModel): + """SemVer object.""" + + root: str + + @field_validator("root") + @classmethod + def semantic_version( + cls, + value, + ) -> Any: # ruff: noqa: ANN202, N805, ANN001 + """Pydantic Validator to check semver.""" + version = value.split(".") + + assert ( + len(version) == 3 # ruff: noqa: PLR2004 + ), f""" + Invalid version ({value}). Version must follow + semantic versioning (see semver.org)""" + if "-" in version[-1]: # with hyphen + idn = version[-1].split("-")[-1] + id_reg = re.compile("[0-9A-Za-z-]+") + assert bool( + id_reg.match(idn), + ), f"""Invalid version ({value}). + Version must follow semantic versioning (see semver.org)""" + + assert all( + map(_check_version_number, version), + ), f"""Invalid version ({value}). + Version must follow semantic versioning (see semver.org)""" + return value + + @property + def major(self): + """Return x from x.y.z .""" + return int(self.root.split(".")[0]) + + @property + def minor(self): + """Return y from x.y.z .""" + return int(self.root.split(".")[1]) + + @property + def patch(self): + """Return z from x.y.z .""" + if not self.root.split(".")[2].isdigit(): + msg = "Patch version is not a digit, comparison may not be accurate." + logger.warning(msg) + return self.root.split(".")[2] + return int(self.root.split(".")[2]) + + def __str__(self) -> str: + """Return string representation of Version object.""" + return self.root + + @singledispatchmethod + def __lt__(self, other: Any) -> bool: + """Compare if Version is less than other object.""" + msg = "invalid type for comparison." + raise TypeError(msg) + + @singledispatchmethod + def __gt__(self, other: Any) -> bool: + """Compare if Version is less than other object.""" + msg = "invalid type for comparison." + raise TypeError(msg) + + @singledispatchmethod + def __eq__(self, other: Any) -> bool: + """Compare if two Version objects are equal.""" + msg = "invalid type for comparison." + raise TypeError(msg) + + def __hash__(self) -> int: + """Needed to use Version objects as dict keys.""" + return hash(self.root) + + +@Version.__eq__.register(Version) # pylint: disable=no-member +def _(self, other): + return ( + other.major == self.major + and other.minor == self.minor + and other.patch == self.patch + ) + + +@Version.__eq__.register(str) # pylint: disable=no-member +def _(self, other): + return self == Version(**{"version": other}) + + +@Version.__lt__.register(Version) # pylint: disable=no-member +def _(self, other): + if other.major > self.major: + return True + if other.major == self.major: + if other.minor > self.minor: + return True + if other.minor == self.minor: + if other.patch > self.patch: + return True + return False + return False + return False + + +@Version.__lt__.register(str) # pylint: disable=no-member +def _(self, other): + v = Version(**{"version": other}) + return self < v + + +@Version.__gt__.register(Version) # pylint: disable=no-member +def _(self, other): + return other < self + + +@Version.__gt__.register(str) # pylint: disable=no-member +def _(self, other): + v = Version(**{"version": other}) + return self > v + + +class DuplicateVersionFoundError(Exception): + """Raise when two equal versions found.""" + + +CWL_INPUT_TYPES = { + "path": "Directory", # always Dir? Yes + "string": "string", + "number": "double", + "boolean": "boolean", + "genericData": "Directory", + "collection": "Directory", + "enum": "string", # for compatibility with workflows + "stitchingVector": "Directory", + # not yet implemented: array +} + + +def _type_in(inp: Input): + """Return appropriate value for `type` based on input type.""" + val = inp.type.value + req = "" if inp.required else "?" + + # NOT compatible with CWL workflows, ok in CLT + # if val == "enum": + # if input.required: + + # if val in CWL_INPUT_TYPES: + return CWL_INPUT_TYPES[val] + req if val in CWL_INPUT_TYPES else "string" + req + + +def input_to_cwl(inp: Input): + """Return dict of inputs for cwl.""" + return { + f"{inp.name}": { + "type": _type_in(inp), + "inputBinding": {"prefix": f"--{inp.name}"}, + }, + } + + +def output_to_cwl(out: Output): + """Return dict of output args for cwl for input section.""" + return { + f"{out.name}": { + "type": "Directory", + "inputBinding": {"prefix": f"--{out.name}"}, + }, + } + + +def outputs_cwl(out: Output): + """Return dict of output for `outputs` in cwl.""" + return { + f"{out.name}": { + "type": "Directory", + "outputBinding": {"glob": f"$(inputs.{out.name}.basename)"}, + }, + } + + +# -- I/O as arguments in .yml + + +@singledispatch +def _io_value_to_yml(io) -> Union[str, dict]: + return str(io) + + +@_io_value_to_yml.register +def _(io: pathlib.Path): + return {"class": "Directory", "location": str(io)} + + +@_io_value_to_yml.register +def _(io: enum.Enum): + return io.name + + +def io_to_yml(io): + """Return IO entry for yml file.""" + return _io_value_to_yml(io.value) diff --git a/src/polus/plugins/_plugins/manifests/__init__.py b/src/polus/plugins/_plugins/manifests/__init__.py new file mode 100644 index 000000000..b2642a73f --- /dev/null +++ b/src/polus/plugins/_plugins/manifests/__init__.py @@ -0,0 +1,26 @@ +"""Initialize manifests module.""" + +import pydantic + +PYDANTIC_VERSION = pydantic.__version__ + +if PYDANTIC_VERSION.split(".")[0] == "1": + from polus.plugins._plugins.manifests.manifest_utils_v1 import InvalidManifestError + from polus.plugins._plugins.manifests.manifest_utils_v1 import _error_log + from polus.plugins._plugins.manifests.manifest_utils_v1 import _load_manifest + from polus.plugins._plugins.manifests.manifest_utils_v1 import _scrape_manifests + from polus.plugins._plugins.manifests.manifest_utils_v1 import validate_manifest +elif PYDANTIC_VERSION.split(".")[0] == "2": + from polus.plugins._plugins.manifests.manifest_utils_v2 import InvalidManifestError + from polus.plugins._plugins.manifests.manifest_utils_v2 import _error_log + from polus.plugins._plugins.manifests.manifest_utils_v2 import _load_manifest + from polus.plugins._plugins.manifests.manifest_utils_v2 import _scrape_manifests + from polus.plugins._plugins.manifests.manifest_utils_v2 import validate_manifest + +__all__ = [ + "InvalidManifestError", + "_load_manifest", + "validate_manifest", + "_error_log", + "_scrape_manifests", +] diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils.py b/src/polus/plugins/_plugins/manifests/manifest_utils.py deleted file mode 100644 index ebc9d0556..000000000 --- a/src/polus/plugins/_plugins/manifests/manifest_utils.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Utilities for manifest parsing and validation.""" -import json -import logging -import pathlib -import typing -from urllib.parse import urlparse - -import github -import requests -from pydantic import ValidationError, errors -from tqdm import tqdm - -from polus.plugins._plugins.models import ComputeSchema, WIPPPluginManifest -from polus.plugins._plugins.utils import cast_version - -logger = logging.getLogger("polus.plugins") - -# Fields that must be in a plugin manifest -REQUIRED_FIELDS = [ - "name", - "version", - "description", - "author", - "containerId", - "inputs", - "outputs", - "ui", -] - - -class InvalidManifest(Exception): - """Raised when manifest has validation errors.""" - - -def is_valid_manifest(plugin: dict) -> bool: - """Validate basic attributes of a plugin manifest. - - Args: - plugin: A parsed plugin json file - - Returns: - True if the plugin has the minimal json fields - """ - fields = list(plugin.keys()) - - try: - for field in REQUIRED_FIELDS: - assert field in fields, f"Missing json field, {field}, in plugin manifest." - except AssertionError: - return False - - return True - - -def _load_manifest(m: typing.Union[str, dict, pathlib.Path]) -> dict: - """Convert to dictionary if pathlib.Path or str.""" - if isinstance(m, dict): - return m - elif isinstance(m, pathlib.Path): - assert ( - m.suffix == ".json" - ), "Plugin manifest must be a json file with .json extension." - - with open(m) as fr: - manifest = json.load(fr) - - elif isinstance(m, str): - if urlparse(m).netloc == "": - manifest = json.loads(m) - else: - manifest = requests.get(m).json() - else: - msg = "invalid manifest" - raise ValueError(msg) - return manifest - - -def validate_manifest( - manifest: typing.Union[str, dict, pathlib.Path], -) -> typing.Union[WIPPPluginManifest, ComputeSchema]: - """Validate a plugin manifest against schema.""" - manifest = _load_manifest(manifest) - manifest["version"] = cast_version( - manifest["version"], - ) # cast version to semver object - if "name" in manifest: - name = manifest["name"] - else: - raise InvalidManifest(f"{manifest} has no value for name") - - if "pluginHardwareRequirements" in manifest: - # Parse the manifest - try: - plugin = ComputeSchema(**manifest) - except ValidationError as e: - raise InvalidManifest(f"{name} does not conform to schema") from e - except BaseException as e: - raise e - else: - # Parse the manifest - try: - plugin = WIPPPluginManifest(**manifest) - except ValidationError as e: - raise InvalidManifest( - f"{manifest['name']} does not conform to schema" - ) from e - except BaseException as e: - raise e - return plugin - - -def _scrape_manifests( - repo: typing.Union[str, github.Repository.Repository], # type: ignore - gh: github.Github, - min_depth: int = 1, - max_depth: typing.Optional[int] = None, - return_invalid: bool = False, -) -> typing.Union[list, tuple[list, list]]: - if max_depth is None: - max_depth = min_depth - min_depth = 0 - - assert max_depth >= min_depth, "max_depth is smaller than min_depth" - - if isinstance(repo, str): - repo = gh.get_repo(repo) - - contents = list(repo.get_contents("")) # type: ignore - next_contents = [] - valid_manifests = [] - invalid_manifests = [] - - for d in range(0, max_depth): - for content in tqdm(contents, desc=f"{repo.full_name}: {d}"): - if content.type == "dir": - next_contents.extend(repo.get_contents(content.path)) # type: ignore - elif content.name.endswith(".json"): - if d >= min_depth: - manifest = json.loads(content.decoded_content) - if is_valid_manifest(manifest): - valid_manifests.append(manifest) - else: - invalid_manifests.append(manifest) - - contents = next_contents.copy() - next_contents = [] - - if return_invalid: - return valid_manifests, invalid_manifests - else: - return valid_manifests - - -def _error_log(val_err, manifest, fct): - report = [] - - for err in val_err.args[0]: - if isinstance(err, list): - err = err[0] - - if isinstance(err, AssertionError): - report.append( - "The plugin ({}) failed an assertion check: {}".format( - manifest["name"], - err.args[0], - ), - ) - logger.critical(f"{fct}: {report[-1]}") - elif isinstance(err.exc, errors.MissingError): - report.append( - "The plugin ({}) is missing fields: {}".format( - manifest["name"], - err.loc_tuple(), - ), - ) - logger.critical(f"{fct}: {report[-1]}") - elif errors.ExtraError: - if err.loc_tuple()[0] in ["inputs", "outputs", "ui"]: - report.append( - "The plugin ({}) had unexpected values in the {} ({}): {}".format( - manifest["name"], - err.loc_tuple()[0], - manifest[err.loc_tuple()[0]][err.loc_tuple()[1]]["name"], - err.exc.args[0][0].loc_tuple(), - ), - ) - else: - report.append( - "The plugin ({}) had an error: {}".format( - manifest["name"], - err.exc.args[0][0], - ), - ) - logger.critical(f"{fct}: {report[-1]}") - else: - logger.warning( - "{}: Uncaught manifest Error in ({}): {}".format( - fct, - manifest["name"], - str(val_err).replace("\n", ", ").replace(" ", " "), - ), - ) diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils_v1.py b/src/polus/plugins/_plugins/manifests/manifest_utils_v1.py new file mode 100644 index 000000000..927126f4a --- /dev/null +++ b/src/polus/plugins/_plugins/manifests/manifest_utils_v1.py @@ -0,0 +1,206 @@ +"""Utilities for manifest parsing and validation.""" +import json +import logging +import pathlib +from typing import Optional +from typing import Union + +import github +import requests +import validators +from polus.plugins._plugins.models import ComputeSchema +from polus.plugins._plugins.models import WIPPPluginManifest +from polus.plugins._plugins.utils import cast_version +from pydantic import ValidationError +from pydantic import errors +from tqdm import tqdm + +logger = logging.getLogger("polus.plugins") + +# Fields that must be in a plugin manifest +REQUIRED_FIELDS = [ + "name", + "version", + "description", + "author", + "containerId", + "inputs", + "outputs", + "ui", +] + + +class InvalidManifestError(Exception): + """Raised when manifest has validation errors.""" + + +def is_valid_manifest(plugin: dict) -> bool: + """Validate basic attributes of a plugin manifest. + + Args: + plugin: A parsed plugin json file + + Returns: + True if the plugin has the minimal json fields + """ + fields = list(plugin.keys()) + + for field in REQUIRED_FIELDS: + if field not in fields: + msg = f"Missing json field, {field}, in plugin manifest." + logger.error(msg) + return False + return True + + +def _load_manifest(manifest: Union[str, dict, pathlib.Path]) -> dict: + """Return manifest as dict from str (url or path) or pathlib.Path.""" + if isinstance(manifest, dict): # is dict + return manifest + if isinstance(manifest, pathlib.Path): # is path + if manifest.suffix != ".json": + msg = "plugin manifest must be a json file with .json extension." + raise ValueError(msg) + + with manifest.open("r", encoding="utf-8") as manifest_json: + manifest_ = json.load(manifest_json) + elif isinstance(manifest, str): # is str + if validators.url(manifest): # is url + manifest_ = requests.get(manifest, timeout=10).json() + else: # could (and should) be path + try: + manifest_ = _load_manifest(pathlib.Path(manifest)) + except Exception as exc: # was not a Path? # noqa + msg = "invalid manifest" + raise ValueError(msg) from exc + else: # is not str, dict, or path + msg = f"invalid manifest type {type(manifest)}" + raise ValueError(msg) + return manifest_ + + +def validate_manifest( + manifest: Union[str, dict, pathlib.Path], +) -> Union[WIPPPluginManifest, ComputeSchema]: + """Validate a plugin manifest against schema.""" + manifest = _load_manifest(manifest) + manifest["version"] = cast_version( + manifest["version"], + ) # cast version to semver object + if "name" in manifest: + name = manifest["name"] + else: + msg = f"{manifest} has no value for name" + raise InvalidManifestError(msg) + + if "pluginHardwareRequirements" in manifest: + # Parse the manifest + try: + plugin = ComputeSchema(**manifest) + except ValidationError as e: + msg = f"{name} does not conform to schema" + raise InvalidManifestError(msg) from e + except BaseException as e: + raise e + else: + # Parse the manifest + try: + plugin = WIPPPluginManifest(**manifest) + except ValidationError as e: + msg = f"{manifest['name']} does not conform to schema" + raise InvalidManifestError( + msg, + ) from e + except BaseException as e: + raise e + return plugin + + +def _scrape_manifests( + repo: Union[str, github.Repository.Repository], # type: ignore + gh: github.Github, + min_depth: int = 1, + max_depth: Optional[int] = None, + return_invalid: bool = False, +) -> Union[list, tuple[list, list]]: + if max_depth is None: + max_depth = min_depth + min_depth = 0 + + if not max_depth >= min_depth: + msg = "max_depth is smaller than min_depth" + raise ValueError(msg) + + if isinstance(repo, str): + repo = gh.get_repo(repo) + + contents = list(repo.get_contents("")) # type: ignore + next_contents: list = [] + valid_manifests: list = [] + invalid_manifests: list = [] + + for d in range(0, max_depth): + for content in tqdm(contents, desc=f"{repo.full_name}: {d}"): + if content.type == "dir": + next_contents.extend(repo.get_contents(content.path)) # type: ignore + elif content.name.endswith(".json") and d >= min_depth: + manifest = json.loads(content.decoded_content) + if is_valid_manifest(manifest): + valid_manifests.append(manifest) + else: + invalid_manifests.append(manifest) + + contents = next_contents.copy() + next_contents = [] + + if return_invalid: + return valid_manifests, invalid_manifests + return valid_manifests + + +def _error_log(val_err: ValidationError, manifest: dict, fct: str) -> None: + report = [] + + for error in val_err.args[0]: + if isinstance(error, list): + error = error[0] # noqa + + if isinstance(error, AssertionError): + msg = ( + f"The plugin ({manifest['name']}) " + "failed an assertion check: {err.args[0]}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + elif isinstance(error.exc, errors.MissingError): + msg = ( + f"The plugin ({manifest['name']}) " + "is missing fields: {err.loc_tuple()}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + elif errors.ExtraError: + if error.loc_tuple()[0] in ["inputs", "outputs", "ui"]: + manifest_ = manifest[error.loc_tuple()[0]][error.loc_tuple()[1]]["name"] + msg = ( + f"The plugin ({manifest['name']}) " + "had unexpected values in the " + f"{error.loc_tuple()[0]} " + f"({manifest_}): " + f"{error.exc.args[0][0].loc_tuple()}" + ) + report.append(msg) + else: + msg = ( + f"The plugin ({manifest['name']}) " + "had an error: {err.exc.args[0][0]}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + else: + str_val_err = str(val_err).replace("\n", ", ").replace(" ", " ") + msg = ( + f"{fct}: Uncaught manifest error in ({manifest['name']}): " + f"{str_val_err}" + ) + logger.warning(msg) diff --git a/src/polus/plugins/_plugins/manifests/manifest_utils_v2.py b/src/polus/plugins/_plugins/manifests/manifest_utils_v2.py new file mode 100644 index 000000000..9c37c111b --- /dev/null +++ b/src/polus/plugins/_plugins/manifests/manifest_utils_v2.py @@ -0,0 +1,202 @@ +"""Utilities for manifest parsing and validation.""" +import json +import logging +import pathlib +from typing import Optional +from typing import Union + +import github +import requests +import validators +from polus.plugins._plugins.models import ComputeSchema +from polus.plugins._plugins.models import WIPPPluginManifest +from pydantic import ValidationError +from pydantic import errors +from tqdm import tqdm + +logger = logging.getLogger("polus.plugins") + +# Fields that must be in a plugin manifest +REQUIRED_FIELDS = [ + "name", + "version", + "description", + "author", + "containerId", + "inputs", + "outputs", + "ui", +] + + +class InvalidManifestError(Exception): + """Raised when manifest has validation errors.""" + + +def is_valid_manifest(plugin: dict) -> bool: + """Validate basic attributes of a plugin manifest. + + Args: + plugin: A parsed plugin json file + + Returns: + True if the plugin has the minimal json fields + """ + fields = list(plugin.keys()) + + for field in REQUIRED_FIELDS: + if field not in fields: + msg = f"Missing json field, {field}, in plugin manifest." + logger.error(msg) + return False + return True + + +def _load_manifest(manifest: Union[str, dict, pathlib.Path]) -> dict: + """Return manifest as dict from str (url or path) or pathlib.Path.""" + if isinstance(manifest, dict): # is dict + return manifest + if isinstance(manifest, pathlib.Path): # is path + if manifest.suffix != ".json": + msg = "plugin manifest must be a json file with .json extension." + raise ValueError(msg) + + with manifest.open("r", encoding="utf-8") as manifest_json: + manifest_ = json.load(manifest_json) + elif isinstance(manifest, str): # is str + if validators.url(manifest): # is url + manifest_ = requests.get(manifest, timeout=10).json() + else: # could (and should) be path + try: + manifest_ = _load_manifest(pathlib.Path(manifest)) + except Exception as exc: # was not a Path? # noqa + msg = "invalid manifest" + raise ValueError(msg) from exc + else: # is not str, dict, or path + msg = f"invalid manifest type {type(manifest)}" + raise ValueError(msg) + return manifest_ + + +def validate_manifest( + manifest: Union[str, dict, pathlib.Path], +) -> Union[WIPPPluginManifest, ComputeSchema]: + """Validate a plugin manifest against schema.""" + manifest = _load_manifest(manifest) + if "name" in manifest: + name = manifest["name"] + else: + msg = f"{manifest} has no value for name" + raise InvalidManifestError(msg) + + if "pluginHardwareRequirements" in manifest: + # Parse the manifest + try: + plugin = ComputeSchema(**manifest) + except ValidationError as e: + msg = f"{name} does not conform to schema" + raise InvalidManifestError(msg) from e + except BaseException as e: + raise e + else: + # Parse the manifest + try: + plugin = WIPPPluginManifest(**manifest) + except ValidationError as e: + msg = f"{manifest['name']} does not conform to schema" + raise InvalidManifestError( + msg, + ) from e + except BaseException as e: + raise e + return plugin + + +def _scrape_manifests( + repo: Union[str, github.Repository.Repository], # type: ignore + gh: github.Github, + min_depth: int = 1, + max_depth: Optional[int] = None, + return_invalid: bool = False, +) -> Union[list, tuple[list, list]]: + if max_depth is None: + max_depth = min_depth + min_depth = 0 + + if not max_depth >= min_depth: + msg = "max_depth is smaller than min_depth" + raise ValueError(msg) + + if isinstance(repo, str): + repo = gh.get_repo(repo) + + contents = list(repo.get_contents("")) # type: ignore + next_contents: list = [] + valid_manifests: list = [] + invalid_manifests: list = [] + + for d in range(0, max_depth): + for content in tqdm(contents, desc=f"{repo.full_name}: {d}"): + if content.type == "dir": + next_contents.extend(repo.get_contents(content.path)) # type: ignore + elif content.name.endswith(".json") and d >= min_depth: + manifest = json.loads(content.decoded_content) + if is_valid_manifest(manifest): + valid_manifests.append(manifest) + else: + invalid_manifests.append(manifest) + + contents = next_contents.copy() + next_contents = [] + + if return_invalid: + return valid_manifests, invalid_manifests + return valid_manifests + + +def _error_log(val_err: ValidationError, manifest: dict, fct: str) -> None: + report = [] + + for error in val_err.args[0]: + if isinstance(error, list): + error = error[0] # noqa + + if isinstance(error, AssertionError): + msg = ( + f"The plugin ({manifest['name']}) " + "failed an assertion check: {err.args[0]}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + elif isinstance(error.exc, errors.MissingError): + msg = ( + f"The plugin ({manifest['name']}) " + "is missing fields: {err.loc_tuple()}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + elif errors.ExtraError: + if error.loc_tuple()[0] in ["inputs", "outputs", "ui"]: + manifest_ = manifest[error.loc_tuple()[0]][error.loc_tuple()[1]]["name"] + msg = ( + f"The plugin ({manifest['name']}) " + "had unexpected values in the " + f"{error.loc_tuple()[0]} " + f"({manifest_}): " + f"{error.exc.args[0][0].loc_tuple()}" + ) + report.append(msg) + else: + msg = ( + f"The plugin ({manifest['name']}) " + "had an error: {err.exc.args[0][0]}" + ) + report.append(msg) + logger.critical(f"{fct}: {report[-1]}") # pylint: disable=W1203 + else: + str_val_err = str(val_err).replace("\n", ", ").replace(" ", " ") + msg = ( + f"{fct}: Uncaught manifest error in ({manifest['name']}): " + f"{str_val_err}" + ) + logger.warning(msg) diff --git a/src/polus/plugins/_plugins/models/PolusComputeSchema.py b/src/polus/plugins/_plugins/models/PolusComputeSchema.py deleted file mode 100644 index ea7e1afb8..000000000 --- a/src/polus/plugins/_plugins/models/PolusComputeSchema.py +++ /dev/null @@ -1,136 +0,0 @@ -# generated by datamodel-codegen: -# filename: PolusComputeSchema.json -# timestamp: 2022-09-21T03:41:58+00:00 - -from __future__ import annotations - -from enum import Enum -from typing import Any, List, Optional, Union - -from pydantic import BaseModel, Field, constr - - -class Model(BaseModel): - __root__: Any - - -class PluginInputType(Enum): - path = "path" - string = "string" - number = "number" - array = "array" - boolean = "boolean" - - -class PluginInput(BaseModel): - format: Optional[str] = Field(None, title="Format") - label: Optional[str] = Field(None, title="Label") - name: str = Field(..., title="Name") - required: bool = Field(..., title="Required") - type: PluginInputType - default: Optional[Union[str, float, bool]] = Field(None, title="Default") - - -class PluginOutputType(Enum): - path = "path" - - -class PluginOutput(BaseModel): - format: Optional[str] = Field(None, title="Format") - label: Optional[str] = Field(None, title="Label") - name: str = Field(..., title="Name") - type: PluginOutputType - - -class GpuVendor(Enum): - none = "none" - amd = "amd" - tpu = "tpu" - nvidia = "nvidia" - - -class PluginHardwareRequirements(BaseModel): - coresMax: Optional[Union[str, float]] = Field(None, title="Coresmax") - coresMin: Optional[Union[str, float]] = Field(None, title="Coresmin") - cpuAVX: Optional[bool] = Field(None, title="Cpuavx") - cpuAVX2: Optional[bool] = Field(None, title="Cpuavx2") - cpuMin: Optional[str] = Field(None, title="Cpumin") - gpu: Optional[GpuVendor] = None - gpuCount: Optional[float] = Field(None, title="Gpucount") - gpuDriverVersion: Optional[str] = Field(None, title="Gpudriverversion") - gpuType: Optional[str] = Field(None, title="Gputype") - outDirMax: Optional[Union[str, float]] = Field(None, title="Outdirmax") - outDirMin: Optional[Union[str, float]] = Field(None, title="Outdirmin") - ramMax: Optional[Union[str, float]] = Field(None, title="Rammax") - ramMin: Optional[Union[str, float]] = Field(None, title="Rammin") - tmpDirMax: Optional[Union[str, float]] = Field(None, title="Tmpdirmax") - tmpDirMin: Optional[Union[str, float]] = Field(None, title="Tmpdirmin") - - -class ThenEntry(BaseModel): - action: str = Field(..., title="Action") - input: str = Field(..., title="Input") - value: str = Field(..., title="Value") - - -class ConditionEntry(BaseModel): - expression: str = Field(..., title="Expression") - - -class Validator(BaseModel): - then: Optional[List[ThenEntry]] = Field(None, title="Then") - validator: Optional[List[ConditionEntry]] = Field(None, title="Validator") - - -class PluginUIType(Enum): - checkbox = "checkbox" - color = "color" - date = "date" - email = "email" - number = "number" - password = "password" - radio = "radio" - range = "range" - text = "text" - time = "time" - - -class PluginUIInput(BaseModel): - bind: Optional[str] = Field(None, title="Bind") - condition: Optional[Union[List[Validator], str]] = Field(None, title="Condition") - default: Optional[Union[str, float, bool]] = Field(None, title="Default") - description: Optional[str] = Field(None, title="Description") - fieldset: Optional[List[str]] = Field(None, title="Fieldset") - hidden: Optional[bool] = Field(None, title="Hidden") - key: str = Field(..., title="Key") - title: str = Field(..., title="Title") - type: PluginUIType - - -class PluginUIOutput(BaseModel): - description: str = Field(..., title="Description") - format: Optional[str] = Field(None, title="Format") - name: str = Field(..., title="Name") - type: PluginUIType - website: Optional[str] = Field(None, title="Website") - - -class PluginSchema(BaseModel): - author: Optional[str] = Field(None, title="Author") - baseCommand: Optional[List[str]] = Field(None, title="Basecommand") - citation: Optional[str] = Field(None, title="Citation") - containerId: str = Field(..., title="Containerid") - customInputs: Optional[bool] = Field(None, title="Custominputs") - description: str = Field(..., title="Description") - inputs: List[PluginInput] = Field(..., title="Inputs") - institution: Optional[str] = Field(None, title="Institution") - name: str = Field(..., title="Name") - outputs: List[PluginOutput] = Field(..., title="Outputs") - pluginHardwareRequirements: PluginHardwareRequirements - repository: Optional[str] = Field(None, title="Repository") - title: str = Field(..., title="Title") - ui: List[Union[PluginUIInput, PluginUIOutput]] = Field(..., title="Ui") - version: constr( - regex=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" - ) = Field(..., examples=["0.1.0", "0.1.0rc1"], title="Version") - website: Optional[str] = Field(None, title="Website") diff --git a/src/polus/plugins/_plugins/models/__init__.py b/src/polus/plugins/_plugins/models/__init__.py index ce1d984dd..9b9092c75 100644 --- a/src/polus/plugins/_plugins/models/__init__.py +++ b/src/polus/plugins/_plugins/models/__init__.py @@ -1,10 +1,31 @@ """Pydantic Models based on JSON schemas.""" -from polus.plugins._plugins.models.compute import PluginSchema as ComputeSchema -from polus.plugins._plugins.models.PolusComputeSchema import ( - PluginUIInput, - PluginUIOutput, -) -from polus.plugins._plugins.models.wipp import WIPPPluginManifest + +import pydantic + +PYDANTIC_VERSION = pydantic.__version__ + +if PYDANTIC_VERSION.split(".")[0] == "1": + from polus.plugins._plugins.models.pydanticv1.compute import ( + PluginSchema as ComputeSchema, + ) + from polus.plugins._plugins.models.pydanticv1.PolusComputeSchema import ( + PluginUIInput, + ) + from polus.plugins._plugins.models.pydanticv1.PolusComputeSchema import ( + PluginUIOutput, + ) + from polus.plugins._plugins.models.pydanticv1.wipp import WIPPPluginManifest +elif PYDANTIC_VERSION.split(".")[0] == "2": + from polus.plugins._plugins.models.pydanticv2.compute import ( + PluginSchema as ComputeSchema, + ) + from polus.plugins._plugins.models.pydanticv2.PolusComputeSchema import ( + PluginUIInput, + ) + from polus.plugins._plugins.models.pydanticv2.PolusComputeSchema import ( + PluginUIOutput, + ) + from polus.plugins._plugins.models.pydanticv2.wipp import WIPPPluginManifest __all__ = [ "WIPPPluginManifest", diff --git a/src/polus/plugins/_plugins/models/pydanticv1/PolusComputeSchema.py b/src/polus/plugins/_plugins/models/pydanticv1/PolusComputeSchema.py new file mode 100644 index 000000000..a40b5b402 --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv1/PolusComputeSchema.py @@ -0,0 +1,137 @@ +# generated by datamodel-codegen: +# timestamp: 2022-09-21T03:41:58+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Any + +from pydantic import BaseModel +from pydantic import Field +from pydantic import constr + + +class Model(BaseModel): + __root__: Any + + +class PluginInputType(Enum): + path = "path" + string = "string" + number = "number" + array = "array" + boolean = "boolean" + + +class PluginInput(BaseModel): + format: str | None = Field(None, title="Format") + label: str | None = Field(None, title="Label") + name: str = Field(..., title="Name") + required: bool = Field(..., title="Required") + type: PluginInputType + default: str | float | bool | None = Field(None, title="Default") + + +class PluginOutputType(Enum): + path = "path" + + +class PluginOutput(BaseModel): + format: str | None = Field(None, title="Format") + label: str | None = Field(None, title="Label") + name: str = Field(..., title="Name") + type: PluginOutputType + + +class GpuVendor(Enum): + none = "none" + amd = "amd" + tpu = "tpu" + nvidia = "nvidia" + + +class PluginHardwareRequirements(BaseModel): + coresMax: str | float | None = Field(None, title="Coresmax") + coresMin: str | float | None = Field(None, title="Coresmin") + cpuAVX: bool | None = Field(None, title="Cpuavx") + cpuAVX2: bool | None = Field(None, title="Cpuavx2") + cpuMin: str | None = Field(None, title="Cpumin") + gpu: GpuVendor | None = None + gpuCount: float | None = Field(None, title="Gpucount") + gpuDriverVersion: str | None = Field(None, title="Gpudriverversion") + gpuType: str | None = Field(None, title="Gputype") + outDirMax: str | float | None = Field(None, title="Outdirmax") + outDirMin: str | float | None = Field(None, title="Outdirmin") + ramMax: str | float | None = Field(None, title="Rammax") + ramMin: str | float | None = Field(None, title="Rammin") + tmpDirMax: str | float | None = Field(None, title="Tmpdirmax") + tmpDirMin: str | float | None = Field(None, title="Tmpdirmin") + + +class ThenEntry(BaseModel): + action: str = Field(..., title="Action") + input: str = Field(..., title="Input") + value: str = Field(..., title="Value") + + +class ConditionEntry(BaseModel): + expression: str = Field(..., title="Expression") + + +class Validator(BaseModel): + then: list[ThenEntry] | None = Field(None, title="Then") + validator: list[ConditionEntry] | None = Field(None, title="Validator") + + +class PluginUIType(Enum): + checkbox = "checkbox" + color = "color" + date = "date" + email = "email" + number = "number" + password = "password" + radio = "radio" + range = "range" + text = "text" + time = "time" + + +class PluginUIInput(BaseModel): + bind: str | None = Field(None, title="Bind") + condition: list[Validator] | str | None = Field(None, title="Condition") + default: str | float | bool | None = Field(None, title="Default") + description: str | None = Field(None, title="Description") + fieldset: list[str] | None = Field(None, title="Fieldset") + hidden: bool | None = Field(None, title="Hidden") + key: str = Field(..., title="Key") + title: str = Field(..., title="Title") + type: PluginUIType + + +class PluginUIOutput(BaseModel): + description: str = Field(..., title="Description") + format: str | None = Field(None, title="Format") + name: str = Field(..., title="Name") + type: PluginUIType + website: str | None = Field(None, title="Website") + + +class PluginSchema(BaseModel): + author: str | None = Field(None, title="Author") + baseCommand: list[str] | None = Field(None, title="Basecommand") + citation: str | None = Field(None, title="Citation") + containerId: str = Field(..., title="Containerid") + customInputs: bool | None = Field(None, title="Custominputs") + description: str = Field(..., title="Description") + inputs: list[PluginInput] = Field(..., title="Inputs") + institution: str | None = Field(None, title="Institution") + name: str = Field(..., title="Name") + outputs: list[PluginOutput] = Field(..., title="Outputs") + pluginHardwareRequirements: PluginHardwareRequirements + repository: str | None = Field(None, title="Repository") + title: str = Field(..., title="Title") + ui: list[PluginUIInput | PluginUIOutput] = Field(..., title="Ui") + version: constr( + regex=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", + ) = Field(..., examples=["0.1.0", "0.1.0rc1"], title="Version") + website: str | None = Field(None, title="Website") diff --git a/src/polus/plugins/_plugins/models/WIPPPluginSchema.py b/src/polus/plugins/_plugins/models/pydanticv1/WIPPPluginSchema.py similarity index 69% rename from src/polus/plugins/_plugins/models/WIPPPluginSchema.py rename to src/polus/plugins/_plugins/models/pydanticv1/WIPPPluginSchema.py index 8f9e1eb2a..718d3a3fa 100644 --- a/src/polus/plugins/_plugins/models/WIPPPluginSchema.py +++ b/src/polus/plugins/_plugins/models/pydanticv1/WIPPPluginSchema.py @@ -1,13 +1,15 @@ # generated by datamodel-codegen: -# filename: wipp-plugin-manifest-schema.json # timestamp: 2023-01-04T14:54:38+00:00 from __future__ import annotations from enum import Enum -from typing import Any, List, Optional, Union +from typing import Any -from pydantic import AnyUrl, BaseModel, Field, constr +from pydantic import AnyUrl +from pydantic import BaseModel +from pydantic import Field +from pydantic import constr class Type(Enum): @@ -35,12 +37,16 @@ class Input(BaseModel): title="Input name", ) type: Type = Field( - ..., examples=["collection", "string", "number"], title="Input Type" + ..., + examples=["collection", "string", "number"], + title="Input Type", ) description: constr(regex=r"^(.*)$") = Field( - ..., examples=["Input Images"], title="Input description" + ..., + examples=["Input Images"], + title="Input description", ) - required: Optional[bool] = Field( + required: bool | None = Field( True, description="Whether an input is required or not", examples=[True], @@ -61,18 +67,24 @@ class Type1(Enum): class Output(BaseModel): name: constr(regex=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$") = Field( - ..., examples=["outputCollection"], title="Output name" + ..., + examples=["outputCollection"], + title="Output name", ) type: Type1 = Field( - ..., examples=["stitchingVector", "collection"], title="Output type" + ..., + examples=["stitchingVector", "collection"], + title="Output type", ) description: constr(regex=r"^(.*)$") = Field( - ..., examples=["Output collection"], title="Output description" + ..., + examples=["Output collection"], + title="Output description", ) class UiItem(BaseModel): - key: Union[Any, Any] = Field( + key: Any | Any = Field( ..., description="Key of the input which this UI definition applies to, the expected format is 'inputs.inputName'. Special keyword 'fieldsets' can be used to define arrangement of inputs by sections.", examples=["inputs.inputImages", "inputs.fileNamPattern", "fieldsets"], @@ -81,10 +93,12 @@ class UiItem(BaseModel): class CudaRequirements(BaseModel): - deviceMemoryMin: Optional[float] = Field( - 0, examples=[100], title="Minimum device memory" + deviceMemoryMin: float | None = Field( + 0, + examples=[100], + title="Minimum device memory", ) - cudaComputeCapability: Optional[Union[str, List[Any]]] = Field( + cudaComputeCapability: str | list[Any] | None = Field( None, description="Specify either a single minimum value, or an array of valid values", examples=["8.0", ["3.5", "5.0", "6.0", "7.0", "7.5", "8.0"]], @@ -93,26 +107,32 @@ class CudaRequirements(BaseModel): class ResourceRequirements(BaseModel): - ramMin: Optional[float] = Field( - None, examples=[2048], title="Minimum RAM in mebibytes (Mi)" + ramMin: float | None = Field( + None, + examples=[2048], + title="Minimum RAM in mebibytes (Mi)", ) - coresMin: Optional[float] = Field( - None, examples=[1], title="Minimum number of CPU cores" + coresMin: float | None = Field( + None, + examples=[1], + title="Minimum number of CPU cores", ) - cpuAVX: Optional[bool] = Field( + cpuAVX: bool | None = Field( False, examples=[True], title="Advanced Vector Extensions (AVX) CPU capability required", ) - cpuAVX2: Optional[bool] = Field( + cpuAVX2: bool | None = Field( False, examples=[False], title="Advanced Vector Extensions 2 (AVX2) CPU capability required", ) - gpu: Optional[bool] = Field( - False, examples=[True], title="GPU/accelerator required" + gpu: bool | None = Field( + False, + examples=[True], + title="GPU/accelerator required", ) - cudaRequirements: Optional[CudaRequirements] = Field( + cudaRequirements: CudaRequirements | None = Field( {}, examples=[{"deviceMemoryMin": 100, "cudaComputeCapability": "8.0"}], title="GPU Cuda-related requirements", @@ -143,26 +163,30 @@ class WippPluginManifest(BaseModel): examples=["Custom image segmentation plugin"], title="Short description of the plugin", ) - author: Optional[Optional[constr(regex=r"^(.*)$")]] = Field( - "", examples=["FirstName LastName"], title="Author(s)" + author: constr(regex="^(.*)$") | None | None = Field( + "", + examples=["FirstName LastName"], + title="Author(s)", ) - institution: Optional[Optional[constr(regex=r"^(.*)$")]] = Field( + institution: constr(regex="^(.*)$") | None | None = Field( "", examples=["National Institute of Standards and Technology"], title="Institution", ) - repository: Optional[Optional[AnyUrl]] = Field( + repository: AnyUrl | None | None = Field( "", examples=["https://github.com/usnistgov/WIPP"], title="Source code repository", ) - website: Optional[Optional[AnyUrl]] = Field( - "", examples=["http://usnistgov.github.io/WIPP"], title="Website" + website: AnyUrl | None | None = Field( + "", + examples=["http://usnistgov.github.io/WIPP"], + title="Website", ) - citation: Optional[Optional[constr(regex=r"^(.*)$")]] = Field( + citation: constr(regex="^(.*)$") | None | None = Field( "", examples=[ - "Peter Bajcsy, Joe Chalfoun, and Mylene Simon (2018). Web Microanalysis of Big Image Data. Springer-Verlag International" + "Peter Bajcsy, Joe Chalfoun, and Mylene Simon (2018). Web Microanalysis of Big Image Data. Springer-Verlag International", ], title="Citation", ) @@ -172,23 +196,25 @@ class WippPluginManifest(BaseModel): examples=["docker.io/wipp/plugin-example:1.0.0"], title="ContainerId", ) - baseCommand: Optional[List[str]] = Field( + baseCommand: list[str] | None = Field( None, description="Base command to use while running container image", examples=[["python3", "/opt/executable/main.py"]], title="Base command", ) - inputs: List[Input] = Field( + inputs: list[Input] = Field( ..., description="Defines inputs to the plugin", title="List of Inputs", unique_items=True, ) - outputs: List[Output] = Field( - ..., description="Defines the outputs of the plugin", title="List of Outputs" + outputs: list[Output] = Field( + ..., + description="Defines the outputs of the plugin", + title="List of Outputs", ) - ui: List[UiItem] = Field(..., title="Plugin form UI definition") - resourceRequirements: Optional[ResourceRequirements] = Field( + ui: list[UiItem] = Field(..., title="Plugin form UI definition") + resourceRequirements: ResourceRequirements | None = Field( {}, examples=[ { @@ -201,7 +227,7 @@ class WippPluginManifest(BaseModel): "deviceMemoryMin": 100, "cudaComputeCapability": "8.0", }, - } + }, ], title="Plugin Resource Requirements", ) diff --git a/src/polus/plugins/_plugins/models/pydanticv1/__init__.py b/src/polus/plugins/_plugins/models/pydanticv1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/polus/plugins/_plugins/models/compute.py b/src/polus/plugins/_plugins/models/pydanticv1/compute.py similarity index 57% rename from src/polus/plugins/_plugins/models/compute.py rename to src/polus/plugins/_plugins/models/pydanticv1/compute.py index 8584143c3..2a34a3ae7 100644 --- a/src/polus/plugins/_plugins/models/compute.py +++ b/src/polus/plugins/_plugins/models/pydanticv1/compute.py @@ -4,14 +4,12 @@ functions of PolusComputeSchema.py which is automatically generated by datamodel-codegen from JSON schema. """ -from typing import List -from polus.plugins._plugins.io import IOBase, Version -from polus.plugins._plugins.models.PolusComputeSchema import ( - PluginInput, - PluginOutput, - PluginSchema, -) +from polus.plugins._plugins.io import IOBase +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.models.pydanticv1.PolusComputeSchema import PluginInput +from polus.plugins._plugins.models.pydanticv1.PolusComputeSchema import PluginOutput +from polus.plugins._plugins.models.pydanticv1.PolusComputeSchema import PluginSchema class PluginInput(PluginInput, IOBase): # type: ignore @@ -25,6 +23,6 @@ class PluginOutput(PluginOutput, IOBase): # type: ignore class PluginSchema(PluginSchema): # type: ignore """Extended Compute Plugin Schema with extended IO defs.""" - inputs: List[PluginInput] - outputs: List[PluginOutput] + inputs: list[PluginInput] + outputs: list[PluginOutput] version: Version diff --git a/src/polus/plugins/_plugins/models/pydanticv1/wipp.py b/src/polus/plugins/_plugins/models/pydanticv1/wipp.py new file mode 100644 index 000000000..6557355a2 --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv1/wipp.py @@ -0,0 +1,79 @@ +"""Extending automatically generated wipp model. + +This file modifies and extend certain fields and +functions of WIPPPluginSchema.py which is automatically +generated by datamodel-codegen from JSON schema. +""" +from typing import Literal +from typing import Optional +from typing import Union + +from polus.plugins._plugins.io import Input +from polus.plugins._plugins.io import Output +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.models.pydanticv1.WIPPPluginSchema import ( + ResourceRequirements, +) +from polus.plugins._plugins.models.pydanticv1.WIPPPluginSchema import WippPluginManifest +from pydantic import BaseModel +from pydantic import Field + + +class UI1(BaseModel): + """Base class for UI items.""" + + key: str = Field(constr=r"^inputs.[a-zA-Z0-9][-a-zA-Z0-9]*$") + title: str + description: Optional[str] + condition: Optional[str] + default: Optional[Union[str, float, int, bool]] + hidden: Optional[bool] = Field(default=False) + bind: Optional[str] + + +class FieldSet(BaseModel): + """Base class for FieldSet.""" + + title: str + fields: list[str] = Field(min_items=1, unique_items=True) + + +class UI2(BaseModel): + """UI items class for fieldsets.""" + + key: Literal["fieldsets"] + fieldsets: list[FieldSet] = Field(min_items=1, unique_items=True) + + +class WIPPPluginManifest(WippPluginManifest): + """Extended WIPP Plugin Schema.""" + + inputs: list[Input] = Field( + ..., + description="Defines inputs to the plugin", + title="List of Inputs", + ) + outputs: list[Output] = Field( + ..., + description="Defines the outputs of the plugin", + title="List of Outputs", + ) + ui: list[Union[UI1, UI2]] = Field(..., title="Plugin form UI definition") + version: Version + resourceRequirements: Optional[ResourceRequirements] = Field( # noqa + None, + examples=[ + { + "ramMin": 2048, + "coresMin": 1, + "cpuAVX": True, + "cpuAVX2": False, + "gpu": True, + "cudaRequirements": { + "deviceMemoryMin": 100, + "cudaComputeCapability": "8.0", + }, + }, + ], + title="Plugin Resource Requirements", + ) diff --git a/src/polus/plugins/_plugins/models/pydanticv2/PolusComputeSchema.py b/src/polus/plugins/_plugins/models/pydanticv2/PolusComputeSchema.py new file mode 100644 index 000000000..d87a986fd --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv2/PolusComputeSchema.py @@ -0,0 +1,136 @@ +# generated by datamodel-codegen: edited by Camilo Velez +# timestamp: 2022-09-21T03:41:58+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated + +from pydantic import BaseModel +from pydantic import Field +from pydantic import StringConstraints + + +class PluginInputType(Enum): + path = "path" + string = "string" + number = "number" + array = "array" + boolean = "boolean" + + +class PluginInput(BaseModel): + format: str | None = Field(None, title="Format") + label: str | None = Field(None, title="Label") + name: str = Field(..., title="Name") + required: bool = Field(..., title="Required") + type: PluginInputType + default: str | float | bool | None = Field(None, title="Default") + + +class PluginOutputType(Enum): + path = "path" + + +class PluginOutput(BaseModel): + format: str | None = Field(None, title="Format") + label: str | None = Field(None, title="Label") + name: str = Field(..., title="Name") + type: PluginOutputType + + +class GpuVendor(Enum): + none = "none" + amd = "amd" + tpu = "tpu" + nvidia = "nvidia" + + +class PluginHardwareRequirements(BaseModel): + coresMax: str | float | None = Field(None, title="Coresmax") + coresMin: str | float | None = Field(None, title="Coresmin") + cpuAVX: bool | None = Field(None, title="Cpuavx") + cpuAVX2: bool | None = Field(None, title="Cpuavx2") + cpuMin: str | None = Field(None, title="Cpumin") + gpu: GpuVendor | None = None + gpuCount: float | None = Field(None, title="Gpucount") + gpuDriverVersion: str | None = Field(None, title="Gpudriverversion") + gpuType: str | None = Field(None, title="Gputype") + outDirMax: str | float | None = Field(None, title="Outdirmax") + outDirMin: str | float | None = Field(None, title="Outdirmin") + ramMax: str | float | None = Field(None, title="Rammax") + ramMin: str | float | None = Field(None, title="Rammin") + tmpDirMax: str | float | None = Field(None, title="Tmpdirmax") + tmpDirMin: str | float | None = Field(None, title="Tmpdirmin") + + +class ThenEntry(BaseModel): + action: str = Field(..., title="Action") + input: str = Field(..., title="Input") + value: str = Field(..., title="Value") + + +class ConditionEntry(BaseModel): + expression: str = Field(..., title="Expression") + + +class Validator(BaseModel): + then: list[ThenEntry] | None = Field(None, title="Then") + validator: list[ConditionEntry] | None = Field(None, title="Validator") + + +class PluginUIType(Enum): + checkbox = "checkbox" + color = "color" + date = "date" + email = "email" + number = "number" + password = "password" + radio = "radio" + range = "range" + text = "text" + time = "time" + + +class PluginUIInput(BaseModel): + bind: str | None = Field(None, title="Bind") + condition: list[Validator] | str | None = Field(None, title="Condition") + default: str | float | bool | None = Field(None, title="Default") + description: str | None = Field(None, title="Description") + fieldset: list[str] | None = Field(None, title="Fieldset") + hidden: bool | None = Field(None, title="Hidden") + key: str = Field(..., title="Key") + title: str = Field(..., title="Title") + type: PluginUIType + + +class PluginUIOutput(BaseModel): + description: str = Field(..., title="Description") + format: str | None = Field(None, title="Format") + name: str = Field(..., title="Name") + type: PluginUIType + website: str | None = Field(None, title="Website") + + +class PluginSchema(BaseModel): + author: str | None = Field(None, title="Author") + baseCommand: list[str] | None = Field(None, title="Basecommand") + citation: str | None = Field(None, title="Citation") + containerId: str = Field(..., title="Containerid") + customInputs: bool | None = Field(None, title="Custominputs") + description: str = Field(..., title="Description") + inputs: list[PluginInput] = Field(..., title="Inputs") + institution: str | None = Field(None, title="Institution") + name: str = Field(..., title="Name") + outputs: list[PluginOutput] = Field(..., title="Outputs") + pluginHardwareRequirements: PluginHardwareRequirements + repository: str | None = Field(None, title="Repository") + title: str = Field(..., title="Title") + ui: list[PluginUIInput | PluginUIOutput] = Field(..., title="Ui") + version: Annotated[ + str, + StringConstraints( + pattern=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", + ), + ] = Field(..., examples=["0.1.0", "0.1.0rc1"], title="Version") + website: str | None = Field(None, title="Website") diff --git a/src/polus/plugins/_plugins/models/pydanticv2/WIPPPluginSchema.py b/src/polus/plugins/_plugins/models/pydanticv2/WIPPPluginSchema.py new file mode 100644 index 000000000..099cb32d2 --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv2/WIPPPluginSchema.py @@ -0,0 +1,241 @@ +# generated by datamodel-codegen: edited by Camilo Velez +# timestamp: 2023-01-04T14:54:38+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Annotated +from typing import Any + +from pydantic import AnyUrl +from pydantic import BaseModel +from pydantic import Field +from pydantic import StringConstraints + + +class Type(Enum): + collection = "collection" + stitchingVector = "stitchingVector" + tensorflowModel = "tensorflowModel" + csvCollection = "csvCollection" + pyramid = "pyramid" + pyramidAnnotation = "pyramidAnnotation" + notebook = "notebook" + genericData = "genericData" + string = "string" + number = "number" + integer = "integer" + enum = "enum" + array = "array" + boolean = "boolean" + + +class Input(BaseModel): + name: Annotated[ + str, + StringConstraints(pattern=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$"), + ] = Field( + ..., + description="Input name as expected by the plugin CLI", + examples=["inputImages", "fileNamePattern", "thresholdValue"], + title="Input name", + ) + type: Type = Field( + ..., + examples=["collection", "string", "number"], + title="Input Type", + ) + description: Annotated[str, StringConstraints(pattern=r"^(.*)$")] = Field( + ..., + examples=["Input Images"], + title="Input description", + ) + required: bool | None = Field( + True, + description="Whether an input is required or not", + examples=[True], + title="Required input", + ) + + +class Type1(Enum): + collection = "collection" + stitchingVector = "stitchingVector" + tensorflowModel = "tensorflowModel" + tensorboardLogs = "tensorboardLogs" + csvCollection = "csvCollection" + pyramid = "pyramid" + pyramidAnnotation = "pyramidAnnotation" + genericData = "genericData" + + +class Output(BaseModel): + name: Annotated[ + str, + StringConstraints(pattern=r"^[a-zA-Z0-9][-a-zA-Z0-9]*$"), + ] = Field(..., examples=["outputCollection"], title="Output name") + type: Type1 = Field( + ..., + examples=["stitchingVector", "collection"], + title="Output type", + ) + description: Annotated[str, StringConstraints(pattern=r"^(.*)$")] = Field( + ..., + examples=["Output collection"], + title="Output description", + ) + + +class UiItem(BaseModel): + key: Any | Any = Field( + ..., + description="Key of the input which this UI definition applies to, the expected format is 'inputs.inputName'. Special keyword 'fieldsets' can be used to define arrangement of inputs by sections.", + examples=["inputs.inputImages", "inputs.fileNamPattern", "fieldsets"], + title="UI key", + ) + + +class CudaRequirements(BaseModel): + deviceMemoryMin: float | None = Field( + 0, + examples=[100], + title="Minimum device memory", + ) + cudaComputeCapability: str | list[Any] | None = Field( + None, + description="Specify either a single minimum value, or an array of valid values", + examples=["8.0", ["3.5", "5.0", "6.0", "7.0", "7.5", "8.0"]], + title="The cudaComputeCapability Schema", + ) + + +class ResourceRequirements(BaseModel): + ramMin: float | None = Field( + None, + examples=[2048], + title="Minimum RAM in mebibytes (Mi)", + ) + coresMin: float | None = Field( + None, + examples=[1], + title="Minimum number of CPU cores", + ) + cpuAVX: bool | None = Field( + False, + examples=[True], + title="Advanced Vector Extensions (AVX) CPU capability required", + ) + cpuAVX2: bool | None = Field( + False, + examples=[False], + title="Advanced Vector Extensions 2 (AVX2) CPU capability required", + ) + gpu: bool | None = Field( + False, + examples=[True], + title="GPU/accelerator required", + ) + cudaRequirements: CudaRequirements | None = Field( + {}, + examples=[{"deviceMemoryMin": 100, "cudaComputeCapability": "8.0"}], + title="GPU Cuda-related requirements", + ) + + +class WippPluginManifest(BaseModel): + name: Annotated[str, StringConstraints(pattern=r"^(.*)$", min_length=1)] = Field( + ..., + description="Name of the plugin (format: org/name)", + examples=["wipp/plugin-example"], + title="Plugin name", + ) + version: Annotated[str, StringConstraints(pattern=r"^(.*)$", min_length=1)] = Field( + ..., + description="Version of the plugin (semantic versioning preferred)", + examples=["1.0.0"], + title="Plugin version", + ) + title: Annotated[str, StringConstraints(pattern=r"^(.*)$", min_length=1)] = Field( + ..., + description="Plugin title to display in WIPP forms", + examples=["WIPP Plugin example"], + title="Plugin title", + ) + description: Annotated[ + str, + StringConstraints(pattern=r"^(.*)$", min_length=1), + ] = Field( + ..., + examples=["Custom image segmentation plugin"], + title="Short description of the plugin", + ) + author: Annotated[str, StringConstraints(pattern="^(.*)$")] | None | None = Field( + "", + examples=["FirstName LastName"], + title="Author(s)", + ) + institution: Annotated[ + str, + StringConstraints(pattern="^(.*)$"), + ] | None | None = Field( + "", + examples=["National Institute of Standards and Technology"], + title="Institution", + ) + repository: AnyUrl | None | None = Field( + "", + examples=["https://github.com/usnistgov/WIPP"], + title="Source code repository", + ) + website: AnyUrl | None | None = Field( + "", + examples=["http://usnistgov.github.io/WIPP"], + title="Website", + ) + citation: Annotated[str, StringConstraints(pattern="^(.*)$")] | None | None = Field( + "", + examples=[ + "Peter Bajcsy, Joe Chalfoun, and Mylene Simon (2018). Web Microanalysis of Big Image Data. Springer-Verlag International", + ], + title="Citation", + ) + containerId: Annotated[str, StringConstraints(pattern=r"^(.*)$")] = Field( + ..., + description="Docker image ID", + examples=["docker.io/wipp/plugin-example:1.0.0"], + title="ContainerId", + ) + baseCommand: list[str] | None = Field( + None, + description="Base command to use while running container image", + examples=[["python3", "/opt/executable/main.py"]], + title="Base command", + ) + inputs: set[Input] = Field( + ..., + description="Defines inputs to the plugin", + title="List of Inputs", + ) + outputs: list[Output] = Field( + ..., + description="Defines the outputs of the plugin", + title="List of Outputs", + ) + ui: list[UiItem] = Field(..., title="Plugin form UI definition") + resourceRequirements: ResourceRequirements | None = Field( + {}, + examples=[ + { + "ramMin": 2048, + "coresMin": 1, + "cpuAVX": True, + "cpuAVX2": False, + "gpu": True, + "cudaRequirements": { + "deviceMemoryMin": 100, + "cudaComputeCapability": "8.0", + }, + }, + ], + title="Plugin Resource Requirements", + ) diff --git a/src/polus/plugins/_plugins/models/pydanticv2/__init__.py b/src/polus/plugins/_plugins/models/pydanticv2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/polus/plugins/_plugins/models/pydanticv2/compute.py b/src/polus/plugins/_plugins/models/pydanticv2/compute.py new file mode 100644 index 000000000..250574f12 --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv2/compute.py @@ -0,0 +1,28 @@ +"""Extending automatically generated compute model. + +This file modifies and extend certain fields and +functions of PolusComputeSchema.py which is automatically +generated by datamodel-codegen from JSON schema. +""" + +from polus.plugins._plugins.io import IOBase +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.models.pydanticv2.PolusComputeSchema import PluginInput +from polus.plugins._plugins.models.pydanticv2.PolusComputeSchema import PluginOutput +from polus.plugins._plugins.models.pydanticv2.PolusComputeSchema import PluginSchema + + +class PluginInput(PluginInput, IOBase): # type: ignore + """Base Class for Input Args.""" + + +class PluginOutput(PluginOutput, IOBase): # type: ignore + """Base Class for Output Args.""" + + +class PluginSchema(PluginSchema): # type: ignore + """Extended Compute Plugin Schema with extended IO defs.""" + + inputs: list[PluginInput] + outputs: list[PluginOutput] + version: Version diff --git a/src/polus/plugins/_plugins/models/pydanticv2/wipp.py b/src/polus/plugins/_plugins/models/pydanticv2/wipp.py new file mode 100644 index 000000000..41c0dff31 --- /dev/null +++ b/src/polus/plugins/_plugins/models/pydanticv2/wipp.py @@ -0,0 +1,79 @@ +"""Extending automatically generated wipp model. + +This file modifies and extend certain fields and +functions of WIPPPluginSchema.py which is automatically +generated by datamodel-codegen from JSON schema. +""" +from typing import Literal +from typing import Optional +from typing import Union + +from polus.plugins._plugins.io import Input +from polus.plugins._plugins.io import Output +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.models.pydanticv2.WIPPPluginSchema import ( + ResourceRequirements, +) +from polus.plugins._plugins.models.pydanticv2.WIPPPluginSchema import WippPluginManifest +from pydantic import BaseModel +from pydantic import Field + + +class UI1(BaseModel): + """Base class for UI items.""" + + key: str = Field(constr=r"^inputs.[a-zA-Z0-9][-a-zA-Z0-9]*$") + title: str + description: Optional[str] = None + condition: Optional[str] = None + default: Optional[Union[str, float, int, bool]] = None + hidden: Optional[bool] = Field(default=False) + bind: Optional[str] = None + + +class FieldSet(BaseModel): + """Base class for FieldSet.""" + + title: str + fields: set[str] = Field(min_length=1) + + +class UI2(BaseModel): + """UI items class for fieldsets.""" + + key: Literal["fieldsets"] + fieldsets: set[FieldSet] = Field(min_length=1) + + +class WIPPPluginManifest(WippPluginManifest): + """Extended WIPP Plugin Schema.""" + + inputs: list[Input] = Field( + ..., + description="Defines inputs to the plugin", + title="List of Inputs", + ) + outputs: list[Output] = Field( + ..., + description="Defines the outputs of the plugin", + title="List of Outputs", + ) + ui: list[Union[UI1, UI2]] = Field(..., title="Plugin form UI definition") + version: Version + resourceRequirements: Optional[ResourceRequirements] = Field( # noqa + None, + examples=[ + { + "ramMin": 2048, + "coresMin": 1, + "cpuAVX": True, + "cpuAVX2": False, + "gpu": True, + "cudaRequirements": { + "deviceMemoryMin": 100, + "cudaComputeCapability": "8.0", + }, + }, + ], + title="Plugin Resource Requirements", + ) diff --git a/src/polus/plugins/_plugins/models/wipp.py b/src/polus/plugins/_plugins/models/wipp.py deleted file mode 100644 index 619d615fb..000000000 --- a/src/polus/plugins/_plugins/models/wipp.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Extending automatically generated wipp model. - -This file modifies and extend certain fields and -functions of WIPPPluginSchema.py which is automatically -generated by datamodel-codegen from JSON schema. -""" -from typing import List, Literal, Optional, Union - -from pydantic import BaseModel, Field - -from polus.plugins._plugins.io import Input, Output, Version -from polus.plugins._plugins.models.WIPPPluginSchema import WippPluginManifest - - -class ui1(BaseModel): - """Base class for UI items.""" - - key: str = Field(constr=r"^inputs.[a-zA-Z0-9][-a-zA-Z0-9]*$") - title: str - description: Optional[str] - condition: Optional[str] - default: Optional[Union[str, float, int, bool]] - hidden: Optional[bool] = Field(default=False) - bind: Optional[str] - - -class FieldSet(BaseModel): - """Base class for FieldSet.""" - - title: str - fields: List[str] = Field(min_items=1, unique_items=True) - - -class ui2(BaseModel): - """UI items class for fieldsets.""" - - key: Literal["fieldsets"] - fieldsets: List[FieldSet] = Field(min_items=1, unique_items=True) - - -class WIPPPluginManifest(WippPluginManifest): - """Extended WIPP Plugin Schema.""" - - inputs: List[Input] = Field( - ..., description="Defines inputs to the plugin", title="List of Inputs" - ) - outputs: List[Output] = Field( - ..., description="Defines the outputs of the plugin", title="List of Outputs" - ) - ui: List[Union[ui1, ui2]] = Field(..., title="Plugin form UI definition") - version: Version diff --git a/src/polus/plugins/_plugins/update/__init__.py b/src/polus/plugins/_plugins/update/__init__.py new file mode 100644 index 000000000..ff8010c6e --- /dev/null +++ b/src/polus/plugins/_plugins/update/__init__.py @@ -0,0 +1,14 @@ +"""Initialize update module.""" + +import pydantic + +PYDANTIC_VERSION = pydantic.__version__ + +if PYDANTIC_VERSION.split(".")[0] == "1": + from polus.plugins._plugins.update.update_v1 import update_nist_plugins + from polus.plugins._plugins.update.update_v1 import update_polus_plugins +elif PYDANTIC_VERSION.split(".")[0] == "2": + from polus.plugins._plugins.update.update_v2 import update_nist_plugins + from polus.plugins._plugins.update.update_v2 import update_polus_plugins + +__all__ = ["update_polus_plugins", "update_nist_plugins"] diff --git a/src/polus/plugins/_plugins/update.py b/src/polus/plugins/_plugins/update/update_v1.py similarity index 57% rename from src/polus/plugins/_plugins/update.py rename to src/polus/plugins/_plugins/update/update_v1.py index 8651fb92d..9a0dc508f 100644 --- a/src/polus/plugins/_plugins/update.py +++ b/src/polus/plugins/_plugins/update/update_v1.py @@ -1,77 +1,79 @@ +# pylint: disable=W1203, W1201 import json import logging import re import typing -from pydantic import ValidationError -from tqdm import tqdm - -from polus.plugins._plugins.classes import refresh, submit_plugin +from polus.plugins._plugins.classes import refresh +from polus.plugins._plugins.classes import submit_plugin from polus.plugins._plugins.gh import _init_github from polus.plugins._plugins.io import Version -from polus.plugins._plugins.manifests.manifest_utils import ( - _error_log, - _scrape_manifests, -) +from polus.plugins._plugins.manifests import _error_log +from polus.plugins._plugins.manifests import _scrape_manifests +from pydantic import ValidationError +from tqdm import tqdm logger = logging.getLogger("polus.plugins") def update_polus_plugins( - gh_auth: typing.Optional[str] = None, min_depth: int = 2, max_depth: int = 3 -): + gh_auth: typing.Optional[str] = None, + min_depth: int = 2, + max_depth: int = 3, +) -> None: """Scrape PolusAI GitHub repo and create local versions of Plugins.""" logger.info("Updating polus plugins.") # Get all manifests valid, invalid = _scrape_manifests( - "polusai/polus-plugins", _init_github(gh_auth), min_depth, max_depth, True + "polusai/polus-plugins", + _init_github(gh_auth), + min_depth, + max_depth, + True, ) manifests = valid.copy() manifests.extend(invalid) - logger.info("Submitting %s plugins." % len(manifests)) + logger.info(f"Submitting {len(manifests)} plugins.") for manifest in manifests: try: plugin = submit_plugin(manifest) - """ Parsing checks specific to polus-plugins """ + # Parsing checks specific to polus-plugins error_list = [] # Check that plugin version matches container version tag container_name, version = tuple(plugin.containerId.split(":")) version = Version(version=version) organization, container_name = tuple(container_name.split("/")) - try: - assert ( - plugin.version == version - ), f"containerId version ({version}) does not match plugin version ({plugin.version})" - except AssertionError as err: - error_list.append(err) + if plugin.version != version: + msg = ( + f"containerId version ({version}) does not " + f"match plugin version ({plugin.version})" + ) + logger.error(msg) + error_list.append(ValueError(msg)) # Check to see that the plugin is registered to Labshare - try: - assert organization in [ - "polusai", - "labshare", - ], "All polus plugin containers must be under the Labshare organization." - except AssertionError as err: - error_list.append(err) + if organization not in ["polusai", "labshare"]: + msg = ( + "all polus plugin containers must be" + " under the Labshare organization." + ) + logger.error(msg) + error_list.append(ValueError(msg)) # Checks for container name, they are somewhat related to our # Jenkins build - try: - assert container_name.startswith( - "polus" - ), "containerId name must begin with polus-" - except AssertionError as err: - error_list.append(err) + if not container_name.startswith("polus"): + msg = "containerId name must begin with polus-" + logger.error(msg) + error_list.append(ValueError(msg)) - try: - assert container_name.endswith( - "plugin" - ), "containerId name must end with -plugin" - except AssertionError as err: - error_list.append(err) + if not container_name.endswith("plugin"): + msg = "containerId name must end with -plugin" + logger.error(msg) + error_list.append(ValueError(msg)) if len(error_list) > 0: raise ValidationError(error_list, plugin.__class__) @@ -79,16 +81,14 @@ def update_polus_plugins( except ValidationError as val_err: try: _error_log(val_err, manifest, "update_polus_plugins") - except BaseException as e: - # logger.debug(f"There was an error {e} in {plugin.name}") + except BaseException as e: # pylint: disable=W0718 logger.exception(f"In {plugin.name}: {e}") - except BaseException as e: - # logger.debug(f"There was an error {e} in {plugin.name}") + except BaseException as e: # pylint: disable=W0718 logger.exception(f"In {plugin.name}: {e}") refresh() -def update_nist_plugins(gh_auth: typing.Optional[str] = None): +def update_nist_plugins(gh_auth: typing.Optional[str] = None) -> None: """Scrape NIST GitHub repo and create local versions of Plugins.""" # Parse README links gh = _init_github(gh_auth) @@ -96,7 +96,7 @@ def update_nist_plugins(gh_auth: typing.Optional[str] = None): contents = repo.get_contents("plugins") readme = [r for r in contents if r.name == "README.md"][0] pattern = re.compile( - r"\[manifest\]\((https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))\)" + r"\[manifest\]\((https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))\)", ) matches = pattern.findall(str(readme.decoded_content)) logger.info("Updating NIST plugins.") @@ -104,7 +104,7 @@ def update_nist_plugins(gh_auth: typing.Optional[str] = None): url_parts = match[0].split("/")[3:] plugin_repo = gh.get_repo("/".join(url_parts[:2])) manifest = json.loads( - plugin_repo.get_contents("/".join(url_parts[4:])).decoded_content + plugin_repo.get_contents("/".join(url_parts[4:])).decoded_content, ) try: diff --git a/src/polus/plugins/_plugins/update/update_v2.py b/src/polus/plugins/_plugins/update/update_v2.py new file mode 100644 index 000000000..67bd0038c --- /dev/null +++ b/src/polus/plugins/_plugins/update/update_v2.py @@ -0,0 +1,115 @@ +# pylint: disable=W1203, W1201 +import json +import logging +import re +import typing + +from polus.plugins._plugins.classes import refresh +from polus.plugins._plugins.classes import submit_plugin +from polus.plugins._plugins.gh import _init_github +from polus.plugins._plugins.io import Version +from polus.plugins._plugins.manifests import _error_log +from polus.plugins._plugins.manifests import _scrape_manifests +from pydantic import ValidationError +from tqdm import tqdm + +logger = logging.getLogger("polus.plugins") + + +def update_polus_plugins( + gh_auth: typing.Optional[str] = None, + min_depth: int = 2, + max_depth: int = 3, +) -> None: + """Scrape PolusAI GitHub repo and create local versions of Plugins.""" + logger.info("Updating polus plugins.") + # Get all manifests + valid, invalid = _scrape_manifests( + "polusai/polus-plugins", + _init_github(gh_auth), + min_depth, + max_depth, + True, + ) + manifests = valid.copy() + manifests.extend(invalid) + logger.info(f"Submitting {len(manifests)} plugins.") + + for manifest in manifests: + try: + plugin = submit_plugin(manifest) + + # Parsing checks specific to polus-plugins + error_list = [] + + # Check that plugin version matches container version tag + container_name, version = tuple(plugin.containerId.split(":")) + version = Version(version) + organization, container_name = tuple(container_name.split("/")) + if plugin.version != version: + msg = ( + f"containerId version ({version}) does not " + f"match plugin version ({plugin.version})" + ) + logger.error(msg) + error_list.append(ValueError(msg)) + + # Check to see that the plugin is registered to Labshare + if organization not in ["polusai", "labshare"]: + msg = ( + "all polus plugin containers must be" + " under the Labshare organization." + ) + logger.error(msg) + error_list.append(ValueError(msg)) + + # Checks for container name, they are somewhat related to our + # Jenkins build + if not container_name.startswith("polus"): + msg = "containerId name must begin with polus-" + logger.error(msg) + error_list.append(ValueError(msg)) + + if not container_name.endswith("plugin"): + msg = "containerId name must end with -plugin" + logger.error(msg) + error_list.append(ValueError(msg)) + + if len(error_list) > 0: + raise ValidationError(error_list, plugin.__class__) + + except ValidationError as val_err: + try: + _error_log(val_err, manifest, "update_polus_plugins") + except BaseException as e: # pylint: disable=W0718 + logger.exception(f"In {plugin.name}: {e}") + except BaseException as e: # pylint: disable=W0718 + logger.exception(f"In {plugin.name}: {e}") + refresh() + + +def update_nist_plugins(gh_auth: typing.Optional[str] = None) -> None: + """Scrape NIST GitHub repo and create local versions of Plugins.""" + # Parse README links + gh = _init_github(gh_auth) + repo = gh.get_repo("usnistgov/WIPP") + contents = repo.get_contents("plugins") + readme = [r for r in contents if r.name == "README.md"][0] + pattern = re.compile( + r"\[manifest\]\((https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))\)", + ) + matches = pattern.findall(str(readme.decoded_content)) + logger.info("Updating NIST plugins.") + for match in tqdm(matches, desc="NIST Manifests"): + url_parts = match[0].split("/")[3:] + plugin_repo = gh.get_repo("/".join(url_parts[:2])) + manifest = json.loads( + plugin_repo.get_contents("/".join(url_parts[4:])).decoded_content, + ) + + try: + submit_plugin(manifest) + + except ValidationError as val_err: + _error_log(val_err, manifest, "update_nist_plugins") + refresh() From 0a579d1cc414e3c80b461fd900b70af22e5c95fb Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Sun, 3 Dec 2023 23:43:16 -0500 Subject: [PATCH 46/47] fix: fixed load_config and save_config --- pyproject.toml | 1 - .../plugins/_plugins/classes/plugin_base.py | 7 --- .../_plugins/classes/plugin_classes_v1.py | 40 ++++++++-------- .../_plugins/classes/plugin_classes_v2.py | 48 +++++++++---------- src/polus/plugins/_plugins/io/io_v2.py | 10 ++-- 5 files changed, 52 insertions(+), 54 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 19152f7c4..c528efdf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" pytest-sugar = "^0.9.7" pytest-xdist = "^3.3.1" -python = ">=3.9, <3.12" pyyaml = "^6.0" ruff = "^0.0.274" tqdm = "^4.64.1" diff --git a/src/polus/plugins/_plugins/classes/plugin_base.py b/src/polus/plugins/_plugins/classes/plugin_base.py index ec9a18fe9..02d142b7f 100644 --- a/src/polus/plugins/_plugins/classes/plugin_base.py +++ b/src/polus/plugins/_plugins/classes/plugin_base.py @@ -167,13 +167,6 @@ def sig( ) print(docker_) # noqa - @property - def _config(self) -> dict: - model_ = self.dict() - for inp in model_["inputs"]: - inp["value"] = None - return model_ - @property def manifest(self) -> dict: """Plugin manifest.""" diff --git a/src/polus/plugins/_plugins/classes/plugin_classes_v1.py b/src/polus/plugins/_plugins/classes/plugin_classes_v1.py index c2808fadd..90e67ddcc 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes_v1.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes_v1.py @@ -1,5 +1,5 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" -# pylint: disable=W1203, enable=W1201 +# pylint: disable=W1203, W0212, enable=W1201 import json import logging import shutil @@ -85,6 +85,20 @@ def list_plugins() -> list: return output +def _get_config(plugin: Union["Plugin", "ComputePlugin"], class_: str) -> dict: + model_ = plugin.dict() + # iterate over I/O to convert to dict + for io_name, io in model_["_io_keys"].items(): + # overwrite val if enum + if io["type"] == "enum": + val_ = io["value"].name # mapDirectory.raw + model_["_io_keys"][io_name]["value"] = val_.split(".")[-1] # raw + for inp in model_["inputs"]: + inp["value"] = None + model_["class"] = class_ + return model_ + + class Plugin(WIPPPluginManifest, BasePlugin): """WIPP Plugin Class. @@ -170,16 +184,10 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" BasePlugin.__setattr__(self, name, value) - @property - def _config_file(self) -> dict: - config_ = self._config - config_["class"] = "WIPP" - return config_ - def save_config(self, path: Union[str, Path]) -> None: """Save manifest with configured I/O parameters to specified path.""" with Path(path).open("w", encoding="utf-8") as file: - json.dump(self._config_file, file, indent=4, default=str) + json.dump(_get_config(self, "WIPP"), file, indent=4, default=str) logger.debug(f"Saved config to {path}") def __repr__(self) -> str: @@ -281,12 +289,6 @@ def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(self.name)]) - @property - def _config_file(self) -> dict: - config_ = self._config - config_["class"] = "Compute" - return config_ - def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" BasePlugin.__setattr__(self, name, value) @@ -294,7 +296,7 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 def save_config(self, path: Union[str, Path]) -> None: """Save configured manifest with I/O parameters to specified path.""" with Path(path).open("w", encoding="utf-8") as file: - json.dump(self._config_file, file, indent=4) + json.dump(_get_config(self, "Compute"), file, indent=4, default=str) logger.debug(f"Saved config to {path}") def save_manifest(self, path: Union[str, Path]) -> None: @@ -383,15 +385,15 @@ def get_plugin( return _load_plugin(PLUGINS[name][Version(**{"version": version})]) -def load_config(config: Union[dict, Path]) -> Union[Plugin, ComputePlugin]: +def load_config(config: Union[dict, Path, str]) -> Union[Plugin, ComputePlugin]: """Load configured plugin from config file/dict.""" - if isinstance(config, Path): - with config.open("r", encoding="utf-8") as file: + if isinstance(config, (Path, str)): + with Path(config).open("r", encoding="utf-8") as file: manifest_ = json.load(file) elif isinstance(config, dict): manifest_ = config else: - msg = "config must be a dict or a path" + msg = "config must be a dict, str, or a path" raise TypeError(msg) io_keys_ = manifest_["_io_keys"] class_ = manifest_["class"] diff --git a/src/polus/plugins/_plugins/classes/plugin_classes_v2.py b/src/polus/plugins/_plugins/classes/plugin_classes_v2.py index e59352f24..5a2aadae2 100644 --- a/src/polus/plugins/_plugins/classes/plugin_classes_v2.py +++ b/src/polus/plugins/_plugins/classes/plugin_classes_v2.py @@ -1,5 +1,5 @@ """Classes for Plugin objects containing methods to configure, run, and save.""" -# pylint: disable=W1203, enable=W1201 +# pylint: disable=W1203, W0212, enable=W1201 import json import logging import shutil @@ -86,6 +86,21 @@ def list_plugins() -> list: return output +def _get_config(plugin: Union["Plugin", "ComputePlugin"], class_: str) -> dict: + model_ = json.loads(plugin.model_dump_json()) + model_["_io_keys"] = deepcopy(plugin._io_keys) # type: ignore + # iterate over I/O to convert to dict + for io_name, io in model_["_io_keys"].items(): + model_["_io_keys"][io_name] = json.loads(io.model_dump_json()) + # overwrite val if enum + if io.type.value == "enum": + model_["_io_keys"][io_name]["value"] = io.value.name # str + for inp in model_["inputs"]: + inp["value"] = None + model_["class"] = class_ + return model_ + + class Plugin(WIPPPluginManifest, BasePlugin): """WIPP Plugin Class. @@ -164,16 +179,10 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" BasePlugin.__setattr__(self, name, value) - @property - def _config_file(self) -> dict: - config_ = self._config - config_["class"] = "WIPP" - return config_ - def save_config(self, path: Union[str, Path]) -> None: """Save manifest with configured I/O parameters to specified path.""" with Path(path).open("w", encoding="utf-8") as file: - json.dump(self._config_file, file, indent=4, default=str) + json.dump(_get_config(self, "WIPP"), file, indent=4, default=str) logger.debug(f"Saved config to {path}") def __repr__(self) -> str: @@ -271,12 +280,6 @@ def versions(self) -> list: # cannot be in PluginMethods because PLUGINS lives """Return list of local versions of a Plugin.""" return list(PLUGINS[name_cleaner(self.name)]) - @property - def _config_file(self) -> dict: - config_ = self._config - config_["class"] = "Compute" - return config_ - def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 """Set I/O parameters as attributes.""" BasePlugin.__setattr__(self, name, value) @@ -284,7 +287,7 @@ def __setattr__(self, name: str, value: Any) -> None: # noqa: ANN401 def save_config(self, path: Union[str, Path]) -> None: """Save configured manifest with I/O parameters to specified path.""" with Path(path).open("w", encoding="utf-8") as file: - json.dump(self._config_file, file, indent=4) + json.dump(_get_config(self, "Compute"), file, indent=4, default=str) logger.debug(f"Saved config to {path}") def save_manifest(self, path: Union[str, Path]) -> None: @@ -369,18 +372,18 @@ def get_plugin( """ if version is None: return _load_plugin(PLUGINS[name][max(PLUGINS[name])]) - return _load_plugin(PLUGINS[name][Version(**{"version": version})]) + return _load_plugin(PLUGINS[name][Version(version)]) -def load_config(config: Union[dict, Path]) -> Union[Plugin, ComputePlugin]: +def load_config(config: Union[dict, Path, str]) -> Union[Plugin, ComputePlugin]: """Load configured plugin from config file/dict.""" - if isinstance(config, Path): - with config.open("r", encoding="utf-8") as file: + if isinstance(config, (Path, str)): + with Path(config).open("r", encoding="utf-8") as file: manifest_ = json.load(file) elif isinstance(config, dict): manifest_ = config else: - msg = "config must be a dict or a path" + msg = "config must be a dict, str, or a path" raise TypeError(msg) io_keys_ = manifest_["_io_keys"] class_ = manifest_["class"] @@ -409,10 +412,7 @@ def remove_plugin(plugin: str, version: Optional[Union[str, list[str]]] = None) for version_ in version: remove_plugin(plugin, version_) return - if not isinstance(version, Version): - version_ = cast_version(version) - else: - version_ = version + version_ = Version(version) if not isinstance(version, Version) else version path = PLUGINS[plugin][version_] path.unlink() refresh() diff --git a/src/polus/plugins/_plugins/io/io_v2.py b/src/polus/plugins/_plugins/io/io_v2.py index 846a8cd3e..81c42f2bc 100644 --- a/src/polus/plugins/_plugins/io/io_v2.py +++ b/src/polus/plugins/_plugins/io/io_v2.py @@ -334,6 +334,10 @@ def __hash__(self) -> int: """Needed to use Version objects as dict keys.""" return hash(self.root) + def __repr__(self) -> str: + """Return string representation of Version object.""" + return self.root + @Version.__eq__.register(Version) # pylint: disable=no-member def _(self, other): @@ -346,7 +350,7 @@ def _(self, other): @Version.__eq__.register(str) # pylint: disable=no-member def _(self, other): - return self == Version(**{"version": other}) + return self == Version(other) @Version.__lt__.register(Version) # pylint: disable=no-member @@ -366,7 +370,7 @@ def _(self, other): @Version.__lt__.register(str) # pylint: disable=no-member def _(self, other): - v = Version(**{"version": other}) + v = Version(other) return self < v @@ -377,7 +381,7 @@ def _(self, other): @Version.__gt__.register(str) # pylint: disable=no-member def _(self, other): - v = Version(**{"version": other}) + v = Version(other) return self > v From 7186daec0862a5a3291ff3a138c08242b121f3bf Mon Sep 17 00:00:00 2001 From: Camilo Velez Date: Sun, 3 Dec 2023 23:43:46 -0500 Subject: [PATCH 47/47] tests: updated tests to include pydantic v2 --- tests/test_cwl.py | 12 +++--- tests/test_io.py | 4 +- tests/test_manifests.py | 33 ++++++++------- tests/test_plugins.py | 20 +++++++-- tests/test_version.py | 91 ++++++++++++++++++++++++++++++++--------- 5 files changed, 116 insertions(+), 44 deletions(-) diff --git a/tests/test_cwl.py b/tests/test_cwl.py index 94fce2a4f..58ac9fd08 100644 --- a/tests/test_cwl.py +++ b/tests/test_cwl.py @@ -3,12 +3,14 @@ """Tests for CWL utils.""" from pathlib import Path +import pydantic import pytest import yaml import polus.plugins as pp -from polus.plugins._plugins.classes.plugin_methods import MissingInputValues +from polus.plugins._plugins.classes.plugin_base import MissingInputValuesError +PYDANTIC_VERSION = pydantic.__version__.split(".")[0] RSRC_PATH = Path(__file__).parent.joinpath("resources") OMECONVERTER = RSRC_PATH.joinpath("omeconverter030.json") @@ -20,7 +22,7 @@ def submit_plugin(): if "OmeConverter" not in pp.list: pp.submit_plugin(OMECONVERTER) else: - if "0.3.0" not in [x.version for x in pp.OmeConverter.versions]: + if "0.3.0" not in pp.OmeConverter.versions: pp.submit_plugin(OMECONVERTER) @@ -65,7 +67,7 @@ def test_save_read_cwl(plug, cwl_path): def test_save_cwl_io_not_inp(plug, cwl_io_path): """Test save_cwl IO.""" - with pytest.raises(MissingInputValues): + with pytest.raises(MissingInputValuesError): plug.save_cwl_io(cwl_io_path) @@ -73,7 +75,7 @@ def test_save_cwl_io_not_inp2(plug, cwl_io_path): """Test save_cwl IO.""" plug.inpDir = RSRC_PATH.absolute() plug.filePattern = "img_r{rrr}_c{ccc}.tif" - with pytest.raises(MissingInputValues): + with pytest.raises(MissingInputValuesError): plug.save_cwl_io(cwl_io_path) @@ -83,7 +85,7 @@ def test_save_cwl_io_not_yml(plug, cwl_io_path): plug.filePattern = "img_r{rrr}_c{ccc}.tif" plug.fileExtension = ".ome.zarr" plug.outDir = RSRC_PATH.absolute() - with pytest.raises(AssertionError): + with pytest.raises(ValueError): plug.save_cwl_io(cwl_io_path.with_suffix(".txt")) diff --git a/tests/test_io.py b/tests/test_io.py index f1135e504..83e289a6e 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -5,8 +5,8 @@ import pytest from fsspec.implementations.local import LocalFileSystem -from polus.plugins._plugins.classes.plugin_classes import _load_plugin -from polus.plugins._plugins.classes.plugin_methods import IOKeyError +from polus.plugins._plugins.classes import _load_plugin +from polus.plugins._plugins.classes.plugin_base import IOKeyError from polus.plugins._plugins.io import Input, IOBase RSRC_PATH = Path(__file__).parent.joinpath("resources") diff --git a/tests/test_manifests.py b/tests/test_manifests.py index dd4cccb7e..286e93dd6 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -3,15 +3,26 @@ from collections import OrderedDict from pathlib import Path +import pydantic import pytest -from polus.plugins._plugins.classes import list_plugins -from polus.plugins._plugins.classes.plugin_classes import PLUGINS -from polus.plugins._plugins.manifests.manifest_utils import ( - InvalidManifest, - _load_manifest, - validate_manifest, -) +PYDANTIC_VERSION = pydantic.__version__.split(".")[0] + +from polus.plugins._plugins.classes import PLUGINS, list_plugins + +if PYDANTIC_VERSION == "1": + from polus.plugins._plugins.manifests.manifest_utils_v1 import ( + InvalidManifestError, + _load_manifest, + validate_manifest, + ) +elif PYDANTIC_VERSION == "2": + from polus.plugins._plugins.manifests.manifest_utils_v2 import ( + InvalidManifestError, + _load_manifest, + validate_manifest, + ) + from polus.plugins._plugins.models import ComputeSchema, WIPPPluginManifest RSRC_PATH = Path(__file__).parent.joinpath("resources") @@ -219,12 +230,6 @@ def test_load_manifest(type_): # test path and dict assert _load_manifest(type_) == d_val -def test_load_manifest_str(): - """Test _load_manifest() for str.""" - st_ = """{"a": 2, "b": "Polus"}""" - assert _load_manifest(st_) == {"a": 2, "b": "Polus"} - - bad = [f"b{x}.json" for x in [1, 2, 3]] good = [f"g{x}.json" for x in [1, 2, 3]] @@ -232,7 +237,7 @@ def test_load_manifest_str(): @pytest.mark.parametrize("manifest", bad, ids=bad) def test_bad_manifest(manifest): """Test bad manifests raise InvalidManifest error.""" - with pytest.raises(InvalidManifest): + with pytest.raises(InvalidManifestError): validate_manifest(REPO_PATH.joinpath("tests", "resources", manifest)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 302759940..4a0d60ca1 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -6,7 +6,7 @@ import pytest import polus.plugins as pp -from polus.plugins._plugins.classes.plugin_classes import Plugin, _load_plugin +from polus.plugins._plugins.classes import Plugin, _load_plugin RSRC_PATH = Path(__file__).parent.joinpath("resources") OMECONVERTER = RSRC_PATH.joinpath("omeconverter022.json") @@ -101,9 +101,7 @@ def test_attr2(submit_basic131): def test_versions(submit_basic131, submit_basic127): """Test versions.""" - assert sorted( - [x for x in pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions] - ) == [ + assert sorted(pp.get_plugin("BasicFlatfieldCorrectionPlugin").versions) == [ "1.2.7", "1.3.1", ] @@ -141,6 +139,20 @@ def test_remove_all_versions_plugin( assert pp.list == ["OmeConverter"] +def test_submit_str_1(): + """Test submit_plugin with string.""" + pp.remove_all() + pp.submit_plugin(str(OMECONVERTER)) + assert pp.list == ["OmeConverter"] + + +def test_submit_str_2(): + """Test submit_plugin with string.""" + pp.remove_all() + pp.submit_plugin(str(OMECONVERTER.absolute())) + assert pp.list == ["OmeConverter"] + + @pytest.fixture def plug1(): """Configure the class.""" diff --git a/tests/test_version.py b/tests/test_version.py index 2a10f0c63..512bdc420 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,10 +1,13 @@ """Test Version object and cast_version utility function.""" +import pydantic import pytest from pydantic import ValidationError from polus.plugins._plugins.io import Version from polus.plugins._plugins.utils import cast_version +PYDANTIC_VERSION = pydantic.__version__.split(".", maxsplit=1)[0] + GOOD_VERSIONS = [ "1.2.3", "1.4.7-rc1", @@ -17,13 +20,19 @@ ] BAD_VERSIONS = ["02.2.3", "002.2.3", "1.2", "1.0", "1.03.2", "23.3.03", "d.2.4"] +PV = PYDANTIC_VERSION +print(PV) + @pytest.mark.parametrize("ver", GOOD_VERSIONS, ids=GOOD_VERSIONS) def test_version(ver): """Test Version pydantic model.""" - Version(version=ver) + if PV == "1": + assert isinstance(Version(version=ver), Version) + assert isinstance(Version(ver), Version) +@pytest.mark.skipif(int(PV) > 1, reason="requires pydantic 1") @pytest.mark.parametrize("ver", GOOD_VERSIONS, ids=GOOD_VERSIONS) def test_cast_version(ver): """Test cast_version utility function.""" @@ -33,8 +42,11 @@ def test_cast_version(ver): @pytest.mark.parametrize("ver", BAD_VERSIONS, ids=BAD_VERSIONS) def test_bad_version1(ver): """Test ValidationError is raised for invalid versions.""" + if PV == "1": + with pytest.raises(ValidationError): + assert isinstance(cast_version(ver), Version) with pytest.raises(ValidationError): - assert isinstance(cast_version(ver), Version) + assert isinstance(Version(ver), Version) MAJOR_VERSION_EQUAL = ["2.4.3", "2.98.28", "2.1.2", "2.0.0", "2.4.0"] @@ -45,74 +57,115 @@ def test_bad_version1(ver): @pytest.mark.parametrize("ver", MAJOR_VERSION_EQUAL, ids=MAJOR_VERSION_EQUAL) def test_major(ver): """Test major version.""" - assert cast_version(ver).major == 2 + if PV == "2": + assert Version(ver).major == 2 + else: + assert cast_version(ver).major == 2 @pytest.mark.parametrize("ver", MINOR_VERSION_EQUAL, ids=MINOR_VERSION_EQUAL) def test_minor(ver): """Test minor version.""" - assert cast_version(ver).minor == 3 + if PV == "2": + assert Version(ver).minor == 3 + else: + assert cast_version(ver).minor == 3 @pytest.mark.parametrize("ver", PATCH_EQUAL, ids=PATCH_EQUAL) def test_patch(ver): """Test patch version.""" - assert cast_version(ver).patch == 7 - + if PV == "2": + assert Version(ver).patch == 7 + else: + assert cast_version(ver).patch == 7 def test_gt1(): """Test greater than operator.""" - assert cast_version("1.2.3") > cast_version("1.2.1") + if PV == "2": + assert Version("1.2.3") > Version("1.2.1") + else: + assert cast_version("1.2.3") > cast_version("1.2.1") def test_gt2(): """Test greater than operator.""" - assert cast_version("5.7.3") > cast_version("5.6.3") + if PV == "2": + assert Version("5.7.3") > Version("5.6.3") + else: + assert cast_version("5.7.3") > cast_version("5.6.3") def test_st1(): """Test less than operator.""" - assert cast_version("5.7.3") < cast_version("5.7.31") + if PV == "2": + assert Version("5.7.3") < Version("5.7.31") + else: + assert cast_version("5.7.3") < cast_version("5.7.31") def test_st2(): """Test less than operator.""" - assert cast_version("1.0.2") < cast_version("2.0.2") + if PV == "2": + assert Version("1.0.2") < Version("2.0.2") + else: + assert cast_version("1.0.2") < cast_version("2.0.2") def test_eq1(): """Test equality operator.""" - assert Version(version="1.3.3") == cast_version("1.3.3") + if PV == "2": + assert Version("1.3.3") == Version("1.3.3") + else: + assert Version(version="1.3.3") == cast_version("1.3.3") def test_eq2(): """Test equality operator.""" - assert Version(version="5.4.3") == cast_version("5.4.3") + if PV == "2": + assert Version("5.4.3") == Version("5.4.3") + else: + assert Version(version="5.4.3") == cast_version("5.4.3") def test_eq3(): """Test equality operator.""" - assert Version(version="1.3.3") != cast_version("1.3.8") + if PV == "2": + assert Version("1.3.3") != Version("1.3.8") + else: + assert Version(version="1.3.3") != cast_version("1.3.8") def test_eq_str1(): """Test equality with str.""" - assert Version(version="1.3.3") == "1.3.3" + if PV == "2": + assert Version("1.3.3") == "1.3.3" + else: + assert Version(version="1.3.3") == "1.3.3" def test_lt_str1(): """Test equality with str.""" - assert Version(version="1.3.3") < "1.5.3" + if PV == "2": + assert Version("1.3.3") < "1.5.3" + else: + assert Version(version="1.3.3") < "1.5.3" def test_gt_str1(): """Test equality with str.""" - assert Version(version="4.5.10") > "4.5.9" + if PV == "2": + assert Version("4.5.10") > "4.5.9" + else: + assert Version(version="4.5.10") > "4.5.9" def test_eq_no_str(): """Test equality with non-string.""" - with pytest.raises(TypeError): - assert Version(version="1.3.3") == 1.3 - + if PV == "2": + with pytest.raises(TypeError): + assert Version("1.3.3") == 1.3 + else: + with pytest.raises(TypeError): + assert Version(version="1.3.3") == 1.3