jmri_web
jmri-web — Stage 5 of the JMRI Python rewrite.
A server-rendered web UI built on htmx + Jinja2 + FastAPI. Mounts on top
of jmri-server so a single process serves both the JSON wire API and
the human-facing pages, sharing one in-memory InternalConnection (or
a driver-backed one, once Stage 5.5 wires connection selection).
1"""jmri-web — Stage 5 of the JMRI Python rewrite. 2 3A server-rendered web UI built on htmx + Jinja2 + FastAPI. Mounts on top 4of ``jmri-server`` so a single process serves both the JSON wire API and 5the human-facing pages, sharing one in-memory ``InternalConnection`` (or 6a driver-backed one, once Stage 5.5 wires connection selection). 7""" 8from jmri_web.app import build_web_app, create_app 9 10__version__ = "0.1.0" 11 12__all__ = ["__version__", "build_web_app", "create_app"]
__version__ =
'0.1.0'
def
build_web_app( *, metadata: jmri_server.context.ServerMetadata | None = None) -> jmri_web.app.WebHandle:
33def build_web_app(*, metadata: ServerMetadata | None = None) -> WebHandle: 34 server = build_server_app(metadata=metadata) 35 app = server.app 36 37 # Static assets + Jinja2 templates. 38 if STATIC_DIR.is_dir(): 39 app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") 40 # Generated pdoc reference lives at ``static/docs/``. Mounting 41 # it directly at ``/docs/`` gives a cleaner URL for the "read 42 # the docs" links in topic-help panels + the OVERVIEW page. 43 # When docs haven't been built yet the directory is missing; 44 # skip the mount silently so ``build_docs.py`` remains an 45 # optional developer step, not a hard startup requirement. 46 docs_dir = STATIC_DIR / "docs" 47 if docs_dir.is_dir(): 48 app.mount( 49 "/docs", 50 StaticFiles(directory=str(docs_dir), html=True), 51 name="docs", 52 ) 53 templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) 54 55 install_template_routes(app, server, templates) 56 57 # Auto-load the decoder catalog when ``JMRI_DECODER_XML_ROOT`` is 58 # set. Chained onto the server's existing lifespan so it runs 59 # AFTER server startup (config store, hardware connection, etc. 60 # are all up) but before any user request lands. The load itself 61 # runs in a background thread so startup stays fast — pages that 62 # check ``len(catalog) > 0`` see an empty catalog until the scan 63 # completes (same as the "user hasn't clicked Load yet" state). 64 # 65 # FastAPI removed ``add_event_handler`` in newer releases; the 66 # lifespan-chaining pattern is the supported replacement. 67 from jmri_web.catalog_autoload import schedule_autoload # noqa: PLC0415 68 69 _server_lifespan = app.router.lifespan_context 70 71 @asynccontextmanager 72 async def _web_lifespan(_app): # type: ignore[no-untyped-def] 73 async with _server_lifespan(_app): 74 schedule_autoload(server) 75 yield 76 77 app.router.lifespan_context = _web_lifespan 78 79 return WebHandle(app=app, server=server, templates=templates)
def
create_app() -> fastapi.applications.FastAPI:
82def create_app() -> FastAPI: 83 """Default ASGI entry point. ``uvicorn jmri_web:create_app --factory``. 84 85 Production wiring: builds the full web + server stack and attaches 86 the multi-profile registry + ``ConfigStore`` (reading 87 ``<settings>/profiles.xml``, honouring ``JMRI_SETTINGS_DIR``). 88 Tests that go through :func:`build_web_app` directly skip this for 89 hermeticity. 90 """ 91 from jmri_core.persistence import default_settings_dir # noqa: PLC0415 92 from jmri_server.app import ( # noqa: PLC0415 93 attach_auth, 94 attach_profile_persistence, 95 attach_withrottle_server, 96 ) 97 handle = build_web_app() 98 attach_profile_persistence(handle.server) 99 attach_withrottle_server(handle.server) 100 # Auth is opt-in via ``JMRI_AUTH_ENABLED=1`` — the attach is a 101 # no-op when the env var isn't set, so this line is safe to 102 # keep unconditionally. 103 attach_auth(handle.server, default_settings_dir()) 104 return handle.app
Default ASGI entry point. uvicorn jmri_web:create_app --factory.
Production wiring: builds the full web + server stack and attaches
the multi-profile registry + ConfigStore (reading
<settings>/profiles.xml, honouring JMRI_SETTINGS_DIR).
Tests that go through build_web_app() directly skip this for
hermeticity.