From b41361eb1bf6eb6f5ff8dc9d656ed3333e0292cc Mon Sep 17 00:00:00 2001 From: Byron Gamatos Date: Fri, 10 Jul 2026 19:05:01 +0200 Subject: [PATCH] refactor(server): extract the loops routes into routers/loops.py (R3) (#839) Third router. Practice loops (saved A/B regions per song): GET/POST/DELETE /api/loops, meta_db-only (0 setattr targets, 0 helpers to relocate per router_scan.py). Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db. include_router at the original site; 143-route table identical to origin/main. server.py: 9,337 -> 9,301. No test retargeting (test_demo_mode only names the paths in middleware regexes). Verified: pyflakes clean; route table identical; pytest 2398 passed; packaging guard green; eslint 0; boot smoke drives POST (auto-names "Loop N") / GET / DELETE / missing-fields error, and demo mode 403s both writes while allowing the read. Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- docs/size-exemptions.md | 4 ++-- lib/routers/loops.py | 52 +++++++++++++++++++++++++++++++++++++++++ server.py | 43 ++++------------------------------ 4 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 lib/routers/loops.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 67857be..55fc46f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 next root-level module can't ship broken. ### Added -- **`routers/` — extracting `server.py`'s route layer, cheapest-first (R3).** Each PR moves a cohesive route group into a `fastapi.APIRouter` under `lib/routers/`, mounted with `app.include_router(...)` at its original site (FastAPI matches in registration order; the full route table stays byte-identical). Bodies are verbatim — only the decorator receiver (`@app` → `@router`) and singleton reads (`meta_db` → `appstate.meta_db`, resolved at call time) change. So far: `audio_effects` (5 routes), `artist_aliases` (5 routes — the Tidy-up/P4 alias overrides). The next cut is picked by a dependency-closure scan that ranks groups by how many `monkeypatch.setattr(server, …)` targets they'd drag along. +- **`routers/` — extracting `server.py`'s route layer, cheapest-first (R3).** Each PR moves a cohesive route group into a `fastapi.APIRouter` under `lib/routers/`, mounted with `app.include_router(...)` at its original site (FastAPI matches in registration order; the full route table stays byte-identical). Bodies are verbatim — only the decorator receiver (`@app` → `@router`) and singleton reads (`meta_db` → `appstate.meta_db`, resolved at call time) change. So far: `audio_effects` (5), `artist_aliases` (5), `loops` (3 — saved A/B practice regions). The next cut is picked by a dependency-closure scan that ranks groups by how many `monkeypatch.setattr(server, …)` targets they'd drag along. - **`routers/` — the first extracted route module (R3).** The five audio-effects mapping endpoints move out of `server.py` into `lib/routers/audio_effects.py` as a `fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the file diff --git a/docs/size-exemptions.md b/docs/size-exemptions.md index eb57bd8..9adda18 100644 --- a/docs/size-exemptions.md +++ b/docs/size-exemptions.md @@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable. ## Planned, NOT exempt (owned by split plans — listed so nothing falls between states) core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py` -(9,337 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` -extractions and the first two `routers/` modules) · +(9,302 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB` +extractions and the first three `routers/` modules) · `lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines and is a monolith in its own right, to be split per-table once the router train lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js` diff --git a/lib/routers/loops.py b/lib/routers/loops.py new file mode 100644 index 0000000..bb17628 --- /dev/null +++ b/lib/routers/loops.py @@ -0,0 +1,52 @@ +"""Practice loops — saved A/B regions per song. + +Extracted verbatim from ``server.py`` (R3); only the decorator receiver +(``@app`` -> ``@router``) and the singleton reads (``meta_db`` -> +``appstate.meta_db``) changed. See ``appstate.py`` for why the reads stay +module attributes. +""" + +from fastapi import APIRouter + +import appstate + +router = APIRouter() + + +@router.get("/api/loops") +def list_loops(filename: str): + rows = appstate.meta_db.conn.execute( + "SELECT id, name, start_time, end_time FROM loops WHERE filename = ? ORDER BY start_time", + (filename,) + ).fetchall() + return [{"id": r[0], "name": r[1], "start": r[2], "end": r[3]} for r in rows] + + +@router.post("/api/loops") +def save_loop(data: dict): + filename = data.get("filename", "") + name = data.get("name", "").strip() + start = data.get("start") + end = data.get("end") + if not filename or start is None or end is None: + return {"error": "Missing fields"} + if not name: + count = appstate.meta_db.conn.execute( + "SELECT COUNT(*) FROM loops WHERE filename = ?", (filename,) + ).fetchone()[0] + name = f"Loop {count + 1}" + with appstate.meta_db._lock: + appstate.meta_db.conn.execute( + "INSERT INTO loops (filename, name, start_time, end_time) VALUES (?, ?, ?, ?)", + (filename, name, float(start), float(end)) + ) + appstate.meta_db.conn.commit() + return {"ok": True, "name": name} + + +@router.delete("/api/loops/{loop_id}") +def delete_loop(loop_id: int): + with appstate.meta_db._lock: + appstate.meta_db.conn.execute("DELETE FROM loops WHERE id = ?", (loop_id,)) + appstate.meta_db.conn.commit() + return {"ok": True} diff --git a/server.py b/server.py index a7b3ab9..816b662 100644 --- a/server.py +++ b/server.py @@ -71,7 +71,7 @@ from audio_effects_db import AudioEffectsMappingDB # Lives in lib/ because that is the one core dir every packaging path copies. import appstate # Extracted route modules. They import `appstate`, never `server` — one-way graph. -from routers import audio_effects, artist_aliases +from routers import audio_effects, artist_aliases, loops import sloppak as sloppak_mod import drums as drums_mod import notation as notation_mod @@ -5903,44 +5903,9 @@ def api_remove_wanted(wanted_id: int): # ── Loops API ──────────────────────────────────────────────────────────────── - -@app.get("/api/loops") -def list_loops(filename: str): - rows = meta_db.conn.execute( - "SELECT id, name, start_time, end_time FROM loops WHERE filename = ? ORDER BY start_time", - (filename,) - ).fetchall() - return [{"id": r[0], "name": r[1], "start": r[2], "end": r[3]} for r in rows] - - -@app.post("/api/loops") -def save_loop(data: dict): - filename = data.get("filename", "") - name = data.get("name", "").strip() - start = data.get("start") - end = data.get("end") - if not filename or start is None or end is None: - return {"error": "Missing fields"} - if not name: - count = meta_db.conn.execute( - "SELECT COUNT(*) FROM loops WHERE filename = ?", (filename,) - ).fetchone()[0] - name = f"Loop {count + 1}" - with meta_db._lock: - meta_db.conn.execute( - "INSERT INTO loops (filename, name, start_time, end_time) VALUES (?, ?, ?, ?)", - (filename, name, float(start), float(end)) - ) - meta_db.conn.commit() - return {"ok": True, "name": name} - - -@app.delete("/api/loops/{loop_id}") -def delete_loop(loop_id: int): - with meta_db._lock: - meta_db.conn.execute("DELETE FROM loops WHERE id = ?", (loop_id,)) - meta_db.conn.commit() - return {"ok": True} +# Mounted here, where these routes used to be defined (FastAPI matches in +# registration order). Implementation in lib/routers/loops.py. +app.include_router(loops.router) # ── Audio Effects Mapping API ───────────────────────────────────────────────