diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c349249 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +.claude +.venv +__pycache__ +*.pyc +.pytest_cache +.mypy_cache +.ruff_cache +.coverage +htmlcov +site +dist +.dockerignore +Dockerfile +docker-compose.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6e217..493aff8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,96 +1,35 @@ -name: CI +name: hier-config-api build and test on: push: - branches: [ main, develop ] + branches: [develop, next] pull_request: - branches: [ main, develop ] + branches: [develop, next] jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 2.3.1 - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - - name: Install project - run: poetry install --no-interaction - - - name: Run Ruff linter - run: poetry run ruff check . - - - name: Run Ruff formatter check - run: poetry run ruff format --check . - - - name: Run mypy - run: poetry run mypy hier_config_api - - test: - name: Test (Python ${{ matrix.python-version }}) + build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] - + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 2.3.1 - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: .venv - key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - - name: Install project - run: poetry install --no-interaction - - - name: Run tests - run: poetry run pytest --cov=hier_config_api --cov-report=xml --cov-report=term-missing - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - if: matrix.python-version == '3.11' - with: - file: ./coverage.xml - fail_ci_if_error: false + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Run tests + run: | + poetry install --no-interaction + poetry run python scripts/build.py lint + poetry run python scripts/build.py pytest --coverage diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cad7ab0..58e516a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,10 +3,12 @@ name: Deploy Documentation on: push: branches: - - main + - develop + - next pull_request: branches: - - main + - develop + - next permissions: contents: write @@ -20,7 +22,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install Poetry uses: snok/install-poetry@v1 @@ -44,10 +46,10 @@ jobs: run: poetry install --no-interaction - name: Build documentation - run: poetry run mkdocs build + run: poetry run mkdocs build --strict - name: Deploy to GitHub Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..289b6cb --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,92 @@ +name: Prepare Release + +# The branch to release from is chosen via the standard "Run workflow" +# branch dropdown in the GitHub UI — this workflow runs against, and +# releases from, whatever ref it was dispatched on. +on: + workflow_dispatch: + inputs: + bump: + description: Version bump type + required: true + type: choice + options: + - major + - minor + - patch + - prerelease + +permissions: + contents: write + pull-requests: write + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - name: Require repository admin + uses: actions/github-script@v8 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + if (data.permission !== 'admin') { + core.setFailed( + `@${context.actor} has '${data.permission}' permission; ` + + 'only repository admins may prepare a release.', + ); + } + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Bump version + id: bump + run: | + poetry version "${{ github.event.inputs.bump }}" + version="$(poetry version --short)" + echo "version=${version}" >> "$GITHUB_OUTPUT" + case "$version" in + *a*|*b*|*rc*|*dev*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;; + *) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;; + esac + - name: Commit bump to release branch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "release/v${{ steps.bump.outputs.version }}" + git commit -am "chore(release): prepare ${{ steps.bump.outputs.version }}" + git push origin "release/v${{ steps.bump.outputs.version }}" + - name: Open release pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base "${{ github.ref_name }}" \ + --head "release/v${{ steps.bump.outputs.version }}" \ + --title "chore(release): prepare ${{ steps.bump.outputs.version }}" \ + --body "Bumps the version to ${{ steps.bump.outputs.version }} (bump type: ${{ github.event.inputs.bump }}). + + Merge this PR first, then publish the draft release v${{ steps.bump.outputs.version }} to trigger the PyPI publish." + - name: Create draft GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + args=() + if [ "${{ steps.bump.outputs.prerelease }}" = "true" ]; then + args+=(--prerelease) + fi + gh release create "v${{ steps.bump.outputs.version }}" \ + --target "${{ github.ref_name }}" \ + --draft \ + --title "v${{ steps.bump.outputs.version }}" \ + --generate-notes \ + "${args[@]}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4c95aa7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release + +# Fires when a release is published, including a draft release being +# published (the `created` type never fires for drafts). +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + # On release events, checkout defaults to the release's tag ref. + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Publish to PyPI + run: | + poetry config pypi-token.pypi "${{ secrets.PYPI_TOKEN }}" + poetry publish --build diff --git a/.github/workflows/sync-standards.yml b/.github/workflows/sync-standards.yml new file mode 100644 index 0000000..bed4f74 --- /dev/null +++ b/.github/workflows/sync-standards.yml @@ -0,0 +1,39 @@ +name: hier-config-api sync standards + +on: + schedule: + # Weekly, Monday 06:00 UTC + - cron: "0 6 * * 1" + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install poetry + uses: snok/install-poetry@v1 + with: + version: 2.3.1 + - name: Apply canonical standards + run: | + poetry install --no-interaction + poetry run python scripts/sync_standards.py apply + - name: Open pull request if standards drifted + uses: peter-evans/create-pull-request@v7 + with: + branch: sync-standards + base: develop + title: Sync development standards from netdevops/hier_config + commit-message: Sync development standards from netdevops/hier_config + body: > + Automated update of shared development-standard files from the + canonical repository declared in `.standards.yml`. Review CI + results and reconcile any incompatibilities before merging. diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5503c1b..22f32df 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,12 +1,10 @@ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.12" jobs: @@ -14,18 +12,11 @@ build: # Install poetry - pip install poetry post_install: - # Install dependencies with poetry - - poetry install --with dev + # Install the project and all dependency groups into the Read the Docs + # virtualenv (VIRTUAL_ENV must point at it, otherwise poetry installs + # into its own virtualenv and mkdocs is not found at build time) + - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with dev # Build documentation with MkDocs mkdocs: configuration: mkdocs.yml - -# Optional but recommended, declare the Python requirements required -# to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -python: - install: - - method: pip - path: . - - requirements: docs/requirements.txt diff --git a/.standards.yml b/.standards.yml new file mode 100644 index 0000000..885a206 --- /dev/null +++ b/.standards.yml @@ -0,0 +1,20 @@ +# Shared development standards for netdevops hier-config projects. +# +# The files listed below are owned by the canonical repository and kept in +# sync across all hier-config projects. Use `invoke sync-standards` (or +# `poetry run python scripts/sync_standards.py check|apply`) to compare the +# local copies against the canonical versions and pull in upstream changes. +source: + repo: netdevops/hier_config + ref: master + +# Whole-word replacements applied to fetched file contents so package +# references match this project. +substitutions: + hier_config: hier_config_api + +files: + - scripts/build.py + - scripts/sync_standards.py + - .yamllint.yml + - .dockerignore diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..3c99231 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default + +rules: + indentation: + spaces: 2 + indent-sequences: consistent + document-start: + present: false + line-length: false diff --git a/CLAUDE.md b/CLAUDE.md index 54fdec4..0582a9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,31 +8,35 @@ hier-config-api is a FastAPI REST API providing an interface to the [hier_config ## Commands +All commands use **poetry** (not pip): + ```bash # Install dependencies -poetry install # production deps -poetry install --with dev # include dev deps +poetry install # Run development server (with hot reload) poetry run uvicorn hier_config_api.main:app --reload -# Linting & formatting -poetry run ruff check . # lint -poetry run ruff check . --fix # lint with auto-fix -poetry run ruff format . # format -poetry run ruff format --check . # check formatting +# Full lint + test suite (equivalent to CI) +poetry run python scripts/build.py lint-and-test + +# Lint only (ruff format + check, mypy, pyright, pylint, yamllint, flynt — run in parallel) +poetry run python scripts/build.py lint -# Type checking -poetry run mypy hier_config_api +# Lint with auto-fixes (ruff --fix, ruff format, flynt) +poetry run python scripts/build.py lint --fix -# Tests -poetry run pytest # all tests (includes coverage) +# Tests with coverage (95% coverage required) +poetry run python scripts/build.py pytest --coverage + +# Tests without coverage +poetry run pytest # all tests poetry run pytest tests/test_configs.py -v # single test file poetry run pytest tests/test_configs.py::test_parse_config -v # single test # Documentation -poetry run mkdocs serve # local preview with live reload -poetry run mkdocs build # build static site +poetry run mkdocs serve # local preview with live reload +poetry run mkdocs build --strict # build static site (CI runs this) ``` ## Architecture @@ -54,13 +58,20 @@ poetry run mkdocs build # build static site ## Code Quality -- **Ruff**: linter and formatter, line length 100, target Python 3.10 -- **MyPy**: strict mode enabled; `hier_config.*` has `ignore_missing_imports` -- **Pytest**: asyncio_mode="auto", coverage configured via `--cov=hier_config_api` +This repo follows the hier_config lint/typing/testing standards, enforced by +`scripts/build.py` and CI: + +- **Ruff**: `select = ["ALL"]` with preview rules, line length 88, target Python 3.10; formatting via `ruff format` with `docstring-code-format` +- **MyPy**: `strict = true` with the pydantic plugin +- **Pyright**: `typeCheckingMode = "strict"` +- **Pylint**: extension plugins + `pylint_pydantic`; rules already covered by ruff are disabled +- **yamllint / flynt**: YAML style (2-space indent, no document-start) and f-string enforcement +- **Pytest**: flat function-based tests with full type annotations; 95% coverage floor (`--cov=hier_config_api --cov-fail-under=95`) - Tests use `FastAPI.TestClient` with fixtures in `tests/conftest.py` +- Never loosen lint or coverage configuration to make a change pass; do not add unjustified `# noqa` / `# type: ignore` suppressions ## CI -GitHub Actions runs on push/PR to `main`/`develop`: -- **Lint job**: ruff check, ruff format --check, mypy -- **Test job**: pytest across Python 3.10, 3.11, 3.12 +GitHub Actions runs on push/PR to `develop`/`next`: +- **build job** (Python 3.10–3.14 matrix): `poetry run python scripts/build.py lint` then `poetry run python scripts/build.py pytest --coverage` +- **docs job**: `poetry run mkdocs build --strict`; deploys to GitHub Pages on push to `develop` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..664e451 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false + +RUN pip install --no-cache-dir "poetry>=2.0,<3.0" + +WORKDIR /app + +# README.md is required because pyproject.toml declares it as the readme +COPY pyproject.toml poetry.lock README.md ./ + + +# Development image: full toolchain; source is bind-mounted by docker compose +FROM base AS development + +RUN poetry install --no-root --with dev + +COPY . . + +RUN poetry install --with dev + +EXPOSE 8000 + +CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + + +# Production image: runtime dependencies only +FROM base AS production + +RUN poetry install --no-root --only main + +COPY hier_config_api ./hier_config_api + +RUN poetry install --only main + +EXPOSE 8000 + +CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/README.md b/README.md index ade6876..044e707 100644 --- a/README.md +++ b/README.md @@ -238,34 +238,85 @@ Get the results of a completed batch job. ## Development +This project follows the same development, testing, and linting standards as +[hier_config](https://github.com/netdevops/hier_config). All checks are driven +by `scripts/build.py`: + +```bash +# Full lint + test suite (what CI runs) +poetry run python scripts/build.py lint-and-test + +# Lint only (ruff format + check, mypy, pyright, pylint, yamllint, flynt — in parallel) +poetry run python scripts/build.py lint + +# Lint with auto-fixes applied +poetry run python scripts/build.py lint --fix + +# Tests with coverage (95% minimum enforced) +poetry run python scripts/build.py pytest --coverage +``` + +### Docker Development Environment + +A Docker-based development environment, driven by +[invoke](https://www.pyinvoke.org/) tasks, is available as an alternative to a +local Poetry setup: + +```bash +invoke build # build the development image +invoke serve # API with hot reload at http://localhost:8000 +invoke docs # docs with live reload at http://localhost:8001 +invoke pytest # tests inside the container (--coverage for the gate) +invoke lint # linters inside the container (--fix for auto-fixes) +invoke lint-and-test # full suite, same as CI +invoke cli # shell inside the container +invoke sync-standards # check drift against canonical netdevops standards +invoke destroy # tear down containers +``` + +Shared development standards (lint/typing/test tooling, YAML style) are owned +by [hier_config](https://github.com/netdevops/hier_config) and declared in +`.standards.yml`; `invoke sync-standards --apply` pulls in the canonical +versions, and a weekly workflow opens a PR when they change. + ### Running Tests ```bash # Run all tests poetry run pytest -# Run with coverage -poetry run pytest --cov=hier_config_api --cov-report=html - # Run specific test file poetry run pytest tests/test_configs.py -v + +# Run a single test +poetry run pytest tests/test_configs.py::test_parse_config -v ``` -### Code Quality +### Code Quality Standards -```bash -# Run ruff linter -poetry run ruff check . +- **ruff**: `select = ["ALL"]` with preview rules, line length 88; formatting via `ruff format` +- **mypy**: strict mode with the pydantic plugin +- **pyright**: `typeCheckingMode = "strict"` +- **pylint**: extension plugins + `pylint_pydantic`, for rules not covered by ruff +- **yamllint / flynt**: YAML style and f-string enforcement +- **pytest**: flat function-based tests with full type annotations; 95% coverage floor -# Run ruff formatter -poetry run ruff format . +Never loosen the lint or coverage configuration to make a change pass. -# Run mypy type checker -poetry run mypy hier_config_api +### Releasing -# Run all linters -poetry run ruff check . && poetry run mypy hier_config_api -``` +Releases are driven by two GitHub Actions workflows and require repository +admin permission: + +1. Run the **Prepare Release** workflow (`Actions` → `Prepare Release` → + `Run workflow`), choosing the branch to release from in the branch + dropdown and the version bump type (`major`, `minor`, `patch`, or + `prerelease`). The workflow bumps the version in `pyproject.toml`, opens + a `release/vX.Y.Z` pull request against the chosen branch, and creates a + draft GitHub release tagged `vX.Y.Z` targeting that branch. +2. Merge the release pull request. +3. Publish the draft release. Publishing fires the **Release** workflow, + which builds the package and uploads it to PyPI automatically. ## Supported Platforms diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7078c2f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + api: + build: + context: . + target: development + ports: + - "8000:8000" + volumes: + - .:/app + command: > + uvicorn hier_config_api.main:app --host 0.0.0.0 --port 8000 --reload + + # Live-reloading documentation server; opt in with: docker compose --profile docs up + docs: + build: + context: . + target: development + profiles: + - docs + ports: + - "8001:8001" + volumes: + - .:/app + command: mkdocs serve --dev-addr 0.0.0.0:8001 diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 81d6710..c8255ef 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -76,38 +76,15 @@ sudo certbot --nginx -d api.example.com ### Dockerfile -Create `Dockerfile`: - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -# Install Poetry -RUN pip install poetry==2.3.1 - -# Copy dependency files -COPY pyproject.toml poetry.lock ./ - -# Install dependencies -RUN poetry config virtualenvs.create false \ - && poetry install --no-interaction --no-ansi --no-root --only main - -# Copy application -COPY hier_config_api ./hier_config_api - -# Expose port -EXPOSE 8000 - -# Run application -CMD ["uvicorn", "hier_config_api.main:app", "--host", "0.0.0.0", "--port", "8000"] -``` +The repository ships a multi-stage `Dockerfile`. The `production` target +installs only runtime dependencies; the `development` target (used by +`docker-compose.yml` for local development) adds the full toolchain. ### Build and Run ```bash -# Build image -docker build -t hier-config-api:latest . +# Build the production image +docker build --target production -t hier-config-api:latest . # Run container docker run -d \ @@ -118,26 +95,26 @@ docker run -d \ ### Docker Compose -Create `docker-compose.yml`: +The `docker-compose.yml` in the repository is tuned for development (hot +reload, bind-mounted source). For a production deployment, use a compose file +that targets the production image: ```yaml -version: '3.8' - services: api: - build: . + build: + context: . + target: production ports: - "8000:8000" restart: always - environment: - - LOG_LEVEL=info command: uvicorn hier_config_api.main:app --host 0.0.0.0 --port 8000 --workers 4 ``` Run: ```bash -docker-compose up -d +docker compose up -d ``` ## Kubernetes Deployment diff --git a/docs/guides/development.md b/docs/guides/development.md index 1b882c8..7f0f5bf 100644 --- a/docs/guides/development.md +++ b/docs/guides/development.md @@ -24,6 +24,69 @@ poetry install poetry shell ``` +### Docker + +As an alternative to a local Poetry environment, the repository ships a Docker +development environment driven by [invoke](https://www.pyinvoke.org/) tasks +(`tasks.py`). The `development` image target installs the full toolchain, and +`docker-compose.yml` bind-mounts the repository so the API server hot-reloads +on changes: + +```bash +# Build the development image (rerun after dependency changes) +invoke build + +# Start the API with hot reload at http://localhost:8000 +invoke serve + +# Serve the documentation with live reload at http://localhost:8001 +invoke docs + +# Run tests and linters inside the container +invoke pytest # add --coverage for the 95% coverage gate +invoke lint # add --fix to apply auto-fixes +invoke lint-and-test # full suite, same as CI + +# Open a shell inside the container +invoke cli + +# Tear everything down +invoke destroy +``` + +The raw `docker compose` commands work as well; the invoke tasks are thin +wrappers around them. + +## Common Development Standards + +All netdevops hier-config projects share a common development model: the same +lint/typing/test tooling, YAML style, Docker development environment, and +invoke task names. + +The canonical copies live in +[netdevops/hier_config](https://github.com/netdevops/hier_config) — see its +[Shared Development Standards](https://netdevops.github.io/hier_config/dev/shared-standards/) +guide for the full model. This repository declares what it pulls in via the +`.standards.yml` manifest: `scripts/build.py`, `scripts/sync_standards.py`, +`.yamllint.yml`, and `.dockerignore`. Package references are rewritten +(`hier_config` → `hier_config_api`) as files are fetched. + +Do not edit those files directly — the next sync overwrites them. Change them +in `hier_config` instead. To compare the local copies against the canonical +versions: + +```bash +# Report drift (exits non-zero when local files differ from canonical) +invoke sync-standards + +# Pull the canonical versions into the local files +invoke sync-standards --apply +``` + +A scheduled GitHub Actions workflow (`sync-standards.yml`) runs the sync +weekly and opens a pull request when the canonical standards change, so CI +validates the update before it merges. + ## Project Structure ``` diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index ab222c0..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Documentation build requirements for ReadTheDocs -mkdocs>=1.6.1,<2.0.0 -mkdocs-material>=9.7.1,<10.0.0 -mkdocstrings[python]>=1.0.2,<2.0.0 -mkdocs-gen-files>=0.6.0,<0.7.0 -mkdocs-literate-nav>=0.6.2,<0.7.0 -mkdocs-section-index>=0.3.10,<0.4.0 diff --git a/hier_config_api/main.py b/hier_config_api/main.py index 453678d..64bf77e 100644 --- a/hier_config_api/main.py +++ b/hier_config_api/main.py @@ -50,4 +50,6 @@ async def health() -> dict[str, str]: if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + # Dev/container entry point: binding all interfaces is intentional so the + # API is reachable from outside the container. + uvicorn.run(app, host="0.0.0.0", port=8000) # ruff: ignore[hardcoded-bind-all-interfaces] diff --git a/hier_config_api/models/config.py b/hier_config_api/models/config.py index 8cecabc..5618ea3 100644 --- a/hier_config_api/models/config.py +++ b/hier_config_api/models/config.py @@ -8,7 +8,9 @@ class ParseConfigRequest(BaseModel): """Request model for parsing configuration.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) config_text: str = Field(..., description="Raw configuration text to parse") @@ -16,13 +18,17 @@ class ParseConfigResponse(BaseModel): """Response model for parsed configuration.""" platform: str = Field(..., description="Platform type") - structured_config: dict[str, Any] = Field(..., description="Structured configuration tree") + structured_config: dict[str, Any] = Field( + ..., description="Structured configuration tree" + ) class CompareConfigRequest(BaseModel): """Request model for comparing configurations.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") @@ -38,9 +44,13 @@ class CompareConfigResponse(BaseModel): class PredictConfigRequest(BaseModel): """Request model for predicting future configuration state.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) current_config: str = Field(..., description="Current configuration state") - commands_to_apply: str = Field(..., description="Commands to apply to current config") + commands_to_apply: str = Field( + ..., description="Commands to apply to current config" + ) class PredictConfigResponse(BaseModel): @@ -55,7 +65,9 @@ class PredictConfigResponse(BaseModel): class MergeConfigRequest(BaseModel): """Request model for merging multiple configurations.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) configs: list[str] = Field(..., description="List of configuration texts to merge") @@ -69,18 +81,22 @@ class MergeConfigResponse(BaseModel): class MatchRule(BaseModel): """Rule for matching configuration lines.""" - equals: str | None = Field(None, description="Exact match string") - contains: str | None = Field(None, description="Substring to match") - startswith: str | None = Field(None, description="Prefix to match") - regex: str | None = Field(None, description="Regular expression pattern") + equals: str | None = Field(default=None, description="Exact match string") + contains: str | None = Field(default=None, description="Substring to match") + startswith: str | None = Field(default=None, description="Prefix to match") + regex: str | None = Field(default=None, description="Regular expression pattern") class SearchConfigRequest(BaseModel): """Request model for searching configuration.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) config_text: str = Field(..., description="Configuration text to search") - match_rules: MatchRule = Field(..., description="Rules for matching configuration sections") + match_rules: MatchRule = Field( + ..., description="Rules for matching configuration sections" + ) class SearchConfigResponse(BaseModel): diff --git a/hier_config_api/models/platform.py b/hier_config_api/models/platform.py index 3ce9836..d88ae82 100644 --- a/hier_config_api/models/platform.py +++ b/hier_config_api/models/platform.py @@ -5,13 +5,19 @@ from pydantic import BaseModel, Field +def _ordering_rules_default() -> list[dict[str, Any]]: + return [] + + class PlatformInfo(BaseModel): """Information about a supported platform.""" platform_name: str = Field(..., description="Platform identifier") display_name: str = Field(..., description="Human-readable platform name") vendor: str = Field(..., description="Vendor name") - supported: bool = Field(True, description="Whether platform is currently supported") + supported: bool = Field( + default=True, description="Whether platform is currently supported" + ) class PlatformRules(BaseModel): @@ -19,13 +25,14 @@ class PlatformRules(BaseModel): platform_name: str = Field(..., description="Platform identifier") negation_default_when: list[str] = Field( - default_factory=list, description="Default negation patterns for when conditions" + default_factory=list, + description="Default negation patterns for when conditions", ) negation_negate_with: list[str] = Field( default_factory=list, description="How to negate specific commands" ) ordering: list[dict[str, Any]] = Field( - default_factory=list, description="Command ordering rules" + default_factory=_ordering_rules_default, description="Command ordering rules" ) idempotent_commands_avoid: list[str] = Field( default_factory=list, description="Commands to avoid in idempotent configs" @@ -53,7 +60,9 @@ class ValidateConfigResponse(BaseModel): class BatchJobRequest(BaseModel): """Request model for batch job.""" - device_configs: list[dict[str, Any]] = Field(..., description="List of device configurations") + device_configs: list[dict[str, Any]] = Field( + ..., description="List of device configurations" + ) class BatchJobResponse(BaseModel): @@ -67,11 +76,15 @@ class BatchJobStatus(BaseModel): """Status of a batch job.""" job_id: str = Field(..., description="Job identifier") - status: str = Field(..., description="Job status (pending, running, completed, failed)") - progress: float = Field(0.0, description="Progress percentage (0.0 to 100.0)", ge=0.0, le=100.0) + status: str = Field( + ..., description="Job status (pending, running, completed, failed)" + ) + progress: float = Field( + default=0.0, description="Progress percentage (0.0 to 100.0)", ge=0.0, le=100.0 + ) total_devices: int = Field(..., description="Total number of devices") - completed_devices: int = Field(0, description="Number of completed devices") - failed_devices: int = Field(0, description="Number of failed devices") + completed_devices: int = Field(default=0, description="Number of completed devices") + failed_devices: int = Field(default=0, description="Number of failed devices") class BatchJobResults(BaseModel): diff --git a/hier_config_api/models/remediation.py b/hier_config_api/models/remediation.py index 56540c9..576685f 100644 --- a/hier_config_api/models/remediation.py +++ b/hier_config_api/models/remediation.py @@ -13,31 +13,47 @@ class TagRule(BaseModel): class RemediationSummary(BaseModel): """Summary of remediation changes.""" - additions: int = Field(0, description="Number of configuration additions") - deletions: int = Field(0, description="Number of configuration deletions") - modifications: int = Field(0, description="Number of configuration modifications") + additions: int = Field(default=0, description="Number of configuration additions") + deletions: int = Field(default=0, description="Number of configuration deletions") + modifications: int = Field( + default=0, description="Number of configuration modifications" + ) class GenerateRemediationRequest(BaseModel): """Request model for generating remediation.""" - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") - tag_rules: list[TagRule] | None = Field(None, description="Optional tag rules to apply") - include_tags: list[str] | None = Field(None, description="Only include these tags") - exclude_tags: list[str] | None = Field(None, description="Exclude these tags") + tag_rules: list[TagRule] | None = Field( + default=None, description="Optional tag rules to apply" + ) + include_tags: list[str] | None = Field( + default=None, description="Only include these tags" + ) + exclude_tags: list[str] | None = Field( + default=None, description="Exclude these tags" + ) class GenerateRemediationResponse(BaseModel): """Response model for generated remediation.""" - remediation_id: str = Field(..., description="Unique identifier for this remediation") + remediation_id: str = Field( + ..., description="Unique identifier for this remediation" + ) platform: str = Field(..., description="Platform type") - remediation_config: str = Field(..., description="Commands to achieve desired state") + remediation_config: str = Field( + ..., description="Commands to achieve desired state" + ) rollback_config: str = Field(..., description="Commands to rollback changes") summary: RemediationSummary = Field(..., description="Summary of changes") - tags: dict[str, list[str]] = Field(default_factory=dict, description="Tags applied to commands") + tags: dict[str, list[str]] = Field( + default_factory=dict, description="Tags applied to commands" + ) class ApplyTagsRequest(BaseModel): @@ -57,8 +73,12 @@ class ApplyTagsResponse(BaseModel): class FilterRemediationRequest(BaseModel): """Request model for filtering remediation by tags.""" - include_tags: list[str] | None = Field(None, description="Only include these tags") - exclude_tags: list[str] | None = Field(None, description="Exclude these tags") + include_tags: list[str] | None = Field( + default=None, description="Only include these tags" + ) + exclude_tags: list[str] | None = Field( + default=None, description="Exclude these tags" + ) class FilterRemediationResponse(BaseModel): diff --git a/hier_config_api/models/report.py b/hier_config_api/models/report.py index c8c31a8..df6083b 100644 --- a/hier_config_api/models/report.py +++ b/hier_config_api/models/report.py @@ -7,7 +7,9 @@ class DeviceRemediation(BaseModel): """Model for a single device's remediation data.""" device_id: str = Field(..., description="Unique device identifier") - platform: str = Field(..., description="Platform type (e.g., cisco_ios, juniper_junos)") + platform: str = Field( + ..., description="Platform type (e.g., cisco_ios, juniper_junos)" + ) running_config: str = Field(..., description="Current running configuration") intended_config: str = Field(..., description="Desired configuration state") @@ -32,7 +34,9 @@ class ReportSummary(BaseModel): total_devices: int = Field(..., description="Total number of devices") devices_with_changes: int = Field(..., description="Number of devices with changes") - total_changes: int = Field(..., description="Total number of changes across all devices") + total_changes: int = Field( + ..., description="Total number of changes across all devices" + ) changes_by_tag: dict[str, int] = Field( default_factory=dict, description="Count of changes by tag" ) @@ -44,7 +48,9 @@ class ChangeDetail(BaseModel): change_text: str = Field(..., description="The configuration change text") device_count: int = Field(..., description="Number of devices with this change") device_ids: list[str] = Field(..., description="List of affected device IDs") - tags: list[str] = Field(default_factory=list, description="Tags associated with this change") + tags: list[str] = Field( + default_factory=list, description="Tags associated with this change" + ) class GetReportChangesResponse(BaseModel): @@ -58,4 +64,4 @@ class GetReportChangesResponse(BaseModel): class ExportFormat(BaseModel): """Supported export formats.""" - format: str = Field("json", description="Export format (json, csv, yaml)") + format: str = Field(default="json", description="Export format (json, csv, yaml)") diff --git a/hier_config_api/routers/batch.py b/hier_config_api/routers/batch.py index e321e25..2fb097e 100644 --- a/hier_config_api/routers/batch.py +++ b/hier_config_api/routers/batch.py @@ -1,5 +1,7 @@ """API router for batch operations.""" +from typing import Any + from fastapi import APIRouter, HTTPException from hier_config_api.models.platform import ( @@ -14,23 +16,32 @@ router = APIRouter(prefix="/api/v1/batch", tags=["batch"]) -@router.post("/remediation", response_model=BatchJobResponse) +def _create_and_run_job( + device_configs: list[dict[str, Any]], +) -> tuple[str, dict[str, Any]]: + """Create a batch job, process it, and persist the result.""" + job_data = PlatformService.create_batch_job(device_configs) + job_id = storage.store_job(job_data) + + # Process the job (in a real implementation, this would be async/background) + PlatformService.process_batch_job(job_data) + storage.update_job(job_id, job_data) + return job_id, job_data + + +@router.post("/remediation") async def create_batch_remediation(request: BatchJobRequest) -> BatchJobResponse: """Create a batch remediation job for multiple devices.""" try: - job_data = PlatformService.create_batch_job(request.device_configs) - job_id = storage.store_job(job_data) - - # Process the job (in a real implementation, this would be async/background) - PlatformService.process_batch_job(job_data) - storage.update_job(job_id, job_data) - - return BatchJobResponse(job_id=job_id, total_devices=job_data["total_devices"]) + job_id, job_data = _create_and_run_job(request.device_configs) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to create batch job: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to create batch job: {e!s}" + ) from e + return BatchJobResponse(job_id=job_id, total_devices=job_data["total_devices"]) -@router.get("/jobs/{job_id}", response_model=BatchJobStatus) +@router.get("/jobs/{job_id}") async def get_batch_job_status(job_id: str) -> BatchJobStatus: """Get the status of a batch job.""" job_data = storage.get_job(job_id) @@ -47,27 +58,29 @@ async def get_batch_job_status(job_id: str) -> BatchJobStatus: failed_devices=job_data["failed_devices"], ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to get job status: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to get job status: {e!s}" + ) from e -@router.get("/jobs/{job_id}/results", response_model=BatchJobResults) +@router.get("/jobs/{job_id}/results") async def get_batch_job_results(job_id: str) -> BatchJobResults: """Get the results of a completed batch job.""" job_data = storage.get_job(job_id) if not job_data: raise HTTPException(status_code=404, detail="Job not found") - if job_data["status"] not in ["completed", "failed"]: + if job_data["status"] not in {"completed", "failed"}: raise HTTPException(status_code=400, detail="Job is not yet completed") - try: - summary = { - "total_devices": job_data["total_devices"], - "completed_devices": job_data["completed_devices"], - "failed_devices": job_data["failed_devices"], - "status": job_data["status"], - } + summary = { + "total_devices": job_data["total_devices"], + "completed_devices": job_data["completed_devices"], + "failed_devices": job_data["failed_devices"], + "status": job_data["status"], + } + try: return BatchJobResults( job_id=job_id, status=job_data["status"], @@ -75,4 +88,6 @@ async def get_batch_job_results(job_id: str) -> BatchJobResults: summary=summary, ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to get job results: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to get job results: {e!s}" + ) from e diff --git a/hier_config_api/routers/configs.py b/hier_config_api/routers/configs.py index faa6283..78ae2a1 100644 --- a/hier_config_api/routers/configs.py +++ b/hier_config_api/routers/configs.py @@ -19,66 +19,81 @@ router = APIRouter(prefix="/api/v1/configs", tags=["configurations"]) -@router.post("/parse", response_model=ParseConfigResponse) +@router.post("/parse") async def parse_config(request: ParseConfigRequest) -> ParseConfigResponse: """Parse configuration text into structured format.""" try: - structured_config = ConfigService.parse_config(request.platform, request.config_text) - return ParseConfigResponse(platform=request.platform, structured_config=structured_config) + structured_config = ConfigService.parse_config( + request.platform, request.config_text + ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to parse config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to parse config: {e!s}" + ) from e + return ParseConfigResponse( + platform=request.platform, structured_config=structured_config + ) -@router.post("/compare", response_model=CompareConfigResponse) +@router.post("/compare") async def compare_configs(request: CompareConfigRequest) -> CompareConfigResponse: """Compare two configurations and return differences.""" try: unified_diff, has_changes = ConfigService.compare_configs( request.platform, request.running_config, request.intended_config ) - return CompareConfigResponse( - platform=request.platform, unified_diff=unified_diff, has_changes=has_changes - ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to compare configs: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to compare configs: {e!s}" + ) from e + return CompareConfigResponse( + platform=request.platform, + unified_diff=unified_diff, + has_changes=has_changes, + ) -@router.post("/predict", response_model=PredictConfigResponse) +@router.post("/predict") async def predict_config(request: PredictConfigRequest) -> PredictConfigResponse: """Predict future configuration state after applying commands.""" try: predicted_config = ConfigService.predict_config( request.platform, request.current_config, request.commands_to_apply ) - return PredictConfigResponse(platform=request.platform, predicted_config=predicted_config) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to predict config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to predict config: {e!s}" + ) from e + return PredictConfigResponse( + platform=request.platform, predicted_config=predicted_config + ) -@router.post("/merge", response_model=MergeConfigResponse) +@router.post("/merge") async def merge_configs(request: MergeConfigRequest) -> MergeConfigResponse: """Merge multiple configurations into one.""" try: merged_config = ConfigService.merge_configs(request.platform, request.configs) - return MergeConfigResponse(platform=request.platform, merged_config=merged_config) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to merge configs: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to merge configs: {e!s}" + ) from e + return MergeConfigResponse(platform=request.platform, merged_config=merged_config) -@router.post("/search", response_model=SearchConfigResponse) +@router.post("/search") async def search_config(request: SearchConfigRequest) -> SearchConfigResponse: """Search configuration for matching sections.""" try: matches = ConfigService.search_config( platform=request.platform, config_text=request.config_text, - equals=request.match_rules.equals, - contains=request.match_rules.contains, - startswith=request.match_rules.startswith, - regex_pattern=request.match_rules.regex, - ) - return SearchConfigResponse( - platform=request.platform, matches=matches, match_count=len(matches) + match_rules=request.match_rules, ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to search config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to search config: {e!s}" + ) from e + return SearchConfigResponse( + platform=request.platform, matches=matches, match_count=len(matches) + ) diff --git a/hier_config_api/routers/platforms.py b/hier_config_api/routers/platforms.py index 93cf56f..da890fc 100644 --- a/hier_config_api/routers/platforms.py +++ b/hier_config_api/routers/platforms.py @@ -13,31 +13,37 @@ router = APIRouter(prefix="/api/v1/platforms", tags=["platforms"]) -@router.get("", response_model=list[PlatformInfo]) +@router.get("") async def list_platforms() -> list[PlatformInfo]: """List all supported platforms.""" try: return PlatformService.list_platforms() except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to list platforms: {str(e)}") from e + raise HTTPException( + status_code=500, detail=f"Failed to list platforms: {e!s}" + ) from e -@router.get("/{platform}/rules", response_model=PlatformRules) +@router.get("/{platform}/rules") async def get_platform_rules(platform: str) -> PlatformRules: """Get platform-specific rules and behaviors.""" try: return PlatformService.get_platform_rules(platform) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get platform rules: {str(e)}" + status_code=400, detail=f"Failed to get platform rules: {e!s}" ) from e -@router.post("/{platform}/validate", response_model=ValidateConfigResponse) -async def validate_config(platform: str, request: ValidateConfigRequest) -> ValidateConfigResponse: +@router.post("/{platform}/validate") +async def validate_config( + platform: str, request: ValidateConfigRequest +) -> ValidateConfigResponse: """Validate configuration for a specific platform.""" try: result = PlatformService.validate_config(platform, request.config_text) - return ValidateConfigResponse(**result) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to validate config: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to validate config: {e!s}" + ) from e + return ValidateConfigResponse(**result) diff --git a/hier_config_api/routers/remediation.py b/hier_config_api/routers/remediation.py index 0961f1c..e93a40b 100644 --- a/hier_config_api/routers/remediation.py +++ b/hier_config_api/routers/remediation.py @@ -1,5 +1,7 @@ """API router for remediation operations.""" +from typing import Annotated, Any + from fastapi import APIRouter, HTTPException, Query from hier_config_api.models.remediation import ( @@ -15,81 +17,95 @@ router = APIRouter(prefix="/api/v1/remediation", tags=["remediation"]) -@router.post("/generate", response_model=GenerateRemediationResponse) -async def generate_remediation(request: GenerateRemediationRequest) -> GenerateRemediationResponse: +def _generate_and_store(request: GenerateRemediationRequest) -> dict[str, Any]: + """Generate a remediation, persist it, and stamp it with its ID.""" + result = RemediationService.generate_remediation(request) + remediation_id = storage.store_remediation(result) + result["remediation_id"] = remediation_id + return result + + +@router.post("/generate") +async def generate_remediation( + request: GenerateRemediationRequest, +) -> GenerateRemediationResponse: """Generate remediation and rollback configurations.""" try: - result = RemediationService.generate_remediation( - platform=request.platform, - running_config=request.running_config, - intended_config=request.intended_config, - tag_rules=request.tag_rules, - include_tags=request.include_tags, - exclude_tags=request.exclude_tags, - ) - - # Store remediation - remediation_id = storage.store_remediation(result) - result["remediation_id"] = remediation_id - - return GenerateRemediationResponse( - remediation_id=remediation_id, - platform=result["platform"], - remediation_config=result["remediation_config"], - rollback_config=result["rollback_config"], - summary=result["summary"], - tags=result["tags"], - ) + result = _generate_and_store(request) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to generate remediation: {str(e)}" + status_code=400, detail=f"Failed to generate remediation: {e!s}" ) from e + return GenerateRemediationResponse( + remediation_id=result["remediation_id"], + platform=result["platform"], + remediation_config=result["remediation_config"], + rollback_config=result["rollback_config"], + summary=result["summary"], + tags=result["tags"], + ) -@router.post("/{remediation_id}/tags", response_model=ApplyTagsResponse) -async def apply_tags(remediation_id: str, request: ApplyTagsRequest) -> ApplyTagsResponse: +def _apply_and_store_tags( + remediation_id: str, + remediation_data: dict[str, Any], + request: ApplyTagsRequest, +) -> tuple[str, dict[str, list[str]]]: + """Apply tag rules to a stored remediation and persist the tags.""" + remediation_config = remediation_data["remediation_config"] + tagged_config, tags = RemediationService.apply_tags( + remediation_config, request.tag_rules + ) + storage.update_remediation(remediation_id, {"tags": tags}) + return tagged_config, tags + + +@router.post("/{remediation_id}/tags") +async def apply_tags( + remediation_id: str, request: ApplyTagsRequest +) -> ApplyTagsResponse: """Apply tags to an existing remediation.""" remediation_data = storage.get_remediation(remediation_id) if not remediation_data: raise HTTPException(status_code=404, detail="Remediation not found") try: - remediation_config = remediation_data["remediation_config"] - tagged_config, tags = RemediationService.apply_tags(remediation_config, request.tag_rules) - - # Update stored remediation - storage.update_remediation(remediation_id, {"tags": tags}) - - return ApplyTagsResponse( - remediation_id=remediation_id, remediation_config=tagged_config, tags=tags + tagged_config, tags = _apply_and_store_tags( + remediation_id, remediation_data, request ) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to apply tags: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to apply tags: {e!s}" + ) from e + return ApplyTagsResponse( + remediation_id=remediation_id, remediation_config=tagged_config, tags=tags + ) -@router.get("/{remediation_id}/filter", response_model=FilterRemediationResponse) +@router.get("/{remediation_id}/filter") async def filter_remediation( remediation_id: str, - include_tags: list[str] = Query(None), - exclude_tags: list[str] = Query(None), + include_tags: Annotated[list[str] | None, Query()] = None, + exclude_tags: Annotated[list[str] | None, Query()] = None, ) -> FilterRemediationResponse: """Filter remediation by tags.""" remediation_data = storage.get_remediation(remediation_id) if not remediation_data: raise HTTPException(status_code=404, detail="Remediation not found") - try: - remediation_config = remediation_data["remediation_config"] - tags = remediation_data.get("tags", {}) + remediation_config = remediation_data["remediation_config"] + tags = remediation_data.get("tags", {}) + try: filtered_config, summary = RemediationService.filter_remediation( remediation_config, tags, include_tags, exclude_tags ) - - return FilterRemediationResponse( - remediation_id=remediation_id, filtered_config=filtered_config, summary=summary - ) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to filter remediation: {str(e)}" + status_code=400, detail=f"Failed to filter remediation: {e!s}" ) from e + return FilterRemediationResponse( + remediation_id=remediation_id, + filtered_config=filtered_config, + summary=summary, + ) diff --git a/hier_config_api/routers/reports.py b/hier_config_api/routers/reports.py index 4adf11e..53beccc 100644 --- a/hier_config_api/routers/reports.py +++ b/hier_config_api/routers/reports.py @@ -1,5 +1,7 @@ """API router for multi-device reporting.""" +from typing import Annotated + from fastapi import APIRouter, HTTPException, Query from fastapi.responses import PlainTextResponse @@ -15,19 +17,22 @@ router = APIRouter(prefix="/api/v1/reports", tags=["reports"]) -@router.post("", response_model=CreateReportResponse) +@router.post("") async def create_report(request: CreateReportRequest) -> CreateReportResponse: """Create a multi-device report.""" try: report_data = ReportService.create_report(request.remediations) - report_id = storage.store_report(report_data) - - return CreateReportResponse(report_id=report_id, total_devices=report_data["total_devices"]) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to create report: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to create report: {e!s}" + ) from e + report_id = storage.store_report(report_data) + return CreateReportResponse( + report_id=report_id, total_devices=report_data["total_devices"] + ) -@router.get("/{report_id}/summary", response_model=ReportSummary) +@router.get("/{report_id}/summary") async def get_report_summary(report_id: str) -> ReportSummary: """Get summary statistics for a report.""" report_data = storage.get_report(report_id) @@ -38,13 +43,15 @@ async def get_report_summary(report_id: str) -> ReportSummary: return ReportService.get_summary(report_data) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get report summary: {str(e)}" + status_code=400, detail=f"Failed to get report summary: {e!s}" ) from e -@router.get("/{report_id}/changes", response_model=GetReportChangesResponse) +@router.get("/{report_id}/changes") async def get_report_changes( - report_id: str, tag: str | None = Query(None), min_devices: int = Query(1) + report_id: str, + tag: Annotated[str | None, Query()] = None, + min_devices: Annotated[int, Query()] = 1, ) -> GetReportChangesResponse: """Get detailed change analysis for a report.""" report_data = storage.get_report(report_id) @@ -52,28 +59,36 @@ async def get_report_changes( raise HTTPException(status_code=404, detail="Report not found") try: - changes = ReportService.get_changes(report_data, tag_filter=tag, min_devices=min_devices) - - return GetReportChangesResponse( - report_id=report_id, changes=changes, total_unique_changes=len(changes) + changes = ReportService.get_changes( + report_data, tag_filter=tag, min_devices=min_devices ) except Exception as e: raise HTTPException( - status_code=400, detail=f"Failed to get report changes: {str(e)}" + status_code=400, detail=f"Failed to get report changes: {e!s}" ) from e + return GetReportChangesResponse( + report_id=report_id, changes=changes, total_unique_changes=len(changes) + ) @router.get("/{report_id}/export", response_class=PlainTextResponse) -async def export_report(report_id: str, format: str = Query("json")) -> str: +async def export_report( + report_id: str, + export_format: Annotated[str, Query(alias="format")] = "json", +) -> str: """Export report in specified format (json, csv, yaml).""" report_data = storage.get_report(report_id) if not report_data: raise HTTPException(status_code=404, detail="Report not found") - if format not in ["json", "csv", "yaml"]: - raise HTTPException(status_code=400, detail="Format must be one of: json, csv, yaml") + if export_format not in {"json", "csv", "yaml"}: + raise HTTPException( + status_code=400, detail="Format must be one of: json, csv, yaml" + ) try: - return ReportService.export_report(report_data, format_type=format) + return ReportService.export_report(report_data, format_type=export_format) except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to export report: {str(e)}") from e + raise HTTPException( + status_code=400, detail=f"Failed to export report: {e!s}" + ) from e diff --git a/hier_config_api/services/config_service.py b/hier_config_api/services/config_service.py index 8395938..eaf8022 100644 --- a/hier_config_api/services/config_service.py +++ b/hier_config_api/services/config_service.py @@ -3,7 +3,20 @@ import re from typing import Any -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, HConfigChild, Platform, WorkflowRemediation + +from hier_config_api.models.config import MatchRule + + +def _line_matches(config_line: str, match_rules: MatchRule) -> bool: + """Check whether a configuration line satisfies any of the match rules.""" + if match_rules.equals and config_line == match_rules.equals: + return True + if match_rules.contains and match_rules.contains in config_line: + return True + if match_rules.startswith and config_line.startswith(match_rules.startswith): + return True + return bool(match_rules.regex and re.search(match_rules.regex, config_line)) class ConfigService: @@ -26,18 +39,14 @@ def _get_platform(platform_str: str) -> Platform: def parse_config(platform: str, config_text: str) -> dict[str, Any]: """Parse configuration text into structured format.""" platform_enum = ConfigService._get_platform(platform) - hconfig = get_hconfig(platform_enum, config_text) + hconfig = HConfig.from_text(platform_enum, config_text) # Convert HConfig tree to dictionary representation - def config_to_dict(config_obj: Any) -> dict[str, Any]: - result: dict[str, Any] = { + def config_to_dict(config_obj: HConfig | HConfigChild) -> dict[str, Any]: + return { "text": str(config_obj), - "children": [], + "children": [config_to_dict(child) for child in config_obj.children], } - if hasattr(config_obj, "children"): - for child in config_obj.children: - result["children"].append(config_to_dict(child)) - return result return config_to_dict(hconfig) @@ -47,27 +56,26 @@ def compare_configs( ) -> tuple[str, bool]: """Compare two configurations and return unified diff.""" platform_enum = ConfigService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config rollback = workflow.rollback_config - diff_lines = [] + diff_lines: list[str] = [] # Generate unified diff format if remediation: - diff_lines.append("--- running_config") - diff_lines.append("+++ intended_config") - for line in str(remediation).splitlines(): - if line.strip(): - diff_lines.append(f"+ {line}") + diff_lines.extend(("--- running_config", "+++ intended_config")) + diff_lines.extend( + f"+ {line}" for line in str(remediation).splitlines() if line.strip() + ) if rollback: - for line in str(rollback).splitlines(): - if line.strip(): - diff_lines.append(f"- {line}") + diff_lines.extend( + f"- {line}" for line in str(rollback).splitlines() if line.strip() + ) unified_diff = "\n".join(diff_lines) if diff_lines else "No differences found" has_changes = bool(diff_lines) @@ -75,7 +83,9 @@ def compare_configs( return unified_diff, has_changes @staticmethod - def predict_config(platform: str, current_config: str, commands_to_apply: str) -> str: + def predict_config( + _platform: str, current_config: str, commands_to_apply: str + ) -> str: """Predict configuration state after applying commands.""" # Simple merge: append new commands config_lines = current_config.splitlines() @@ -100,13 +110,13 @@ def merge_configs(platform: str, configs: list[str]) -> str: # Merge each subsequent config for config in configs[1:]: - running_hconfig = get_hconfig(platform_enum, merged) - intended_hconfig = get_hconfig(platform_enum, config) + running_hconfig = HConfig.from_text(platform_enum, merged) + intended_hconfig = HConfig.from_text(platform_enum, config) workflow = WorkflowRemediation(running_hconfig, intended_hconfig) remediation = workflow.remediation_config if remediation: - merged += "\n" + str(remediation) + merged += f"\n{remediation!s}" return merged @@ -114,38 +124,23 @@ def merge_configs(platform: str, configs: list[str]) -> str: def search_config( platform: str, config_text: str, - equals: str | None = None, - contains: str | None = None, - startswith: str | None = None, - regex_pattern: str | None = None, + match_rules: MatchRule, ) -> list[str]: """Search configuration for matching lines.""" platform_enum = ConfigService._get_platform(platform) - hconfig = get_hconfig(platform_enum, config_text) + hconfig = HConfig.from_text(platform_enum, config_text) - matches = [] + matches: list[str] = [] - def search_recursive(config_obj: Any) -> None: + def search_recursive(config_obj: HConfig | HConfigChild) -> None: config_line = str(config_obj).strip() - # Check matching conditions - is_match = False - if equals and config_line == equals: - is_match = True - elif contains and contains in config_line: - is_match = True - elif startswith and config_line.startswith(startswith): - is_match = True - elif regex_pattern and re.search(regex_pattern, config_line): - is_match = True - - if is_match: + if _line_matches(config_line, match_rules): matches.append(config_line) # Recursively search children - if hasattr(config_obj, "children"): - for child in config_obj.children: - search_recursive(child) + for child in config_obj.children: + search_recursive(child) search_recursive(hconfig) return matches diff --git a/hier_config_api/services/platform_service.py b/hier_config_api/services/platform_service.py index c78e195..be92e7e 100644 --- a/hier_config_api/services/platform_service.py +++ b/hier_config_api/services/platform_service.py @@ -1,8 +1,8 @@ """Service layer for platform information and batch operations.""" -from typing import Any +from typing import Any, ClassVar -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, Platform, WorkflowRemediation from hier_config_api.models.platform import PlatformInfo, PlatformRules @@ -11,7 +11,7 @@ class PlatformService: """Service for handling platform-related operations.""" # Common platform definitions - PLATFORMS = { + PLATFORMS: ClassVar[dict[str, PlatformInfo]] = { "cisco_ios": PlatformInfo( platform_name="cisco_ios", display_name="Cisco IOS", @@ -79,29 +79,29 @@ def _get_platform(platform_str: str) -> Platform: @staticmethod def validate_config(platform: str, config_text: str) -> dict[str, Any]: """Validate configuration for a platform.""" - warnings = [] - errors = [] + warnings: list[str] = [] + errors: list[str] = [] is_valid = True + platform_enum = PlatformService._get_platform(platform) try: # Try to parse the configuration - platform_enum = PlatformService._get_platform(platform) - get_hconfig(platform_enum, config_text) - + HConfig.from_text(platform_enum, config_text) + # A validation endpoint must convert any parsing failure into a + # validation error rather than propagate it as a server error. + except Exception as exc: # ruff: ignore[blind-except] # pylint: disable=broad-exception-caught + errors.append(f"Configuration parsing error: {exc!s}") + is_valid = False + else: # Basic validation checks if not config_text.strip(): warnings.append("Configuration is empty") is_valid = False # Platform-specific validation could be added here - if platform == "cisco_ios": - # Check for common Cisco IOS patterns - if "hostname" not in config_text: - warnings.append("No hostname configured") - - except Exception as e: - errors.append(f"Configuration parsing error: {str(e)}") - is_valid = False + # Check for common Cisco IOS patterns + if platform == "cisco_ios" and "hostname" not in config_text: + warnings.append("No hostname configured") return { "platform": platform, @@ -123,46 +123,59 @@ def create_batch_job(device_configs: list[dict[str, Any]]) -> dict[str, Any]: "results": [], } + @staticmethod + def _remediate_device(device_config: dict[str, Any]) -> dict[str, Any]: + """Generate remediation and rollback for a single batch device.""" + platform = device_config.get("platform", "cisco_ios") + running_config = device_config.get("running_config", "") + intended_config = device_config.get("intended_config", "") + + platform_enum = PlatformService._get_platform(platform) + running_hconfig = HConfig.from_text(platform_enum, running_config) + intended_hconfig = HConfig.from_text(platform_enum, intended_config) + + workflow = WorkflowRemediation(running_hconfig, intended_hconfig) + remediation = workflow.remediation_config + rollback = workflow.rollback_config + + return { + "device_id": device_config.get("device_id"), + "status": "success", + "remediation": str(remediation) if remediation else "", + "rollback": str(rollback) if rollback else "", + } + + @staticmethod + def _process_device(device_config: dict[str, Any]) -> tuple[dict[str, Any], bool]: + """Process one batch device, returning its result and success flag.""" + try: + result = PlatformService._remediate_device(device_config) + # Batch jobs record per-device failures instead of aborting the job, + # so any processing error must be captured here. + except Exception as exc: # ruff: ignore[blind-except] # pylint: disable=broad-exception-caught + return ( + { + "device_id": device_config.get("device_id"), + "status": "failed", + "error": str(exc), + }, + False, + ) + return result, True + @staticmethod def process_batch_job(job_data: dict[str, Any]) -> dict[str, Any]: """Process a batch job (simplified synchronous version).""" - results = [] + results: list[dict[str, Any]] = [] completed = 0 failed = 0 for device_config in job_data["device_configs"]: - try: - # Process each device - platform = device_config.get("platform", "cisco_ios") - running_config = device_config.get("running_config", "") - intended_config = device_config.get("intended_config", "") - - platform_enum = PlatformService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) - - workflow = WorkflowRemediation(running_hconfig, intended_hconfig) - remediation = workflow.remediation_config - rollback = workflow.rollback_config - - results.append( - { - "device_id": device_config.get("device_id"), - "status": "success", - "remediation": str(remediation) if remediation else "", - "rollback": str(rollback) if rollback else "", - } - ) + result, succeeded = PlatformService._process_device(device_config) + results.append(result) + if succeeded: completed += 1 - - except Exception as e: - results.append( - { - "device_id": device_config.get("device_id"), - "status": "failed", - "error": str(e), - } - ) + else: failed += 1 # Update job data diff --git a/hier_config_api/services/remediation_service.py b/hier_config_api/services/remediation_service.py index 56fc3b0..d8bd79b 100644 --- a/hier_config_api/services/remediation_service.py +++ b/hier_config_api/services/remediation_service.py @@ -2,9 +2,13 @@ from typing import Any -from hier_config import Platform, WorkflowRemediation, get_hconfig +from hier_config import HConfig, Platform, WorkflowRemediation -from hier_config_api.models.remediation import RemediationSummary, TagRule +from hier_config_api.models.remediation import ( + GenerateRemediationRequest, + RemediationSummary, + TagRule, +) class RemediationService: @@ -24,24 +28,11 @@ def _get_platform(platform_str: str) -> Platform: return platform_map.get(platform_str.lower(), Platform.GENERIC) @staticmethod - def generate_remediation( - platform: str, - running_config: str, - intended_config: str, - tag_rules: list[TagRule] | None = None, - include_tags: list[str] | None = None, - exclude_tags: list[str] | None = None, - ) -> dict[str, Any]: + def generate_remediation(request: GenerateRemediationRequest) -> dict[str, Any]: """Generate remediation and rollback configurations.""" - platform_enum = RemediationService._get_platform(platform) - running_hconfig = get_hconfig(platform_enum, running_config) - intended_hconfig = get_hconfig(platform_enum, intended_config) - - # Load tag rules if provided - if tag_rules: - # Convert tag rules to hier_config format - # This is simplified - actual implementation would need proper tag loading - pass + platform_enum = RemediationService._get_platform(request.platform) + running_hconfig = HConfig.from_text(platform_enum, request.running_config) + intended_hconfig = HConfig.from_text(platform_enum, request.intended_config) # Generate remediation and rollback workflow = WorkflowRemediation(running_hconfig, intended_hconfig) @@ -61,23 +52,16 @@ def generate_remediation( additions=additions, deletions=deletions, modifications=modifications ) - # Apply tag filtering if specified - filtered_remediation = str(remediation) if remediation else "" - if include_tags or exclude_tags: - # Simplified tag filtering - # In a real implementation, you'd filter based on tags - pass + tags: dict[str, list[str]] = {} - result = { - "remediation_config": filtered_remediation, + return { + "remediation_config": str(remediation) if remediation else "", "rollback_config": str(rollback) if rollback else "", "summary": summary, - "tags": {}, - "platform": platform, + "tags": tags, + "platform": request.platform, } - return result - @staticmethod def apply_tags( remediation_config: str, tag_rules: list[TagRule] @@ -105,7 +89,7 @@ def filter_remediation( ) -> tuple[str, RemediationSummary]: """Filter remediation configuration by tags.""" lines = remediation_config.splitlines() - filtered_lines = [] + filtered_lines: list[str] = [] for i, line in enumerate(lines): line_tags = tags.get(str(i), []) diff --git a/hier_config_api/services/report_service.py b/hier_config_api/services/report_service.py index e2b6c27..e742557 100644 --- a/hier_config_api/services/report_service.py +++ b/hier_config_api/services/report_service.py @@ -8,6 +8,7 @@ import yaml +from hier_config_api.models.remediation import GenerateRemediationRequest from hier_config_api.models.report import ChangeDetail, DeviceRemediation, ReportSummary from hier_config_api.services.remediation_service import RemediationService @@ -35,9 +36,11 @@ def create_report(remediations: list[DeviceRemediation]) -> dict[str, Any]: for device_rem in remediations: # Generate remediation for this device remediation_result = RemediationService.generate_remediation( - platform=device_rem.platform, - running_config=device_rem.running_config, - intended_config=device_rem.intended_config, + GenerateRemediationRequest( + platform=device_rem.platform, + running_config=device_rem.running_config, + intended_config=device_rem.intended_config, + ) ) remediation_config = remediation_result["remediation_config"] @@ -123,16 +126,22 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str if format_type == "json": return json.dumps(report_data, indent=2) - elif format_type == "yaml": + if format_type == "yaml": return yaml.dump(report_data, default_flow_style=False) - elif format_type == "csv": + if format_type == "csv": output = io.StringIO() writer = csv.writer(output) # Write header writer.writerow( - ["Device ID", "Platform", "Has Changes", "Change Count", "Remediation Summary"] + [ + "Device ID", + "Platform", + "Has Changes", + "Change Count", + "Remediation Summary", + ] ) # Write device rows @@ -144,7 +153,7 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str device["has_changes"], device["change_count"], ( - device["remediation"][:50] + "..." + f"{device['remediation'][:50]}..." if len(device["remediation"]) > 50 else device["remediation"] ), @@ -153,5 +162,5 @@ def export_report(report_data: dict[str, Any], format_type: str = "json") -> str return output.getvalue() - else: - raise ValueError(f"Unsupported format: {format_type}") + message = f"Unsupported format: {format_type}" + raise ValueError(message) diff --git a/poetry.lock b/poetry.lock index 2cdf02a..5ba35aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-doc" @@ -6,7 +6,7 @@ version = "0.0.4" description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, @@ -18,7 +18,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -46,14 +46,14 @@ trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python [[package]] name = "astroid" -version = "4.0.3" +version = "4.0.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14"}, - {file = "astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3"}, + {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, + {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, ] [package.dependencies] @@ -61,47 +61,34 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - [[package]] name = "backrefs" -version = "6.1" +version = "6.2" description = "A wrapper around re and regex that adds additional back references." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"}, - {file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"}, - {file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"}, - {file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"}, - {file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"}, - {file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"}, - {file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"}, + {file = "backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8"}, + {file = "backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be"}, + {file = "backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90"}, + {file = "backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b"}, + {file = "backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7"}, + {file = "backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7"}, + {file = "backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49"}, ] [package.extras] @@ -109,137 +96,137 @@ extras = ["regex"] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"}, + {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, + {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, ] [[package]] @@ -272,104 +259,118 @@ markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coverage" -version = "7.13.2" +version = "7.13.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b"}, - {file = "coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc"}, - {file = "coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f"}, - {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8"}, - {file = "coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c"}, - {file = "coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99"}, - {file = "coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e"}, - {file = "coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b"}, - {file = "coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1"}, - {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059"}, - {file = "coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031"}, - {file = "coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e"}, - {file = "coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28"}, - {file = "coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d"}, - {file = "coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f"}, - {file = "coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5"}, - {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b"}, - {file = "coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41"}, - {file = "coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e"}, - {file = "coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894"}, - {file = "coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6"}, - {file = "coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9"}, - {file = "coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c"}, - {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31"}, - {file = "coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8"}, - {file = "coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb"}, - {file = "coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557"}, - {file = "coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e"}, - {file = "coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421"}, - {file = "coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f"}, - {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573"}, - {file = "coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343"}, - {file = "coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47"}, - {file = "coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7"}, - {file = "coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef"}, - {file = "coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27"}, - {file = "coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82"}, - {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892"}, - {file = "coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe"}, - {file = "coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859"}, - {file = "coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6"}, - {file = "coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b"}, - {file = "coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d"}, - {file = "coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f"}, - {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f"}, - {file = "coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3"}, - {file = "coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba"}, - {file = "coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c"}, - {file = "coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5"}, - {file = "coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, ] [package.dependencies] @@ -413,6 +414,21 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "fastapi" version = "0.128.0" @@ -436,6 +452,24 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (> standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "flynt" +version = "1.0.6" +description = "CLI tool to convert a python project's %-formatted strings to f-strings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flynt-1.0.6-py3-none-any.whl", hash = "sha256:4e837c9597036b634a347855a89acf1483c4f8b73daa82c49372b10b6e1d1778"}, + {file = "flynt-1.0.6.tar.gz", hash = "sha256:471b7ff00756678e2912d4261dcbcd8fc1395129b66bf6977f88a3b3ad220c90"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["build", "pre-commit", "pytest", "pytest-cov", "ruff", "twine"] + [[package]] name = "ghp-import" version = "2.1.0" @@ -455,20 +489,28 @@ python-dateutil = ">=2.8.1" dev = ["flake8", "markdown", "twine", "wheel"] [[package]] -name = "griffe" -version = "1.15.0" +name = "gprof2dot" +version = "2025.4.14" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, + {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, +] + +[[package]] +name = "griffelib" +version = "2.0.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3"}, - {file = "griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea"}, + {file = "griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f"}, ] -[package.dependencies] -colorama = ">=0.4" - [package.extras] pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] @@ -486,22 +528,18 @@ files = [ [[package]] name = "hier-config" -version = "3.4.0" +version = "4.0.0b1" description = "A network configuration query and comparison library, used to build remediation configurations." optional = false -python-versions = ">=3.10.0,<4.0" +python-versions = "<4.0,>=3.10.0" groups = ["main"] -files = [] -develop = false +files = [ + {file = "hier_config-4.0.0b1-py3-none-any.whl", hash = "sha256:8beb63844c5ad832e03b88e763be7789e20c522aecd3ebde25ad0822ee5b9326"}, + {file = "hier_config-4.0.0b1.tar.gz", hash = "sha256:238da00c357ebc638972398fb65c2473b0f3c99db8dcf3e7b6bb62bfac630cd6"}, +] [package.dependencies] -pydantic = "^2.9" - -[package.source] -type = "git" -url = "https://github.com/netdevops/hier_config.git" -reference = "HEAD" -resolved_reference = "aa119e54d716e6aaedc9035d10e370a14e929ae9" +pydantic = ">=2.9,<3.0" [[package]] name = "httpcore" @@ -630,21 +668,32 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "invoke" +version = "3.0.3" +description = "Pythonic task execution" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, + {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, +] + [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, - {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, ] [package.extras] colors = ["colorama"] -plugins = ["setuptools"] [[package]] name = "jinja2" @@ -666,107 +715,145 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "librt" -version = "0.7.8" +version = "0.8.1" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, - {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, - {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, - {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, - {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, - {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, - {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, - {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, - {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, - {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, - {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, - {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, - {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, - {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, - {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, - {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, - {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, - {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, - {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, - {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, - {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, - {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, - {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, - {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, - {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, - {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, - {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, - {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, - {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, - {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, - {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, - {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, - {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, - {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, - {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, - {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, - {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, - {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, ] [[package]] name = "markdown" -version = "3.10.1" +version = "3.10.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"}, - {file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"}, + {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, + {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, ] [package.extras] docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -878,6 +965,18 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mergedeep" version = "1.3.4" @@ -923,14 +1022,14 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform [[package]] name = "mkdocs-autorefs" -version = "1.4.3" +version = "1.4.4" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"}, - {file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"}, + {file = "mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089"}, + {file = "mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197"}, ] [package.dependencies] @@ -987,14 +1086,14 @@ mkdocs = ">=1.4.1" [[package]] name = "mkdocs-material" -version = "9.7.1" +version = "9.7.4" description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c"}, - {file = "mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8"}, + {file = "mkdocs_material-9.7.4-py3-none-any.whl", hash = "sha256:6549ad95e4d130ed5099759dfa76ea34c593eefdb9c18c97273605518e99cfbf"}, + {file = "mkdocs_material-9.7.4.tar.gz", hash = "sha256:711b0ee63aca9a8c7124d4c73e83a25aa996e27e814767c3a3967df1b9e56f32"}, ] [package.dependencies] @@ -1011,9 +1110,9 @@ pymdown-extensions = ">=10.2" requests = ">=2.30" [package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<12.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] +git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"] +imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"] +recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"] [[package]] name = "mkdocs-material-extensions" @@ -1044,14 +1143,14 @@ mkdocs = ">=1.2" [[package]] name = "mkdocstrings" -version = "1.0.2" +version = "1.0.3" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mkdocstrings-1.0.2-py3-none-any.whl", hash = "sha256:41897815a8026c3634fe5d51472c3a569f92ded0ad8c7a640550873eea3b6817"}, - {file = "mkdocstrings-1.0.2.tar.gz", hash = "sha256:48edd0ccbcb9e30a3121684e165261a9d6af4d63385fc4f39a54a49ac3b32ea8"}, + {file = "mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046"}, + {file = "mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434"}, ] [package.dependencies] @@ -1070,18 +1169,18 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.3" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90"}, - {file = "mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732"}, + {file = "mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12"}, + {file = "mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8"}, ] [package.dependencies] -griffe = ">=1.13" +griffelib = ">=2.0" mkdocs-autorefs = ">=1.4" mkdocstrings = ">=0.30" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} @@ -1160,6 +1259,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + [[package]] name = "packaging" version = "26.0" @@ -1190,14 +1301,14 @@ lint = ["black"] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c"}, - {file = "pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] [package.extras] @@ -1208,21 +1319,16 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, ] -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -1245,7 +1351,7 @@ version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, @@ -1267,7 +1373,7 @@ version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, @@ -1436,14 +1542,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "4.0.4" +version = "4.0.5" description = "python code static checker" optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0"}, - {file = "pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2"}, + {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"}, + {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"}, ] [package.dependencies] @@ -1454,7 +1560,7 @@ dill = [ {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] -isort = ">=5,<5.13 || >5.13,<8" +isort = ">=5,<5.13 || >5.13,<9" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} @@ -1464,16 +1570,47 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pylint-plugin-utils" +version = "0.9.0" +description = "Utilities and helpers for writing Pylint plugins" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +files = [ + {file = "pylint_plugin_utils-0.9.0-py3-none-any.whl", hash = "sha256:16e9b84e5326ba893a319a0323fcc8b4bcc9c71fc654fcabba0605596c673818"}, + {file = "pylint_plugin_utils-0.9.0.tar.gz", hash = "sha256:5468d763878a18d5cc4db46eaffdda14313b043c962a263a7d78151b90132055"}, +] + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pylint-pydantic" +version = "0.4.1" +description = "A Pylint plugin to help Pylint understand the Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pylint_pydantic-0.4.1-py3-none-any.whl", hash = "sha256:d1b937abe5c346d38de69ee1ada80c93d38ee2356addbabb687e2eb44036ac93"}, +] + +[package.dependencies] +pydantic = "<3.0" +pylint = ">2.0,<5.0" +pylint-plugin-utils = "*" + [[package]] name = "pymdown-extensions" -version = "10.20.1" +version = "10.21" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0"}, - {file = "pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a"}, + {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, + {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, ] [package.dependencies] @@ -1483,6 +1620,27 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.19.1)"] +[[package]] +name = "pyright" +version = "1.1.411" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9"}, + {file = "pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" + +[package.extras] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] + [[package]] name = "pytest" version = "9.0.2" @@ -1508,45 +1666,62 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -name = "pytest-asyncio" -version = "1.3.0" -description = "Pytest support for asyncio" +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, - {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, ] [package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." +name = "pytest-profiling" +version = "1.8.1" +description = "Profiling plugin for py.test" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d"}, + {file = "pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea"}, +] + +[package.dependencies] +gprof2dot = "*" +pytest = "*" +six = "*" + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, ] [package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "python-dateutil" @@ -1698,33 +1873,63 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" -version = "0.14.14" +version = "0.16.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed"}, - {file = "ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c"}, - {file = "ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b"}, - {file = "ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167"}, - {file = "ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd"}, - {file = "ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c"}, - {file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"}, + {file = "ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7"}, + {file = "ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081"}, + {file = "ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d"}, + {file = "ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a"}, + {file = "ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948"}, + {file = "ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a"}, + {file = "ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] @@ -1828,6 +2033,24 @@ files = [ {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, ] +[[package]] +name = "typer" +version = "0.27.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56"}, + {file = "typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df"}, +] + +[package.dependencies] +annotated-doc = ">=0.0.2" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +rich = ">=13.8.0" +shellingham = ">=1.3.0" + [[package]] name = "types-pyyaml" version = "6.0.12.20250915" @@ -1858,7 +2081,7 @@ version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -2213,7 +2436,26 @@ files = [ {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, ] +[[package]] +name = "yamllint" +version = "1.38.0" +description = "A linter for YAML files." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220"}, + {file = "yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d"}, +] + +[package.dependencies] +pathspec = ">=1.0.0" +pyyaml = "*" + +[package.extras] +dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "ruff", "sphinx"] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "58fe3f1249d51a986bbce23d9aabc53a97a5a673250c30bd66f292268806d7cc" +content-hash = "d6def5174ede90e4e2d205b0b0181e656c5741112e458c54385fca6afdb2788d" diff --git a/pyproject.toml b/pyproject.toml index 02112d1..0c72ed1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "uvicorn[standard] (>=0.40.0,<0.41.0)", "pydantic (>=2.12.5,<3.0.0)", "pydantic-settings (>=2.12.0,<3.0.0)", - "hier-config @ git+https://github.com/netdevops/hier_config.git" + "hier-config (>=4.0.0b1,<5.0.0)" ] @@ -22,91 +22,130 @@ build-backend = "poetry.core.masonry.api" [dependency-groups] dev = [ - "pytest (>=9.0.2,<10.0.0)", - "pytest-asyncio (>=1.3.0,<2.0.0)", - "pytest-cov (>=7.0.0,<8.0.0)", + "flynt (>=1.0.0,<2.0.0)", "httpx (>=0.28.1,<0.29.0)", - "ruff (>=0.14.14,<0.15.0)", - "mypy (>=1.19.1,<2.0.0)", - "pylint (>=4.0.4,<5.0.0)", - "types-pyyaml (>=6.0.12.20250915,<7.0.0.0)", "mkdocs (>=1.6.1,<2.0.0)", "mkdocs-material (>=9.7.1,<10.0.0)", "mkdocstrings[python] (>=1.0.2,<2.0.0)", "mkdocs-gen-files (>=0.6.0,<0.7.0)", "mkdocs-literate-nav (>=0.6.2,<0.7.0)", - "mkdocs-section-index (>=0.3.10,<0.4.0)" + "mkdocs-section-index (>=0.3.10,<0.4.0)", + "mypy (>=1.19.1,<2.0.0)", + "pylint (>=4.0.4,<5.0.0)", + "pylint-pydantic (>=0.4.1,<0.5.0)", + "pyright (>=1.1.390,<2.0.0)", + "pytest (>=9.0.2,<10.0.0)", + "pytest-cov (>=7.0.0,<8.0.0)", + "pytest-xdist (>=3.6.1,<4.0.0)", + "ruff (>=0.16.1,<0.17.0)", + "typer (>=0.15.1,<1.0.0)", + "types-pyyaml (>=6.0.12.20250915,<7.0.0.0)", + "yamllint (>=1.35.1,<2.0.0)", + "invoke (>=3.0.3,<4.0.0)", + "pyyaml (>=6.0.3,<7.0.0)", + "pytest-profiling (>=1.8.1,<2.0.0)" ] -[tool.ruff] -line-length = 100 -target-version = "py310" -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade +[tool.pyright] +typeCheckingMode = "strict" + + +[tool.pylint.message_control] +load-plugins = [ + "pylint.extensions.bad_builtin", + "pylint.extensions.broad_try_clause", + "pylint.extensions.check_elif", + "pylint.extensions.comparison_placement", + "pylint.extensions.confusing_elif", + "pylint.extensions.consider_ternary_expression", + "pylint.extensions.dict_init_mutate", + "pylint.extensions.dunder", + "pylint.extensions.eq_without_hash", + "pylint.extensions.for_any_all", + "pylint.extensions.overlapping_exceptions", + "pylint.extensions.redefined_loop_name", + "pylint.extensions.set_membership", + "pylint.extensions.typing", + "pylint_pydantic", ] -ignore = [ - "E501", # line too long, handled by formatter - "B008", # do not perform function calls in argument defaults - "C901", # too complex +disable = [ + "consider-alternative-union-syntax", + "duplicate-code", # Enable this at some point in the future + "fixme", # Covered by ruff FIX002 + "import-outside-toplevel", # Covered by ruff PLC0415 + "line-too-long", # Whatever ruff format determines the line should look like is fine + "missing-function-docstring", + "missing-module-docstring", + "protected-access", # Covered by ruff SLF001 + "redefined-loop-name", # Covered by ruff PLW2901 + "too-many-arguments", # Covered by ruff PLR0913 + "too-many-return-statements", # Covered by ruff PLR0911 + "too-many-locals", # Covered by ruff PLR0914 + "too-many-public-methods", # Covered by ruff PLR0904 ] -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401"] [tool.mypy] -python_version = "3.10" strict = true -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_any_generics = false -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true - -[[tool.mypy.overrides]] -module = "hier_config.*" -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "hier_config" -ignore_missing_imports = true - -[tool.pylint.main] -max-line-length = 100 -disable = [ - "C0111", # missing-docstring - "C0103", # invalid-name - "R0903", # too-few-public-methods - "R0913", # too-many-arguments +pretty = true +show_column_numbers = true +show_error_codes = true +show_error_context = true +warn_unreachable = true +plugins = [ + "pydantic.mypy" +] + + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = ["ALL"] +preview = true +ignore = [ + # The below checks we might never enable + "B019", # using lru_cache can cause memory leaks + "COM812", # DO NOT REMOVE: conflicts with ruff formater + "CPY001", # Missing copyright notice at top of file + "D213", # DO NOT REMOVE: multi-line-summary-second-line is incompatible with D212(multi-line-summary-first-line) + "E501", # line too long + "EXE002", # DO NOT REMOVE: The file is executable but no shebang is present + "ISC001", # DO NOT REMOVE: The following rule may cause conflicts when used with the formatter + "PLR2004", # Magic value used in comparison + # The below checks should eventually be enabled + "D100", # Missing docstring in public module + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "D200", # One-line docstring should fit on one line + "D203", # 1 blank line required before class docstring - Incompatible with D211 + "D205", # 1 blank line required between summary line and description + "D401", # First line of docstring should be in imperative mood + "DOC201", # `return` is not documented in docstring + "DOC402", # `yield` is not documented in docstring + "DOC501", # Raised exception missing from docstring ] +[tool.ruff.lint.flake8-pytest-style] +parametrize-values-type = "tuple" + +[tool.ruff.lint.per-file-ignores] +"**/tests/*" = ["PLC2701", "S101"] + [tool.pytest.ini_options] -asyncio_mode = "auto" testpaths = ["tests"] -python_files = ["test_*.py"] -python_classes = ["Test*"] -python_functions = ["test_*"] -addopts = "--cov=hier_config_api --cov-report=term-missing --cov-report=html" - -[tool.coverage.run] -source = ["hier_config_api"] -omit = ["*/tests/*", "*/__init__.py"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", - "def __repr__", - "raise AssertionError", - "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] diff --git a/scripts/build.py b/scripts/build.py new file mode 100755 index 0000000..df5185c --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import subprocess # ruff:ignore[suspicious-subprocess-import] +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from functools import cache +from pathlib import Path +from typing import TYPE_CHECKING, NoReturn + +from typer import Typer + +if TYPE_CHECKING: + from collections.abc import Iterable + +app = Typer() + + +@app.callback() +def callback() -> None: + """Build tools.""" + + +@app.command() +def lint(*, fix: bool = False) -> None: + """Run all linters and type checkers in order of importance - returns the first non-zero exit code or zero.""" + _run_commands_threaded( + ( + _ruff_format_command(fix=fix), + _ruff_check_command(fix=fix, unsafe_fixes=False, statistics=False), + _mypy_command(), + _pyright_command(), + _pylint_command(), + _yamllint_command(), + _flynt_command(fix=fix), + ), + ) + + +@app.command() +def lint_and_test(*, fix: bool = False) -> None: + """Run all code fixs in order of importance - returns the first non-zero exit code or zero.""" + _run_commands_threaded( + ( + _ruff_format_command(fix=fix), + _ruff_check_command(fix=fix, unsafe_fixes=False, statistics=False), + _mypy_command(), + _pyright_command(), + _pylint_command(), + _pytest_command(), + _yamllint_command(), + _flynt_command(fix=fix), + ), + ) + + +@app.command() +def ruff_format(*, fix: bool = False) -> None: + """Run Ruff linter.""" + _run(_ruff_format_command(fix=fix)) + + +def _ruff_format_command(*, fix: bool) -> str: + return f"ruff format {'' if fix else '--check '}{_python_base_paths_str()}" + + +@app.command() +def ruff_check( + *, + fix: bool = False, + unsafe_fixes: bool = False, + statistics: bool = False, +) -> None: + """Run Ruff linter.""" + _run(_ruff_check_command(fix=fix, unsafe_fixes=unsafe_fixes, statistics=statistics)) + + +def _ruff_check_command(*, fix: bool, unsafe_fixes: bool, statistics: bool) -> str: + return f"ruff check --output-format=concise {'--fix ' if fix else ''}{'--unsafe-fixes ' if unsafe_fixes else ''}{'--statistics ' if statistics else ''}{_python_base_paths_str()}" + + +@app.command() +def pytest( + *, + profile: bool = False, + coverage: bool = True, + threaded: bool = False, +) -> None: + """Run pytest unittests.""" + _run( + _pytest_command( + profile=profile, + coverage=coverage, + threaded=threaded, + ), + environment={"COVERAGE_CORE": "sysmon"}, + ) + + +def _pytest_command( + *, + profile: bool = False, + coverage: bool = True, + threaded: bool = False, +) -> str: + command = "pytest" + if profile: + command += " --profile --profile-svg" + if coverage: + command += ( + " --cov=hier_config_api --cov-fail-under=95 --cov-report=term-missing" + ) + if threaded: + command += " -n auto" + return command + + +@app.command() +def yamllint() -> None: + """Run yamllint to check YAML syntax.""" + _run(_yamllint_command()) + + +def _yamllint_command() -> str: + return f"yamllint {_repo_path().relative_to(Path.cwd())}" + + +@app.command() +def pylint() -> None: + """Run pylint linter.""" + _run(_pylint_command()) + + +def _pylint_command() -> str: + return f"pylint {_python_base_paths_str()}" + + +@app.command() +def mypy() -> None: + """Run mypy type checker.""" + _run(_mypy_command()) + + +def _mypy_command() -> str: + return f"mypy {_python_base_paths_str()}" + + +@app.command() +def pyright() -> None: + """Run pyright type checker.""" + _run(_pyright_command()) + + +def _pyright_command() -> str: + return f"pyright {_python_base_paths_str()}" + + +@app.command() +def flynt(*, fix: bool = False) -> None: + """Run flynt to enforce the use of f-strings.""" + _run(_flynt_command(fix=fix)) + + +def _flynt_command(*, fix: bool) -> str: + if fix: + return f"flynt -tc {_python_base_paths_str()}" + return f"flynt -d -tc -f {_python_base_paths_str()}" + + +@cache +def _python_base_paths_str() -> str: + return " ".join(str(p) for p in _python_base_paths()) + + +def _python_base_paths() -> Iterable[Path]: + yield from (f.relative_to(Path.cwd()) for f in _project_base_paths("*.py")) + + +def _project_base_paths(glob: str) -> Iterable[Path]: + yield from _project_paths(glob) + yield from _project_base_files(glob) + + +def _project_base_files(glob: str) -> Iterable[Path]: + yield from _repo_path().glob(glob) + + +def _project_paths(glob: str) -> Iterable[Path]: + for base_dir in ("hier_config_api", "tests", "scripts"): + base_path = _repo_path().joinpath(base_dir) + if not base_path.exists(): + message = f"{base_path=} does not exist" + raise FileNotFoundError(message) + + if next(base_path.glob(glob), None): + yield base_path + + +def _run_commands_threaded(commands: tuple[str, ...]) -> NoReturn: + return_codes: dict[str, int] = {} + with ThreadPoolExecutor(max_workers=len(commands)) as executor: + for future in as_completed( + executor.submit( + _run_for_thread, + command, + ) + for command in commands + ): + command, return_code, output = future.result() + if return_code: + print(output) # ruff:ignore[print] + return_codes[command] = return_code + + error_found = False + for command, return_code in return_codes.items(): + if return_code != 0: + print(f"{command.split()[0]} -> {return_code}") # ruff:ignore[print] + error_found = True + if error_found: + sys.exit(1) + print("No issues found") # ruff:ignore[print] + sys.exit() + + +def _run( + command: str, + *, + check: bool = True, + environment: dict[str, str] | None = None, +) -> int: + print(f"\n======== {command} ========\n") # ruff:ignore[print] + my_env = os.environ.copy() + if environment: + my_env.update(environment) + result = subprocess.run(command.split(), check=False, env=my_env) # ruff:ignore[subprocess-without-shell-equals-true] + if check: + sys.exit(result.returncode) + return result.returncode + + +def _run_for_thread(command: str) -> tuple[str, int, str]: + print(f"Running: {command}") # ruff:ignore[print] + result = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true] + command.split(), + check=False, + capture_output=True, + ) + output = f"\n======== {command} ========\n{result.stdout.decode()}\n{result.stderr.decode()}" + return command, result.returncode, output + + +@cache +def _repo_path() -> Path: + return Path(__file__).parents[1] + + +def main() -> None: + app() + + +if __name__ == "__main__": + main() diff --git a/scripts/sync_standards.py b/scripts/sync_standards.py new file mode 100755 index 0000000..536c8f0 --- /dev/null +++ b/scripts/sync_standards.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Sync shared development-standard files from the canonical netdevops repository. + +The `.standards.yml` manifest at the repository root declares the canonical +source repository and the files it owns. `check` reports drift between the +local copies and the canonical versions; `apply` overwrites the local copies +with the canonical versions. + +Package-name substitutions can push a canonical line past the formatter's +line-length limit, so Python files are re-formatted with `ruff format` after +substitution. Without this the synced file would never converge: `apply` +would write a file that `ruff format` immediately rewrites, and the next +`check` would report drift again. +""" + +from __future__ import annotations + +import difflib +import re +import shutil +import subprocess # ruff: ignore[suspicious-subprocess-import] +import sys +from http import HTTPStatus +from pathlib import Path + +import httpx +import yaml +from pydantic import BaseModel, Field +from typer import Typer + +app = Typer() + +_REPO_ROOT = Path(__file__).parent.parent +_MANIFEST_PATH = _REPO_ROOT / ".standards.yml" + + +class Source(BaseModel): + """Canonical repository holding the standard files.""" + + repo: str + ref: str + + +class Manifest(BaseModel): + """Parsed representation of the `.standards.yml` manifest.""" + + source: Source + substitutions: dict[str, str] = Field(default_factory=dict) + files: tuple[str, ...] + + +@app.callback() +def callback() -> None: + """Sync shared development standards from the canonical repository.""" + + +@app.command() +def check() -> None: + """Report drift between local files and the canonical standards.""" + if _sync(write=False): + sys.exit(1) + + +@app.command() +def apply() -> None: + """Overwrite local files with the canonical standards.""" + _sync(write=True) + + +def _sync(*, write: bool) -> list[str]: + manifest = _load_manifest() + drifted: list[str] = [] + for file_path in manifest.files: + canonical = _canonical_content(manifest, file_path) + if canonical is None: + source = manifest.source + print( # ruff: ignore[print] + f"{file_path}: not published on {source.repo}@{source.ref} — " + "the manifest lists a file the canonical source does not have yet", + ) + drifted.append(file_path) + continue + local_path = _REPO_ROOT / file_path + local = local_path.read_text(encoding="utf-8") if local_path.is_file() else None + if local == canonical: + print(f"{file_path}: in sync") # ruff: ignore[print] + continue + drifted.append(file_path) + if write: + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.write_text(canonical, encoding="utf-8") + print(f"{file_path}: updated from canonical") # ruff: ignore[print] + else: + print(f"{file_path}: drifted from canonical") # ruff: ignore[print] + _print_diff(file_path, local or "", canonical) + return drifted + + +def _load_manifest() -> Manifest: + data = yaml.safe_load(_MANIFEST_PATH.read_text(encoding="utf-8")) + return Manifest.model_validate(data) + + +def _fetch(source: Source, file_path: str) -> str | None: + """Return the canonical file contents, or None when the source lacks the file.""" + url = f"https://raw.githubusercontent.com/{source.repo}/{source.ref}/{file_path}" + response = httpx.get(url, timeout=30.0, follow_redirects=True) + if response.status_code == HTTPStatus.NOT_FOUND: + return None + response.raise_for_status() + return response.text + + +def _canonical_content(manifest: Manifest, file_path: str) -> str | None: + fetched = _fetch(manifest.source, file_path) + if fetched is None: + return None + content = _apply_substitutions(fetched, manifest.substitutions) + if file_path.endswith(".py"): + content = _ruff_format(content, file_path) + return content + + +def _apply_substitutions(content: str, substitutions: dict[str, str]) -> str: + for old, new in substitutions.items(): + content = re.sub(rf"\b{re.escape(old)}\b", new, content) + return content + + +def _ruff_format(content: str, file_path: str) -> str: + ruff = shutil.which("ruff") + if ruff is None: + message = "ruff is not installed; run this from the dev environment" + raise RuntimeError(message) + result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] + [ruff, "format", "--stdin-filename", file_path, "-"], + check=True, + capture_output=True, + text=True, + input=content, + ) + return result.stdout + + +def _print_diff(file_path: str, local: str, canonical: str) -> None: + diff = difflib.unified_diff( + local.splitlines(keepends=True), + canonical.splitlines(keepends=True), + fromfile=f"local/{file_path}", + tofile=f"canonical/{file_path}", + ) + sys.stdout.writelines(diff) + + +if __name__ == "__main__": + app() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..0309591 --- /dev/null +++ b/tasks.py @@ -0,0 +1,78 @@ +"""Invoke tasks for the Docker development environment.""" + +from typing import TYPE_CHECKING + +from invoke.context import Context + +if TYPE_CHECKING: + from collections.abc import Callable + + # Typed stand-in for invoke's partially typed task decorator + def task(_func: Callable[..., None]) -> Callable[..., None]: ... + +else: + from invoke.tasks import task + + +@task +def build(context: Context) -> None: + """Build the Docker development image.""" + context.run("docker compose build", pty=True) + + +@task +def serve(context: Context) -> None: + """Run the API with hot reload at http://localhost:8000.""" + context.run("docker compose up api", pty=True) + + +@task +def docs(context: Context) -> None: + """Serve the documentation with live reload at http://localhost:8001.""" + context.run("docker compose --profile docs up docs", pty=True) + + +@task +def pytest(context: Context, *, coverage: bool = False) -> None: + """Run the test suite inside the development container.""" + command = "python scripts/build.py pytest --coverage" if coverage else "pytest" + context.run(f"docker compose run --rm api {command}", pty=True) + + +@task +def lint(context: Context, *, fix: bool = False) -> None: + """Run all linters and type checkers inside the development container.""" + context.run( + f"docker compose run --rm api python scripts/build.py lint{' --fix' if fix else ''}", + pty=True, + ) + + +@task +def lint_and_test(context: Context) -> None: + """Run the full lint + test suite (what CI runs) inside the development container.""" + context.run( + "docker compose run --rm api python scripts/build.py lint-and-test", pty=True + ) + + +@task +def cli(context: Context) -> None: + """Open a shell inside the development container.""" + context.run("docker compose run --rm api bash", pty=True) + + +@task +def sync_standards(context: Context, *, apply: bool = False) -> None: + """Check drift against the canonical netdevops standards (--apply to update).""" + action = "apply" if apply else "check" + context.run( + f"docker compose run --rm api python scripts/sync_standards.py {action}", + pty=True, + ) + + +@task +def destroy(context: Context) -> None: + """Stop and remove the development containers.""" + context.run("docker compose --profile docs down --remove-orphans", pty=True) diff --git a/tests/test_configs.py b/tests/test_configs.py index 647a056..d99efa3 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -16,7 +16,9 @@ def test_parse_config(client: TestClient, sample_cisco_ios_config: str) -> None: def test_compare_configs( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test comparing configurations.""" response = client.post( diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..21fc936 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,231 @@ +"""Tests for API error handling paths.""" + +from typing import NoReturn + +import pytest +from fastapi.testclient import TestClient +from hier_config import HConfig + + +def _raise_parse_error(*_args: object, **_kwargs: object) -> NoReturn: + """Stand-in for HConfig.from_text that always fails.""" + message = "simulated parse failure" + raise ValueError(message) + + +def test_parse_config_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a parsing failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/parse", + json={"platform": "cisco_ios", "config_text": "hostname router1"}, + ) + assert response.status_code == 400 + assert "Failed to parse config" in response.json()["detail"] + + +def test_compare_configs_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a comparison failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/compare", + json={ + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + }, + ) + assert response.status_code == 400 + assert "Failed to compare configs" in response.json()["detail"] + + +def test_merge_configs_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a merge failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/configs/merge", + json={"platform": "cisco_ios", "configs": ["hostname a", "hostname b"]}, + ) + assert response.status_code == 400 + assert "Failed to merge configs" in response.json()["detail"] + + +def test_search_config_invalid_regex(client: TestClient) -> None: + """Test that an invalid regex pattern returns a 400 error.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1", + "match_rules": {"regex": "["}, + }, + ) + assert response.status_code == 400 + assert "Failed to search config" in response.json()["detail"] + + +def test_generate_remediation_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a remediation generation failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/remediation/generate", + json={ + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + }, + ) + assert response.status_code == 400 + assert "Failed to generate remediation" in response.json()["detail"] + + +def test_apply_tags_unknown_remediation(client: TestClient) -> None: + """Test that applying tags to an unknown remediation returns 404.""" + response = client.post( + "/api/v1/remediation/unknown-id/tags", + json={"tag_rules": [{"match_rules": ["interface"], "tags": ["intf"]}]}, + ) + assert response.status_code == 404 + + +def test_filter_unknown_remediation(client: TestClient) -> None: + """Test that filtering an unknown remediation returns 404.""" + response = client.get("/api/v1/remediation/unknown-id/filter") + assert response.status_code == 404 + + +def test_create_report_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that a report creation failure returns a 400 error.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": "hostname a", + "intended_config": "hostname b", + } + ] + }, + ) + assert response.status_code == 400 + assert "Failed to create report" in response.json()["detail"] + + +def test_get_summary_unknown_report(client: TestClient) -> None: + """Test that requesting a summary for an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/summary") + assert response.status_code == 404 + + +def test_get_changes_unknown_report(client: TestClient) -> None: + """Test that requesting changes for an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/changes") + assert response.status_code == 404 + + +def test_export_unknown_report(client: TestClient) -> None: + """Test that exporting an unknown report returns 404.""" + response = client.get("/api/v1/reports/unknown-id/export") + assert response.status_code == 404 + + +def test_export_report_invalid_format( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test that exporting a report with an unsupported format returns 400.""" + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + response = client.get(f"/api/v1/reports/{report_id}/export?format=xml") + assert response.status_code == 400 + + +def test_get_status_unknown_batch_job(client: TestClient) -> None: + """Test that requesting the status of an unknown batch job returns 404.""" + response = client.get("/api/v1/batch/jobs/unknown-id") + assert response.status_code == 404 + + +def test_get_results_unknown_batch_job(client: TestClient) -> None: + """Test that requesting the results of an unknown batch job returns 404.""" + response = client.get("/api/v1/batch/jobs/unknown-id/results") + assert response.status_code == 404 + + +def test_validate_config_parse_error( + client: TestClient, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that validation reports parsing failures as validation errors.""" + monkeypatch.setattr(HConfig, "from_text", _raise_parse_error) + response = client.post( + "/api/v1/platforms/cisco_ios/validate", + json={"config_text": "hostname router1"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["is_valid"] is False + assert any("parsing error" in error for error in data["errors"]) + + +def test_batch_job_with_failing_device( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test that a batch job records per-device failures without aborting.""" + response = client.post( + "/api/v1/batch/remediation", + json={ + "device_configs": [ + { + "device_id": "good-router", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + }, + { + "device_id": "bad-router", + "platform": "cisco_ios", + "running_config": 123, + "intended_config": 456, + }, + ] + }, + ) + assert response.status_code == 200 + job_id = response.json()["job_id"] + + results_response = client.get(f"/api/v1/batch/jobs/{job_id}/results") + assert results_response.status_code == 200 + data = results_response.json() + statuses = {result["device_id"]: result["status"] for result in data["results"]} + assert statuses == {"good-router": "success", "bad-router": "failed"} + assert data["summary"]["failed_devices"] == 1 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..c2dfebd --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,19 @@ +"""Tests for the application root endpoints.""" + +from fastapi.testclient import TestClient + + +def test_root(client: TestClient) -> None: + """Test the root endpoint.""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Hier-Config API" + assert data["docs"] == "/api/docs" + + +def test_health(client: TestClient) -> None: + """Test the health check endpoint.""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} diff --git a/tests/test_platforms.py b/tests/test_platforms.py index 2c78d4d..560ff73 100644 --- a/tests/test_platforms.py +++ b/tests/test_platforms.py @@ -7,7 +7,7 @@ def test_list_platforms(client: TestClient) -> None: """Test listing all platforms.""" response = client.get("/api/v1/platforms") assert response.status_code == 200 - data = response.json() + data: list[dict[str, str]] = response.json() assert isinstance(data, list) assert len(data) > 0 # Check that cisco_ios is in the list @@ -28,7 +28,8 @@ def test_get_platform_rules(client: TestClient) -> None: def test_validate_config(client: TestClient, sample_cisco_ios_config: str) -> None: """Test validating configuration.""" response = client.post( - "/api/v1/platforms/cisco_ios/validate", json={"config_text": sample_cisco_ios_config} + "/api/v1/platforms/cisco_ios/validate", + json={"config_text": sample_cisco_ios_config}, ) assert response.status_code == 200 data = response.json() @@ -40,7 +41,9 @@ def test_validate_config(client: TestClient, sample_cisco_ios_config: str) -> No def test_validate_empty_config(client: TestClient) -> None: """Test validating empty configuration.""" - response = client.post("/api/v1/platforms/cisco_ios/validate", json={"config_text": ""}) + response = client.post( + "/api/v1/platforms/cisco_ios/validate", json={"config_text": ""} + ) assert response.status_code == 200 data = response.json() assert data["platform"] == "cisco_ios" @@ -49,7 +52,9 @@ def test_validate_empty_config(client: TestClient) -> None: def test_create_batch_job( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test creating a batch remediation job.""" response = client.post( @@ -78,7 +83,9 @@ def test_create_batch_job( def test_get_batch_job_status( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting batch job status.""" # First, create a batch job @@ -108,7 +115,9 @@ def test_get_batch_job_status( def test_get_batch_job_results( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting batch job results.""" # First, create a batch job diff --git a/tests/test_remediation.py b/tests/test_remediation.py index 9ad3d32..5b04bba 100644 --- a/tests/test_remediation.py +++ b/tests/test_remediation.py @@ -4,7 +4,9 @@ def test_generate_remediation( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test generating remediation.""" response = client.post( @@ -26,7 +28,9 @@ def test_generate_remediation( def test_generate_remediation_with_tags( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test generating remediation with tag rules.""" response = client.post( @@ -46,7 +50,9 @@ def test_generate_remediation_with_tags( def test_apply_tags( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test applying tags to remediation.""" # First, generate a remediation @@ -73,7 +79,9 @@ def test_apply_tags( def test_filter_remediation( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test filtering remediation by tags.""" # First, generate a remediation @@ -90,7 +98,8 @@ def test_filter_remediation( # Filter remediation response = client.get( - f"/api/v1/remediation/{remediation_id}/filter", params={"include_tags": ["safe"]} + f"/api/v1/remediation/{remediation_id}/filter", + params={"include_tags": ["safe"]}, ) assert response.status_code == 200 data = response.json() diff --git a/tests/test_reports.py b/tests/test_reports.py index e0d9d90..badf4d2 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -4,7 +4,9 @@ def test_create_report( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test creating a multi-device report.""" response = client.post( @@ -33,7 +35,9 @@ def test_create_report( def test_get_report_summary( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting report summary.""" # First, create a report @@ -63,7 +67,9 @@ def test_get_report_summary( def test_get_report_changes( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test getting detailed change analysis.""" # First, create a report @@ -93,7 +99,9 @@ def test_get_report_changes( def test_export_report_json( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test exporting report as JSON.""" # First, create a report @@ -120,7 +128,9 @@ def test_export_report_json( def test_export_report_csv( - client: TestClient, sample_cisco_ios_config: str, sample_cisco_ios_intended_config: str + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, ) -> None: """Test exporting report as CSV.""" # First, create a report @@ -144,3 +154,61 @@ def test_export_report_csv( response = client.get(f"/api/v1/reports/{report_id}/export?format=csv") assert response.status_code == 200 assert "Device ID" in response.text + + +def test_export_report_yaml( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test exporting report as YAML.""" + # First, create a report + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + # Export as YAML + response = client.get(f"/api/v1/reports/{report_id}/export?format=yaml") + assert response.status_code == 200 + assert "total_devices" in response.text + + +def test_get_report_changes_with_tag_filter( + client: TestClient, + sample_cisco_ios_config: str, + sample_cisco_ios_intended_config: str, +) -> None: + """Test filtering report changes by tag.""" + # First, create a report + create_response = client.post( + "/api/v1/reports", + json={ + "remediations": [ + { + "device_id": "router1", + "platform": "cisco_ios", + "running_config": sample_cisco_ios_config, + "intended_config": sample_cisco_ios_intended_config, + } + ] + }, + ) + assert create_response.status_code == 200 + report_id = create_response.json()["report_id"] + + # No changes carry tags, so filtering by a tag yields no changes + response = client.get(f"/api/v1/reports/{report_id}/changes?tag=missing-tag") + assert response.status_code == 200 + assert response.json()["total_unique_changes"] == 0 diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..0c0c1c8 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,97 @@ +"""Unit tests for service and utility edge cases.""" + +import pytest +from fastapi.testclient import TestClient + +from hier_config_api.models.platform import PlatformRules +from hier_config_api.services.config_service import ConfigService +from hier_config_api.services.remediation_service import RemediationService +from hier_config_api.services.report_service import ReportService +from hier_config_api.utils.storage import InMemoryStorage + + +def test_platform_rules_defaults() -> None: + """Test that PlatformRules fields default to empty collections.""" + rules = PlatformRules(platform_name="cisco_ios") + assert not rules.ordering + assert not rules.negation_default_when + + +def test_storage_update_unknown_job() -> None: + """Test that updating an unknown job returns False.""" + storage = InMemoryStorage() + assert storage.update_job("unknown-id", {"status": "completed"}) is False + + +def test_storage_update_unknown_remediation() -> None: + """Test that updating an unknown remediation returns False.""" + storage = InMemoryStorage() + assert storage.update_remediation("unknown-id", {"tags": {}}) is False + + +def test_merge_configs_empty() -> None: + """Test that merging an empty config list returns an empty string.""" + assert not ConfigService.merge_configs("cisco_ios", []) + + +def test_merge_configs_single() -> None: + """Test that merging a single config returns it unchanged.""" + assert ConfigService.merge_configs("cisco_ios", ["hostname a"]) == "hostname a" + + +def test_filter_remediation_include_tags() -> None: + """Test filtering remediation lines by include tags.""" + filtered_config, summary = RemediationService.filter_remediation( + "line one\nline two", + {"0": ["keep"]}, + include_tags=["keep"], + ) + assert filtered_config == "line one" + assert summary.additions == 1 + + +def test_filter_remediation_exclude_tags() -> None: + """Test filtering remediation lines by exclude tags.""" + filtered_config, summary = RemediationService.filter_remediation( + "line one\nline two", + {"0": ["drop"]}, + exclude_tags=["drop"], + ) + assert filtered_config == "line two" + assert summary.additions == 1 + + +def test_export_report_unsupported_format() -> None: + """Test that exporting with an unsupported format raises ValueError.""" + with pytest.raises(ValueError, match="Unsupported format"): + ReportService.export_report({"devices": []}, format_type="xml") + + +def test_search_config_contains(client: TestClient) -> None: + """Test searching configuration lines with a contains rule.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1\ninterface GigabitEthernet0/0", + "match_rules": {"contains": "Gigabit"}, + }, + ) + assert response.status_code == 200 + matches = response.json()["matches"] + assert "interface GigabitEthernet0/0" in matches + + +def test_search_config_startswith(client: TestClient) -> None: + """Test searching configuration lines with a startswith rule.""" + response = client.post( + "/api/v1/configs/search", + json={ + "platform": "cisco_ios", + "config_text": "hostname router1\ninterface GigabitEthernet0/0", + "match_rules": {"startswith": "hostname"}, + }, + ) + assert response.status_code == 200 + matches = response.json()["matches"] + assert "hostname router1" in matches