Use fastapi-vue RuntimeConfig passing, simplifying code.

This commit is contained in:
2026-09-18 19:00:13 +00:00
parent 3e4f77ba93
commit 4de164c457
4 changed files with 43 additions and 34 deletions
+16 -24
View File
@@ -190,18 +190,6 @@ def cmd_migrate(args: argparse.Namespace) -> None:
print(f"{action} {db_file_path()} (domains: {', '.join(rp_ids)})") print(f"{action} {db_file_path()} (domains: {', '.join(rp_ids)})")
def _save_listen(db_path: Path, listen: list[str] | None) -> None:
"""Persist the listen endpoints to the stored configuration."""
kanta = Kanta(str(db_path), DB())
async def _write() -> None:
async with kanta:
with kanta.transaction("serve:save_listen"):
kanta.data.config.listen = listen
asyncio.run(_write())
def cmd_serve(args: argparse.Namespace) -> None: def cmd_serve(args: argparse.Namespace) -> None:
"""Open the combined database and serve all configured domains.""" """Open the combined database and serve all configured domains."""
db_path = db_file_path() db_path = db_file_path()
@@ -214,14 +202,20 @@ def cmd_serve(args: argparse.Namespace) -> None:
) )
raise SystemExit(f"Database {db_path} not found — run 'paskia init' first.") raise SystemExit(f"Database {db_path} not found — run 'paskia init' first.")
if args.save and args.listen is not None:
# '--listen ""' clears the stored endpoints (back to the default)
_save_listen(db_path, _split_multi(args.listen) or None)
config = _load_stored_config(db_path) config = _load_stored_config(db_path)
listen = _split_multi(args.listen) or config.listen # Effective serve parameters, teleported to the server process(es); the
configure_domains(listen=listen) # app persists the listen endpoints to the database when save is set.
cfg = serve_config()
cfg.save = bool(args.save and args.listen is not None)
if cfg.save:
# '--listen ""' clears the stored endpoints (back to the default)
cfg.listen = _split_multi(args.listen) or None
else:
cfg.listen = _split_multi(args.listen) or config.listen
teleport() # Serialize bound config before spawning workers
configure_domains(listen=cfg.listen)
try: try:
registry = build_registry(config) registry = build_registry(config)
except ValueError as e: except ValueError as e:
@@ -229,18 +223,16 @@ def cmd_serve(args: argparse.Namespace) -> None:
# Sanitization warnings (serving is best-effort; fixing the stored config # Sanitization warnings (serving is best-effort; fixing the stored config
# is the admin's job via the admin interface) are logged by build(). # is the admin's job via the admin interface) are logged by build().
# Pass process-global serve parameters to the server process(es) startupbox.print_startup_config(
serve_config().listen = listen registry, listen=cfg.listen, default_port=DEFAULT_PORT
teleport() # Serialize bound config before spawning workers )
startupbox.print_startup_config(registry, listen=listen, default_port=DEFAULT_PORT)
# Run the server (spawns processes in dev mode) # Run the server (spawns processes in dev mode)
# tracerite, access logging and log config are handled by fastapi_vue.server; # tracerite, access logging and log config are handled by fastapi_vue.server;
# we print our own startup config box, so disable the built-in one. # we print our own startup config box, so disable the built-in one.
server.run( server.run(
"paskia.fastapi.mainapp:app", "paskia.fastapi.mainapp:app",
listen=listen, listen=cfg.listen,
default_port=DEFAULT_PORT, default_port=DEFAULT_PORT,
server_header=False, server_header=False,
startup_box=None, startup_box=None,
+9 -4
View File
@@ -29,10 +29,12 @@ _EXAMPLES_DIR = Path(__file__).parent.parent.parent / "examples"
async def lifespan(app: FastAPI): # pragma: no cover - startup path async def lifespan(app: FastAPI): # pragma: no cover - startup path
"""Application lifespan: open the combined database and build the domain registry. """Application lifespan: open the combined database and build the domain registry.
Process-global serve parameters (listen endpoints) are passed via the Process-global serve parameters (listen endpoints, save flag) are passed
PASKIA_CONFIG JSON env variable (set by the CLI entrypoint) so that via the PASKIA_CONFIG JSON env variable (set by the CLI entrypoint) so
uvicorn reload / multiprocess workers derive site URLs the same way. that uvicorn reload / multiprocess workers derive site URLs the same
Domain configuration is read from the database. way. With the save flag set, the listen endpoints are persisted here —
the CLI never opens the database read-write. Domain configuration is
read from the database.
""" """
cfg = serve_config() cfg = serve_config()
domains.configure(listen=cfg.listen) domains.configure(listen=cfg.listen)
@@ -41,6 +43,9 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
Path(kanta.filename).parent.mkdir, parents=True, exist_ok=True Path(kanta.filename).parent.mkdir, parents=True, exist_ok=True
) )
async with kanta: async with kanta:
if cfg.save:
with kanta.transaction("serve:save_listen"):
db.data().config.listen = cfg.listen
try: try:
domains.init_registry(db.data().config) domains.init_registry(db.data().config)
await remoteauth.init() await remoteauth.init()
+5 -3
View File
@@ -2,9 +2,10 @@
Domain configuration lives in the database (``Config.domains``); the Domain configuration lives in the database (``Config.domains``); the
``PASKIA_CONFIG`` environment variable only carries the effective listen ``PASKIA_CONFIG`` environment variable only carries the effective listen
endpoints so that child processes (uvicorn reload / workers) derive site endpoints and whether to persist them, so that child processes (uvicorn
URLs the same way the parent did. The CLI entry point mutates the bound reload / workers) derive site URLs the same way the parent did. The CLI
object before ``server.run()`` calls ``teleport()`` to pass it on. entry point mutates the bound object before ``server.run()`` calls
``teleport()`` to pass it on.
""" """
import msgspec import msgspec
@@ -15,6 +16,7 @@ class ServeConfig(msgspec.Struct):
"""Process-global serve parameters.""" """Process-global serve parameters."""
listen: list[str] | None = None listen: list[str] | None = None
save: bool = False # Persist listen to the stored config on startup
def serve_config() -> ServeConfig: def serve_config() -> ServeConfig:
+13 -3
View File
@@ -172,6 +172,7 @@ def test_serve_uses_stored_config(run_cli, tmp_path):
assert calls["listen"] is None # stored listen (None) used assert calls["listen"] is None # stored listen (None) used
serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig) serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
assert serve.listen is None assert serve.listen is None
assert serve.save is False
def test_serve_listen_override_not_persisted(run_cli, tmp_path): def test_serve_listen_override_not_persisted(run_cli, tmp_path):
@@ -181,24 +182,33 @@ def test_serve_listen_override_not_persisted(run_cli, tmp_path):
assert calls["listen"] == ["4403"] assert calls["listen"] == ["4403"]
serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig) serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
assert serve.listen == ["4403"] assert serve.listen == ["4403"]
assert serve.save is False
# Stored config keeps the original listen value # Stored config keeps the original listen value
assert stored_config(tmp_path).listen == ["4402"] assert stored_config(tmp_path).listen == ["4402"]
def test_serve_listen_save_persists(run_cli, tmp_path): def test_serve_listen_save_persists(run_cli, tmp_path):
"""--save teleports the save flag; the app persists, the CLI is read-only."""
run_cli("init", "--listen", "4402") run_cli("init", "--listen", "4402")
calls = run_cli("--listen", "4403", "--save") calls = run_cli("--listen", "4403", "--save")
assert calls["listen"] == ["4403"] assert calls["listen"] == ["4403"]
assert stored_config(tmp_path).listen == ["4403"] serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
assert serve.listen == ["4403"]
assert serve.save is True
# The CLI itself does not write the database
assert stored_config(tmp_path).listen == ["4402"]
def test_serve_listen_save_clear(run_cli, tmp_path): def test_serve_listen_save_clear(run_cli, tmp_path):
"""--listen "" --save clears the stored endpoints (back to default).""" """--listen "" --save teleports a clear (back to default) for the app."""
run_cli("init", "--listen", "4402") run_cli("init", "--listen", "4402")
run_cli("--listen", "", "--save") run_cli("--listen", "", "--save")
assert stored_config(tmp_path).listen is None serve = msgspec.json.decode(os.environ["PASKIA_CONFIG"].encode(), type=ServeConfig)
assert serve.listen is None
assert serve.save is True
assert stored_config(tmp_path).listen == ["4402"]
def test_serve_suggests_migrate_when_legacy_present(run_cli, tmp_path): def test_serve_suggests_migrate_when_legacy_present(run_cli, tmp_path):