From a9faa8dee40793111c9b90c519116c77ffc07d06 Mon Sep 17 00:00:00 2001 From: Sebastien Larinier Date: Fri, 15 May 2026 22:59:55 +0200 Subject: [PATCH 1/5] feat(docker): nginx image serving the SPA, GHCR publish on tag Adds a multi-stage Dockerfile (docker/Dockerfile): - Stage 1 (python:3.12-alpine) regenerates docs/data/*.json from the per-phase Markdown via the stdlib-only dima_convert.py. - Stage 2 (nginxinc/nginx-unprivileged:alpine) serves the result on port 8080 with a custom nginx.conf: gzip, no-cache for html/json, long cache for static assets, SPA fallback, autoindex off. - HEALTHCHECK pings /index.html every 30 s. Workflow .github/workflows/docker.yml builds and pushes a multi-arch image (linux/amd64 + linux/arm64) to GHCR: - on git tag push (v*): publishes AND latest; - on pull_request touching docker/**: build-only (no push) for safety; - on workflow_dispatch: manual run. Uses gha cache to keep rebuilds fast. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker.yml | 60 ++++++++++++++++++++++++++++++++++++ docker/Dockerfile | 32 +++++++++++++++++++ docker/nginx.conf | 40 ++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 docker/Dockerfile create mode 100644 docker/nginx.conf diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..b94a416 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,60 @@ +name: Docker image + +on: + push: + tags: ["v*"] + pull_request: + paths: + - "docker/**" + - ".github/workflows/docker.yml" + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU (multi-arch emulation) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + - name: Build (and push on tag) + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..1e89453 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1.6 + +# ---- Stage 1: regenerate the per-phase JSONs from the Markdown sources ---- +FROM python:3.12-alpine AS builder +WORKDIR /build + +# Only stdlib is needed by the converter, no pip install required. +COPY src/dima_convert.py ./src/dima_convert.py +COPY DETECT/ ./DETECT/ +COPY INFORM/ ./INFORM/ +COPY MEMORISE/ ./MEMORISE/ +COPY ACT/ ./ACT/ +COPY docs/ ./docs/ + +RUN python src/dima_convert.py md2json --all -o docs/data \ + && rm -rf docs/data/.gitkeep 2>/dev/null || true + +# ---- Stage 2: serve the SPA via nginx (unprivileged image, listens on 8080) ---- +FROM nginxinc/nginx-unprivileged:alpine + +LABEL org.opencontainers.image.title="DIMA Navigator" \ + org.opencontainers.image.description="Static SPA serving the DIMA matrix (Detect/Inform/Memorise/Act)." \ + org.opencontainers.image.source="https://github.com/M82-project/DIMA" \ + org.opencontainers.image.licenses="see repository" + +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /build/docs /usr/share/nginx/html + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:8080/ >/dev/null || exit 1 diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6dca1f0 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,40 @@ +server { + listen 8080 default_server; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Active la compression text/json/css/js + gzip on; + gzip_vary on; + gzip_min_length 256; + gzip_proxied any; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + image/svg+xml; + + # HTML et JSON : pas de cache long, pour qu'un redeploiement + # se voie immediatement. + location ~* \.(html|json)$ { + add_header Cache-Control "no-cache, must-revalidate"; + try_files $uri =404; + } + + # Assets versionnes (fonts, images) : cache long. + location ~* \.(woff2?|ttf|otf|png|jpg|jpeg|gif|webp|svg|ico)$ { + add_header Cache-Control "public, max-age=2592000, immutable"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } + + # Pas d'index automatique d'un repertoire sans index.html. + autoindex off; +} From 681d79d9b00de98e289bd88e168494e3037642e8 Mon Sep 17 00:00:00 2001 From: Sebastien Larinier Date: Fri, 15 May 2026 23:03:27 +0200 Subject: [PATCH 2/5] ci(docker): :latest tracks main, :vX.Y.Z published on tag push Splits the two image streams so they never overwrite each other: - push to main (default branch) -> ghcr.io/.../dima:latest - push of a git tag v* -> ghcr.io/.../dima: A tagged release no longer touches :latest, and a regular merge into main no longer waits for a tag to refresh :latest. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b94a416..92fb897 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,6 +2,7 @@ name: Docker image on: push: + branches: [main] tags: ["v*"] pull_request: paths: @@ -45,7 +46,7 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=tag - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + type=raw,value=latest,enable={{is_default_branch}} - name: Build (and push on tag) uses: docker/build-push-action@v6 From b432e8ec7e304156450852ef6c16d8bc6f40a60b Mon Sep 17 00:00:00 2001 From: Sebastien Larinier Date: Fri, 15 May 2026 23:06:18 +0200 Subject: [PATCH 3/5] feat(docker): publish a sibling test image runnable with pytest Adds a tests stage to the Dockerfile alongside the existing nginx runtime. The stage installs pytest, copies the source and the four phase markdown files, then runs the suite as part of the build so a failing test breaks `docker build --target tests`. CMD points at pytest so the image is also usable as `docker run --rm `. The Docker workflow gains a second job that builds and publishes this test image, tagged with the same scheme as the runtime image plus a -tests suffix: main push -> ghcr.io/.../dima:latest + :latest-tests v1.2.3 push -> ghcr.io/.../dima:v1.2.3 + :v1.2.3-tests The test image is amd64-only (no need to emulate arm64 just to run pytest) and uses its own GHA cache scope. Both jobs only push outside PRs; on PR they only build, which still triggers the in-build pytest run via the tests stage and therefore catches regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker.yml | 60 ++++++++++++++++++++++++++++++++---- docker/Dockerfile | 26 +++++++++++++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 92fb897..11a86a4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,6 +8,11 @@ on: paths: - "docker/**" - ".github/workflows/docker.yml" + - "src/**" + - "DETECT/**" + - "INFORM/**" + - "MEMORISE/**" + - "ACT/**" workflow_dispatch: permissions: @@ -19,13 +24,14 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - build: + runtime: + name: Runtime image (nginx) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU (multi-arch emulation) + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -39,7 +45,7 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract image metadata + - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: @@ -48,14 +54,56 @@ jobs: type=ref,event=tag type=raw,value=latest,enable={{is_default_branch}} - - name: Build (and push on tag) + - name: Build (and push on tag or main) uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile + target: runtime platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=runtime + cache-to: type=gha,mode=max,scope=runtime + + tests: + name: Test image (pytest) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: suffix=-tests,onlatest=true + tags: | + type=ref,event=tag + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build (linux/amd64 only) and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: tests + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=tests + cache-to: type=gha,mode=max,scope=tests diff --git a/docker/Dockerfile b/docker/Dockerfile index 1e89453..cc7f214 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,11 +12,29 @@ COPY MEMORISE/ ./MEMORISE/ COPY ACT/ ./ACT/ COPY docs/ ./docs/ -RUN python src/dima_convert.py md2json --all -o docs/data \ - && rm -rf docs/data/.gitkeep 2>/dev/null || true +RUN python src/dima_convert.py md2json --all -o docs/data -# ---- Stage 2: serve the SPA via nginx (unprivileged image, listens on 8080) ---- -FROM nginxinc/nginx-unprivileged:alpine +# ---- Stage 2: pytest runner (build standalone with --target tests) ---- +FROM python:3.12-alpine AS tests +WORKDIR /app + +COPY pyproject.toml ./ +COPY src/ ./src/ +COPY DETECT/ ./DETECT/ +COPY INFORM/ ./INFORM/ +COPY MEMORISE/ ./MEMORISE/ +COPY ACT/ ./ACT/ + +RUN pip install --no-cache-dir pytest + +# Run the suite at build time so `docker build --target tests` fails on regression. +RUN pytest + +# And keep the same entrypoint for interactive runs (docker run --rm ). +CMD ["pytest", "-v"] + +# ---- Stage 3: nginx runtime (default target) ---- +FROM nginxinc/nginx-unprivileged:alpine AS runtime LABEL org.opencontainers.image.title="DIMA Navigator" \ org.opencontainers.image.description="Static SPA serving the DIMA matrix (Detect/Inform/Memorise/Act)." \ From 4bb20e34aa7fca6a4c31eb3192909afc7615099b Mon Sep 17 00:00:00 2001 From: Sebastien Larinier Date: Fri, 15 May 2026 23:18:38 +0200 Subject: [PATCH 4/5] fix(docker): address Copilot review and standardise on uv Five fixes raised by the Copilot review on this PR: - Switch both builder and tests stages to ghcr.io/astral-sh/uv:python3.12-alpine and drop `pip install pytest`. The tests stage now runs `uv sync --group dev` to pick up pytest with the version range pinned in pyproject.toml, then `uv run pytest` to execute it. Same toolchain (uv) everywhere. - nginx: re-apply Cache-Control: no-cache to the SPA fallback in `location /` so that /index.html served on unknown routes (e.g. /campaign/123) doesn't silently sit in a downstream cache after a redeploy. - workflow: add a `type=sha,prefix=sha-` fallback tag enabled on workflow_dispatch, and guard the build-push step with `steps.meta.outputs.tags != ''`, so a manual run from a non-default branch never tries to push with no tag. - New .dockerignore keeping .git/, the LaTeX article/, the published PDF, plugin/, misp/, .claude/ and various caches out of the build context. Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 34 ++++++++++++++++++++++++++++++++++ .github/workflows/docker.yml | 6 ++++-- docker/Dockerfile | 19 ++++++++++++------- docker/nginx.conf | 5 +++++ 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f07fb29 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Git +.git/ +.gitignore +.gitattributes + +# CI / IDE / harness metadata +.github/ +.vscode/ +.idea/ +.claude/ + +# Python / uv caches and virtualenvs +.venv/ +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Large or unrelated repo content +DIMA_V7.pdf +article/ +plugin/ +misp/ + +# Docs & licensing not needed at build time +README.md +LICENSE + +# Test outputs +_regen_misp/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 11a86a4..d9afe66 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -53,6 +53,7 @@ jobs: tags: | type=ref,event=tag type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=sha-,enable=${{ github.event_name == 'workflow_dispatch' }} - name: Build (and push on tag or main) uses: docker/build-push-action@v6 @@ -61,7 +62,7 @@ jobs: file: docker/Dockerfile target: runtime platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' && steps.meta.outputs.tags != '' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=runtime @@ -94,6 +95,7 @@ jobs: tags: | type=ref,event=tag type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=sha-,enable=${{ github.event_name == 'workflow_dispatch' }} - name: Build (linux/amd64 only) and push uses: docker/build-push-action@v6 @@ -102,7 +104,7 @@ jobs: file: docker/Dockerfile target: tests platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' && steps.meta.outputs.tags != '' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=tests diff --git a/docker/Dockerfile b/docker/Dockerfile index cc7f214..7acadf5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,10 +1,12 @@ # syntax=docker/dockerfile:1.6 # ---- Stage 1: regenerate the per-phase JSONs from the Markdown sources ---- -FROM python:3.12-alpine AS builder +# Uses the official uv image (Python 3.12 on Alpine) so the same toolchain +# is used in every stage. The converter itself depends on stdlib only, +# so we invoke python directly via `uv run --no-project`. +FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder WORKDIR /build -# Only stdlib is needed by the converter, no pip install required. COPY src/dima_convert.py ./src/dima_convert.py COPY DETECT/ ./DETECT/ COPY INFORM/ ./INFORM/ @@ -12,10 +14,12 @@ COPY MEMORISE/ ./MEMORISE/ COPY ACT/ ./ACT/ COPY docs/ ./docs/ -RUN python src/dima_convert.py md2json --all -o docs/data +RUN uv run --no-project python src/dima_convert.py md2json --all -o docs/data # ---- Stage 2: pytest runner (build standalone with --target tests) ---- -FROM python:3.12-alpine AS tests +# Pytest version comes from pyproject.toml's [dependency-groups].dev, +# so the build stays reproducible (no implicit pytest latest). +FROM ghcr.io/astral-sh/uv:python3.12-alpine AS tests WORKDIR /app COPY pyproject.toml ./ @@ -25,13 +29,14 @@ COPY INFORM/ ./INFORM/ COPY MEMORISE/ ./MEMORISE/ COPY ACT/ ./ACT/ -RUN pip install --no-cache-dir pytest +# Synchronise the dev group (pytest) into /app/.venv. +RUN uv sync --group dev # Run the suite at build time so `docker build --target tests` fails on regression. -RUN pytest +RUN uv run pytest # And keep the same entrypoint for interactive runs (docker run --rm ). -CMD ["pytest", "-v"] +CMD ["uv", "run", "pytest", "-v"] # ---- Stage 3: nginx runtime (default target) ---- FROM nginxinc/nginx-unprivileged:alpine AS runtime diff --git a/docker/nginx.conf b/docker/nginx.conf index 6dca1f0..251d1c0 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -33,6 +33,11 @@ server { location / { try_files $uri $uri/ /index.html; + # Le fallback SPA sert /index.html sur des routes type /campaign/123 : + # comme la location regex ci-dessus ne matche pas (pas de .html dans + # l'URL), il faut redonner le no-cache ici pour que les redeploys + # soient visibles tout de suite. + add_header Cache-Control "no-cache, must-revalidate" always; } # Pas d'index automatique d'un repertoire sans index.html. From befbfb9d786a2228ff35875c8cd5d5bcffc5ce69 Mon Sep 17 00:00:00 2001 From: Sebastien Larinier Date: Sat, 16 May 2026 00:07:14 +0200 Subject: [PATCH 5/5] fix(docker): lockfile-frozen tests image and broader PR path filter Two remaining Copilot remarks: - Commit uv.lock and drop the .gitignore entry for it. The tests stage now does `uv sync --group dev --frozen`, so rebuilding the test image from the same commit always resolves the exact pytest version that passed CI (pytest>=8 alone could otherwise drift). - Extend the docker.yml pull_request path filter to also watch docs/** and pyproject.toml (and uv.lock now that it's tracked). Both feed into the Docker build (COPY docs/ in runtime, uv sync in tests), so a PR touching only the SPA assets or dependency config should still validate the Docker build. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker.yml | 3 + .gitignore | 1 - docker/Dockerfile | 8 +- uv.lock | 156 +++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 uv.lock diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d9afe66..21d7bf9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,10 +9,13 @@ on: - "docker/**" - ".github/workflows/docker.yml" - "src/**" + - "docs/**" - "DETECT/**" - "INFORM/**" - "MEMORISE/**" - "ACT/**" + - "pyproject.toml" + - "uv.lock" workflow_dispatch: permissions: diff --git a/.gitignore b/.gitignore index d35de5a..4e7d347 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ env/ # uv .uv/ -uv.lock # Tests / outils .pytest_cache/ diff --git a/docker/Dockerfile b/docker/Dockerfile index 7acadf5..deb905f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,15 +22,17 @@ RUN uv run --no-project python src/dima_convert.py md2json --all -o docs/data FROM ghcr.io/astral-sh/uv:python3.12-alpine AS tests WORKDIR /app -COPY pyproject.toml ./ +COPY pyproject.toml uv.lock ./ COPY src/ ./src/ COPY DETECT/ ./DETECT/ COPY INFORM/ ./INFORM/ COPY MEMORISE/ ./MEMORISE/ COPY ACT/ ./ACT/ -# Synchronise the dev group (pytest) into /app/.venv. -RUN uv sync --group dev +# Synchronise the dev group (pytest) into /app/.venv with the exact +# versions resolved in uv.lock (--frozen). The image is reproducible: +# rebuilding from the same commit always pulls the same pytest. +RUN uv sync --group dev --frozen # Run the suite at build time so `docker build --target tests` fails on regression. RUN uv run pytest diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a8b8fe6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,156 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dima" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8" }] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]