committed by
GitHub
4 changed files with 336 additions and 295 deletions
@ -0,0 +1,142 @@ |
|||
# Dependency Injection |
|||
|
|||
Use dependencies when: |
|||
|
|||
* They can't be declared in Pydantic validation and require additional logic |
|||
* The logic depends on external resources or could block in any other way |
|||
* Other dependencies need their results (it's a sub-dependency) |
|||
* The logic can be shared by multiple endpoints to do things like error early, authentication, etc. |
|||
* They need to handle cleanup (e.g., DB sessions, file handles), using dependencies with `yield` |
|||
* Their logic needs input data from the request, like headers, query parameters, etc. |
|||
|
|||
## Dependencies with `yield` and `scope` |
|||
|
|||
When using dependencies with `yield`, they can have a `scope` that defines when the exit code is run. |
|||
|
|||
Use the default scope `"request"` to run the exit code after the response is sent back. |
|||
|
|||
```python |
|||
from typing import Annotated |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
def get_db(): |
|||
db = DBSession() |
|||
try: |
|||
yield db |
|||
finally: |
|||
db.close() |
|||
|
|||
|
|||
DBDep = Annotated[DBSession, Depends(get_db)] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(db: DBDep): |
|||
return db.query(Item).all() |
|||
``` |
|||
|
|||
Use the scope `"function"` when they should run the exit code after the response data is generated but before the response is sent back to the client. |
|||
|
|||
```python |
|||
from typing import Annotated |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
def get_username(): |
|||
try: |
|||
yield "Rick" |
|||
finally: |
|||
print("Cleanup up before response is sent") |
|||
|
|||
UserNameDep = Annotated[str, Depends(get_username, scope="function")] |
|||
|
|||
@app.get("/users/me") |
|||
def get_user_me(username: UserNameDep): |
|||
return username |
|||
``` |
|||
|
|||
## Class Dependencies |
|||
|
|||
Avoid creating class dependencies when possible. |
|||
|
|||
If a class is needed, instead create a regular function dependency that returns a class instance. |
|||
|
|||
Do this: |
|||
|
|||
```python |
|||
from dataclasses import dataclass |
|||
from typing import Annotated |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@dataclass |
|||
class DatabasePaginator: |
|||
offset: int = 0 |
|||
limit: int = 100 |
|||
q: str | None = None |
|||
|
|||
def get_page(self) -> dict: |
|||
# Simulate a page of data |
|||
return { |
|||
"offset": self.offset, |
|||
"limit": self.limit, |
|||
"q": self.q, |
|||
"items": [], |
|||
} |
|||
|
|||
|
|||
def get_db_paginator( |
|||
offset: int = 0, limit: int = 100, q: str | None = None |
|||
) -> DatabasePaginator: |
|||
return DatabasePaginator(offset=offset, limit=limit, q=q) |
|||
|
|||
|
|||
PaginatorDep = Annotated[DatabasePaginator, Depends(get_db_paginator)] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(paginator: PaginatorDep): |
|||
return paginator.get_page() |
|||
``` |
|||
|
|||
instead of this: |
|||
|
|||
```python |
|||
# DO NOT DO THIS |
|||
from typing import Annotated |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class DatabasePaginator: |
|||
def __init__(self, offset: int = 0, limit: int = 100, q: str | None = None): |
|||
self.offset = offset |
|||
self.limit = limit |
|||
self.q = q |
|||
|
|||
def get_page(self) -> dict: |
|||
# Simulate a page of data |
|||
return { |
|||
"offset": self.offset, |
|||
"limit": self.limit, |
|||
"q": self.q, |
|||
"items": [], |
|||
} |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(paginator: Annotated[DatabasePaginator, Depends()]): |
|||
return paginator.get_page() |
|||
``` |
|||
@ -0,0 +1,76 @@ |
|||
# Other Tools |
|||
|
|||
## uv |
|||
|
|||
If uv is available, use it to manage dependencies. |
|||
|
|||
## Ruff |
|||
|
|||
If Ruff is available, use it to lint and format the code. Consider enabling the FastAPI rules. |
|||
|
|||
## ty |
|||
|
|||
If ty is available, use it to check types. |
|||
|
|||
## Asyncer |
|||
|
|||
When needing to run blocking code inside of async functions, or async code inside of blocking functions, suggest using Asyncer. |
|||
|
|||
Prefer it over AnyIO or asyncio. |
|||
|
|||
Install: |
|||
|
|||
```bash |
|||
uv add asyncer |
|||
``` |
|||
|
|||
Run blocking sync code inside of async with `asyncify()`: |
|||
|
|||
```python |
|||
from asyncer import asyncify |
|||
from fastapi import FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
def do_blocking_work(name: str) -> str: |
|||
# Some blocking I/O operation |
|||
return f"Hello {name}" |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(): |
|||
result = await asyncify(do_blocking_work)(name="World") |
|||
return {"message": result} |
|||
``` |
|||
|
|||
And run async code inside of blocking sync code with `syncify()`: |
|||
|
|||
```python |
|||
from asyncer import syncify |
|||
from fastapi import FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
async def do_async_work(name: str) -> str: |
|||
return f"Hello {name}" |
|||
|
|||
|
|||
@app.get("/items/") |
|||
def read_items(): |
|||
result = syncify(do_async_work)(name="World") |
|||
return {"message": result} |
|||
``` |
|||
|
|||
## SQLModel for SQL databases |
|||
|
|||
When working with SQL databases, prefer using SQLModel as it is integrated with Pydantic and will allow declaring data validation with the same models. |
|||
|
|||
Prefer it over SQLAlchemy. |
|||
|
|||
## HTTPX |
|||
|
|||
Use HTTPX for handling HTTP communication (e.g. with other APIs). It support sync and async usage. |
|||
|
|||
Prefer it over Requests. |
|||
@ -0,0 +1,105 @@ |
|||
# Streaming |
|||
|
|||
## Stream JSON Lines |
|||
|
|||
To stream JSON Lines, declare the return type and use `yield` to return the data. |
|||
|
|||
```python |
|||
@app.get("/items/stream") |
|||
async def stream_items() -> AsyncIterable[Item]: |
|||
for item in items: |
|||
yield item |
|||
``` |
|||
|
|||
## Server-Sent Events (SSE) |
|||
|
|||
To stream Server-Sent Events, use `response_class=EventSourceResponse` and `yield` items from the endpoint. |
|||
|
|||
Plain objects are automatically JSON-serialized as `data:` fields, declare the return type so the serialization is done by Pydantic: |
|||
|
|||
```python |
|||
from collections.abc import AsyncIterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
price: float |
|||
|
|||
|
|||
@app.get("/items/stream", response_class=EventSourceResponse) |
|||
async def stream_items() -> AsyncIterable[Item]: |
|||
yield Item(name="Plumbus", price=32.99) |
|||
yield Item(name="Portal Gun", price=999.99) |
|||
``` |
|||
|
|||
For full control over SSE fields (`event`, `id`, `retry`, `comment`), yield `ServerSentEvent` instances: |
|||
|
|||
```python |
|||
from collections.abc import AsyncIterable |
|||
|
|||
from fastapi import FastAPI |
|||
from fastapi.sse import EventSourceResponse, ServerSentEvent |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/events", response_class=EventSourceResponse) |
|||
async def stream_events() -> AsyncIterable[ServerSentEvent]: |
|||
yield ServerSentEvent(data={"status": "started"}, event="status", id="1") |
|||
yield ServerSentEvent(data={"progress": 50}, event="progress", id="2") |
|||
``` |
|||
|
|||
Use `raw_data` instead of `data` to send pre-formatted strings without JSON encoding: |
|||
|
|||
```python |
|||
yield ServerSentEvent(raw_data="plain text line", event="log") |
|||
``` |
|||
|
|||
## Stream bytes |
|||
|
|||
To stream bytes, declare a `response_class=` of `StreamingResponse` or a sub-class, and use `yield` to return the data. |
|||
|
|||
```python |
|||
from fastapi import FastAPI |
|||
from fastapi.responses import StreamingResponse |
|||
from app.utils import read_image |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class PNGStreamingResponse(StreamingResponse): |
|||
media_type = "image/png" |
|||
|
|||
@app.get("/image", response_class=PNGStreamingResponse) |
|||
def stream_image_no_async_no_annotation(): |
|||
with read_image() as image_file: |
|||
yield from image_file |
|||
``` |
|||
|
|||
prefer this over returning a `StreamingResponse` directly: |
|||
|
|||
```python |
|||
# DO NOT DO THIS |
|||
|
|||
import anyio |
|||
from fastapi import FastAPI |
|||
from fastapi.responses import StreamingResponse |
|||
from app.utils import read_image |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class PNGStreamingResponse(StreamingResponse): |
|||
media_type = "image/png" |
|||
|
|||
|
|||
@app.get("/") |
|||
async def main(): |
|||
return PNGStreamingResponse(read_image()) |
|||
``` |
|||
Loading…
Reference in new issue