From 72385ec7b519deaeac07a8b347a47a71f48aa81f Mon Sep 17 00:00:00 2001 From: Nat Noordanus Date: Sun, 19 Nov 2023 15:53:19 +0100 Subject: [PATCH] Major refactor of config classes a and config include logic **BREAKING CHANGE:** included config now loads envfiles relative to the cwd specified on the include option (if any). New/updated special env vars: - POE_CONF_DIR: the path to the dir containing the config file that defines the running task or the cwd set when including the config - POE_DEBUG makes poe print more stuff to help with debugging Refactor specifics: - Introduce PoeOptions class for deserializing config. I would have used pydantic but it's too heavy for use in a CLI tool like this. - Keep config from different files separate within PoeConfig - Collect config validation logic in the config classes and declare config types in more detail - Make error reporting more structured and consistent Also: - Fixed bug that caused some task validation to be skipped for tasks with args - Improve consistency and coverage for config validation - Fix crash in switch with args naked string for control - PoeConfig.load will load everything again if called a second time - remove pylint from the project - Enable ANSI color output by default inside GitHub Actions --- .gitignore | 1 + .pylintrc | 364 ------- docs/guides/include_guide.rst | 13 +- poethepoet/__init__.py | 6 +- poethepoet/app.py | 40 +- poethepoet/config.py | 586 +++++++---- poethepoet/context.py | 31 +- poethepoet/env/cache.py | 31 +- poethepoet/env/manager.py | 88 +- poethepoet/exceptions.py | 25 + poethepoet/executor/base.py | 59 +- poethepoet/executor/poetry.py | 6 +- poethepoet/executor/virtualenv.py | 24 +- poethepoet/helpers/command/ast.py | 10 - poethepoet/options.py | 270 +++++ poethepoet/plugin.py | 18 +- poethepoet/task/__init__.py | 2 - poethepoet/task/args.py | 352 ++++--- poethepoet/task/base.py | 670 +++++++------ poethepoet/task/cmd.py | 50 +- poethepoet/task/expr.py | 66 +- poethepoet/task/ref.py | 90 +- poethepoet/task/script.py | 70 +- poethepoet/task/sequence.py | 186 ++-- poethepoet/task/shell.py | 83 +- poethepoet/task/switch.py | 287 +++--- poethepoet/ui.py | 68 +- poetry.lock | 929 +++++++----------- pyproject.toml | 15 +- tests/conftest.py | 1 + tests/fixtures/cwd_project/pyproject.toml | 2 +- tests/fixtures/envfile_project/pyproject.toml | 4 +- tests/fixtures/monorepo_project/env1 | 1 + tests/fixtures/monorepo_project/env1t | 1 + tests/fixtures/monorepo_project/env2 | 1 + tests/fixtures/monorepo_project/env2t | 1 + .../fixtures/monorepo_project/pyproject.toml | 6 + .../monorepo_project/subproject_3/env3 | 1 + .../monorepo_project/subproject_3/env3t | 1 + .../subproject_3/pyproject.toml | 22 + .../monorepo_project/subproject_4/env3 | 1 + .../monorepo_project/subproject_4/env3t | 1 + .../subproject_4/exec_dir/env0 | 1 + .../subproject_4/exec_dir/env0t | 1 + .../subproject_4/pyproject.toml | 23 + tests/test_cmd_tasks.py | 2 +- tests/test_ignore_fail.py | 8 +- tests/test_includes.py | 130 ++- tests/test_poetry_plugin.py | 16 + tests/test_script_tasks.py | 14 +- tests/test_shell_task.py | 7 +- tests/test_switch_task.py | 5 +- 52 files changed, 2480 insertions(+), 2210 deletions(-) delete mode 100644 .pylintrc create mode 100644 poethepoet/options.py create mode 100644 tests/fixtures/monorepo_project/env1 create mode 100644 tests/fixtures/monorepo_project/env1t create mode 100644 tests/fixtures/monorepo_project/env2 create mode 100644 tests/fixtures/monorepo_project/env2t create mode 100644 tests/fixtures/monorepo_project/subproject_3/env3 create mode 100644 tests/fixtures/monorepo_project/subproject_3/env3t create mode 100644 tests/fixtures/monorepo_project/subproject_4/env3 create mode 100644 tests/fixtures/monorepo_project/subproject_4/env3t create mode 100644 tests/fixtures/monorepo_project/subproject_4/exec_dir/env0 create mode 100644 tests/fixtures/monorepo_project/subproject_4/exec_dir/env0t create mode 100644 tests/fixtures/monorepo_project/subproject_4/pyproject.toml diff --git a/.gitignore b/.gitignore index 1ed9f78a..356dee53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ notes/ tests/temp +out.txt # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 0b2d381b..00000000 --- a/.pylintrc +++ /dev/null @@ -1,364 +0,0 @@ -[MASTER] - -# Pickle collected data for later comparisons -persistent=no - -[MESSAGES CONTROL] - -reports=no - -disable=all - -output-format=parseable - -enable=invalid-name, - import-error, - import-self, - reimported, - wildcard-import, - misplaced-future, - deprecated-module, - unpacking-non-sequence, - invalid-all-object, - undefined-all-variable, - used-before-assignment, - cell-var-from-loop, - global-variable-undefined, - redefine-in-handler, - unused-import, - unused-wildcard-import, - global-variable-not-assigned, - undefined-loop-variable, - global-statement, - global-at-module-level, - bad-open-mode, - redundant-unittest-assert, - boolean-datetime - deprecated-method, - anomalous-unicode-escape-in-string, - anomalous-backslash-in-string, - not-in-loop, - continue-in-finally, - abstract-class-instantiated, - star-needs-assignment-target, - duplicate-argument-name, - return-in-init, - too-many-star-expressions, - nonlocal-and-global, - return-outside-function, - return-arg-in-generator, - invalid-star-assignment-target, - bad-reversed-sequence, - nonexistent-operator, - yield-outside-function, - init-is-generator, - nonlocal-without-binding, - lost-exception, - assert-on-tuple, - dangerous-default-value, - duplicate-key, - useless-else-on-loop - expression-not-assigned, - confusing-with-statement, - unnecessary-lambda, - pointless-statement, - unnecessary-pass, - unreachable, - eval-used, - exec-used, - using-constant-test, - bad-super-call, - missing-super-argument, - slots-on-old-class, - super-on-old-class, - property-on-old-class, - not-an-iterable, - not-a-mapping, - format-needs-mapping, - truncated-format-string, - missing-format-string-key, - mixed-format-string, - too-few-format-args, - bad-str-strip-call, - too-many-format-args, - bad-format-character, - format-combined-specification, - bad-format-string-key, - bad-format-string, - missing-format-attribute, - missing-format-argument-key, - unused-format-string-argument - unused-format-string-key, - invalid-format-index, - lowercase-l-suffix, - invalid-encoded-data, - unpacking-in-except, - import-star-module-level, - long-suffix, - old-octal-literal, - old-ne-operator, - backtick, - old-raise-syntax, - metaclass-assignment, - next-method-called, - dict-iter-method, - dict-view-method, - indexing-exception, - raising-string, - using-cmp-argument, - cmp-method, - coerce-method, - delslice-method, - getslice-method, - hex-method, - nonzero-method, - t-method, - setslice-method, - old-division, - logging-format-truncated, - logging-too-few-args, - logging-too-many-args, - logging-unsupported-format, - logging-format-interpolation, - invalid-unary-operand-type, - unsupported-binary-operation, - not-callable, - redundant-keyword-arg, - assignment-from-no-return, - assignment-from-none, - not-context-manager, - repeated-keyword, - missing-kwoa, - no-value-for-parameter, - invalid-sequence-index, - invalid-slice-index, - unsupported-membership-test, - access-member-before-definition, - method-hidden, - assigning-non-slot, - duplicate-bases, - inconsistent-mro, - inherit-non-class, - invalid-slots, - invalid-slots-object, - no-method-argument, - no-self-argument, - unexpected-special-method-signature, - non-iterator-returned, - arguments-differ, - signature-differs, - bad-staticmethod-argument, - non-parent-init-called, - bad-except-order, - catching-non-exception, - bad-exception-context, - notimplemented-raised, - raising-bad-type, - raising-non-exception, - misplaced-bare-raise, - duplicate-except, - broad-except, - nonstandard-exception, - binary-op-exception, - bare-except, - not-async-context-manager, - yield-inside-async-function - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format logging-modules=logging - - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,TODO - - -[BASIC] - -good-names=T,_,NoValue - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][A-Za-z0-9_]{1,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{1,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{1,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__ - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make diff --git a/docs/guides/include_guide.rst b/docs/guides/include_guide.rst index 922cf2d4..bb15314b 100644 --- a/docs/guides/include_guide.rst +++ b/docs/guides/include_guide.rst @@ -45,7 +45,7 @@ If an included task file itself includes other files, these second order include Setting a working directory for included tasks ---------------------------------------------- -When including files from another location, you can also specify that tasks from that other file should be run from within a specific directory. For example with the following configuration, when tasks imported from my_subproject are run from the root, the task will actually execute as if it had been run from the *my_subproject* subdirectory. +When including files from another location, you can also specify that tasks from that other file should be run from within a specific directory. For example with the following configuration, when tasks imported from *my_subproject* are run from the root, the task will actually execute as if it had been run from the *my_subproject* subdirectory. .. code-block:: toml @@ -53,4 +53,13 @@ When including files from another location, you can also specify that tasks from path = "my_subproject/pyproject.toml" cwd = "my_subproject" -The cwd option still has the limitation that it cannot be used to specify a directory outside of parent directory of the pyproject.toml file that poe is running with. +The directory indicated by the ``cwd`` option will also be used as the base directory for global or task level ``envfile`` imports for tasks defined within an included file. + +Tasks and config in an included file can access the ``cwd`` value via the ``POE_CONF_DIR`` environment variable. When no ``cwd`` is set on the include then ``POE_CONF_DIR`` refers the to the parent directory of the config file where a task is defined. + +You can still specify that an envfile referenced within an included file should be imported relative to the main project root, using the ``POE_ROOT`` environment variable like so: + +.. code-block:: toml + + [tool.poe] + envfile = "${POE_ROOT}/.env" diff --git a/poethepoet/__init__.py b/poethepoet/__init__.py index 48e0efb0..029df2a8 100644 --- a/poethepoet/__init__.py +++ b/poethepoet/__init__.py @@ -48,9 +48,9 @@ def _list_tasks(): from .config import PoeConfig config = PoeConfig() - config.load() - task_names = (task for task in config.tasks.keys() if task and task[0] != "_") + config.load(strict=False) + task_names = (task for task in config.task_names if task and task[0] != "_") print(" ".join(task_names)) - except Exception: # pylint: disable=broad-except + except Exception: # this happens if there's no pyproject.toml present pass diff --git a/poethepoet/app.py b/poethepoet/app.py index fed4ae14..8a0d44c9 100644 --- a/poethepoet/app.py +++ b/poethepoet/app.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from .config import PoeConfig from .context import RunContext - from .task import PoeTask + from .task.base import PoeTask from .ui import PoeUi @@ -27,7 +27,7 @@ class PoeThePoet: :param cwd: The directory that poe should take as the current working directory, this determines where to look for a pyproject.toml file, defaults to - ``Path(".").resolve()`` + ``Path().resolve()`` :type cwd: Path, optional :param config: Either a dictionary with the same schema as a pyproject.toml file, or a @@ -56,6 +56,8 @@ class PoeThePoet: ui: "PoeUi" config: "PoeConfig" + _task_specs: Optional[Dict[str, "PoeTask.TaskSpec"]] = None + def __init__( self, cwd: Optional[Union[Path, str]] = None, @@ -98,8 +100,9 @@ def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int: return 0 try: - self.config.load(self.ui["project_root"]) - self.config.validate() + self.config.load(target_path=self.ui["project_root"]) + for task_spec in self.task_specs.load_all(): + task_spec.validate(self.config, self.task_specs) except PoeException as error: if self.ui["help"]: self.print_help() @@ -122,8 +125,16 @@ def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int: else: return self.run_task(task) or 0 + @property + def task_specs(self): + if not self._task_specs: + from .task.base import TaskSpecFactory + + self._task_specs = TaskSpecFactory(self.config) + return self._task_specs + def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]: - from .task import PoeTask + from .task.base import TaskContext task = tuple(self.ui["task"]) if not task: @@ -131,7 +142,7 @@ def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]: return None task_name = task[0] - if task_name not in self.config.tasks: + if task_name not in self.config.task_names: self.print_help(error=PoeException(f"Unrecognised task {task_name!r}")) return None @@ -143,8 +154,15 @@ def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]: ) return None - return PoeTask.from_config( - task_name, config=self.config, ui=self.ui, invocation=task + task_spec = self.task_specs.get(task_name) + return task_spec.create_task( + invocation=task, + ctx=TaskContext( + config=self.config, + cwd=str(task_spec.source.cwd), + specs=self.task_specs, + ui=self.ui, + ), ) def run_task( @@ -154,12 +172,12 @@ def run_task( context = self.get_run_context() try: return task.run(context=context) - except PoeException as error: - self.print_help(error=error) - return 1 except ExecutionError as error: self.ui.print_error(error=error) return 1 + except PoeException as error: + self.print_help(error=error) + return 1 def run_task_graph(self, task: "PoeTask") -> Optional[int]: from .task.graph import TaskExecutionGraph diff --git a/poethepoet/config.py b/poethepoet/config.py index 1ed996da..c859a594 100644 --- a/poethepoet/config.py +++ b/poethepoet/config.py @@ -1,17 +1,250 @@ import json from pathlib import Path +from types import MappingProxyType try: import tomllib as tomli except ImportError: import tomli # type: ignore[no-redef] -from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + Any, + Iterator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +from .exceptions import ConfigValidationError, PoeException +from .options import NoValue, PoeOptions + + +class ConfigPartition: + options: PoeOptions + full_config: Mapping[str, Any] + poe_options: Mapping[str, Any] + path: Path + project_dir: Path + _cwd: Optional[Path] + + ConfigOptions: Type[PoeOptions] + is_primary: bool = False -from .exceptions import PoeException + def __init__( + self, + full_config: Mapping[str, Any], + path: Path, + project_dir: Optional[Path] = None, + cwd: Optional[Path] = None, + strict: bool = True, + ): + self.poe_options: Mapping[str, Any] = ( + full_config["tool"].get("poe", {}) + if "tool" in full_config + else full_config.get("tool.poe", {}) + ) + self.options = next( + self.ConfigOptions.parse( + self.poe_options, + strict=strict, + # Allow and standard config keys, even if not declared + # This avoids misguided validation errors on included config + extra_keys=tuple(ProjectConfig.ConfigOptions.get_fields()), + ) + ) + self.full_config = full_config + self.path = path + self._cwd = cwd + self.project_dir = project_dir or self.path.parent + + @property + def cwd(self): + return self._cwd or self.project_dir + + @property + def config_dir(self): + return self._cwd or self.path.parent + + def get(self, key: str, default: Any = NoValue): + return self.options.get(key, default) + + +EmptyDict: Mapping = MappingProxyType({}) + + +class ProjectConfig(ConfigPartition): + is_primary = True + + class ConfigOptions(PoeOptions): + """ + Options supported directly under tool.poe in the main config i.e. pyproject.toml + """ + + default_task_type: str = "cmd" + default_array_task_type: str = "sequence" + default_array_item_task_type: str = "ref" + env: Mapping[str, str] = EmptyDict + envfile: Union[str, Sequence[str]] = tuple() + executor: Mapping[str, str] = MappingProxyType({"type": "auto"}) + include: Sequence[str] = tuple() + poetry_command: str = "poe" + poetry_hooks: Mapping[str, str] = EmptyDict + shell_interpreter: Union[str, Sequence[str]] = "posix" + verbosity: int = 0 + tasks: Mapping[str, Any] = EmptyDict + + @classmethod + def normalize( + cls, + config: Any, + strict: bool = True, + ): + if isinstance(config, (list, tuple)): + raise ConfigValidationError("Expected ") + + # Normalize include option: + # > Union[str, Sequence[str], Mapping[str, str]] => List[dict] + if "include" in config: + includes: Any = [] + include_option = config.get("include", None) + + if isinstance(include_option, (dict, str)): + include_option = [include_option] + + if isinstance(include_option, list): + valid_keys = {"path", "cwd"} + for include in include_option: + if isinstance(include, str): + includes.append({"path": include}) + elif ( + isinstance(include, dict) + and include.get("path") + and set(include.keys()) <= valid_keys + ): + includes.append(include) + else: + raise ConfigValidationError( + f"Invalid item for the include option {include!r}", + global_option="include", + ) + else: + # Something is wrong, let option validation handle it + includes = include_option + + config = {**config, "include": includes} + + yield config + + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + super().validate() + + from .executor import PoeExecutor + from .task.base import PoeTask + + # Validate default_task_type value + if not PoeTask.is_task_type(self.default_task_type, content_type=str): + raise ConfigValidationError( + "Invalid value for option 'default_task_type': " + f"{self.default_task_type!r}\n" + f"Expected one of {PoeTask.get_task_types(str)!r}" + ) + + # Validate default_array_task_type value + if not PoeTask.is_task_type( + self.default_array_task_type, content_type=list + ): + raise ConfigValidationError( + "Invalid value for option 'default_array_task_type': " + f"{self.default_array_task_type!r}\n" + f"Expected one of {PoeTask.get_task_types(list)!r}" + ) + + # Validate default_array_item_task_type value + if not PoeTask.is_task_type( + self.default_array_item_task_type, content_type=str + ): + raise ConfigValidationError( + "Invalid value for option 'default_array_item_task_type': " + f"{self.default_array_item_task_type!r}\n" + f"Expected one of {PoeTask.get_task_types(str)!r}" + ) + + # Validate shell_interpreter type + if self.shell_interpreter: + shell_interpreter = ( + (self.shell_interpreter,) + if isinstance(self.shell_interpreter, str) + else self.shell_interpreter + ) + for interpreter in shell_interpreter: + if interpreter not in PoeConfig.KNOWN_SHELL_INTERPRETERS: + raise ConfigValidationError( + f"Unsupported value {interpreter!r} for option " + "'shell_interpreter'\n" + f"Expected one of {PoeConfig.KNOWN_SHELL_INTERPRETERS!r}" + ) + + # Validate default verbosity. + if self.verbosity < -1 or self.verbosity > 2: + raise ConfigValidationError( + f"Invalid value for option 'verbosity': {self.verbosity!r},\n" + "Expected value be between -1 and 2." + ) + + self.validate_env(self.env) + + # Validate executor config + PoeExecutor.validate_config(self.executor) + + @classmethod + def validate_env(cls, env: Mapping[str, str]): + # Validate env value + for key, value in env.items(): + if isinstance(value, dict): + if tuple(value.keys()) != ("default",) or not isinstance( + value["default"], str + ): + raise ConfigValidationError( + f"Invalid declaration at {key!r} in option 'env': {value!r}" + ) + elif not isinstance(value, str): + raise ConfigValidationError( + f"Value of {key!r} in option 'env' should be a string, " + f"but found {type(value).__name__!r}" + ) + + +class IncludedConfig(ConfigPartition): + class ConfigOptions(PoeOptions): + """ + Options supported directly under tool.poe in included config files + """ + + env: Mapping[str, str] = EmptyDict + envfile: Union[str, Sequence[str]] = tuple() + tasks: Mapping[str, Any] = EmptyDict + + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + super().validate() + + # Apply same validation to env option as for the main config + ProjectConfig.ConfigOptions.validate_env(self.env) class PoeConfig: + _project_config: ProjectConfig + _included_config: List[IncludedConfig] + KNOWN_SHELL_INTERPRETERS = ( "posix", "sh", @@ -24,22 +257,13 @@ class PoeConfig: ) """ - Options allowed directly under tool.poe in pyproject.toml + The filename to look for when loading config """ - __options__ = { - "default_task_type": str, - "default_array_task_type": str, - "default_array_item_task_type": str, - "env": dict, - "envfile": (str, list), - "executor": dict, - "include": (str, list, dict), - "poetry_command": str, - "poetry_hooks": dict, - "shell_interpreter": (str, list), - "verbosity": int, - } - + _config_name: str = "pyproject.toml" + """ + The parent directory of the project config file + """ + _project_dir: Path """ This can be overridden, for example to align with poetry """ @@ -51,264 +275,214 @@ def __init__( table: Optional[Mapping[str, Any]] = None, config_name: str = "pyproject.toml", ): - self.cwd = Path().resolve() if cwd is None else Path(cwd) - self._poe = {} if table is None else dict(table) self._config_name = config_name - self._project_dir: Optional[Path] = None + self._project_dir = self._resolve_project_dir( + Path().resolve() if cwd is None else Path(cwd) + ) + self._project_config = ProjectConfig( + {"tool.poe": table or {}}, + path=self._project_dir.joinpath(config_name), + strict=False, + ) + self._included_config = [] + + def lookup_task( + self, name: str + ) -> Union[Tuple[Mapping[str, Any], ConfigPartition], Tuple[None, None]]: + task = self._project_config.get("tasks", {}).get(name, None) + if task is not None: + return task, self._project_config + + for include in reversed(self._included_config): + task = include.get("tasks", {}).get(name, None) + if task is not None: + return task, include + + return None, None + + def partitions(self, included_first=True) -> Iterator[ConfigPartition]: + if not included_first: + yield self._project_config + yield from self._included_config + if included_first: + yield self._project_config @property def executor(self) -> Mapping[str, Any]: - return self._poe.get("executor", {"type": "auto"}) + return self._project_config.options.executor + + @property + def task_names(self) -> Iterator[str]: + result = list(self._project_config.get("tasks", {}).keys()) + for config_part in self._included_config: + for task_name in config_part.get("tasks", {}).keys(): + # Don't use a set to dedup because we want to preserve task order + if task_name not in result: + result.append(task_name) + yield from result @property def tasks(self) -> Mapping[str, Any]: - return self._poe.get("tasks", {}) + result = dict(self._project_config.get("tasks", {})) + for config in self._included_config: + for task_name, task_def in config.get("tasks", {}).items(): + if task_name in result: + continue + result[task_name] = task_def + return result @property def default_task_type(self) -> str: - return self._poe.get("default_task_type", "cmd") + return self._project_config.options.default_task_type @property def default_array_task_type(self) -> str: - return self._poe.get("default_array_task_type", "sequence") + return self._project_config.options.default_array_task_type @property def default_array_item_task_type(self) -> str: - return self._poe.get("default_array_item_task_type", "ref") - - @property - def global_env(self) -> Dict[str, Union[str, Dict[str, str]]]: - return self._poe.get("env", {}) - - @property - def global_envfile(self) -> Optional[str]: - return self._poe.get("envfile") + return self._project_config.options.default_array_item_task_type @property def shell_interpreter(self) -> Tuple[str, ...]: - raw_value = self._poe.get("shell_interpreter", "posix") + raw_value = self._project_config.options.shell_interpreter if isinstance(raw_value, list): return tuple(raw_value) return (raw_value,) @property def verbosity(self) -> int: - return self._poe.get("verbosity", self._baseline_verbosity) + return self._project_config.get("verbosity", self._baseline_verbosity) @property - def project(self) -> Any: - return self._project + def is_poetry_project(self) -> bool: + return "poetry" in self._project_config.full_config @property - def project_dir(self) -> str: - return str(self._project_dir or self.cwd) + def project_dir(self) -> Path: + return self._project_dir + + def load(self, target_path: Optional[Union[Path, str]] = None, strict: bool = True): + """ + target_path is the path to a file or directory for loading config + If strict is false then some errors in the config structure are tolerated + """ - def load(self, target_dir: Optional[str] = None): - if self._poe: - return + config_path = self.find_config_file( + target_path=Path(target_path) if target_path else None, + search_parent=target_path is None, + ) + self._project_dir = config_path.parent - config_path = self.find_config_file(target_dir) try: - self._project = self._read_config_file(config_path) - self._poe = self._project["tool"]["poe"] + self._project_config = ProjectConfig( + self._read_config_file(config_path), + path=config_path, + project_dir=self._project_dir, + strict=strict, + ) except KeyError: raise PoeException( f"No poe configuration found in file at {self._config_name}" ) - self._project_dir = config_path.parent - self._load_includes(self._project_dir) - - def validate(self): - from .executor import PoeExecutor - from .task import PoeTask - - # Validate keys - supported_keys = {"tasks", *self.__options__} - unsupported_keys = set(self._poe) - supported_keys - if unsupported_keys: - raise PoeException(f"Unsupported keys in poe config: {unsupported_keys!r}") - - # Validate types of option values - for key, option_type in self.__options__.items(): - if key in self._poe and not isinstance(self._poe[key], option_type): - raise PoeException( - f"Unsupported value for option {key!r}, expected type to be " - f"{option_type.__name__}." - ) - - # Validate executor config - error = PoeExecutor.validate_config(self.executor) - if error: - raise PoeException(error) - - # Validate default_task_type value - if not PoeTask.is_task_type(self.default_task_type, content_type=str): - raise PoeException( - "Unsupported value for option `default_task_type` " - f"{self.default_task_type!r}" + except ConfigValidationError: + # Try again to load Config with minimal validation so we can still display + # the task list alongside the error + self._project_config = ProjectConfig( + self._read_config_file(config_path), + path=config_path, + project_dir=self._project_dir, + strict=False, ) + raise - # Validate default_array_task_type value - if not PoeTask.is_task_type(self.default_array_task_type, content_type=list): - raise PoeException( - "Unsupported value for option `default_array_task_type` " - f"{self.default_array_task_type!r}" - ) + self._load_includes(strict=strict) - # Validate default_array_item_task_type value - if not PoeTask.is_task_type(self.default_array_item_task_type): - raise PoeException( - "Unsupported value for option `default_array_item_task_type` " - f"{self.default_array_item_task_type!r}" - ) + def find_config_file( + self, target_path: Optional[Path] = None, search_parent: bool = True + ) -> Path: + """ + If search_parent is False then check if the target_path points to a config file + or a directory containing a config file. - # Validate env value - for key, value in self.global_env.items(): - if isinstance(value, dict): - if tuple(value.keys()) != ("default",) or not isinstance( - value["default"], str - ): - raise PoeException( - f"Invalid declaration at {key!r} in option `env`: {value!r}" - ) - elif not isinstance(value, str): - raise PoeException( - f"Value of {key!r} in option `env` should be a string, but found " - f"{type(value)!r}" - ) + If search_parent is True then also search for the config file in parent + directories in ascending order. - # Validate tasks - for task_name, task_def in self.tasks.items(): - error = PoeTask.validate_def(task_name, task_def, self) - if error is None: - continue - raise PoeException(error) + If no target_path is provided then start with self._project_dir - # Validate shell_interpreter type - for interpreter in self.shell_interpreter: - if interpreter not in self.KNOWN_SHELL_INTERPRETERS: - raise PoeException( - f"Unsupported value {interpreter!r} for option `shell_interpreter`." - ) + If the given target_path is a file, then it may be named as any toml or json + file, otherwise the config file name must match `self._config_name`. - # Validate default verbosity. - if self.verbosity < -1 or self.verbosity > 2: - raise PoeException( - f"Invalid value for option `verbosity`: {self.verbosity!r}. " - "Should be between -1 and 2." - ) - - def find_config_file(self, target_dir: Optional[str] = None) -> Path: + If no config file can be found then raise a PoeException """ - Resolve a path to a self._config_name using one of two strategies: - 1. If target_dir is provided then only look there, (accept path to config file - or to a directory). - 2. Otherwise look for the self._config_name in the current working directory, - following by all parent directories in ascending order. + if target_path is None: + target_path = self._project_dir + else: + target_path = target_path.resolve() - Both strategies result in an Exception on failure. - """ - if target_dir: - target_path = Path(target_dir).resolve() + if not search_parent: if not ( target_path.name.endswith(".toml") or target_path.name.endswith(".json") ): target_path = target_path.joinpath(self._config_name) if not target_path.exists(): raise PoeException( - f"Poe could not find a {self._config_name} file at the given " - f"location: {target_dir}" + f"Poe could not find a {self._config_name!r} file at the given " + f"location: {target_path!r}" ) return target_path - maybe_result = self.cwd.joinpath(self._config_name) + return self._resolve_project_dir(target_path, raise_on_fail=True) + + def _resolve_project_dir(self, target_dir: Path, raise_on_fail: bool = False): + """ + Look for the self._config_name in the current working directory, + followed by all parent directories in ascending order. + Return the path of the parent directory of the first config file found. + """ + maybe_result = target_dir.joinpath(self._config_name) while not maybe_result.exists(): if len(maybe_result.parents) == 1: - raise PoeException( - f"Poe could not find a {self._config_name} file in {self.cwd} or" - " its parents" - ) - maybe_result = maybe_result.parents[1].joinpath(self._config_name).resolve() - return maybe_result - - def _load_includes(self, project_dir: Path): - include_option: Union[str, Sequence[str]] = self._poe.get("include", tuple()) - includes: List[Dict[str, str]] = [] - - if isinstance(include_option, str): - includes.append({"path": include_option}) - elif isinstance(include_option, dict): - includes.append(include_option) - elif isinstance(include_option, list): - valid_keys = {"path", "cwd"} - for include in include_option: - if isinstance(include, str): - includes.append({"path": include}) - elif ( - isinstance(include, dict) - and include.get("path") - and set(include.keys()) <= valid_keys - ): - includes.append(include) - else: + if raise_on_fail: raise PoeException( - f"Invalid item for the include option {include!r}" + f"Poe could not find a {self._config_name!r} file in " + f"{target_dir} or any parent directory." ) + else: + return target_dir + maybe_result = maybe_result.parents[1].joinpath(self._config_name).resolve() + return maybe_result - for include in includes: - include_path = project_dir.joinpath(include["path"]).resolve() + def _load_includes(self: "PoeConfig", strict: bool = True): + # Attempt to load each of the included configs + for include in self._project_config.options.include: + include_path = self._project_dir.joinpath(include["path"]).resolve() if not include_path.exists(): # TODO: print warning in verbose mode, requires access to ui somehow + # Maybe there should be something like a WarningService? continue try: - include_config = PoeConfig( - cwd=include.get("cwd", self.project_dir), - table=self._read_config_file(include_path)["tool"]["poe"], + self._included_config.append( + IncludedConfig( + self._read_config_file(include_path), + path=include_path, + project_dir=self._project_dir, + cwd=( + self.project_dir.joinpath(include["cwd"]).resolve() + if include.get("cwd") + else None + ), + strict=strict, + ) ) - include_config._project_dir = self._project_dir except (PoeException, KeyError) as error: - raise PoeException( - f"Invalid content in included file from {include_path}", error + raise ConfigValidationError( + f"Invalid content in included file from {include_path}", + filename=str(include_path), ) from error - self._merge_config(include_config) - - def _merge_config(self, include_config: "PoeConfig"): - from .task import PoeTask - - # Env is special because it can be extended rather than just overwritten - if include_config.global_env: - self._poe["env"] = {**include_config.global_env, **self._poe.get("env", {})} - - if include_config.global_envfile and "envfile" not in self._poe: - self._poe["envfile"] = include_config.global_envfile - - # Includes additional tasks with preserved ordering - self._poe["tasks"] = own_tasks = self._poe.get("tasks", {}) - for task_name, task_def in include_config.tasks.items(): - if task_name in own_tasks: - # don't override tasks from the base config - continue - - task_def = PoeTask.normalize_task_def(task_def, include_config) - if include_config.cwd: - # Override the config of each task to use the include level cwd as a - # base for the task level cwd - if "cwd" in task_def: - # rebase the configured cwd onto the include level cwd - task_def["cwd"] = str( - Path(include_config.cwd) - .resolve() - .joinpath(task_def["cwd"]) - .relative_to(self.project_dir) - ) - else: - task_def["cwd"] = str(include_config.cwd) - - own_tasks[task_name] = task_def - @staticmethod def _read_config_file(path: Path) -> Mapping[str, Any]: try: diff --git a/poethepoet/context.py b/poethepoet/context.py index 685ad9fa..99044622 100644 --- a/poethepoet/context.py +++ b/poethepoet/context.py @@ -40,30 +40,21 @@ def __init__( self.multistage = multistage self.exec_cache = {} self.captured_stdout = {} + + # Init root EnvVarsManager self.env = EnvVarsManager(self.config, self.ui, base_env=env, cwd=cwd) + for config_part in self.config.partitions(): + self.env.apply_env_config( + envfile=config_part.get("envfile", None), + config_env=config_part.get("env", None), + config_dir=config_part.config_dir, + config_working_dir=config_part.cwd, + ) @property def executor_type(self) -> Optional[str]: return self.config.executor["type"] - def get_task_env( - self, - parent_env: Optional["EnvVarsManager"], - task_envfile: Optional[str], - task_env: Optional[Mapping[str, str]], - task_uses: Optional[Mapping[str, Tuple[str, ...]]] = None, - ) -> "EnvVarsManager": - if parent_env is None: - parent_env = self.env - - result = parent_env.for_task(task_envfile, task_env) - - # Include env vars from dependencies - if task_uses is not None: - result.update(self._get_dep_values(task_uses)) - - return result - def _get_dep_values( self, used_task_invocations: Mapping[str, Tuple[str, ...]] ) -> Dict[str, str]: @@ -104,7 +95,7 @@ def get_executor( env: "EnvVarsManager", working_dir: Path, executor_config: Optional[Mapping[str, str]] = None, - capture_stdout: bool = False, + capture_stdout: Union[str, bool] = False, ) -> "PoeExecutor": from .executor import PoeExecutor @@ -113,7 +104,7 @@ def get_executor( context=self, env=env, working_dir=working_dir, - dry=self.dry, executor_config=executor_config, capture_stdout=capture_stdout, + dry=self.dry, ) diff --git a/poethepoet/env/cache.py b/poethepoet/env/cache.py index 15816048..ca554674 100644 --- a/poethepoet/env/cache.py +++ b/poethepoet/env/cache.py @@ -1,11 +1,14 @@ +from os import environ from pathlib import Path -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Dict, Optional, Union from ..exceptions import ExecutionError if TYPE_CHECKING: from .ui import PoeUi +POE_DEBUG = environ.get("POE_DEBUG") + class EnvFileCache: _cache: Dict[str, Dict[str, str]] = {} @@ -16,19 +19,23 @@ def __init__(self, project_dir: Path, ui: Optional["PoeUi"]): self._project_dir = project_dir self._ui = ui - def get(self, envfile_path_str: str) -> Dict[str, str]: + def get(self, envfile: Union[str, Path]) -> Dict[str, str]: from .parse import parse_env_file + envfile_path_str = str(envfile) + if envfile_path_str in self._cache: return self._cache[envfile_path_str] result = {} - envfile_path = self._project_dir.joinpath(Path(envfile_path_str).expanduser()) + envfile_path = self._project_dir.joinpath(Path(envfile).expanduser()) if envfile_path.is_file(): try: - with envfile_path.open(encoding="utf-8") as envfile: - result = parse_env_file(envfile.readlines()) + with envfile_path.open(encoding="utf-8") as envfile_file: + result = parse_env_file(envfile_file.readlines()) + if POE_DEBUG: + print(f" - Loaded Envfile from {envfile_path}") except ValueError as error: message = error.args[0] raise ExecutionError( @@ -36,11 +43,15 @@ def get(self, envfile_path_str: str) -> Dict[str, str]: f" {message}" ) from error - elif self._ui is not None: - self._ui.print_msg( - f"Warning: Poe failed to locate envfile at {envfile_path_str!r}", - verbosity=1, - ) + else: + if POE_DEBUG: + print(f" ! Envfile not found at {envfile_path}") + + if self._ui is not None: + self._ui.print_msg( + f"Warning: Poe failed to locate envfile at {envfile_path_str!r}", + verbosity=1, + ) self._cache[envfile_path_str] = result return result diff --git a/poethepoet/env/manager.py b/poethepoet/env/manager.py index 04b4b4db..f692876e 100644 --- a/poethepoet/env/manager.py +++ b/poethepoet/env/manager.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union from .template import apply_envvars_to_template @@ -16,7 +16,7 @@ class EnvVarsManager: _vars: Dict[str, str] envfiles: "EnvFileCache" - def __init__( + def __init__( # TODO: check if we still need all these args! self, config: "PoeConfig", ui: Optional["PoeUi"], @@ -39,74 +39,60 @@ def __init__( **(base_env or {}), } - if parent_env is None: - # Get env vars from envfile(s) referenced in global options - global_envfile = self._config.global_envfile - if isinstance(global_envfile, str): - self._vars.update(self.envfiles.get(global_envfile)) - elif isinstance(global_envfile, list): - for task_envfile_path in global_envfile: - self._vars.update(self.envfiles.get(task_envfile_path)) - - # Get env vars from global options - self._apply_env_config(self._config.global_env) - self._vars["POE_ROOT"] = str(self._config.project_dir) self.cwd = str(cwd or os.getcwd()) - if "POE_PWD" not in self._vars: - self._vars["POE_PWD"] = self.cwd + if "POE_CWD" not in self._vars: + self._vars["POE_CWD"] = self.cwd + self._vars["POE_PWD"] = self.cwd # Deprecated def get(self, key: str, default: Optional[str] = None) -> Optional[str]: return self._vars.get(key, default) - def _apply_env_config( + def set(self, key: str, value: str): + self._vars[key] = value + + def apply_env_config( self, - config_env: Mapping[str, Union[str, Mapping[str, str]]], + envfile: Optional[Union[str, List[str]]], + config_env: Optional[Mapping[str, Union[str, Mapping[str, str]]]], + config_dir: Path, + config_working_dir: Path, ): """ Used for including env vars from global or task config. If a value is provided as a mapping from `"default"` to `str` then it is only used if the associated key doesn't already have a value. """ - for key, value in config_env.items(): + + vars_scope = dict(self._vars, POE_CONF_DIR=str(config_dir)) + if envfile: + if isinstance(envfile, str): + envfile = [envfile] + for envfile_path in envfile: + self.update( + self.envfiles.get( + config_working_dir.joinpath( + apply_envvars_to_template( + envfile_path, vars_scope, require_braces=True + ) + ) + ) + ) + + vars_scope = dict(self._vars, POE_CONF_DIR=str(config_dir)) + for key, value in (config_env or {}).items(): if isinstance(value, str): value_str = value - elif key not in self._vars: + elif key not in vars_scope: value_str = value["default"] else: continue self._vars[key] = apply_envvars_to_template( - value_str, self._vars, require_braces=True + value_str, vars_scope, require_braces=True ) - def for_task( - self, task_envfile: Optional[str], task_env: Optional[Mapping[str, str]] - ) -> "EnvVarsManager": - """ - Create a copy of self and extend it to include vars for the task. - """ - result = EnvVarsManager( - self._config, - self._ui, - parent_env=self, - cwd=self.cwd, - ) - - # Include env vars from envfile(s) referenced in task options - if isinstance(task_envfile, str): - result.update(self.envfiles.get(task_envfile)) - elif isinstance(task_envfile, list): - for task_envfile_path in task_envfile: - result.update(self.envfiles.get(task_envfile_path)) - - # Include env vars from task options - if task_env is not None: - result._apply_env_config(task_env) - - return result - def update(self, env_vars: Mapping[str, Any]): # ensure all values are strings str_vars: Dict[str, str] = {} @@ -120,6 +106,14 @@ def update(self, env_vars: Mapping[str, Any]): return self + def clone(self): + return EnvVarsManager( + config=self._config, + ui=self._ui, + parent_env=self, + cwd=self.cwd, + ) + def to_dict(self): return dict(self._vars) diff --git a/poethepoet/exceptions.py b/poethepoet/exceptions.py index a7011c92..9cbcc8a5 100644 --- a/poethepoet/exceptions.py +++ b/poethepoet/exceptions.py @@ -19,6 +19,31 @@ class ExpressionParseError(PoeException): pass +class ConfigValidationError(PoeException): + context: Optional[str] + task_name: Optional[str] + index: Optional[int] + global_option: Optional[str] + filename: Optional[str] + + def __init__( + self, + msg, + *args, + context: Optional[str] = None, + task_name: Optional[str] = None, + index: Optional[int] = None, + global_option: Optional[str] = None, + filename: Optional[str] = None + ): + super().__init__(msg, *args) + self.context = context + self.task_name = task_name + self.index = index + self.global_option = global_option + self.filename = filename + + class ExecutionError(RuntimeError): cause: Optional[str] diff --git a/poethepoet/executor/base.py b/poethepoet/executor/base.py index 76736107..86a04b43 100644 --- a/poethepoet/executor/base.py +++ b/poethepoet/executor/base.py @@ -5,6 +5,7 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, Dict, Mapping, MutableMapping, @@ -15,7 +16,7 @@ Union, ) -from ..exceptions import ExecutionError, PoeException +from ..exceptions import ConfigValidationError, ExecutionError, PoeException if TYPE_CHECKING: from ..context import RunContext @@ -47,8 +48,8 @@ class PoeExecutor(metaclass=MetaPoeExecutor): working_dir: Optional[Path] - __executor_types: Dict[str, Type["PoeExecutor"]] = {} - __key__: Optional[str] = None + __executor_types: ClassVar[Dict[str, Type["PoeExecutor"]]] = {} + __key__: ClassVar[Optional[str]] = None def __init__( self, @@ -57,22 +58,22 @@ def __init__( options: Mapping[str, str], env: "EnvVarsManager", working_dir: Optional[Path] = None, - dry: bool = False, capture_stdout: Union[str, bool] = False, + dry: bool = False, ): self.invocation = invocation self.context = context self.options = options self.working_dir = working_dir self.env = env - self.dry = dry self.capture_stdout = ( Path(self.context.config.project_dir).joinpath( self.env.fill_template(capture_stdout) ) - if isinstance(capture_stdout, str) - else capture_stdout + if capture_stdout and isinstance(capture_stdout, str) + else bool(capture_stdout) ) + self.dry = dry self._is_windows = sys.platform == "win32" @classmethod @@ -86,15 +87,15 @@ def get( context: "RunContext", env: "EnvVarsManager", working_dir: Optional[Path] = None, - dry: bool = False, executor_config: Optional[Mapping[str, str]] = None, capture_stdout: Union[str, bool] = False, + dry: bool = False, ) -> "PoeExecutor": """""" # use task specific executor config or fallback to global options = executor_config or context.config.executor return cls._resolve_implementation(context, executor_config)( - invocation, context, options, env, working_dir, dry, capture_stdout + invocation, context, options, env, working_dir, capture_stdout, dry ) @classmethod @@ -109,11 +110,12 @@ def _resolve_implementation( config_executor_type = context.executor_type if executor_config: - if executor_config["type"] not in cls.__executor_types: + executor_type = executor_config["type"] + if executor_type not in cls.__executor_types: raise PoeException( - f"Cannot instantiate unknown executor {executor_config['type']!r}" + f"Cannot instantiate unknown executor {executor_type!r}" ) - return cls.__executor_types[executor_config["type"]] + return cls.__executor_types[executor_type] elif config_executor_type == "auto": for impl in [ cls.__executor_types["poetry"], @@ -180,7 +182,7 @@ def _execute_cmd( return self._handle_file_not_found(cmd, error) if error.filename == self.working_dir: raise PoeException( - "The specified working directory does not exists " + "The specified working directory does not exist " f"'{self.working_dir}'" ) raise @@ -270,25 +272,40 @@ def handle_sigint(signum, _frame): return proc.returncode @classmethod - def validate_config(cls, config: Dict[str, Any]) -> Optional[str]: + def validate_config(cls, config: Dict[str, Any]): + if "type" not in config: + raise ConfigValidationError( + "Missing required key 'type' from executor option", + global_option="executor", + ) + executor_type = config["type"] if executor_type == "auto": extra_options = set(config.keys()) - {"type"} if extra_options: - return f"Unexpected keys for executor config: {extra_options!r}" + raise ConfigValidationError( + f"Unexpected keys for executor config: {extra_options!r}", + global_option="executor", + ) + elif executor_type not in cls.__executor_types: - return f"Unknown executor type: {executor_type!r}" + raise ConfigValidationError( + f"Unknown executor type: {executor_type!r}", + global_option="executor.type", + ) + else: - return cls.__executor_types[executor_type].validate_executor_config(config) - return None + cls.__executor_types[executor_type].validate_executor_config(config) @classmethod - def validate_executor_config(cls, config: Dict[str, Any]) -> Optional[str]: + def validate_executor_config(cls, config: Dict[str, Any]): """To be overridden by subclasses if they accept options""" extra_options = set(config.keys()) - {"type"} if extra_options: - return f"Unexpected keys for executor config: {extra_options!r}" - return None + raise ConfigValidationError( + f"Unexpected keys for executor config: {extra_options!r}", + global_option="executor", + ) def _stop_coverage(): diff --git a/poethepoet/executor/poetry.py b/poethepoet/executor/poetry.py index c282a702..2e496639 100644 --- a/poethepoet/executor/poetry.py +++ b/poethepoet/executor/poetry.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Dict, Optional, Sequence, Type -from ..exceptions import PoeException +from ..exceptions import ExecutionError from .base import PoeExecutor if TYPE_CHECKING: @@ -20,7 +20,7 @@ class PoetryExecutor(PoeExecutor): @classmethod def works_with_context(cls, context: "RunContext") -> bool: - if "poetry" not in context.config.project["tool"]: + if not context.config.is_poetry_project: return False return bool(cls._poetry_cmd_from_path()) @@ -61,7 +61,7 @@ def _handle_file_not_found( ) -> int: poetry_env = self._get_poetry_virtualenv() error_context = f" using virtualenv {poetry_env!r}" if poetry_env else "" - raise PoeException( + raise ExecutionError( f"executable {cmd[0]!r} could not be found{error_context}" ) from error diff --git a/poethepoet/executor/virtualenv.py b/poethepoet/executor/virtualenv.py index b80c6083..9f658772 100644 --- a/poethepoet/executor/virtualenv.py +++ b/poethepoet/executor/virtualenv.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type -from ..exceptions import PoeException +from ..exceptions import ConfigValidationError, ExecutionError from .base import PoeExecutor if TYPE_CHECKING: @@ -42,7 +42,7 @@ def _handle_file_not_found( ) -> int: venv = self._resolve_virtualenv() error_context = f" using virtualenv {str(venv.path)!r}" if venv else "" - raise PoeException( + raise ExecutionError( f"executable {cmd[0]!r} could not be found{error_context}" ) from error @@ -55,7 +55,7 @@ def _resolve_virtualenv(self) -> "Virtualenv": ) if venv.valid(): return venv - raise PoeException( + raise ExecutionError( f"Could not find valid virtualenv at configured location: {venv.path}" ) @@ -67,22 +67,26 @@ def _resolve_virtualenv(self) -> "Virtualenv": if hidden_venv.valid(): return hidden_venv - raise PoeException( + raise ExecutionError( f"Could not find valid virtualenv at either of: {venv.path} or " - f"{hidden_venv.path}" + f"{hidden_venv.path}.\n" + "You can configure another location as tool.poe.executor.location" ) @classmethod - def validate_executor_config(cls, config: Dict[str, Any]) -> Optional[str]: + def validate_executor_config(cls, config: Dict[str, Any]): """ Validate that location is a string if given and no other options are given. """ if "location" in config and not isinstance(config["location"], str): - return ( + raise ConfigValidationError( "The location option virtualenv executor must be a string not: " - f"{config['location']!r}" + f"{config['location']!r}", + global_option="executor", ) extra_options = set(config.keys()) - {"type", "location"} if extra_options: - return f"Unexpected keys for executor config: {extra_options!r}" - return None + raise ConfigValidationError( + f"Unexpected keys for executor config: {extra_options!r}", + global_option="executor", + ) diff --git a/poethepoet/helpers/command/ast.py b/poethepoet/helpers/command/ast.py index 02ebc715..9eacc872 100644 --- a/poethepoet/helpers/command/ast.py +++ b/poethepoet/helpers/command/ast.py @@ -297,14 +297,11 @@ def _parse(self, chars: ParseCursor): return self.__consume_unquoted(chars) def __consume_single_quoted(self, chars): - # pylint: disable=invalid-name SingleQuotedTextCls = self.get_child_node_cls(SingleQuotedText) self._children.append(SingleQuotedTextCls(chars, self.config)) def __consume_double_quoted(self, chars): - # pylint: disable=invalid-name DoubleQuotedTextCls = self.get_child_node_cls(DoubleQuotedText) - # pylint: disable=invalid-name ParamExpansionCls = self.get_child_node_cls(ParamExpansion) while next_char := chars.peek(): @@ -329,11 +326,8 @@ def __consume_double_quoted(self, chars): raise ParseError("Unexpected end of input with unmatched double quote") def __consume_unquoted(self, chars): - # pylint: disable=invalid-name UnquotedTextCls = self.get_child_node_cls(UnquotedText) - # pylint: disable=invalid-name GlobCls = self.get_child_node_cls(Glob) - # pylint: disable=invalid-name ParamExpansionCls = self.get_child_node_cls(ParamExpansion) while next_char := chars.peek(): @@ -366,7 +360,6 @@ def segments(self) -> Tuple[Segment, ...]: return tuple(self._children) def _parse(self, chars: ParseCursor): - # pylint: disable=invalid-name SegmentCls = self.get_child_node_cls(Segment) self._children = [] @@ -395,9 +388,7 @@ def comment(self) -> str: return "" def _parse(self, chars: ParseCursor): - # pylint: disable=invalid-name WordCls = self.get_child_node_cls(Word) - # pylint: disable=invalid-name CommentCls = self.get_child_node_cls(Comment) self._children = [] @@ -439,7 +430,6 @@ def command_lines(self): return tuple(line for line in self._children if line.words) def _parse(self, chars: ParseCursor): - # pylint: disable=invalid-name LineCls = self.get_child_node_cls(Line) self._children = [] diff --git a/poethepoet/options.py b/poethepoet/options.py new file mode 100644 index 00000000..489de7e4 --- /dev/null +++ b/poethepoet/options.py @@ -0,0 +1,270 @@ +import collections +from keyword import iskeyword +from typing import ( + Any, + Dict, + List, + Literal, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + Type, + Union, + get_args, + get_origin, +) + +from .exceptions import ConfigValidationError + +NoValue = object() + + +class PoeOptions: + """ + A special kind of config object that parses options ... + """ + + __annotations: Dict[str, Type] + + def __init__(self, **options: Any): + for key in self.get_fields(): + unsanitized_key = key.rstrip("_") + if unsanitized_key in options: + setattr(self, key, options[unsanitized_key]) + + def __getattribute__(self, name: str): + if name.endswith("_") and iskeyword(name[:-1]): + # keyword attributes are accessed with a "_" suffix + name = name[:-1] + + return object.__getattribute__(self, name) + + def __getattr__(self, name: str): + if name not in self.get_fields(): + raise AttributeError( + f"{self.__class__.__name__} has no such attribute {name!r}" + ) + + if name in self.__dict__: + return self.__dict__[name] + if hasattr(self.__class__, name): + return getattr(self.__class__, name) + if self.__is_optional(name): + return None + + raise AttributeError( + f"{self.__class__.__name__} has no value for option {name!r}" + ) + + @classmethod + def parse( + cls, + source: Union[Mapping[str, Any], list], + strict: bool = True, + extra_keys: Sequence[str] = tuple(), + ): + config_keys = { + key[:-1] if key.endswith("_") and iskeyword(key[:-1]) else key: vtype + for key, vtype in cls.get_fields().items() + } + if strict: + for index, item in enumerate(cls.normalize(source, strict)): + options = {} + for key, value_type in config_keys.items(): + if key in item: + options[key] = cls._parse_value( + index, key, item[key], value_type, strict + ) + elif not hasattr(cls, cls._resolve_key(key)): + raise ConfigValidationError( + f"Missing required option {key!r}", index=index + ) + + for key in item: + if key not in config_keys and key not in extra_keys: + raise ConfigValidationError( + f"Unrecognised option {key!r}", index=index + ) + + result = cls(**options) + result.validate() + yield result + + else: + for index, item in enumerate(cls.normalize(source, strict)): + yield cls( + **{ + key: cls._parse_value(index, key, item[key], value_type, strict) + for key, value_type in config_keys.items() + if key in item + } + ) + + @classmethod + def _parse_value( + cls, index: int, key: str, value: Any, value_type: Any, strict: bool + ): + if isinstance(value_type, type) and issubclass(value_type, PoeOptions): + return value_type.parse(value, strict=strict) + + if strict: + expected_type: Union[Type, Tuple[Type, ...]] = cls._type_of(value_type) + if not isinstance(value, expected_type): + # Try format expected_type nicely in the error message + if not isinstance(expected_type, tuple): + expected_type = (expected_type,) + formated_type = " | ".join( + type_.__name__ for type_ in expected_type if type_ is not type(None) + ) + raise ConfigValidationError( + f"Option {key!r} should have a value of type: {formated_type}", + index=index, + ) + + annotation = cls.get_annotation(key) + if get_origin(annotation) is Literal: + allowed_values = get_args(annotation) + if value not in allowed_values: + raise ConfigValidationError( + f"Option {key!r} must be one of {allowed_values!r}", + index=index, + ) + + # TODO: validate list/dict contents + + return value + + @classmethod + def normalize( + cls, + config: Any, + strict: bool = True, + ): + if isinstance(config, (list, tuple)): + yield from config + else: + yield config + + def validate(self): + pass + + def get(self, key: str, default: Any = NoValue) -> Any: + """ + This is the most tolerant way to fetch a config value using the following + strategies in priority order: + + 1. Get the config value + 2. Return the default value provided as an argument + 3. Return the default value declared for this field + 4. Return the zero value for the type of this field + """ + + key = self._resolve_key(key) + + if key in self.__dict__: + return self.__dict__[key] + + if default is NoValue: + default = getattr(self.__class__, key, default) + if default is NoValue: + # Fallback to getting getting the zero value for the type of this attribute + # e.g. 0, False, empty list, empty dict, etc + return self.__get_zero_value(key) + + return default + + def __get_zero_value(self, key: str): + type_of_attr = self.type_of(key) + if isinstance(type_of_attr, tuple): + if type(None) in type_of_attr: + # Optional types default to None + return None + type_of_attr = type_of_attr[0] + assert type_of_attr + return type_of_attr() + + def __is_optional(self, key: str): + # TODO: precache optional options keys? + type_of_attr = self.type_of(key) + if isinstance(type_of_attr, tuple): + return type(None) in type_of_attr + return False + + def update(self, options_dict: Dict[str, Any]): + new_options_dict = {} + for key in self.get_fields().keys(): + if key in options_dict: + new_options_dict[key] = options_dict[key] + elif hasattr(self, key): + new_options_dict[key] = getattr(self, key) + + @classmethod + def type_of(cls, key: str) -> Optional[Union[Type, Tuple[Type, ...]]]: + return cls._type_of(cls.get_annotation(key)) + + @classmethod + def get_annotation(cls, key: str) -> Optional[Type]: + return cls.get_fields().get(cls._resolve_key(key)) + + @classmethod + def _resolve_key(cls, key: str) -> str: + """ + Map from a config key to the config object attribute, which must not but a + python keyword. + """ + if iskeyword(key): + return f"{key}_" + return key + + @classmethod + def _type_of(cls, annotation: Any) -> Union[Type, Tuple[Type, ...]]: + if get_origin(annotation) is Union: + result: List[Type] = [] + for component in get_args(annotation): + component_type = cls._type_of(component) + if isinstance(component_type, tuple): + result.extend(component_type) + else: + result.append(component_type) + return tuple(result) + + if get_origin(annotation) in ( + dict, + Mapping, + MutableMapping, + collections.abc.Mapping, + collections.abc.MutableMapping, + ): + return dict + + if get_origin(annotation) in ( + list, + Sequence, + collections.abc.Sequence, + ): + return list + + if get_origin(annotation) is Literal: + return tuple({type(arg) for arg in get_args(annotation)}) + + return annotation + + @classmethod + def get_fields(cls) -> Dict[str, Any]: + """ + Recent python versions removed inheritance for __annotations__ + so we have to implement it explicitly + """ + if not hasattr(cls, "__annotations"): + annotations = {} + for base_cls in cls.__bases__: + annotations.update(base_cls.__annotations__) + annotations.update(cls.__annotations__) + cls.__annotations = { + key: type_ + for key, type_ in annotations.items() + if not key.startswith("_") + } + return cls.__annotations diff --git a/poethepoet/plugin.py b/poethepoet/plugin.py index f17614af..18d5a7bc 100644 --- a/poethepoet/plugin.py +++ b/poethepoet/plugin.py @@ -1,5 +1,3 @@ -# pylint: disable=import-error - from pathlib import Path from typing import Any, Dict, List @@ -52,7 +50,6 @@ def get_poe(cls, application: Application, io: IO): from poetry.utils.env import EnvManager poetry_env_path = EnvManager(application.poetry).get().path - # pylint: disable=bare-except except: # noqa: E722 poetry_env_path = None @@ -84,7 +81,6 @@ def activate(self, application: Application) -> None: try: return self._activate(application) - # pylint: disable=bare-except except: # noqa: E722 import os import sys @@ -152,8 +148,6 @@ def _activate(self, application: Application) -> None: def _get_config(cls, application: Application) -> Dict[str, Any]: try: pyproject = application.poetry.pyproject.data - - # pylint: disable=bare-except except: # noqa: E722 # Fallback to loading the config again in case of future failure of the # above undocumented API @@ -161,7 +155,17 @@ def _get_config(cls, application: Application) -> Dict[str, Any]: from .config import PoeConfig - pyproject = tomlkit.loads(Path(PoeConfig().find_config_file()).read_text()) + # Try respect poetry's '--directory' if set + try: + pyproject_path = Path(application.poetry.pyproject_path) + except AttributeError: + pyproject_path = None + + pyproject = tomlkit.loads( + Path( + PoeConfig().find_config_file(target_path=pyproject_path) + ).read_text() + ) return pyproject.get("tool", {}).get("poe", {}) diff --git a/poethepoet/task/__init__.py b/poethepoet/task/__init__.py index 1a851d1c..da462a38 100644 --- a/poethepoet/task/__init__.py +++ b/poethepoet/task/__init__.py @@ -1,4 +1,3 @@ -from .base import PoeTask from .cmd import CmdTask from .expr import ExprTask from .ref import RefTask @@ -8,7 +7,6 @@ from .switch import SwitchTask __all__ = [ - "PoeTask", "CmdTask", "ExprTask", "RefTask", diff --git a/poethepoet/task/args.py b/poethepoet/task/args.py index ec32e91e..bd86ac70 100644 --- a/poethepoet/task/args.py +++ b/poethepoet/task/args.py @@ -3,9 +3,10 @@ Any, Dict, List, + Literal, + Mapping, Optional, Sequence, - Set, Tuple, Type, Union, @@ -16,6 +17,9 @@ from ..env.manager import EnvVarsManager +from ..exceptions import ConfigValidationError +from ..options import PoeOptions + ArgParams = Dict[str, Any] ArgsDef = Union[List[str], List[ArgParams], Dict[str, ArgParams]] @@ -37,61 +41,170 @@ } -class PoeTaskArgs: - _args: Tuple[ArgParams, ...] - - def __init__( - self, - args_def: ArgsDef, - task_name: str, - program_name: str, - env: "EnvVarsManager", - ): - self._args = self._normalize_args_def(args_def) - self._program_name = program_name - self._task_name = task_name - self._env = env +class ArgSpec(PoeOptions): + default: Optional[Union[str, int, float, bool]] = None + help: str = "" + name: str + options: Union[list, tuple] + positional: Union[bool, str] = False + required: bool = False + type: Literal["string", "float", "integer", "boolean"] = "string" + multiple: Union[bool, int] = False @classmethod - def _normalize_args_def(cls, args_def: ArgsDef) -> Tuple[ArgParams, ...]: + def normalize(cls, args_def: ArgsDef, strict: bool = True): """ - args_def can be defined as a dictionary of ArgParams, or a list of strings, or - ArgParams. Here we normalize it to a list of ArgParams, assuming that it has - already been validated. + Becuase arguments can be declared with different structures + (i.e. dict or list), this function normalizes the input into a list of + dictionaries with necessary keys. + + This is also where we do any validation that requires access to the raw + config. """ - result = [] if isinstance(args_def, list): for item in args_def: if isinstance(item, str): - result.append({"name": item, "options": (f"--{item}",)}) - else: - result.append( - dict( - item, - options=cls._get_arg_options_list(item), - ) + yield {"name": item, "options": (f"--{item}",)} + elif isinstance(item, dict): + yield dict( + item, + options=cls._get_arg_options_list(item, strict=strict), ) - else: + elif strict: + raise ConfigValidationError( + f"Argument {item!r} has invlaid type, a string or dict is " + "expected" + ) + + elif isinstance(args_def, dict): for name, params in args_def.items(): - result.append( - dict( - params, - name=name, - options=cls._get_arg_options_list(params, name), + if strict and "name" in params: + raise ConfigValidationError( + f"Unexpected 'name' option for argument {name!r}" ) + yield dict( + params, + name=name, + options=cls._get_arg_options_list(params, name, strict), ) - return tuple(result) + + @classmethod + def parse( + cls, + source: Union[Mapping[str, Any], list], + strict: bool = True, + extra_keys: Sequence[str] = tuple(), + ): + """ + Override parse function to perform validations that require considering all + argument declarations at once. + """ + result = tuple(super().parse(source, strict, extra_keys)) + + if strict: + arg_names = set() + positional_multiple = None + for arg in result: + if arg.name in arg_names: + raise ConfigValidationError(f"Duplicate argument name {arg.name!r}") + arg_names.add(arg.name) + + if arg.positional: + if positional_multiple: + raise ConfigValidationError( + f"Only the last positional arg of task may accept" + f" multiple values (not {positional_multiple!r})." + ) + if arg.multiple: + positional_multiple = arg.name + + yield from result @staticmethod - def _get_arg_options_list(arg: ArgParams, name: Optional[str] = None): + def _get_arg_options_list( + arg: ArgParams, name: Optional[str] = None, strict: bool = True + ): position = arg.get("positional", False) - name = name or arg["name"] + name = name or arg.get("name") if position: + if strict and arg.get("options"): + raise ConfigValidationError( + f"Positional argument {name!r} may not declare options" + ) + # Fill in the options param in a way that makes sesne for argparse if isinstance(position, str): return [position] return [name] return tuple(arg.get("options", (f"--{name}",))) + def validate(self): + try: + return self._validate() + except ConfigValidationError as error: + error.context = f"Invalid argument {self.name!r} declared" + raise + + def _validate(self): + if not self.name.replace("-", "_").isidentifier(): + raise ConfigValidationError( + f"Argument name {self.name!r} is not a valid 'identifier',\n" + f"see the following documentation for details " + f"https://docs.python.org/3/reference/lexical_analysis.html#identifiers" + ) + + if self.positional: + if self.type == "boolean": + raise ConfigValidationError( + f"Positional argument {self.name!r} may not have type 'boolean'" + ) + + if isinstance(self.positional, str) and not self.positional.isidentifier(): + raise ConfigValidationError( + f"positional name {self.positional!r} for arg {self.name!r} is " + "not a valid 'identifier'\n" + "see the following documentation for details " + "https://docs.python.org/3/reference/lexical_analysis.html#identifiers" + ) + + if ( + not isinstance(self.multiple, bool) + and isinstance(self.multiple, int) + and self.multiple < 2 + ): + raise ConfigValidationError( + "The 'multiple' option accepts a boolean or integer >= 2" + ) + + if self.multiple is not False and self.type == "boolean": + raise ConfigValidationError( + "Argument with type 'boolean' may not delcare option 'multiple'" + ) + + +class PoeTaskArgs: + _args: Tuple[ArgSpec, ...] + + def __init__(self, args_def: ArgsDef, task_name: str): + self._task_name = task_name + self._args = self._parse_args_def(args_def) + + def _parse_args_def(self, args_def: ArgsDef): + try: + return tuple(ArgSpec.parse(args_def)) + except ConfigValidationError as error: + if isinstance(error.index, int): + if isinstance(args_def, list): + item = args_def[error.index] + if arg_name := (isinstance(item, dict) and item.get("name")): + arg_ref = arg_name + elif arg_name := tuple(args_def.keys())[error.index]: + arg_ref = arg_name + else: + arg_ref = error.index + error.context = f"Invalid argument {arg_ref!r} declared" + error.task_name = self._task_name + raise + @classmethod def get_help_content( cls, args_def: Optional[ArgsDef] @@ -107,165 +220,30 @@ def format_default(arg) -> str: return [ (arg["options"], arg.get("help", ""), format_default(arg)) - for arg in cls._normalize_args_def(args_def) + for arg in ArgSpec.normalize(args_def, strict=False) ] - @classmethod - def validate_def(cls, task_name: str, args_def: ArgsDef) -> Optional[str]: - arg_names: Set[str] = set() - arg_params = [] - - if isinstance(args_def, list): - for item in args_def: - # can be a list of strings (just arg name) or ArgConfig dictionaries - if isinstance(item, str): - arg_name = item - elif isinstance(item, dict): - arg_name = item.get("name", "") - arg_params.append((item, arg_name, task_name)) - else: - return f"Arg {item!r} of task {task_name!r} has invlaid type" - error = cls._validate_name(arg_name, task_name, arg_names) - if error: - return error - - elif isinstance(args_def, dict): - for arg_name, params in args_def.items(): - error = cls._validate_name(arg_name, task_name, arg_names) - if error: - return error - if "name" in params: - return ( - f"Unexpected 'name' option for arg {arg_name!r} of task " - f"{task_name!r}" - ) - arg_params.append((params, arg_name, task_name)) - - positional_multiple = None - for params, arg_name, task_name in arg_params: - error = cls._validate_type(params, arg_name, task_name) - if error: - return error - - error = cls._validate_params(params, arg_name, task_name) - if error: - return error - - if params.get("positional", False): - if positional_multiple: - return ( - f"Only the last positional arg of task {task_name!r} may accept" - f" multiple values ({positional_multiple!r})." - ) - if params.get("multiple", False): - positional_multiple = arg_name - - return None - - @classmethod - def _validate_name( - cls, name: Any, task_name: str, arg_names: Set[str] - ) -> Optional[str]: - if not isinstance(name, str): - return f"Arg name {name!r} of task {task_name!r} should be a string" - if not name.replace("-", "_").isidentifier(): - return ( - f"Arg name {name!r} of task {task_name!r} is not a valid 'identifier'" - f"see the following documentation for details" - f"https://docs.python.org/3/reference/lexical_analysis.html#identifiers" - ) - if name in arg_names: - return f"Duplicate arg name {name!r} for task {task_name!r}" - arg_names.add(name) - return None - - @classmethod - def _validate_params( - cls, params: ArgParams, arg_name: str, task_name: str - ) -> Optional[str]: - for param, value in params.items(): - if param not in arg_param_schema: - return ( - f"Invalid option {param!r} for arg {arg_name!r} of task " - f"{task_name!r}" - ) - if not isinstance(value, arg_param_schema[param]): - return ( - f"Invalid value for option {param!r} of arg {arg_name!r} of" - f" task {task_name!r}" - ) - - positional = params.get("positional", False) - if positional: - if params.get("type") == "boolean": - return ( - f"Positional argument {arg_name!r} of task {task_name!r} may not" - "have type 'boolean'" - ) - if params.get("options") is not None: - return ( - f"Positional argument {arg_name!r} of task {task_name!r} may not" - "have options defined" - ) - if isinstance(positional, str) and not positional.isidentifier(): - return ( - f"positional name {positional!r} for arg {arg_name!r} of task " - f"{task_name!r} is not a valid 'identifier' see the following " - "documentation for details" - "https://docs.python.org/3/reference/lexical_analysis.html#identifiers" - ) - - multiple = params.get("multiple", False) - if ( - not isinstance(multiple, bool) - and isinstance(multiple, int) - and multiple < 2 - ): - return ( - f"The multiple option for arg {arg_name!r} of {task_name!r}" - " must be given a boolean or integer >= 2" - ) - if multiple is not False and params.get("type") == "boolean": - return ( - "Incompatible param 'multiple' for arg {arg_name!r} of {task_name!r} " - "with type: 'boolean'" - ) - - return None - - @classmethod - def _validate_type( - cls, params: ArgParams, arg_name: str, task_name: str - ) -> Optional[str]: - if "type" in params and params["type"] not in arg_types: - return ( - f"{params['type']!r} is not a valid type for arg {arg_name!r} of task " - f"{task_name!r}. Choose one of " - "{" - f'{" ".join(sorted(str_type for str_type in arg_types.keys()))}' - "}" - ) - return None - - def build_parser(self) -> "ArgumentParser": + def build_parser( + self, env: "EnvVarsManager", program_name: str + ) -> "ArgumentParser": import argparse parser = argparse.ArgumentParser( - prog=f"{self._program_name} {self._task_name}", + prog=f"{program_name} {self._task_name}", add_help=False, allow_abbrev=False, ) for arg in self._args: parser.add_argument( - *arg["options"], - **self._get_argument_params(arg), + *arg.options, + **self._get_argument_params(arg, env), ) return parser - def _get_argument_params(self, arg: ArgParams): + def _get_argument_params(self, arg: ArgSpec, env: "EnvVarsManager"): default = arg.get("default") if isinstance(default, str): - default = self._env.fill_template(default) + default = env.fill_template(default) result = { "default": default, @@ -288,7 +266,7 @@ def _get_argument_params(self, arg: ArgParams): if not multiple and not required: result["nargs"] = "?" else: - result["dest"] = arg["name"] + result["dest"] = arg.name result["required"] = required if arg_type == "boolean": @@ -298,15 +276,13 @@ def _get_argument_params(self, arg: ArgParams): return result - def parse(self, args: Sequence[str]) -> Dict[str, str]: - parsed_args = vars(self.build_parser().parse_args(args)) - + def parse(self, args: Sequence[str], env: "EnvVarsManager", program_name: str): + parsed_args = vars(self.build_parser(env, program_name).parse_args(args)) # Ensure positional args are still exposed by name even if they were parsed with # alternate identifiers for arg in self._args: - if isinstance(arg.get("positional"), str): - parsed_args[arg["name"]] = parsed_args[arg["positional"]] - del parsed_args[arg["positional"]] - + if isinstance(arg.positional, str): + parsed_args[arg.name] = parsed_args[arg.positional] + del parsed_args[arg.positional] # args named with dash case are converted to snake case before being exposed return {name.replace("-", "_"): value for name, value in parsed_args.items()} diff --git a/poethepoet/task/base.py b/poethepoet/task/base.py index 850fd526..ec2b7c59 100644 --- a/poethepoet/task/base.py +++ b/poethepoet/task/base.py @@ -5,28 +5,30 @@ from typing import ( TYPE_CHECKING, Any, - Collection, + ClassVar, Dict, Iterator, List, + Mapping, NamedTuple, Optional, + Sequence, Tuple, Type, Union, ) -from ..exceptions import PoeException +from ..exceptions import ConfigValidationError, PoeException +from ..options import PoeOptions if TYPE_CHECKING: - from ..config import PoeConfig + from ..config import ConfigPartition, PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager from ..ui import PoeUi + from .args import PoeTaskArgs -TaskDef = Union[str, Dict[str, Any], List[Union[str, Dict[str, Any]]]] - _TASK_NAME_PATTERN = re.compile(r"^\w[\w\d\-\_\+\:]*$") @@ -40,154 +42,342 @@ def __init__(cls, *args): super().__init__(*args) if cls.__name__ == "PoeTask": return + assert isinstance(getattr(cls, "__key__", None), str) - assert isinstance(getattr(cls, "__options__", None), dict) + assert issubclass(getattr(cls, "TaskOptions", None), PoeOptions) PoeTask._PoeTask__task_types[cls.__key__] = cls + # Give each TaskSpec a reference to its parent PoeTask + if "TaskSpec" in cls.__dict__: + cls.TaskSpec.task_type = cls + + +TaskContent = Union[str, Sequence[Union[str, Mapping[str, Any]]]] + +TaskDef = Union[str, Mapping[str, Any], Sequence[Union[str, Mapping[str, Any]]]] + -TaskContent = Union[str, List[Union[str, Dict[str, Any]]]] +class TaskSpecFactory: + __cache: Dict[str, "PoeTask.TaskSpec"] + config: "PoeConfig" + + def __init__(self, config: "PoeConfig"): + self.__cache = {} + self.config = config + + def __contains__(self, other) -> bool: + return other in self.__cache + + def get( + self, + task_name: str, + task_def: Optional[TaskDef] = None, + task_type: Optional[str] = None, + parent: Optional["PoeTask.TaskSpec"] = None, + ) -> "PoeTask.TaskSpec": + if task_def and parent: + # This is probably a subtask and will be cached by the parent task_spec + if not task_type: + task_type = PoeTask.resolve_task_type(task_def, self.config) + assert task_type + return self.create( + task_name, task_type, task_def, source=parent.source, parent=parent + ) + + if task_name not in self.__cache: + self.load(task_name) + + return self.__cache[task_name] + + def create( + self, + task_name: str, + task_type: str, + task_def: TaskDef, + source: "ConfigPartition", + parent: Optional["PoeTask.TaskSpec"] = None, + ) -> "PoeTask.TaskSpec": + """ + A parent task should be provided when this task is defined inline within another + task, for example as part of a sequence. + """ + if not isinstance(task_def, dict): + task_def = {task_type: task_def} + + return PoeTask.lookup_task_spec_cls(task_type)( + name=task_name, + task_def=task_def, + factory=self, + source=source, + parent=parent, + ) + def load_all(self): + for task_name in self.config.task_names: + self.load(task_name) -class TaskInheritance(NamedTuple): + return self + + def load(self, task_name: str): + task_def, config_partition = self.config.lookup_task(task_name) + + if task_def is None or config_partition is None: + raise PoeException(f"Cannot instantiate unknown task {task_name!r}") + + task_type = PoeTask.resolve_task_type(task_def, self.config) + if not task_type: + raise ConfigValidationError( + "Task definition must be a string, a list, or a table including exactly" + " one task key\n" + f"Available task keys: {set(PoeTask.get_task_types())!r}", + task_name=task_name, + filename=( + None if config_partition.is_primary else str(config_partition.path) + ), + ) + + self.__cache[task_name] = self.create( + task_name, task_type, task_def, source=config_partition + ) + + def __iter__(self): + return iter(self.__cache.values()) + + +class TaskContext(NamedTuple): """ - Collection of inheritanced config from a parent task to a child task + Collection of contextual config inherited from a parent task to a child task """ + config: "PoeConfig" cwd: str + ui: "PoeUi" + specs: "TaskSpecFactory" @classmethod def from_task(cls, parent_task: "PoeTask"): - return cls(cwd=str(parent_task.options.get("cwd", parent_task.inheritance.cwd))) + return cls( + config=parent_task.ctx.config, + cwd=str(parent_task.spec.options.get("cwd", parent_task.ctx.cwd)), + specs=parent_task.ctx.specs, + ui=parent_task.ctx.ui, + ) class PoeTask(metaclass=MetaPoeTask): - name: str - content: TaskContent - options: Dict[str, Any] - inheritance: TaskInheritance - _parsed_args: Optional[Tuple[Dict[str, str], Tuple[str, ...]]] = None + __key__: ClassVar[str] + __content_type__: ClassVar[Type] = str + + class TaskOptions(PoeOptions): + args: Optional[Union[dict, list]] = None + capture_stdout: Optional[str] = None + cwd: Optional[str] = None + deps: Optional[Sequence[str]] = None + env: Optional[dict] = None + envfile: Optional[Union[str, list]] = None + executor: Optional[dict] = None + help: Optional[str] = None + uses: Optional[dict] = None + + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + if self.help and "\n" in self.help: + raise ConfigValidationError( + "Help messages must not contain line breaks" + ) + + class TaskSpec: + name: str + content: TaskContent + options: "PoeTask.TaskOptions" + task_type: Type["PoeTask"] + source: "ConfigPartition" + parent: Optional["PoeTask.TaskSpec"] = None + + _args: Optional["PoeTaskArgs"] = None + + def __init__( + self, + name: str, + task_def: Dict[str, Any], + factory: TaskSpecFactory, + source: "ConfigPartition", + parent: Optional["PoeTask.TaskSpec"] = None, + ): + self.name = name + self.content = task_def[self.task_type.__key__] + self.options = self._parse_options(task_def) + self.source = source + self.parent = parent + + def _parse_options(self, task_def: Dict[str, Any]): + try: + return next( + self.task_type.TaskOptions.parse( + task_def, extra_keys=(self.task_type.__key__,) + ) + ) + except ConfigValidationError as error: + error.task_name = self.name + raise + + def get_task_env( + self, + parent_env: "EnvVarsManager", + uses_values: Optional[Mapping[str, str]] = None, + ) -> "EnvVarsManager": + """ + Resolve the EnvVarsManager for this task, relative to the given parent_env + """ + task_envfile = self.options.get("envfile") + task_env = self.options.get("env") + + result = parent_env.clone() + + # Include env vars from outputs of upstream dependencies + if uses_values: + result.update(uses_values) + + result.set("POE_CONF_DIR", str(self.source.config_dir)) + result.apply_env_config( + task_envfile, + task_env, + config_dir=self.source.config_dir, + config_working_dir=self.source.cwd, + ) + + return result + + @property + def args(self) -> Optional["PoeTaskArgs"]: + from .args import PoeTaskArgs + + if not self._args and self.options.args: + self._args = PoeTaskArgs(self.options.args, self.name) - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} - __content_type__: Type = str - __base_options: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "args": (dict, list), - "capture_stdout": str, - "cwd": str, - "deps": list, - "env": dict, - "envfile": (str, list), - "executor": dict, - "help": str, - "uses": dict, - } - __task_types: Dict[str, Type["PoeTask"]] = {} + return self._args + def create_task( + self, + invocation: Tuple[str, ...], + ctx: TaskContext, + capture_stdout: Union[str, bool] = False, + ) -> "PoeTask": + return self.task_type( + spec=self, + invocation=invocation, + capture_stdout=capture_stdout, + ctx=ctx, + ) + + def validate(self, config: "PoeConfig", task_specs: TaskSpecFactory): + try: + self._base_validations(config, task_specs) + self._task_validations(config, task_specs) + except ConfigValidationError as error: + error.task_name = self.name + raise + + def _base_validations(self, config: "PoeConfig", task_specs: TaskSpecFactory): + """ + Perform validations on this TaskSpec that apply to all task types + """ + if not (self.name[0].isalpha() or self.name[0] == "_"): + raise ConfigValidationError( + "Task names must start with a letter or underscore." + ) + + if not self.parent and not _TASK_NAME_PATTERN.match(self.name): + raise ConfigValidationError( + "Task names characters must be alphanumeric, colon, underscore or " + "dash." + ) + + if not isinstance(self.content, self.task_type.__content_type__): + raise ConfigValidationError( + f"Content for {self.task_type.__name__} must be a " + f"{self.task_type.__content_type__.__name__}" + ) + + if self.options.deps: + for dep in self.options.deps: + dep_task_name = dep.split(" ", 1)[0] + if dep_task_name not in task_specs: + raise ConfigValidationError( + "'deps' option includes reference to unknown task: " + f"{dep_task_name!r}" + ) + + if task_specs.get(dep_task_name).options.get("use_exec", False): + raise ConfigValidationError( + f"'deps' option includes reference to task with " + f"'use_exec' set to true: {dep_task_name!r}" + ) + + if self.options.uses: + from ..helpers import is_valid_env_var + + for key, dep in self.options.uses.items(): + if not is_valid_env_var(key): + raise ConfigValidationError( + f"'uses' option includes invalid key: {key!r}" + ) + + dep_task_name = dep.split(" ", 1)[0] + if dep_task_name not in task_specs: + raise ConfigValidationError( + "'uses' options includes reference to unknown task: " + f"{dep_task_name!r}" + ) + + referenced_task = task_specs.get(dep_task_name) + if referenced_task.options.get("use_exec", False): + raise ConfigValidationError( + f"'uses' option references task with 'use_exec' set to " + f"true: {dep_task_name!r}" + ) + if referenced_task.options.get("capture_stdout"): + raise ConfigValidationError( + f"'uses' option references task with 'capture_stdout' " + f"option set: {dep_task_name!r}" + ) + + def _task_validations(self, config: "PoeConfig", task_specs: TaskSpecFactory): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + + spec: TaskSpec + ctx: TaskContext + _parsed_args: Optional[Tuple[Dict[str, str], Tuple[str, ...]]] = None + + __task_types: ClassVar[Dict[str, Type["PoeTask"]]] = {} __upstream_invocations: Optional[ Dict[str, Union[List[Tuple[str, ...]], Dict[str, Tuple[str, ...]]]] ] = None def __init__( self, - name: str, - content: TaskContent, - options: Dict[str, Any], - ui: "PoeUi", - config: "PoeConfig", + spec: TaskSpec, invocation: Tuple[str, ...], - capture_stdout: bool = False, - inheritance: Optional[TaskInheritance] = None, + ctx: TaskContext, + capture_stdout: Union[str, bool] = False, ): - self.name = name - self.content = content - self.options = dict(options, capture_stdout=True) if capture_stdout else options - self._ui = ui - self._config = config - self._is_windows = sys.platform == "win32" + self.spec = spec self.invocation = invocation - self.inheritance = inheritance or TaskInheritance(cwd=str(config.project_dir)) - - @classmethod - def from_config( - cls, - task_name: str, - config: "PoeConfig", - ui: "PoeUi", - invocation: Tuple[str, ...], - capture_stdout: Optional[bool] = None, - inheritance: Optional[TaskInheritance] = None, - ) -> "PoeTask": - task_def = config.tasks.get(task_name) - if not task_def: - raise PoeException(f"Cannot instantiate unknown task {task_name!r}") - return cls.from_def( - task_def, - task_name, - config, - ui, - invocation=invocation, - capture_stdout=capture_stdout, - inheritance=inheritance, - ) - - @classmethod - def from_def( - cls, - task_def: TaskDef, - task_name: str, - config: "PoeConfig", - ui: "PoeUi", - invocation: Tuple[str, ...], - array_item: Union[bool, str] = False, - capture_stdout: Optional[bool] = None, - inheritance: Optional[TaskInheritance] = None, - ) -> "PoeTask": - task_type = cls.resolve_task_type(task_def, config, array_item) - if task_type is None: - # Something is wrong with this task_def - raise cls.Error(cls.validate_def(task_name, task_def, config)) - - options: Dict[str, Any] = {} - if capture_stdout is not None: - # Override config because we want to specifically capture the stdout of this - # task for internal use - options["capture_stdout"] = capture_stdout - - if isinstance(task_def, (str, list)): - task_def = cls.normalize_task_def( - task_def, config, task_type=cls.__task_types[task_type] - ) + self.ctx = ctx + self.capture_stdout = spec.options.capture_stdout or capture_stdout + self._is_windows = sys.platform == "win32" - assert isinstance(task_def, dict) - options = dict(task_def, **options) - content = options.pop(task_type) - return cls.__task_types[task_type]( - name=task_name, - content=content, - options=options, - ui=ui, - config=config, - invocation=invocation, - inheritance=inheritance, - ) + @property + def name(self): + return self.spec.name @classmethod - def normalize_task_def( - cls, - task_def: TaskDef, - config: "PoeConfig", - *, - task_type: Optional[Type["PoeTask"]] = None, - array_item: Union[bool, str] = False, - ): - if isinstance(task_def, dict): - return task_def - - if not task_type: - task_type_key = cls.resolve_task_type(task_def, config, array_item) - assert task_type_key - task_type = cls.__task_types[task_type_key] - - return {getattr(task_type, "__key__", "__key_unknown__"): task_def} + def lookup_task_spec_cls(cls, task_key: str) -> Type[TaskSpec]: + return cls.__task_types[task_key].TaskSpec @classmethod def resolve_task_type( @@ -216,15 +406,21 @@ def resolve_task_type( return None + def _parse_named_args( + self, extra_args: Sequence[str], env: "EnvVarsManager" + ) -> Optional[Dict[str, str]]: + if task_args := self.spec.args: + return task_args.parse(extra_args, env, self.ctx.ui.program_name) + + return None + def get_parsed_arguments( self, env: "EnvVarsManager" ) -> Tuple[Dict[str, str], Tuple[str, ...]]: if self._parsed_args is None: all_args = self.invocation[1:] - if args_def := self.options.get("args"): - from .args import PoeTaskArgs - + if task_args := self.spec.args: try: split_index = all_args.index("--") option_args = all_args[:split_index] @@ -234,9 +430,7 @@ def get_parsed_arguments( extra_args = tuple() self._parsed_args = ( - PoeTaskArgs(args_def, self.name, self._ui.program_name, env).parse( - option_args - ), + task_args.parse(option_args, env, self.ctx.ui.program_name), extra_args, ) @@ -272,11 +466,9 @@ def run( ) return 0 - task_env = context.get_task_env( - parent_env, - self.options.get("envfile"), - self.options.get("env"), - upstream_invocations["uses"], + task_env = self.spec.get_task_env( + parent_env or context.env, + context._get_dep_values(upstream_invocations["uses"]), ) if environ.get("POE_DEBUG"): @@ -302,19 +494,19 @@ def _get_executor(self, context: "RunContext", env: "EnvVarsManager"): self.invocation, env, working_dir=self.get_working_dir(env), - executor_config=self.options.get("executor"), - capture_stdout=self.options.get("capture_stdout", False), + executor_config=self.spec.options.get("executor"), + capture_stdout=self.capture_stdout, ) def get_working_dir( self, env: "EnvVarsManager", ) -> Path: - cwd_option = env.fill_template(self.options.get("cwd", self.inheritance.cwd)) + cwd_option = env.fill_template(self.spec.options.get("cwd", self.ctx.cwd)) working_dir = Path(cwd_option) if not working_dir.is_absolute(): - working_dir = self._config.project_dir / working_dir + working_dir = self.ctx.config.project_dir / working_dir return working_dir @@ -336,20 +528,20 @@ def _get_upstream_invocations(self, context: "RunContext"): """ import shlex + options = self.spec.options + if self.__upstream_invocations is None: - env = context.get_task_env( - None, self.options.get("envfile"), self.options.get("env") - ) + env = self.spec.get_task_env(context.env) env.update(self.get_parsed_arguments(env)[0]) self.__upstream_invocations = { "deps": [ tuple(shlex.split(env.fill_template(task_ref))) - for task_ref in self.options.get("deps", tuple()) + for task_ref in options.get("deps", tuple()) ], "uses": { key: tuple(shlex.split(env.fill_template(task_ref))) - for key, task_ref in self.options.get("uses", {}).items() + for key, task_ref in options.get("uses", {}).items() }, } @@ -358,175 +550,21 @@ def _get_upstream_invocations(self, context: "RunContext"): def _instantiate_dep( self, invocation: Tuple[str, ...], capture_stdout: bool ) -> "PoeTask": - return self.from_config( - invocation[0], - config=self._config, - ui=self._ui, + return self.ctx.specs.get(invocation[0]).create_task( invocation=invocation, + ctx=TaskContext( + config=self.ctx.config, + cwd=str(self.ctx.config.project_dir), + specs=self.ctx.specs, + ui=self.ctx.ui, + ), capture_stdout=capture_stdout, ) def has_deps(self) -> bool: - return bool(self.options.get("deps", False) or self.options.get("uses", False)) - - @classmethod - def validate_def( - cls, - task_name: str, - task_def: TaskDef, - config: "PoeConfig", - *, - anonymous: bool = False, - extra_options: Collection[str] = tuple(), - ) -> Optional[str]: - """ - Check the given task name and definition for validity and return a message - describing the first encountered issue if any. - If anonymous is set to True then task_name is not validated. - """ - if not anonymous and (not (task_name[0].isalpha() or task_name[0] == "_")): - return ( - f"Invalid task name: {task_name!r}. Task names must start with a letter" - " or underscore." - ) - - if not anonymous and not _TASK_NAME_PATTERN.match(task_name): - return ( - f"Invalid task name: {task_name!r}. Task names characters must be " - "alphanumeric, colon, underscore or dash." - ) - - if isinstance(task_def, dict): - task_type_keys = set(task_def.keys()).intersection(cls.__task_types) - if len(task_type_keys) != 1: - return ( - f"Invalid task: {task_name!r}. Task definition must include exactly" - f" one task key from {set(cls.__task_types)!r}" - ) - task_type_key = next(iter(task_type_keys)) - task_content = task_def[task_type_key] - task_type = cls.__task_types[task_type_key] - if not isinstance(task_content, task_type.__content_type__): - return ( - f"Invalid task: {task_name!r}. Content for {task_type.__name__} " - f"must be a {task_type.__content_type__.__name__}" - ) - - for key in set(task_def) - {task_type_key}: - expected_type = cls.__base_options.get( - key, task_type.__options__.get(key) - ) - if expected_type is None: - if key not in extra_options: - return ( - f"Invalid task: {task_name!r}. Unrecognised option " - f"{key!r} for task of type: {task_type_key}." - ) - elif not isinstance(task_def[key], expected_type): - return ( - f"Invalid task: {task_name!r}. Option {key!r} should " - f"have a value of type {expected_type!r}" - ) - else: - if hasattr(task_type, "_validate_task_def"): - task_type_issue = task_type._validate_task_def( - task_name, task_def, config - ) - if task_type_issue: - return task_type_issue - - if "args" in task_def: - from .args import PoeTaskArgs - - return PoeTaskArgs.validate_def(task_name, task_def["args"]) - - if "cwd" in task_def: - path = Path(config.project_dir).joinpath(task_def["cwd"]).resolve() - if not str(path).startswith(config.project_dir): - return ( - f"Invalid task: {task_name!r}. cwd option may not specify a " - "directory outside of the project." - ) - - if "\n" in task_def.get("help", ""): - return ( - f"Invalid task: {task_name!r}. Help messages cannot contain " - "line breaks" - ) - - if task_def.get("use_exec") and task_def.get("capture_stdout"): - return ( - f"Invalid task: {task_name!r}, 'use_exec' and 'capture_stdout'" - " options cannot be both provided on the same task." - ) - - all_task_names = set(config.tasks.keys()) - - if "deps" in task_def: - for dep in task_def["deps"]: - dep_task_name = dep.split(" ", 1)[0] - if dep_task_name not in all_task_names: - return ( - f"Invalid task: {task_name!r}. deps options contains " - f"reference to unknown task: {dep_task_name!r}" - ) - - referenced_task = config.tasks[dep_task_name] - if isinstance(referenced_task, dict) and referenced_task.get( - "use_exec" - ): - return ( - f"Invalid task: {task_name!r}. deps options contains " - "reference to task with use_exec set to true: " - f"{dep_task_name!r}" - ) - - if "uses" in task_def: - from ..helpers import is_valid_env_var - - for key, dep in task_def["uses"].items(): - if not is_valid_env_var(key): - return ( - f"Invalid task: {task_name!r} uses options contains invalid" - f" key: {key!r}" - ) - - dep_task_name = dep.split(" ", 1)[0] - if dep_task_name not in all_task_names: - return ( - f"Invalid task: {task_name!r}. uses options contains " - f"reference to unknown task: {dep_task_name!r}" - ) - - referenced_task = config.tasks[dep_task_name] - if isinstance(referenced_task, dict): - if referenced_task.get("use_exec"): - return ( - f"Invalid task: {task_name!r}, 'uses' option references" - " task with 'use_exec' set to true: " - f"{dep_task_name!r}" - ) - if referenced_task.get("capture_stdout"): - return ( - f"Invalid task: {task_name!r}, 'uses' option references" - " task with 'capture_stdout' option set: " - f"{dep_task_name!r}" - ) - - elif isinstance(task_def, list): - task_type_key = config.default_array_task_type - task_type = cls.__task_types[task_type_key] - normalized_task_def = cls.normalize_task_def( - task_def, config, task_type=task_type - ) - if hasattr(task_type, "_validate_task_def"): - task_type_issue = task_type._validate_task_def( - task_name, normalized_task_def, config - ) - if task_type_issue: - return task_type_issue - - return None + return bool( + self.spec.options.get("deps", False) or self.spec.options.get("uses", False) + ) @classmethod def is_task_type( @@ -552,27 +590,13 @@ def get_task_types(cls, content_type: Optional[Type] = None) -> Tuple[str, ...]: ) return tuple(task_type for task_type in cls.__task_types.keys()) - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - """ - To be overriden by subclasses to check the given task definition for validity - specific to that task type and return a message describing the first encountered - issue if any. - """ - issue = None - return issue - def _print_action(self, action: str, dry: bool, unresolved: bool = False): """ Print the action taken by a task just before executing it. """ min_verbosity = -1 if dry else 0 - arrow = ( - "??" if unresolved else "<=" if self.options.get("capture_stdout") else "=>" - ) - self._ui.print_msg( + arrow = "??" if unresolved else "<=" if self.capture_stdout else "=>" + self.ctx.ui.print_msg( f"Poe {arrow} {action}", min_verbosity ) diff --git a/poethepoet/task/cmd.py b/poethepoet/task/cmd.py index 22eb8a58..fd175623 100644 --- a/poethepoet/task/cmd.py +++ b/poethepoet/task/cmd.py @@ -1,13 +1,14 @@ import shlex -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING -from ..exceptions import PoeException +from ..exceptions import ConfigValidationError, PoeException from .base import PoeTask if TYPE_CHECKING: from ..config import PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager + from .base import TaskSpecFactory class CmdTask(PoeTask): @@ -15,12 +16,34 @@ class CmdTask(PoeTask): A task consisting of a reference to a shell command """ - content: str - __key__ = "cmd" - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "use_exec": bool, - } + + class TaskOptions(PoeTask.TaskOptions): + use_exec: bool = False + + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + super().validate() + if self.use_exec and self.capture_stdout: + raise ConfigValidationError( + "'use_exec' and 'capture_stdout'" + " options cannot be both provided on the same task." + ) + + class TaskSpec(PoeTask.TaskSpec): + content: str + options: "CmdTask.TaskOptions" + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + if not self.content.strip(): + raise ConfigValidationError("Task has no content") + + spec: TaskSpec def _handle_run( self, @@ -35,7 +58,7 @@ def _handle_run( self._print_action(shlex.join(cmd), context.dry) return self._get_executor(context, env).execute( - cmd, use_exec=self.options.get("use_exec", False) + cmd, use_exec=self.spec.options.get("use_exec", False) ) def _resolve_commandline(self, context: "RunContext", env: "EnvVarsManager"): @@ -43,7 +66,7 @@ def _resolve_commandline(self, context: "RunContext", env: "EnvVarsManager"): from ..helpers.command.ast_core import ParseError try: - command_lines = parse_poe_cmd(self.content).command_lines + command_lines = parse_poe_cmd(self.spec.content).command_lines except ParseError as error: raise PoeException( f"Couldn't parse command line for task {self.name!r}: {error.args[0]}" @@ -71,12 +94,3 @@ def _resolve_commandline(self, context: "RunContext", env: "EnvVarsManager"): result.append(cmd_token) return result - - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - if not task_def["cmd"].strip(): - return f"Task {task_name!r} has no content" - - return None diff --git a/poethepoet/task/expr.py b/poethepoet/task/expr.py index ae5a5e0f..d65ed175 100644 --- a/poethepoet/task/expr.py +++ b/poethepoet/task/expr.py @@ -6,18 +6,19 @@ Iterable, Mapping, Optional, + Sequence, Tuple, - Type, Union, ) -from ..exceptions import ExpressionParseError +from ..exceptions import ConfigValidationError, ExpressionParseError from .base import PoeTask if TYPE_CHECKING: from ..config import PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager + from .base import TaskSpecFactory class ExprTask(PoeTask): @@ -28,11 +29,35 @@ class ExprTask(PoeTask): content: str __key__ = "expr" - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "imports": list, - "assert": (bool, int), - "use_exec": bool, - } + + class TaskOptions(PoeTask.TaskOptions): + imports: Sequence[str] = tuple() + assert_: Union[bool, int] = False + use_exec: bool = False + + def validate(self): + super().validate() + if self.use_exec and self.capture_stdout: + raise ConfigValidationError( + "'use_exec' and 'capture_stdout'" + " options cannot be both provided on the same task." + ) + + class TaskSpec(PoeTask.TaskSpec): + content: str + options: "ExprTask.TaskOptions" + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + try: + # ruff: noqa: E501 + self.task_type._substitute_env_vars(self.content.strip(), {}) # type: ignore[attr-defined] + except (ValueError, ExpressionParseError) as error: + raise ConfigValidationError(f"Invalid expression: {error}") + + spec: TaskSpec def _handle_run( self, @@ -44,13 +69,11 @@ def _handle_run( named_arg_values, extra_args = self.get_parsed_arguments(env) env.update(named_arg_values) - # TODO: do something about extra_args, error? - - imports = self.options.get("imports", tuple()) + imports = self.spec.options.imports expr, env_values = self.parse_content(named_arg_values, env, imports) argv = [ - self.name, + self.spec.name, *(env.fill_template(token) for token in self.invocation[1:]), ] @@ -63,7 +86,7 @@ def _handle_run( "print(result);", ] - falsy_return_code = int(self.options.get("assert", False)) + falsy_return_code = int(self.spec.options.get("assert")) if falsy_return_code: script.append(f"exit(0 if result else {falsy_return_code});") @@ -72,22 +95,11 @@ def _handle_run( # windows cmd = ("python", "-c", "".join(script)) - self._print_action(self.content.strip(), context.dry) + self._print_action(self.spec.content.strip(), context.dry) return self._get_executor(context, env).execute( - cmd, use_exec=self.options.get("use_exec", False) + cmd, use_exec=self.spec.options.use_exec ) - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - try: - cls._substitute_env_vars(task_def["expr"].strip(), {}) - except (ValueError, ExpressionParseError) as error: - return f"Task {task_name!r} contains invalid expression: {error}" - - return None - def parse_content( self, args: Optional[Dict[str, Any]], @@ -106,7 +118,7 @@ def parse_content( from ..helpers.python import resolve_expression expression, accessed_vars = self._substitute_env_vars( - self.content.strip(), env.to_dict() + self.spec.content.strip(), env.to_dict() ) expression = resolve_expression( @@ -124,7 +136,7 @@ def parse_content( @classmethod def _substitute_env_vars(cls, content: str, env: Mapping[str, str]): """ - Substitute ${template} references to env vars with a refernece to a python class + Substitute ${template} references to env vars with a reference to a python class attribute like __env.var, and collect the accessed env vars so we can construct that class with the required attributes later. """ diff --git a/poethepoet/task/ref.py b/poethepoet/task/ref.py index d637cff8..b8e11a63 100644 --- a/poethepoet/task/ref.py +++ b/poethepoet/task/ref.py @@ -1,11 +1,13 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING -from .base import PoeTask, TaskInheritance +from ..exceptions import ConfigValidationError +from .base import PoeTask, TaskContext if TYPE_CHECKING: from ..config import PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager + from .base import TaskSpecFactory class RefTask(PoeTask): @@ -13,10 +15,47 @@ class RefTask(PoeTask): A task consisting of a reference to another task """ - content: str - __key__ = "ref" - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} + + class TaskOptions(PoeTask.TaskOptions): + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + if self.executor: + raise ConfigValidationError( + "Option 'executor' cannot be set on a ref task" + ) + if self.capture_stdout: + raise ConfigValidationError( + "Option 'capture_stdout' cannot be set on a ref task" + ) + + class TaskSpec(PoeTask.TaskSpec): + content: str + options: "RefTask.TaskOptions" + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + + import shlex + + task_name_ref = shlex.split(self.content)[0] + + if task_name_ref not in task_specs: + raise ConfigValidationError( + f"Includes reference to unknown task {task_name_ref!r}" + ) + + if task_specs.get(task_name_ref).options.get("use_exec", False): + raise ConfigValidationError( + f"Illegal reference to task with " + f"'use_exec' set to true: {task_name_ref!r}" + ) + + spec: TaskSpec def _handle_run( self, @@ -34,17 +73,13 @@ def _handle_run( ref_invocation = ( *( env.fill_template(token) - for token in shlex.split(env.fill_template(self.content.strip())) + for token in shlex.split(env.fill_template(self.spec.content.strip())) ), *extra_args, ) - task = self.from_config( - ref_invocation[0], - self._config, - self._ui, - invocation=ref_invocation, - inheritance=TaskInheritance.from_task(self), + task = self.ctx.specs.get(ref_invocation[0]).create_task( + invocation=ref_invocation, ctx=TaskContext.from_task(self) ) if task.has_deps(): @@ -75,34 +110,3 @@ def _run_task_graph( f"Task graph aborted after failed task {stage_task.name!r}" ) return 0 - - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - """ - Check the given task definition for validity specific to this task type and - return a message describing the first encountered issue if any. - """ - - # TODO: disallow capture_stdout and executor options? - - import shlex - - task_ref = task_def["ref"] - task_name_ref = shlex.split(task_ref)[0] - - if task_name_ref not in config.tasks: - return ( - f"Task {task_name!r} contains reference to unknown task " - f"{task_name_ref!r}" - ) - - referenced_task = config.tasks[task_name_ref] - if isinstance(referenced_task, dict) and referenced_task.get("use_exec"): - return ( - f"Invalid task: {task_name!r}. contains illegal reference to task with " - f"use_exec set to true: {task_ref!r}" - ) - - return None diff --git a/poethepoet/task/script.py b/poethepoet/task/script.py index 5ff6dc9e..8ed37445 100644 --- a/poethepoet/task/script.py +++ b/poethepoet/task/script.py @@ -1,14 +1,15 @@ import re import shlex -from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple -from ..exceptions import ExpressionParseError +from ..exceptions import ConfigValidationError, ExpressionParseError from .base import PoeTask if TYPE_CHECKING: from ..config import PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager + from .base import TaskSpecFactory class ScriptTask(PoeTask): @@ -19,10 +20,40 @@ class ScriptTask(PoeTask): content: str __key__ = "script" - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "use_exec": bool, - "print_result": bool, - } + + class TaskOptions(PoeTask.TaskOptions): + use_exec: bool = False + print_result: bool = False + + def validate(self): + super().validate() + if self.use_exec and self.capture_stdout: + raise ConfigValidationError( + "'use_exec' and 'capture_stdout'" + " options cannot be both provided on the same task." + ) + + class TaskSpec(PoeTask.TaskSpec): + content: str + options: "ScriptTask.TaskOptions" + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + from ..helpers.python import parse_and_validate + + try: + target_module, target_ref = self.content.split(":", 1) + if not target_ref.isidentifier(): + parse_and_validate(target_ref, call_only=True) + except (ValueError, ExpressionParseError): + raise ConfigValidationError( + f"Invalid callable reference {self.content!r}\n" + "(expected something like `module:callable` or `module:callable()`)" + ) + + spec: TaskSpec def _handle_run( self, @@ -59,7 +90,7 @@ def _handle_run( f" else _m.{function_call};", ] - if self.options.get("print_result"): + if self.spec.options.get("print_result"): script.append("_r is not None and print(_r);") # Exactly which python executable to use is usually resolved by the executor @@ -69,28 +100,9 @@ def _handle_run( self._print_action(shlex.join(argv), context.dry) return self._get_executor(context, env).execute( - cmd, use_exec=self.options.get("use_exec", False) + cmd, use_exec=self.spec.options.get("use_exec", False) ) - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - from ..helpers.python import parse_and_validate - - try: - target_module, target_ref = task_def["script"].split(":", 1) - if not target_ref.isidentifier(): - parse_and_validate(target_ref, call_only=True) - except (ValueError, ExpressionParseError): - return ( - f"Task {task_name!r} contains invalid callable reference " - f"{task_def['script']!r} (expected something like `module:callable`" - " or `module:callable()`)" - ) - - return None - def parse_content(self, args: Optional[Dict[str, Any]]) -> Tuple[str, str]: """ Returns the module to load, and the function call to execute. @@ -102,10 +114,10 @@ def parse_content(self, args: Optional[Dict[str, Any]]) -> Tuple[str, str]: from ..helpers.python import resolve_expression try: - target_module, target_ref = self.content.strip().split(":", 1) + target_module, target_ref = self.spec.content.strip().split(":", 1) except ValueError: raise ExpressionParseError( - f"Invalid task content: {self.content.strip()!r}" + f"Invalid task content: {self.spec.content.strip()!r}" ) if target_ref.isidentifier(): diff --git a/poethepoet/task/sequence.py b/poethepoet/task/sequence.py index 805ed1a5..dd3b0d87 100644 --- a/poethepoet/task/sequence.py +++ b/poethepoet/task/sequence.py @@ -1,22 +1,25 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, Dict, List, + Literal, Optional, + Sequence, Tuple, Type, Union, ) -from ..exceptions import ExecutionError, PoeException -from .base import PoeTask, TaskContent, TaskInheritance +from ..exceptions import ConfigValidationError, ExecutionError, PoeException +from .base import PoeTask, TaskContext if TYPE_CHECKING: - from ..config import PoeConfig + from ..config import ConfigPartition, PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager - from ..ui import PoeUi + from .base import TaskSpecFactory class SequenceTask(PoeTask): @@ -27,42 +30,101 @@ class SequenceTask(PoeTask): content: List[Union[str, Dict[str, Any]]] __key__ = "sequence" - __content_type__: Type = list - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "ignore_fail": (bool, str), - "default_item_type": str, - } + __content_type__: ClassVar[Type] = list + + class TaskOptions(PoeTask.TaskOptions): + ignore_fail: Literal[True, False, "return_zero", "return_non_zero"] = False + default_item_type: Optional[str] = None + + def validate(self): + """ + Validation rules that don't require any extra context go here. + """ + super().validate() + if self.default_item_type is not None and not PoeTask.is_task_type( + self.default_item_type, content_type=str + ): + raise ConfigValidationError( + "Unsupported value for option `default_item_type`,\n" + f"Expected one of {PoeTask.get_task_types(content_type=str)}" + ) + + class TaskSpec(PoeTask.TaskSpec): + content: list + options: "SequenceTask.TaskOptions" + subtasks: Sequence[PoeTask.TaskSpec] + + def __init__( + self, + name: str, + task_def: Dict[str, Any], + factory: "TaskSpecFactory", + source: "ConfigPartition", + parent: Optional["PoeTask.TaskSpec"] = None, + ): + super().__init__(name, task_def, factory, source, parent) + + self.subtasks = [] + for index, sub_task_def in enumerate(task_def[SequenceTask.__key__]): + if not isinstance(sub_task_def, (str, dict, list)): + raise ConfigValidationError( + f"Item #{index} in sequence task should be a value of " + "type: str | dict | list", + task_name=self.name, + ) + + subtask_name = ( + sub_task_def + if isinstance(sub_task_def, str) + else SequenceTask._subtask_name(name, index) + ) + task_type_key = self.task_type.resolve_task_type( + sub_task_def, + factory.config, + array_item=task_def.get("default_item_type", True), + ) + + try: + self.subtasks.append( + factory.get( + subtask_name, sub_task_def, task_type_key, parent=self + ) + ) + except PoeException: + raise ConfigValidationError( + f"Failed to interpret subtask #{index} in sequence", + task_name=self.name, + ) + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + """ + Perform validations on this TaskSpec that apply to a specific task type + """ + for index, subtask in enumerate(self.subtasks): + if subtask.args: + raise ConfigValidationError( + "Unsupported option 'args' for task declared inside sequence" + ) + + subtask.validate(config, task_specs) + + spec: TaskSpec def __init__( self, - name: str, - content: TaskContent, - options: Dict[str, Any], - ui: "PoeUi", - config: "PoeConfig", + spec: TaskSpec, invocation: Tuple[str, ...], + ctx: TaskContext, capture_stdout: bool = False, - inheritance: Optional[TaskInheritance] = None, ): - assert capture_stdout is False - super().__init__( - name, content, options, ui, config, invocation, False, inheritance - ) - + assert capture_stdout in (False, None) + super().__init__(spec, invocation, ctx) self.subtasks = [ - self.from_def( - task_def=item, - task_name=task_name, - config=config, - invocation=(task_name,), - ui=ui, - array_item=self.options.get("default_item_type", True), - inheritance=TaskInheritance.from_task(self), - ) - for index, item in enumerate(self.content) - for task_name in ( - item if isinstance(item, str) else self._subtask_name(name, index), + task_spec.create_task( + invocation=(self._subtask_name(task_spec.name, index),), + ctx=TaskContext.from_task(self), ) + for index, task_spec in enumerate(spec.subtasks) ] def _handle_run( @@ -80,7 +142,7 @@ def _handle_run( # Indicate on the global context that there are multiple stages context.multistage = True - ignore_fail = self.options.get("ignore_fail") + ignore_fail = self.spec.options.ignore_fail non_zero_subtasks: List[str] = list() for subtask in self.subtasks: task_result = subtask.run(context=context, parent_env=env) @@ -100,61 +162,3 @@ def _handle_run( @classmethod def _subtask_name(cls, task_name: str, index: int): return f"{task_name}[{index}]" - - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - default_item_type = task_def.get("default_item_type") - if default_item_type is not None and not cls.is_task_type( - default_item_type, content_type=str - ): - return ( - "Unsupported value for option `default_item_type` for task " - f"{task_name!r}. Expected one of {cls.get_task_types(content_type=str)}" - ) - - ignore_fail = task_def.get("ignore_fail") - if ignore_fail is not None and ignore_fail not in ( - True, - False, - "return_zero", - "return_non_zero", - ): - return ( - f"Unsupported value for option `ignore_fail` for task {task_name!r}." - ' Expected one of (true, false, "return_zero", "return_non_zero")' - ) - - for index, task_item in enumerate(task_def["sequence"]): - if isinstance(task_item, dict): - if len(task_item.get("args", tuple())): - return ( - "Unsupported option `args` for task declared inside sequence " - f"task {task_name!r}." - ) - - subtask_issue = cls.validate_def( - cls._subtask_name(task_name, index), - task_item, - config, - anonymous=True, - ) - if subtask_issue: - return subtask_issue - - else: - subtask_issue = cls.validate_def( - cls._subtask_name(task_name, index), - cls.normalize_task_def( - task_item, - config, - array_item=default_item_type or True, - ), - config, - anonymous=True, - ) - if subtask_issue: - return subtask_issue - - return None diff --git a/poethepoet/task/shell.py b/poethepoet/task/shell.py index c93bf63e..5ba3f3f9 100644 --- a/poethepoet/task/shell.py +++ b/poethepoet/task/shell.py @@ -2,20 +2,16 @@ from os import environ from typing import ( TYPE_CHECKING, - Any, - Dict, List, Optional, Tuple, - Type, Union, ) -from ..exceptions import PoeException +from ..exceptions import ConfigValidationError, PoeException from .base import PoeTask if TYPE_CHECKING: - from ..config import PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager @@ -28,7 +24,44 @@ class ShellTask(PoeTask): content: str __key__ = "shell" - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {"interpreter": (str, list)} + + class TaskOptions(PoeTask.TaskOptions): + interpreter: Optional[Union[str, list]] = None + + def validate(self): + super().validate() + + from ..config import PoeConfig + + valid_interpreters = PoeConfig.KNOWN_SHELL_INTERPRETERS + + if ( + isinstance(self.interpreter, str) + and self.interpreter not in valid_interpreters + ): + raise ConfigValidationError( + "Invalid value for option 'interpreter',\n" + f"Expected one of {valid_interpreters}" + ) + + if isinstance(self.interpreter, list): + if len(self.interpreter) == 0: + raise ConfigValidationError( + "Invalid value for option 'interpreter',\n" + "Expected at least one item in list." + ) + for item in self.interpreter: + if item not in valid_interpreters: + raise ConfigValidationError( + f"Invalid item {item!r} in option 'interpreter',\n" + f"Expected one of {valid_interpreters!r}" + ) + + class TaskSpec(PoeTask.TaskSpec): + content: str + options: "ShellTask.TaskOptions" + + spec: TaskSpec def _handle_run( self, @@ -39,7 +72,9 @@ def _handle_run( env.update(named_arg_values) if not named_arg_values and any(arg.strip() for arg in self.invocation[1:]): - raise PoeException(f"Shell task {self.name!r} does not accept arguments") + raise PoeException( + f"Shell task {self.spec.name!r} does not accept arguments" + ) interpreter_cmd = self.resolve_interpreter_cmd() if not interpreter_cmd: @@ -54,7 +89,7 @@ def _handle_run( message += "Some dependencies may be missing from your system." raise PoeException(message) - content = _unindent_code(self.content).rstrip() + content = _unindent_code(self.spec.content).rstrip() self._print_action(content, context.dry) @@ -63,8 +98,8 @@ def _handle_run( ) def _get_interpreter_config(self) -> Tuple[str, ...]: - result: Union[str, Tuple[str, ...]] = self.options.get( - "interpreter", self._config.shell_interpreter + result: Union[str, Tuple[str, ...]] = self.spec.options.get( + "interpreter", self.ctx.config.shell_interpreter ) if isinstance(result, str): return (result,) @@ -152,34 +187,6 @@ def _locate_interpreter(self, interpreter: str) -> Optional[str]: return result - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - interpreter = task_def.get("interpreter") - valid_interpreters = config.KNOWN_SHELL_INTERPRETERS - - if isinstance(interpreter, str) and interpreter not in valid_interpreters: - return ( - "Unsupported value for option `interpreter` for task " - f"{task_name!r}. Expected one of {valid_interpreters}" - ) - - if isinstance(interpreter, list): - if len(interpreter) == 0: - return ( - "Unsupported value for option `interpreter` for task " - f"{task_name!r}. Expected at least one item in list." - ) - for item in interpreter: - if item not in valid_interpreters: - return ( - "Unsupported item {item!r} in option `interpreter` for task " - f"{task_name!r}. Expected one of {valid_interpreters}" - ) - - return None - def _unindent_code(python_code: str): """ diff --git a/poethepoet/task/switch.py b/poethepoet/task/switch.py index 977508dc..e553c211 100644 --- a/poethepoet/task/switch.py +++ b/poethepoet/task/switch.py @@ -1,27 +1,28 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, Dict, - List, + Literal, MutableMapping, Optional, Tuple, Type, Union, - cast, ) -from ..exceptions import ExecutionError, PoeException -from .base import PoeTask, TaskContent, TaskInheritance +from ..exceptions import ConfigValidationError, ExecutionError, PoeException +from .base import PoeTask, TaskContext if TYPE_CHECKING: - from ..config import PoeConfig + from ..config import ConfigPartition, PoeConfig from ..context import RunContext from ..env.manager import EnvVarsManager - from ..ui import PoeUi + from .base import TaskSpecFactory DEFAULT_CASE = "__default__" +SUBTASK_OPTIONS_BLOCKLIST = ("args", "uses", "deps") class SwitchTask(PoeTask): @@ -30,65 +31,165 @@ class SwitchTask(PoeTask): `switch` subtask. """ - content: List[Union[str, Dict[str, Any]]] - __key__ = "switch" - __content_type__: Type = list - __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { - "control": (str, dict), - "default": str, - } + __content_type__: ClassVar[Type] = list + + class TaskOptions(PoeTask.TaskOptions): + control: Union[str, dict] + default: Literal["pass", "fail"] = "fail" + + @classmethod + def normalize( + cls, + config: Any, + strict: bool = True, + ): + """ + Perform validations that require access to to the raw config. + """ + if strict and isinstance(config, dict): + # Subtasks may not declare certain options + for subtask_def in config.get("switch", tuple()): + for banned_option in SUBTASK_OPTIONS_BLOCKLIST: + if banned_option in subtask_def: + if "case" not in subtask_def: + raise ConfigValidationError( + "Default case includes incompatible option " + f"{banned_option!r}" + ) + raise ConfigValidationError( + f"Case {subtask_def.get('case')!r} includes " + f"incompatible option {banned_option!r}" + ) + + return super().normalize(config, strict) + + class TaskSpec(PoeTask.TaskSpec): + control_task_spec: PoeTask.TaskSpec + case_task_specs: Tuple[Tuple[Tuple[Any, ...], PoeTask.TaskSpec], ...] + options: "SwitchTask.TaskOptions" + + def __init__( + self, + name: str, + task_def: Dict[str, Any], + factory: "TaskSpecFactory", + source: "ConfigPartition", + parent: Optional["PoeTask.TaskSpec"] = None, + ): + super().__init__(name, task_def, factory, source, parent) + + switch_args = task_def.get("args") + control_task_def = task_def["control"] + + if switch_args: + if isinstance(control_task_def, str): + control_task_def = { + factory.config.default_task_type: control_task_def + } + control_task_def = dict(control_task_def, args=switch_args) + + self.control_task_spec = factory.get( + task_name=f"{name}[__control__]", task_def=control_task_def, parent=self + ) + + case_task_specs = [] + for switch_item in task_def["switch"]: + case_task_def = dict(switch_item, args=switch_args) + case = case_task_def.pop("case", DEFAULT_CASE) + case_tuple = tuple(case) if isinstance(case, list) else (case,) + case_task_index = ",".join(str(value) for value in case_tuple) + case_task_specs.append( + ( + case_tuple, + factory.get( + task_name=f"{name}[{case_task_index}]", + task_def=case_task_def, + parent=self, + ), + ) + ) + + self.case_task_specs = tuple(case_task_specs) + + def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"): + from collections import defaultdict + + allowed_control_task_types = ("expr", "cmd", "script") + if ( + self.control_task_spec.task_type.__key__ + not in allowed_control_task_types + ): + raise ConfigValidationError( + f"Control task must have a type that is one of " + f"{allowed_control_task_types!r}" + ) + + cases: MutableMapping[Any, int] = defaultdict(int) + for case_keys, case_task_spec in self.case_task_specs: + for case_key in case_keys: + cases[case_key] += 1 + + # Ensure case keys don't overlap (and only one default case) + for case, count in cases.items(): + if count > 1: + if case is DEFAULT_CASE: + raise ConfigValidationError( + "Switch array includes more than one default case" + ) + raise ConfigValidationError( + f"Switch array includes more than one case for {case!r}" + ) + + if self.options.default != "fail" and DEFAULT_CASE in cases: + raise ConfigValidationError( + "switch tasks should not declare both a default case and the " + "'default' option" + ) + + # Validate subtask specs + self.control_task_spec.validate(config, task_specs) + for _, case_task_spec in self.case_task_specs: + case_task_spec.validate(config, task_specs) + + spec: TaskSpec + control_task: PoeTask + switch_tasks: Dict[str, PoeTask] def __init__( self, - name: str, - content: TaskContent, - options: Dict[str, Any], - ui: "PoeUi", - config: "PoeConfig", + spec: TaskSpec, invocation: Tuple[str, ...], + ctx: TaskContext, capture_stdout: bool = False, - inheritance: Optional[TaskInheritance] = None, ): - super().__init__( - name, content, options, ui, config, invocation, False, inheritance - ) + super().__init__(spec, invocation, ctx, capture_stdout) - control_task_name = f"{name}[control]" + control_task_name = f"{spec.name}[__control__]" control_invocation: Tuple[str, ...] = (control_task_name,) - if self.options.get("args"): - self.options["control"]["args"] = self.options["args"] + options = self.spec.options + if options.get("args"): control_invocation = (*control_invocation, *invocation[1:]) - self.control_task = self.from_def( - task_def=self.options.get("control", ""), - task_name=control_task_name, - config=config, + self.control_task = self.spec.control_task_spec.create_task( invocation=control_invocation, - ui=ui, + ctx=TaskContext.from_task(self), capture_stdout=True, - inheritance=TaskInheritance.from_task(self), ) self.switch_tasks = {} - for item in cast(List[Dict[str, Any]], content): - task_def = {key: value for key, value in item.items() if key != "case"} - - task_invocation: Tuple[str, ...] = (name,) - if self.options.get("args"): - task_def["args"] = self.options["args"] + for case_keys, case_spec in spec.case_task_specs: + task_invocation: Tuple[str, ...] = (f"{spec.name}[{','.join(case_keys)}]",) + if options.get("args"): task_invocation = (*task_invocation, *invocation[1:]) - for case_key in self._get_case_keys(item): - self.switch_tasks[case_key] = self.from_def( - task_def=task_def, - task_name=f"{name}__{case_key}", - config=config, - invocation=task_invocation, - ui=ui, - capture_stdout=self.options.get("capture_stdout", capture_stdout), - inheritance=TaskInheritance.from_task(self), - ) + case_task = case_spec.create_task( + invocation=task_invocation, + ctx=TaskContext.from_task(self), + capture_stdout=self.capture_stdout, + ) + for case_key in case_keys: + self.switch_tasks[case_key] = case_task def _handle_run( self, @@ -122,96 +223,20 @@ def _handle_run( ) if case_task is None: - if self.options.get("default", "fail") == "pass": + if self.spec.options.default == "pass": return 0 raise ExecutionError( f"Control value {control_task_output!r} did not match any cases in " f"switch task {self.name!r}." ) - return case_task.run(context=context, parent_env=env) + result = case_task.run(context=context, parent_env=env) - @classmethod - def _get_case_keys(cls, task_def: Dict[str, Any]) -> List[Any]: - case_value = task_def.get("case", DEFAULT_CASE) - if isinstance(case_value, list): - return case_value - return [case_value] - - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - from collections import defaultdict - - control_task_def = task_def.get("control") - if not control_task_def: - return f"Switch task {task_name!r} has no control task." - - allowed_control_task_types = ("expr", "cmd", "script") - if isinstance(control_task_def, dict) and not any( - key in control_task_def for key in allowed_control_task_types - ): - return ( - f"Control task for {task_name!r} must have a type that is one of " - f"{allowed_control_task_types!r}" + if self.capture_stdout is True: + # The executor saved output for the case task, but we need it to be + # registered for this switch task as well + context.save_task_output( + self.invocation, context.get_task_output(case_task.invocation).encode() ) - control_task_issue = PoeTask.validate_def( - f"{task_name}[control]", control_task_def, config, anonymous=True - ) - if control_task_issue: - return control_task_issue - - cases: MutableMapping[Any, int] = defaultdict(int) - for switch_task in task_def["switch"]: - for case_key in cls._get_case_keys(switch_task): - cases[case_key] += 1 - - case_key = switch_task.get("case", DEFAULT_CASE) - for invalid_option in ("args", "deps"): - if invalid_option in switch_task: - if case_key is DEFAULT_CASE: - return ( - f"Default case of switch task {task_name!r} includes " - f"invalid option {invalid_option!r}" - ) - return ( - f"Case {case_key!r} switch task {task_name!r} include invalid " - f"option {invalid_option!r}" - ) - - switch_task_issue = PoeTask.validate_def( - f"{task_name}[{case_key}]", - switch_task, - config, - anonymous=True, - extra_options=("case",), - ) - if switch_task_issue: - return switch_task_issue - - for case, count in cases.items(): - if count > 1: - if case is DEFAULT_CASE: - return ( - f"Switch task {task_name!r} includes more than one default case" - ) - return ( - f"Switch task {task_name!r} includes more than one case for " - f"{case!r}" - ) - - if "default" in task_def: - if task_def["default"] not in ("pass", "fail"): - return ( - f"The 'default' option for switch task {task_name!r} should be one " - "of ('pass', 'fail')" - ) - if DEFAULT_CASE in cases: - return ( - f"Switch task {task_name!r} should not have both a default case " - f"and the 'default' option." - ) - - return None + return result diff --git a/poethepoet/ui.py b/poethepoet/ui.py index 8c38333d..e22b109e 100644 --- a/poethepoet/ui.py +++ b/poethepoet/ui.py @@ -3,7 +3,7 @@ from typing import IO, TYPE_CHECKING, List, Mapping, Optional, Sequence, Tuple, Union from .__version__ import __version__ -from .exceptions import ExecutionError, PoeException +from .exceptions import ConfigValidationError, ExecutionError, PoeException if TYPE_CHECKING: from argparse import ArgumentParser, Namespace @@ -16,6 +16,12 @@ def guess_ansi_support(file): # https://no-color.org/ return False + if ( + os.environ.get("GITHUB_ACTIONS", "false") == "true" + and "PYTEST_CURRENT_TEST" not in os.environ + ): + return True + return ( (sys.platform != "win32" or "ANSICON" in os.environ) and hasattr(file, "isatty") @@ -172,10 +178,28 @@ def print_help( if error: # TODO: send this to stderr instead? - error_line = [f"Error: {error.msg} "] + error_lines = [] + if isinstance(error, ConfigValidationError): + if error.task_name: + if error.context: + error_lines.append( + f"{error.context} in task {error.task_name!r}" + ) + else: + error_lines.append(f"Invalid task {error.task_name!r}") + if error.filename: + error_lines[-1] += f" in file {error.filename}" + elif error.global_option: + error_lines.append(f"Invalid global option {error.global_option!r}") + if error.filename: + error_lines[-1] += f" in file {error.filename}" + error_lines.extend(error.msg.split("\n")) if error.cause: - error_line.append(f" From: {error.cause} ") - result.append(error_line) + error_lines.append(error.cause) + if error.__cause__: + error_lines.append(f"From: {error.__cause__!r}") + + result.append(self._format_error_lines(error_lines)) if verbosity >= 0: result.append( @@ -201,7 +225,13 @@ def print_help( max_task_len = max( max( len(task), - max([len(", ".join(opts)) for (opts, _, _) in args] or (0,)) + max( + [ + len(", ".join(str(opt) for opt in opts)) + for (opts, _, _) in args + ] + or (0,) + ) + 2, ) for task, (_, args) in tasks.items() @@ -216,9 +246,10 @@ def print_help( f" {self._padr(task, col_width)} {help_text}" ) for options, arg_help_text, default in args_help: + formatted_options = ", ".join(str(opt) for opt in options) task_arg_help = [ " ", - f"{self._padr(', '.join(options), col_width-1)}", + f"{self._padr(formatted_options, col_width-1)}", ] if arg_help_text: task_arg_help.append(arg_help_text) @@ -231,6 +262,11 @@ def print_help( else: result.append("NO TASKS CONFIGURED") + if error and os.environ.get("POE_DEBUG", "0").lower() == "1": + import traceback + + result.append("".join(traceback.format_exception(error)).strip()) + self._print( "\n\n".join( section if isinstance(section, str) else "\n".join(section).strip("\n") @@ -251,9 +287,25 @@ def print_msg(self, message: str, verbosity=0, end="\n"): self._print(message, end=end) def print_error(self, error: Union[PoeException, ExecutionError]): - self._print(f"Error: {error.msg} ") + error_lines = error.msg.split("\n") if error.cause: - self._print(f" From: {error.cause} ") + error_lines.append(f"From: {error.cause}") + if error.__cause__: + error_lines.append(f"From: {error.__cause__!r}") + + for line in self._format_error_lines(error_lines): + self._print(line) + + if os.environ.get("POE_DEBUG", "0").lower() == "1": + import traceback + + self._print("".join(traceback.format_exception(error)).strip()) + + def _format_error_lines(self, lines: Sequence[str]): + return ( + f"Error: {lines[0]}", + *(f" | {line}" for line in lines[1:]), + ) def print_version(self): if self.verbosity >= 0: diff --git a/poetry.lock b/poetry.lock index 8bb46ae7..8bf606d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,23 +36,6 @@ files = [ {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] -[[package]] -name = "astroid" -version = "2.11.7" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, - {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, -] - -[package.dependencies] -lazy-object-proxy = ">=1.4.0" -setuptools = ">=20.0" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = ">=1.11,<2" - [[package]] name = "attrs" version = "23.1.0" @@ -73,18 +56,17 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" description = "Internationalization utilities" optional = false python-versions = ">=3.7" files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] @@ -109,29 +91,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "23.11.0" +version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, - {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, - {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, - {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, - {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, - {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, - {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, - {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, - {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, - {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, - {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, - {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, - {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, - {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, - {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, - {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, - {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, - {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -145,7 +131,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -456,63 +442,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.dependencies] @@ -523,34 +509,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = true python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -626,29 +612,15 @@ files = [ {file = "cwcwidth-0.1.9.tar.gz", hash = "sha256:f19d11a0148d4a8cacd064c96e93bca8ce3415a186ae8204038f45e108db76b8"}, ] -[[package]] -name = "dill" -version = "0.3.7" -description = "serialize all of Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -664,13 +636,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -711,72 +683,73 @@ sphinx-basic-ng = "*" [[package]] name = "greenlet" -version = "3.0.1" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, - {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, - {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, - {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, - {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, - {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, - {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, - {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, - {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, - {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, - {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, - {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, - {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, - {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, - {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -802,13 +775,13 @@ lxml = ["lxml"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" 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"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -824,20 +797,20 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "7.0.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] @@ -852,23 +825,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -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 = "jeepney" version = "0.8.0" @@ -903,13 +859,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jinxed" -version = "1.2.0" +version = "1.2.1" description = "Jinxed Terminal Library" optional = false python-versions = "*" files = [ - {file = "jinxed-1.2.0-py2.py3-none-any.whl", hash = "sha256:cfc2b2e4e3b4326954d546ba6d6b9a7a796ddcb0aef8d03161d005177eb0d48b"}, - {file = "jinxed-1.2.0.tar.gz", hash = "sha256:032acda92d5c57cd216033cbbd53de731e6ed50deb63eb4781336ca55f72cda5"}, + {file = "jinxed-1.2.1-py2.py3-none-any.whl", hash = "sha256:37422659c4925969c66148c5e64979f553386a4226b9484d910d3094ced37d30"}, + {file = "jinxed-1.2.1.tar.gz", hash = "sha256:30c3f861b73279fea1ed928cfd4dfb1f273e16cd62c8a32acfac362da0f78f3f"}, ] [package.dependencies] @@ -955,51 +911,6 @@ secretstorage = {version = "*", markers = "sys_platform == \"linux\""} docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8"] -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -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 = "livereload" version = "2.6.3" @@ -1109,17 +1020,6 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -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 = "mdurl" version = "0.1.2" @@ -1198,38 +1098,38 @@ files = [ [[package]] name = "mypy" -version = "1.7.0" +version = "1.8.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"}, - {file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"}, - {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, - {file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"}, - {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, - {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, - {file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"}, - {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, - {file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"}, - {file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, - {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, - {file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"}, - {file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"}, - {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, - {file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"}, - {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, - {file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"}, - {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, - {file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"}, - {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, - {file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"}, - {file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"}, - {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, - {file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"}, - {file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [package.dependencies] @@ -1278,24 +1178,24 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = true 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"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] @@ -1317,13 +1217,13 @@ testing = ["pytest", "pytest-cov"] [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] @@ -1410,18 +1310,18 @@ files = [ [[package]] name = "pydantic" -version = "2.5.1" +version = "2.5.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-2.5.1-py3-none-any.whl", hash = "sha256:dc5244a8939e0d9a68f1f1b5f550b2e1c879912033b1becbedb315accc75441b"}, - {file = "pydantic-2.5.1.tar.gz", hash = "sha256:0b8be5413c06aadfbe56f6dc1d45c9ed25fd43264414c571135c97dd77c2bedb"}, + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.14.3" +pydantic-core = "2.14.6" typing-extensions = ">=4.6.1" [package.extras] @@ -1429,116 +1329,116 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.3" +version = "2.14.6" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic_core-2.14.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ba44fad1d114539d6a1509966b20b74d2dec9a5b0ee12dd7fd0a1bb7b8785e5f"}, - {file = "pydantic_core-2.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a70d23eedd88a6484aa79a732a90e36701048a1509078d1b59578ef0ea2cdf5"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cc24728a1a9cef497697e53b3d085fb4d3bc0ef1ef4d9b424d9cf808f52c146"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab4a2381005769a4af2ffddae74d769e8a4aae42e970596208ec6d615c6fb080"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a12bf088d6fa20e094f9a477bf84bd823651d8b8384f59bcd50eaa92e6a52"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38aed5a1bbc3025859f56d6a32f6e53ca173283cb95348e03480f333b1091e7d"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1767bd3f6370458e60c1d3d7b1d9c2751cc1ad743434e8ec84625a610c8b9195"}, - {file = "pydantic_core-2.14.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7cb0c397f29688a5bd2c0dbd44451bc44ebb9b22babc90f97db5ec3e5bb69977"}, - {file = "pydantic_core-2.14.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ff737f24b34ed26de62d481ef522f233d3c5927279f6b7229de9b0deb3f76b5"}, - {file = "pydantic_core-2.14.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1a39fecb5f0b19faee9a8a8176c805ed78ce45d760259a4ff3d21a7daa4dfc1"}, - {file = "pydantic_core-2.14.3-cp310-none-win32.whl", hash = "sha256:ccbf355b7276593c68fa824030e68cb29f630c50e20cb11ebb0ee450ae6b3d08"}, - {file = "pydantic_core-2.14.3-cp310-none-win_amd64.whl", hash = "sha256:536e1f58419e1ec35f6d1310c88496f0d60e4f182cacb773d38076f66a60b149"}, - {file = "pydantic_core-2.14.3-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:f1f46700402312bdc31912f6fc17f5ecaaaa3bafe5487c48f07c800052736289"}, - {file = "pydantic_core-2.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:88ec906eb2d92420f5b074f59cf9e50b3bb44f3cb70e6512099fdd4d88c2f87c"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:056ea7cc3c92a7d2a14b5bc9c9fa14efa794d9f05b9794206d089d06d3433dc7"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076edc972b68a66870cec41a4efdd72a6b655c4098a232314b02d2bfa3bfa157"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e71f666c3bf019f2490a47dddb44c3ccea2e69ac882f7495c68dc14d4065eac2"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f518eac285c9632be337323eef9824a856f2680f943a9b68ac41d5f5bad7df7c"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dbab442a8d9ca918b4ed99db8d89d11b1f067a7dadb642476ad0889560dac79"}, - {file = "pydantic_core-2.14.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0653fb9fc2fa6787f2fa08631314ab7fc8070307bd344bf9471d1b7207c24623"}, - {file = "pydantic_core-2.14.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c54af5069da58ea643ad34ff32fd6bc4eebb8ae0fef9821cd8919063e0aeeaab"}, - {file = "pydantic_core-2.14.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc956f78651778ec1ab105196e90e0e5f5275884793ab67c60938c75bcca3989"}, - {file = "pydantic_core-2.14.3-cp311-none-win32.whl", hash = "sha256:5b73441a1159f1fb37353aaefb9e801ab35a07dd93cb8177504b25a317f4215a"}, - {file = "pydantic_core-2.14.3-cp311-none-win_amd64.whl", hash = "sha256:7349f99f1ef8b940b309179733f2cad2e6037a29560f1b03fdc6aa6be0a8d03c"}, - {file = "pydantic_core-2.14.3-cp311-none-win_arm64.whl", hash = "sha256:ec79dbe23702795944d2ae4c6925e35a075b88acd0d20acde7c77a817ebbce94"}, - {file = "pydantic_core-2.14.3-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:8f5624f0f67f2b9ecaa812e1dfd2e35b256487566585160c6c19268bf2ffeccc"}, - {file = "pydantic_core-2.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c2d118d1b6c9e2d577e215567eedbe11804c3aafa76d39ec1f8bc74e918fd07"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe863491664c6720d65ae438d4efaa5eca766565a53adb53bf14bc3246c72fe0"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:136bc7247e97a921a020abbd6ef3169af97569869cd6eff41b6a15a73c44ea9b"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aeafc7f5bbddc46213707266cadc94439bfa87ecf699444de8be044d6d6eb26f"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16aaf788f1de5a85c8f8fcc9c1ca1dd7dd52b8ad30a7889ca31c7c7606615b8"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc652c354d3362e2932a79d5ac4bbd7170757a41a62c4fe0f057d29f10bebb"}, - {file = "pydantic_core-2.14.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f1b92e72babfd56585c75caf44f0b15258c58e6be23bc33f90885cebffde3400"}, - {file = "pydantic_core-2.14.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:75f3f534f33651b73f4d3a16d0254de096f43737d51e981478d580f4b006b427"}, - {file = "pydantic_core-2.14.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c9ffd823c46e05ef3eb28b821aa7bc501efa95ba8880b4a1380068e32c5bed47"}, - {file = "pydantic_core-2.14.3-cp312-none-win32.whl", hash = "sha256:12e05a76b223577a4696c76d7a6b36a0ccc491ffb3c6a8cf92d8001d93ddfd63"}, - {file = "pydantic_core-2.14.3-cp312-none-win_amd64.whl", hash = "sha256:1582f01eaf0537a696c846bea92082082b6bfc1103a88e777e983ea9fbdc2a0f"}, - {file = "pydantic_core-2.14.3-cp312-none-win_arm64.whl", hash = "sha256:96fb679c7ca12a512d36d01c174a4fbfd912b5535cc722eb2c010c7b44eceb8e"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:71ed769b58d44e0bc2701aa59eb199b6665c16e8a5b8b4a84db01f71580ec448"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:5402ee0f61e7798ea93a01b0489520f2abfd9b57b76b82c93714c4318c66ca06"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaab9dc009e22726c62fe3b850b797e7f0e7ba76d245284d1064081f512c7226"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92486a04d54987054f8b4405a9af9d482e5100d6fe6374fc3303015983fc8bda"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf08b43d1d5d1678f295f0431a4a7e1707d4652576e1d0f8914b5e0213bfeee5"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8ca13480ce16daad0504be6ce893b0ee8ec34cd43b993b754198a89e2787f7e"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44afa3c18d45053fe8d8228950ee4c8eaf3b5a7f3b64963fdeac19b8342c987f"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56814b41486e2d712a8bc02a7b1f17b87fa30999d2323bbd13cf0e52296813a1"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c3dc2920cc96f9aa40c6dc54256e436cc95c0a15562eb7bd579e1811593c377e"}, - {file = "pydantic_core-2.14.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e483b8b913fcd3b48badec54185c150cb7ab0e6487914b84dc7cde2365e0c892"}, - {file = "pydantic_core-2.14.3-cp37-none-win32.whl", hash = "sha256:364dba61494e48f01ef50ae430e392f67ee1ee27e048daeda0e9d21c3ab2d609"}, - {file = "pydantic_core-2.14.3-cp37-none-win_amd64.whl", hash = "sha256:a402ae1066be594701ac45661278dc4a466fb684258d1a2c434de54971b006ca"}, - {file = "pydantic_core-2.14.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:10904368261e4509c091cbcc067e5a88b070ed9a10f7ad78f3029c175487490f"}, - {file = "pydantic_core-2.14.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:260692420028319e201b8649b13ac0988974eeafaaef95d0dfbf7120c38dc000"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1bf1a7b05a65d3b37a9adea98e195e0081be6b17ca03a86f92aeb8b110f468"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7abd17a838a52140e3aeca271054e321226f52df7e0a9f0da8f91ea123afe98"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5c51460ede609fbb4fa883a8fe16e749964ddb459966d0518991ec02eb8dfb9"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d06c78074646111fb01836585f1198367b17d57c9f427e07aaa9ff499003e58d"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af452e69446fadf247f18ac5d153b1f7e61ef708f23ce85d8c52833748c58075"}, - {file = "pydantic_core-2.14.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3ad4968711fb379a67c8c755beb4dae8b721a83737737b7bcee27c05400b047"}, - {file = "pydantic_core-2.14.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c5ea0153482e5b4d601c25465771c7267c99fddf5d3f3bdc238ef930e6d051cf"}, - {file = "pydantic_core-2.14.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96eb10ef8920990e703da348bb25fedb8b8653b5966e4e078e5be382b430f9e0"}, - {file = "pydantic_core-2.14.3-cp38-none-win32.whl", hash = "sha256:ea1498ce4491236d1cffa0eee9ad0968b6ecb0c1cd711699c5677fc689905f00"}, - {file = "pydantic_core-2.14.3-cp38-none-win_amd64.whl", hash = "sha256:2bc736725f9bd18a60eec0ed6ef9b06b9785454c8d0105f2be16e4d6274e63d0"}, - {file = "pydantic_core-2.14.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:1ea992659c03c3ea811d55fc0a997bec9dde863a617cc7b25cfde69ef32e55af"}, - {file = "pydantic_core-2.14.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2b53e1f851a2b406bbb5ac58e16c4a5496038eddd856cc900278fa0da97f3fc"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c7f8e8a7cf8e81ca7d44bea4f181783630959d41b4b51d2f74bc50f348a090f"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3b9c91eeb372a64ec6686c1402afd40cc20f61a0866850f7d989b6bf39a41a"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ef3e2e407e4cad2df3c89488a761ed1f1c33f3b826a2ea9a411b0a7d1cccf1b"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f86f20a9d5bee1a6ede0f2757b917bac6908cde0f5ad9fcb3606db1e2968bcf5"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61beaa79d392d44dc19d6f11ccd824d3cccb865c4372157c40b92533f8d76dd0"}, - {file = "pydantic_core-2.14.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d41df8e10b094640a6b234851b624b76a41552f637b9fb34dc720b9fe4ef3be4"}, - {file = "pydantic_core-2.14.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c08ac60c3caa31f825b5dbac47e4875bd4954d8f559650ad9e0b225eaf8ed0c"}, - {file = "pydantic_core-2.14.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d8b3932f1a369364606417ded5412c4ffb15bedbcf797c31317e55bd5d920e"}, - {file = "pydantic_core-2.14.3-cp39-none-win32.whl", hash = "sha256:caa94726791e316f0f63049ee00dff3b34a629b0d099f3b594770f7d0d8f1f56"}, - {file = "pydantic_core-2.14.3-cp39-none-win_amd64.whl", hash = "sha256:2494d20e4c22beac30150b4be3b8339bf2a02ab5580fa6553ca274bc08681a65"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:fe272a72c7ed29f84c42fedd2d06c2f9858dc0c00dae3b34ba15d6d8ae0fbaaf"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7e63a56eb7fdee1587d62f753ccd6d5fa24fbeea57a40d9d8beaef679a24bdd6"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7692f539a26265cece1e27e366df5b976a6db6b1f825a9e0466395b314ee48b"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af46f0b7a1342b49f208fed31f5a83b8495bb14b652f621e0a6787d2f10f24ee"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e2f9d76c00e805d47f19c7a96a14e4135238a7551a18bfd89bb757993fd0933"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:de52ddfa6e10e892d00f747bf7135d7007302ad82e243cf16d89dd77b03b649d"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:38113856c7fad8c19be7ddd57df0c3e77b1b2336459cb03ee3903ce9d5e236ce"}, - {file = "pydantic_core-2.14.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:354db020b1f8f11207b35360b92d95725621eb92656725c849a61e4b550f4acc"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:76fc18653a5c95e5301a52d1b5afb27c9adc77175bf00f73e94f501caf0e05ad"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2646f8270f932d79ba61102a15ea19a50ae0d43b314e22b3f8f4b5fabbfa6e38"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37dad73a2f82975ed563d6a277fd9b50e5d9c79910c4aec787e2d63547202315"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:113752a55a8eaece2e4ac96bc8817f134c2c23477e477d085ba89e3aa0f4dc44"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:8488e973547e8fb1b4193fd9faf5236cf1b7cd5e9e6dc7ff6b4d9afdc4c720cb"}, - {file = "pydantic_core-2.14.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3d1dde10bd9962b1434053239b1d5490fc31a2b02d8950a5f731bc584c7a5a0f"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:2c83892c7bf92b91d30faca53bb8ea21f9d7e39f0ae4008ef2c2f91116d0464a"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:849cff945284c577c5f621d2df76ca7b60f803cc8663ff01b778ad0af0e39bb9"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa89919fbd8a553cd7d03bf23d5bc5deee622e1b5db572121287f0e64979476"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf15145b1f8056d12c67255cd3ce5d317cd4450d5ee747760d8d088d85d12a2d"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cc6bb11f4e8e5ed91d78b9880774fbc0856cb226151b0a93b549c2b26a00c19"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:832d16f248ca0cc96929139734ec32d21c67669dcf8a9f3f733c85054429c012"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b02b5e1f54c3396c48b665050464803c23c685716eb5d82a1d81bf81b5230da4"}, - {file = "pydantic_core-2.14.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1f2d4516c32255782153e858f9a900ca6deadfb217fd3fb21bb2b60b4e04d04d"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0a3e51c2be472b7867eb0c5d025b91400c2b73a0823b89d4303a9097e2ec6655"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:df33902464410a1f1a0411a235f0a34e7e129f12cb6340daca0f9d1390f5fe10"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27828f0227b54804aac6fb077b6bb48e640b5435fdd7fbf0c274093a7b78b69c"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2979dc80246e18e348de51246d4c9b410186ffa3c50e77924bec436b1e36cb"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b28996872b48baf829ee75fa06998b607c66a4847ac838e6fd7473a6b2ab68e7"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ca55c9671bb637ce13d18ef352fd32ae7aba21b4402f300a63f1fb1fd18e0364"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:aecd5ed096b0e5d93fb0367fd8f417cef38ea30b786f2501f6c34eabd9062c38"}, - {file = "pydantic_core-2.14.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:44aaf1a07ad0824e407dafc637a852e9a44d94664293bbe7d8ee549c356c8882"}, - {file = "pydantic_core-2.14.3.tar.gz", hash = "sha256:3ad083df8fe342d4d8d00cc1d3c1a23f0dc84fce416eb301e69f1ddbbe124d3f"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, ] [package.dependencies] @@ -1546,13 +1446,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.17.0" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "pygments-2.17.0-py3-none-any.whl", hash = "sha256:cd0c46944b2551af02ecc15961050182ea120d3895000e2676160820f3421527"}, - {file = "pygments-2.17.0.tar.gz", hash = "sha256:edaa0fa2453d055d0ac94449d1f73ec7bc52c5e318204da1377c1392978c4a8d"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] @@ -1570,30 +1470,6 @@ files = [ {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, ] -[[package]] -name = "pylint" -version = "2.13.9" -description = "python code static checker" -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, - {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, -] - -[package.dependencies] -astroid = ">=2.11.5,<=2.12.0-dev0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -testutil = ["gitpython (>3)"] - [[package]] name = "pyparsing" version = "2.4.7" @@ -1839,17 +1715,17 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "68.2.2" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false +optional = true python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "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 (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] @@ -2058,13 +1934,13 @@ test = ["pytest"] [[package]] name = "sphinxext-opengraph" -version = "0.9.0" +version = "0.9.1" description = "Sphinx Extension to enable OGP support" optional = false python-versions = ">=3.8" files = [ - {file = "sphinxext-opengraph-0.9.0.tar.gz", hash = "sha256:4e57e25b6d56f47b9c06a5a5d68a2a00ed3577c8a39e459b52118c6bfe5e8c8b"}, - {file = "sphinxext_opengraph-0.9.0-py3-none-any.whl", hash = "sha256:ab1eb2ffb531fb85b695e719dba7b0245b0643f6b6c0d1cc258d15a81e72a9f1"}, + {file = "sphinxext-opengraph-0.9.1.tar.gz", hash = "sha256:dd2868a1e7c9497977fbbf44cc0844a42af39ca65fe1bb0272518af225d06fc5"}, + {file = "sphinxext_opengraph-0.9.1-py3-none-any.whl", hash = "sha256:b3b230cc6a5b5189139df937f0d9c7b23c7c204493b22646273687969dcb760e"}, ] [package.dependencies] @@ -2094,22 +1970,22 @@ files = [ [[package]] name = "tornado" -version = "6.3.3" +version = "6.4" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">= 3.8" files = [ - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, - {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"}, - {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"}, - {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"}, - {file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"}, - {file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"}, - {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, ] [[package]] @@ -2138,13 +2014,13 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6. [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] @@ -2165,19 +2041,19 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.6" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] @@ -2185,13 +2061,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wcwidth" -version = "0.2.10" +version = "0.2.12" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.10-py2.py3-none-any.whl", hash = "sha256:aec5179002dd0f0d40c456026e74a729661c9d468e1ed64405e3a6c2176ca36f"}, - {file = "wcwidth-0.2.10.tar.gz", hash = "sha256:390c7454101092a6a5e43baad8f83de615463af459201709556b6e4b1c861f97"}, + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, ] [[package]] @@ -2205,85 +2081,6 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - [[package]] name = "zipp" version = "3.17.0" @@ -2305,4 +2102,4 @@ poetry-plugin = ["poetry"] [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "31d1ea280f619d3573d9b16bb93e992cebf8ddc3953a4d5fe59b580cc844c23b" +content-hash = "bc143bfbfbdc35f145adb30e9427178934ce3eb35654aba47cb3276f12e5d11d" diff --git a/pyproject.toml b/pyproject.toml index 35b1cf07..9e5fa36a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ poetry = {version = "^1.0", allow-prereleases = true, optional = true} [tool.poetry.group.ci.dependencies] black = "^23.3.0" mypy = "^1.1.1" -pylint = "^2.13.0" pytest = "^7.1.2" pytest-cov = "^3.0.0" rstcheck = { version = "^6.1.2", python = "<4" } @@ -115,16 +114,8 @@ _clean_docs.script = "shutil:rmtree('docs/_build', ignore_errors=1)" [tool.poe.tasks.lint] help = "Run linting tools on the code base" - sequence = ["lint-ruff", "lint-pylint"] - - [tool.poe.tasks.lint-ruff] - help = "Evaluate ruff rules" cmd = "ruff check ." - [tool.poe.tasks.lint-pylint] - help = "Evaluate pylint rules" - cmd = "pylint poethepoet" - [tool.poe.tasks.style] help = "Validate black code style" cmd = "black . --check --diff" @@ -147,7 +138,7 @@ _clean_docs.script = "shutil:rmtree('docs/_build', ignore_errors=1)" [tool.rstcheck] -ignore_messages = [ +ignore_messages = [ "Unknown directive type \"autoclass\"", "Hyperlink target \"shell-completion\" is not referenced.", "Hyperlink target \"envfile-option\" is not referenced.", @@ -193,7 +184,7 @@ select = [ "PTH", # flake8-use-pathlib "PGH", # pygrep-hooks "PERF", # perflint - "RUF" # ruff-specific rules + "RUF", # ruff-specific rules ] ignore = [ "C408", # unnecessary-collection-call @@ -201,7 +192,7 @@ ignore = [ "SIM118", # in-dict-keys "PTH109", # os-getcwd "PTH123", # builtin-open - "RUF012" # mutable-class-default + "RUF012", # mutable-class-default ] fixable = ["E", "F", "I"] diff --git a/tests/conftest.py b/tests/conftest.py index 94112d91..f7dd92b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,6 +146,7 @@ def run_poe_subproc( subproc_env = dict(os.environ) subproc_env.pop("VIRTUAL_ENV", None) + subproc_env.pop("POE_CWD", None) # do not inherit this from the test subproc_env.pop("POE_PWD", None) # do not inherit this from the test if env: subproc_env.update(env) diff --git a/tests/fixtures/cwd_project/pyproject.toml b/tests/fixtures/cwd_project/pyproject.toml index eb760bd8..ecb9f491 100644 --- a/tests/fixtures/cwd_project/pyproject.toml +++ b/tests/fixtures/cwd_project/pyproject.toml @@ -11,7 +11,7 @@ cwd = "./subdir/${BAR_ENV}" [tool.poe.tasks.cwd_poe_pwd] cmd = "poe_test_pwd" -cwd = "${POE_PWD}" +cwd = "${POE_CWD}" [tool.poe.tasks.cwd_arg] cmd = "poe_test_pwd" diff --git a/tests/fixtures/envfile_project/pyproject.toml b/tests/fixtures/envfile_project/pyproject.toml index 9dd6e198..a1ed0b8a 100644 --- a/tests/fixtures/envfile_project/pyproject.toml +++ b/tests/fixtures/envfile_project/pyproject.toml @@ -1,12 +1,12 @@ [tool.poe] envfile = "credentials.env" -env = { HOST = "${HOST}:80" } +env = { HOST = "${HOST}:80" } # reference and override value from envfile [tool.poe.tasks.deploy-dev] cmd = """ poe_test_echo "deploying to ${USER}:${PASSWORD}@${HOST}${PATH_SUFFIX}" """ -env = { HOST = "${HOST}80" } # reference and override value from envfile +env = { HOST = "${HOST}80" } # reference and override value from envfile again [tool.poe.tasks.deploy-prod] cmd = """ diff --git a/tests/fixtures/monorepo_project/env1 b/tests/fixtures/monorepo_project/env1 new file mode 100644 index 00000000..20af8f5f --- /dev/null +++ b/tests/fixtures/monorepo_project/env1 @@ -0,0 +1 @@ +REL_ROOT="rel to root" diff --git a/tests/fixtures/monorepo_project/env1t b/tests/fixtures/monorepo_project/env1t new file mode 100644 index 00000000..fb1a32c9 --- /dev/null +++ b/tests/fixtures/monorepo_project/env1t @@ -0,0 +1 @@ +TASK_REL_ROOT="task level rel to root" diff --git a/tests/fixtures/monorepo_project/env2 b/tests/fixtures/monorepo_project/env2 new file mode 100644 index 00000000..5353a7aa --- /dev/null +++ b/tests/fixtures/monorepo_project/env2 @@ -0,0 +1 @@ +REL_PROC_CWD="rel to process cwd" diff --git a/tests/fixtures/monorepo_project/env2t b/tests/fixtures/monorepo_project/env2t new file mode 100644 index 00000000..d13a1086 --- /dev/null +++ b/tests/fixtures/monorepo_project/env2t @@ -0,0 +1 @@ +TASK_REL_PROC_CWD="task level rel to process cwd" diff --git a/tests/fixtures/monorepo_project/pyproject.toml b/tests/fixtures/monorepo_project/pyproject.toml index 9e2be830..27f7e22e 100644 --- a/tests/fixtures/monorepo_project/pyproject.toml +++ b/tests/fixtures/monorepo_project/pyproject.toml @@ -1,12 +1,18 @@ [[tool.poe.include]] path = "subproject_1/pyproject.toml" + [[tool.poe.include]] path = "subproject_2/pyproject.toml" cwd = "subproject_2" + [[tool.poe.include]] path = "subproject_3/pyproject.toml" +[[tool.poe.include]] +path = "subproject_4/pyproject.toml" +cwd = "subproject_4/exec_dir" + [tool.poe.tasks.get_cwd_0] interpreter = "python" diff --git a/tests/fixtures/monorepo_project/subproject_3/env3 b/tests/fixtures/monorepo_project/subproject_3/env3 new file mode 100644 index 00000000..da6d20f6 --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_3/env3 @@ -0,0 +1 @@ +REL_SOURCE_CONFIG="rel to source config" diff --git a/tests/fixtures/monorepo_project/subproject_3/env3t b/tests/fixtures/monorepo_project/subproject_3/env3t new file mode 100644 index 00000000..f0c1d1c7 --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_3/env3t @@ -0,0 +1 @@ +TASK_REL_SOURCE_CONFIG="task level rel to source config" diff --git a/tests/fixtures/monorepo_project/subproject_3/pyproject.toml b/tests/fixtures/monorepo_project/subproject_3/pyproject.toml index f33eb7fe..c2a0cf6d 100644 --- a/tests/fixtures/monorepo_project/subproject_3/pyproject.toml +++ b/tests/fixtures/monorepo_project/subproject_3/pyproject.toml @@ -1,5 +1,27 @@ +[tool.poe] +envfile = ["env1", "${POE_CWD}/env2", "${POE_CONF_DIR}/env3"] + +[tool.poe.tasks.subproj3_env] +shell = """ +echo POE_ROOT: ${POE_ROOT} +echo POE_CWD: ${POE_CWD} +echo POE_CONF_DIR: ${POE_CONF_DIR} +echo POE_ROOT_COPY: ${POE_ROOT_COPY} +echo POE_CWD_COPY: ${POE_CWD_COPY} +echo POE_CONF_DIR_COPY: ${POE_CONF_DIR_COPY} +echo REL_ROOT: ${REL_ROOT} +echo REL_PROC_CWD: ${REL_PROC_CWD} +echo REL_SOURCE_CONFIG: ${REL_SOURCE_CONFIG} +echo TASK_REL_ROOT: ${TASK_REL_ROOT} +echo TASK_REL_PROC_CWD: ${TASK_REL_PROC_CWD} +echo TASK_REL_SOURCE_CONFIG: ${TASK_REL_SOURCE_CONFIG} +""" +envfile = ["env1t", "${POE_CWD}/env2t", "${POE_CONF_DIR}/env3t"] +env = { POE_ROOT_COPY = "${POE_ROOT}", POE_CWD_COPY = "${POE_CWD}", POE_CONF_DIR_COPY = "${POE_CONF_DIR}" } + + [tool.poe.tasks.get_cwd_3] interpreter = "python" diff --git a/tests/fixtures/monorepo_project/subproject_4/env3 b/tests/fixtures/monorepo_project/subproject_4/env3 new file mode 100644 index 00000000..da6d20f6 --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_4/env3 @@ -0,0 +1 @@ +REL_SOURCE_CONFIG="rel to source config" diff --git a/tests/fixtures/monorepo_project/subproject_4/env3t b/tests/fixtures/monorepo_project/subproject_4/env3t new file mode 100644 index 00000000..f0c1d1c7 --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_4/env3t @@ -0,0 +1 @@ +TASK_REL_SOURCE_CONFIG="task level rel to source config" diff --git a/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0 b/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0 new file mode 100644 index 00000000..8ab149fe --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0 @@ -0,0 +1 @@ +FROM_INCLUDE_CWD="rel to cwd" diff --git a/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0t b/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0t new file mode 100644 index 00000000..b411a9c7 --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_4/exec_dir/env0t @@ -0,0 +1 @@ +TASK_FROM_INCLUDE_CWD="task level rel to cwd" diff --git a/tests/fixtures/monorepo_project/subproject_4/pyproject.toml b/tests/fixtures/monorepo_project/subproject_4/pyproject.toml new file mode 100644 index 00000000..b10e25ae --- /dev/null +++ b/tests/fixtures/monorepo_project/subproject_4/pyproject.toml @@ -0,0 +1,23 @@ + +[tool.poe] +envfile = ["env0", "${POE_ROOT}/env1", "${POE_CWD}/env2", "${POE_CONF_DIR}/../env3"] + +[tool.poe.tasks.subproj4_env] +shell = """ +echo POE_ROOT: ${POE_ROOT} +echo POE_CWD: ${POE_CWD} +echo POE_CONF_DIR: ${POE_CONF_DIR} +echo POE_ROOT_COPY: ${POE_ROOT_COPY} +echo POE_CWD_COPY: ${POE_CWD_COPY} +echo POE_CONF_DIR_COPY: ${POE_CONF_DIR_COPY} +echo FROM_INCLUDE_CWD: ${FROM_INCLUDE_CWD} +echo REL_ROOT: ${REL_ROOT} +echo REL_PROC_CWD: ${REL_PROC_CWD} +echo REL_SOURCE_CONFIG: ${REL_SOURCE_CONFIG} +echo TASK_FROM_INCLUDE_CWD: ${TASK_FROM_INCLUDE_CWD} +echo TASK_REL_ROOT: ${TASK_REL_ROOT} +echo TASK_REL_PROC_CWD: ${TASK_REL_PROC_CWD} +echo TASK_REL_SOURCE_CONFIG: ${TASK_REL_SOURCE_CONFIG} +""" +envfile = ["env0t", "${POE_ROOT}/env1t", "${POE_CWD}/env2t", "${POE_CONF_DIR}/../env3t"] +env = { POE_ROOT_COPY = "${POE_ROOT}", POE_CWD_COPY = "${POE_CWD}", POE_CONF_DIR_COPY = "${POE_CONF_DIR}" } diff --git a/tests/test_cmd_tasks.py b/tests/test_cmd_tasks.py index 1965dee2..37f1cd0d 100644 --- a/tests/test_cmd_tasks.py +++ b/tests/test_cmd_tasks.py @@ -115,7 +115,7 @@ def test_cmd_task_with_cwd_option_pwd_override(run_poe_subproc, poe_project_path "cwd_poe_pwd", project="cwd", env={ - "POE_PWD": str( + "POE_CWD": str( poe_project_path.joinpath( "tests", "fixtures", "cwd_project", "subdir", "bar" ) diff --git a/tests/test_ignore_fail.py b/tests/test_ignore_fail.py index 335cf44a..65c8b240 100644 --- a/tests/test_ignore_fail.py +++ b/tests/test_ignore_fail.py @@ -19,6 +19,7 @@ def generator(ignore_fail): project_tmpl += "\nignore_fail = true" elif not isinstance(ignore_fail, bool): project_tmpl += f'\nignore_fail = "{ignore_fail}"' + with open(tmp_path / "pyproject.toml", "w") as fp: fp.write(project_tmpl) @@ -62,7 +63,6 @@ def test_invalid_ingore_value(generate_pyproject, run_poe): result = run_poe("all_tasks", cwd=project_path) assert result.code == 1, "Expected non-zero result" assert ( - "Unsupported value for option `ignore_fail` for task 'all_tasks'." - ' Expected one of (true, false, "return_zero", "return_non_zero")' - in result.capture - ) + "| Option 'ignore_fail' must be one of " + "(True, False, 'return_zero', 'return_non_zero')\n" + ) in result.capture diff --git a/tests/test_includes.py b/tests/test_includes.py index 15990f9b..17d9909d 100644 --- a/tests/test_includes.py +++ b/tests/test_includes.py @@ -103,7 +103,9 @@ def test_monorepo_contains_only_expected_tasks(run_poe_subproc, projects): " get_cwd_1 \n" " add \n" " get_cwd_2 \n" - " get_cwd_3 \n\n\n" + " subproj3_env \n" + " get_cwd_3 \n" + " subproj4_env \n\n\n" ) assert result.stdout == "" assert result.stderr == "" @@ -180,3 +182,129 @@ def test_monorepo_runs_each_task_with_expected_cwd( else: assert result.stdout.endswith("/tests/fixtures/example_project\n") assert result.stderr == "" + + +def test_include_subproject_envfiles_no_cwd_set(run_poe_subproc, projects, is_windows): + result = run_poe_subproc("subproj3_env", project="monorepo") + assert result.capture == ( + "Poe => echo POE_ROOT: ${POE_ROOT}\n" + "echo POE_CWD: ${POE_CWD}\n" + "echo POE_CONF_DIR: ${POE_CONF_DIR}\n" + "echo POE_ROOT_COPY: ${POE_ROOT_COPY}\n" + "echo POE_CWD_COPY: ${POE_CWD_COPY}\n" + "echo POE_CONF_DIR_COPY: ${POE_CONF_DIR_COPY}\n" + "echo REL_ROOT: ${REL_ROOT}\n" + "echo REL_PROC_CWD: ${REL_PROC_CWD}\n" + "echo REL_SOURCE_CONFIG: ${REL_SOURCE_CONFIG}\n" + "echo TASK_REL_ROOT: ${TASK_REL_ROOT}\n" + "echo TASK_REL_PROC_CWD: ${TASK_REL_PROC_CWD}\n" + "echo TASK_REL_SOURCE_CONFIG: ${TASK_REL_SOURCE_CONFIG}\n" + ) + printed_vars = { + line.split(": ")[0]: line.split(": ")[1] + for line in result.stdout.split("\n") + if ": " in line + } + if is_windows: + assert printed_vars["POE_ROOT"].endswith("\\tests\\fixtures\\monorepo_project") + assert printed_vars["POE_CWD"].endswith("\\tests\\fixtures\\monorepo_project") + assert printed_vars["POE_CONF_DIR"].endswith( + "\\tests\\fixtures\\monorepo_project\\subproject_3" + ) + assert printed_vars["POE_ROOT_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project" + ) + assert printed_vars["POE_CWD_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project" + ) + assert printed_vars["POE_CONF_DIR_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project\\subproject_3" + ) + else: + assert printed_vars["POE_ROOT"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CWD"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CONF_DIR"].endswith( + "/tests/fixtures/monorepo_project/subproject_3" + ) + assert printed_vars["POE_ROOT_COPY"].endswith( + "/tests/fixtures/monorepo_project" + ) + assert printed_vars["POE_CWD_COPY"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CONF_DIR_COPY"].endswith( + "/tests/fixtures/monorepo_project/subproject_3" + ) + assert result.stdout.endswith( + "REL_ROOT: rel to root\n" + "REL_PROC_CWD: rel to process cwd\n" + "REL_SOURCE_CONFIG: rel to source config\n" + "TASK_REL_ROOT: task level rel to root\n" + "TASK_REL_PROC_CWD: task level rel to process cwd\n" + "TASK_REL_SOURCE_CONFIG: task level rel to source config\n" + ) + assert result.stderr == "" + + +def test_include_subproject_envfiles_with_cwd_set( + run_poe_subproc, projects, is_windows +): + result = run_poe_subproc("subproj4_env", project="monorepo") + assert result.capture == ( + "Poe => echo POE_ROOT: ${POE_ROOT}\n" + "echo POE_CWD: ${POE_CWD}\n" + "echo POE_CONF_DIR: ${POE_CONF_DIR}\n" + "echo POE_ROOT_COPY: ${POE_ROOT_COPY}\n" + "echo POE_CWD_COPY: ${POE_CWD_COPY}\n" + "echo POE_CONF_DIR_COPY: ${POE_CONF_DIR_COPY}\n" + "echo FROM_INCLUDE_CWD: ${FROM_INCLUDE_CWD}\n" + "echo REL_ROOT: ${REL_ROOT}\n" + "echo REL_PROC_CWD: ${REL_PROC_CWD}\n" + "echo REL_SOURCE_CONFIG: ${REL_SOURCE_CONFIG}\n" + "echo TASK_FROM_INCLUDE_CWD: ${TASK_FROM_INCLUDE_CWD}\n" + "echo TASK_REL_ROOT: ${TASK_REL_ROOT}\n" + "echo TASK_REL_PROC_CWD: ${TASK_REL_PROC_CWD}\n" + "echo TASK_REL_SOURCE_CONFIG: ${TASK_REL_SOURCE_CONFIG}\n" + ) + printed_vars = { + line.split(": ")[0]: line.split(": ")[1] + for line in result.stdout.split("\n") + if ": " in line + } + if is_windows: + assert printed_vars["POE_ROOT"].endswith("\\tests\\fixtures\\monorepo_project") + assert printed_vars["POE_CWD"].endswith("\\tests\\fixtures\\monorepo_project") + assert printed_vars["POE_CONF_DIR"].endswith( + "\\tests\\fixtures\\monorepo_project\\subproject_4\\exec_dir" + ) + assert printed_vars["POE_ROOT_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project" + ) + assert printed_vars["POE_CWD_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project" + ) + assert printed_vars["POE_CONF_DIR_COPY"].endswith( + "\\tests\\fixtures\\monorepo_project\\subproject_4\\exec_dir" + ) + else: + assert printed_vars["POE_ROOT"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CWD"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CONF_DIR"].endswith( + "/tests/fixtures/monorepo_project/subproject_4/exec_dir" + ) + assert printed_vars["POE_ROOT_COPY"].endswith( + "/tests/fixtures/monorepo_project" + ) + assert printed_vars["POE_CWD_COPY"].endswith("/tests/fixtures/monorepo_project") + assert printed_vars["POE_CONF_DIR_COPY"].endswith( + "/tests/fixtures/monorepo_project/subproject_4/exec_dir" + ) + assert result.stdout.endswith( + "FROM_INCLUDE_CWD: rel to cwd\n" + "REL_ROOT: rel to root\n" + "REL_PROC_CWD: rel to process cwd\n" + "REL_SOURCE_CONFIG: rel to source config\n" + "TASK_FROM_INCLUDE_CWD: task level rel to cwd\n" + "TASK_REL_ROOT: task level rel to root\n" + "TASK_REL_PROC_CWD: task level rel to process cwd\n" + "TASK_REL_SOURCE_CONFIG: task level rel to source config\n" + ) + assert result.stderr == "" diff --git a/tests/test_poetry_plugin.py b/tests/test_poetry_plugin.py index c76f3734..d265beb9 100644 --- a/tests/test_poetry_plugin.py +++ b/tests/test_poetry_plugin.py @@ -1,3 +1,4 @@ +import os import re import pytest @@ -27,6 +28,11 @@ def test_poetry_help(run_poetry, projects): # assert result.stderr == "" +# TODO: re-enable this test +@pytest.mark.skipif( + os.environ.get("GITHUB_ACTIONS", "false") == "true", + reason="Skipping test the doesn't seem to work in GitHub Actions lately", +) @pytest.mark.slow() @pytest.mark.usefixtures("_setup_poetry_project") def test_task_with_cli_dependency(run_poetry, projects, is_windows): @@ -47,6 +53,11 @@ def test_task_with_cli_dependency(run_poetry, projects, is_windows): # assert result.stderr == "" +# TODO: re-enable this test +@pytest.mark.skipif( + os.environ.get("GITHUB_ACTIONS", "false") == "true", + reason="Skipping test the doesn't seem to work in GitHub Actions lately", +) @pytest.mark.slow() @pytest.mark.usefixtures("_setup_poetry_project") def test_task_with_lib_dependency(run_poetry, projects): @@ -148,6 +159,11 @@ def test_running_poetry_command_with_hooks_with_directory(run_poetry, projects): # assert result.stderr == "" +# TODO: re-enable this test +@pytest.mark.skipif( + os.environ.get("GITHUB_ACTIONS", "false") == "true", + reason="Skipping test the doesn't seem to work in GitHub Actions lately", +) @pytest.mark.slow() @pytest.mark.usefixtures("_setup_poetry_project") def test_task_with_cli_dependency_with_directory(run_poetry, projects, is_windows): diff --git a/tests/test_script_tasks.py b/tests/test_script_tasks.py index 35f74c14..cbd9e372 100644 --- a/tests/test_script_tasks.py +++ b/tests/test_script_tasks.py @@ -212,9 +212,11 @@ def test_script_task_bad_type(run_poe_subproc, projects): "--greeting=hello", ) assert ( - "Error: 'datetime' is not a valid type for arg 'greeting' of task 'bad-type'. " - "Choose one of {boolean float integer string} \n" in result.capture - ) + "Error: Invalid argument 'greeting' declared in task 'bad-type'\n" + " | Option 'type' must be one of " + "('string', 'float', 'integer', 'boolean')\n" + ) in result.capture + assert result.stdout == "" assert result.stderr == "" @@ -226,9 +228,9 @@ def test_script_task_bad_content(run_poe_subproc, projects): "--greeting=hello", ) assert ( - "Error: Task 'bad-type' contains invalid callable reference " - "'dummy_package:main[greeting]' " - "(expected something like `module:callable` or `module:callable()`)" + "Error: Invalid task 'bad-type'\n" + " | Invalid callable reference 'dummy_package:main[greeting]'\n" + " | (expected something like `module:callable` or `module:callable()`)\n" ) in result.capture assert result.stdout == "" assert result.stderr == "" diff --git a/tests/test_shell_task.py b/tests/test_shell_task.py index 37fe6fa7..8ab90456 100644 --- a/tests/test_shell_task.py +++ b/tests/test_shell_task.py @@ -97,9 +97,10 @@ def test_bad_interpreter_config(run_poe_subproc, projects): "bad-interpreter", ) assert ( - "Error: Unsupported value for option `interpreter` for task 'bad-interpreter'." - " Expected one of (" - "'posix', 'sh', 'bash', 'zsh', 'fish', 'pwsh', 'powershell', 'python')" + "Error: Invalid task 'bad-interpreter'\n" + " | Invalid value for option 'interpreter',\n" + " | Expected one of " + "('posix', 'sh', 'bash', 'zsh', 'fish', 'pwsh', 'powershell', 'python')\n" ) in result.capture assert result.stdout == "" assert result.stderr == "" diff --git a/tests/test_switch_task.py b/tests/test_switch_task.py index 6f0943fc..10952afd 100644 --- a/tests/test_switch_task.py +++ b/tests/test_switch_task.py @@ -70,8 +70,9 @@ def test_switch_default_pass(run_poe_subproc): def test_switch_default_fail(run_poe_subproc): result = run_poe_subproc("default_fail", project="switch") assert result.capture == ( - "Poe <= poe_test_echo nothing\nError: Control value 'nothing' did not match " - "any cases in switch task 'default_fail'. \n" + "Poe <= poe_test_echo nothing\n" + "Error: Control value 'nothing' did not match any cases in switch task " + "'default_fail'.\n" ) assert result.stdout == "" assert result.stderr == ""