Add env() config binding with teleport() for CLI-to-worker config passing
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""FastAPI Vue integration - serve Vue frontend from FastAPI."""
|
||||
|
||||
from .environ import env
|
||||
from .environ import env, teleport
|
||||
from .staticfiles import Frontend
|
||||
|
||||
__all__ = ["Frontend", "env"]
|
||||
__all__ = ["Frontend", "env", "teleport"]
|
||||
|
||||
@@ -4,12 +4,48 @@ The project entry point (generated __main__.py) sets FASTAPI_VUE to the
|
||||
project-specific prefix (e.g. "MY_APP"). Project settings are then passed
|
||||
as "<PREFIX>_*" environment variables; this module is the single place
|
||||
that resolves those names.
|
||||
|
||||
Call env with a dataclass or msgspec.Struct type to bind an object of
|
||||
that type, e.g. env(Config). It is decoded from "<PREFIX>_<CLASS NAME>"
|
||||
when set (in spawned server processes) and default-constructed otherwise
|
||||
(in the CLI entry point, which mutates it before server.run() calls
|
||||
teleport()).
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any, TypeVar
|
||||
|
||||
PREFIX_VARIABLE = "FASTAPI_VUE"
|
||||
|
||||
# Names already used by fastapi-vue itself; bindings may not take them
|
||||
RESERVED_NAMES = frozenset({"DEV", "VITE_URL", "BACKEND_URL"})
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _mangle(name: str) -> str:
|
||||
"""Class name to env name: underscores normalized, uppercased."""
|
||||
return re.sub(r"_+", "_", name).strip("_").upper()
|
||||
|
||||
|
||||
def _encode(obj: Any) -> str: # noqa: ANN401
|
||||
if hasattr(type(obj), "__struct_fields__"):
|
||||
import msgspec # noqa: PLC0415
|
||||
|
||||
return msgspec.json.encode(obj).decode()
|
||||
return json.dumps(dataclasses.asdict(obj))
|
||||
|
||||
|
||||
def _decode(raw: str, type_: type[T]) -> T:
|
||||
if hasattr(type_, "__struct_fields__"):
|
||||
import msgspec # noqa: PLC0415
|
||||
|
||||
return msgspec.json.decode(raw, type=type_)
|
||||
return type_(**json.loads(raw))
|
||||
|
||||
|
||||
class _Env:
|
||||
"""Lazy accessors for the project's "<PREFIX>_*" environment variables.
|
||||
@@ -18,6 +54,37 @@ class _Env:
|
||||
or the variable itself is not set.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._bindings: dict[str, tuple[type, Any]] = {}
|
||||
|
||||
def __call__(self, type_: type[T], *, name: str | None = None) -> T:
|
||||
"""Bind and return an object of the given type.
|
||||
|
||||
The type must be a dataclass or msgspec.Struct with defaults for
|
||||
all fields. Decoded from the "<PREFIX>_<NAME>" variable when set,
|
||||
default-constructed otherwise. The variable name is derived from
|
||||
the class name unless overridden with name=. Repeated calls with
|
||||
the same type return the same object; conflicting names raise
|
||||
KeyError.
|
||||
"""
|
||||
var = name if name is not None else _mangle(type_.__name__)
|
||||
if var in RESERVED_NAMES:
|
||||
msg = f"{var} is reserved for fastapi-vue itself"
|
||||
raise KeyError(msg)
|
||||
if bound := self._bindings.get(var):
|
||||
bound_type, obj = bound
|
||||
if bound_type is not type_:
|
||||
msg = f"{var} is already bound to {bound_type}"
|
||||
raise KeyError(msg)
|
||||
return obj
|
||||
if not (hasattr(type_, "__struct_fields__") or dataclasses.is_dataclass(type_)):
|
||||
msg = f"{type_} must be a dataclass or msgspec.Struct"
|
||||
raise TypeError(msg)
|
||||
raw = self._get(var)
|
||||
obj = _decode(raw, type_) if raw else type_()
|
||||
self._bindings[var] = (type_, obj)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def prefix(self) -> str | None:
|
||||
"""Return the project prefix from the FASTAPI_VUE environment variable."""
|
||||
@@ -44,3 +111,19 @@ class _Env:
|
||||
|
||||
|
||||
env = _Env()
|
||||
|
||||
|
||||
def teleport() -> None:
|
||||
"""Serialize objects bound via env() into environment variables.
|
||||
|
||||
Called by server.run() before spawning workers, so mutations made in
|
||||
the CLI entry point propagate to them. Call directly only when spawning
|
||||
server processes by other means.
|
||||
"""
|
||||
if not env._bindings: # noqa: SLF001
|
||||
return
|
||||
if not (prefix := env.prefix):
|
||||
msg = f"{PREFIX_VARIABLE} is not set; cannot teleport bound objects"
|
||||
raise RuntimeError(msg)
|
||||
for var, (_, obj) in env._bindings.items(): # noqa: SLF001
|
||||
os.environ[f"{prefix}_{var}"] = _encode(obj)
|
||||
|
||||
@@ -15,7 +15,7 @@ from uvicorn import Config, Server
|
||||
from uvicorn.main import STARTUP_FAILURE
|
||||
from uvicorn.supervisors import ChangeReload, Multiprocess
|
||||
|
||||
from .environ import env
|
||||
from .environ import env, teleport
|
||||
from .hostutil import parse_endpoints
|
||||
from .logging import (
|
||||
install_access_log,
|
||||
@@ -148,6 +148,8 @@ def run( # noqa: PLR0913
|
||||
msg = "No endpoints to serve; check listen configuration"
|
||||
raise ValueError(msg)
|
||||
|
||||
teleport() # Serialize bound objects before spawning workers
|
||||
|
||||
if startup_box:
|
||||
print_startup_box(startup_box, app, endpoints)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user