server
Runs the patient-data API via uvicorn in a dedicated daemon thread.
Mirrors the threading model of _PodControlServer: the ASGI server runs on
its own event loop in a separate thread so it does not interfere with the
pod's main event loop.
Classes
PatientAPIServer
class PatientAPIServer( repository: PatientDataRepository, port: int, verifier: JWTVerifier, host: str | None = None, ssl_certfile: Path | None = None, ssl_keyfile: Path | None = None, allowed_origins: list[str] | None = None,):Hosts the patient-data API in a background daemon thread.
Arguments
repository: The data-access implementation to serve.port: The TCP port to listen on.verifier: The JWT verifier used to authenticate requests.host: The interface to bind to. Defaults to loopback, unlessssl_certfile/ssl_keyfileare provided, in which case it defaults to0.0.0.0so that it's available from the network.ssl_certfile: Path to a PEM file containing the leaf certificate (optionally followed by the CA bundle) to serve over HTTPS. Must be provided together withssl_keyfile.ssl_keyfile: Path to the PEM private key matchingssl_certfile.allowed_origins: Browser origins permitted to read the API cross-origin via CORS. Empty by default (no CORS headers).
Initialise the server with the repository, port, verifier, and host.
Methods
start
def start(self, thread_name: str = 'patient_api_server', daemon: bool = True) ‑> bool:Start serving in a background thread.
uvicorn.Server.run() binds the socket (and, for HTTPS, loads the
certificate/key) as the first step of its own async setup, inside
the background thread; only once that has succeeded does it flip
Server.started to True. A failure there (port still held by a
not-yet-exited previous instance, unreadable/invalid cert or key)
doesn't raise back into this method - uvicorn either calls
sys.exit(1), which is swallowed at the thread boundary, or the
thread just dies with an uncaught exception - so the only way for
the caller to distinguish "started" from "failed" is to wait for
Server.started or for the thread to die. Callers rely on this to
know whether it's safe to treat the server as actually serving (see
Pod._run_patient_api_server, which only records the certificate as
picked up once this returns True).
Arguments
thread_name: Name for the server thread.daemon: Whether the thread is a daemon thread.
Returns
True if startup was confirmed within _STARTUP_CONFIRM_TIMEOUT
seconds, False otherwise.
stop
def stop(self) ‑> None:Signal graceful shutdown and wait for the thread to exit.