Up until now we've only seen dependencies that return a value directly. But often you'll want to do some work (like creating a database connection), yield that thing (the connection object) and then do some more work to teardown that thing (for example closing the connection).
Xpresso lets you declare this type of execution using context manager dependencies.
These are dependencies that use the yield keyword once to give back control and then wait until they get back control to run their teardown.
We can apply this concept to our
httpx.AsyncClient example to clean up the client after we are done using it.
All we have to do is change our function to be a context manager like function (an async one in this case) and then use
httpx.AsyncClient's context manager within the function:
from typing import AsyncGenerator import httpx from pydantic import BaseSettings from xpresso import App, Dependant, Path from xpresso.typing import Annotated class HttpBinConfigModel(BaseSettings): url: str = "https://httpbin.org" class Config(BaseSettings.Config): env_prefix = "HTTPBIN_" HttpBinConfig = Annotated[ HttpBinConfigModel, Dependant(lambda: HttpBinConfigModel()) ] async def get_client( config: HttpBinConfig, ) -> AsyncGenerator[httpx.AsyncClient, None]: async with httpx.AsyncClient(base_url=config.url) as client: yield client HttpbinClient = Annotated[httpx.AsyncClient, Dependant(get_client)] async def echo_url(client: HttpbinClient) -> str: resp = await client.get("/get") resp.raise_for_status() # or some other error handling return resp.json()["url"] app = App( routes=[ Path( "/echo/url", get=echo_url, ) ] )
Did you notice that we also converted
get_client() from a
def function to an
async def function?
Making changes like this is super easy using Xpresso's dependency injection system!
It decouples you from execution so that you can mix and match sync and async dependencies without worrying about
awaiting from a sync dependency and other complexities of cooperative concurrency.
It is always best to use
httpx.AsyncClient as a context manager to ensure that connections get cleaned up.
Otherwise, httpx will give you a warning which you'd see in your logs.
Once again, nothing will change from the application user's perspective, but our backend is now a lot more resilient!
The order of execution here is
get_client() -> echo_headers() -> get_client() and is roughly equivalent to:
async with asynccontextmanager(get_client(HttpBinConfigModel())) as client: await echo_headers(client)