mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
feat: initial commit
This commit is contained in:
@@ -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 `<dst>/etc/ssl/certs` and update the system's CA certificates.
|
||||||
|
- the configuration specifies a apt source to add and `<src>` contains an existing `/etc/apt/sources.list`, the tool will merge the new source into the existing sources and write the result to `<dst>/etc/apt/sources.list`.
|
||||||
|
|
||||||
|
The configuration is expected to be a JSON or YAML object with the following structure:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uuid: <UUID>
|
||||||
|
organisation: <string>
|
||||||
|
description: <string>
|
||||||
|
configurations:
|
||||||
|
certificates:
|
||||||
|
- name: <string>
|
||||||
|
content: <base64-encoded PEM>
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- name: <string>
|
||||||
|
content: <string>
|
||||||
|
keys:
|
||||||
|
- name: <string>
|
||||||
|
content: <base64-encoded GPG key>
|
||||||
|
yum:
|
||||||
|
repositories:
|
||||||
|
- name: <string>
|
||||||
|
index-url: <string>
|
||||||
|
gpg-url: <string>
|
||||||
|
python:
|
||||||
|
repositories:
|
||||||
|
- name: <string>
|
||||||
|
index-url: <string>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 `<src>`, and write modified files to `<dst>`.
|
||||||
|
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 `<dst>`.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from id_configurator.cli import app, main
|
||||||
|
|
||||||
|
__all__ = ["app", "main"]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from id_configurator.cli import main
|
||||||
|
|
||||||
|
main(__name__)
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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())
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Detect existing APT sources and configure new ones, in the DEB822 format.
|
||||||
|
|
||||||
|
If `<src>/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 `<dst>/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)
|
||||||
@@ -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)
|
||||||
@@ -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())
|
||||||
+38
-2
@@ -2,6 +2,16 @@
|
|||||||
build-backend = "uv_build"
|
build-backend = "uv_build"
|
||||||
requires = ["uv_build>=0.12.3,<0.13.0"]
|
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]
|
[project]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "InterDiode", email = "contact@mail.interdiode.fr"}
|
{name = "InterDiode", email = "contact@mail.interdiode.fr"}
|
||||||
@@ -17,7 +27,11 @@ classifiers = [
|
|||||||
'Programming Language :: Python :: 3.13',
|
'Programming Language :: Python :: 3.13',
|
||||||
'Programming Language :: Python :: 3.14'
|
'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."
|
description = "Configure a device in an air-gapped environment."
|
||||||
license = {text = "Proprietary"}
|
license = {text = "Proprietary"}
|
||||||
maintainers = [
|
maintainers = [
|
||||||
@@ -25,11 +39,33 @@ maintainers = [
|
|||||||
]
|
]
|
||||||
name = "id-configurator"
|
name = "id-configurator"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.13"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
id-configurator = "id_configurator:main"
|
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]
|
[tool.uv.build-backend]
|
||||||
module-root = ""
|
module-root = ""
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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"}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user