refactor(server): extract the audio-effects routes into routers/audio_effects.py (R3) (#834)
ship-ci / ci (push) Waiting to run

The first route module through the appstate seam (#833). Picked BY MEASUREMENT,
not by the plan's guess: a transitive dep-closure scan over every route group
ranked audio-effects at 0 monkeypatch.setattr targets and exactly one exclusive
helper. (The same scan disproved the plan's assumption that artists/aliases was
free -- api_artist_links reaches _mb_http_get and _enrich_network_enabled, both
setattr targets.)

Bodies are verbatim. The only edits are mechanical:
  @app.get(...)            -> @router.get(...)
  audio_effect_mappings.x  -> appstate.audio_effect_mappings.x

The singleton read must stay a module attribute resolved at call time, so a
re-imported server re-publishes a fresh DB into the seam and monkeypatch reaches
this module. `routers/` never imports `server`: server -> routers -> appstate.

`app.include_router(...)` sits exactly where the routes used to be defined --
FastAPI matches in registration order, so the mount site preserves it. Verified
by diffing the FULL route table against origin/main: 143 routes, identical
paths, methods AND order.

server.py: 9,445 -> 9,386 lines. `fastapi.Query` went dead with the move and was
removed (the other four unused imports are pre-existing on main).

Packaging: COPY routers/ /app/routers/ plus `!routers/` + `!routers/**` in
.dockerignore (that file opens with a blanket `*`). Verified against the real
docker daemon: routers/ reaches the build context, __pycache__ does not.

Verified: pyflakes clean on routers/; no new undefined name in server.py;
pytest 2348 passed (75 in the audio-effects + demo-mode suites); eslint 0
errors; boot smoke drives all five routes end-to-end (create -> read back ->
activate -> clear -> delete -> 404 on missing -> 400 on bad body), Query(...)
still 422s on a missing required param, and demo mode still 403s all four
moved write routes while allowing the read.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-10 15:24:31 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent d6f2df14f7
commit ebe59d3f97
8 changed files with 129 additions and 68 deletions
+6 -65
View File
@@ -21,7 +21,7 @@ configure_logging()
log = logging.getLogger("feedBack.server")
from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, HTTPException, Query
from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, HTTPException
from fastapi.concurrency import run_in_threadpool
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
@@ -69,6 +69,8 @@ from audio_effects_db import AudioEffectsMappingDB
# The router seam. Imported as a module (never `from appstate import ...`) so
# `appstate.configure(...)` below publishes into the same namespace routers read.
import appstate
# Extracted route modules. They import `appstate`, never `server` — one-way graph.
from routers import audio_effects
import sloppak as sloppak_mod
import drums as drums_mod
import notation as notation_mod
@@ -5991,70 +5993,9 @@ def delete_loop(loop_id: int):
# ── Audio Effects Mapping API ───────────────────────────────────────────────
def _audio_effects_error(exc: Exception):
return JSONResponse({"error": str(exc)}, status_code=400)
@app.get("/api/audio-effects/mappings")
def list_audio_effect_mappings(
song_key: str = Query(""),
filename: str = Query(""),
tone_key: str = Query(""),
provider_id: str = Query(""),
):
try:
return {
"mappings": audio_effect_mappings.list(
song_key=song_key,
filename=filename,
tone_key=tone_key,
provider_id=provider_id,
)
}
except ValueError as exc:
return _audio_effects_error(exc)
@app.post("/api/audio-effects/mappings")
def upsert_audio_effect_mapping(data: dict = Body(...)):
try:
mapping = audio_effect_mappings.upsert(data)
except ValueError as exc:
return _audio_effects_error(exc)
return {"ok": True, "mapping": mapping}
@app.delete("/api/audio-effects/mappings/{mapping_id}")
def delete_audio_effect_mapping(mapping_id: int, provider_id: str = Query("")):
try:
deleted = audio_effect_mappings.delete(mapping_id, provider_id=provider_id)
except ValueError as exc:
return _audio_effects_error(exc)
if not deleted:
return JSONResponse({"error": "mapping not found"}, status_code=404)
return {"ok": True}
@app.post("/api/audio-effects/mappings/{mapping_id}/activate")
def activate_audio_effect_mapping(mapping_id: int, data: dict = Body(default_factory=dict)):
try:
provider_id = data.get("provider_id") if "provider_id" in data else data.get("providerId")
mapping = audio_effect_mappings.activate(mapping_id, provider_id="" if provider_id is None else provider_id)
except ValueError as exc:
return _audio_effects_error(exc)
if not mapping:
return JSONResponse({"error": "mapping not found"}, status_code=404)
return {"ok": True, "mapping": mapping}
@app.delete("/api/audio-effects/active-mapping")
def clear_audio_effect_active_mapping(song_key: str = Query(...), tone_key: str = Query("")):
try:
cleared = audio_effect_mappings.clear_active(song_key=song_key, tone_key=tone_key)
except ValueError as exc:
return _audio_effects_error(exc)
return {"ok": True, "cleared": cleared}
# Mounted here, where these routes used to be defined: FastAPI matches in
# registration order, so the mount site preserves it.
app.include_router(audio_effects.router)
# ── Settings API ──────────────────────────────────────────────────────────────