mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
refactor(server): extract the audio-effects routes into routers/audio_effects.py (R3) (#834)
ship-ci / ci (push) Waiting to run
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:
co-authored by
Claude Opus 4.8
parent
d6f2df14f7
commit
ebe59d3f97
+4
-1
@@ -7,8 +7,11 @@
|
|||||||
!server.py
|
!server.py
|
||||||
# The router seam server.py injects its singletons into (R3). Root-level Python
|
# The router seam server.py injects its singletons into (R3). Root-level Python
|
||||||
# that ships in the image must be re-allowed explicitly — this file starts with
|
# that ships in the image must be re-allowed explicitly — this file starts with
|
||||||
# a blanket `*` exclusion. `routers/` needs the same treatment when it lands.
|
# a blanket `*` exclusion.
|
||||||
!appstate.py
|
!appstate.py
|
||||||
|
# Route modules extracted from server.py (R3).
|
||||||
|
!routers/
|
||||||
|
!routers/**
|
||||||
!main.py
|
!main.py
|
||||||
!VERSION
|
!VERSION
|
||||||
!tailwind.config.js
|
!tailwind.config.js
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **`routers/` — the first extracted route module (R3).** The five audio-effects mapping
|
||||||
|
endpoints move out of `server.py` into `routers/audio_effects.py` as a
|
||||||
|
`fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the file
|
||||||
|
where they used to be defined** — FastAPI matches routes in registration order, so the
|
||||||
|
mount site preserves it. Verified: the full 143-route table (paths, methods, *and*
|
||||||
|
order) is byte-for-byte identical to `main`. Bodies are verbatim; the only edits are
|
||||||
|
the decorator receiver (`@app.get` → `@router.get`) and the singleton read
|
||||||
|
(`audio_effect_mappings` → `appstate.audio_effect_mappings`, a module attribute
|
||||||
|
resolved at call time). This proves the seam from #833 under a real consumer, including
|
||||||
|
the second slot. The `_demo_mode_guard` middleware still blocks all four moved write
|
||||||
|
routes with 403, and `Query(...)` validation still 422s — both checked against a running
|
||||||
|
server. Ships via `COPY routers/ /app/routers/` plus `!routers/` + `!routers/**` in
|
||||||
|
`.dockerignore`. `server.py`: **9,445 → 9,386 lines**.
|
||||||
- **`appstate.py` — the router seam (R3).** Route modules moving out of `server.py`
|
- **`appstate.py` — the router seam (R3).** Route modules moving out of `server.py`
|
||||||
need `meta_db` and friends but must not `import server`, or the import graph goes
|
need `meta_db` and friends but must not `import server`, or the import graph goes
|
||||||
circular the moment `server` imports them back. So `server.py` keeps *constructing*
|
circular the moment `server` imports them back. So `server.py` keeps *constructing*
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ COPY server.py /app/
|
|||||||
# The router seam server.py injects its singletons into (R3). Root-level, like
|
# The router seam server.py injects its singletons into (R3). Root-level, like
|
||||||
# server.py, so `import appstate` resolves off PYTHONPATH=/app.
|
# server.py, so `import appstate` resolves off PYTHONPATH=/app.
|
||||||
COPY appstate.py /app/
|
COPY appstate.py /app/
|
||||||
|
COPY routers/ /app/routers/
|
||||||
COPY main.py /app/
|
COPY main.py /app/
|
||||||
COPY VERSION /app/
|
COPY VERSION /app/
|
||||||
# Built-in diagnostic sloppaks seeded into DLC_DIR/diagnostics-builtin/ at scan
|
# Built-in diagnostic sloppaks seeded into DLC_DIR/diagnostics-builtin/ at scan
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
- ./server.py:/app/server.py
|
- ./server.py:/app/server.py
|
||||||
- ./appstate.py:/app/appstate.py
|
- ./appstate.py:/app/appstate.py
|
||||||
|
- ./routers:/app/routers
|
||||||
- ./VERSION:/app/VERSION
|
- ./VERSION:/app/VERSION
|
||||||
- ./ug_browser.py:/app/ug_browser.py
|
- ./ug_browser.py:/app/ug_browser.py
|
||||||
- ./lib:/app/lib
|
- ./lib:/app/lib
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable.
|
|||||||
## Planned, NOT exempt (owned by split plans — listed so nothing falls between states)
|
## 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`
|
core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py`
|
||||||
(9,445 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
(9,386 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
||||||
extractions, plus 12 lines for the `appstate.py` seam) ·
|
extractions and the first `routers/` module) ·
|
||||||
`lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines
|
`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
|
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`
|
lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js`
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""FastAPI route modules extracted from ``server.py`` (R3).
|
||||||
|
|
||||||
|
Each module here exposes a module-level ``router`` (a ``fastapi.APIRouter``)
|
||||||
|
that ``server.py`` mounts with ``app.include_router(...)`` at the point in the
|
||||||
|
file where those routes used to be defined — FastAPI matches routes in
|
||||||
|
registration order, so keeping the mount site preserves it.
|
||||||
|
|
||||||
|
**Routers must never ``import server``.** They reach core singletons through
|
||||||
|
the injected seam instead::
|
||||||
|
|
||||||
|
import appstate
|
||||||
|
|
||||||
|
@router.get("/api/thing")
|
||||||
|
def get_thing():
|
||||||
|
return appstate.meta_db.thing()
|
||||||
|
|
||||||
|
and always as a **module attribute, at call time** — never
|
||||||
|
``from appstate import meta_db``, which freezes the binding and defeats both a
|
||||||
|
later ``appstate.configure()`` and ``monkeypatch.setattr``. See ``appstate.py``.
|
||||||
|
|
||||||
|
Dependencies flow one way: ``server -> routers -> appstate``.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Audio-effects mapping API — the core-owned song/tone -> provider routing index.
|
||||||
|
|
||||||
|
Extracted verbatim from ``server.py`` (R3); only the decorator receiver
|
||||||
|
(``@app`` -> ``@router``) and the singleton read (``audio_effect_mappings`` ->
|
||||||
|
``appstate.audio_effect_mappings``) changed. The read must stay a module
|
||||||
|
attribute so a re-imported ``server`` re-publishes a fresh DB into the seam and
|
||||||
|
`monkeypatch.setattr` reaches this module — see ``appstate.py``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
import appstate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _audio_effects_error(exc: Exception):
|
||||||
|
return JSONResponse({"error": str(exc)}, status_code=400)
|
||||||
|
|
||||||
|
|
||||||
|
@router.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": appstate.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)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/audio-effects/mappings")
|
||||||
|
def upsert_audio_effect_mapping(data: dict = Body(...)):
|
||||||
|
try:
|
||||||
|
mapping = appstate.audio_effect_mappings.upsert(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
return _audio_effects_error(exc)
|
||||||
|
return {"ok": True, "mapping": mapping}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/audio-effects/mappings/{mapping_id}")
|
||||||
|
def delete_audio_effect_mapping(mapping_id: int, provider_id: str = Query("")):
|
||||||
|
try:
|
||||||
|
deleted = appstate.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}
|
||||||
|
|
||||||
|
|
||||||
|
@router.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 = appstate.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}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/audio-effects/active-mapping")
|
||||||
|
def clear_audio_effect_active_mapping(song_key: str = Query(...), tone_key: str = Query("")):
|
||||||
|
try:
|
||||||
|
cleared = appstate.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}
|
||||||
@@ -21,7 +21,7 @@ configure_logging()
|
|||||||
|
|
||||||
log = logging.getLogger("feedBack.server")
|
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.concurrency import run_in_threadpool
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
|
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
|
# The router seam. Imported as a module (never `from appstate import ...`) so
|
||||||
# `appstate.configure(...)` below publishes into the same namespace routers read.
|
# `appstate.configure(...)` below publishes into the same namespace routers read.
|
||||||
import appstate
|
import appstate
|
||||||
|
# Extracted route modules. They import `appstate`, never `server` — one-way graph.
|
||||||
|
from routers import audio_effects
|
||||||
import sloppak as sloppak_mod
|
import sloppak as sloppak_mod
|
||||||
import drums as drums_mod
|
import drums as drums_mod
|
||||||
import notation as notation_mod
|
import notation as notation_mod
|
||||||
@@ -5991,70 +5993,9 @@ def delete_loop(loop_id: int):
|
|||||||
|
|
||||||
|
|
||||||
# ── Audio Effects Mapping API ───────────────────────────────────────────────
|
# ── Audio Effects Mapping API ───────────────────────────────────────────────
|
||||||
|
# Mounted here, where these routes used to be defined: FastAPI matches in
|
||||||
def _audio_effects_error(exc: Exception):
|
# registration order, so the mount site preserves it.
|
||||||
return JSONResponse({"error": str(exc)}, status_code=400)
|
app.include_router(audio_effects.router)
|
||||||
|
|
||||||
|
|
||||||
@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}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Settings API ──────────────────────────────────────────────────────────────
|
# ── Settings API ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user