mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-20 03:41:28 +00:00
The packaged desktop app died at startup:
File ".../Resources/slopsmith/server.py", line 71, in <module>
import appstate
ModuleNotFoundError: No module named 'appstate'
feedback-desktop's scripts/bundle-slopsmith.sh copies a HARDCODED list of core
files into the bundle -- server.py, VERSION, lib/, data/, static/,
plugins/__init__.py. The root-level appstate.py (#833) and routers/ (#834)
shipped fine in Docker, passed every test, and were silently dropped from the
packaged app.
Both now live under lib/, the one core directory every packaging path already
copies wholesale -- Dockerfile `COPY lib/`, docker-compose.yml, and the desktop
bundler's `cp -r lib` -- and that all three put on sys.path (on Windows via the
embeddable-Python ._pth, where PYTHONPATH is ignored: build-windows.sh writes
`../slopsmith` and `../slopsmith/lib`). No feedback-desktop change and no new
release are needed for this to take effect.
lib/ is also the CORRECT home under Principle V, and always was once the design
settled: with the injection seam, appstate.py constructs nothing and does no
import-time IO, and a route module only builds an APIRouter. The premise that
forced root placement -- "appstate opens sqlite at import" -- stopped being true
when configure() replaced ownership. The Dockerfile / .dockerignore /
docker-compose.yml entries added for the root layout are reverted; nothing else
in core changes (git mv, so --follow survives).
tests/test_packaging.py is the guard: it walks server.py's module-level imports,
keeps the ones resolving inside this repo, and fails if any lives outside a
directory the packagers copy -- with the ModuleNotFoundError spelled out. So the
next root-level core module can't ship broken. Negative-checked: restoring
appstate.py to the root fails it; the message names the file and the four
packaging files a root module would have to teach.
(It also has to skip `origin in {"built-in","frozen"}` -- on 3.14 the frozen
stdlib reports origin="frozen", and Path("frozen").resolve() lands inside the
repo, which flagged `os` and `stat` as first-party.)
Verified: the bundler's copy replicated exactly into a temp dir and booted with
PYTHONPATH=<bundle>:<bundle>/lib -- `import server`, `import appstate`,
`import routers.audio_effects` all resolve, appstate.meta_db is server.meta_db,
143 routes. The same simulation against origin/main reproduces the production
ModuleNotFoundError. pytest 2398 passed (2348 + 50 new); route table still
identical to origin/main (paths, methods, order); docker build context reaches
lib/appstate.py and lib/routers/ with no __pycache__; native uvicorn boot smoke
serves /api/version, /api/library, the moved audio-effects router, and all three
migrated plugins' src/ graphs; eslint 0 errors.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""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}
|