From d520d1e23703b8a5bfa5e26118df0a4a044ec6e3 Mon Sep 17 00:00:00 2001 From: Shroominic <34897716+shroominic@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:10:40 +0800 Subject: [PATCH] ci: add no-docker mock money-path lane + fix silent-pass orchestrator exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a two-lane GitHub Actions workflow (the repo had no CI at all) and fixes the bug that would have made the money-path lane a false-safety net. Root cause: runner.orchestrate.main() returned 0 regardless of the scenario outcome — a scenario whose tests FAILED (status=failed) or ERRORED (status=error) still exited 0. A CI guard that drives the orchestrator (the intended money-path gate) would stay green while the swap/refund logic under test was red — the silent-pass failure mode. Fix (surgical): - main() now reads the finalized run status from runs.db and exits 1 on failed/error; the JSON summary gains a "status" field (additive). - server.runs.spawn_orchestrator tolerates the new non-zero exit: a run that was recorded (run_id summary present) still returns its id so the UI shows status=failed; a non-zero exit with no run_id still raises. CI lanes (.github/workflows/ci.yml, docs/ci.md): - pr: no Docker, no funds. ruff + mypy(runner/) + unit/mock pytest + orchestrator pipeline guards (smoke MUST exit 0; smoke_fail MUST exit non-zero, inverted so a silent-pass fails the build). The mock money guard tests/integration/test_spend_unit.py runs here. - nightly: docker lane stub (make up/down) with the foreign-mint swap + retry scenario steps commented out until Routstr/routstr-testing#1 merges (it adds those services_required scenarios; not on main yet). Tooling made hermetic: pinned ruff/mypy/types-PyYAML in a [dependency- groups] dev group; [tool.ruff]/[tool.mypy] config added (mypy scoped to the money-path runner/ package — server/ SQLModel typing is out of scope). Cleared 10 pre-existing ruff lint errors so the lint gate is meaningful. Regression tests: - tests/test_orchestrate_exit_code.py — passed→0, failed→non-zero, error→non-zero (failed/error cases fail without the fix). - tests/test_server_orchestrator_exit.py — server returns run_id on non-zero-with-run_id, raises on non-zero-without-run_id. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 142 +++++++++++++++++++++++++ docs/ci.md | 46 ++++++++ pyproject.toml | 22 ++++ runner/orchestrate.py | 26 ++++- server/runs.py | 22 ++-- tests/cli/test_handwritten.py | 2 +- tests/test_orchestrate_balance.py | 1 - tests/test_orchestrate_exit_code.py | 134 +++++++++++++++++++++++ tests/test_orchestrate_upstream.py | 1 - tests/test_server_orchestrator_exit.py | 84 +++++++++++++++ tests/test_server_runs.py | 1 - tests/test_server_token_hygiene.py | 6 +- uv.lock | 93 +++++++++++++--- 13 files changed, 550 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/ci.md create mode 100644 tests/test_orchestrate_exit_code.py create mode 100644 tests/test_server_orchestrator_exit.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8361d40 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,142 @@ +name: CI + +# Two-lane CI for the Routstr e2e harness (see docs/ci.md): +# +# pr — fast, no-Docker, no-funds. Runs on every PR + push to main. +# Gates on: ruff + mypy(runner) + the unit/mock pytest suite +# + the orchestrator money-path *pipeline* guard (smoke passes / +# smoke_fail fails -> non-zero exit). Uses only local nutshell +# FakeWallet-free mock primitives; never touches real sats or a +# real mint. +# +# nightly — Docker lane (scheduled + manual). Brings up the compose +# topology and runs the services_required money-path scenarios +# (foreign-mint swap + retry once Routstr/routstr-testing#1 merges) +# against local nutshell FakeWallet mints — real Cashu crypto, +# zero real sats. Stubbed here; enable once PR #1 lands. + +on: + pull_request: + push: + branches: [main] + schedule: + # 03:17 UTC nightly — odd minute to dodge the top-of-hour scheduler rush. + - cron: "17 3 * * *" + workflow_dispatch: + inputs: + run_nightly: + description: "Force the Docker nightly lane on a manual run" + type: boolean + default: false + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ────────────────────────────────────────────────────────────────────── + # Fast PR lane: no Docker, no funds. This is the gate on every PR. + # ────────────────────────────────────────────────────────────────────── + pr: + name: pr (no-docker / mock money-path) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Sync env (incl. dev tools) + run: uv sync --group dev --frozen + + - name: Lint (ruff) + run: uv run ruff check . + + - name: Type-check money-path package (mypy runner/) + run: uv run mypy + + - name: Unit + mock suite (no Docker, no funds) + # The full pytest suite. Service-backed integration tests guard + # themselves and SKIP without their env (no REMOTE_NODE_URLS / + # X_CASHU_TOKENS / funded daemon), so nothing here needs Docker or + # real sats. SCENARIO_ID mirrors what the orchestrator exports so the + # smoke env-propagation assertion holds; test_smoke_fail.py is the + # deliberately-red negative control and is exercised via the + # orchestrator guard below, not collected directly. + env: + SCENARIO_ID: smoke + run: uv run pytest tests --deselect tests/test_smoke_fail.py -q + + - name: Money-path pipeline guard — smoke MUST pass (exit 0) + # Drives the no-services smoke scenario through the orchestrator and + # asserts the *process exit code* is 0. Proves the runner -> pytest -> + # junit -> sqlite pipeline is intact. + env: + SKIP_SYNC: "1" + run: uv run python -m runner.orchestrate --scenario smoke --token placeholder + + - name: Money-path pipeline guard — smoke_fail MUST fail (non-zero) + # The anti-silent-pass check: a scenario whose tests fail has to make + # the orchestrator exit non-zero, or the nightly money-path lane would + # be green while swap/refund logic is red. We invert the exit code: + # this step fails the build if the orchestrator wrongly returns 0. + env: + SKIP_SYNC: "1" + run: | + if uv run python -m runner.orchestrate --scenario smoke_fail --token placeholder; then + echo "::error::orchestrator exited 0 for a FAILED scenario — the money-path CI guard is a silent-pass" + exit 1 + else + echo "orchestrator correctly returned non-zero for smoke_fail" + fi + + # ────────────────────────────────────────────────────────────────────── + # Nightly Docker lane (STUB). Real Cashu crypto via local nutshell mints, + # zero real sats. Enable the scenario steps once Routstr/routstr-testing#1 + # (foreign-mint swap + retry) merges to main. + # ────────────────────────────────────────────────────────────────────── + nightly: + name: nightly (docker money-path) + runs-on: ubuntu-latest + timeout-minutes: 45 + # Only on the schedule or an explicit manual opt-in — never on PRs. + if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.run_nightly) + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Sync env + run: uv sync --frozen + + - name: Bring up compose topology (vendor refs default to main) + run: make up + + # --- Money-path scenarios (enable after Routstr/routstr-testing#1 merges) --- + # PR #1 adds two services_required scenarios that exercise routstr-core's + # swap_to_primary_mint fee handling against local nutshell FakeWallet + # mints + a fault-proxy. Until it lands, these steps are intentionally + # inert so the lane is wired but does not reference files not yet on main. + # + # - name: Foreign-mint swap (covers core#549/#468 fund-loss class) + # env: { SKIP_SYNC: "1", KEEP_UP: "1" } + # run: uv run python -m runner.orchestrate --scenario swap_foreign_mint --token placeholder + # + # - name: Foreign-mint swap retry + # # Pin ROUTSTR_CORE_REF=refs/pull/549/head until core#549 merges; + # # on core main this asserts the no-retry 400/abort path. + # env: { SKIP_SYNC: "1", KEEP_UP: "1", ROUTSTR_CORE_REF: "refs/pull/549/head" } + # run: uv run python -m runner.orchestrate --scenario swap_foreign_mint_retry --token placeholder + + - name: Nightly stub notice + run: echo "Docker topology is up. Money-path scenario steps are stubbed until Routstr/routstr-testing#1 merges." + + - name: Tear down + if: always() + run: make down diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000..48492c4 --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,46 @@ +# CI lanes + +This repo ships a two-lane GitHub Actions workflow (`.github/workflows/ci.yml`). + +## `pr` lane — fast, no Docker, no funds (gates every PR) + +Runs on every pull request and push to `main`. Steps: + +1. `uv sync --group dev --frozen` — hermetic env incl. pinned `ruff`/`mypy`. +2. `ruff check .` — lint gate (config in `pyproject.toml`). +3. `mypy` — type-checks the **money-path orchestrator package** `runner/` + only (`[tool.mypy] files = ["runner"]`). `server/` uses idiomatic SQLModel + query expressions that trip SQLAlchemy's typing overloads and is out of + scope for this lane. +4. `pytest tests --deselect tests/test_smoke_fail.py` with `SCENARIO_ID=smoke`. + The full unit/mock suite. Service-backed integration tests guard themselves + and **skip** without their env (no `REMOTE_NODE_URLS`, `X_CASHU_TOKENS`, or a + funded daemon), so nothing here needs Docker or real sats. The mock + money-path guard `tests/integration/test_spend_unit.py` (Cashu TokenV4 + amount decode + spend accounting) runs here. +5. **Money-path pipeline guards** — drive the orchestrator and assert its + *process exit code*: + - `smoke` MUST exit 0 (pipeline intact). + - `smoke_fail` MUST exit non-zero (anti-silent-pass: a scenario whose tests + fail has to fail the build). The step inverts the exit code so a wrong + `0` fails CI. + +No real sats, no mint, no Docker. Safe to run on forked-PR runners. + +## `nightly` lane — Docker money-path (scheduled / manual) + +Runs on a nightly `cron` (and `workflow_dispatch` with `run_nightly=true`). +Never runs on PRs. Brings up the compose topology (`make up`) — local nutshell +FakeWallet mints, so **real Cashu crypto, zero real sats** — and is wired to run +the `services_required` swap scenarios. + +The swap scenarios (`swap_foreign_mint`, `swap_foreign_mint_retry`) are added by +[`Routstr/routstr-testing#1`](https://github.com/Routstr/routstr-testing/pull/1) +(foreign-mint swap + retry; covers `routstr-core#549`/`#468` fund-loss). Their +orchestrator steps are present but **commented out** in the workflow until PR #1 +merges to `main`, so the lane never references files not yet on `main`. Once +PR #1 lands: uncomment the two steps. Pin `ROUTSTR_CORE_REF=refs/pull/549/head` +for the *retry* scenario until `core#549` merges (on `core` main it asserts the +no-retry 400/abort path), then drop the pin. + +Vendor refs default to `main` (`scripts/sync.sh`) so CI tests shipped code. diff --git a/pyproject.toml b/pyproject.toml index 30ba689..3610796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,31 @@ dependencies = [ "uvicorn>=0.27", ] +[dependency-groups] +# Tooling the no-docker `pr` CI lane runs (ruff + mypy). Pinned and declared +# here so CI is hermetic (`uv sync --group dev`) instead of leaking whatever +# ruff/mypy happen to be on a contributor's PATH. types-PyYAML supplies the +# stubs mypy needs for runner/{providers,scenario}.py. +dev = [ + "ruff==0.1.7", + "mypy==1.9.0", + "types-PyYAML>=6.0", +] + [tool.setuptools] packages = ["runner", "server", "tests"] +[tool.mypy] +# The no-docker CI lane type-checks the money-path orchestrator package +# (runner/) only. server/ uses idiomatic SQLModel query expressions that +# trip SQLAlchemy's typing overloads — out of scope for this lane. +python_version = "3.11" +files = ["runner"] + +[tool.ruff] +target-version = "py311" +line-length = 100 + [tool.pytest.ini_options] markers = [ "requires_funded_daemon: test depends on a topped-up routstrd", diff --git a/runner/orchestrate.py b/runner/orchestrate.py index 5a387d9..8bde249 100644 --- a/runner/orchestrate.py +++ b/runner/orchestrate.py @@ -27,7 +27,6 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any from sqlmodel import select @@ -830,11 +829,34 @@ def main(argv: list[str] | None = None) -> int: print(f"[orchestrate] upstream config error: {exc}", file=sys.stderr) return 2 + # The run row carries the authoritative outcome. CI lanes drive this + # entry point and gate on the *process exit code*, so a scenario whose + # tests failed (status=failed) or that errored (status=error, e.g. no + # tests collected / compose failure / timeout) MUST surface as a non-zero + # exit. Otherwise a money-path guard lane silently passes while the + # swap/refund logic under test is red. + status = _read_run_status(Path(args.db), run_id) + # Echo a machine-friendly summary so callers (FastAPI server in ROU-134) # can pick up the new run id without re-querying SQLite. - print(json.dumps({"run_id": run_id, "db": args.db})) + print(json.dumps({"run_id": run_id, "db": args.db, "status": status})) + + if status in {"failed", "error"}: + print( + f"[orchestrate] run #{run_id} status={status} → exit 1", + file=sys.stderr, + ) + return 1 return 0 +def _read_run_status(db_path: Path, run_id: int) -> str | None: + """Read the finalized status of a run row (CI exit-code source).""" + engine = get_engine(db_path) + with get_session(engine) as session: + row = session.get(Run, run_id) + return row.status if row is not None else None + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/server/runs.py b/server/runs.py index b8d4733..e598bec 100644 --- a/server/runs.py +++ b/server/runs.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from fastapi import APIRouter, HTTPException, Query, Request, status from fastapi.responses import PlainTextResponse from sqlalchemy import desc from sqlmodel import select @@ -327,11 +327,21 @@ def spawn_orchestrator( text=True, timeout=int(env.get("SERVER_ORCHESTRATE_TIMEOUT", "1800")), ) - if proc.returncode != 0: - raise OrchestratorError( - f"orchestrator exited {proc.returncode}: {proc.stderr.strip()[:400]}" - ) - return _parse_run_id(proc.stdout) + # The orchestrator now exits non-zero when a scenario's tests fail/error + # (so CLI/CI can gate on the exit code). For the UI that is still a + # *recorded* run — the run row exists with status=failed/error and should + # be returned so the UI can show it. A non-zero exit with NO run_id summary + # line means the orchestrator never recorded a run (e.g. config error) — a + # true failure we surface as an OrchestratorError. + try: + return _parse_run_id(proc.stdout) + except OrchestratorError: + if proc.returncode != 0: + raise OrchestratorError( + f"orchestrator exited {proc.returncode}: " + f"{proc.stderr.strip()[:400]}" + ) from None + raise def _parse_run_id(stdout: str) -> int: diff --git a/tests/cli/test_handwritten.py b/tests/cli/test_handwritten.py index b1e6a3d..4e6b57c 100644 --- a/tests/cli/test_handwritten.py +++ b/tests/cli/test_handwritten.py @@ -10,7 +10,7 @@ import pytest -from tests.cli.helpers import run_cli, NODE_A_INTERNAL +from tests.cli.helpers import run_cli # CLI side-effects here are local config writes (init/instruct/show) — they # don't mutate the routstr node — so the whole module is safe to point at a diff --git a/tests/test_orchestrate_balance.py b/tests/test_orchestrate_balance.py index b5ff3d2..a27e92f 100644 --- a/tests/test_orchestrate_balance.py +++ b/tests/test_orchestrate_balance.py @@ -14,7 +14,6 @@ from pathlib import Path import pytest -from sqlmodel import select from runner import orchestrate as orch_mod from runner.models import Run, get_engine, get_session diff --git a/tests/test_orchestrate_exit_code.py b/tests/test_orchestrate_exit_code.py new file mode 100644 index 0000000..9ee6e57 --- /dev/null +++ b/tests/test_orchestrate_exit_code.py @@ -0,0 +1,134 @@ +"""Orchestrator process exit code must reflect the money-path outcome. + +This is the CI-integrity guard. The no-docker `pr` lane wires a mock +money-path scenario through `python -m runner.orchestrate` and relies on +the process exit code to gate the PR. If the orchestrator exits 0 even +when the scenario's tests FAIL (status=failed) or ERROR (status=error), +the CI lane is a silent-pass: it stays green while the money logic is +broken — the exact false-safety failure mode flagged on the foreign-mint +swap PR. + +These tests drive `runner.orchestrate.main(argv)` (the module entry point +CI calls) end-to-end with sync/compose/topup short-circuited, so they need +no Docker and no funds, and assert the returned exit code maps: + + passed -> 0 + failed -> non-zero + error -> non-zero +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from runner import orchestrate as orch_mod + +PASS_SCENARIO = """\ +id: exit_pass +name: Exit Pass +description: Mock money-path scenario whose tests pass. +services_required: false +selection: + paths: [tests/test_smoke.py] + markers: [] +parameters: {} +expected_cost_sats: 0 +timeout_seconds: 60 +""" + +FAIL_SCENARIO = """\ +id: exit_fail +name: Exit Fail +description: Mock money-path scenario whose tests fail (status=failed). +services_required: false +selection: + paths: [tests/test_smoke_fail.py] + markers: [] +parameters: {} +expected_cost_sats: 0 +timeout_seconds: 60 +""" + + +def _junit(failures: int) -> str: + return ( + '' + f'' + '' + + ('boom' if failures else '') + + '' + ) + + +@pytest.fixture() +def harness(tmp_path: Path, monkeypatch): + """scenarios/ dir + bypassed sync/compose/topup, mockable pytest rc.""" + scenarios_dir = tmp_path / "scenarios" + scenarios_dir.mkdir() + (scenarios_dir / "exit_pass.yaml").write_text(PASS_SCENARIO) + (scenarios_dir / "exit_fail.yaml").write_text(FAIL_SCENARIO) + + monkeypatch.setenv("SKIP_SYNC", "1") + # No docker -> services_required paths are inert, but these scenarios + # are services_required: false anyway. + monkeypatch.setattr(orch_mod.shutil, "which", lambda _: None) + + db_path = tmp_path / "runs.db" + monkeypatch.setattr(orch_mod, "DEFAULT_DB", db_path) + monkeypatch.setattr(orch_mod, "DEFAULT_SCENARIOS", scenarios_dir) + + return tmp_path, scenarios_dir, db_path + + +def _run_main(monkeypatch, scenarios_dir: Path, db_path: Path, scenario: str, + rc: int) -> int: + """Drive main() with pytest stubbed to return `rc` + a matching junit.""" + + def fake_run_pytest(scenario_obj, junit_path, env): + junit_path.write_text(_junit(failures=1 if rc else 0)) + return rc, "stubbed pytest output" + + monkeypatch.setattr(orch_mod, "_run_pytest", fake_run_pytest) + return orch_mod.main( + [ + "--scenario", + scenario, + "--token", + "placeholder", + "--db", + str(db_path), + "--scenarios-dir", + str(scenarios_dir), + ] + ) + + +def test_passing_scenario_exits_zero(harness, monkeypatch): + _, scenarios_dir, db_path = harness + code = _run_main(monkeypatch, scenarios_dir, db_path, "exit_pass", rc=0) + assert code == 0 + + +def test_failing_scenario_exits_nonzero(harness, monkeypatch): + """A money-path scenario whose tests FAIL must not exit 0. + + Regression guard: previously main() returned 0 regardless of the + scenario outcome, so a CI money-path lane stayed green while the + swap/refund logic under test was red. + """ + _, scenarios_dir, db_path = harness + code = _run_main(monkeypatch, scenarios_dir, db_path, "exit_fail", rc=1) + assert code != 0, ( + "orchestrator exited 0 for a FAILED money-path scenario — the CI " + "guard would silently pass while the money logic is broken" + ) + + +def test_errored_scenario_exits_nonzero(harness, monkeypatch): + """no-tests-collected (pytest rc=5) -> status=error -> non-zero exit.""" + _, scenarios_dir, db_path = harness + code = _run_main(monkeypatch, scenarios_dir, db_path, "exit_pass", rc=5) + assert code != 0 diff --git a/tests/test_orchestrate_upstream.py b/tests/test_orchestrate_upstream.py index 01461b9..a99ae6e 100644 --- a/tests/test_orchestrate_upstream.py +++ b/tests/test_orchestrate_upstream.py @@ -116,7 +116,6 @@ def test_remote_plus_real_no_provider_key_needed(): def test_price_usage_absent_is_none(tmp_path: Path): - models = PROVIDERS_DIR / "providers" / "models" # wrong path on purpose assert price_usage_file(tmp_path / USAGE_FILENAME, tmp_path / "x.json") is None diff --git a/tests/test_server_orchestrator_exit.py b/tests/test_server_orchestrator_exit.py new file mode 100644 index 0000000..8f6fb84 --- /dev/null +++ b/tests/test_server_orchestrator_exit.py @@ -0,0 +1,84 @@ +"""Server tolerance of the orchestrator's new non-zero exit on failed runs. + +The orchestrator now exits non-zero when a scenario's tests fail/error so +CLI/CI can gate on the process exit code. For the FastAPI UI that is still a +*recorded* run (the run row exists with status=failed), so `spawn_orchestrator` +must return the run_id instead of raising — as long as a run_id summary line +was emitted. A non-zero exit with NO run_id line (e.g. a config error before +any run was recorded) must still raise OrchestratorError. +""" + +from __future__ import annotations + +import sys +import textwrap +from pathlib import Path + +import pytest + +from server.runs import OrchestratorError, spawn_orchestrator + + +def _cfg(tmp_path: Path, orchestrate_cmd: list[str]): + class Cfg: + db_path = tmp_path / "runs.db" + scenarios_dir = tmp_path / "scenarios" + logs_dir = tmp_path / "logs" + compose_file = tmp_path / "compose.yml" + cors_origins = ["http://localhost:5173"] + + Cfg.orchestrate_cmd = orchestrate_cmd + return Cfg + + +def _fake_orchestrator(tmp_path: Path, *, body: str) -> list[str]: + script = tmp_path / "fake_orchestrate.py" + script.write_text(textwrap.dedent(body)) + return [sys.executable, str(script)] + + +def test_nonzero_exit_with_run_id_returns_run_id(tmp_path: Path): + """A failed scenario (run recorded, exit 1) → return the run_id, not raise.""" + cmd = _fake_orchestrator( + tmp_path, + body=""" + import json, sys + print(json.dumps({'run_id': 7, 'db': 'x', 'status': 'failed'})) + sys.exit(1) + """, + ) + run_id = spawn_orchestrator( + scenario_id="smoke", token="cashuX", config=_cfg(tmp_path, cmd) + ) + assert run_id == 7 + + +def test_nonzero_exit_without_run_id_raises(tmp_path: Path): + """A true orchestrator failure (no run recorded) must still raise.""" + cmd = _fake_orchestrator( + tmp_path, + body=""" + import sys + sys.stderr.write('upstream config error: boom') + sys.exit(2) + """, + ) + with pytest.raises(OrchestratorError): + spawn_orchestrator( + scenario_id="smoke", token="cashuX", config=_cfg(tmp_path, cmd) + ) + + +def test_zero_exit_with_run_id_returns_run_id(tmp_path: Path): + """The unchanged happy path still works.""" + cmd = _fake_orchestrator( + tmp_path, + body=""" + import json + print(json.dumps({'run_id': 3, 'db': 'x', 'status': 'passed'})) + """, + ) + run_id = spawn_orchestrator( + scenario_id="smoke", token="cashuX", config=_cfg(tmp_path, cmd) + ) + assert run_id == 3 diff --git a/tests/test_server_runs.py b/tests/test_server_runs.py index b3ae11a..d978599 100644 --- a/tests/test_server_runs.py +++ b/tests/test_server_runs.py @@ -13,7 +13,6 @@ import pytest from fastapi.testclient import TestClient -from sqlmodel import select from runner.models import Run, TestResult, get_engine, get_session from server.config import ServerConfig diff --git a/tests/test_server_token_hygiene.py b/tests/test_server_token_hygiene.py index f616c40..d3764db 100644 --- a/tests/test_server_token_hygiene.py +++ b/tests/test_server_token_hygiene.py @@ -7,7 +7,6 @@ from __future__ import annotations import sqlite3 -import subprocess import sys import textwrap from pathlib import Path @@ -43,7 +42,6 @@ def test_no_token_column_on_runs_table(config): """Hard schema check: there must be no column whose name suggests storage of the cashu token on the `runs` table. """ - engine = get_engine(config.db_path) with sqlite3.connect(config.db_path) as conn: cols = [row[1] for row in conn.execute("PRAGMA table_info(runs)").fetchall()] banned = {"cashu_token", "token", "cashutoken"} @@ -149,8 +147,8 @@ class Cfg: text = capture.read_text() assert "argv=" in text and "env=" in text - argv_line = [l for l in text.splitlines() if l.startswith("argv=")][0] - env_line = [l for l in text.splitlines() if l.startswith("env=")][0] + argv_line = [ln for ln in text.splitlines() if ln.startswith("argv=")][0] + env_line = [ln for ln in text.splitlines() if ln.startswith("env=")][0] assert TOKEN not in argv_line, "token was passed on argv — never do this" assert TOKEN in env_line, "token must be passed via E2E_CASHU_TOKEN" diff --git a/uv.lock b/uv.lock index eeccb7a..81bddc3 100644 --- a/uv.lock +++ b/uv.lock @@ -88,9 +88,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" }, @@ -98,9 +96,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, @@ -108,9 +104,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, @@ -118,9 +112,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, @@ -128,18 +120,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, - { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, @@ -147,9 +135,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, @@ -211,6 +197,38 @@ 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 = "mypy" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/1e/a587a862c766a755a58b62d8c00aed11b74a15dc415c1bf5da7b607b0efd/mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", size = 2995901, upload-time = "2024-03-08T16:10:12.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/ce/c62c0c0d83b8a936ad6d5e0294e956e881acc5d680deb4929ea259fb50f6/mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3", size = 10605548, upload-time = "2024-03-08T16:09:51.882Z" }, + { url = "https://files.pythonhosted.org/packages/da/e2/1864612774cf8a445f6d42ce73ce0f1492a37ed2af1c908e989f1ec7d349/mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc", size = 9767399, upload-time = "2024-03-08T16:10:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/a1/81/97e8539d6cdcfb3a8ae7eb1438c6983a9fc434ef9664572bfa7fd285cab9/mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129", size = 12421347, upload-time = "2024-03-08T16:09:28.368Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cf/43c1e29b9d3b2bf6c75e32d021d7db4631c516e4c0bd72b75bc8836680d8/mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612", size = 12490944, upload-time = "2024-03-08T16:08:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/59/56/a33d610a9cf692669690a89b54a6a920fd7c7ebcca00da2c36c9d975de8e/mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3", size = 9226854, upload-time = "2024-03-08T16:09:56.504Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/40f0f605b1d4e2ad1fb11d21988ce3a3e205886c0fcbd35c9789a214de9a/mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", size = 10725390, upload-time = "2024-03-08T16:10:01.099Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d2/072e40384b53051106b4fcf03537fb88e2a6ad0757d2ab7f6c8c2f188a69/mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", size = 9731292, upload-time = "2024-03-08T16:08:48.463Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/b7dc7eb69eda899fd07e71403b51b598a1f4df0f452d1da5844374082bcd/mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", size = 12455450, upload-time = "2024-03-08T16:08:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1b/3e962a201d2f0f57c9fa1990e0dd6076f4f2f94954ab56e4a701ec3cc070/mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", size = 12530368, upload-time = "2024-03-08T16:09:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/72/1f/8b214b69d08cc5e4bd8c3769ac55a43318f3529362ea55e5957774b69924/mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6", size = 9319112, upload-time = "2024-03-08T16:09:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0ba2eaedca52bf5276275e8489951c26206030b3d31bf06f00875ae75d5d/mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", size = 2555887, upload-time = "2024-03-08T16:09:48.584Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -439,6 +457,13 @@ dependencies = [ { name = "uvicorn" }, ] +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "types-pyyaml" }, +] + [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.110" }, @@ -449,6 +474,37 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.27" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = "==1.9.0" }, + { name = "ruff", specifier = "==0.1.7" }, + { name = "types-pyyaml", specifier = ">=6.0" }, +] + +[[package]] +name = "ruff" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/e8/3166c7f559e2c2449af4af5203f5ce4a5d886c576152f4dfbda02430c9f1/ruff-0.1.7.tar.gz", hash = "sha256:dffd699d07abf54833e5f6cc50b85a6ff043715da8788c4a79bcd4ab4734d306", size = 1842985, upload-time = "2023-12-04T21:54:48.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/34/fa53ed50bd20797b8aa27dcf0a52548b629fd0c65008d334ab2fae9bdce8/ruff-0.1.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7f80496854fdc65b6659c271d2c26e90d4d401e6a4a31908e7e334fab4645aac", size = 11761048, upload-time = "2023-12-04T21:53:47.32Z" }, + { url = "https://files.pythonhosted.org/packages/a6/54/a3f6d5f94aaaaf049ddcaf346bd2c1ed1598c54ddcc938c7eb8863a0ce91/ruff-0.1.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1ea109bdb23c2a4413f397ebd8ac32cb498bee234d4191ae1a310af760e5d287", size = 6054318, upload-time = "2023-12-04T21:53:53.335Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/110973f5409a3a044cb0f9fe9a452a44c8e6be5e9576bbbefd4ce8e8c197/ruff-0.1.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0c2de9dd9daf5e07624c24add25c3a490dbf74b0e9bca4145c632457b3b42a", size = 6000535, upload-time = "2023-12-04T21:53:57.201Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a3/e740355f220ea0dd4c24449b94063a4018f62290841cf8f589e945cf274b/ruff-0.1.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:69a4bed13bc1d5dabf3902522b5a2aadfebe28226c6269694283c3b0cecb45fd", size = 5689144, upload-time = "2023-12-04T21:54:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f1/9d3f2aee19293496a72a772e62f66a8828294af0b9164a3ec6f34d1104b8/ruff-0.1.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de02ca331f2143195a712983a57137c5ec0f10acc4aa81f7c1f86519e52b92a1", size = 6150987, upload-time = "2023-12-04T21:54:06.971Z" }, + { url = "https://files.pythonhosted.org/packages/78/a4/e53ab7643eccff6ef842a4865a652e8c403a052c9a2f756f7d436839318d/ruff-0.1.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45b38c3f8788a65e6a2cab02e0f7adfa88872696839d9882c13b7e2f35d64c5f", size = 6829095, upload-time = "2023-12-04T21:54:10.425Z" }, + { url = "https://files.pythonhosted.org/packages/73/c4/f20c811eb11869b39c14f4b03cf3da64092491788b9565f71ea2fa2170c9/ruff-0.1.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c64cb67b2025b1ac6d58e5ffca8f7b3f7fd921f35e78198411237e4f0db8e73", size = 6688056, upload-time = "2023-12-04T21:54:13.924Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4f/c1aee31c7ac44ce8b90650e37576b5146f8b61b2c6a4601b42dd5c6c7332/ruff-0.1.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dcc6bb2f4df59cb5b4b40ff14be7d57012179d69c6565c1da0d1f013d29951b", size = 7816655, upload-time = "2023-12-04T21:54:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/41/7f/df4c0887ec638b4b18a9cfada73db3cb6653742b49432e08d55d417c41b3/ruff-0.1.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2bb4bb6bbe921f6b4f5b6fdd8d8468c940731cb9406f274ae8c5ed7a78c478", size = 6358832, upload-time = "2023-12-04T21:54:20.347Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/7c0a228b661ef4fd91dcea97fdbd75d3f3e5aca16c3697e31daaa51cf051/ruff-0.1.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:276a89bcb149b3d8c1b11d91aa81898fe698900ed553a08129b38d9d6570e717", size = 5949976, upload-time = "2023-12-04T21:54:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9e/b5e88e6a9622e48f36ae9bf9d5d39c5ab4cffc7a98491d2e6ee2ec51a39a/ruff-0.1.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:90c958fe950735041f1c80d21b42184f1072cc3975d05e736e8d66fc377119ea", size = 5690159, upload-time = "2023-12-04T21:54:26.697Z" }, + { url = "https://files.pythonhosted.org/packages/97/17/e04b89505180e4190ef5f5438ec14209736d65f80dd90ddef403f2c11643/ruff-0.1.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b05e3b123f93bb4146a761b7a7d57af8cb7384ccb2502d29d736eaade0db519", size = 6032203, upload-time = "2023-12-04T21:54:29.469Z" }, + { url = "https://files.pythonhosted.org/packages/8a/47/a259b32ad202c9d90edf1134804a860b1d0ba72715aaa8eb19d19a370ff6/ruff-0.1.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:290ecab680dce94affebefe0bbca2322a6277e83d4f29234627e0f8f6b4fa9ce", size = 6419631, upload-time = "2023-12-04T21:54:33.445Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ef/33c8c31f09c7ee1027d0aeba48eaaf35d3b8c307e6650dbc0ff990e7ca9a/ruff-0.1.7-py3-none-win32.whl", hash = "sha256:416dfd0bd45d1a2baa3b1b07b1b9758e7d993c256d3e51dc6e03a5e7901c7d80", size = 5902118, upload-time = "2023-12-04T21:54:36.42Z" }, + { url = "https://files.pythonhosted.org/packages/02/18/554a06e27125849118a33014fc658a1d689bc149940ed07e36f123aa2fed/ruff-0.1.7-py3-none-win_amd64.whl", hash = "sha256:4af95fd1d3b001fc41325064336db36e3d27d2004cdb6d21fd617d45a172dd96", size = 6289114, upload-time = "2023-12-04T21:54:40.037Z" }, + { url = "https://files.pythonhosted.org/packages/40/40/e5afee4d3f197a1f5da5c971092ef50439972bbc15599abf47150cf947b4/ruff-0.1.7-py3-none-win_arm64.whl", hash = "sha256:0683b7bfbb95e6df3c7c04fe9d78f631f8e8ba4868dfc932d43d690698057e2e", size = 6035594, upload-time = "2023-12-04T21:54:43.622Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.50" @@ -524,6 +580,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"