diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4fe8cb3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +`id-configurator` is a proprietary CLI tool by InterDiode to "configure a device in an air-gapped environment". +It targets Python 3.13+ and is packaged with the `uv_build` backend. +It takes a configuration (JSON or YAML, either from a file or from a base64-encoded environment variable) and applies it to the system, including installing root certificates, configuring package managers, and other system-level settings. +Modified files are written to a destination directory given with --dst by the user, this directory can be used to build a Docker image. +The original file system can be provided with --src by the user to detect existing configuration. + +Example: +- the configuration specifies a root certificate to install, the tool will copy it to `/etc/ssl/certs` and update the system's CA certificates. +- the configuration specifies a apt source to add and `` contains an existing `/etc/apt/sources.list`, the tool will merge the new source into the existing sources and write the result to `/etc/apt/sources.list`. + +The configuration is expected to be a JSON or YAML object with the following structure: + +```yaml +uuid: +organisation: +description: +configurations: + certificates: + - name: + content: + apt: + sources: + - name: + content: + keys: + - name: + content: + yum: + repositories: + - name: + index-url: + gpg-url: + python: + repositories: + - name: + index-url: +``` + +The tool must have an extensible modular structure. Each module can use any part of the configuration, detect if is relevant by checking the current filesystem from ``, and write modified files to ``. +Several modules can read the certificates from the configuration and use them differently. +The tool must be able to run in a Docker container, and must not require any network access. +It is forbidden to write any file outside of ``. + +The tool must be called by `python -m id_configurator`, or via the console script entry point `id-configurator`. + +Here is a preliminary list of modules that will be implemented: +- install root certificates in the system's CA store +- configure system package managers: + - apt + - yum + - apk + - dnf +- configure programming language package managers: + - python (pip) + - ruby (gem) + - nodejs (npm) + - golang (go) + - java (maven, gradle) + - php (composer) + - + +## Current state + +This repository is a skeleton: `id_configurator/__init__.py` is currently empty, and `README.md` is empty. `pyproject.toml` already declares a console-script entry point: + +``` +id-configurator = "id_configurator:main" +``` + +There is no `main` callable yet — implementing it in `id_configurator/__init__.py` (or importing it there) is the first thing needed to make the CLI runnable. No tests, no `[tool.ruff]`/`[tool.pytest]` config, and no CI config exist yet. + +## Commands + +- Install/sync dependencies: `uv sync` +- Run the CLI (once `main` exists): `uv run id-configurator` +- Lint and auto-fix: `uv run ruff check --fix .` +- Format: `uv run ruff format .` +- Run all pre-commit hooks (ruff, bandit, shellcheck, commitizen, check-yaml/toml/json, etc.): `uv run pre-commit run --all-files` + +There is no test suite yet. + +## Conventions + +- Python version is pinned to 3.13 (`.python-version`, `requires-python = ">=3.13"` in `pyproject.toml`). +- `.editorconfig`: 4-space indentation, single quotes, LF line endings, 120 max line length for `.py` files. +- Commit messages are linted by commitizen via pre-commit, implying Conventional Commits format. +- The package is proprietary (`Private :: Do Not Upload` classifier) and is not published to PyPI. diff --git a/id_configurator/__init__.py b/id_configurator/__init__.py index e69de29..28b6a26 100644 --- a/id_configurator/__init__.py +++ b/id_configurator/__init__.py @@ -0,0 +1,3 @@ +from id_configurator.cli import app, main + +__all__ = ["app", "main"] diff --git a/id_configurator/__main__.py b/id_configurator/__main__.py new file mode 100644 index 0000000..c05a076 --- /dev/null +++ b/id_configurator/__main__.py @@ -0,0 +1,3 @@ +from id_configurator.cli import main + +main(__name__) diff --git a/id_configurator/cli.py b/id_configurator/cli.py new file mode 100644 index 0000000..36097e0 --- /dev/null +++ b/id_configurator/cli.py @@ -0,0 +1,120 @@ +"""Typer CLI for id-configurator.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Annotated + +import typer + +from id_configurator import modules # noqa: F401 (registers the built-in modules) +from id_configurator.config import load_configuration +from id_configurator.context import Context +from id_configurator.modules.base import all_modules + +app = typer.Typer( + name="id-configurator", + help="Configure a device in an air-gapped environment.", + no_args_is_help=True, +) + +logger = logging.getLogger("id_configurator") + +ConfigFileOption = Annotated[ + Path | None, + typer.Option( + "--config", + exists=True, + dir_okay=False, + help="Path to a JSON or YAML configuration file.", + ), +] +ConfigEnvOption = Annotated[ + str | None, + typer.Option( + "--config-env", + envvar="ID_CONFIGURATOR_CONFIG", + help="Base64-encoded JSON/YAML configuration.", + ), +] +SrcOption = Annotated[ + Path | None, + typer.Option( + "--src", + exists=True, + file_okay=False, + help="Existing filesystem used to detect the current configuration.", + ), +] +DstOption = Annotated[ + Path, + typer.Option( + "--dst", + file_okay=False, + help="Destination directory receiving the modified files.", + ), +] +VerboseOption = Annotated[ + bool, typer.Option("--verbose", "-v", help="Enable verbose (debug) logging.") +] + + +def _configure_logging(verbose: bool) -> None: + logging.basicConfig( + level=logging.DEBUG if verbose else logging.INFO, + format="%(levelname)s %(name)s: %(message)s", + ) + + +def _load_configuration(config: Path | None, config_env: str | None): + if config is None and config_env is None: + raise typer.BadParameter( + "either --config or --config-env (or $ID_CONFIGURATOR_CONFIG) must be provided" + ) + return load_configuration(config, config_env) + + +@app.command() +def apply( + dst: DstOption, + config: ConfigFileOption = None, + config_env: ConfigEnvOption = None, + src: SrcOption = None, + verbose: VerboseOption = False, +) -> None: + """Apply a configuration to the destination directory.""" + _configure_logging(verbose) + configuration = _load_configuration(config, config_env) + dst.mkdir(parents=True, exist_ok=True) + context = Context(config=configuration, dst=dst, src=src) + for module_cls in all_modules(): + module = module_cls() + if module.is_relevant(context): + logger.info("applying module %s", module.name) + module.apply(context) + else: + logger.debug("skipping module %s (not relevant)", module.name) + + +@app.command() +def validate( + config: ConfigFileOption = None, config_env: ConfigEnvOption = None +) -> None: + """Validate a configuration file without applying it.""" + configuration = _load_configuration(config, config_env) + typer.echo( + f"configuration {configuration.uuid} is valid (organisation: {configuration.organisation})" + ) + + +@app.command("list-modules") +def list_modules_command() -> None: + """List the available configuration modules.""" + for module_cls in all_modules(): + typer.echo(module_cls.name) + + +def main(name: str = "__main__") -> None: + if name == "__main__": + app() diff --git a/id_configurator/config.py b/id_configurator/config.py new file mode 100644 index 0000000..ababa7f --- /dev/null +++ b/id_configurator/config.py @@ -0,0 +1,165 @@ +"""Configuration schema and loading helpers for id-configurator.""" + +from __future__ import annotations + +import base64 +import json +from pathlib import Path +from uuid import UUID + +import yaml +from pydantic import BaseModel, ConfigDict, Field + + +class NamedContent(BaseModel): + """A named piece of content, generally base64-encoded (a certificate, a GPG key, ...).""" + + name: str + content: str + + +def _yes_no(value: bool) -> str: + return "yes" if value else "no" + + +class AptSource(BaseModel): + """A single APT source, in the DEB822 format (see sources.list(5)).""" + + model_config = ConfigDict(populate_by_name=True) + + name: str + types: list[str] = Field(default_factory=lambda: ["deb"]) + uris: list[str] + suites: list[str] + components: list[str] = Field(default_factory=list) + architectures: list[str] = Field(default_factory=list) + languages: list[str] = Field(default_factory=list) + targets: list[str] = Field(default_factory=list) + enabled: bool = True + trusted: bool | None = None + signed_by: str | None = Field(default=None, alias="signed-by") + pdiffs: bool | None = None + by_hash: bool | None = Field(default=None, alias="by-hash") + allow_insecure: bool | None = Field(default=None, alias="allow-insecure") + allow_weak: bool | None = Field(default=None, alias="allow-weak") + allow_downgrade_to_insecure: bool | None = Field( + default=None, alias="allow-downgrade-to-insecure" + ) + check_valid_until: bool | None = Field(default=None, alias="check-valid-until") + valid_until_min: str | None = Field(default=None, alias="valid-until-min") + valid_until_max: str | None = Field(default=None, alias="valid-until-max") + check_date: bool | None = Field(default=None, alias="check-date") + date_max_future: str | None = Field(default=None, alias="date-max-future") + description: str | None = None + + def to_deb822(self) -> str: + """Render this source as a DEB822 stanza (as used in /etc/apt/sources.list.d/*.sources).""" + fields = [ + ("Types", " ".join(self.types)), + ("URIs", " ".join(self.uris)), + ("Suites", " ".join(self.suites)), + ] + if self.components: + fields.append(("Components", " ".join(self.components))) + if self.architectures: + fields.append(("Architectures", " ".join(self.architectures))) + if self.languages: + fields.append(("Languages", " ".join(self.languages))) + if self.targets: + fields.append(("Targets", " ".join(self.targets))) + if self.signed_by: + fields.append(("Signed-By", self.signed_by)) + if self.trusted is not None: + fields.append(("Trusted", _yes_no(self.trusted))) + if self.pdiffs is not None: + fields.append(("PDiffs", _yes_no(self.pdiffs))) + if self.by_hash is not None: + fields.append(("By-Hash", _yes_no(self.by_hash))) + if self.allow_insecure is not None: + fields.append(("Allow-Insecure", _yes_no(self.allow_insecure))) + if self.allow_weak is not None: + fields.append(("Allow-Weak", _yes_no(self.allow_weak))) + if self.allow_downgrade_to_insecure is not None: + fields.append( + ( + "Allow-Downgrade-To-Insecure", + _yes_no(self.allow_downgrade_to_insecure), + ) + ) + if self.check_valid_until is not None: + fields.append(("Check-Valid-Until", _yes_no(self.check_valid_until))) + if self.valid_until_min: + fields.append(("Valid-Until-Min", self.valid_until_min)) + if self.valid_until_max: + fields.append(("Valid-Until-Max", self.valid_until_max)) + if self.check_date is not None: + fields.append(("Check-Date", _yes_no(self.check_date))) + if self.date_max_future: + fields.append(("Date-Max-Future", self.date_max_future)) + if self.description: + fields.append(("Description", self.description)) + fields.append(("Enabled", _yes_no(self.enabled))) + return "\n".join(f"{key}: {value}" for key, value in fields) + "\n" + + +class AptConfig(BaseModel): + sources: list[AptSource] = Field(default_factory=list) + keys: list[NamedContent] = Field(default_factory=list) + + +class YumRepository(BaseModel): + model_config = ConfigDict(populate_by_name=True) + name: str + index_url: str = Field(alias="index-url") + gpg_url: str | None = Field(default=None, alias="gpg-url") + + +class YumConfig(BaseModel): + repositories: list[YumRepository] = Field(default_factory=list) + + +class PythonRepository(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + name: str + index_url: str = Field(alias="index-url") + + +class PythonConfig(BaseModel): + repositories: list[PythonRepository] = Field(default_factory=list) + + +class ConfigurationsBlock(BaseModel): + certificates: list[NamedContent] = Field(default_factory=list) + apt: AptConfig = Field(default_factory=AptConfig) + yum: YumConfig = Field(default_factory=YumConfig) + python: PythonConfig = Field(default_factory=PythonConfig) + + +class Configuration(BaseModel): + uuid: UUID + organisation: str + description: str = "" + configurations: ConfigurationsBlock = Field(default_factory=ConfigurationsBlock) + + +def parse_configuration(raw: str) -> Configuration: + """Parse a configuration from a JSON or YAML string.""" + try: + data = json.loads(raw) + except json.JSONDecodeError: + data = yaml.safe_load(raw) + return Configuration.model_validate(data) + + +def load_configuration( + config_file: Path | None, config_env: str | None +) -> Configuration: + """Load a configuration from a file, or from a base64-encoded string (typically an env var).""" + if config_file is not None: + raw = config_file.read_text(encoding="utf-8") + elif config_env is not None: + raw = base64.b64decode(config_env).decode("utf-8") + else: + raise ValueError("either --config or --config-env must be provided") + return parse_configuration(raw) diff --git a/id_configurator/context.py b/id_configurator/context.py new file mode 100644 index 0000000..14bc3ae --- /dev/null +++ b/id_configurator/context.py @@ -0,0 +1,56 @@ +"""Execution context shared by all modules. + +The context is responsible for enforcing that modules never write outside of +the destination directory, and gives read access to the (optional) source +filesystem used to detect the existing configuration. +""" + +from __future__ import annotations + +from pathlib import Path + +from id_configurator.config import Configuration + + +class Context: + def __init__( + self, config: Configuration, dst: Path, src: Path | None = None + ) -> None: + self.config = config + self.dst = dst.resolve() + self.src = src.resolve() if src is not None else None + + def dst_path(self, relative: str) -> Path: + """Resolve a path relative to the destination directory, refusing to escape it.""" + path = (self.dst / relative.lstrip("/")).resolve() + if path != self.dst and self.dst not in path.parents: + raise ValueError( + f"refusing to write outside of the destination directory: {relative}" + ) + return path + + def src_path(self, relative: str) -> Path | None: + """Resolve a path relative to the source directory, or None if no source was given.""" + if self.src is None: + return None + return (self.src / relative.lstrip("/")).resolve() + + def write_file(self, relative: str, content: bytes, mode: int = 0o644) -> Path: + path = self.dst_path(relative) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + path.chmod(mode) + return path + + def read_src_file(self, relative: str) -> bytes | None: + path = self.src_path(relative) + if path is None or not path.is_file(): + return None + return path.read_bytes() + + def iter_src_dir(self, relative: str) -> list[Path]: + """List the files present in a directory relative to the source filesystem, if any.""" + path = self.src_path(relative) + if path is None or not path.is_dir(): + return [] + return sorted(entry for entry in path.iterdir() if entry.is_file()) diff --git a/id_configurator/modules/__init__.py b/id_configurator/modules/__init__.py new file mode 100644 index 0000000..0fec04d --- /dev/null +++ b/id_configurator/modules/__init__.py @@ -0,0 +1,8 @@ +"""Configuration modules. + +Importing this package registers every built-in module (see `base.register`). +""" + +from id_configurator.modules import apt, certificates + +__all__ = ["apt", "certificates"] diff --git a/id_configurator/modules/apt.py b/id_configurator/modules/apt.py new file mode 100644 index 0000000..066c49a --- /dev/null +++ b/id_configurator/modules/apt.py @@ -0,0 +1,100 @@ +"""Detect existing APT sources and configure new ones, in the DEB822 format. + +If `/etc/apt` exists, any source file already present there (classic +one-line `.list` files or DEB822 `.sources` files, whether at the top level +or under `sources.list.d/`) is copied into `/etc/apt`, disabled so it no +longer takes effect, and the sources declared in the configuration are added +as new, enabled `.sources` files. +""" + +from __future__ import annotations + +import base64 +import logging +from pathlib import Path + +from id_configurator.context import Context +from id_configurator.modules.base import Module, register + +logger = logging.getLogger(__name__) + +_APT_DIR = "etc/apt" +_SOURCES_LIST = "etc/apt/sources.list" +_SOURCES_LIST_D = "etc/apt/sources.list.d" +_KEYRINGS_DIR = "etc/apt/keyrings" + + +def _disable_list_content(content: str) -> str: + lines = [ + line if not line.strip() or line.strip().startswith("#") else f"# {line}" + for line in content.splitlines() + ] + return "\n".join(lines) + "\n" + + +def _disable_deb822_content(content: str) -> str: + disabled_stanzas = [] + for stanza in content.split("\n\n"): + if not stanza.strip(): + continue + lines = stanza.splitlines() + new_lines = [line for line in lines if not line.lower().startswith("enabled:")] + new_lines.append("Enabled: no") + disabled_stanzas.append("\n".join(new_lines)) + return "\n\n".join(disabled_stanzas) + "\n" + + +@register +class AptModule(Module): + name = "apt" + + def is_relevant(self, context: Context) -> bool: + return self._src_apt_dir(context) is not None + + @staticmethod + def _src_apt_dir(context: Context) -> Path | None: + path = context.src_path(_APT_DIR) + if path is not None and path.is_dir(): + return path + return None + + def apply(self, context: Context) -> None: + self._copy_existing_disabled(context) + self._write_configured_keys(context) + self._write_configured_sources(context) + + @staticmethod + def _copy_existing_disabled(context: Context) -> None: + existing_top_level = context.read_src_file(_SOURCES_LIST) + if existing_top_level is not None: + disabled = _disable_list_content(existing_top_level.decode("utf-8")) + context.write_file(_SOURCES_LIST, disabled.encode("utf-8")) + logger.info("disabled existing apt source list %s", _SOURCES_LIST) + + for entry in context.iter_src_dir(_SOURCES_LIST_D): + if entry.suffix == ".sources": + disabled = _disable_deb822_content(entry.read_text(encoding="utf-8")) + elif entry.suffix == ".list": + disabled = _disable_list_content(entry.read_text(encoding="utf-8")) + else: + continue + context.write_file( + f"{_SOURCES_LIST_D}/{entry.name}", disabled.encode("utf-8") + ) + logger.info("disabled existing apt source %s", entry.name) + + @staticmethod + def _write_configured_keys(context: Context) -> None: + for key in context.config.configurations.apt.keys: + content = base64.b64decode(key.content) + context.write_file(f"{_KEYRINGS_DIR}/{key.name}.asc", content) + logger.info("installed apt keyring %s", key.name) + + @staticmethod + def _write_configured_sources(context: Context) -> None: + for source in context.config.configurations.apt.sources: + context.write_file( + f"{_SOURCES_LIST_D}/{source.name}.sources", + source.to_deb822().encode("utf-8"), + ) + logger.info("added apt source %s", source.name) diff --git a/id_configurator/modules/base.py b/id_configurator/modules/base.py new file mode 100644 index 0000000..1d9055a --- /dev/null +++ b/id_configurator/modules/base.py @@ -0,0 +1,36 @@ +"""Base class and registry for configuration modules. + +Each module inspects the configuration and the source filesystem (via the +`Context`) to decide whether it applies, then writes the resulting files to +the destination directory. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from id_configurator.context import Context + +_REGISTRY: list[type[Module]] = [] + + +class Module(ABC): + name: str + + @abstractmethod + def is_relevant(self, context: Context) -> bool: + """Return True if this module has something to do for the given configuration.""" + + @abstractmethod + def apply(self, context: Context) -> None: + """Write the module's changes to the destination directory.""" + + +def register(module_cls: type[Module]) -> type[Module]: + """Class decorator registering a module so it is picked up by the CLI.""" + _REGISTRY.append(module_cls) + return module_cls + + +def all_modules() -> list[type[Module]]: + return list(_REGISTRY) diff --git a/id_configurator/modules/certificates.py b/id_configurator/modules/certificates.py new file mode 100644 index 0000000..e3e1d6f --- /dev/null +++ b/id_configurator/modules/certificates.py @@ -0,0 +1,81 @@ +"""Install root certificates into the system's CA store. + +Certificates from the configuration are dropped into the "local" CA +directory, then the real `update-ca-certificates` binary is invoked to +rebuild the trust store (bundle + hashed symlinks) exactly like it would on +a real Debian/Ubuntu system, instead of reimplementing that logic here. +""" + +from __future__ import annotations + +import base64 +import logging +import subprocess + +from id_configurator.context import Context +from id_configurator.modules.base import Module, register + +logger = logging.getLogger(__name__) + +_LOCAL_CERTS_DIR = "usr/local/share/ca-certificates" +_CERTSCONF = "etc/ca-certificates.conf" +_CERTSDIR = "usr/share/ca-certificates" +_ETCCERTSDIR = "etc/ssl/certs" + + +@register +class CertificatesModule(Module): + name = "certificates" + + def is_relevant(self, context: Context) -> bool: + return bool(context.config.configurations.certificates) + + def apply(self, context: Context) -> None: + configured_names = { + certificate.name + for certificate in context.config.configurations.certificates + } + for existing in context.iter_src_dir(_LOCAL_CERTS_DIR): + if existing.stem not in configured_names: + context.write_file( + f"{_LOCAL_CERTS_DIR}/{existing.name}", existing.read_bytes() + ) + for certificate in context.config.configurations.certificates: + content = base64.b64decode(certificate.content) + context.write_file(f"{_LOCAL_CERTS_DIR}/{certificate.name}.crt", content) + logger.info("installed root certificate %s", certificate.name) + self._update_ca_certificates_conf(context) + + @staticmethod + def _update_ca_certificates_conf(context: Context) -> None: + etccertsdir = context.dst_path(_ETCCERTSDIR) + localcertsdir = context.dst_path(_LOCAL_CERTS_DIR) + etccertsdir.mkdir(parents=True, exist_ok=True) + localcertsdir.mkdir(parents=True, exist_ok=True) + + args = [ + "update-ca-certificates", + "--etccertsdir", + str(etccertsdir), + "--localcertsdir", + str(localcertsdir), + ] + certsconf = context.src_path(_CERTSCONF) + if certsconf is not None and certsconf.is_file(): + args += ["--certsconf", str(certsconf)] + certsdir = context.src_path(_CERTSDIR) + if certsdir is not None and certsdir.is_dir(): + args += ["--certsdir", str(certsdir)] + + logger.debug("running: %s", " ".join(args)) + try: + result = subprocess.run(args, check=True, capture_output=True, text=True) + except FileNotFoundError as exc: + raise RuntimeError( + "update-ca-certificates is not available; the ca-certificates package must be installed " + "in the environment running id-configurator" + ) from exc + except subprocess.CalledProcessError as exc: + raise RuntimeError(f"update-ca-certificates failed:\n{exc.stderr}") from exc + if result.stderr: + logger.debug("%s", result.stderr.strip()) diff --git a/pyproject.toml b/pyproject.toml index 0d505d1..48277d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,16 @@ build-backend = "uv_build" requires = ["uv_build>=0.12.3,<0.13.0"] +[dependency-groups] +dev = [ + "tox>=4.60.1", + "tox-uv>=1.36.0" +] +test = [ + "pytest>=9.1.1", + "pytest-cov>=7.1.0" +] + [project] authors = [ {name = "InterDiode", email = "contact@mail.interdiode.fr"} @@ -17,7 +27,11 @@ classifiers = [ 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14' ] -dependencies = [] +dependencies = [ + "pydantic>=2.13.4", + "pyyaml>=6.0.3", + "typer>=0.27.1" +] description = "Configure a device in an air-gapped environment." license = {text = "Proprietary"} maintainers = [ @@ -25,11 +39,33 @@ maintainers = [ ] name = "id-configurator" readme = "README.md" -requires-python = ">=3.14" +requires-python = ">=3.13" version = "0.1.0" [project.scripts] id-configurator = "id_configurator:main" +[tool.coverage.paths] +source = [ + "id_configurator", + "*/site-packages/id_configurator" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:" +] +fail_under = 100 +show_missing = true + +[tool.coverage.run] +branch = true +source = ["id_configurator"] + +[tool.pytest.ini_options] +addopts = "--cov=id_configurator --cov-report=term-missing" +testpaths = ["tests"] + [tool.uv.build-backend] module-root = "" diff --git a/tests/test_apt.py b/tests/test_apt.py new file mode 100644 index 0000000..a00b6d4 --- /dev/null +++ b/tests/test_apt.py @@ -0,0 +1,124 @@ +import base64 + +from id_configurator.config import Configuration +from id_configurator.context import Context +from id_configurator.modules.apt import AptModule + +SOURCE_CONTENT = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "configurations": { + "apt": { + "sources": [ + { + "name": "interdiode", + "uris": ["http://example.invalid/debian"], + "suites": ["stable"], + "signed-by": "/etc/apt/keyrings/interdiode.asc", + } + ], + "keys": [ + { + "name": "interdiode", + "content": base64.b64encode(b"fake-key-bytes").decode("ascii"), + } + ], + } + }, +} + + +def make_config() -> Configuration: + return Configuration.model_validate(SOURCE_CONTENT) + + +def test_is_relevant_false_without_src(tmp_path) -> None: + context = Context(config=make_config(), dst=tmp_path / "dst", src=None) + assert AptModule().is_relevant(context) is False # nosec B101 + + +def test_is_relevant_false_without_apt_dir(tmp_path) -> None: + src = tmp_path / "src" + src.mkdir() + context = Context(config=make_config(), dst=tmp_path / "dst", src=src) + assert AptModule().is_relevant(context) is False # nosec B101 + + +def test_is_relevant_true_with_apt_dir(tmp_path) -> None: + src = tmp_path / "src" + (src / "etc/apt").mkdir(parents=True) + context = Context(config=make_config(), dst=tmp_path / "dst", src=src) + assert AptModule().is_relevant(context) is True # nosec B101 + + +def test_apply_disables_existing_sources_and_adds_configured_ones(tmp_path) -> None: + src = tmp_path / "src" + apt_dir = src / "etc/apt" + sources_list_d = apt_dir / "sources.list.d" + sources_list_d.mkdir(parents=True) + + (apt_dir / "sources.list").write_text( + "# already disabled\ndeb http://example.invalid/legacy stable main\n\n", + encoding="utf-8", + ) + (sources_list_d / "extra.list").write_text( + "deb http://example.invalid/extra stable main\n", encoding="utf-8" + ) + (sources_list_d / "docker.sources").write_text( + "Types: deb\nURIs: http://example.invalid/docker\nSuites: stable\nEnabled: yes\n" + "\n" + "Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\n" + "\n\n", + encoding="utf-8", + ) + (sources_list_d / "ignored.txt").write_text("not a source file\n", encoding="utf-8") + + dst = tmp_path / "dst" + context = Context(config=make_config(), dst=dst, src=src) + + AptModule().apply(context) + + disabled_top_level = (dst / "etc/apt/sources.list").read_text(encoding="utf-8") + assert ( + disabled_top_level + == "# already disabled\n# deb http://example.invalid/legacy stable main\n\n" + ) # nosec B101 + + disabled_list = (dst / "etc/apt/sources.list.d/extra.list").read_text( + encoding="utf-8" + ) # nosec B101 + assert disabled_list == "# deb http://example.invalid/extra stable main\n" # nosec B101 + + disabled_sources = (dst / "etc/apt/sources.list.d/docker.sources").read_text( + encoding="utf-8" + ) # nosec B101 + assert disabled_sources == ( # nosec B101 + "Types: deb\nURIs: http://example.invalid/docker\nSuites: stable\nEnabled: no" + "\n\n" + "Types: deb\nURIs: http://example.invalid/docker2\nSuites: stable\nEnabled: no\n" + ) + + assert not (dst / "etc/apt/sources.list.d/ignored.txt").exists() # nosec B101 + + keyring = (dst / "etc/apt/keyrings/interdiode.asc").read_bytes() + assert keyring == b"fake-key-bytes" # nosec B101 + + configured_source = (dst / "etc/apt/sources.list.d/interdiode.sources").read_text( + encoding="utf-8" + ) + assert configured_source == ( # nosec B101 + "Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\n" + "Signed-By: /etc/apt/keyrings/interdiode.asc\nEnabled: yes\n" + ) + + +def test_apply_without_existing_top_level_sources_list(tmp_path) -> None: + src = tmp_path / "src" + (src / "etc/apt").mkdir(parents=True) + dst = tmp_path / "dst" + context = Context(config=make_config(), dst=dst, src=src) + + AptModule().apply(context) + + assert not (dst / "etc/apt/sources.list").exists() # nosec B101 + assert (dst / "etc/apt/sources.list.d/interdiode.sources").exists() # nosec B101 diff --git a/tests/test_certificates.py b/tests/test_certificates.py new file mode 100644 index 0000000..be44158 --- /dev/null +++ b/tests/test_certificates.py @@ -0,0 +1,119 @@ +import base64 +import subprocess +from unittest.mock import MagicMock + +import pytest + +from id_configurator.config import Configuration +from id_configurator.context import Context +from id_configurator.modules.certificates import CertificatesModule + +CERT_CONTENT = base64.b64encode(b"fake-cert-bytes").decode("ascii") + + +def make_config(with_certificate: bool) -> Configuration: + data = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + } + if with_certificate: + data["configurations"] = { + "certificates": [{"name": "new-root-ca", "content": CERT_CONTENT}] + } + return Configuration.model_validate(data) + + +def test_is_relevant_true_when_certificates_configured(tmp_path) -> None: + context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) + assert CertificatesModule().is_relevant(context) is True # nosec B101 + + +def test_is_relevant_false_when_no_certificates(tmp_path) -> None: + context = Context(config=make_config(False), dst=tmp_path / "dst", src=None) + assert CertificatesModule().is_relevant(context) is False # nosec B101 + + +def _mock_success(monkeypatch, stderr: str = "") -> MagicMock: + mock_run = MagicMock( + return_value=subprocess.CompletedProcess( + args=[], returncode=0, stdout="", stderr=stderr + ) + ) + monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run) + return mock_run + + +def test_apply_writes_certificate_and_calls_update_ca_certificates( + tmp_path, monkeypatch +) -> None: + mock_run = _mock_success(monkeypatch) + dst = tmp_path / "dst" + context = Context(config=make_config(True), dst=dst, src=None) + + CertificatesModule().apply(context) + + written = dst / "usr/local/share/ca-certificates/new-root-ca.crt" + assert written.read_bytes() == b"fake-cert-bytes" # nosec B101 + mock_run.assert_called_once() + args = mock_run.call_args.args[0] + assert args[0] == "update-ca-certificates" # nosec B101 + assert "--certsconf" not in args # nosec B101 + assert "--certsdir" not in args # nosec B101 + + +def test_apply_preserves_existing_local_certificates_from_src( + tmp_path, monkeypatch +) -> None: + mock_run = _mock_success(monkeypatch, stderr="some warning") + src = tmp_path / "src" + local_certs_dir = src / "usr/local/share/ca-certificates" + local_certs_dir.mkdir(parents=True) + (local_certs_dir / "existing.crt").write_bytes(b"existing-bytes") + (local_certs_dir / "new-root-ca.crt").write_bytes( + b"stale-bytes-should-be-overwritten" + ) + (src / "etc/ca-certificates.conf").parent.mkdir(parents=True) + (src / "etc/ca-certificates.conf").write_text("existing.crt\n", encoding="utf-8") + (src / "usr/share/ca-certificates").mkdir(parents=True) + + dst = tmp_path / "dst" + context = Context(config=make_config(True), dst=dst, src=src) + + CertificatesModule().apply(context) + + local_dst = dst / "usr/local/share/ca-certificates" + assert (local_dst / "existing.crt").read_bytes() == b"existing-bytes" # nosec B101 + assert (local_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes" # nosec B101 + + args = mock_run.call_args.args[0] + assert "--certsconf" in args # nosec B101 + assert str(src / "etc/ca-certificates.conf") in args # nosec B101 + assert "--certsdir" in args # nosec B101 + assert str(src / "usr/share/ca-certificates") in args # nosec B101 + + +def test_update_ca_certificates_raises_when_binary_missing( + tmp_path, monkeypatch +) -> None: + monkeypatch.setattr( + "id_configurator.modules.certificates.subprocess.run", + MagicMock(side_effect=FileNotFoundError()), + ) + context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) + + with pytest.raises(RuntimeError, match="update-ca-certificates is not available"): + CertificatesModule().apply(context) + + +def test_update_ca_certificates_raises_on_failure(tmp_path, monkeypatch) -> None: + error = subprocess.CalledProcessError( + returncode=1, cmd=["update-ca-certificates"], stderr="boom" + ) + monkeypatch.setattr( + "id_configurator.modules.certificates.subprocess.run", + MagicMock(side_effect=error), + ) + context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) + + with pytest.raises(RuntimeError, match="update-ca-certificates failed"): + CertificatesModule().apply(context) # nosec B101 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..69fac16 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,93 @@ +import base64 +import json +import subprocess +from unittest.mock import MagicMock + +from typer.testing import CliRunner + +from id_configurator import cli + +runner = CliRunner() + +MINIMAL_CONFIG = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", +} +CONFIG_WITH_CERT = { + **MINIMAL_CONFIG, + "configurations": { + "certificates": [ + {"name": "my-ca", "content": base64.b64encode(b"fake").decode("ascii")} + ] + }, +} + + +def write_config(tmp_path, data) -> str: + path = tmp_path / "config.json" + path.write_text(json.dumps(data), encoding="utf-8") + return str(path) + + +def test_validate_valid_config(tmp_path) -> None: + config_path = write_config(tmp_path, MINIMAL_CONFIG) + result = runner.invoke(cli.app, ["validate", "--config", config_path]) + assert result.exit_code == 0 # nosec B101 + assert "is valid" in result.stdout # nosec B101 + + +def test_validate_without_config_fails() -> None: + result = runner.invoke(cli.app, ["validate"]) + assert result.exit_code != 0 # nosec B101 + assert "either --config or --config-env" in result.output # nosec B101 + + +def test_list_modules() -> None: + result = runner.invoke(cli.app, ["list-modules"]) + assert result.exit_code == 0 # nosec B101 + assert set(result.stdout.splitlines()) == {"apt", "certificates"} # nosec B101 + + +def test_apply_skips_irrelevant_modules(tmp_path, caplog) -> None: + config_path = write_config(tmp_path, MINIMAL_CONFIG) + dst = tmp_path / "dst" + with caplog.at_level("DEBUG", logger="id_configurator"): + result = runner.invoke( + cli.app, ["apply", "--config", config_path, "--dst", str(dst), "-v"] + ) + assert result.exit_code == 0 # nosec B101 + assert "skipping module apt" in caplog.text # nosec B101 + assert "skipping module certificates" in caplog.text # nosec B101 + + +def test_apply_runs_relevant_module(tmp_path, monkeypatch) -> None: + mock_run = MagicMock( + return_value=subprocess.CompletedProcess( + args=[], returncode=0, stdout="", stderr="" + ) + ) + monkeypatch.setattr("id_configurator.modules.certificates.subprocess.run", mock_run) + + config_path = write_config(tmp_path, CONFIG_WITH_CERT) + dst = tmp_path / "dst" + result = runner.invoke( + cli.app, ["apply", "--config", config_path, "--dst", str(dst)] + ) + + assert result.exit_code == 0 # nosec B101 + assert (dst / "usr/local/share/ca-certificates/my-ca.crt").read_bytes() == b"fake" # nosec B101 + mock_run.assert_called_once() + + +def test_main_default_name_calls_app(monkeypatch) -> None: + mock_app = MagicMock() + monkeypatch.setattr(cli, "app", mock_app) + cli.main() + mock_app.assert_called_once() + + +def test_main_other_name_does_not_call_app(monkeypatch) -> None: + mock_app = MagicMock() + monkeypatch.setattr(cli, "app", mock_app) + cli.main("id_configurator.__main__") + mock_app.assert_not_called() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..2974816 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,163 @@ +import base64 +import json + +import pytest + +from id_configurator.config import ( + AptSource, + Configuration, + load_configuration, + parse_configuration, +) + +FULL_CONFIG = { + "uuid": "12345678-1234-5678-1234-567812345678", + "organisation": "InterDiode", + "description": "a test configuration", + "configurations": { + "certificates": [{"name": "my-ca", "content": "ZmFrZQ=="}], + "apt": { + "sources": [ + { + "name": "interdiode", + "types": ["deb"], + "uris": ["http://example.invalid/debian"], + "suites": ["stable"], + "components": ["main", "contrib"], + "signed-by": "/etc/apt/keyrings/interdiode.asc", + } + ], + "keys": [{"name": "interdiode", "content": "ZmFrZQ=="}], + }, + "yum": { + "repositories": [ + { + "name": "interdiode", + "index-url": "http://example.invalid/yum", + "gpg-url": "http://example.invalid/gpg", + } + ] + }, + "python": { + "repositories": [ + {"name": "interdiode", "index-url": "http://example.invalid/simple"} + ] + }, + }, +} + + +def test_parse_configuration_from_json() -> None: + configuration = parse_configuration(json.dumps(FULL_CONFIG)) + assert isinstance(configuration, Configuration) # nosec B101 + assert configuration.organisation == "InterDiode" # nosec B101 + assert configuration.configurations.certificates[0].name == "my-ca" # nosec B101 + assert ( + configuration.configurations.yum.repositories[0].index_url + == "http://example.invalid/yum" + ) # nosec B101 + assert ( + configuration.configurations.yum.repositories[0].gpg_url + == "http://example.invalid/gpg" + ) # nosec B101 + assert ( + configuration.configurations.python.repositories[0].index_url + == "http://example.invalid/simple" + ) # nosec B101 + assert ( + configuration.configurations.apt.sources[0].signed_by + == "/etc/apt/keyrings/interdiode.asc" + ) # nosec B101 + assert configuration.configurations.apt.keys[0].name == "interdiode" # nosec B101 + + +def test_parse_configuration_from_yaml() -> None: + raw = """ + uuid: 12345678-1234-5678-1234-567812345678 + organisation: InterDiode + """ + configuration = parse_configuration(raw) + assert configuration.organisation == "InterDiode" # nosec B101 + assert configuration.description == "" # nosec B101 + assert configuration.configurations.certificates == [] # nosec B101 + + +def test_load_configuration_from_file(tmp_path) -> None: + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps(FULL_CONFIG), encoding="utf-8") + configuration = load_configuration(config_file, None) + assert configuration.organisation == "InterDiode" # nosec B101 + + +def test_load_configuration_from_env() -> None: + encoded = base64.b64encode(json.dumps(FULL_CONFIG).encode("utf-8")).decode("ascii") + configuration = load_configuration(None, encoded) + assert configuration.organisation == "InterDiode" # nosec B101 + + +def test_load_configuration_without_source_raises() -> None: + with pytest.raises(ValueError, match="either --config or --config-env"): + load_configuration(None, None) + + +def test_apt_source_to_deb822_minimal() -> None: + source = AptSource( + name="interdiode", uris=["http://example.invalid/debian"], suites=["stable"] + ) + assert ( + source.to_deb822() + == "Types: deb\nURIs: http://example.invalid/debian\nSuites: stable\nEnabled: yes\n" + ) # nosec B101 + + +def test_apt_source_to_deb822_full() -> None: + source = AptSource( + name="interdiode", + types=["deb", "deb-src"], + uris=["http://example.invalid/debian"], + suites=["stable"], + components=["main", "contrib"], + architectures=["amd64"], + languages=["en"], + targets=["deb"], + enabled=False, + trusted=True, + **{ + "signed-by": "/etc/apt/keyrings/interdiode.asc", + "by-hash": True, + "allow-insecure": False, + "allow-weak": False, + "allow-downgrade-to-insecure": False, + "check-valid-until": True, + "valid-until-min": "10", + "valid-until-max": "1w", + "check-date": True, + "date-max-future": "1d", + }, + pdiffs=True, + description="InterDiode APT repository", + ) + rendered = source.to_deb822() + assert rendered.splitlines() == [ # nosec B101 + "Types: deb deb-src", + "URIs: http://example.invalid/debian", + "Suites: stable", + "Components: main contrib", + "Architectures: amd64", + "Languages: en", + "Targets: deb", + "Signed-By: /etc/apt/keyrings/interdiode.asc", + "Trusted: yes", + "PDiffs: yes", + "By-Hash: yes", + "Allow-Insecure: no", + "Allow-Weak: no", + "Allow-Downgrade-To-Insecure: no", + "Check-Valid-Until: yes", + "Valid-Until-Min: 10", + "Valid-Until-Max: 1w", + "Check-Date: yes", + "Date-Max-Future: 1d", + "Description: InterDiode APT repository", + "Enabled: no", + ] diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..a3b6fd1 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,85 @@ +import pytest + +from id_configurator.config import Configuration +from id_configurator.context import Context + +CONFIG = Configuration.model_validate( + {"uuid": "12345678-1234-5678-1234-567812345678", "organisation": "InterDiode"} +) + + +def make_context(tmp_path, with_src: bool = True) -> Context: + dst = tmp_path / "dst" + dst.mkdir() + src = None + if with_src: + src = tmp_path / "src" + src.mkdir() + return Context(config=CONFIG, dst=dst, src=src) + + +def test_dst_path_within_destination(tmp_path) -> None: + context = make_context(tmp_path) + resolved = context.dst_path("etc/foo.conf") + assert resolved == context.dst / "etc" / "foo.conf" # nosec B101 + + +def test_dst_path_rejects_escape(tmp_path) -> None: + context = make_context(tmp_path) + with pytest.raises(ValueError, match="refusing to write outside"): + context.dst_path("../escape.conf") + + +def test_src_path_without_src(tmp_path) -> None: + context = make_context(tmp_path, with_src=False) + assert context.src_path("etc/foo.conf") is None # nosec B101 + + +def test_src_path_with_src(tmp_path) -> None: + context = make_context(tmp_path) + resolved = context.src_path("etc/foo.conf") + assert resolved == context.src / "etc" / "foo.conf" # nosec B101 + + +def test_write_file_creates_parents_and_content(tmp_path) -> None: + context = make_context(tmp_path) + path = context.write_file("etc/nested/foo.conf", b"hello") + assert path.read_bytes() == b"hello" # nosec B101 + assert path.stat().st_mode & 0o777 == 0o644 # nosec B101 + + +def test_read_src_file_without_src(tmp_path) -> None: + context = make_context(tmp_path, with_src=False) + assert context.read_src_file("etc/foo.conf") is None # nosec B101 + + +def test_read_src_file_missing(tmp_path) -> None: + context = make_context(tmp_path) + assert context.read_src_file("etc/missing.conf") is None # nosec B101 + + +def test_read_src_file_present(tmp_path) -> None: + context = make_context(tmp_path) + (context.src / "foo.conf").write_bytes(b"existing") + assert context.read_src_file("foo.conf") == b"existing" # nosec B101 + + +def test_iter_src_dir_without_src(tmp_path) -> None: + context = make_context(tmp_path, with_src=False) + assert context.iter_src_dir("some/dir") == [] # nosec B101 + + +def test_iter_src_dir_missing_dir(tmp_path) -> None: + context = make_context(tmp_path) + assert context.iter_src_dir("missing-dir") == [] # nosec B101 + + +def test_iter_src_dir_lists_only_files(tmp_path) -> None: + context = make_context(tmp_path) + target = context.src / "certs" + target.mkdir() + (target / "a.crt").write_bytes(b"a") + (target / "b.crt").write_bytes(b"b") + (target / "subdir").mkdir() + result = context.iter_src_dir("certs") + assert [entry.name for entry in result] == ["a.crt", "b.crt"] # nosec B101 diff --git a/tests/test_modules_base.py b/tests/test_modules_base.py new file mode 100644 index 0000000..1bdaa01 --- /dev/null +++ b/tests/test_modules_base.py @@ -0,0 +1,19 @@ +from id_configurator.modules import base as base_module +from id_configurator.modules.base import Module, all_modules, register + + +def test_register_and_all_modules_are_isolated(monkeypatch) -> None: + monkeypatch.setattr(base_module, "_REGISTRY", []) + + class DummyModule(Module): + name = "dummy" + + def is_relevant(self, context) -> bool: + return True + + def apply(self, context) -> None: + pass + + register(DummyModule) + + assert all_modules() == [DummyModule] # nosec B101 diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..5ce03f5 --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,20 @@ +import importlib +import sys +from unittest.mock import MagicMock + + +def test_package_exposes_app_and_main() -> None: + import id_configurator + + assert id_configurator.main is id_configurator.cli.main # nosec B101 + assert id_configurator.app is id_configurator.cli.app # nosec B101 + + +def test_dunder_main_module_does_not_launch_app_when_imported(monkeypatch) -> None: + mock_app = MagicMock() + monkeypatch.setattr("id_configurator.cli.app", mock_app) + sys.modules.pop("id_configurator.__main__", None) + + importlib.import_module("id_configurator.__main__") + + mock_app.assert_not_called() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4a1e216 --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +env_list = py313,py314,coverage +isolated_build = true +requires = + tox>=4 + tox-uv>=1 + +[testenv] +package = wheel +dependency_groups = test +change_dir = {env_tmp_dir} +set_env = + COVERAGE_FILE = {tox_root}/.coverage.{env_name} +commands = + pytest {tox_root}/tests --cov-config={tox_root}/pyproject.toml --cov-fail-under=0 {posargs} + +[testenv:coverage] +package = skip +depends = py313,py314 +change_dir = {tox_root} +dependency_groups = test +set_env = + COVERAGE_FILE = {tox_root}/.coverage +commands = + coverage combine + coverage report diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..653f6c8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,559 @@ +requires-python = ">=3.13" +revision = 3 +version = 1 + +[[package]] +name = "annotated-doc" +sdist = {url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.0.5" +wheels = [ + {url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z"} +] + +[[package]] +name = "annotated-types" +sdist = {url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.8.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z"} +] + +[[package]] +name = "cachetools" +sdist = {url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z"} +source = {registry = "https://pypi.org/simple"} +version = "7.1.7" +wheels = [ + {url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z"} +] + +[[package]] +name = "colorama" +sdist = {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.4.6" +wheels = [ + {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z"} +] + +[[package]] +name = "coverage" +sdist = {url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z"} +source = {registry = "https://pypi.org/simple"} +version = "7.15.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z"}, + {url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z"}, + {url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z"}, + {url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z"}, + {url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z"}, + {url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z"}, + {url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z"}, + {url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z"}, + {url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z"}, + {url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z"}, + {url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z"}, + {url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z"}, + {url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z"}, + {url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z"}, + {url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z"}, + {url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z"}, + {url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z"}, + {url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z"}, + {url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z"}, + {url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z"}, + {url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z"}, + {url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z"}, + {url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z"}, + {url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z"}, + {url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z"}, + {url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z"}, + {url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z"}, + {url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z"}, + {url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z"}, + {url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z"}, + {url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z"}, + {url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z"}, + {url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z"}, + {url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z"}, + {url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z"}, + {url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z"}, + {url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z"}, + {url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z"}, + {url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z"}, + {url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z"}, + {url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z"}, + {url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z"}, + {url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z"}, + {url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z"}, + {url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z"}, + {url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z"}, + {url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z"}, + {url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z"}, + {url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z"}, + {url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z"}, + {url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z"}, + {url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z"}, + {url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z"}, + {url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z"}, + {url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z"}, + {url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z"}, + {url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z"}, + {url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z"}, + {url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z"}, + {url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z"}, + {url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z"}, + {url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z"}, + {url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z"}, + {url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z"}, + {url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z"}, + {url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z"}, + {url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z"}, + {url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z"}, + {url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z"}, + {url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z"}, + {url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z"}, + {url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z"}, + {url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z"}, + {url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z"}, + {url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z"}, + {url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z"} +] + +[[package]] +name = "distlib" +sdist = {url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.4.3" +wheels = [ + {url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z"} +] + +[[package]] +name = "filelock" +sdist = {url = "https://files.pythonhosted.org/packages/6d/30/03b03951873a1a0ffc7e8ca0e10c15597b59e8d0e39260704cd2ea087bc4/filelock-3.32.4.tar.gz", hash = "sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30", size = 222126, upload-time = "2026-08-23T17:37:55.363Z"} +source = {registry = "https://pypi.org/simple"} +version = "3.32.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/01/a4/9b63d595d748e3aff8812b65eacc1a2c4bd90b7c2012e08e72373b4835eb/filelock-3.32.4-py3-none-any.whl", hash = "sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd", size = 99864, upload-time = "2026-08-23T17:37:53.913Z"} +] + +[[package]] +dependencies = [ + {name = "pydantic"}, + {name = "pyyaml"}, + {name = "typer"} +] +name = "id-configurator" +source = {editable = "."} +version = "0.1.0" + +[package.dev-dependencies] +dev = [ + {name = "tox"}, + {name = "tox-uv"} +] +test = [ + {name = "pytest"}, + {name = "pytest-cov"} +] + +[package.metadata] +requires-dist = [ + {name = "pydantic", specifier = ">=2.13.4"}, + {name = "pyyaml", specifier = ">=6.0.3"}, + {name = "typer", specifier = ">=0.27.1"} +] + +[package.metadata.requires-dev] +dev = [ + {name = "tox", specifier = ">=4.60.1"}, + {name = "tox-uv", specifier = ">=1.36.0"} +] +test = [ + {name = "pytest", specifier = ">=9.1.1"}, + {name = "pytest-cov", specifier = ">=7.1.0"} +] + +[[package]] +name = "iniconfig" +sdist = {url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z"} +source = {registry = "https://pypi.org/simple"} +version = "2.3.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z"} +] + +[[package]] +dependencies = [ + {name = "mdurl"} +] +name = "markdown-it-py" +sdist = {url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.2.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z"} +] + +[[package]] +name = "mdurl" +sdist = {url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.1.2" +wheels = [ + {url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z"} +] + +[[package]] +name = "packaging" +sdist = {url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z"} +source = {registry = "https://pypi.org/simple"} +version = "26.3" +wheels = [ + {url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z"} +] + +[[package]] +name = "platformdirs" +sdist = {url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.11.5" +wheels = [ + {url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z"} +] + +[[package]] +name = "pluggy" +sdist = {url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.6.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z"} +] + +[[package]] +dependencies = [ + {name = "annotated-types"}, + {name = "pydantic-core"}, + {name = "typing-extensions"}, + {name = "typing-inspection"} +] +name = "pydantic" +sdist = {url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z"} +source = {registry = "https://pypi.org/simple"} +version = "2.13.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z"} +] + +[[package]] +dependencies = [ + {name = "typing-extensions"} +] +name = "pydantic-core" +sdist = {url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z"} +source = {registry = "https://pypi.org/simple"} +version = "2.46.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z"}, + {url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z"}, + {url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z"}, + {url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z"}, + {url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z"}, + {url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z"}, + {url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z"}, + {url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z"}, + {url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z"}, + {url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z"}, + {url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z"}, + {url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z"}, + {url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z"}, + {url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z"}, + {url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z"}, + {url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z"}, + {url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z"}, + {url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z"}, + {url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z"}, + {url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z"}, + {url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z"}, + {url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z"}, + {url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z"}, + {url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z"}, + {url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z"}, + {url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z"}, + {url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z"}, + {url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z"}, + {url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z"}, + {url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z"}, + {url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z"}, + {url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z"}, + {url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z"}, + {url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z"}, + {url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z"}, + {url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z"}, + {url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z"}, + {url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z"}, + {url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z"}, + {url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z"}, + {url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z"}, + {url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z"}, + {url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z"}, + {url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z"}, + {url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z"} +] + +[[package]] +name = "pygments" +sdist = {url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z"} +source = {registry = "https://pypi.org/simple"} +version = "2.21.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z"} +] + +[[package]] +dependencies = [ + {name = "packaging"} +] +name = "pyproject-api" +sdist = {url = "https://files.pythonhosted.org/packages/ab/bd/2f985c12bf33fdd8637e3a8f9418d6806f177601dee7c4924d0b2bb28650/pyproject_api-1.11.0.tar.gz", hash = "sha256:b8807d85a293e6c9f133e6575946fed45f1d42b22d58c780b33aa2421a799549", size = 23787, upload-time = "2026-07-21T13:09:33.744Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.11.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/a3/6a/2822ee12bafe87af8f6cc620f7d1f126e77f82ce56ba888b94a9af5a979c/pyproject_api-1.11.0-py3-none-any.whl", hash = "sha256:860060c8832dce983b5eec6f41c4c43eb3ec06ff7332387a63acdf5ca27b68d8", size = 13275, upload-time = "2026-07-21T13:09:32.559Z"} +] + +[[package]] +dependencies = [ + {name = "colorama", marker = "sys_platform == 'win32'"}, + {name = "iniconfig"}, + {name = "packaging"}, + {name = "pluggy"}, + {name = "pygments"} +] +name = "pytest" +sdist = {url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z"} +source = {registry = "https://pypi.org/simple"} +version = "9.1.1" +wheels = [ + {url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z"} +] + +[[package]] +dependencies = [ + {name = "coverage"}, + {name = "pluggy"}, + {name = "pytest"} +] +name = "pytest-cov" +sdist = {url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z"} +source = {registry = "https://pypi.org/simple"} +version = "7.1.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z"} +] + +[[package]] +dependencies = [ + {name = "filelock"} +] +name = "python-discovery" +sdist = {url = "https://files.pythonhosted.org/packages/b2/8f/3c92c45737f654f2488ab3662b7604a55d3d35146d37c9ce80f5c95b95a6/python_discovery-1.5.3.tar.gz", hash = "sha256:e500eb24025fb7c4876c1fdcfbafd9028a10c71b661aee38cb6fb0de594518c1", size = 82477, upload-time = "2026-08-24T14:48:46.396Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.5.3" +wheels = [ + {url = "https://files.pythonhosted.org/packages/30/12/823d9a321904ccfd2969a24b84fdfd1e6614c707ec569c62879bf1dbc6c5/python_discovery-1.5.3-py3-none-any.whl", hash = "sha256:8305296358f1aa2ed302a25b84be7df84fef8ca47c7dce2da63cb7325333044e", size = 38290, upload-time = "2026-08-24T14:48:45.305Z"} +] + +[[package]] +name = "pyyaml" +sdist = {url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z"} +source = {registry = "https://pypi.org/simple"} +version = "6.0.3" +wheels = [ + {url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z"}, + {url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z"}, + {url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z"}, + {url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z"}, + {url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z"}, + {url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z"}, + {url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z"}, + {url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z"}, + {url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z"}, + {url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z"}, + {url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z"}, + {url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z"}, + {url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z"}, + {url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z"}, + {url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z"}, + {url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z"}, + {url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z"}, + {url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z"}, + {url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z"}, + {url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z"}, + {url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z"}, + {url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z"}, + {url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z"}, + {url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z"}, + {url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z"}, + {url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z"}, + {url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z"}, + {url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z"} +] + +[[package]] +dependencies = [ + {name = "markdown-it-py"}, + {name = "pygments"} +] +name = "rich" +sdist = {url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z"} +source = {registry = "https://pypi.org/simple"} +version = "15.0.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z"} +] + +[[package]] +name = "shellingham" +sdist = {url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.5.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z"} +] + +[[package]] +name = "tomli-w" +sdist = {url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.2.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z"} +] + +[[package]] +dependencies = [ + {name = "cachetools"}, + {name = "colorama"}, + {name = "filelock"}, + {name = "packaging"}, + {name = "platformdirs"}, + {name = "pluggy"}, + {name = "pyproject-api"}, + {name = "python-discovery"}, + {name = "tomli-w"}, + {name = "virtualenv"} +] +name = "tox" +sdist = {url = "https://files.pythonhosted.org/packages/ec/ae/05210c695efa7349af65aeb988604b15520eade3b6e2d43bdf44ce69b4b3/tox-4.60.1.tar.gz", hash = "sha256:2611b77ebe06fa571ae2f4176eaf644115d08891befb9c7cb705cb038d5ab38e", size = 303833, upload-time = "2026-08-25T05:39:00.934Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.60.1" +wheels = [ + {url = "https://files.pythonhosted.org/packages/83/d8/5ca808e8b2d98ac55e9141309c63b273caec25eb5bdaf3d7bf6d809903bc/tox-4.60.1-py3-none-any.whl", hash = "sha256:6f23a6417ad761b17c047e14b6e278968c09eb9b1fc00025269f82ec8327bc75", size = 226880, upload-time = "2026-08-25T05:38:59.194Z"} +] + +[[package]] +dependencies = [ + {name = "tox-uv-bare"}, + {name = "uv"} +] +name = "tox-uv" +source = {registry = "https://pypi.org/simple"} +version = "1.36.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/f9/d7/3fce976b9e295218a2d7e541e0f1e9b1259cb893de7a8d277952a66a798d/tox_uv-1.36.0-py3-none-any.whl", hash = "sha256:5f81b39be3fe4e14c6b9bb7ba637ed97d34efa6214efd5f525d95ee9559a99ac", size = 6564, upload-time = "2026-07-21T13:09:54.316Z"} +] + +[[package]] +dependencies = [ + {name = "packaging"}, + {name = "tox"} +] +name = "tox-uv-bare" +sdist = {url = "https://files.pythonhosted.org/packages/56/df/9f90a59de8c87cece6e691eee53dc1cb0a824d73aea8f380ae2f6ecb71de/tox_uv_bare-1.36.0.tar.gz", hash = "sha256:d9b0a2fd0f74fa65d9597108f8a0ef7abb08651c9196a998a6073b781b45cfd0", size = 32548, upload-time = "2026-07-21T13:09:56.435Z"} +source = {registry = "https://pypi.org/simple"} +version = "1.36.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/f7/0a/6dc462e4fb543305283a6157c80f43e3d12ca4702da6ae6521d541c6b55c/tox_uv_bare-1.36.0-py3-none-any.whl", hash = "sha256:ba397dd0396df95a75744d4e42a50ee27207c0ffcf277b62ffba9c3de455a939", size = 22489, upload-time = "2026-07-21T13:09:55.389Z"} +] + +[[package]] +dependencies = [ + {name = "annotated-doc"}, + {name = "colorama", marker = "sys_platform == 'win32'"}, + {name = "rich"}, + {name = "shellingham"} +] +name = "typer" +sdist = {url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.27.1" +wheels = [ + {url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z"} +] + +[[package]] +name = "typing-extensions" +sdist = {url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z"} +source = {registry = "https://pypi.org/simple"} +version = "4.16.0" +wheels = [ + {url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z"} +] + +[[package]] +dependencies = [ + {name = "typing-extensions"} +] +name = "typing-inspection" +sdist = {url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.4.4" +wheels = [ + {url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z"} +] + +[[package]] +name = "uv" +sdist = {url = "https://files.pythonhosted.org/packages/f7/ab/790e25d2c685f5a41b9567001a43fbe049a595c1b08b8a93bf82d5fc704b/uv-0.12.7.tar.gz", hash = "sha256:4b320f84763a80308fd830ecf5c4c44505a8ed910fe265c5977d0a3727cfcd55", size = 7107999, upload-time = "2026-08-27T22:13:22.978Z"} +source = {registry = "https://pypi.org/simple"} +version = "0.12.7" +wheels = [ + {url = "https://files.pythonhosted.org/packages/91/63/cc236f80a868b28525e0f7aa61f7ee998a0540ccba8cc4d01b2d42fc7838/uv-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:d568fd3448c24354753fd8333c978aeeeb6b51db4b100e234461a7eb50882fae", size = 22215689, upload-time = "2026-08-27T22:12:36.485Z"}, + {url = "https://files.pythonhosted.org/packages/ba/c5/40f4a5736d2f795688977b3b8bf841daca1e9c93b358778b7ed3dcd747fe/uv-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec5b437aa60e8c94da263ad709d0bf6c8f268ac81f305d89c6115badd7d1cbe7", size = 20471214, upload-time = "2026-08-27T22:12:39.147Z"}, + {url = "https://files.pythonhosted.org/packages/e5/3e/5946d33de437467190b51d7d9f9e74a53628782565d50b64f6051050f482/uv-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:016fe4b9a2e0d2a35b17b6c3efbb45b929189c5b4b37aa921265265ccfe42cc1", size = 17317478, upload-time = "2026-08-27T22:12:41.58Z"}, + {url = "https://files.pythonhosted.org/packages/2e/5b/0e42dd88d9928b51a4e85d89d8aed46933951187b508ae68c145b06e3334/uv-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fe9a871bd638ee6d2fd73bf40c2ee98153e44d06f796a03fcecf9d12b36d42d8", size = 21379352, upload-time = "2026-08-27T22:12:44.123Z"}, + {url = "https://files.pythonhosted.org/packages/8f/5c/b6417812ef87fc713ec368b1001b41c78eaedbfa521789db7d103b56c500/uv-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1014a13854c45eb1daa9a32602e0f4d07f3edd298826d3e8d22740eed60a7c95", size = 21686985, upload-time = "2026-08-27T22:12:47.686Z"}, + {url = "https://files.pythonhosted.org/packages/69/23/a6284df1dc4233fea564227f77c402969b4f6aebcb647826ce9e82a413de/uv-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ad3e91cc911596bb54197057853b64b36a066462d8f2fc4d4f60b61b707ffa8", size = 21744177, upload-time = "2026-08-27T22:12:50.572Z"}, + {url = "https://files.pythonhosted.org/packages/fb/e1/96526f3ea72667352e1a83dff3cbb8701dbfc830aafb3ac9dd532d4fb827/uv-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6d4bd67b488ef2766cfa885947c1093c18caf5d665ba9156963ad0241f196e9", size = 22527940, upload-time = "2026-08-27T22:12:53.479Z"}, + {url = "https://files.pythonhosted.org/packages/39/7b/816e7c4ef1b59faf295d4eec133b15d6d3a227dc4bc341dceccc31a3ba15/uv-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:debeccc5eca0063cd922bc67caa4a8c0df5f69090179866ed17fa7264905bda2", size = 23630281, upload-time = "2026-08-27T22:12:55.941Z"}, + {url = "https://files.pythonhosted.org/packages/ac/63/03d00b74c5582163b8c544b2dc39396388d904a13118303fb2c32ece2811/uv-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36c8f93d182b766b9ed4a9c1da5ec0f7dc9f934887df3404d996f66321fd18d5", size = 23345465, upload-time = "2026-08-27T22:12:58.726Z"}, + {url = "https://files.pythonhosted.org/packages/8a/01/616cc5f80952eef6fa48c5dbd6ea8aeed04a494e7dfbe9f5f65cfcf85ad9/uv-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4545e87c7ac64af317d8daffd279e23e93b0e05035662363033d3525923339d2", size = 20058968, upload-time = "2026-08-27T22:13:01.239Z"}, + {url = "https://files.pythonhosted.org/packages/66/23/051acc24b21bbf0e67bc00e2b4239892e37cf39fdd6428fc246a2ae62b79/uv-0.12.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:56a5730f8eff477501b3276a0059c2c2843302d5d4a6cc10f993a5cd66ddace8", size = 19450815, upload-time = "2026-08-27T22:13:03.636Z"}, + {url = "https://files.pythonhosted.org/packages/42/a4/a84adae83d6891401284ec4929ea7892c7efc5484cb53283147fbb4881b6/uv-0.12.7-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d83419298e202f56e381cef6406b519b9336e58fc90a559617d86576a3d8a4e8", size = 22402981, upload-time = "2026-08-27T22:13:06.247Z"}, + {url = "https://files.pythonhosted.org/packages/8a/35/6fcdff1292ba1cc027d1d741531dfc4d19f92e1312262833beacf3b7fbe8/uv-0.12.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fc57436f2f012b885454f465dbf077f573745f0d4275a6a38194203b67e94ea4", size = 22535567, upload-time = "2026-08-27T22:13:08.802Z"}, + {url = "https://files.pythonhosted.org/packages/2b/ea/7ac04384642a4ba808b20a94cb09e0f4b4a6aef6cf9671858be0567c153a/uv-0.12.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b2bd0f25f17f0000a2415347471e713cd1597f4525cd3412d17875b131f4b1ac", size = 21667314, upload-time = "2026-08-27T22:13:11.197Z"}, + {url = "https://files.pythonhosted.org/packages/e1/c8/348089faf45a3c24c1d5a5ee3957cd1c7fb4a6aef1d9646114a4b7cc9c7a/uv-0.12.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ff33305718665c6fba25efdd260c67a6bd500c665e3d5d61059612791ca10c90", size = 22919105, upload-time = "2026-08-27T22:13:13.596Z"}, + {url = "https://files.pythonhosted.org/packages/3e/7f/5fb8ed68270666ed675daead854bde3b14d7ab42c2b7c0d234b5ea8ea928/uv-0.12.7-py3-none-win32.whl", hash = "sha256:3ac3321ccd6097dbef154d27044e0762a67b2f6eb017dcc65be6574f4671fb0d", size = 19880506, upload-time = "2026-08-27T22:13:16.072Z"}, + {url = "https://files.pythonhosted.org/packages/e7/97/40a91354862028e8f8e547ac27c6acd7b7cc67b9003acc28596d9e90cd6b/uv-0.12.7-py3-none-win_amd64.whl", hash = "sha256:277d326d7e63b912f3425c6e6d7d5d49f21b43d080d21859ff3c6819353f1847", size = 18085016, upload-time = "2026-08-27T22:13:18.352Z"}, + {url = "https://files.pythonhosted.org/packages/52/69/6cdd022fff6a8d8f5c9af2686f61f2605ddd03b5709efb73df6e08f40249/uv-0.12.7-py3-none-win_arm64.whl", hash = "sha256:95c3a4fa65e72bab3ca1b4c8ce18fbe784cf3137e9f9234588b69d09b341a4a7", size = 19522843, upload-time = "2026-08-27T22:13:20.633Z"} +] + +[[package]] +dependencies = [ + {name = "distlib"}, + {name = "filelock"}, + {name = "platformdirs"}, + {name = "python-discovery"} +] +name = "virtualenv" +sdist = {url = "https://files.pythonhosted.org/packages/1d/60/fc54e876e34f94dd0cf0185aaecfd4bfa906653f003d9b2fb21428642fca/virtualenv-21.7.5.tar.gz", hash = "sha256:a73c4246fba3c8901ff9717399f466e00eeca5a3834981f1a6ebb4f1e94de2f8", size = 5346743, upload-time = "2026-08-25T05:39:16.14Z"} +source = {registry = "https://pypi.org/simple"} +version = "21.7.5" +wheels = [ + {url = "https://files.pythonhosted.org/packages/ca/d8/401141bf45637be916c86d325bd821c5838c7eff83294b934cd94e774e4f/virtualenv-21.7.5-py3-none-any.whl", hash = "sha256:e36ca889510ab6cb0b1dca93c59e5431dd4422a3c88f487358d470c90af8c07a", size = 5324697, upload-time = "2026-08-25T05:39:14.229Z"} +]