Xpresso supports lifespan context managers from Starlette. This is the only way to handle startup/shutdown; there are no startup/shutdown events in Xpresso.
The main difference vs. Starlette is that the lifespan context manager is allowed to depend on
"app" scoped dependencies (see Dependency Scopes), including the
from contextlib import asynccontextmanager from typing import AsyncIterator from pydantic import BaseModel from xpresso import App, Path class AppState(BaseModel): started: bool = False @asynccontextmanager async def lifespan(state: AppState) -> AsyncIterator[None]: state.started = True yield class AppHealth(BaseModel): running: bool async def healthcheck(state: AppState) -> AppHealth: return AppHealth(running=state.started) app = App( lifespan=lifespan, routes=[Path("/health", get=healthcheck)] )
You don't need
request.state in Xpresso.
Instead, you can create you own strongly typed mutable or immutable state object and inject it into your lifespan and/or endpoints like in the example above.
Lifespan dependencies are automatically assigned the
"app" scope, you don't need to explicitly set it.
Routers can also have lifespans, and these lifespans will be executed when the top level
App's lifespan executes:
from contextlib import asynccontextmanager from typing import AsyncIterator, List from xpresso import App, Path, Router from xpresso.routing.mount import Mount class Logger(List[str]): pass @asynccontextmanager async def app_lifespan(logger: Logger) -> AsyncIterator[None]: logger.append("App lifespan") yield @asynccontextmanager async def router_lifespan(logger: Logger) -> AsyncIterator[None]: logger.append("Router lifespan") yield async def get_logs(logger: Logger) -> List[str]: return logger app = App( routes=[ Mount( "", app=Router( routes=[ Path( "/logs", get=get_logs, ) ], lifespan=router_lifespan, ), ) ], lifespan=app_lifespan, )
Only Xpresso Routers and mounted Apps support multiple lifespans.
Lifespans for arbitrary mounted ASGI apps (using
Mount) will not work.