From 70e1fc2843c8f12fcff221ebcf8d4d640b37ce4e Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 5 Sep 2026 22:48:52 +0200 Subject: [PATCH 1/3] chore(deps): track stable internal releases and test Python compatibility Co-authored-by: Koru Agent --- .github/dependabot.yml | 31 +++++++ .github/internal-dependencies.json | 35 ++++++++ .../internal-dependency-freshness.yml | 36 ++++++++ .github/workflows/test-locked.yml | 30 +++++++ README.md | 4 + docs/dependencies.md | 27 ++++++ pyproject.toml | 7 +- uv.lock | 89 +++++++++++-------- 8 files changed, 223 insertions(+), 36 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/internal-dependencies.json create mode 100644 .github/workflows/internal-dependency-freshness.yml create mode 100644 .github/workflows/test-locked.yml create mode 100644 docs/dependencies.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fbb9cf8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: +- package-ecosystem: uv + directory: / + schedule: + interval: cron + cronjob: 35 4 * * * + timezone: Europe/Warsaw + open-pull-requests-limit: 1 + versioning-strategy: increase-if-necessary + allow: + - dependency-name: costs + dependency-type: all + - dependency-name: goal + dependency-type: all + - dependency-name: pfix + dependency-type: all + - dependency-name: clickmd + dependency-type: all + - dependency-name: code2llm + dependency-type: all + groups: + internal-packages: + patterns: + - costs + - goal + - pfix + - clickmd + - code2llm + commit-message: + prefix: "chore(deps)" diff --git a/.github/internal-dependencies.json b/.github/internal-dependencies.json new file mode 100644 index 0000000..ce582b0 --- /dev/null +++ b/.github/internal-dependencies.json @@ -0,0 +1,35 @@ +{ + "schema": "goal.internal-dependencies/v1", + "packages": [ + { + "name": "costs", + "repository": "semcod/costs", + "registry": "pypi", + "versioning": "semver" + }, + { + "name": "goal", + "repository": "semcod/goal", + "registry": "pypi", + "versioning": "semver" + }, + { + "name": "pfix", + "repository": "semcod/pfix", + "registry": "pypi", + "versioning": "semver" + }, + { + "name": "clickmd", + "repository": "semcod/clickmd", + "registry": "pypi", + "versioning": "semver" + }, + { + "name": "code2llm", + "repository": "semcod/code2llm", + "registry": "pypi", + "versioning": "semver" + } + ] +} diff --git a/.github/workflows/internal-dependency-freshness.yml b/.github/workflows/internal-dependency-freshness.yml new file mode 100644 index 0000000..d64bfc8 --- /dev/null +++ b/.github/workflows/internal-dependency-freshness.yml @@ -0,0 +1,36 @@ +name: Internal dependency freshness + +on: + schedule: + - cron: "47 5 * * *" + workflow_dispatch: + pull_request: + paths: + - pyproject.toml + - uv.lock + - .github/internal-dependencies.json + - .github/workflows/internal-dependency-freshness.yml + +permissions: + contents: read + +jobs: + freshness: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - name: Install released checker + run: python -m pip install "goal==2.2.0" + - name: Verify published stable targets + run: goal dependencies --catalog .github/internal-dependencies.json --check > dependency-freshness.json + - name: Retain freshness evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: dependency-freshness + path: dependency-freshness.json + if-no-files-found: ignore diff --git a/.github/workflows/test-locked.yml b/.github/workflows/test-locked.yml new file mode 100644 index 0000000..c42bf24 --- /dev/null +++ b/.github/workflows/test-locked.yml @@ -0,0 +1,30 @@ +name: Locked tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.13"] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install uv + - name: Install locked test dependencies + run: uv sync --locked --extra dev --python "${{ matrix.python-version }}" + - name: Test supported Python versions + run: uv run --no-sync python -m pytest -q diff --git a/README.md b/README.md index b7a69dc..c1314c9 100644 --- a/README.md +++ b/README.md @@ -370,3 +370,7 @@ PYTHONPATH=src python -m unittest discover -s tests -v ## License Licensed under Apache-2.0. + +## Dependency maintenance + +See [internal dependency updates](docs/dependencies.md) for daily updates, locked tests and Python support. diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 0000000..7d132f5 --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,27 @@ +# Internal dependency updates + +This repository owns its dependency configuration and this guide, following the repository ownership principle in [wellmanifest/docs](https://github.com/wellmanifest/docs/blob/f64de5806577769672ebc1730d2e144b4c7671ec/README.md). The checker belongs to [semcod/goal](https://github.com/semcod/goal/blob/84f18540d14c24cc8ff5b7f202d2874344779ecc/docs/internal-dependencies.md). + +## Python and installation + +Application support remains Python >=3.10. Source inspection found no Goal imports or executable invocation in application Python code. Goal's old declarations moved to an `automation` dependency group requiring Python >=3.12, so updating release tooling does not remove Python 3.10 application support. + +```sh +uv sync --locked --extra dev --python 3.10 +uv run --no-sync python -m pytest -q +UV_PROJECT_ENVIRONMENT=.venv-automation uv sync --locked --group automation --python 3.12 +``` + +The automation command uses a separate environment. Synchronize the application environment again after merging tested dependency updates. + +## Daily checks + +[Dependabot](../.github/dependabot.yml) checks the explicit costs, Goal, pfix, clickmd and code2llm catalog daily, including weekends, and groups updates into one PR. It includes transitive packages and may widen constraints when necessary. This is a bounded catalog, not discovery of every internal package. Local/Git sources and other packages need separate review. + +[Freshness CI](../.github/workflows/internal-dependency-freshness.yml) uses released Goal 2.2.0 to compare uv.lock with the highest published stable three-part versions of the catalog packages actually used here. It has read-only repository permissions, runs daily/manually/on dependency PRs, and retains JSON evidence. Registry and resolution errors remain visible. An audit does not prove a deployed environment has installed the new versions. + +[Locked tests](../.github/workflows/test-locked.yml) install the dev extra from uv.lock and run the full project suite on Python 3.10 and 3.13. A permissive version declaration alone does not refresh the lockfile; update creation, testing, merge and installation are separate steps. + +## Delivery + +The registry targets observed on 2026-09-05 are costs 0.2.0, Goal 2.2.0, pfix 0.1.79, clickmd 1.1.15 and code2llm 0.5.176. PR and Actions checks hold the test results; the [ecosystem rollout history](https://github.com/semcod/costs/tree/main/docs/dependencies) belongs to costs. Reports and instructions are versioned in repository docs, rather than temporary machine directories. diff --git a/pyproject.toml b/pyproject.toml index 99d8053..5560e43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dev = [ "pytest-mock", "ruff", "mypy", - "goal>=2.1.0", "costs>=0.1.20", "pfix>=0.1.60", ] @@ -96,3 +95,9 @@ max_commits = 500 # Cost thresholds for badge colors (USD) badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 } + +[tool.uv.dependency-groups.automation] +requires-python = ">=3.12" + +[dependency-groups] +automation = ["goal>=2.2.0"] diff --git a/uv.lock b/uv.lock index 60a7395..cecb19d 100644 --- a/uv.lock +++ b/uv.lock @@ -8,9 +8,12 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -549,16 +552,16 @@ wheels = [ [[package]] name = "clickmd" -version = "1.1.14" +version = "1.1.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "costs" }, { name = "goal" }, { name = "pfix" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/1b/84df87d056991c2fb0789ae47b49d5afb8496f2c45541760e81553d00b43/clickmd-1.1.14.tar.gz", hash = "sha256:a3625777736ba5d3d43f7de8ff81bc01d2a4d77f6c252ce6ea7d55b860279f43", size = 41134, upload-time = "2026-04-08T09:11:28.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/12/82b3b80bb2751e0d6c120c1beb11116527cca9322202b5a1924811b58376/clickmd-1.1.15.tar.gz", hash = "sha256:0a2971b952e471c4726bc2f88ed397abd395283c79e4885e435921426493d01c", size = 41145, upload-time = "2026-06-29T07:35:58.128Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/29/625bc2004029ce7297d0709e6486447c4a0f0d832c2ddd962cada14d32c7/clickmd-1.1.14-py3-none-any.whl", hash = "sha256:005bbc7cd97d8a58ba25b4fc0018b7970e37b57ee50d53aa33df5f445b4ff46b", size = 45540, upload-time = "2026-04-08T09:11:27.228Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5c/398fafad25e456d86fe8fe7bf9a3873f2077564d42dcdd1426b76d01e915/clickmd-1.1.15-py3-none-any.whl", hash = "sha256:b191c0c26be5ef009ea27a3e88c1c7d633d95a6da2fc737f04f6727481bfeb5e", size = 45514, upload-time = "2026-06-29T07:35:56.53Z" }, ] [[package]] @@ -572,7 +575,7 @@ wheels = [ [[package]] name = "costs" -version = "0.1.51" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, @@ -584,11 +587,12 @@ dependencies = [ { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dotenv" }, { name = "tiktoken" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/2c/d9f9a9c0acd33bc40788aaeef792fe045efd42368cde2ad00a0e15eeb625/costs-0.1.51.tar.gz", hash = "sha256:727f5f14874c41731e2485cc309ec4cc87c447288c8be4c0ddb38f72391d49cc", size = 30777, upload-time = "2026-05-12T08:33:20.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/6f/7f3c72d6d4b5264bd5a3447e18a3b5184bffee143f72c8d023bb759afd1a/costs-0.2.0.tar.gz", hash = "sha256:3782bae859725dae30f697b5f3e5aa5d84e06feadebc5782ae706e21d46437af", size = 44606, upload-time = "2026-09-05T15:49:55.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/15/4490e3afc8e0e903a984582db06a34ec9feeddf52ef99902d456a2e15da5/costs-0.1.51-py3-none-any.whl", hash = "sha256:15e2ebf518fec04e5883612d96dd2673f56cbeaf58408e448eb0837c01a2dc85", size = 35771, upload-time = "2026-05-12T08:33:19.271Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7e/fc91d9fdc7a1ceb9116daffab27124a54f6e8887c9d59d906df43dcbfa6a/costs-0.2.0-py3-none-any.whl", hash = "sha256:4798b8535cbbcbb354e440ebce91365f17c910414bf5831a0610998ae6e679af", size = 48625, upload-time = "2026-09-05T15:49:54.054Z" }, ] [[package]] @@ -695,7 +699,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -948,7 +952,7 @@ wheels = [ [[package]] name = "goal" -version = "2.1.247" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -958,9 +962,9 @@ dependencies = [ { name = "tomlkit" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/e8/36af5becc255f4d2f1832e7d72c0642a4dfbba194ea9eef216a50ffb3470/goal-2.1.247.tar.gz", hash = "sha256:95af032f02364c63361775f5a70640148947974d68d75df51de1b421011b46cf", size = 279595, upload-time = "2026-06-13T22:42:49.021Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/135e3f67a7324c3c49ef4246f5518b59ad90226e62c3d30ae20cba005c1d/goal-2.2.0.tar.gz", hash = "sha256:e9e2c1ac9493dae3b887b16d503f7c1f83d02ddcbdd3c5cf58d44a458bb2662f", size = 399786, upload-time = "2026-09-05T19:41:55.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/ed/43be4ef17b88479cc52e1074edc1fd2f875d24a783ab4d2d8b4ffa1ddb3a/goal-2.1.247-py3-none-any.whl", hash = "sha256:83b0e2da09d801074d24cf882b5bcc2d247c6e6759975b50a55f55c30e41c601", size = 273372, upload-time = "2026-06-13T22:42:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/24/69/8a4cf5710683bb777f1f6e4cd44dd3907befe97b94c6ea6e97d4305b4e44/goal-2.2.0-py3-none-any.whl", hash = "sha256:0d8f93aa317ac92a66b64b797e210a5d66044ce8db2cb05b012b03d42aced95e", size = 349709, upload-time = "2026-09-05T19:41:53.615Z" }, ] [[package]] @@ -1874,9 +1878,12 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ @@ -1971,7 +1978,6 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "costs" }, - { name = "goal" }, { name = "mypy" }, { name = "pfix" }, { name = "pytest" }, @@ -1979,10 +1985,14 @@ dev = [ { name = "ruff" }, ] +[package.dev-dependencies] +automation = [ + { name = "goal", marker = "python_full_version >= '3.12'" }, +] + [package.metadata] requires-dist = [ { name = "costs", marker = "extra == 'dev'", specifier = ">=0.1.20" }, - { name = "goal", marker = "extra == 'dev'", specifier = ">=2.1.0" }, { name = "httpx", specifier = ">=0.27" }, { name = "mypy", marker = "extra == 'dev'" }, { name = "pfix", marker = "extra == 'dev'", specifier = ">=0.1.60" }, @@ -1999,6 +2009,9 @@ requires-dist = [ ] provides-extras = ["dev"] +[package.metadata.requires-dev] +automation = [{ name = "goal", marker = "python_full_version >= '3.12'", specifier = ">=2.2.0" }] + [[package]] name = "openai" version = "2.41.1" @@ -2035,10 +2048,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -2102,14 +2115,17 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ @@ -2194,7 +2210,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "python_full_version < '3.11' or sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2203,7 +2219,7 @@ wheels = [ [[package]] name = "pfix" -version = "0.1.73" +version = "0.1.79" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "litellm" }, @@ -2212,9 +2228,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/76/982888ddbfd8398ac2c1a04bfe16caddfa4091f84af347ee878cdb5e5cf9/pfix-0.1.73.tar.gz", hash = "sha256:f1c55a01b738b5c6465815ad9d61371d6e2a1a6074199fa0174309996de7310c", size = 2837935, upload-time = "2026-05-12T08:34:08.486Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/7e/252be5e1d55321b947013010f45b02d781de87e25b9a524c0ee72c1ad3c0/pfix-0.1.79.tar.gz", hash = "sha256:6bc4c572e22e0f0d952bc5e2df78f52f8f999cab64e22138b097b2eab4d83092", size = 2845663, upload-time = "2026-07-05T17:00:57.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/4f/abe3e6992a7df0237fc75d000ad950da8cf20d1c767415e4605b31fa143a/pfix-0.1.73-py3-none-any.whl", hash = "sha256:0e0f022c0277b8c9633a2be2d2def366c856170953d2f9ff9acf4a473c1c6bf2", size = 149278, upload-time = "2026-05-12T08:34:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/39/bf/548982294e795d79003680b2214c7f5dd00ca4b0ea856064bf9cb6869ba7/pfix-0.1.79-py3-none-any.whl", hash = "sha256:bb7ab83b99c0bfbaae5e6e95cb801a27d4fd68749485600fdfe49dad00607461", size = 151754, upload-time = "2026-07-05T17:00:52.816Z" }, ] [[package]] @@ -3166,9 +3182,12 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } wheels = [ From 22c99d98376ebd2309ab5dea101490a9f379c9b9 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 5 Sep 2026 22:58:21 +0200 Subject: [PATCH 2/3] fix: count each bug-fix commit once and cover planning workflows Co-authored-by: Koru Agent --- docs/dependencies.md | 2 + pyproject.toml | 1 + src/nxdo/metrics/coupling.py | 2 +- src/nxdo/metrics/hotspots.py | 27 +++--- tests/test_auto_cli.py | 60 ++++++++++++++ tests/test_history_metrics.py | 130 +++++++++++++++++++++++++++++ tests/test_koru_context.py | 85 +++++++++++++++++++ tests/test_metrics.py | 7 -- uv.lock | 149 ++++++++++++++++++++++++++++++++++ 9 files changed, 439 insertions(+), 24 deletions(-) create mode 100644 tests/test_auto_cli.py create mode 100644 tests/test_history_metrics.py create mode 100644 tests/test_koru_context.py diff --git a/docs/dependencies.md b/docs/dependencies.md index 7d132f5..74e82d9 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -25,3 +25,5 @@ The automation command uses a separate environment. Synchronize the application ## Delivery The registry targets observed on 2026-09-05 are costs 0.2.0, Goal 2.2.0, pfix 0.1.79, clickmd 1.1.15 and code2llm 0.5.176. PR and Actions checks hold the test results; the [ecosystem rollout history](https://github.com/semcod/costs/tree/main/docs/dependencies) belongs to costs. Reports and instructions are versioned in repository docs, rather than temporary machine directories. + +The existing CI retains its 95% coverage gate. Regression tests exercise real temporary Git histories, optional Koru failures, and auto-mode dry-run/synchronization boundaries. Bug-fix counting now combines keyword filters in one Git query and counts each commit once, preventing densities above 100% caused by repeated keywords. `pytest-cov` is declared in the dev extra so the coverage gate can also run from the lockfile. diff --git a/pyproject.toml b/pyproject.toml index 5560e43..8cadbdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dev = [ "mypy", "costs>=0.1.20", "pfix>=0.1.60", + "pytest-cov>=5.0", ] [project.urls] diff --git a/src/nxdo/metrics/coupling.py b/src/nxdo/metrics/coupling.py index ddcc9ed..bf76a67 100644 --- a/src/nxdo/metrics/coupling.py +++ b/src/nxdo/metrics/coupling.py @@ -35,7 +35,7 @@ def _get_commits_with_files(repo_path: Path, max_commits: int = 100) -> list[lis return [] commits = [] - current_files = [] + current_files: list[str] = [] for line in result.stdout.strip().split("\n"): if not line: diff --git a/src/nxdo/metrics/hotspots.py b/src/nxdo/metrics/hotspots.py index f2aa6d0..989a41b 100644 --- a/src/nxdo/metrics/hotspots.py +++ b/src/nxdo/metrics/hotspots.py @@ -73,22 +73,17 @@ def _get_bug_fix_commits(repo_path: Path, file_path: str, since: str = "90.days. bug_patterns = ["fix", "bug", "repair", "hotfix", "patch", "resolve", "issue"] try: - # Use git log with grep for each pattern - total = 0 - for pattern in bug_patterns: - result = subprocess.run( - ["git", "log", f"--since={since}", f"--grep={pattern}", "-i", - "--oneline", "--", file_path], - cwd=repo_path, - capture_output=True, - text=True, - ) - if result.returncode == 0: - # Count unique commits (git log --grep can return duplicates across patterns) - commits = set(line.strip() for line in result.stdout.strip().split("\n") if line.strip()) - total += len(commits) - - return total + result = subprocess.run( + ["git", "log", f"--since={since}", "--format=%H", "-i", + *(f"--grep={pattern}" for pattern in bug_patterns), "--", file_path], + cwd=repo_path, + capture_output=True, + text=True, + ) + if result.returncode != 0: + return 0 + # Git combines these patterns with OR and emits each commit once. + return len({line.strip() for line in result.stdout.splitlines() if line.strip()}) except Exception: return 0 diff --git a/tests/test_auto_cli.py b/tests/test_auto_cli.py new file mode 100644 index 0000000..7d0c7ef --- /dev/null +++ b/tests/test_auto_cli.py @@ -0,0 +1,60 @@ +"""Verify auto mode dispatch and its dry-run boundary without a live provider.""" + +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest +from typer.testing import CliRunner + +from nxdo import cli, metrics +from nxdo.models import Task, TaskPlan + + +@pytest.fixture +def workflow(monkeypatch): + plan = TaskPlan(project_name="demo", summary="Repair the parser", tasks=[ + Task(number=1, title="Parser tests", description="Cover malformed input"), + ]) + generate = Mock(return_value=plan) + sync = Mock() + provider = Mock() + monkeypatch.setattr(cli, "generate_next_tasks", generate) + monkeypatch.setattr(cli, "_sync_planfile_if_requested", sync) + monkeypatch.setattr(cli, "OpenAICompatProvider", provider) + monkeypatch.setattr(cli, "get_settings", Mock()) + monkeypatch.setattr(metrics, "identify_bug_hotspots", Mock(return_value=[])) + monkeypatch.setattr(metrics, "collect_file_metrics", Mock(return_value=[])) + return plan, generate, sync, provider + + +@pytest.mark.parametrize("has_issues", [False, True]) +def test_auto_dry_run_does_not_call_provider_or_write_tickets(tmp_path, workflow, has_issues): + if has_issues: + metrics.identify_bug_hotspots.return_value = [SimpleNamespace()] + metrics.collect_file_metrics.return_value = [SimpleNamespace(cyclomatic_complexity=12)] + result = CliRunner().invoke(cli.app, ["auto", str(tmp_path), "--dry-run"]) + assert result.exit_code == 0, result.output + assert "would execute" in result.output + assert ("high-complexity" if has_issues else "healthy") in result.output + for mock in workflow[1:]: + mock.assert_not_called() + assert list(tmp_path.iterdir()) == [] + + +def test_auto_syncs_the_generated_plan_and_forwards_context(tmp_path, workflow): + plan, generate, sync, provider = workflow + result = CliRunner().invoke(cli.app, ["auto", str(tmp_path), "--extra-context", "Parser failures"]) + assert result.exit_code == 0, result.output + assert "1 tickets queued" in result.output + assert generate.call_args.kwargs["extra_context"] == "Parser failures" + assert generate.call_args.kwargs["koru_aware"] is True + assert generate.call_args.kwargs["provider"] is provider.return_value + sync.assert_called_once_with(plan, tmp_path.resolve(), sync_planfile=True) + + +def test_auto_provider_error_does_not_sync_partial_output(tmp_path, workflow): + workflow[1].side_effect = ValueError("provider offline") + result = CliRunner().invoke(cli.app, ["auto", str(tmp_path)]) + assert result.exit_code == 1 + assert "provider offline" in result.output + workflow[2].assert_not_called() diff --git a/tests/test_history_metrics.py b/tests/test_history_metrics.py new file mode 100644 index 0000000..568e502 --- /dev/null +++ b/tests/test_history_metrics.py @@ -0,0 +1,130 @@ +"""Exercise metrics against a real, local history and unavailable Git.""" + +import subprocess +from types import SimpleNamespace + +import pytest +from typer.testing import CliRunner + +from nxdo.cli import app +from nxdo.metrics import coupling, hotspots + + +@pytest.fixture +def history(tmp_path): + def git(*args): + return subprocess.run( + ["git", *args], cwd=tmp_path, check=True, capture_output=True, text=True, + ) + + git("init") + hooks = tmp_path / ".git" / "test-hooks" + hooks.mkdir() + git("config", "core.hooksPath", str(hooks)) + + def commit(author, message, files): + for name, content in files.items(): + (tmp_path / name).write_text(content) + git("add", ".") + git("-c", f"user.name={author}", "-c", "user.email=tests@example.invalid", + "commit", "-m", message) + + source = "def decision(x):\n" + "".join( + f" if x == {n}: return {n}\n" for n in range(12) + ) + commit("Ada", "feature: initial modules", {"a.py": source, "b.py": "value = 1\n"}) + commit("Bob", "fix bug: repair hotfix patch resolves issue", { + "a.py": source + " return 0\n", "b.py": "value = 2\n", + }) + commit("Cara", "feature: extend modules", { + "a.py": source + " return -1\n", "c.py": "value = 3\n", + "README.md": "Documentation\n", + }) + return tmp_path + + +def test_fix_commit_matches_many_keywords_but_is_counted_once(history, monkeypatch): + real_run = subprocess.run + calls = [] + + def run(*args, **kwargs): + calls.append(args[0]) + return real_run(*args, **kwargs) + + monkeypatch.setattr(hotspots.subprocess, "run", run) + assert hotspots._get_bug_fix_commits(history, "a.py") == 1 + assert len(calls) == 1 + assert hotspots._get_bug_fix_commits(history, "c.py") == 0 + + +def test_history_preserves_churn_authors_and_risk_density(history): + commits, churn = hotspots._get_file_commits_with_info(history, "a.py") + assert len(commits) == 3 + assert {author for _, author, _ in commits} == {"Ada", "Bob", "Cara"} + assert churn == sum(amount for _, _, amount in commits) + risks = hotspots.identify_bug_hotspots(history) + by_path = {risk.file_path: risk for risk in risks} + assert by_path["a.py"].bug_density == 0.33 + assert by_path["b.py"].bug_density == 0.5 + assert by_path["a.py"].author_count == 3 + assert "README.md" not in by_path + assert len(hotspots.identify_bug_hotspots(history, top_n=1)) == 1 + assert hotspots.identify_bug_hotspots(history, files=["missing.py"]) == [] + + +def test_bus_factor_identifies_single_author_and_shared_files(history): + assert hotspots.calculate_bus_factor(history) == {"b.py": 2, "c.py": 1} + critical = hotspots.get_critical_bus_factor_files(history) + assert [(name, count) for name, count, _ in critical] == [("c.py", 1), ("b.py", 2)] + assert critical[0][2] == ["Cara"] + assert set(critical[1][2]) == {"Ada", "Bob"} + + +def test_coupling_and_clusters_follow_real_commits(history): + pairs = coupling.collect_coupling_matrix(history, file_filter={".py"}) + by_pair = {(pair.file_a, pair.file_b): pair for pair in pairs} + assert by_pair[("a.py", "b.py")].commits_together == 2 + assert by_pair[("a.py", "b.py")].coupling_score == 1 + assert coupling.get_coupling_clusters(pairs) == [{"a.py", "b.py", "c.py"}] + assert coupling.collect_coupling_matrix(history, min_coupling=1.1) == [] + assert coupling.collect_coupling_matrix(history, file_filter={".js"}) == [] + + +def test_metrics_cli_reports_actual_repository(history): + result = CliRunner().invoke(app, ["metrics", str(history)]) + assert result.exit_code == 0, result.output + for heading in ["Cyclomatic Complexity", "Coupling Clusters", "Bug Hotspots", "Low Bus Factor"]: + assert heading in result.output + assert "a.py" in result.output + assert "1/3 commits" in result.output + + +@pytest.mark.parametrize("fails_by_exception", [False, True]) +def test_metrics_degrade_when_git_is_unavailable(tmp_path, monkeypatch, fails_by_exception): + def unavailable(*args, **kwargs): + if fails_by_exception: + raise OSError("git unavailable") + return SimpleNamespace(returncode=128, stdout="") + + monkeypatch.setattr(subprocess, "run", unavailable) + assert coupling.collect_coupling_matrix(tmp_path) == [] + assert hotspots._get_file_commits_with_info(tmp_path, "a.py") == ([], 0) + assert hotspots._get_bug_fix_commits(tmp_path, "a.py") == 0 + assert hotspots.identify_bug_hotspots(tmp_path) == [] + assert hotspots.calculate_bus_factor(tmp_path) == {} + assert hotspots.calculate_bus_factor(tmp_path, files=["a.py", "README.md"]) == {} + assert hotspots.get_critical_bus_factor_files(tmp_path, {"a.py": 1}) == [] + + +def test_numstat_tolerates_binary_and_malformed_entries(tmp_path, monkeypatch): + output = "abc|Ada\n-\t-\timage.bin\ninvalid\t3\ta.py\n2\t3\ta.py\n" + monkeypatch.setattr(subprocess, "run", lambda *a, **k: SimpleNamespace(returncode=0, stdout=output)) + assert hotspots._get_file_commits_with_info(tmp_path, "a.py") == ([("abc", "Ada", 5)], 5) + + +def test_coupling_filters_generated_files_and_normalizes_pair_order(tmp_path, monkeypatch): + monkeypatch.setattr(coupling, "_get_commits_with_files", lambda *a: [ + ["z.py", "a.py", "tests/a.py", "dist/a.py"], ["a.py", "z.py"], + ]) + pairs = coupling.collect_coupling_matrix(tmp_path) + assert [(p.file_a, p.file_b, p.commits_together) for p in pairs] == [("a.py", "z.py", 2)] diff --git a/tests/test_koru_context.py b/tests/test_koru_context.py new file mode 100644 index 0000000..7213367 --- /dev/null +++ b/tests/test_koru_context.py @@ -0,0 +1,85 @@ +"""Keep optional Koru discovery and prompt enrichment deterministic.""" + +import sys +from types import ModuleType, SimpleNamespace +from unittest.mock import Mock + +import pytest + +from nxdo import koru_context, planner +from nxdo.config import NxdoSettings + + +@pytest.fixture +def koru(monkeypatch): + package = ModuleType("koruapi") + integrations = ModuleType("koruapi.integrations") + invoke = ModuleType("koruapi.invoke") + integrations.list_integrations = Mock(return_value=[ + SimpleNamespace(id="planfile.tickets", title="Tickets", description="List tickets", + transport="python", methods=("list", "create"), tags=("planfile",), + cli_equivalent="planfile tickets list"), + SimpleNamespace(id="custom.read", title="Custom", description="Read state", + transport="python", methods=(), tags=(), cli_equivalent=None), + ]) + invoke.invoke_integration = Mock(side_effect=lambda name, **kwargs: ( + {"ok": True, "tickets": [{"id": str(n), "title": f"Work {n}"} for n in range(12)]} + if name == "planfile.tickets" else {"ok": False, "report": {"failures": {"git": "missing"}}} + )) + for name, module in [("koruapi", package), ("koruapi.integrations", integrations), ("koruapi.invoke", invoke)]: + monkeypatch.setitem(sys.modules, name, module) + return integrations, invoke + + +def test_optional_koru_absence_does_not_query_project(tmp_path, monkeypatch): + monkeypatch.setitem(sys.modules, "koruapi.integrations", None) + context = koru_context.build_koru_context(tmp_path) + assert not context.available + assert context.schema_text == "" + assert context.project_state.open_tickets == [] + + +def test_catalog_and_bounded_project_state_enrich_prompt(tmp_path, koru): + context = koru_context.build_koru_context(tmp_path) + assert context.available + assert len(context.project_state.open_tickets) == 10 + assert context.project_state.doctor_issues == ["git: missing"] + assert not context.project_state.doctor_ok + for value in ["planfile.tickets", "CLI: planfile tickets list", "Methods: invoke", + "Work 4", "... and 5 more", "git: missing"]: + assert value in context.schema_text + assert "Work 5" not in context.schema_text + assert koru[1].invoke_integration.call_count == 2 + assert "KORU-AWARE PLANNING MODE" in koru_context.get_koru_system_prompt_extension() + + +def test_catalog_only_mode_avoids_live_state_queries(tmp_path, koru): + context = koru_context.build_koru_context(tmp_path, include_project_state=False) + assert context.available + assert "Open planfile tickets: none" in context.schema_text + koru[1].invoke_integration.assert_not_called() + + +@pytest.mark.parametrize("response", [RuntimeError("offline"), {"ok": None}]) +def test_failed_state_queries_do_not_remove_catalog(tmp_path, koru, response): + invoke = koru[1].invoke_integration + if isinstance(response, Exception): + invoke.side_effect = response + else: + invoke.side_effect = None + invoke.return_value = response + context = koru_context.build_koru_context(tmp_path) + assert context.available + assert not context.project_state.open_tickets + assert invoke.call_count == 2 + + +def test_planner_passes_koru_context_to_provider_without_network(tmp_path, koru): + provider = Mock() + result = planner.generate_next_tasks( + tmp_path, provider=provider, settings=NxdoSettings(_env_file=None), koru_aware=True, + ) + assert result is provider.generate_plan.return_value + prompt = provider.generate_plan.call_args.args[0] + assert "planfile.tickets" in prompt + assert "git: missing" in prompt diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 825dda0..0b78703 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -6,10 +6,7 @@ from nxdo.metrics import ( collect_file_metrics, - collect_coupling_matrix, get_coupling_clusters, - identify_bug_hotspots, - calculate_bus_factor, ) @@ -83,10 +80,6 @@ def test_coupling_clusters_empty_when_low_coupling(self) -> None: clusters = get_coupling_clusters(coupling, min_coupling=0.5) self.assertEqual(len(clusters), 0) - def test_calculate_bus_factor_detects_silos(self) -> None: - # This test requires a git repo, so we'll test the logic manually - # In a real repo with history, this would return actual values - pass if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index cecb19d..5db2784 100644 --- a/uv.lock +++ b/uv.lock @@ -595,6 +595,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/7e/fc91d9fdc7a1ceb9116daffab27124a54f6e8887c9d59d906df43dcbfa6a/costs-0.2.0-py3-none-any.whl", hash = "sha256:4798b8535cbbcbb354e440ebce91365f17c910414bf5831a0610998ae6e679af", size = 48625, upload-time = "2026-09-05T15:49:54.054Z" }, ] +[[package]] +name = "coverage" +version = "7.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/fc/fbd92ecbe5efbe68cf9e708d858570ebd33f0761600358948882f1a2a96b/coverage-7.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36aed4951aedf04cbe9465e76f8e71219980a52b73d07afe69746cba6ba7b97a", size = 222890, upload-time = "2026-08-28T21:50:37.361Z" }, + { url = "https://files.pythonhosted.org/packages/c6/54/16d2a7602ddf169353344e135541731cc24c7c3ef0001b0302f4d1a3de1e/coverage-7.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb953835dbfa6d641ac3943e0986bc680f8abbdc2985af15b46c54985347146a", size = 223415, upload-time = "2026-08-28T21:50:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/a1/15a36a42b35f6dd66214701c4f797856a9e42d12d85f441101eeb349404c/coverage-7.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97051c4903689b1afedc2a354d6118223051e03588078b53048603bda9014577", size = 250146, upload-time = "2026-08-28T21:50:42.509Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/677f6363054d2de71fd0ca2071a796e3cf7cf82f8046933b34f2f91eb031/coverage-7.16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:770d4244c423dcafb5c31db393f429fe952b1bba23bbff7cc3886f8133769ba5", size = 251977, upload-time = "2026-08-28T21:50:44.137Z" }, + { url = "https://files.pythonhosted.org/packages/40/fc/9be462bb9257d84e3cc7517dc118c364db0eabdd6cb42272bb8667abedce/coverage-7.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e7de0cb87960c6c9b5cad760068dab767b2b49a3b9376e1992c1e2691a015e", size = 253840, upload-time = "2026-08-28T21:50:45.822Z" }, + { url = "https://files.pythonhosted.org/packages/24/cd/dc003310b876c793c88f8dcf64ca52db244e4b6f96772251b1814fbb0653/coverage-7.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c2c45ee1853668f0ea1a0ddff396421c9dc5ad25a56bfb94a895970c2d8e7c2", size = 255756, upload-time = "2026-08-28T21:50:47.351Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f2/7a0a3c57e488b24d3ed560fc0e449c3e94fe8da0b59ef8dd00b2581c813a/coverage-7.16.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6b2b9599e7513b0a9c5bf0357f9f8deaa4c2c821025b0693d420e6602748981", size = 250811, upload-time = "2026-08-28T21:50:48.974Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1d/fd0cffd02a34eec7b92cfa4089a9d82e95390facebd56e5603de780b4727/coverage-7.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fde65e0ea945920265dfe4a2108fc45eee2e2ea3d9c3073af6373ff9836aa71", size = 251882, upload-time = "2026-08-28T21:50:50.516Z" }, + { url = "https://files.pythonhosted.org/packages/97/b1/82159b5ab545209764eb3eb0f06e315e9a2fd975a72bbf6da48d5deb6e76/coverage-7.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78103e79f9378cb0e43ddaa728629a373c070df903c5dfa98b63ba2cfb4e8c42", size = 249886, upload-time = "2026-08-28T21:50:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d3/91/ad689bb219fc78eaea8ff2b2212fa6202d4b716a65a533387183bb7a78ad/coverage-7.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e40e323711b485592354069b1c027ef879cc2d11657eac09a6e5ad0b49ab7406", size = 253698, upload-time = "2026-08-28T21:50:53.832Z" }, + { url = "https://files.pythonhosted.org/packages/4a/57/8eef40eb196d3fa1c0bbd99466119a40f70e5f2242eaa8f8852f98f4d985/coverage-7.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c94ef980f7b94d9dab9dac076d44ca706654cd51bad19734e029084adf528c8e", size = 250158, upload-time = "2026-08-28T21:50:55.64Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/0c0240eb6917c81ea21180bbc098bc01c624868bb917e9a10fffed23b970/coverage-7.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b37ad5cbb77776f446e1b55b461eec2eef5c3e7130c72dc0e1447c3a9da2d199", size = 250759, upload-time = "2026-08-28T21:50:57.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/6b4986aedfaa69a4607f24cf127527dbab4d57bfbeef4fffa126a5dbad6a/coverage-7.16.0-cp310-cp310-win32.whl", hash = "sha256:9421dde689e68d9fd2b6cd7d8c4498e79b5431467b6298517e3f3e60fdbe80a7", size = 224939, upload-time = "2026-08-28T21:50:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/90/b4/43cdf444ace1c30c15446b143fa79d4bdd756cfdfce4c63f10c7697ba957/coverage-7.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:81d63b68b26304e3668edb103311c17fe13c2ed1c7fe973309819f27bf61c5b8", size = 225568, upload-time = "2026-08-28T21:51:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/c76bf165ff01664ca8b1ca7f2b2b5f311353d3959dbac1187dd21c6cc7f8/coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d", size = 223019, upload-time = "2026-08-28T21:51:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/16/7d/a47cebf71cb789b6e25de07035d350bff110d02f9c28bf32f92b4c818874/coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db", size = 223524, upload-time = "2026-08-28T21:51:03.632Z" }, + { url = "https://files.pythonhosted.org/packages/51/b3/42e46d7e247ba33758156a0cc88dc64715f7e7b04640fbe430c4da437ab1/coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3", size = 253934, upload-time = "2026-08-28T21:51:05.365Z" }, + { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4", size = 255846, upload-time = "2026-08-28T21:51:06.922Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/38e5d8cf45af5db7419e9580bba4017113f8f1e2697cb6c52213bf7e7e40/coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719", size = 257953, upload-time = "2026-08-28T21:51:08.51Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bb/2f44b99723d0306095dacdf90f994631e299ff8f087a384b42ecc2d1ccb9/coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3", size = 259915, upload-time = "2026-08-28T21:51:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7d/3f1c312944d88b2d3cae8af72007c15dcf5f92bda6da6d433c2d5f050ee7/coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a", size = 254028, upload-time = "2026-08-28T21:51:11.845Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f6/52a7e26baeeca7f3114b15da5e840bebcfe6491eb234f6922d33c79ee8fc/coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c", size = 255648, upload-time = "2026-08-28T21:51:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d1/0673e78d9ca29d56f663623791338647753c673f0bc964e860086da07bce/coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9", size = 253708, upload-time = "2026-08-28T21:51:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6c/23/b74c87828369059415b20884b6f48260f049bff750d6eb454be8554732ab/coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d", size = 257479, upload-time = "2026-08-28T21:51:16.988Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/09e172472c45a956e226dddf82d449f245764208b7cea47b32a73df955a3/coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6", size = 253428, upload-time = "2026-08-28T21:51:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/62/22/e378e4f7ffa290ea4775b34e319fa182640bba650a2c6781af791b66b79a/coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce", size = 254337, upload-time = "2026-08-28T21:51:20.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/9a6ca653d86e46c3383a905f726a28bcf7bb2528088794d30a53687b381c/coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce", size = 225103, upload-time = "2026-08-28T21:51:22.685Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/6d4627be89ac02f579d88806875a5d6e328c59d7d79c594643c7a4460ef6/coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a", size = 225577, upload-time = "2026-08-28T21:51:24.334Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3d/d7be38564d00a17775426685776b4bf18e8a6048a085eccf65d75eb0fa5a/coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7", size = 225126, upload-time = "2026-08-28T21:51:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, + { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, + { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, + { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, + { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, + { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, + { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, + { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, + { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, + { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, + { url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z" }, + { url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/318e4379106bc8047ba235e3732ddc87d1b393ac3db9776f5405ff14f322/coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024", size = 223257, upload-time = "2026-08-28T21:53:25.376Z" }, + { url = "https://files.pythonhosted.org/packages/81/4d/a5c54d9144e9db6505749758ba50a28be624148873751728a59cbb72d27a/coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c", size = 223596, upload-time = "2026-08-28T21:53:27.461Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/38e93a10899c9315964c0a4e729b3e5867f8f46e977808f9c6fbda52525a/coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633", size = 254699, upload-time = "2026-08-28T21:53:29.715Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/acddda030b4630f68167f3daa94b41d22071847822a70d8178d43dcf678e/coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b", size = 257614, upload-time = "2026-08-28T21:53:31.948Z" }, + { url = "https://files.pythonhosted.org/packages/15/7e/225b182497c1ce6d3f0d76a3074a4dbc9f272300e92bb100df53b03de0aa/coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f", size = 259236, upload-time = "2026-08-28T21:53:34.291Z" }, + { url = "https://files.pythonhosted.org/packages/2e/19/76641ddc50cb2410ebbd0ed7fe1052614d0e5612e802a2817521adb9febb/coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68", size = 261433, upload-time = "2026-08-28T21:53:36.401Z" }, + { url = "https://files.pythonhosted.org/packages/12/9e/5f89de8b7c2017f36b68b4e4a25940723a748b21474820bf61e8bce0891c/coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be", size = 255182, upload-time = "2026-08-28T21:53:38.496Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c1/ce94b2ec502e79775efb5efa22c741ebb0bd2be10bdd29650825ff57bdcb/coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a", size = 257329, upload-time = "2026-08-28T21:53:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/3f5374df3a6ca19ee5f98a6bd21dbb05f1e9d399bd9978e9821d260eab5e/coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae", size = 255210, upload-time = "2026-08-28T21:53:43.393Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b8/1bc5751496d0be6fd9dde8ca547d9a8a9f07847856aba3f3ae5ac594cd81/coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee", size = 259442, upload-time = "2026-08-28T21:53:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/dc/8aca78e47e1e6fcc761cd28a20daf4a84bd847a7369e2701a93ccfc3d1fd/coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f", size = 254618, upload-time = "2026-08-28T21:53:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/fd/787842cdf6ce16ac5c1bd8a26549bab3b3f27b02500075bc540dc7853bca/coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73", size = 256541, upload-time = "2026-08-28T21:53:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/8df302cbef373dd1f3401044cdb94dfc74517e5af2af27b4d0e721557e0e/coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889", size = 225429, upload-time = "2026-08-28T21:53:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/85/87/5bad7ac45f76b3728ca211028ee561c2ede3ba44da401129e28bb8737291/coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627", size = 225903, upload-time = "2026-08-28T21:53:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ea/67d84b11caf240f059ec313f616d82212df5004e8bc85802c1edfc50bb3d/coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9", size = 225334, upload-time = "2026-08-28T21:53:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/65/21/a88349cce3ff720729b754916ac47e2e3646a8137552e4fa7cdd5967cc7f/coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025", size = 223980, upload-time = "2026-08-28T21:54:00.082Z" }, + { url = "https://files.pythonhosted.org/packages/fd/02/4d54abf3e6a4d8b7675921b20e91163b1064a5a9dbefebb71c05065dd136/coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3", size = 224276, upload-time = "2026-08-28T21:54:02.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/10dbc96d95d20b9b041045d293480bd49e536180e93af62dd7662376284d/coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a", size = 265135, upload-time = "2026-08-28T21:54:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/6b326544afd1a8aef3a495bbae109a7ab5baf23e04a2741d8d64e2df2ba2/coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178", size = 268216, upload-time = "2026-08-28T21:54:06.97Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/1dc8265f3ed990690e24d5f31ff79bc9fb9b25d54f9f89bebad5a6a8b7a1/coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2", size = 270772, upload-time = "2026-08-28T21:54:09.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/a7/3a8463713a402b44044ec832f4a76e442ce4b3a207804303f4d1dc1a9bb4/coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0", size = 271752, upload-time = "2026-08-28T21:54:11.701Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/dd5e795cfbe1842f69899189089ae289a96d6a68de312960ea668542e33c/coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b", size = 265589, upload-time = "2026-08-28T21:54:14.12Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/fd90636cbd95cb018312f6ca1ca2bbd70fbe8e4ee6f3992fc36a4230364e/coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0", size = 268596, upload-time = "2026-08-28T21:54:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/ef2d59264f3b3b358cc5885ca375e6cdbda7c195e78304d5aae800a72d9d/coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e", size = 265072, upload-time = "2026-08-28T21:54:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5b/400891c364c0170408d172501b340b18611800f4c42d8fbb16f9f5497c24/coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54", size = 269768, upload-time = "2026-08-28T21:54:20.985Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/9792c80271df04d287d21ed5d662fd8fa58b1737888d817679b1ce5d2fab/coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb", size = 265211, upload-time = "2026-08-28T21:54:23.344Z" }, + { url = "https://files.pythonhosted.org/packages/81/67/5b8f827cfa6616e6bd7ba9397acfe7e3c4fd5b9fca4125511d5089f55d5a/coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897", size = 267170, upload-time = "2026-08-28T21:54:25.85Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/c135d2d2cb617d744bc3e13c922f2fae66964494176ddef225dc4656bd2c/coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743", size = 225731, upload-time = "2026-08-28T21:54:28.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/dc3d53eadf155916e183bf5dfacbfc4aa5bfb7f13b7da11c01caa7a05cbc/coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3", size = 226562, upload-time = "2026-08-28T21:54:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/2f/00/ac9da1a60a4e84c3ad0f7db4723fd327154a8f9add210c0dcd2db3ec5156/coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc", size = 225872, upload-time = "2026-08-28T21:54:32.806Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -1981,6 +2114,7 @@ dev = [ { name = "mypy" }, { name = "pfix" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "ruff" }, ] @@ -2000,6 +2134,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2" }, { name = "pydantic-settings", specifier = ">=2" }, { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, { name = "pytest-mock", marker = "extra == 'dev'" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=13" }, @@ -2703,6 +2838,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "pytest-mock" version = "3.15.1" From d7be0bf5d83586147e4a872df6d47ecffe00b1d2 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Sat, 5 Sep 2026 23:01:27 +0200 Subject: [PATCH 3/3] ci: use the lockfile for all existing quality gates Co-authored-by: Koru Agent --- .github/workflows/ci.yml | 18 +++++++++++------- .github/workflows/test-locked.yml | 30 ------------------------------ docs/dependencies.md | 4 +++- 3 files changed, 14 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/test-locked.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd4e098..fd007d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,15 @@ on: pull_request: branches: [main, develop] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] @@ -23,22 +28,21 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install pytest-cov - pip install -e ".[dev]" + python -m pip install uv + uv sync --locked --extra dev --python "${{ matrix.python-version }}" - name: Run tests with coverage run: | - PYTHONPATH=src python -m pytest tests/ -v --cov=nxdo --cov-report=term-missing --cov-fail-under=95 + uv run --no-sync python -m pytest tests/ -v --cov=nxdo --cov-report=term-missing --cov-fail-under=95 - name: Run type checking run: | - mypy src/nxdo + uv run --no-sync mypy src/nxdo - name: Run linting run: | - ruff check src/nxdo tests/ + uv run --no-sync ruff check src/nxdo tests/ - name: Verify committed examples run: | - ./examples/check-examples.sh + uv run --no-sync bash examples/check-examples.sh diff --git a/.github/workflows/test-locked.yml b/.github/workflows/test-locked.yml deleted file mode 100644 index c42bf24..0000000 --- a/.github/workflows/test-locked.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Locked tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - python-version: ["3.10", "3.13"] - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 - with: - python-version: ${{ matrix.python-version }} - - run: python -m pip install uv - - name: Install locked test dependencies - run: uv sync --locked --extra dev --python "${{ matrix.python-version }}" - - name: Test supported Python versions - run: uv run --no-sync python -m pytest -q diff --git a/docs/dependencies.md b/docs/dependencies.md index 74e82d9..241ee03 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -20,10 +20,12 @@ The automation command uses a separate environment. Synchronize the application [Freshness CI](../.github/workflows/internal-dependency-freshness.yml) uses released Goal 2.2.0 to compare uv.lock with the highest published stable three-part versions of the catalog packages actually used here. It has read-only repository permissions, runs daily/manually/on dependency PRs, and retains JSON evidence. Registry and resolution errors remain visible. An audit does not prove a deployed environment has installed the new versions. -[Locked tests](../.github/workflows/test-locked.yml) install the dev extra from uv.lock and run the full project suite on Python 3.10 and 3.13. A permissive version declaration alone does not refresh the lockfile; update creation, testing, merge and installation are separate steps. +[Locked CI](../.github/workflows/ci.yml) installs the dev extra from uv.lock and runs the full suite, 95% coverage gate, mypy, Ruff and examples on Python 3.10, 3.11, 3.12 and 3.13. A permissive version declaration alone does not refresh the lockfile; update creation, testing, merge and installation are separate steps. ## Delivery The registry targets observed on 2026-09-05 are costs 0.2.0, Goal 2.2.0, pfix 0.1.79, clickmd 1.1.15 and code2llm 0.5.176. PR and Actions checks hold the test results; the [ecosystem rollout history](https://github.com/semcod/costs/tree/main/docs/dependencies) belongs to costs. Reports and instructions are versioned in repository docs, rather than temporary machine directories. The existing CI retains its 95% coverage gate. Regression tests exercise real temporary Git histories, optional Koru failures, and auto-mode dry-run/synchronization boundaries. Bug-fix counting now combines keyword filters in one Git query and counts each commit once, preventing densities above 100% caused by repeated keywords. `pytest-cov` is declared in the dev extra so the coverage gate can also run from the lockfile. + +The existing CI now uses uv.lock for all its gates. Its previous pip installation selected Ruff 0.16.6 while the lockfile contained 0.15.17, producing different lint rules from local checks. The lockfile is the shared source of dependency versions; updates to third-party tools need their own tested lockfile changes.