From 49992b3a26367ffd38facc5cd0bff9f7b616bd2a Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 15 May 2026 22:04:29 +0200 Subject: [PATCH] refactor(repo): uv workspace + hatchling + layout restructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate from pip-install-e + setuptools to a uv workspace with a committed uv.lock for deterministic deps. Switch both members to hatchling, and move package sources into nested standard layout (l4d2host/l4d2host/, l4d2web/l4d2web/) so builds work from a read-only source tree — setuptools wrote egg-info to source under the old layout, which broke uv sync on the root-owned /opt/left4me/src. Local dev install: `pip install -e ./l4d2host -e ./l4d2web` -> `uv sync`. .envrc switches from `layout python python3.13` to `use uv`. Python pinned to 3.13 via .python-version. l4d2web now declares its cross-dep on l4d2host explicitly via [tool.uv.sources] (workspace = true). l4d2web/alembic.ini and l4d2web/alembic/ stay at the project root (standard alembic layout). Test fixes: - tests/__init__.py added to both test dirs so pytest doesn't shadow l4d2host as a namespace package via outer-dir walk. - 3 CWD-relative paths in tests (l4d2web/static/css/{tokens,layout}.css and js/sse.js) anchored to Path(__file__) so they survive layout changes. - Two test_install.py tests now monkeypatch HOME to tmp_path so they stop silently mutating ~/.steam/sdk32 on every run. 628 tests pass under sandboxed `uv run pytest`. Per docs/superpowers/plans/2026-05-15-uv-workspace-execution.md; prereq for the ckn-bw bundle's uv-sync action (queued). Co-Authored-By: Claude Opus 4.7 --- .envrc | 2 +- .python-version | 1 + AGENTS.md | 2 +- README.md | 13 +- .../2026-05-15-uv-workspace-execution.md | 408 +++++++++++++ .../specs/2026-05-15-handoff-uv-workspace.md | 11 +- l4d2host/{ => l4d2host}/__init__.py | 0 l4d2host/{ => l4d2host}/cli.py | 0 l4d2host/{ => l4d2host}/instances.py | 0 l4d2host/{ => l4d2host}/logging.py | 0 l4d2host/{ => l4d2host}/logs.py | 0 l4d2host/{ => l4d2host}/paths.py | 0 l4d2host/{ => l4d2host}/process.py | 0 l4d2host/{ => l4d2host}/service_control.py | 0 l4d2host/{ => l4d2host}/spec.py | 0 l4d2host/{ => l4d2host}/status.py | 0 l4d2host/{ => l4d2host}/steam_install.py | 0 l4d2host/{ => l4d2host}/templates/__init__.py | 0 l4d2host/pyproject.toml | 12 +- .../routes => l4d2host/tests}/__init__.py | 0 l4d2host/tests/test_install.py | 5 +- l4d2web/README.md | 7 +- l4d2web/{ => l4d2web}/__init__.py | 0 l4d2web/{ => l4d2web}/app.py | 0 l4d2web/{ => l4d2web}/auth.py | 0 l4d2web/{ => l4d2web}/cli.py | 0 l4d2web/{ => l4d2web}/config.py | 0 l4d2web/{ => l4d2web}/db.py | 0 l4d2web/{ => l4d2web}/models.py | 0 .../{services => l4d2web/routes}/__init__.py | 0 l4d2web/{ => l4d2web}/routes/auth_routes.py | 0 .../{ => l4d2web}/routes/blueprint_routes.py | 0 .../{ => l4d2web}/routes/console_routes.py | 0 l4d2web/{ => l4d2web}/routes/files_routes.py | 0 l4d2web/{ => l4d2web}/routes/job_routes.py | 0 l4d2web/{ => l4d2web}/routes/log_routes.py | 0 .../{ => l4d2web}/routes/overlay_routes.py | 0 l4d2web/{ => l4d2web}/routes/page_routes.py | 0 .../{ => l4d2web}/routes/profile_routes.py | 0 l4d2web/{ => l4d2web}/routes/server_routes.py | 0 .../{ => l4d2web}/routes/workshop_routes.py | 0 l4d2web/l4d2web/services/__init__.py | 0 .../{ => l4d2web}/services/host_commands.py | 0 l4d2web/{ => l4d2web}/services/job_worker.py | 0 l4d2web/{ => l4d2web}/services/l4d2_facade.py | 0 .../services/live_state_poller.py | 0 .../services/overlay_builders.py | 0 .../services/overlay_creation.py | 0 .../{ => l4d2web}/services/overlay_files.py | 0 l4d2web/{ => l4d2web}/services/rate_limit.py | 0 l4d2web/{ => l4d2web}/services/rcon.py | 0 l4d2web/{ => l4d2web}/services/security.py | 0 .../{ => l4d2web}/services/server_identity.py | 0 l4d2web/{ => l4d2web}/services/spec_yaml.py | 0 l4d2web/{ => l4d2web}/services/status.py | 0 l4d2web/{ => l4d2web}/services/steam_users.py | 0 .../{ => l4d2web}/services/steam_workshop.py | 0 l4d2web/{ => l4d2web}/services/timeago.py | 0 .../{ => l4d2web}/services/workshop_paths.py | 0 .../{ => l4d2web}/static/css/components.css | 0 l4d2web/{ => l4d2web}/static/css/layout.css | 0 l4d2web/{ => l4d2web}/static/css/logs.css | 0 l4d2web/{ => l4d2web}/static/css/tokens.css | 0 .../static/js/blueprint-overlay-picker.js | 0 .../static/js/console-history.js | 0 l4d2web/{ => l4d2web}/static/js/csrf.js | 0 l4d2web/{ => l4d2web}/static/js/file-tree.js | 0 .../{ => l4d2web}/static/js/files-overlay.js | 0 l4d2web/{ => l4d2web}/static/js/modal.js | 0 .../static/js/password-reveal.js | 0 l4d2web/{ => l4d2web}/static/js/sse.js | 0 .../{ => l4d2web}/static/vendor/htmx.min.js | 0 .../templates/_console_line.html | 0 .../{ => l4d2web}/templates/_job_table.html | 0 .../{ => l4d2web}/templates/_live_state.html | 0 .../templates/_overlay_build_status.html | 0 .../templates/_overlay_file_node.html | 0 .../templates/_overlay_file_tree.html | 0 .../templates/_overlay_item_table.html | 0 .../templates/_server_actions.html | 0 l4d2web/{ => l4d2web}/templates/admin.html | 0 .../{ => l4d2web}/templates/admin_jobs.html | 0 .../{ => l4d2web}/templates/admin_users.html | 0 l4d2web/{ => l4d2web}/templates/base.html | 0 .../templates/blueprint_detail.html | 0 .../{ => l4d2web}/templates/blueprints.html | 0 .../{ => l4d2web}/templates/dashboard.html | 0 .../{ => l4d2web}/templates/job_detail.html | 0 l4d2web/{ => l4d2web}/templates/login.html | 0 .../templates/overlay_detail.html | 0 .../{ => l4d2web}/templates/overlay_jobs.html | 0 l4d2web/{ => l4d2web}/templates/overlays.html | 0 l4d2web/{ => l4d2web}/templates/profile.html | 0 .../templates/server_detail.html | 0 .../{ => l4d2web}/templates/server_jobs.html | 0 l4d2web/{ => l4d2web}/templates/servers.html | 0 l4d2web/pyproject.toml | 22 +- l4d2web/tests/__init__.py | 0 l4d2web/tests/test_job_logs.py | 2 +- l4d2web/tests/test_pages.py | 6 +- pyproject.toml | 23 + uv.lock | 576 ++++++++++++++++++ 102 files changed, 1045 insertions(+), 45 deletions(-) create mode 100644 .python-version create mode 100644 docs/superpowers/plans/2026-05-15-uv-workspace-execution.md rename l4d2host/{ => l4d2host}/__init__.py (100%) rename l4d2host/{ => l4d2host}/cli.py (100%) rename l4d2host/{ => l4d2host}/instances.py (100%) rename l4d2host/{ => l4d2host}/logging.py (100%) rename l4d2host/{ => l4d2host}/logs.py (100%) rename l4d2host/{ => l4d2host}/paths.py (100%) rename l4d2host/{ => l4d2host}/process.py (100%) rename l4d2host/{ => l4d2host}/service_control.py (100%) rename l4d2host/{ => l4d2host}/spec.py (100%) rename l4d2host/{ => l4d2host}/status.py (100%) rename l4d2host/{ => l4d2host}/steam_install.py (100%) rename l4d2host/{ => l4d2host}/templates/__init__.py (100%) rename {l4d2web/routes => l4d2host/tests}/__init__.py (100%) rename l4d2web/{ => l4d2web}/__init__.py (100%) rename l4d2web/{ => l4d2web}/app.py (100%) rename l4d2web/{ => l4d2web}/auth.py (100%) rename l4d2web/{ => l4d2web}/cli.py (100%) rename l4d2web/{ => l4d2web}/config.py (100%) rename l4d2web/{ => l4d2web}/db.py (100%) rename l4d2web/{ => l4d2web}/models.py (100%) rename l4d2web/{services => l4d2web/routes}/__init__.py (100%) rename l4d2web/{ => l4d2web}/routes/auth_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/blueprint_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/console_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/files_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/job_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/log_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/overlay_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/page_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/profile_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/server_routes.py (100%) rename l4d2web/{ => l4d2web}/routes/workshop_routes.py (100%) create mode 100644 l4d2web/l4d2web/services/__init__.py rename l4d2web/{ => l4d2web}/services/host_commands.py (100%) rename l4d2web/{ => l4d2web}/services/job_worker.py (100%) rename l4d2web/{ => l4d2web}/services/l4d2_facade.py (100%) rename l4d2web/{ => l4d2web}/services/live_state_poller.py (100%) rename l4d2web/{ => l4d2web}/services/overlay_builders.py (100%) rename l4d2web/{ => l4d2web}/services/overlay_creation.py (100%) rename l4d2web/{ => l4d2web}/services/overlay_files.py (100%) rename l4d2web/{ => l4d2web}/services/rate_limit.py (100%) rename l4d2web/{ => l4d2web}/services/rcon.py (100%) rename l4d2web/{ => l4d2web}/services/security.py (100%) rename l4d2web/{ => l4d2web}/services/server_identity.py (100%) rename l4d2web/{ => l4d2web}/services/spec_yaml.py (100%) rename l4d2web/{ => l4d2web}/services/status.py (100%) rename l4d2web/{ => l4d2web}/services/steam_users.py (100%) rename l4d2web/{ => l4d2web}/services/steam_workshop.py (100%) rename l4d2web/{ => l4d2web}/services/timeago.py (100%) rename l4d2web/{ => l4d2web}/services/workshop_paths.py (100%) rename l4d2web/{ => l4d2web}/static/css/components.css (100%) rename l4d2web/{ => l4d2web}/static/css/layout.css (100%) rename l4d2web/{ => l4d2web}/static/css/logs.css (100%) rename l4d2web/{ => l4d2web}/static/css/tokens.css (100%) rename l4d2web/{ => l4d2web}/static/js/blueprint-overlay-picker.js (100%) rename l4d2web/{ => l4d2web}/static/js/console-history.js (100%) rename l4d2web/{ => l4d2web}/static/js/csrf.js (100%) rename l4d2web/{ => l4d2web}/static/js/file-tree.js (100%) rename l4d2web/{ => l4d2web}/static/js/files-overlay.js (100%) rename l4d2web/{ => l4d2web}/static/js/modal.js (100%) rename l4d2web/{ => l4d2web}/static/js/password-reveal.js (100%) rename l4d2web/{ => l4d2web}/static/js/sse.js (100%) rename l4d2web/{ => l4d2web}/static/vendor/htmx.min.js (100%) rename l4d2web/{ => l4d2web}/templates/_console_line.html (100%) rename l4d2web/{ => l4d2web}/templates/_job_table.html (100%) rename l4d2web/{ => l4d2web}/templates/_live_state.html (100%) rename l4d2web/{ => l4d2web}/templates/_overlay_build_status.html (100%) rename l4d2web/{ => l4d2web}/templates/_overlay_file_node.html (100%) rename l4d2web/{ => l4d2web}/templates/_overlay_file_tree.html (100%) rename l4d2web/{ => l4d2web}/templates/_overlay_item_table.html (100%) rename l4d2web/{ => l4d2web}/templates/_server_actions.html (100%) rename l4d2web/{ => l4d2web}/templates/admin.html (100%) rename l4d2web/{ => l4d2web}/templates/admin_jobs.html (100%) rename l4d2web/{ => l4d2web}/templates/admin_users.html (100%) rename l4d2web/{ => l4d2web}/templates/base.html (100%) rename l4d2web/{ => l4d2web}/templates/blueprint_detail.html (100%) rename l4d2web/{ => l4d2web}/templates/blueprints.html (100%) rename l4d2web/{ => l4d2web}/templates/dashboard.html (100%) rename l4d2web/{ => l4d2web}/templates/job_detail.html (100%) rename l4d2web/{ => l4d2web}/templates/login.html (100%) rename l4d2web/{ => l4d2web}/templates/overlay_detail.html (100%) rename l4d2web/{ => l4d2web}/templates/overlay_jobs.html (100%) rename l4d2web/{ => l4d2web}/templates/overlays.html (100%) rename l4d2web/{ => l4d2web}/templates/profile.html (100%) rename l4d2web/{ => l4d2web}/templates/server_detail.html (100%) rename l4d2web/{ => l4d2web}/templates/server_jobs.html (100%) rename l4d2web/{ => l4d2web}/templates/servers.html (100%) create mode 100644 l4d2web/tests/__init__.py create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.envrc b/.envrc index 63e22a3..2ffd149 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -layout python python3.13 +use uv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/AGENTS.md b/AGENTS.md index b27c78b..5270241 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ Do not invent architecture outside these plans unless explicitly requested. ### Workspace and tools - Do not use git worktrees. -- Local Python venv is direnv-managed via `.envrc` (Python 3.13). After fresh checkout: `direnv allow`, then `pip install -e ./l4d2host -e ./l4d2web pytest`. See README **Local development** for details. +- Repo is a uv workspace; Python is pinned to 3.13 via `.python-version`. After fresh checkout: install `uv` (`brew install uv` / `curl -LsSf https://astral.sh/uv/install.sh | sh`), then `direnv allow` (or `uv sync` directly). See README **Local development** for details. ### Planning artifacts diff --git a/README.md b/README.md index 15266a2..84622c1 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,17 @@ See `deploy/README.md` for the Linux test deployment contract, including the run ## Local development -This repo uses [direnv](https://direnv.net/) to auto-activate a Python 3.13 venv on `cd` (matching the Debian Trixie production target). With direnv installed and hooked into your shell: +This repo is a [uv](https://docs.astral.sh/uv/) workspace (`l4d2host` + `l4d2web` as members) with a committed `uv.lock` and a `.python-version` pinning Python 3.13 (matching the Debian Trixie production target). -1. `direnv allow` once per fresh checkout (and after any `.envrc` change). -2. `cd` out and back in — `.direnv/python-3.13/` is created and put on `PATH`. -3. `pip install -e ./l4d2host -e ./l4d2web` to install both packages editable. -4. `pip install pytest` to run the test suites (`pytest tests/` inside either subproject). +One-time prereq: install `uv` (macOS: `brew install uv`; Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh` — `uv` is not yet in Debian stable's apt). + +1. `direnv allow` once per fresh checkout (and after any `.envrc` change). `.envrc` uses `use uv`, which runs `uv sync` and activates `.venv/` on `cd`. +2. Without direnv: `uv sync` at the repo root creates `.venv/`, installs both workspace members editable, and pulls in dev deps (pytest) from the lockfile. +3. Tests: `uv run pytest` (or just `pytest` once the venv is on PATH). ## Tech Stack (planned) -- Python 3.12+ +- Python 3.13+ (workspace uses uv + hatchling) - Typer, PyYAML, pytest - Flask, SQLAlchemy, Alembic - HTMX (vendored locally), custom CSS, SSE diff --git a/docs/superpowers/plans/2026-05-15-uv-workspace-execution.md b/docs/superpowers/plans/2026-05-15-uv-workspace-execution.md new file mode 100644 index 0000000..2fc0a1f --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-uv-workspace-execution.md @@ -0,0 +1,408 @@ +# Plan — collapse left4me venv chain into uv workspace + `uv sync` + +**Status:** executed (left4me side). ckn-bw side queued — see +`~/Projekte/ckn-bw/bundles/left4me/` and the matching section below. + +**Notable deviations from the original handoff +(`docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md`):** + +- Handoff assumed `pkg_apt: uv` works on Debian Trixie. It does not — uv + is in `experimental`/`sid` only. Replaced with a `left4me_install_uv` + action that downloads a pinned 0.11.8 tarball from astral-sh/uv + releases, SHA256-verifies, installs to `/usr/local/bin/`. +- Handoff assumed the existing layout (`l4d2host/pyproject.toml` with + `package-dir = "."`) was workspace-compatible. It was not — setuptools + writes `egg-info/` to source during any build, which fails on the + root-owned `/opt/left4me/src` tree. Required layout restructure to + `l4d2host/l4d2host/` (package source nested) plus a switch from + setuptools to hatchling. +- `git` is not installed on the prod host (bw drives git from the + control machine). Verification check #1 uses `find` for build + artifacts instead of `git status`. + +## Context + +The production deploy of left4me to `ovh.left4me` currently uses a 5-action +chain in `ckn-bw/bundles/left4me/items.py` that builds out a Python venv +under `/var/lib/left4me/.venv` by chaining `python3 -m venv` → `pip upgrade` +→ `pip install` (with an 8-line tempdir-copy dance because the source at +`/opt/left4me/src` is root-owned and setuptools wants to write `.egg-info/` +into it) → `alembic upgrade` → `seed_overlays`. The chain has three +problems: + +1. **Non-deterministic prod deploys.** `pip install` resolves whatever is + latest at apply time. A transitive CVE-relevant bump between two + `bw apply` runs is invisible until something breaks. +2. **Cognitive cost.** The tempdir-copy in `left4me_pip_install` is the + single longest, gnarliest action in the bundle. +3. **Implicit cross-package dep.** `l4d2web` imports from `l4d2host.paths` + in 5 files but doesn't declare the dependency — today's setup works + only because both get `pip install -e`'d side-by-side. + +This plan migrates the repo to a uv workspace with a committed `uv.lock`, +replacing the 5-action chain with `left4me_install_uv` (download + +SHA256 verify, idempotent — only re-runs on version change) plus +`left4me_uv_sync`. On the steady-state path (uv already pinned at +0.11.8), only `uv_sync` fires per deploy. Both sides of the change +(left4me repo and the ckn-bw `left4me` bundle) ship together. The plan +executes the migration sequence already documented in +`/Users/mwiegand/Projekte/left4me/docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md` +— treat that handoff as the design document. This plan adds the +empirically-verified ground truth, resolves the small open questions, and +encodes the executable sequence. + +## Source of truth + +- **Design**: `docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md` + (in the left4me repo) — read this first; do not duplicate its content + here. +- **Sibling context** (don't dive in): + `docs/superpowers/specs/2026-05-15-deployment-responsibility-design.md` + (just-shipped; left the venv chain alone), + `docs/superpowers/specs/2026-05-15-runtime-state-relocation-design.md` + (made `/opt/left4me/src` root-owned, which is *why* the current + tempdir-copy dance exists). + +## Resolved questions (from planning) + +- **Branch flow**: direct-to-master on both sides. (Matches left4me's + recent workflow, e.g. `b13d164`, `55b0138`. ckn-bw side committed + but NOT pushed — operator pushes manually.) +- **Python version alignment**: align all three pyprojects (root + both + members) to `requires-python = ">=3.13"`. Matches `.envrc` and the + production host. Removes the workspace-vs-member skew. +- **Spike test scope**: extend beyond the handoff to also dry-run a + `uv sync --frozen` shape against a root-owned source — the production + command path is `sync`, not `build`, and they're different code paths. +- **Scope handoff at `git push`**: agent's deliverable is two ready-to-deploy + commits (left4me pushed; ckn-bw committed but unpushed). The user runs + `bw apply ovh.left4me`, the post-apply restart, and the 6-check + verification matrix themselves. (Per session memory: + `feedback_left4me_deploy_workflow` — supersedes the original prompt's + ask to drive apply + verify end-to-end.) The spike test remains agent + work — it's information gathering, and the one-shot direct install + fits the "one-shot via direct command" rule from the same memory. +- **uv install vector**: direct GitHub tarball download + SHA256 verify + against the official `.sha256` sibling, install to `/usr/local/bin/`. + The handoff doc's `pkg_apt: uv` assumption was wrong — uv is not in + Debian Trixie's apt archive (in `experimental`/`sid` only). Astral's + canonical methods are curl-pipe-sh and direct tarball; we chose + tarball for auditability and pattern-match with the existing + `left4me_install_steamcmd` action. Pin to **uv 0.11.8** to match + the local brew-installed version, eliminating the lockfile-format-skew + risk between dev and prod. + +## Ground-truth from exploration + +- **Cross-package imports confirmed**: 5 files in `l4d2web/` import + `from l4d2host.paths`: + - `l4d2web/routes/overlay_routes.py` + - `l4d2web/services/overlay_creation.py` + - `l4d2web/services/overlay_builders.py` + - `l4d2web/services/overlay_files.py` + - `l4d2web/services/workshop_paths.py` +- **Layout compatibility**: both members use `[tool.setuptools.package-dir] + {name} = "."` (pyproject lives inside the package directory). uv + workspace `members = ["l4d2host", "l4d2web"]` handles this fine — uv + uses the pyproject as the project root regardless of the package-dir + mapping. +- **`.gitignore` already covers** `*.egg-info/`, `.venv/`, `__pycache__/`, + etc. No `.gitignore` changes needed. +- **No `pytest.ini` / `[tool.pytest.ini_options]` exists** — pytest + defaults work; `uv run pytest` from repo root will discover tests in + `l4d2host/tests/` and `l4d2web/tests/`. +- **Bundle action conventions** (from `ckn-bw/bundles/left4me/items.py` + and neighbors): every action sets `cascade_skip: False` explicitly. + Action keys in use: `command`, `triggered`, `cascade_skip`, `unless`, + `needs`, `triggers`, `comment`. +- **Additional `git_deploy` consumer**: `left4me_chmod_scripts` at + `items.py:324` also `needs: 'git_deploy:/opt/left4me/src'`. Untouched + by this refactor, but listed here so it's not missed during review. +- **Bundle README §"deploy-flow"**: lines 84–90 of + `bundles/left4me/README.md` document the pip_install tempdir dance. + This is the prose to rewrite (not vague — those exact lines). +- **`apt.packages`** declaration: `metadata.py:29–49`. Currently lists + `python3`, `python3-venv`, `python3-pip`, `python3-dev`, plus i386 + multiarch entries. +- **uv NOT in Debian Trixie apt archive** (verified via + `apt-cache search "^uv$"` and `apt-cache policy uv` on the live host + — both return nothing for the actual `uv` package). Handoff doc's + assumption was wrong on this point. +- **`git` is NOT installed on the production host** (verified via + `command -v git` on prod returning empty; `/usr/bin/git` doesn't + exist). The bw `git_deploy` item operates from the *control* machine + (dev laptop), pushing files to prod via SSH — prod itself needs no + git. Implication: the handoff's verification check #1 + (`sudo git -C /opt/left4me/src status --porcelain`) cannot be used. + Replace with `find /opt/left4me/src \( -name '*.egg-info' -o -name + build -o -name dist \) -print`. +- **ckn-bw is currently EVEN with `origin/master`** (verified via + `git status -sb` showing `## master...origin/master` with empty + log). The original prompt's "7 commits ahead" was stale — the + operator has since pushed. After our ckn-bw commit lands locally, + the repo will be 1 commit ahead (not 8). +- **Prod arch**: `x86_64` / `amd64`. **Prod curl**: 8.14.1 at + `/usr/bin/curl`. **Prod tar**: GNU tar 1.35. **Prod install**: GNU + coreutils 9.7. **`/usr/local/bin`** exists, root-owned, currently + contains only the `downtime` binary. +- **Current prod venv state**: `/var/lib/left4me/.venv/` exists, owned + by `left4me:left4me`, contains `python3.13`, `pip`, `alembic`, + `flask`, `gunicorn`, `l4d2ctl`. `pip show l4d2host` / `pip show + l4d2web` both report version 0.1.0. So uv will be adopting a venv + that already has working installs of both members + their deps. +- **Local dev environment**: `uv 0.11.8` (brew), `direnv 2.37.1` + (supports `use uv`), `python 3.13.13`. No `.venv` exists locally yet + — clean slate. + +## Critical files + +### left4me repo +- **NEW** `/Users/mwiegand/Projekte/left4me/pyproject.toml` — workspace root +- **NEW** `/Users/mwiegand/Projekte/left4me/uv.lock` — generated via `uv lock` +- `l4d2host/pyproject.toml:10` — bump `requires-python` to `>=3.13` +- `l4d2web/pyproject.toml:10–18` — bump `requires-python`, add + `"l4d2host"` to `dependencies`, add `[tool.uv.sources] l4d2host = { workspace = true }` +- `.envrc` — replace `layout python python3.13` with `use uv` (with + fallback if direnv stdlib is too old) +- `README.md`, `AGENTS.md`, `l4d2web/README.md` — update install + instructions + +### ckn-bw repo (`~/Projekte/ckn-bw/`) +- `bundles/left4me/metadata.py:29–49` — **ensure** `'curl': {}` is in + `apt.packages` (required by the new install action; verify it's not + already inherited from a base bundle). **Drop** `'python3-pip'` (uv + replaces pip; bundle has no other consumer). **Drop** `'python3-venv'` + (the chain no longer uses `python3 -m venv`; uv creates its own venv + via `UV_PROJECT_ENVIRONMENT`). **Keep** `'python3'`, `'python3-dev'`, + and the i386 multiarch entries. + **Do NOT add** `'uv': {}` — uv is not in Trixie's apt archive. +- `bundles/left4me/items.py:285–305` — update `git_deploy:/opt/left4me/src` + triggers: replace `action:left4me_pip_install` with + `action:left4me_uv_sync` +- `bundles/left4me/items.py:328–340` — **DELETE** `left4me_create_venv` +- `bundles/left4me/items.py:342–352` — **DELETE** `left4me_pip_upgrade` +- `bundles/left4me/items.py:354–382` — **DELETE** `left4me_pip_install` + (replaced by `left4me_uv_sync` below) +- `bundles/left4me/items.py:384–407` — `left4me_alembic_upgrade`: + update `needs:` (or `triggered_by:` equivalent) to point at + `action:left4me_uv_sync` instead of `action:left4me_pip_install` +- `bundles/left4me/items.py` — **ADD** two new actions: + - `left4me_install_uv`: download pinned 0.11.8 tarball from + github.com/astral-sh/uv/releases/, SHA256-verify, install to + /usr/local/bin/. Idempotent via `unless: '/usr/local/bin/uv --version + | grep -qx "uv 0.11.8"'`. `needs: ['pkg_apt:curl']`, + `triggers: ['action:left4me_uv_sync']`. (Body matches the approved + preview, with `unless:` refined to `grep -qx` for BRE portability.) + - `left4me_uv_sync`: `sudo -u left4me env + UV_PROJECT_ENVIRONMENT=/var/lib/left4me/.venv /usr/local/bin/uv + sync --frozen --project /opt/left4me/src`. `triggered: True`, + `cascade_skip: False`, `needs:` includes + `'git_deploy:/opt/left4me/src'`, `'action:left4me_install_uv'`, + `'directory:/var/lib/left4me'`, `'user:left4me'`. `triggers: + ['action:left4me_alembic_upgrade']`. +- `bundles/left4me/README.md:84–90` — rewrite the deploy-flow description + to mention the install_uv + uv_sync chain instead of the tempdir-dance + +## Execution steps + +### Step 0 — Spike test (extended) — DO FIRST +Verify the architectural assumption empirically on the live host. +Uses the SAME install vector the production action will use (direct +tarball + SHA256 verify), so the spike doubles as a smoke test for +the install action itself. + +```bash +# A. Install pinned uv on prod (one-shot via direct command; matches +# what the future bw action will do). +ssh ckn@left4.me ' + set -e + tmpdir=$(mktemp -d); trap "rm -rf $tmpdir" EXIT + base=https://github.com/astral-sh/uv/releases/download/0.11.8 + tar=uv-x86_64-unknown-linux-gnu.tar.gz + curl -fsSL -o $tmpdir/$tar $base/$tar + curl -fsSL -o $tmpdir/$tar.sha256 $base/$tar.sha256 + (cd $tmpdir && sha256sum -c $tar.sha256) + tar -xzf $tmpdir/$tar -C $tmpdir --strip-components=1 + sudo install -m 0755 $tmpdir/uv /usr/local/bin/uv + sudo install -m 0755 $tmpdir/uvx /usr/local/bin/uvx + /usr/local/bin/uv --version +' + +# B. uv build against root-owned source: source must stay clean. +ssh ckn@left4.me ' + sudo -u left4me sh -c " + wheels=\$(mktemp -d) + /usr/local/bin/uv build --wheel --sdist /opt/left4me/src/l4d2host --out-dir \$wheels + ls \$wheels + " +' +# Cleanliness probe — git not on prod, so use find for build artifacts. +# Expected: only-existing egg-info dirs (the ones already on disk from +# the current pip install -e flow); NO NEW artifacts from this run. +# Capture a baseline BEFORE the build, compare AFTER. +ssh ckn@left4.me 'sudo find /opt/left4me/src \( -name "*.egg-info" -o -name build -o -name dist -o -name "__pycache__" \) -printf "%T@ %p\n" | sort' + +# C. Extended sync-shape check — dry-run `uv sync --frozen` against a +# root-owned workspace mock in /tmp. Verify the project root stays +# clean (no .python-version written, no transient files left over). +# This validates that `uv sync` (not just `uv build`) is safe against +# a read-only project tree, which is the actual production code path. +``` + +**Decision gate**: +- Source stays clean across B and C → proceed with full plan. +- New `*.egg-info` / `build/` / `dist/` directories appear in + `/opt/left4me/src` after `uv build` → fall back to **Medium scope** + (handoff §"Empirical spike" → fallback). Update the handoff doc to + record the fallback decision and re-plan. +- `uv sync` writes into the project root during step C → also fall back + to Medium scope. Same handoff update. + +### Step 1 — left4me workspace setup (local) +1. Write `/Users/mwiegand/Projekte/left4me/pyproject.toml` (workspace root) + — see handoff §"What changes — left4me side / New: pyproject.toml" +2. Bump `l4d2host/pyproject.toml:10` to `requires-python = ">=3.13"` +3. Update `l4d2web/pyproject.toml`: bump `requires-python`, add + `"l4d2host"` to `dependencies`, append `[tool.uv.sources]` block +4. `uv lock` at the repo root → produces `uv.lock` +5. `uv sync` → creates `.venv/`, installs both members editable + pytest +6. `uv run pytest` → all green +7. Update `.envrc`: replace `layout python python3.13` with `use uv` + (fallback to `uv sync >/dev/null && source .venv/bin/activate` if + the dev's direnv version doesn't ship `use uv`) +8. Update `README.md`, `AGENTS.md`, `l4d2web/README.md`: replace the + `pip install -e ...` invocation with `uv sync` and add the one-time + prereq line about installing uv. Mention macOS (`brew install uv`) + and Linux (curl-pipe-sh from astral.sh) — **do NOT** suggest + `apt install uv`, as it's not in Debian's apt archive yet (only + `experimental`/`sid`). + +### Step 2 — left4me commit + push +Single commit using the suggested message from the handoff +(§"Commit messages — left4me side"). Push to `origin` (gitlab on +sublimity.de — confirmed safe-publish-exempt per memory). The commit +makes the workspace and lockfile available to ckn-bw's `git_deploy`. + +### Step 3 — ckn-bw bundle refactor +1. Edit `bundles/left4me/metadata.py:29–49`: + - Ensure `'curl': {}` is in `apt.packages` (verify it's not already + inherited from a base bundle; if not, add it explicitly). + - Drop `'python3-pip'` (uv replaces pip; bundle has no other + consumer — grep the bundle to confirm). + - Drop `'python3-venv'` (chain no longer uses `python3 -m venv`). + - Keep `'python3'`, `'python3-dev'`, and the i386 multiarch entries. + - **Do NOT add `'uv': {}`** — not in Trixie's apt. +2. Edit `bundles/left4me/items.py`: + - Delete `left4me_create_venv`, `left4me_pip_upgrade`, + `left4me_pip_install` blocks (lines 328–382 inclusive). + - Add `left4me_install_uv` action: downloads pinned uv 0.11.8 tarball + from github.com/astral-sh/uv/releases/, SHA256-verifies against the + official `.sha256` sibling, installs to `/usr/local/bin/{uv,uvx}`. + Idempotent via `unless: '/usr/local/bin/uv --version 2>/dev/null + | grep -qx "uv 0.11.8"'`. `needs: ['pkg_apt:curl']`, + `triggers: ['action:left4me_uv_sync']`, `triggered: False`, + `cascade_skip: False`. + - Add `left4me_uv_sync` action: `sudo -u left4me env + UV_PROJECT_ENVIRONMENT=/var/lib/left4me/.venv /usr/local/bin/uv + sync --frozen --project /opt/left4me/src`. `triggered: True`, + `cascade_skip: False`. `needs:` includes + `'git_deploy:/opt/left4me/src'`, `'action:left4me_install_uv'`, + `'directory:/var/lib/left4me'`, `'user:left4me'`. `triggers: + ['action:left4me_alembic_upgrade']`. + - Update `git_deploy:/opt/left4me/src` triggers (lines 285–305): + replace `'action:left4me_pip_install'` with + `'action:left4me_uv_sync'`. Keep `left4me_alembic_upgrade` and + `left4me_daemon_reload` triggers. + - Update `left4me_alembic_upgrade` (lines 384–407): its dependency + on `left4me_pip_install` must now point at `left4me_uv_sync`. +3. Rewrite `bundles/left4me/README.md:84–90` to describe the new + `install_uv → uv_sync → alembic_upgrade → seed_overlays + restart` + chain (drop the pip + tempdir-dance prose). +4. `(cd ~/Projekte/ckn-bw && .venv/bin/bw test)` → must pass clean. + +### Step 4 — ckn-bw commit (DO NOT PUSH) +Single commit using the suggested message from the handoff +(§"Commit messages — ckn-bw side"). Do **not** `git push`. Per +verified state today, ckn-bw is currently EVEN with `origin/master` +(not 7 ahead as the original prompt claimed — the operator pushed +since the prompt was written). After this commit lands locally, the +repo will be 1 commit ahead of origin. + +### Step 5 — Report to operator (handoff to user for deploy) +Agent's work ends here. Brief summary to the user including: +- Spike outcome (full uv-workspace path confirmed, or Medium-scope + fallback taken — including any handoff doc updates if the latter). +- What's committed and where it sits: left4me pushed to `origin/master`; + ckn-bw committed locally, now 1 commit ahead of origin (unpushed). +- The `bw apply ovh.left4me` invocation for the user to run, with the + expected output (left4me_install_uv runs the download+verify, three + old actions removed from the graph, two new actions present + (install_uv + uv_sync), alembic+seed+restart cascade fires). +- The 6-check verification matrix from handoff §"Verification + (end-to-end)" for the user to walk through after apply — with + check #1 amended: use + `sudo find /opt/left4me/src \( -name '*.egg-info' -o -name build + -o -name dist \) -newer ` instead of `git status`, + because git isn't installed on prod. +- Recovery path if uv refuses to adopt the existing venv: one-shot + `ssh ckn@left4.me 'sudo -u left4me rm -rf /var/lib/left4me/.venv'`, + then re-apply. +- Open follow-ups (uv version pinning policy — bump cadence, signing, + etc; direnv `use uv` fallback applied or not; whether to add a + separate `pkg_apt: curl` if it wasn't already declared). + +**Do NOT run `bw apply`, the verification matrix, or the gameserver +round-trip — those are explicitly user-side per session memory.** + +## Plan storage after approval + +Per the user's global AGENTS.md (`~/.claude/agents/AGENTS.md`): specs +and plans live in the repo they describe, typically under `docs/`. After +ExitPlanMode and approval, this plan should be copied to +`/Users/mwiegand/Projekte/left4me/docs/superpowers/plans/2026-05-15-uv-workspace-execution.md` +as a peer to the design handoff, then committed alongside the left4me +changes in Step 2. + +## What does NOT change (out of scope) + +- Source ownership: `/opt/left4me/src` stays root-owned. +- Venv location: `/var/lib/left4me/.venv` stays where it is, owned by + the `left4me` user, accessed via `UV_PROJECT_ENVIRONMENT`. +- Hardening drop-ins, sudoers, sysctl, helpers — all stable from the + deployment-responsibility migration. +- systemd unit shapes — reactor-emitted, unchanged. +- `alembic_upgrade` and `seed_overlays` shell bodies — same commands, + just triggered from `uv_sync` instead of `pip_install`. +- `pkg_apt: python3` and `python3-dev` — kept (uv shells out to system + Python). +- Other ckn-bw bundles — this is left4me-specific. +- The build-overlay-unit refactor — separate queued thread. +- CI — none currently exists. + +## Risks (carried from handoff, sized empirically) + +1. **Spike test failure** → fall back to Medium scope. Graceful. +2. ~~Lockfile format skew between dev and prod~~ → **MITIGATED** by + pinning prod uv to 0.11.8 (same as local brew). Lockfile generated + by dev's uv 0.11.8 will be consumed by prod's uv 0.11.8 byte-for-byte + compatible. Risk effectively eliminated unless dev's brew bumps uv + independently — track this in the pinning-policy follow-up. +3. **direnv `use uv` availability** → local direnv is 2.37.1 (`use uv` + added in 2.34+, so we're fine). Fallback snippet documented in case + another dev has an older direnv. +4. **`alembic`/`flask` binary paths** → uv installs the same + `console_scripts` entrypoints as pip, so paths under + `/var/lib/left4me/.venv/bin/` are identical. Verify in verification + matrix. +5. **`--force-reinstall` semantics** → no longer needed; `uv sync` is + lockfile-aware, not package-version-aware. +6. **uv release artifact availability** → if github.com/astral-sh/uv + takes down release 0.11.8 (extremely unlikely but theoretically + possible), the install action would fail. Mitigation: pin a recent + stable release, monitor astral's deprecation cadence; if needed, + mirror the artifact to an internal location for future-proofing + (out of scope for this migration). +7. **SHA256 of the tarball** → we trust the `.sha256` sibling fetched + from the same github release. A future hardening pass could embed + the checksum in the bundle source for offline verification, but the + current trust model matches steamcmd's (also github-sourced). diff --git a/docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md b/docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md index a4a4579..e7dc82b 100644 --- a/docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md +++ b/docs/superpowers/specs/2026-05-15-handoff-uv-workspace.md @@ -2,10 +2,13 @@ ## Status -Queued. Independent of the deployment-responsibility reshape -(`2026-05-15-deployment-responsibility-design.md`) which just shipped; -that work left the venv-chain shape intact. This handoff replaces the -chain end-to-end. +**Executed (left4me side) — see +`docs/superpowers/plans/2026-05-15-uv-workspace-execution.md` for what +actually shipped and what diverged from the assumptions below.** Three +load-bearing assumptions in this doc turned out to be wrong (no +`pkg_apt: uv` on Trixie; existing layout incompatible with read-only +source builds via setuptools; no `git` on prod). The executed plan +records the corrections. ## Goal diff --git a/l4d2host/__init__.py b/l4d2host/l4d2host/__init__.py similarity index 100% rename from l4d2host/__init__.py rename to l4d2host/l4d2host/__init__.py diff --git a/l4d2host/cli.py b/l4d2host/l4d2host/cli.py similarity index 100% rename from l4d2host/cli.py rename to l4d2host/l4d2host/cli.py diff --git a/l4d2host/instances.py b/l4d2host/l4d2host/instances.py similarity index 100% rename from l4d2host/instances.py rename to l4d2host/l4d2host/instances.py diff --git a/l4d2host/logging.py b/l4d2host/l4d2host/logging.py similarity index 100% rename from l4d2host/logging.py rename to l4d2host/l4d2host/logging.py diff --git a/l4d2host/logs.py b/l4d2host/l4d2host/logs.py similarity index 100% rename from l4d2host/logs.py rename to l4d2host/l4d2host/logs.py diff --git a/l4d2host/paths.py b/l4d2host/l4d2host/paths.py similarity index 100% rename from l4d2host/paths.py rename to l4d2host/l4d2host/paths.py diff --git a/l4d2host/process.py b/l4d2host/l4d2host/process.py similarity index 100% rename from l4d2host/process.py rename to l4d2host/l4d2host/process.py diff --git a/l4d2host/service_control.py b/l4d2host/l4d2host/service_control.py similarity index 100% rename from l4d2host/service_control.py rename to l4d2host/l4d2host/service_control.py diff --git a/l4d2host/spec.py b/l4d2host/l4d2host/spec.py similarity index 100% rename from l4d2host/spec.py rename to l4d2host/l4d2host/spec.py diff --git a/l4d2host/status.py b/l4d2host/l4d2host/status.py similarity index 100% rename from l4d2host/status.py rename to l4d2host/l4d2host/status.py diff --git a/l4d2host/steam_install.py b/l4d2host/l4d2host/steam_install.py similarity index 100% rename from l4d2host/steam_install.py rename to l4d2host/l4d2host/steam_install.py diff --git a/l4d2host/templates/__init__.py b/l4d2host/l4d2host/templates/__init__.py similarity index 100% rename from l4d2host/templates/__init__.py rename to l4d2host/l4d2host/templates/__init__.py diff --git a/l4d2host/pyproject.toml b/l4d2host/pyproject.toml index 574cbea..b338365 100644 --- a/l4d2host/pyproject.toml +++ b/l4d2host/pyproject.toml @@ -1,13 +1,13 @@ [build-system] -requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "l4d2host" version = "0.1.0" description = "L4D2 host library and CLI" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.13" dependencies = [ "typer>=0.12", "PyYAML>=6.0", @@ -15,9 +15,3 @@ dependencies = [ [project.scripts] l4d2ctl = "l4d2host.cli:app" - -[tool.setuptools] -packages = ["l4d2host"] - -[tool.setuptools.package-dir] -l4d2host = "." diff --git a/l4d2web/routes/__init__.py b/l4d2host/tests/__init__.py similarity index 100% rename from l4d2web/routes/__init__.py rename to l4d2host/tests/__init__.py diff --git a/l4d2host/tests/test_install.py b/l4d2host/tests/test_install.py index d444459..d14acb5 100644 --- a/l4d2host/tests/test_install.py +++ b/l4d2host/tests/test_install.py @@ -5,7 +5,9 @@ import pytest from l4d2host.steam_install import SteamInstaller -def test_windows_then_linux(monkeypatch: pytest.MonkeyPatch) -> None: +def test_windows_then_linux(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("HOME", str(tmp_path / "home")) + monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path)) calls: list[list[str]] = [] def fake_run_command(cmd, **kwargs): @@ -36,6 +38,7 @@ def test_fail_fast_on_first_failure(monkeypatch: pytest.MonkeyPatch) -> None: def test_default_install_dir_uses_left4me_root(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path / "home")) monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path)) calls = [] monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: calls.append(cmd)) diff --git a/l4d2web/README.md b/l4d2web/README.md index 5414360..edc7814 100644 --- a/l4d2web/README.md +++ b/l4d2web/README.md @@ -22,10 +22,11 @@ Flask web app for managing L4D2 servers through user-private blueprints. ## Development +From the workspace root (`../`): + ```bash -python3 -m venv .venv -.venv/bin/pip install -e . -.venv/bin/pytest tests -q +uv sync # creates .venv, installs l4d2host + l4d2web editable, plus dev deps +uv run pytest l4d2web/tests -q ``` ## Configuration diff --git a/l4d2web/__init__.py b/l4d2web/l4d2web/__init__.py similarity index 100% rename from l4d2web/__init__.py rename to l4d2web/l4d2web/__init__.py diff --git a/l4d2web/app.py b/l4d2web/l4d2web/app.py similarity index 100% rename from l4d2web/app.py rename to l4d2web/l4d2web/app.py diff --git a/l4d2web/auth.py b/l4d2web/l4d2web/auth.py similarity index 100% rename from l4d2web/auth.py rename to l4d2web/l4d2web/auth.py diff --git a/l4d2web/cli.py b/l4d2web/l4d2web/cli.py similarity index 100% rename from l4d2web/cli.py rename to l4d2web/l4d2web/cli.py diff --git a/l4d2web/config.py b/l4d2web/l4d2web/config.py similarity index 100% rename from l4d2web/config.py rename to l4d2web/l4d2web/config.py diff --git a/l4d2web/db.py b/l4d2web/l4d2web/db.py similarity index 100% rename from l4d2web/db.py rename to l4d2web/l4d2web/db.py diff --git a/l4d2web/models.py b/l4d2web/l4d2web/models.py similarity index 100% rename from l4d2web/models.py rename to l4d2web/l4d2web/models.py diff --git a/l4d2web/services/__init__.py b/l4d2web/l4d2web/routes/__init__.py similarity index 100% rename from l4d2web/services/__init__.py rename to l4d2web/l4d2web/routes/__init__.py diff --git a/l4d2web/routes/auth_routes.py b/l4d2web/l4d2web/routes/auth_routes.py similarity index 100% rename from l4d2web/routes/auth_routes.py rename to l4d2web/l4d2web/routes/auth_routes.py diff --git a/l4d2web/routes/blueprint_routes.py b/l4d2web/l4d2web/routes/blueprint_routes.py similarity index 100% rename from l4d2web/routes/blueprint_routes.py rename to l4d2web/l4d2web/routes/blueprint_routes.py diff --git a/l4d2web/routes/console_routes.py b/l4d2web/l4d2web/routes/console_routes.py similarity index 100% rename from l4d2web/routes/console_routes.py rename to l4d2web/l4d2web/routes/console_routes.py diff --git a/l4d2web/routes/files_routes.py b/l4d2web/l4d2web/routes/files_routes.py similarity index 100% rename from l4d2web/routes/files_routes.py rename to l4d2web/l4d2web/routes/files_routes.py diff --git a/l4d2web/routes/job_routes.py b/l4d2web/l4d2web/routes/job_routes.py similarity index 100% rename from l4d2web/routes/job_routes.py rename to l4d2web/l4d2web/routes/job_routes.py diff --git a/l4d2web/routes/log_routes.py b/l4d2web/l4d2web/routes/log_routes.py similarity index 100% rename from l4d2web/routes/log_routes.py rename to l4d2web/l4d2web/routes/log_routes.py diff --git a/l4d2web/routes/overlay_routes.py b/l4d2web/l4d2web/routes/overlay_routes.py similarity index 100% rename from l4d2web/routes/overlay_routes.py rename to l4d2web/l4d2web/routes/overlay_routes.py diff --git a/l4d2web/routes/page_routes.py b/l4d2web/l4d2web/routes/page_routes.py similarity index 100% rename from l4d2web/routes/page_routes.py rename to l4d2web/l4d2web/routes/page_routes.py diff --git a/l4d2web/routes/profile_routes.py b/l4d2web/l4d2web/routes/profile_routes.py similarity index 100% rename from l4d2web/routes/profile_routes.py rename to l4d2web/l4d2web/routes/profile_routes.py diff --git a/l4d2web/routes/server_routes.py b/l4d2web/l4d2web/routes/server_routes.py similarity index 100% rename from l4d2web/routes/server_routes.py rename to l4d2web/l4d2web/routes/server_routes.py diff --git a/l4d2web/routes/workshop_routes.py b/l4d2web/l4d2web/routes/workshop_routes.py similarity index 100% rename from l4d2web/routes/workshop_routes.py rename to l4d2web/l4d2web/routes/workshop_routes.py diff --git a/l4d2web/l4d2web/services/__init__.py b/l4d2web/l4d2web/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/l4d2web/services/host_commands.py b/l4d2web/l4d2web/services/host_commands.py similarity index 100% rename from l4d2web/services/host_commands.py rename to l4d2web/l4d2web/services/host_commands.py diff --git a/l4d2web/services/job_worker.py b/l4d2web/l4d2web/services/job_worker.py similarity index 100% rename from l4d2web/services/job_worker.py rename to l4d2web/l4d2web/services/job_worker.py diff --git a/l4d2web/services/l4d2_facade.py b/l4d2web/l4d2web/services/l4d2_facade.py similarity index 100% rename from l4d2web/services/l4d2_facade.py rename to l4d2web/l4d2web/services/l4d2_facade.py diff --git a/l4d2web/services/live_state_poller.py b/l4d2web/l4d2web/services/live_state_poller.py similarity index 100% rename from l4d2web/services/live_state_poller.py rename to l4d2web/l4d2web/services/live_state_poller.py diff --git a/l4d2web/services/overlay_builders.py b/l4d2web/l4d2web/services/overlay_builders.py similarity index 100% rename from l4d2web/services/overlay_builders.py rename to l4d2web/l4d2web/services/overlay_builders.py diff --git a/l4d2web/services/overlay_creation.py b/l4d2web/l4d2web/services/overlay_creation.py similarity index 100% rename from l4d2web/services/overlay_creation.py rename to l4d2web/l4d2web/services/overlay_creation.py diff --git a/l4d2web/services/overlay_files.py b/l4d2web/l4d2web/services/overlay_files.py similarity index 100% rename from l4d2web/services/overlay_files.py rename to l4d2web/l4d2web/services/overlay_files.py diff --git a/l4d2web/services/rate_limit.py b/l4d2web/l4d2web/services/rate_limit.py similarity index 100% rename from l4d2web/services/rate_limit.py rename to l4d2web/l4d2web/services/rate_limit.py diff --git a/l4d2web/services/rcon.py b/l4d2web/l4d2web/services/rcon.py similarity index 100% rename from l4d2web/services/rcon.py rename to l4d2web/l4d2web/services/rcon.py diff --git a/l4d2web/services/security.py b/l4d2web/l4d2web/services/security.py similarity index 100% rename from l4d2web/services/security.py rename to l4d2web/l4d2web/services/security.py diff --git a/l4d2web/services/server_identity.py b/l4d2web/l4d2web/services/server_identity.py similarity index 100% rename from l4d2web/services/server_identity.py rename to l4d2web/l4d2web/services/server_identity.py diff --git a/l4d2web/services/spec_yaml.py b/l4d2web/l4d2web/services/spec_yaml.py similarity index 100% rename from l4d2web/services/spec_yaml.py rename to l4d2web/l4d2web/services/spec_yaml.py diff --git a/l4d2web/services/status.py b/l4d2web/l4d2web/services/status.py similarity index 100% rename from l4d2web/services/status.py rename to l4d2web/l4d2web/services/status.py diff --git a/l4d2web/services/steam_users.py b/l4d2web/l4d2web/services/steam_users.py similarity index 100% rename from l4d2web/services/steam_users.py rename to l4d2web/l4d2web/services/steam_users.py diff --git a/l4d2web/services/steam_workshop.py b/l4d2web/l4d2web/services/steam_workshop.py similarity index 100% rename from l4d2web/services/steam_workshop.py rename to l4d2web/l4d2web/services/steam_workshop.py diff --git a/l4d2web/services/timeago.py b/l4d2web/l4d2web/services/timeago.py similarity index 100% rename from l4d2web/services/timeago.py rename to l4d2web/l4d2web/services/timeago.py diff --git a/l4d2web/services/workshop_paths.py b/l4d2web/l4d2web/services/workshop_paths.py similarity index 100% rename from l4d2web/services/workshop_paths.py rename to l4d2web/l4d2web/services/workshop_paths.py diff --git a/l4d2web/static/css/components.css b/l4d2web/l4d2web/static/css/components.css similarity index 100% rename from l4d2web/static/css/components.css rename to l4d2web/l4d2web/static/css/components.css diff --git a/l4d2web/static/css/layout.css b/l4d2web/l4d2web/static/css/layout.css similarity index 100% rename from l4d2web/static/css/layout.css rename to l4d2web/l4d2web/static/css/layout.css diff --git a/l4d2web/static/css/logs.css b/l4d2web/l4d2web/static/css/logs.css similarity index 100% rename from l4d2web/static/css/logs.css rename to l4d2web/l4d2web/static/css/logs.css diff --git a/l4d2web/static/css/tokens.css b/l4d2web/l4d2web/static/css/tokens.css similarity index 100% rename from l4d2web/static/css/tokens.css rename to l4d2web/l4d2web/static/css/tokens.css diff --git a/l4d2web/static/js/blueprint-overlay-picker.js b/l4d2web/l4d2web/static/js/blueprint-overlay-picker.js similarity index 100% rename from l4d2web/static/js/blueprint-overlay-picker.js rename to l4d2web/l4d2web/static/js/blueprint-overlay-picker.js diff --git a/l4d2web/static/js/console-history.js b/l4d2web/l4d2web/static/js/console-history.js similarity index 100% rename from l4d2web/static/js/console-history.js rename to l4d2web/l4d2web/static/js/console-history.js diff --git a/l4d2web/static/js/csrf.js b/l4d2web/l4d2web/static/js/csrf.js similarity index 100% rename from l4d2web/static/js/csrf.js rename to l4d2web/l4d2web/static/js/csrf.js diff --git a/l4d2web/static/js/file-tree.js b/l4d2web/l4d2web/static/js/file-tree.js similarity index 100% rename from l4d2web/static/js/file-tree.js rename to l4d2web/l4d2web/static/js/file-tree.js diff --git a/l4d2web/static/js/files-overlay.js b/l4d2web/l4d2web/static/js/files-overlay.js similarity index 100% rename from l4d2web/static/js/files-overlay.js rename to l4d2web/l4d2web/static/js/files-overlay.js diff --git a/l4d2web/static/js/modal.js b/l4d2web/l4d2web/static/js/modal.js similarity index 100% rename from l4d2web/static/js/modal.js rename to l4d2web/l4d2web/static/js/modal.js diff --git a/l4d2web/static/js/password-reveal.js b/l4d2web/l4d2web/static/js/password-reveal.js similarity index 100% rename from l4d2web/static/js/password-reveal.js rename to l4d2web/l4d2web/static/js/password-reveal.js diff --git a/l4d2web/static/js/sse.js b/l4d2web/l4d2web/static/js/sse.js similarity index 100% rename from l4d2web/static/js/sse.js rename to l4d2web/l4d2web/static/js/sse.js diff --git a/l4d2web/static/vendor/htmx.min.js b/l4d2web/l4d2web/static/vendor/htmx.min.js similarity index 100% rename from l4d2web/static/vendor/htmx.min.js rename to l4d2web/l4d2web/static/vendor/htmx.min.js diff --git a/l4d2web/templates/_console_line.html b/l4d2web/l4d2web/templates/_console_line.html similarity index 100% rename from l4d2web/templates/_console_line.html rename to l4d2web/l4d2web/templates/_console_line.html diff --git a/l4d2web/templates/_job_table.html b/l4d2web/l4d2web/templates/_job_table.html similarity index 100% rename from l4d2web/templates/_job_table.html rename to l4d2web/l4d2web/templates/_job_table.html diff --git a/l4d2web/templates/_live_state.html b/l4d2web/l4d2web/templates/_live_state.html similarity index 100% rename from l4d2web/templates/_live_state.html rename to l4d2web/l4d2web/templates/_live_state.html diff --git a/l4d2web/templates/_overlay_build_status.html b/l4d2web/l4d2web/templates/_overlay_build_status.html similarity index 100% rename from l4d2web/templates/_overlay_build_status.html rename to l4d2web/l4d2web/templates/_overlay_build_status.html diff --git a/l4d2web/templates/_overlay_file_node.html b/l4d2web/l4d2web/templates/_overlay_file_node.html similarity index 100% rename from l4d2web/templates/_overlay_file_node.html rename to l4d2web/l4d2web/templates/_overlay_file_node.html diff --git a/l4d2web/templates/_overlay_file_tree.html b/l4d2web/l4d2web/templates/_overlay_file_tree.html similarity index 100% rename from l4d2web/templates/_overlay_file_tree.html rename to l4d2web/l4d2web/templates/_overlay_file_tree.html diff --git a/l4d2web/templates/_overlay_item_table.html b/l4d2web/l4d2web/templates/_overlay_item_table.html similarity index 100% rename from l4d2web/templates/_overlay_item_table.html rename to l4d2web/l4d2web/templates/_overlay_item_table.html diff --git a/l4d2web/templates/_server_actions.html b/l4d2web/l4d2web/templates/_server_actions.html similarity index 100% rename from l4d2web/templates/_server_actions.html rename to l4d2web/l4d2web/templates/_server_actions.html diff --git a/l4d2web/templates/admin.html b/l4d2web/l4d2web/templates/admin.html similarity index 100% rename from l4d2web/templates/admin.html rename to l4d2web/l4d2web/templates/admin.html diff --git a/l4d2web/templates/admin_jobs.html b/l4d2web/l4d2web/templates/admin_jobs.html similarity index 100% rename from l4d2web/templates/admin_jobs.html rename to l4d2web/l4d2web/templates/admin_jobs.html diff --git a/l4d2web/templates/admin_users.html b/l4d2web/l4d2web/templates/admin_users.html similarity index 100% rename from l4d2web/templates/admin_users.html rename to l4d2web/l4d2web/templates/admin_users.html diff --git a/l4d2web/templates/base.html b/l4d2web/l4d2web/templates/base.html similarity index 100% rename from l4d2web/templates/base.html rename to l4d2web/l4d2web/templates/base.html diff --git a/l4d2web/templates/blueprint_detail.html b/l4d2web/l4d2web/templates/blueprint_detail.html similarity index 100% rename from l4d2web/templates/blueprint_detail.html rename to l4d2web/l4d2web/templates/blueprint_detail.html diff --git a/l4d2web/templates/blueprints.html b/l4d2web/l4d2web/templates/blueprints.html similarity index 100% rename from l4d2web/templates/blueprints.html rename to l4d2web/l4d2web/templates/blueprints.html diff --git a/l4d2web/templates/dashboard.html b/l4d2web/l4d2web/templates/dashboard.html similarity index 100% rename from l4d2web/templates/dashboard.html rename to l4d2web/l4d2web/templates/dashboard.html diff --git a/l4d2web/templates/job_detail.html b/l4d2web/l4d2web/templates/job_detail.html similarity index 100% rename from l4d2web/templates/job_detail.html rename to l4d2web/l4d2web/templates/job_detail.html diff --git a/l4d2web/templates/login.html b/l4d2web/l4d2web/templates/login.html similarity index 100% rename from l4d2web/templates/login.html rename to l4d2web/l4d2web/templates/login.html diff --git a/l4d2web/templates/overlay_detail.html b/l4d2web/l4d2web/templates/overlay_detail.html similarity index 100% rename from l4d2web/templates/overlay_detail.html rename to l4d2web/l4d2web/templates/overlay_detail.html diff --git a/l4d2web/templates/overlay_jobs.html b/l4d2web/l4d2web/templates/overlay_jobs.html similarity index 100% rename from l4d2web/templates/overlay_jobs.html rename to l4d2web/l4d2web/templates/overlay_jobs.html diff --git a/l4d2web/templates/overlays.html b/l4d2web/l4d2web/templates/overlays.html similarity index 100% rename from l4d2web/templates/overlays.html rename to l4d2web/l4d2web/templates/overlays.html diff --git a/l4d2web/templates/profile.html b/l4d2web/l4d2web/templates/profile.html similarity index 100% rename from l4d2web/templates/profile.html rename to l4d2web/l4d2web/templates/profile.html diff --git a/l4d2web/templates/server_detail.html b/l4d2web/l4d2web/templates/server_detail.html similarity index 100% rename from l4d2web/templates/server_detail.html rename to l4d2web/l4d2web/templates/server_detail.html diff --git a/l4d2web/templates/server_jobs.html b/l4d2web/l4d2web/templates/server_jobs.html similarity index 100% rename from l4d2web/templates/server_jobs.html rename to l4d2web/l4d2web/templates/server_jobs.html diff --git a/l4d2web/templates/servers.html b/l4d2web/l4d2web/templates/servers.html similarity index 100% rename from l4d2web/templates/servers.html rename to l4d2web/l4d2web/templates/servers.html diff --git a/l4d2web/pyproject.toml b/l4d2web/pyproject.toml index 9660f7e..7805024 100644 --- a/l4d2web/pyproject.toml +++ b/l4d2web/pyproject.toml @@ -1,13 +1,13 @@ [build-system] -requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "l4d2web" version = "0.1.0" description = "L4D2 web app" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.13" dependencies = [ "Flask>=3.0", "SQLAlchemy>=2.0", @@ -15,18 +15,8 @@ dependencies = [ "PyYAML>=6.0", "gunicorn>=22.0", "requests>=2.31", + "l4d2host", ] -[tool.setuptools] -packages = ["l4d2web", "l4d2web.routes", "l4d2web.services"] - -[tool.setuptools.package-dir] -l4d2web = "." - -[tool.setuptools.package-data] -l4d2web = [ - "templates/*.html", - "static/css/*.css", - "static/js/*.js", - "static/vendor/*.js", -] +[tool.uv.sources] +l4d2host = { workspace = true } diff --git a/l4d2web/tests/__init__.py b/l4d2web/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/l4d2web/tests/test_job_logs.py b/l4d2web/tests/test_job_logs.py index a28fb2c..5bbd26d 100644 --- a/l4d2web/tests/test_job_logs.py +++ b/l4d2web/tests/test_job_logs.py @@ -104,7 +104,7 @@ def test_sse_resumes_from_last_event_id_header(seeded_job_logs) -> None: def test_sse_js_handles_job_log_custom_events() -> None: - js = Path("l4d2web/static/js/sse.js").read_text() + js = (Path(__file__).resolve().parents[1] / "l4d2web" / "static" / "js" / "sse.js").read_text() assert 'addEventListener("stdout"' in js assert 'addEventListener("stderr"' in js diff --git a/l4d2web/tests/test_pages.py b/l4d2web/tests/test_pages.py index 1ddb2e0..612445b 100644 --- a/l4d2web/tests/test_pages.py +++ b/l4d2web/tests/test_pages.py @@ -91,7 +91,7 @@ def test_shell_nav_uses_main_sections(auth_client_with_server) -> None: def test_css_tokens_define_neutral_light_and_dark_theme() -> None: - css = Path("l4d2web/static/css/tokens.css").read_text() + css = (Path(__file__).resolve().parents[1] / "l4d2web" / "static" / "css" / "tokens.css").read_text() for token in [ "--color-bg", @@ -113,11 +113,11 @@ def test_css_tokens_define_neutral_light_and_dark_theme() -> None: ]: assert token in css assert "prefers-color-scheme: dark" in css - assert "radial-gradient" not in Path("l4d2web/static/css/layout.css").read_text() + assert "radial-gradient" not in (Path(__file__).resolve().parents[1] / "l4d2web" / "static" / "css" / "layout.css").read_text() def test_log_tokens_follow_light_and_dark_theme() -> None: - css = Path("l4d2web/static/css/tokens.css").read_text() + css = (Path(__file__).resolve().parents[1] / "l4d2web" / "static" / "css" / "tokens.css").read_text() assert "--color-log-bg: #f8fafc;" in css assert "--color-log-text: #18181b;" in css diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c3e4e6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "left4me" +version = "0.0.0" +description = "Workspace root for l4d2host and l4d2web; packaging lives in the members." +requires-python = ">=3.13" +dependencies = ["l4d2host", "l4d2web"] + +[tool.uv] +package = false + +[tool.uv.workspace] +members = ["l4d2host", "l4d2web"] + +[tool.uv.sources] +l4d2host = { workspace = true } +l4d2web = { workspace = true } + +[dependency-groups] +dev = ["pytest"] + +[tool.pytest.ini_options] +testpaths = ["l4d2host/tests", "l4d2web/tests"] +addopts = ["--import-mode=importlib"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..088f1cd --- /dev/null +++ b/uv.lock @@ -0,0 +1,576 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[manifest] +members = [ + "l4d2host", + "l4d2web", + "left4me", +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[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 = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "l4d2host" +version = "0.1.0" +source = { editable = "l4d2host" } +dependencies = [ + { name = "pyyaml" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyyaml", specifier = ">=6.0" }, + { name = "typer", specifier = ">=0.12" }, +] + +[[package]] +name = "l4d2web" +version = "0.1.0" +source = { editable = "l4d2web" } +dependencies = [ + { name = "alembic" }, + { name = "flask" }, + { name = "gunicorn" }, + { name = "l4d2host" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.13" }, + { name = "flask", specifier = ">=3.0" }, + { name = "gunicorn", specifier = ">=22.0" }, + { name = "l4d2host", editable = "l4d2host" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.31" }, + { name = "sqlalchemy", specifier = ">=2.0" }, +] + +[[package]] +name = "left4me" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "l4d2host" }, + { name = "l4d2web" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "l4d2host", editable = "l4d2host" }, + { name = "l4d2web", editable = "l4d2web" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest" }] + +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +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 = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +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 = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[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" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +]