In our last tutorial with
httpx.AsyncClient we left off at dependency lifecycles.
As you may have noticed, we are creating and tearing down the
httpx.AsyncClient instance for each incoming request.
This is very inefficient!
Really what we want to do is create the
httpx.AsyncClient once, when our application starts up, and then use the same instance for each request, only tearing down the client when our app shuts down.
To achieve this, we need to introduce scopes. Scopes let you control the "lifetime" of your dependency and are inspired by pytest's fixture system. In Pytest you may have used scopes like "session", "module" or "function". In Xpresso there are three scopes available:
"endpoint": the dependency is created right before calling the endpoint function and torn down right after your function returns, but before the response is sent to the client.
"connection"(default): this scope is entered before the endpoint scope and before calling your endpoint function and is torn down right after the response is sent to the client.
"app": the outermost scope. Dependencies in this scope are tied to the [lifespan] of the application.
So for our use case, we'll be wanting to use the
"app" scope for
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(), scope="app"), ] 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, scope="app") ] 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, ) ] )
Everything else can stay the same, this is all we need!
You may notice we also had to change the
HttpBinConfig marker to the
Just like in Pytest, where a
"session" scoped fixture can't depend on a
"function" scoped fixture, in Xpresso an
"app" scoped fixture can't depend on an
"endpoint" scoped fixture, so we are forced to make
"app" scoped fixture.
If you run this and navigate to http://127.0.0.1:8000/echo/url the response will be the same, but you will probably notice reduced latency if you refresh to make several requests.