mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
The biggest router yet — 12 playlist routes + custom covers — and the first that needs the config-path seam. server.py: 9,302 -> 9,085 (-217). Three causally-linked pieces, all required for playlists: - `config_dir` joins the appstate seam (the plan always put the path constants there; deferred in S3, needed now). It's env-derived, so the ~49 pop-and-reimport fixtures reconfigure it for free — ZERO setattr retargeting. STATIC_DIR/SLOPPAK_CACHE_DIR (patched via setattr) stay in server.py until a router that reads them is extracted, and get retargeted then. - `_clean_str` (pure request-field sanitizer, 14 callers) -> lib/reqfields.py; server.py imports it back. Unblocks wanted/saved/collections/profile/... later. - routers/playlists.py: bodies verbatim, `@app`->`@router`, `meta_db`-> `appstate.meta_db`, `CONFIG_DIR`->`appstate.config_dir`, `_clean_str` from reqfields, `_ART_CACHE_HEADERS` as a local const (art keeps server.py's). The two exclusive cover helpers (_playlist_cover_path/_url) move with it. include_router at the original site; full 143-route table identical to origin/main. One test retarget: test_playlists_api called `server._playlist_cover_path` directly -> now imports it from routers.playlists (reads appstate.config_dir, which the `server` fixture configures). Verified: pyflakes clean; route table identical; pytest 2401 passed (28 in playlists+collections+appstate); packaging guard 51 (auto-picked up reqfields); eslint 0; boot smoke drives create/rename/add-song/cover-upload/serve/delete — the cover writes 1.png under CONFIG_DIR THROUGH appstate.config_dir and serves 200 with an mtime cache-bust token; a wrong-typed name field still 400s via _clean_str; demo untouched. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
4.0 KiB
Python
88 lines
4.0 KiB
Python
"""Shared application state — the seam that lets route modules reach core
|
|
singletons without importing ``server``.
|
|
|
|
``server.py`` is the host: it owns the FastAPI ``app``, constructs the DB
|
|
singletons, and runs the lifecycle. As routes move out into ``routers/`` (R3),
|
|
those modules need ``meta_db`` and friends — but they must not ``import
|
|
server``, or the import graph goes circular the moment ``server`` imports them
|
|
back.
|
|
|
|
So ``server`` **injects** its singletons here once, at the point it builds them::
|
|
|
|
# server.py
|
|
meta_db = MetadataDB(CONFIG_DIR)
|
|
appstate.configure(meta_db=meta_db, ...)
|
|
|
|
and a router reads them back as **module attributes, at call time**::
|
|
|
|
# routers/artists.py
|
|
import appstate
|
|
|
|
@router.get("/api/artist/{name}/page")
|
|
def artist_page(name):
|
|
return appstate.meta_db.artist_page(name)
|
|
|
|
This is the Python analogue of the injected `configureX({...})` seams the
|
|
frontend refactor uses (stems' ``configureStreaming``, studio's
|
|
``configureAudioGraph``, the editor's ``src/host.js``), and of the plugin
|
|
``setup(app, context)`` contract in Principle III: dependencies flow one way,
|
|
``server -> routers -> appstate``, and nothing imports back up.
|
|
|
|
Two properties this shape buys, both load-bearing:
|
|
|
|
* **``import appstate`` performs no IO and constructs nothing.** ``server``
|
|
still owns construction, so the ~49 test fixtures that do
|
|
``sys.modules.pop("server")`` + re-import (to rebuild ``meta_db`` under a
|
|
patched ``CONFIG_DIR``) keep working untouched — a singleton *owned* here
|
|
would survive that pop and go stale.
|
|
* **Reads are late-bound.** Routers must use ``appstate.meta_db``, never
|
|
``from appstate import meta_db`` — a ``from`` import freezes the binding at
|
|
its current value, so a later ``configure()`` (or a
|
|
``monkeypatch.setattr(appstate, "meta_db", fake)``) would not reach the
|
|
router. This is the same read-only-binding trap as ES ``import``.
|
|
|
|
Defaults are ``None`` on purpose: they are inert but *type-honest*, so a router
|
|
that runs before ``configure()`` fails loudly on ``NoneType`` instead of
|
|
quietly operating on a stand-in.
|
|
|
|
Slots are added here only when a router actually needs one — this is a seam,
|
|
not a grab-bag for everything in ``server.py``.
|
|
|
|
**Why this lives in ``lib/`` and not the repo root.** Because it constructs
|
|
nothing and does no import-time IO, it satisfies Principle V's rule for ``lib/``
|
|
modules — and ``lib/`` is the only core directory every packaging path already
|
|
copies: the Dockerfile (``COPY lib/``), ``docker-compose.yml``, and
|
|
feedback-desktop's ``bundle-slopsmith.sh`` (``cp -r lib``). All three also put
|
|
both the bundle root and ``lib/`` on ``sys.path``. A root-level module ships in
|
|
Docker but is silently dropped from the packaged desktop app, whose bundler
|
|
copies a hardcoded file list — that regression is what moved this file here.
|
|
"""
|
|
|
|
# The singletons routers may read. Every name here must also be a `_SLOTS` key.
|
|
meta_db = None
|
|
audio_effect_mappings = None
|
|
|
|
# Config paths. server.py derives these from the environment (fresh on every
|
|
# import, so the ~49 pop-and-reimport fixtures keep working) and injects them
|
|
# here. Routers read them as `appstate.config_dir` etc. — a module attribute at
|
|
# call time. NOTE: config_dir/dlc_dir are env-derived, so a `setenv`+reimport
|
|
# test reconfigures them for free; STATIC_DIR/SLOPPAK_CACHE_DIR are patched via
|
|
# `setattr(server, …)` in a few tests, so those slots (when added) need their
|
|
# tests retargeted to appstate in the same PR.
|
|
config_dir = None
|
|
|
|
_SLOTS = frozenset({"meta_db", "audio_effect_mappings", "config_dir"})
|
|
|
|
|
|
def configure(**kwargs) -> None:
|
|
"""Publish `server`'s singletons into this module. Called once per
|
|
`server` import (and again on re-import), so it must be idempotent."""
|
|
unknown = set(kwargs) - _SLOTS
|
|
if unknown:
|
|
raise TypeError(
|
|
f"appstate.configure() got unknown slot(s): {sorted(unknown)}. "
|
|
f"Known slots: {sorted(_SLOTS)}. Add the name to _SLOTS if a router "
|
|
f"genuinely needs it."
|
|
)
|
|
globals().update(kwargs)
|