Spaces:
Running
Running
feat: Day 4 — corpus, ingest script, first 10 golden questions
Browse files- 16 curated FastAPI tutorial markdown files (~9,400 words)
with specific numbers for calculator questions (page sizes,
worker formulas, token expiry, CORS max_age, etc.)
- scripts/ingest.py: chunk → embed → store pipeline
207 chunks from 16 files into FAISS + BM25 hybrid store
- 10 golden questions: 7 positive (incl 1 calculator), 3 out-of-scope
- Pin sentence-transformers <5.0.0 for PyTorch 2.2 compat
Day 4 gate PASS: Recall@5 = 1.00 on all positive questions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agent_bench/evaluation/__init__.py +1 -0
- agent_bench/evaluation/datasets/tech_docs_golden.json +92 -0
- data/tech_docs/fastapi_background_tasks.md +103 -0
- data/tech_docs/fastapi_configuration.md +144 -0
- data/tech_docs/fastapi_dependencies.md +116 -0
- data/tech_docs/fastapi_deployment.md +149 -0
- data/tech_docs/fastapi_error_handling.md +161 -0
- data/tech_docs/fastapi_intro.md +71 -0
- data/tech_docs/fastapi_middleware.md +126 -0
- data/tech_docs/fastapi_openapi.md +210 -0
- data/tech_docs/fastapi_pagination.md +169 -0
- data/tech_docs/fastapi_path_params.md +108 -0
- data/tech_docs/fastapi_query_params.md +134 -0
- data/tech_docs/fastapi_request_body.md +145 -0
- data/tech_docs/fastapi_response_model.md +128 -0
- data/tech_docs/fastapi_security.md +155 -0
- data/tech_docs/fastapi_testing.md +153 -0
- data/tech_docs/fastapi_websockets.md +150 -0
- pyproject.toml +1 -1
- scripts/ingest.py +109 -0
agent_bench/evaluation/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Evaluation harness, metrics, and reporting."""
|
agent_bench/evaluation/datasets/tech_docs_golden.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "q001",
|
| 4 |
+
"question": "How do you define a path parameter in FastAPI?",
|
| 5 |
+
"expected_answer_keywords": ["curly braces", "path", "function parameter", "URL"],
|
| 6 |
+
"expected_sources": ["fastapi_path_params.md"],
|
| 7 |
+
"category": "retrieval",
|
| 8 |
+
"difficulty": "easy",
|
| 9 |
+
"requires_calculator": false
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"id": "q002",
|
| 13 |
+
"question": "What is the default page size for pagination in FastAPI and what is the maximum allowed?",
|
| 14 |
+
"expected_answer_keywords": ["20", "100", "default", "maximum"],
|
| 15 |
+
"expected_sources": ["fastapi_pagination.md"],
|
| 16 |
+
"category": "retrieval",
|
| 17 |
+
"difficulty": "easy",
|
| 18 |
+
"requires_calculator": false
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "q003",
|
| 22 |
+
"question": "How does FastAPI handle CORS and what is the default max_age for preflight caching?",
|
| 23 |
+
"expected_answer_keywords": ["CORSMiddleware", "600", "seconds", "preflight"],
|
| 24 |
+
"expected_sources": ["fastapi_middleware.md"],
|
| 25 |
+
"category": "retrieval",
|
| 26 |
+
"difficulty": "easy",
|
| 27 |
+
"requires_calculator": false
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "q004",
|
| 31 |
+
"question": "What algorithm and expiry time does the FastAPI security example use for JWT tokens?",
|
| 32 |
+
"expected_answer_keywords": ["HS256", "30", "minutes"],
|
| 33 |
+
"expected_sources": ["fastapi_security.md"],
|
| 34 |
+
"category": "retrieval",
|
| 35 |
+
"difficulty": "medium",
|
| 36 |
+
"requires_calculator": false
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"id": "q005",
|
| 40 |
+
"question": "What is the recommended formula for calculating the number of Gunicorn workers for a FastAPI deployment?",
|
| 41 |
+
"expected_answer_keywords": ["2", "CPU", "cores", "1"],
|
| 42 |
+
"expected_sources": ["fastapi_deployment.md"],
|
| 43 |
+
"category": "retrieval",
|
| 44 |
+
"difficulty": "medium",
|
| 45 |
+
"requires_calculator": false
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": "q006",
|
| 49 |
+
"question": "How does dependency caching work in FastAPI, and how can you disable it?",
|
| 50 |
+
"expected_answer_keywords": ["cache", "once", "use_cache", "False"],
|
| 51 |
+
"expected_sources": ["fastapi_dependencies.md"],
|
| 52 |
+
"category": "retrieval",
|
| 53 |
+
"difficulty": "medium",
|
| 54 |
+
"requires_calculator": false
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "q007",
|
| 58 |
+
"question": "If a paginated endpoint returns 20 items per page and there are 10,000 items total, how many total pages are there? And if the page size is changed to 30, how many pages would there be?",
|
| 59 |
+
"expected_answer_keywords": ["500", "334", "ceil", "pages"],
|
| 60 |
+
"expected_sources": ["fastapi_pagination.md"],
|
| 61 |
+
"category": "calculation",
|
| 62 |
+
"difficulty": "medium",
|
| 63 |
+
"requires_calculator": true
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "q008",
|
| 67 |
+
"question": "Does FastAPI support automatic Kubernetes deployment?",
|
| 68 |
+
"expected_answer_keywords": ["not", "does not contain", "no information"],
|
| 69 |
+
"expected_sources": [],
|
| 70 |
+
"category": "out_of_scope",
|
| 71 |
+
"difficulty": "easy",
|
| 72 |
+
"requires_calculator": false
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": "q009",
|
| 76 |
+
"question": "How does FastAPI integrate with Apache Kafka for event streaming?",
|
| 77 |
+
"expected_answer_keywords": ["not", "does not contain", "no information"],
|
| 78 |
+
"expected_sources": [],
|
| 79 |
+
"category": "out_of_scope",
|
| 80 |
+
"difficulty": "easy",
|
| 81 |
+
"requires_calculator": false
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"id": "q010",
|
| 85 |
+
"question": "Can FastAPI generate GraphQL schemas natively?",
|
| 86 |
+
"expected_answer_keywords": ["not", "does not contain", "no information"],
|
| 87 |
+
"expected_sources": [],
|
| 88 |
+
"category": "out_of_scope",
|
| 89 |
+
"difficulty": "easy",
|
| 90 |
+
"requires_calculator": false
|
| 91 |
+
}
|
| 92 |
+
]
|
data/tech_docs/fastapi_background_tasks.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Background Tasks in FastAPI
|
| 2 |
+
|
| 3 |
+
Background tasks allow you to schedule work to run after the response has been sent to the client. This is useful for operations that do not need to complete before the user receives a response, such as sending emails, writing audit logs, or triggering data processing pipelines.
|
| 4 |
+
|
| 5 |
+
## Basic Background Task
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI, BackgroundTasks
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
|
| 12 |
+
def write_log(message: str):
|
| 13 |
+
with open("log.txt", "a") as f:
|
| 14 |
+
f.write(f"{message}\n")
|
| 15 |
+
|
| 16 |
+
@app.post("/items/", status_code=201)
|
| 17 |
+
async def create_item(name: str, background_tasks: BackgroundTasks):
|
| 18 |
+
background_tasks.add_task(write_log, f"Item created: {name}")
|
| 19 |
+
return {"name": name, "status": "created"}
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
Declare `BackgroundTasks` as a parameter in your route handler, and FastAPI injects it automatically. Call `add_task()` with the function to run and any positional or keyword arguments. The task runs after the response is sent, in the same process. The `add_task()` method accepts both synchronous and asynchronous functions -- sync functions are run in a threadpool, while async functions are awaited on the event loop.
|
| 23 |
+
|
| 24 |
+
## Multiple Background Tasks
|
| 25 |
+
|
| 26 |
+
You can add multiple tasks, and they execute sequentially in the order they were added:
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
def send_email(to: str, subject: str, body: str):
|
| 30 |
+
# Simulate sending email (takes ~2 seconds)
|
| 31 |
+
import time
|
| 32 |
+
time.sleep(2)
|
| 33 |
+
print(f"Email sent to {to}: {subject}")
|
| 34 |
+
|
| 35 |
+
def update_analytics(event: str, item_id: int):
|
| 36 |
+
# Record analytics event
|
| 37 |
+
print(f"Analytics: {event} for item {item_id}")
|
| 38 |
+
|
| 39 |
+
@app.post("/items/{item_id}/purchase")
|
| 40 |
+
async def purchase_item(item_id: int, background_tasks: BackgroundTasks):
|
| 41 |
+
# Process purchase immediately
|
| 42 |
+
result = process_purchase(item_id)
|
| 43 |
+
|
| 44 |
+
# Queue background work
|
| 45 |
+
background_tasks.add_task(
|
| 46 |
+
send_email,
|
| 47 |
+
to="buyer@example.com",
|
| 48 |
+
subject="Purchase Confirmation",
|
| 49 |
+
body=f"You purchased item {item_id}",
|
| 50 |
+
)
|
| 51 |
+
background_tasks.add_task(update_analytics, "purchase", item_id)
|
| 52 |
+
|
| 53 |
+
return {"item_id": item_id, "status": "purchased"}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
In this example, the client receives the response immediately after purchase processing. The email and analytics tasks run sequentially in the background. If the first task takes 2 seconds, the second task starts only after the first completes.
|
| 57 |
+
|
| 58 |
+
## Background Tasks in Dependencies
|
| 59 |
+
|
| 60 |
+
Dependencies can also add background tasks, which is useful for cross-cutting concerns like logging:
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
from fastapi import Depends
|
| 64 |
+
|
| 65 |
+
def log_request(background_tasks: BackgroundTasks):
|
| 66 |
+
def _log(method: str, path: str):
|
| 67 |
+
with open("access.log", "a") as f:
|
| 68 |
+
f.write(f"{method} {path}\n")
|
| 69 |
+
return background_tasks, _log
|
| 70 |
+
|
| 71 |
+
async def audit_dependency(
|
| 72 |
+
background_tasks: BackgroundTasks,
|
| 73 |
+
request_method: str = "GET",
|
| 74 |
+
):
|
| 75 |
+
def audit_log(action: str):
|
| 76 |
+
with open("audit.log", "a") as f:
|
| 77 |
+
f.write(f"[{request_method}] {action}\n")
|
| 78 |
+
background_tasks.add_task(audit_log, "endpoint_accessed")
|
| 79 |
+
|
| 80 |
+
@app.get("/items/", dependencies=[Depends(audit_dependency)])
|
| 81 |
+
async def read_items(background_tasks: BackgroundTasks):
|
| 82 |
+
background_tasks.add_task(write_log, "Items listed")
|
| 83 |
+
return [{"item": "Widget"}]
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
When both the dependency and the route handler add tasks to `BackgroundTasks`, all tasks share the same task queue. Dependency tasks are added first (in the order dependencies are resolved), followed by tasks added in the route handler.
|
| 87 |
+
|
| 88 |
+
## Use Cases and Limitations
|
| 89 |
+
|
| 90 |
+
Common use cases for background tasks:
|
| 91 |
+
|
| 92 |
+
- **Email notifications**: Send confirmation or alert emails after an action (typical send time: 1-5 seconds).
|
| 93 |
+
- **Log writing**: Write detailed audit logs without adding latency to the response.
|
| 94 |
+
- **Cache invalidation**: Clear or update caches after data mutations.
|
| 95 |
+
- **Webhook delivery**: POST event payloads to external services with retry logic.
|
| 96 |
+
- **File cleanup**: Remove temporary uploaded files after processing.
|
| 97 |
+
|
| 98 |
+
Important limitations to consider:
|
| 99 |
+
|
| 100 |
+
1. Background tasks run in the same process as the web server. If a task crashes, it does not affect the already-sent response, but unhandled exceptions are logged to stderr.
|
| 101 |
+
2. If the server shuts down, pending background tasks are lost -- they are not persisted to a queue. For critical tasks, use a dedicated task queue like Celery (which supports up to 10,000+ tasks per second with Redis as a broker) or ARQ.
|
| 102 |
+
3. Background tasks share the event loop (for async tasks) or threadpool (for sync tasks, default pool size of 40 threads). A CPU-intensive background task can degrade request handling performance.
|
| 103 |
+
4. There is no built-in retry mechanism. If a background task fails, it fails silently from the client's perspective. Implement retry logic within the task function if needed.
|
data/tech_docs/fastapi_configuration.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration and Settings in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI leverages Pydantic's `BaseSettings` class to manage application configuration through environment variables, `.env` files, and secrets. This approach provides type-safe configuration with validation, default values, and automatic environment variable reading.
|
| 4 |
+
|
| 5 |
+
## Pydantic Settings
|
| 6 |
+
|
| 7 |
+
Install the settings extension:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install pydantic-settings
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
Define your settings as a Pydantic model:
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 17 |
+
|
| 18 |
+
class Settings(BaseSettings):
|
| 19 |
+
model_config = SettingsConfigDict(
|
| 20 |
+
env_file=".env",
|
| 21 |
+
env_file_encoding="utf-8",
|
| 22 |
+
case_sensitive=False,
|
| 23 |
+
env_prefix="",
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
app_name: str = "My FastAPI App"
|
| 27 |
+
admin_email: str = "admin@example.com"
|
| 28 |
+
debug: bool = False
|
| 29 |
+
database_url: str = "sqlite:///./app.db"
|
| 30 |
+
redis_url: str = "redis://localhost:6379/0"
|
| 31 |
+
allowed_hosts: list[str] = ["localhost", "127.0.0.1"]
|
| 32 |
+
max_connections: int = 100
|
| 33 |
+
api_v1_prefix: str = "/api/v1"
|
| 34 |
+
access_token_expire_minutes: int = 30
|
| 35 |
+
secret_key: str = "change-me-in-production"
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Pydantic Settings reads values from these sources in the following priority order (highest priority first):
|
| 39 |
+
|
| 40 |
+
1. Constructor arguments passed directly to `Settings()`
|
| 41 |
+
2. Environment variables
|
| 42 |
+
3. Variables from the `.env` file
|
| 43 |
+
4. Default values defined in the model
|
| 44 |
+
|
| 45 |
+
Setting `case_sensitive=False` (the default) means the environment variable `DATABASE_URL`, `database_url`, and `Database_Url` all map to the `database_url` field.
|
| 46 |
+
|
| 47 |
+
## Environment Variables and .env Files
|
| 48 |
+
|
| 49 |
+
Create a `.env` file in the project root:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
APP_NAME=Production API
|
| 53 |
+
DEBUG=false
|
| 54 |
+
DATABASE_URL=postgresql://user:pass@db-host:5432/mydb
|
| 55 |
+
REDIS_URL=redis://redis-host:6379/0
|
| 56 |
+
MAX_CONNECTIONS=250
|
| 57 |
+
SECRET_KEY=a7f3b9c1d4e8f2a6b0c5d9e3f7a1b4c8
|
| 58 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
The `.env` file is parsed using the `python-dotenv` library (installed automatically with `pydantic-settings`). Multiple `.env` files can be specified as a tuple:
|
| 62 |
+
|
| 63 |
+
```python
|
| 64 |
+
model_config = SettingsConfigDict(
|
| 65 |
+
env_file=(".env", ".env.local"),
|
| 66 |
+
)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
When multiple files are specified, later files take precedence over earlier ones. So `.env.local` overrides values from `.env`.
|
| 70 |
+
|
| 71 |
+
## Settings as a Dependency
|
| 72 |
+
|
| 73 |
+
Use dependency injection to provide settings to route handlers:
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
from functools import lru_cache
|
| 77 |
+
from fastapi import FastAPI, Depends
|
| 78 |
+
|
| 79 |
+
app = FastAPI()
|
| 80 |
+
|
| 81 |
+
@lru_cache
|
| 82 |
+
def get_settings():
|
| 83 |
+
return Settings()
|
| 84 |
+
|
| 85 |
+
@app.get("/info")
|
| 86 |
+
async def info(settings: Settings = Depends(get_settings)):
|
| 87 |
+
return {
|
| 88 |
+
"app_name": settings.app_name,
|
| 89 |
+
"admin_email": settings.admin_email,
|
| 90 |
+
"debug": settings.debug,
|
| 91 |
+
}
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
The `@lru_cache` decorator ensures the `Settings` object is created only once and reused for all subsequent requests. Without caching, Pydantic would read and parse the `.env` file on every request, adding approximately 1-3 milliseconds of overhead per call. The cache has no size limit by default (`maxsize=128` for `lru_cache`), but since `get_settings()` takes no arguments, it effectively stores just one instance.
|
| 95 |
+
|
| 96 |
+
## Nested Settings with Prefixes
|
| 97 |
+
|
| 98 |
+
Organize related settings into nested models using `env_prefix`:
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 102 |
+
from pydantic import BaseModel
|
| 103 |
+
|
| 104 |
+
class DatabaseSettings(BaseSettings):
|
| 105 |
+
model_config = SettingsConfigDict(env_prefix="DB_")
|
| 106 |
+
|
| 107 |
+
host: str = "localhost"
|
| 108 |
+
port: int = 5432
|
| 109 |
+
name: str = "mydb"
|
| 110 |
+
user: str = "postgres"
|
| 111 |
+
password: str = ""
|
| 112 |
+
pool_min_size: int = 5
|
| 113 |
+
pool_max_size: int = 20
|
| 114 |
+
echo: bool = False
|
| 115 |
+
|
| 116 |
+
class Settings(BaseSettings):
|
| 117 |
+
model_config = SettingsConfigDict(env_file=".env")
|
| 118 |
+
|
| 119 |
+
app_name: str = "My App"
|
| 120 |
+
debug: bool = False
|
| 121 |
+
db: DatabaseSettings = DatabaseSettings()
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
With `env_prefix="DB_"`, the environment variable `DB_HOST` maps to `DatabaseSettings.host`, `DB_PORT` maps to `port`, and so on. The default database pool sizes are 5 minimum and 20 maximum connections.
|
| 125 |
+
|
| 126 |
+
## Secrets Management
|
| 127 |
+
|
| 128 |
+
For sensitive values, Pydantic Settings supports reading from secret files (commonly used with Docker Secrets and Kubernetes Secrets):
|
| 129 |
+
|
| 130 |
+
```python
|
| 131 |
+
class Settings(BaseSettings):
|
| 132 |
+
model_config = SettingsConfigDict(
|
| 133 |
+
env_file=".env",
|
| 134 |
+
secrets_dir="/run/secrets",
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
database_password: str
|
| 138 |
+
api_key: str
|
| 139 |
+
jwt_secret: str
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
When `secrets_dir` is set, Pydantic looks for files named after each field (e.g., `/run/secrets/database_password`). The file contents become the field value. Secret files take precedence over `.env` values but are overridden by environment variables.
|
| 143 |
+
|
| 144 |
+
The priority order with secrets becomes: constructor arguments > environment variables > secret files > `.env` file > default values.
|
data/tech_docs/fastapi_dependencies.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependency Injection in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI includes a built-in dependency injection system that allows you to share logic, enforce authentication, manage database connections, and more. Dependencies are declared using `Depends()` and are resolved automatically for each request.
|
| 4 |
+
|
| 5 |
+
## Basic Dependency
|
| 6 |
+
|
| 7 |
+
A dependency is any callable (function or class) that FastAPI calls before the route handler:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from fastapi import FastAPI, Depends, Query
|
| 11 |
+
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
async def common_parameters(
|
| 15 |
+
skip: int = Query(default=0, ge=0),
|
| 16 |
+
limit: int = Query(default=100, ge=1, le=1000),
|
| 17 |
+
):
|
| 18 |
+
return {"skip": skip, "limit": limit}
|
| 19 |
+
|
| 20 |
+
@app.get("/items/")
|
| 21 |
+
async def read_items(commons: dict = Depends(common_parameters)):
|
| 22 |
+
return {"params": commons}
|
| 23 |
+
|
| 24 |
+
@app.get("/users/")
|
| 25 |
+
async def read_users(commons: dict = Depends(common_parameters)):
|
| 26 |
+
return {"params": commons}
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Both `/items/` and `/users/` share the same pagination logic. The `common_parameters` function is called once per request, and its return value is injected into the `commons` parameter.
|
| 30 |
+
|
| 31 |
+
## Class-Based Dependencies
|
| 32 |
+
|
| 33 |
+
Classes work as dependencies because calling a class creates an instance (i.e., `MyClass()` is callable):
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
class PaginationParams:
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
skip: int = Query(default=0, ge=0),
|
| 40 |
+
limit: int = Query(default=100, ge=1, le=1000),
|
| 41 |
+
):
|
| 42 |
+
self.skip = skip
|
| 43 |
+
self.limit = limit
|
| 44 |
+
|
| 45 |
+
@app.get("/items/")
|
| 46 |
+
async def read_items(pagination: PaginationParams = Depends(PaginationParams)):
|
| 47 |
+
return {"skip": pagination.skip, "limit": pagination.limit}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
FastAPI provides a shorthand: `Depends(PaginationParams)` can also be written as `Depends()` when the type annotation already specifies the class: `pagination: PaginationParams = Depends()`.
|
| 51 |
+
|
| 52 |
+
## Sub-Dependencies
|
| 53 |
+
|
| 54 |
+
Dependencies can depend on other dependencies, forming a chain that FastAPI resolves automatically:
|
| 55 |
+
|
| 56 |
+
```python
|
| 57 |
+
def query_extractor(q: str | None = None):
|
| 58 |
+
return q
|
| 59 |
+
|
| 60 |
+
def query_or_default(q: str = Depends(query_extractor)):
|
| 61 |
+
if not q:
|
| 62 |
+
return "default_query"
|
| 63 |
+
return q
|
| 64 |
+
|
| 65 |
+
@app.get("/items/")
|
| 66 |
+
async def read_items(query: str = Depends(query_or_default)):
|
| 67 |
+
return {"query": query}
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
FastAPI resolves the dependency tree from the leaves up. In this case, `query_extractor` runs first, then `query_or_default` receives its result. The maximum depth of the dependency chain is not explicitly limited, but in practice chains deeper than 10 levels indicate a design issue.
|
| 71 |
+
|
| 72 |
+
## Dependencies with Yield (Resource Management)
|
| 73 |
+
|
| 74 |
+
Use `yield` in a dependency to run setup code before and cleanup code after the route handler executes. This is ideal for managing database sessions, file handles, or locks:
|
| 75 |
+
|
| 76 |
+
```python
|
| 77 |
+
from sqlalchemy.orm import Session
|
| 78 |
+
|
| 79 |
+
def get_db():
|
| 80 |
+
db = SessionLocal()
|
| 81 |
+
try:
|
| 82 |
+
yield db
|
| 83 |
+
finally:
|
| 84 |
+
db.close()
|
| 85 |
+
|
| 86 |
+
@app.get("/items/")
|
| 87 |
+
async def read_items(db: Session = Depends(get_db)):
|
| 88 |
+
items = db.query(Item).all()
|
| 89 |
+
return items
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
The code before `yield` runs before the handler, the yielded value is injected as the dependency, and the code after `yield` runs after the response is sent. The `finally` block ensures cleanup happens even if an exception occurs. FastAPI supports up to 32 yield dependencies per request by default.
|
| 93 |
+
|
| 94 |
+
## Global Dependencies
|
| 95 |
+
|
| 96 |
+
Apply dependencies to every route in the application by passing them to the `FastAPI` constructor:
|
| 97 |
+
|
| 98 |
+
```python
|
| 99 |
+
from fastapi import FastAPI, Depends, Header, HTTPException
|
| 100 |
+
|
| 101 |
+
async def verify_api_key(x_api_key: str = Header()):
|
| 102 |
+
if x_api_key != "secret-key-123":
|
| 103 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 104 |
+
|
| 105 |
+
app = FastAPI(dependencies=[Depends(verify_api_key)])
|
| 106 |
+
|
| 107 |
+
@app.get("/items/")
|
| 108 |
+
async def read_items():
|
| 109 |
+
return [{"item": "Widget"}]
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
Every route in this application requires a valid `X-Api-Key` header. You can also scope dependencies to a specific router using `APIRouter(dependencies=[...])`.
|
| 113 |
+
|
| 114 |
+
## Caching Behavior
|
| 115 |
+
|
| 116 |
+
By default, if the same dependency is used multiple times within a single request (e.g., both a route and a sub-dependency use `Depends(get_db)`), FastAPI caches the result and calls the dependency only once. To disable caching and force a fresh call each time, use `Depends(get_db, use_cache=False)`.
|
data/tech_docs/fastapi_deployment.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying FastAPI Applications
|
| 2 |
+
|
| 3 |
+
FastAPI applications are deployed using ASGI servers. This guide covers production deployment with Uvicorn, Gunicorn, Docker, and related infrastructure considerations.
|
| 4 |
+
|
| 5 |
+
## Uvicorn (Single Process)
|
| 6 |
+
|
| 7 |
+
Uvicorn is the recommended ASGI server for FastAPI. For development:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
uvicorn main:app --reload --host 127.0.0.1 --port 8000
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
For production with a single process:
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 --log-level info
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
Key Uvicorn configuration options:
|
| 20 |
+
|
| 21 |
+
| Flag | Default | Description |
|
| 22 |
+
|-------------------|---------------|------------------------------------------|
|
| 23 |
+
| `--host` | `127.0.0.1` | Bind address |
|
| 24 |
+
| `--port` | `8000` | Bind port |
|
| 25 |
+
| `--workers` | `1` | Number of worker processes |
|
| 26 |
+
| `--loop` | `auto` | Event loop (auto, asyncio, uvloop) |
|
| 27 |
+
| `--http` | `auto` | HTTP protocol (auto, h11, httptools) |
|
| 28 |
+
| `--ws` | `auto` | WebSocket protocol (auto, websockets, wsproto) |
|
| 29 |
+
| `--log-level` | `info` | Logging level (critical, error, warning, info, debug, trace) |
|
| 30 |
+
| `--access-log` | `True` | Enable/disable access log |
|
| 31 |
+
| `--ws-max-size` | `16777216` | Max WebSocket message size (16 MB) |
|
| 32 |
+
| `--timeout-keep-alive` | `5` | Keep-alive timeout in seconds |
|
| 33 |
+
|
| 34 |
+
Using `uvloop` and `httptools` (installed automatically on Linux) provides a 20-30% performance boost over the pure-Python `asyncio` and `h11` alternatives.
|
| 35 |
+
|
| 36 |
+
## Gunicorn with Uvicorn Workers
|
| 37 |
+
|
| 38 |
+
For production deployments requiring multiple worker processes, use Gunicorn as the process manager with Uvicorn workers:
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
gunicorn main:app \
|
| 42 |
+
--workers 4 \
|
| 43 |
+
--worker-class uvicorn.workers.UvicornWorker \
|
| 44 |
+
--bind 0.0.0.0:8000 \
|
| 45 |
+
--timeout 120 \
|
| 46 |
+
--graceful-timeout 30 \
|
| 47 |
+
--keep-alive 5 \
|
| 48 |
+
--max-requests 1000 \
|
| 49 |
+
--max-requests-jitter 50 \
|
| 50 |
+
--access-logfile -
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
The recommended number of workers is `(2 * CPU_CORES) + 1`. For a server with 4 CPU cores, that is 9 workers. The `--max-requests 1000` flag restarts each worker after handling 1,000 requests, preventing memory leaks. The `--max-requests-jitter 50` adds a random offset (0-50) so workers do not all restart simultaneously.
|
| 54 |
+
|
| 55 |
+
The `--timeout 120` flag sets the maximum time (in seconds) a worker can take to handle a request before being killed and restarted. The default is 30 seconds. The `--graceful-timeout 30` gives workers 30 seconds to finish current requests during shutdown.
|
| 56 |
+
|
| 57 |
+
## Docker Deployment
|
| 58 |
+
|
| 59 |
+
A production-ready Dockerfile:
|
| 60 |
+
|
| 61 |
+
```dockerfile
|
| 62 |
+
FROM python:3.12-slim
|
| 63 |
+
|
| 64 |
+
WORKDIR /app
|
| 65 |
+
|
| 66 |
+
# Install dependencies first for layer caching
|
| 67 |
+
COPY requirements.txt .
|
| 68 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 69 |
+
|
| 70 |
+
# Copy application code
|
| 71 |
+
COPY ./app ./app
|
| 72 |
+
|
| 73 |
+
# Create non-root user
|
| 74 |
+
RUN adduser --disabled-password --gecos "" appuser
|
| 75 |
+
USER appuser
|
| 76 |
+
|
| 77 |
+
EXPOSE 8000
|
| 78 |
+
|
| 79 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Build and run:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
docker build -t myapi:latest .
|
| 86 |
+
docker run -d --name myapi -p 8000:8000 -e DATABASE_URL=postgresql://... myapi:latest
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
The `python:3.12-slim` base image is approximately 120 MB, compared to the full `python:3.12` image at approximately 890 MB. For even smaller images, use `python:3.12-alpine` (approximately 50 MB), though it may require additional build dependencies for some Python packages.
|
| 90 |
+
|
| 91 |
+
## Proxy Headers and HTTPS
|
| 92 |
+
|
| 93 |
+
When running behind a reverse proxy (Nginx, Traefik, AWS ALB), configure Uvicorn to trust proxy headers:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
uvicorn main:app \
|
| 97 |
+
--host 0.0.0.0 \
|
| 98 |
+
--port 8000 \
|
| 99 |
+
--proxy-headers \
|
| 100 |
+
--forwarded-allow-ips="*"
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
The `--proxy-headers` flag tells Uvicorn to read `X-Forwarded-For` and `X-Forwarded-Proto` headers from the proxy. The `--forwarded-allow-ips` flag specifies which proxy IPs are trusted. Using `"*"` trusts all proxies (acceptable when the application is not directly exposed to the internet).
|
| 104 |
+
|
| 105 |
+
An Nginx reverse proxy configuration:
|
| 106 |
+
|
| 107 |
+
```nginx
|
| 108 |
+
upstream fastapi_backend {
|
| 109 |
+
server 127.0.0.1:8000;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
server {
|
| 113 |
+
listen 443 ssl;
|
| 114 |
+
server_name api.example.com;
|
| 115 |
+
|
| 116 |
+
ssl_certificate /etc/ssl/certs/api.example.com.pem;
|
| 117 |
+
ssl_certificate_key /etc/ssl/private/api.example.com.key;
|
| 118 |
+
|
| 119 |
+
location / {
|
| 120 |
+
proxy_pass http://fastapi_backend;
|
| 121 |
+
proxy_set_header Host $host;
|
| 122 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 123 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 124 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 125 |
+
proxy_buffering off;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Setting `proxy_buffering off` ensures streamed responses (like SSE or large file downloads) are forwarded immediately rather than buffered by Nginx.
|
| 131 |
+
|
| 132 |
+
## Health Checks
|
| 133 |
+
|
| 134 |
+
Include a health check endpoint for container orchestrators:
|
| 135 |
+
|
| 136 |
+
```python
|
| 137 |
+
@app.get("/health", status_code=200)
|
| 138 |
+
async def health_check():
|
| 139 |
+
return {"status": "healthy"}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Docker health check configuration:
|
| 143 |
+
|
| 144 |
+
```dockerfile
|
| 145 |
+
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=10s \
|
| 146 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
This checks health every 30 seconds, allows 10 seconds per check, retries 3 times before marking unhealthy, and waits 10 seconds after container start before the first check.
|
data/tech_docs/fastapi_error_handling.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Error Handling in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI provides a structured approach to error handling using HTTP exceptions, custom exception handlers, and validation error customization. Proper error handling ensures clients receive meaningful, consistent error responses.
|
| 4 |
+
|
| 5 |
+
## HTTPException
|
| 6 |
+
|
| 7 |
+
The `HTTPException` class is the primary way to return error responses from route handlers:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
items = {"widget": {"name": "Widget", "price": 35.99}}
|
| 15 |
+
|
| 16 |
+
@app.get("/items/{item_id}")
|
| 17 |
+
async def read_item(item_id: str):
|
| 18 |
+
if item_id not in items:
|
| 19 |
+
raise HTTPException(
|
| 20 |
+
status_code=404,
|
| 21 |
+
detail="Item not found",
|
| 22 |
+
headers={"X-Error-Code": "ITEM_NOT_FOUND"},
|
| 23 |
+
)
|
| 24 |
+
return items[item_id]
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
When raised, `HTTPException` immediately terminates request processing and returns the specified status code and detail message. The `detail` parameter can be a string, list, or dictionary -- FastAPI serializes it to JSON automatically. The optional `headers` parameter adds custom HTTP headers to the error response.
|
| 28 |
+
|
| 29 |
+
The default error response format is:
|
| 30 |
+
|
| 31 |
+
```json
|
| 32 |
+
{
|
| 33 |
+
"detail": "Item not found"
|
| 34 |
+
}
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
## Custom Exception Handlers
|
| 38 |
+
|
| 39 |
+
Register custom handlers for any exception type using `@app.exception_handler()`:
|
| 40 |
+
|
| 41 |
+
```python
|
| 42 |
+
from fastapi import FastAPI, Request
|
| 43 |
+
from fastapi.responses import JSONResponse
|
| 44 |
+
|
| 45 |
+
class ItemNotFoundException(Exception):
|
| 46 |
+
def __init__(self, item_id: str):
|
| 47 |
+
self.item_id = item_id
|
| 48 |
+
|
| 49 |
+
app = FastAPI()
|
| 50 |
+
|
| 51 |
+
@app.exception_handler(ItemNotFoundException)
|
| 52 |
+
async def item_not_found_handler(request: Request, exc: ItemNotFoundException):
|
| 53 |
+
return JSONResponse(
|
| 54 |
+
status_code=404,
|
| 55 |
+
content={
|
| 56 |
+
"error": "item_not_found",
|
| 57 |
+
"message": f"Item '{exc.item_id}' does not exist",
|
| 58 |
+
"path": str(request.url),
|
| 59 |
+
},
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
@app.get("/items/{item_id}")
|
| 63 |
+
async def read_item(item_id: str):
|
| 64 |
+
if item_id not in items_db:
|
| 65 |
+
raise ItemNotFoundException(item_id)
|
| 66 |
+
return items_db[item_id]
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Custom exception handlers receive the `Request` object and the exception instance. They must return a `Response` object (typically `JSONResponse`). You can register handlers for any Python exception class, including built-in exceptions like `ValueError` or `RuntimeError`.
|
| 70 |
+
|
| 71 |
+
## Handling Validation Errors
|
| 72 |
+
|
| 73 |
+
FastAPI automatically returns a 422 Unprocessable Entity response when request validation fails. The default response includes detailed error information:
|
| 74 |
+
|
| 75 |
+
```json
|
| 76 |
+
{
|
| 77 |
+
"detail": [
|
| 78 |
+
{
|
| 79 |
+
"type": "int_parsing",
|
| 80 |
+
"loc": ["path", "item_id"],
|
| 81 |
+
"msg": "Input should be a valid integer, unable to parse string as an integer",
|
| 82 |
+
"input": "abc",
|
| 83 |
+
"url": "https://errors.pydantic.dev/2/v/int_parsing"
|
| 84 |
+
}
|
| 85 |
+
]
|
| 86 |
+
}
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Each error object contains 5 fields: `type` (the error type identifier), `loc` (the location as a list like `["body", "price"]` or `["query", "limit"]`), `msg` (a human-readable message), `input` (the invalid value), and `url` (a link to Pydantic's error documentation).
|
| 90 |
+
|
| 91 |
+
To customize validation error responses, override the `RequestValidationError` handler:
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
from fastapi import FastAPI, Request, status
|
| 95 |
+
from fastapi.exceptions import RequestValidationError
|
| 96 |
+
from fastapi.responses import JSONResponse
|
| 97 |
+
|
| 98 |
+
app = FastAPI()
|
| 99 |
+
|
| 100 |
+
@app.exception_handler(RequestValidationError)
|
| 101 |
+
async def validation_exception_handler(
|
| 102 |
+
request: Request, exc: RequestValidationError
|
| 103 |
+
):
|
| 104 |
+
error_messages = []
|
| 105 |
+
for error in exc.errors():
|
| 106 |
+
field = " -> ".join(str(loc) for loc in error["loc"])
|
| 107 |
+
error_messages.append(f"{field}: {error['msg']}")
|
| 108 |
+
|
| 109 |
+
return JSONResponse(
|
| 110 |
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
| 111 |
+
content={
|
| 112 |
+
"error": "validation_error",
|
| 113 |
+
"message": "Request validation failed",
|
| 114 |
+
"details": error_messages,
|
| 115 |
+
"error_count": len(exc.errors()),
|
| 116 |
+
},
|
| 117 |
+
)
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Overriding Default Exception Handlers
|
| 121 |
+
|
| 122 |
+
FastAPI has built-in handlers for `HTTPException` and `RequestValidationError`. You can override both:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
from fastapi import FastAPI
|
| 126 |
+
from fastapi.exceptions import RequestValidationError
|
| 127 |
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
| 128 |
+
|
| 129 |
+
app = FastAPI()
|
| 130 |
+
|
| 131 |
+
@app.exception_handler(StarletteHTTPException)
|
| 132 |
+
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
| 133 |
+
return JSONResponse(
|
| 134 |
+
status_code=exc.status_code,
|
| 135 |
+
content={
|
| 136 |
+
"error": True,
|
| 137 |
+
"status_code": exc.status_code,
|
| 138 |
+
"message": exc.detail,
|
| 139 |
+
},
|
| 140 |
+
)
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Note: FastAPI's `HTTPException` inherits from Starlette's `HTTPException`. To override the handler for all HTTP exceptions (including those raised by Starlette internals like 404 for missing routes), register the handler for `StarletteHTTPException` rather than FastAPI's version.
|
| 144 |
+
|
| 145 |
+
## Returning the Request Body in Errors
|
| 146 |
+
|
| 147 |
+
The `RequestValidationError` object contains the original request body, which can be useful for logging or debugging:
|
| 148 |
+
|
| 149 |
+
```python
|
| 150 |
+
@app.exception_handler(RequestValidationError)
|
| 151 |
+
async def validation_handler(request: Request, exc: RequestValidationError):
|
| 152 |
+
return JSONResponse(
|
| 153 |
+
status_code=422,
|
| 154 |
+
content={
|
| 155 |
+
"detail": exc.errors(),
|
| 156 |
+
"body": exc.body, # The raw request body that failed validation
|
| 157 |
+
},
|
| 158 |
+
)
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
The `exc.body` attribute holds the parsed request body (as a Python object) before validation was applied. This is only available for body validation errors, not for path or query parameter errors.
|
data/tech_docs/fastapi_intro.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Introduction to FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI is a modern, high-performance web framework for building APIs with Python 3.7+ based on standard Python type hints. Created by Sebastian Ramirez and first released in December 2018, it has quickly become one of the most popular Python web frameworks, with over 75,000 stars on GitHub.
|
| 4 |
+
|
| 5 |
+
## Key Features
|
| 6 |
+
|
| 7 |
+
FastAPI is built on top of two core libraries:
|
| 8 |
+
|
| 9 |
+
- **Starlette** (version 0.27.0+) for the web framework internals, providing WebSocket support, ASGI compatibility, and background tasks.
|
| 10 |
+
- **Pydantic** (version 2.0+) for data validation, serialization, and settings management using Python type annotations.
|
| 11 |
+
|
| 12 |
+
The framework delivers several standout capabilities:
|
| 13 |
+
|
| 14 |
+
1. **High Performance**: FastAPI achieves performance on par with Node.js and Go frameworks. Independent benchmarks from TechEmpower show it handling approximately 9,000 requests per second for JSON serialization on a single worker, compared to Flask's approximately 1,200 requests per second under comparable conditions.
|
| 15 |
+
|
| 16 |
+
2. **Automatic Interactive Documentation**: Every FastAPI application automatically generates two interactive API documentation interfaces -- Swagger UI (available at `/docs`) and ReDoc (available at `/redoc`) -- with zero additional configuration.
|
| 17 |
+
|
| 18 |
+
3. **Async Support**: Full native support for `async`/`await` syntax, allowing non-blocking I/O operations. Synchronous route handlers are automatically run in a threadpool with a default thread count of 40.
|
| 19 |
+
|
| 20 |
+
4. **Type-Driven Development**: Leverages Python type hints for request validation, serialization, and documentation generation, reducing code duplication by an estimated 40% compared to traditional approaches.
|
| 21 |
+
|
| 22 |
+
## Minimal Example
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
from fastapi import FastAPI
|
| 26 |
+
|
| 27 |
+
app = FastAPI(
|
| 28 |
+
title="My API",
|
| 29 |
+
description="A sample API built with FastAPI",
|
| 30 |
+
version="1.0.0",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
@app.get("/")
|
| 34 |
+
async def root():
|
| 35 |
+
return {"message": "Hello, World"}
|
| 36 |
+
|
| 37 |
+
@app.get("/items/{item_id}")
|
| 38 |
+
async def read_item(item_id: int, q: str = None):
|
| 39 |
+
return {"item_id": item_id, "q": q}
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
To run this application, save it as `main.py` and execute:
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
The `--reload` flag enables auto-reload on code changes and should only be used during development. By default, Uvicorn binds to `127.0.0.1` on port `8000`.
|
| 49 |
+
|
| 50 |
+
## How It Works
|
| 51 |
+
|
| 52 |
+
When the application starts, FastAPI performs the following steps:
|
| 53 |
+
|
| 54 |
+
1. Inspects all route handler function signatures to extract parameter types.
|
| 55 |
+
2. Generates a complete OpenAPI 3.1.0 schema (accessible at `/openapi.json`).
|
| 56 |
+
3. Registers Pydantic models for request validation and response serialization.
|
| 57 |
+
4. Mounts the Swagger UI and ReDoc documentation endpoints.
|
| 58 |
+
|
| 59 |
+
Each incoming request goes through this pipeline: ASGI server receives the request, Starlette routes it to the correct handler, Pydantic validates the input data, the handler executes, and the response is serialized back through Pydantic before being sent to the client.
|
| 60 |
+
|
| 61 |
+
## Installation
|
| 62 |
+
|
| 63 |
+
Install FastAPI and an ASGI server:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
pip install fastapi[standard]
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
This installs FastAPI along with Uvicorn (the recommended ASGI server), python-multipart for form data support, and httpx for the test client. The `[standard]` extra includes 6 additional packages beyond the base installation. If you prefer a minimal install, use `pip install fastapi` which installs only FastAPI, Starlette, and Pydantic.
|
| 70 |
+
|
| 71 |
+
FastAPI requires Python 3.7 or higher, though Python 3.10+ is recommended to take advantage of modern type hint syntax such as `X | None` instead of `Optional[X]`.
|
data/tech_docs/fastapi_middleware.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Middleware in FastAPI
|
| 2 |
+
|
| 3 |
+
Middleware is a function that processes every request before it reaches a route handler and every response before it is returned to the client. FastAPI supports both ASGI middleware (from Starlette) and its own decorator-based middleware.
|
| 4 |
+
|
| 5 |
+
## Custom Middleware
|
| 6 |
+
|
| 7 |
+
Use the `@app.middleware("http")` decorator to create custom middleware:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
import time
|
| 11 |
+
from fastapi import FastAPI, Request
|
| 12 |
+
|
| 13 |
+
app = FastAPI()
|
| 14 |
+
|
| 15 |
+
@app.middleware("http")
|
| 16 |
+
async def add_process_time_header(request: Request, call_next):
|
| 17 |
+
start_time = time.perf_counter()
|
| 18 |
+
response = await call_next(request)
|
| 19 |
+
process_time = time.perf_counter() - start_time
|
| 20 |
+
response.headers["X-Process-Time"] = f"{process_time:.4f}"
|
| 21 |
+
return response
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
The middleware receives the incoming `Request` object and a `call_next` function. Calling `await call_next(request)` passes the request to the next middleware or route handler in the chain and returns the `Response`. You can modify both the request (before `call_next`) and the response (after `call_next`).
|
| 25 |
+
|
| 26 |
+
## CORS Middleware
|
| 27 |
+
|
| 28 |
+
Cross-Origin Resource Sharing (CORS) is configured using `CORSMiddleware` from Starlette:
|
| 29 |
+
|
| 30 |
+
```python
|
| 31 |
+
from fastapi import FastAPI
|
| 32 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 33 |
+
|
| 34 |
+
app = FastAPI()
|
| 35 |
+
|
| 36 |
+
app.add_middleware(
|
| 37 |
+
CORSMiddleware,
|
| 38 |
+
allow_origins=["https://example.com", "https://app.example.com"],
|
| 39 |
+
allow_credentials=True,
|
| 40 |
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
| 41 |
+
allow_headers=["Authorization", "Content-Type"],
|
| 42 |
+
expose_headers=["X-Custom-Header"],
|
| 43 |
+
max_age=600,
|
| 44 |
+
)
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
The `CORSMiddleware` parameters:
|
| 48 |
+
|
| 49 |
+
| Parameter | Default | Description |
|
| 50 |
+
|----------------------|---------|----------------------------------------------------|
|
| 51 |
+
| `allow_origins` | `[]` | List of allowed origin URLs |
|
| 52 |
+
| `allow_origin_regex` | `None` | Regex pattern for matching allowed origins |
|
| 53 |
+
| `allow_methods` | `["GET"]` | HTTP methods allowed for cross-origin requests |
|
| 54 |
+
| `allow_headers` | `[]` | HTTP headers allowed in cross-origin requests |
|
| 55 |
+
| `allow_credentials` | `False` | Whether cookies are permitted in cross-origin requests |
|
| 56 |
+
| `expose_headers` | `[]` | Response headers accessible to the browser |
|
| 57 |
+
| `max_age` | `600` | Seconds the browser caches preflight results |
|
| 58 |
+
|
| 59 |
+
To allow all origins, use `allow_origins=["*"]`. However, when `allow_credentials=True`, you cannot use the wildcard `"*"` for `allow_origins` -- you must list specific origins. This is a CORS specification requirement, not a FastAPI limitation.
|
| 60 |
+
|
| 61 |
+
## Middleware Ordering
|
| 62 |
+
|
| 63 |
+
Middleware executes in reverse order of how it is added. The last middleware added is the first to process the request (outermost layer):
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
app = FastAPI()
|
| 67 |
+
|
| 68 |
+
@app.middleware("http")
|
| 69 |
+
async def middleware_one(request: Request, call_next):
|
| 70 |
+
print("Middleware 1: before") # Runs second
|
| 71 |
+
response = await call_next(request)
|
| 72 |
+
print("Middleware 1: after") # Runs third
|
| 73 |
+
return response
|
| 74 |
+
|
| 75 |
+
@app.middleware("http")
|
| 76 |
+
async def middleware_two(request: Request, call_next):
|
| 77 |
+
print("Middleware 2: before") # Runs first
|
| 78 |
+
response = await call_next(request)
|
| 79 |
+
print("Middleware 2: after") # Runs fourth
|
| 80 |
+
return response
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
The output order for a request is: `Middleware 2: before`, `Middleware 1: before`, (route handler), `Middleware 1: after`, `Middleware 2: after`. This follows the standard "onion" model where each middleware wraps the next layer.
|
| 84 |
+
|
| 85 |
+
## Trusted Host Middleware
|
| 86 |
+
|
| 87 |
+
Protect against HTTP Host header attacks:
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
| 91 |
+
|
| 92 |
+
app.add_middleware(
|
| 93 |
+
TrustedHostMiddleware,
|
| 94 |
+
allowed_hosts=["example.com", "*.example.com"],
|
| 95 |
+
)
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Requests with a `Host` header not matching the allowed hosts receive a 400 Bad Request response.
|
| 99 |
+
|
| 100 |
+
## GZip Middleware
|
| 101 |
+
|
| 102 |
+
Compress responses automatically when the client supports it:
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
from fastapi.middleware.gzip import GZipMiddleware
|
| 106 |
+
|
| 107 |
+
app.add_middleware(GZipMiddleware, minimum_size=500)
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
The `minimum_size` parameter (default: `500` bytes) sets the minimum response body size before compression is applied. Responses smaller than this threshold are sent uncompressed. GZip compression typically reduces JSON response sizes by 60-80%.
|
| 111 |
+
|
| 112 |
+
## ASGI Middleware
|
| 113 |
+
|
| 114 |
+
Since FastAPI is an ASGI application, you can use any ASGI-compatible middleware:
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 118 |
+
|
| 119 |
+
app.add_middleware(
|
| 120 |
+
SessionMiddleware,
|
| 121 |
+
secret_key="your-session-secret",
|
| 122 |
+
max_age=14 * 24 * 60 * 60, # 14 days in seconds = 1,209,600
|
| 123 |
+
)
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
The `add_middleware()` method is the preferred way to add middleware in FastAPI, as it ensures proper integration with the application's middleware stack and exception handling.
|
data/tech_docs/fastapi_openapi.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenAPI and Documentation in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI automatically generates an OpenAPI 3.1.0 schema from your code, providing interactive documentation interfaces with zero configuration. This schema drives Swagger UI and ReDoc, and can be consumed by code generators, API gateways, and testing tools.
|
| 4 |
+
|
| 5 |
+
## Automatic Documentation Endpoints
|
| 6 |
+
|
| 7 |
+
Every FastAPI application exposes three documentation-related endpoints by default:
|
| 8 |
+
|
| 9 |
+
| Endpoint | Description |
|
| 10 |
+
|------------------|--------------------------------------------------|
|
| 11 |
+
| `/docs` | Swagger UI -- interactive API explorer |
|
| 12 |
+
| `/redoc` | ReDoc -- alternative documentation viewer |
|
| 13 |
+
| `/openapi.json` | Raw OpenAPI schema in JSON format |
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
from fastapi import FastAPI
|
| 17 |
+
|
| 18 |
+
app = FastAPI(
|
| 19 |
+
title="My API",
|
| 20 |
+
description="A comprehensive API for managing items and users.",
|
| 21 |
+
version="2.1.0",
|
| 22 |
+
terms_of_service="https://example.com/terms",
|
| 23 |
+
contact={
|
| 24 |
+
"name": "API Support",
|
| 25 |
+
"url": "https://example.com/support",
|
| 26 |
+
"email": "support@example.com",
|
| 27 |
+
},
|
| 28 |
+
license_info={
|
| 29 |
+
"name": "MIT",
|
| 30 |
+
"url": "https://opensource.org/licenses/MIT",
|
| 31 |
+
},
|
| 32 |
+
openapi_url="/openapi.json",
|
| 33 |
+
docs_url="/docs",
|
| 34 |
+
redoc_url="/redoc",
|
| 35 |
+
)
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
To disable any documentation endpoint, set its URL to `None`:
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
app = FastAPI(
|
| 42 |
+
docs_url=None, # Disables Swagger UI
|
| 43 |
+
redoc_url=None, # Disables ReDoc
|
| 44 |
+
openapi_url=None, # Disables OpenAPI schema (also disables both UIs)
|
| 45 |
+
)
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
Disabling `openapi_url` effectively disables all automatic documentation since both Swagger UI and ReDoc depend on the OpenAPI schema.
|
| 49 |
+
|
| 50 |
+
## Tags and Grouping
|
| 51 |
+
|
| 52 |
+
Organize endpoints into logical groups using tags:
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
from fastapi import FastAPI
|
| 56 |
+
|
| 57 |
+
tags_metadata = [
|
| 58 |
+
{
|
| 59 |
+
"name": "users",
|
| 60 |
+
"description": "Operations with users. The **login** logic is also here.",
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"name": "items",
|
| 64 |
+
"description": "Manage items. Each item has a unique integer ID.",
|
| 65 |
+
"externalDocs": {
|
| 66 |
+
"description": "Items external docs",
|
| 67 |
+
"url": "https://example.com/items-docs",
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
app = FastAPI(openapi_tags=tags_metadata)
|
| 73 |
+
|
| 74 |
+
@app.get("/users/", tags=["users"])
|
| 75 |
+
async def read_users():
|
| 76 |
+
return [{"username": "alice"}]
|
| 77 |
+
|
| 78 |
+
@app.get("/items/", tags=["items"])
|
| 79 |
+
async def read_items():
|
| 80 |
+
return [{"name": "Widget"}]
|
| 81 |
+
|
| 82 |
+
@app.post("/items/", tags=["items"])
|
| 83 |
+
async def create_item(name: str):
|
| 84 |
+
return {"name": name}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Tags appear as collapsible sections in Swagger UI. The order of tags in `openapi_tags` determines their display order. An endpoint can have multiple tags, causing it to appear in each corresponding section.
|
| 88 |
+
|
| 89 |
+
## Enriching Endpoint Documentation
|
| 90 |
+
|
| 91 |
+
Add descriptions, summaries, and response documentation to individual endpoints:
|
| 92 |
+
|
| 93 |
+
```python
|
| 94 |
+
from fastapi import FastAPI, Path, Query
|
| 95 |
+
from pydantic import BaseModel
|
| 96 |
+
|
| 97 |
+
app = FastAPI()
|
| 98 |
+
|
| 99 |
+
class Item(BaseModel):
|
| 100 |
+
"""An item in the inventory system."""
|
| 101 |
+
name: str
|
| 102 |
+
price: float
|
| 103 |
+
description: str | None = None
|
| 104 |
+
|
| 105 |
+
model_config = {
|
| 106 |
+
"json_schema_extra": {
|
| 107 |
+
"examples": [
|
| 108 |
+
{
|
| 109 |
+
"name": "Premium Widget",
|
| 110 |
+
"price": 35.99,
|
| 111 |
+
"description": "A high-quality widget",
|
| 112 |
+
}
|
| 113 |
+
]
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
@app.get(
|
| 118 |
+
"/items/{item_id}",
|
| 119 |
+
summary="Get a single item",
|
| 120 |
+
description="Retrieve an item by its unique integer ID. Returns 404 if the item does not exist.",
|
| 121 |
+
response_description="The requested item with all fields populated",
|
| 122 |
+
deprecated=False,
|
| 123 |
+
operation_id="getItemById",
|
| 124 |
+
)
|
| 125 |
+
async def read_item(
|
| 126 |
+
item_id: int = Path(
|
| 127 |
+
title="Item ID",
|
| 128 |
+
description="The unique identifier for the item",
|
| 129 |
+
ge=1,
|
| 130 |
+
example=42,
|
| 131 |
+
),
|
| 132 |
+
):
|
| 133 |
+
return {"item_id": item_id, "name": "Widget", "price": 35.99}
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
If no `summary` is provided, FastAPI uses the function name converted to title case (e.g., `read_item` becomes "Read Item"). If no `description` is provided, FastAPI uses the function's docstring.
|
| 137 |
+
|
| 138 |
+
The `operation_id` sets a unique identifier for the endpoint in the OpenAPI schema. By default, FastAPI generates operation IDs by combining the HTTP method and function name (e.g., `read_item_items__item_id__get`). Custom operation IDs are useful when generating client SDKs.
|
| 139 |
+
|
| 140 |
+
## Customizing the OpenAPI Schema
|
| 141 |
+
|
| 142 |
+
Override or extend the generated schema programmatically:
|
| 143 |
+
|
| 144 |
+
```python
|
| 145 |
+
from fastapi import FastAPI
|
| 146 |
+
from fastapi.openapi.utils import get_openapi
|
| 147 |
+
|
| 148 |
+
app = FastAPI()
|
| 149 |
+
|
| 150 |
+
def custom_openapi():
|
| 151 |
+
if app.openapi_schema:
|
| 152 |
+
return app.openapi_schema
|
| 153 |
+
|
| 154 |
+
openapi_schema = get_openapi(
|
| 155 |
+
title="Custom API",
|
| 156 |
+
version="3.0.0",
|
| 157 |
+
summary="An API with a custom OpenAPI schema",
|
| 158 |
+
description="This schema includes additional vendor extensions.",
|
| 159 |
+
routes=app.routes,
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Add custom vendor extension
|
| 163 |
+
openapi_schema["x-api-audience"] = "public"
|
| 164 |
+
|
| 165 |
+
# Modify schema components
|
| 166 |
+
openapi_schema["info"]["x-logo"] = {
|
| 167 |
+
"url": "https://example.com/logo.png",
|
| 168 |
+
"altText": "API Logo",
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
app.openapi_schema = openapi_schema
|
| 172 |
+
return app.openapi_schema
|
| 173 |
+
|
| 174 |
+
app.openapi = custom_openapi
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
The `get_openapi()` function generates the base schema from the application's routes. By assigning the result to `app.openapi_schema`, you cache it so it is only generated once. The cached schema is served at `/openapi.json` for all subsequent requests.
|
| 178 |
+
|
| 179 |
+
## Multiple Examples
|
| 180 |
+
|
| 181 |
+
Provide multiple request/response examples for a single endpoint:
|
| 182 |
+
|
| 183 |
+
```python
|
| 184 |
+
from fastapi import Body
|
| 185 |
+
|
| 186 |
+
@app.post("/items/")
|
| 187 |
+
async def create_item(
|
| 188 |
+
item: Item = Body(
|
| 189 |
+
openapi_examples={
|
| 190 |
+
"minimal": {
|
| 191 |
+
"summary": "Minimal item",
|
| 192 |
+
"description": "Only required fields",
|
| 193 |
+
"value": {"name": "Widget", "price": 9.99},
|
| 194 |
+
},
|
| 195 |
+
"complete": {
|
| 196 |
+
"summary": "Complete item",
|
| 197 |
+
"description": "All fields populated",
|
| 198 |
+
"value": {
|
| 199 |
+
"name": "Premium Widget",
|
| 200 |
+
"price": 35.99,
|
| 201 |
+
"description": "A high-quality widget",
|
| 202 |
+
},
|
| 203 |
+
},
|
| 204 |
+
},
|
| 205 |
+
),
|
| 206 |
+
):
|
| 207 |
+
return item
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
These examples appear in Swagger UI as a dropdown menu, allowing API consumers to quickly test different request scenarios. Each example requires a `summary` and `value`; the `description` is optional.
|
data/tech_docs/fastapi_pagination.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pagination in FastAPI
|
| 2 |
+
|
| 3 |
+
Pagination is essential for any API that returns collections of resources. Without pagination, endpoints serving large datasets would consume excessive memory, bandwidth, and time. FastAPI supports multiple pagination strategies, each suited to different use cases.
|
| 4 |
+
|
| 5 |
+
## Offset/Limit Pagination (Skip/Limit Pattern)
|
| 6 |
+
|
| 7 |
+
The most common approach uses `skip` and `limit` query parameters:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from fastapi import FastAPI, Query, Depends
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
|
| 13 |
+
app = FastAPI()
|
| 14 |
+
|
| 15 |
+
class Item(BaseModel):
|
| 16 |
+
id: int
|
| 17 |
+
name: str
|
| 18 |
+
price: float
|
| 19 |
+
|
| 20 |
+
# Simulated database with 10,000 items
|
| 21 |
+
all_items = [Item(id=i, name=f"Item {i}", price=round(i * 1.5, 2)) for i in range(1, 10001)]
|
| 22 |
+
|
| 23 |
+
class PaginationParams:
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
skip: int = Query(default=0, ge=0, description="Number of items to skip"),
|
| 27 |
+
limit: int = Query(default=20, ge=1, le=100, description="Number of items to return"),
|
| 28 |
+
):
|
| 29 |
+
self.skip = skip
|
| 30 |
+
self.limit = limit
|
| 31 |
+
|
| 32 |
+
@app.get("/items/")
|
| 33 |
+
async def list_items(pagination: PaginationParams = Depends()):
|
| 34 |
+
items = all_items[pagination.skip : pagination.skip + pagination.limit]
|
| 35 |
+
return {
|
| 36 |
+
"items": items,
|
| 37 |
+
"total": len(all_items),
|
| 38 |
+
"skip": pagination.skip,
|
| 39 |
+
"limit": pagination.limit,
|
| 40 |
+
}
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
This implementation uses a default page size of 20 items, a minimum of 1 item per page, and a maximum of 100 items per page. For a dataset of 10,000 items with the default page size of 20, there are 500 total pages. Requesting page 3 would use `skip=40&limit=20` to retrieve items 41 through 60.
|
| 44 |
+
|
| 45 |
+
The offset/limit pattern is simple to implement but has performance drawbacks for large offsets. A query with `skip=9000` on a SQL database must scan and discard 9,000 rows before returning the requested 20, resulting in O(n) performance where n is the offset value.
|
| 46 |
+
|
| 47 |
+
## Cursor-Based Pagination
|
| 48 |
+
|
| 49 |
+
Cursor-based pagination uses an opaque token (cursor) pointing to the last item in the previous page. This avoids the performance degradation of large offsets:
|
| 50 |
+
|
| 51 |
+
```python
|
| 52 |
+
import base64
|
| 53 |
+
from fastapi import FastAPI, Query
|
| 54 |
+
|
| 55 |
+
app = FastAPI()
|
| 56 |
+
|
| 57 |
+
def encode_cursor(item_id: int) -> str:
|
| 58 |
+
return base64.urlsafe_b64encode(f"id:{item_id}".encode()).decode()
|
| 59 |
+
|
| 60 |
+
def decode_cursor(cursor: str) -> int:
|
| 61 |
+
decoded = base64.urlsafe_b64decode(cursor.encode()).decode()
|
| 62 |
+
return int(decoded.split(":")[1])
|
| 63 |
+
|
| 64 |
+
@app.get("/items/")
|
| 65 |
+
async def list_items(
|
| 66 |
+
cursor: str | None = Query(default=None, description="Pagination cursor"),
|
| 67 |
+
limit: int = Query(default=20, ge=1, le=100),
|
| 68 |
+
):
|
| 69 |
+
if cursor:
|
| 70 |
+
last_id = decode_cursor(cursor)
|
| 71 |
+
# In a real DB: SELECT * FROM items WHERE id > last_id ORDER BY id LIMIT limit
|
| 72 |
+
items = [item for item in all_items if item.id > last_id][:limit]
|
| 73 |
+
else:
|
| 74 |
+
items = all_items[:limit]
|
| 75 |
+
|
| 76 |
+
next_cursor = None
|
| 77 |
+
if len(items) == limit:
|
| 78 |
+
next_cursor = encode_cursor(items[-1].id)
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"items": items,
|
| 82 |
+
"next_cursor": next_cursor,
|
| 83 |
+
"limit": limit,
|
| 84 |
+
"has_more": len(items) == limit,
|
| 85 |
+
}
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
Cursor-based pagination maintains consistent O(1) performance regardless of how deep into the dataset the client has paginated. It is the recommended approach for datasets exceeding 100,000 records or for real-time feeds where items may be inserted or deleted between page requests.
|
| 89 |
+
|
| 90 |
+
## Pagination with Total Count and Link Headers
|
| 91 |
+
|
| 92 |
+
Include total count metadata and RFC 5988 Link headers for discoverability:
|
| 93 |
+
|
| 94 |
+
```python
|
| 95 |
+
from fastapi import FastAPI, Query, Response
|
| 96 |
+
from math import ceil
|
| 97 |
+
|
| 98 |
+
app = FastAPI()
|
| 99 |
+
|
| 100 |
+
@app.get("/items/")
|
| 101 |
+
async def list_items(
|
| 102 |
+
response: Response,
|
| 103 |
+
page: int = Query(default=1, ge=1, description="Page number"),
|
| 104 |
+
per_page: int = Query(default=20, ge=1, le=100, description="Items per page"),
|
| 105 |
+
):
|
| 106 |
+
total = len(all_items)
|
| 107 |
+
total_pages = ceil(total / per_page)
|
| 108 |
+
skip = (page - 1) * per_page
|
| 109 |
+
items = all_items[skip : skip + per_page]
|
| 110 |
+
|
| 111 |
+
# Build Link headers
|
| 112 |
+
base_url = "/items/"
|
| 113 |
+
links = []
|
| 114 |
+
if page > 1:
|
| 115 |
+
links.append(f'<{base_url}?page=1&per_page={per_page}>; rel="first"')
|
| 116 |
+
links.append(f'<{base_url}?page={page - 1}&per_page={per_page}>; rel="prev"')
|
| 117 |
+
if page < total_pages:
|
| 118 |
+
links.append(f'<{base_url}?page={page + 1}&per_page={per_page}>; rel="next"')
|
| 119 |
+
links.append(f'<{base_url}?page={total_pages}&per_page={per_page}>; rel="last"')
|
| 120 |
+
|
| 121 |
+
response.headers["Link"] = ", ".join(links)
|
| 122 |
+
response.headers["X-Total-Count"] = str(total)
|
| 123 |
+
response.headers["X-Total-Pages"] = str(total_pages)
|
| 124 |
+
|
| 125 |
+
return {
|
| 126 |
+
"items": items,
|
| 127 |
+
"page": page,
|
| 128 |
+
"per_page": per_page,
|
| 129 |
+
"total": total,
|
| 130 |
+
"total_pages": total_pages,
|
| 131 |
+
}
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
With 10,000 items and a default page size of 20, the `X-Total-Pages` header returns 500. At 50 items per page, there are 200 total pages. The Link header follows the RFC 5988 standard used by the GitHub API and other major REST APIs.
|
| 135 |
+
|
| 136 |
+
## Pagination Response Model
|
| 137 |
+
|
| 138 |
+
Standardize pagination responses across endpoints with a generic response model:
|
| 139 |
+
|
| 140 |
+
```python
|
| 141 |
+
from typing import Generic, TypeVar, List
|
| 142 |
+
from pydantic import BaseModel
|
| 143 |
+
|
| 144 |
+
T = TypeVar("T")
|
| 145 |
+
|
| 146 |
+
class PaginatedResponse(BaseModel, Generic[T]):
|
| 147 |
+
items: List[T]
|
| 148 |
+
total: int
|
| 149 |
+
page: int
|
| 150 |
+
per_page: int
|
| 151 |
+
total_pages: int
|
| 152 |
+
|
| 153 |
+
@app.get("/items/", response_model=PaginatedResponse[Item])
|
| 154 |
+
async def list_items(
|
| 155 |
+
page: int = Query(default=1, ge=1),
|
| 156 |
+
per_page: int = Query(default=20, ge=1, le=100),
|
| 157 |
+
):
|
| 158 |
+
total = len(all_items)
|
| 159 |
+
skip = (page - 1) * per_page
|
| 160 |
+
return PaginatedResponse(
|
| 161 |
+
items=all_items[skip : skip + per_page],
|
| 162 |
+
total=total,
|
| 163 |
+
page=page,
|
| 164 |
+
per_page=per_page,
|
| 165 |
+
total_pages=ceil(total / per_page),
|
| 166 |
+
)
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
This generic model ensures every paginated endpoint returns a consistent structure. The `total_pages` field is always calculated as `ceil(total / per_page)`. For 10,000 items at 20 per page, that is `ceil(10000 / 20) = 500` pages. For 10,000 items at 30 per page, that is `ceil(10000 / 30) = 334` pages (with the last page containing only 10 items).
|
data/tech_docs/fastapi_path_params.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Path Parameters in FastAPI
|
| 2 |
+
|
| 3 |
+
Path parameters allow you to capture variable segments of a URL path and pass them directly to your route handler function. They are declared using curly braces `{}` in the route path string and must have a corresponding parameter in the function signature.
|
| 4 |
+
|
| 5 |
+
## Basic Path Parameters
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
|
| 12 |
+
@app.get("/users/{user_id}")
|
| 13 |
+
async def read_user(user_id: int):
|
| 14 |
+
return {"user_id": user_id}
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
When a client sends a request to `/users/42`, FastAPI will automatically parse `"42"` from the URL, validate that it can be converted to an `int`, and pass `user_id=42` to the handler. If the client sends `/users/abc`, FastAPI returns a 422 Unprocessable Entity response with a detailed validation error.
|
| 18 |
+
|
| 19 |
+
## Type Annotations and Validation
|
| 20 |
+
|
| 21 |
+
Path parameters support all standard Python types for automatic conversion:
|
| 22 |
+
|
| 23 |
+
- `int` -- integer values (e.g., `/items/5`)
|
| 24 |
+
- `float` -- floating-point values (e.g., `/prices/9.99`)
|
| 25 |
+
- `str` -- string values (this is the default if no type is specified)
|
| 26 |
+
- `bool` -- boolean values, accepts `true`, `false`, `1`, `0`, `yes`, `no`
|
| 27 |
+
- `uuid.UUID` -- UUID strings (e.g., `/records/550e8400-e29b-41d4-a716-446655440000`)
|
| 28 |
+
|
| 29 |
+
## Path Parameter Validation with Path()
|
| 30 |
+
|
| 31 |
+
Use the `Path()` function from FastAPI to add validation constraints:
|
| 32 |
+
|
| 33 |
+
```python
|
| 34 |
+
from fastapi import FastAPI, Path
|
| 35 |
+
|
| 36 |
+
app = FastAPI()
|
| 37 |
+
|
| 38 |
+
@app.get("/items/{item_id}")
|
| 39 |
+
async def read_item(
|
| 40 |
+
item_id: int = Path(
|
| 41 |
+
title="The ID of the item",
|
| 42 |
+
description="A unique integer identifier",
|
| 43 |
+
ge=1,
|
| 44 |
+
le=10000,
|
| 45 |
+
)
|
| 46 |
+
):
|
| 47 |
+
return {"item_id": item_id}
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
The `Path()` function supports these numeric validation parameters:
|
| 51 |
+
|
| 52 |
+
| Parameter | Meaning | Example |
|
| 53 |
+
|-----------|-----------------------|---------|
|
| 54 |
+
| `gt` | greater than | `gt=0` |
|
| 55 |
+
| `ge` | greater than or equal | `ge=1` |
|
| 56 |
+
| `lt` | less than | `lt=100`|
|
| 57 |
+
| `le` | less than or equal | `le=99` |
|
| 58 |
+
|
| 59 |
+
For string path parameters, you can use `min_length` and `max_length` constraints. The default `min_length` is `None` (no minimum), and the maximum allowed `max_length` for path parameters in practice is 255 characters due to URL length limitations in most web servers.
|
| 60 |
+
|
| 61 |
+
## Route Order Matters
|
| 62 |
+
|
| 63 |
+
FastAPI evaluates routes in the order they are defined. This is critical when you have routes that could match the same URL pattern:
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
@app.get("/users/me")
|
| 67 |
+
async def read_current_user():
|
| 68 |
+
return {"user": "the current user"}
|
| 69 |
+
|
| 70 |
+
@app.get("/users/{user_id}")
|
| 71 |
+
async def read_user(user_id: str):
|
| 72 |
+
return {"user_id": user_id}
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The `/users/me` route **must** be declared before `/users/{user_id}`. If the order is reversed, a request to `/users/me` would match the parameterized route first, and `user_id` would receive the string `"me"` as its value instead of triggering the dedicated handler.
|
| 76 |
+
|
| 77 |
+
## Enum Path Parameters
|
| 78 |
+
|
| 79 |
+
You can restrict path parameters to a fixed set of values using Python's `Enum`:
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
from enum import Enum
|
| 83 |
+
|
| 84 |
+
class ModelName(str, Enum):
|
| 85 |
+
alexnet = "alexnet"
|
| 86 |
+
resnet = "resnet"
|
| 87 |
+
lenet = "lenet"
|
| 88 |
+
|
| 89 |
+
@app.get("/models/{model_name}")
|
| 90 |
+
async def get_model(model_name: ModelName):
|
| 91 |
+
if model_name is ModelName.alexnet:
|
| 92 |
+
return {"model_name": model_name, "message": "Deep Learning FTW!"}
|
| 93 |
+
return {"model_name": model_name, "message": "Other model selected"}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
If the client sends a value not in the enum, FastAPI returns a 422 response listing all permitted values.
|
| 97 |
+
|
| 98 |
+
## File Path Parameters
|
| 99 |
+
|
| 100 |
+
To capture an entire file path (including slashes), use the `:path` converter:
|
| 101 |
+
|
| 102 |
+
```python
|
| 103 |
+
@app.get("/files/{file_path:path}")
|
| 104 |
+
async def read_file(file_path: str):
|
| 105 |
+
return {"file_path": file_path}
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
A request to `/files/home/user/data.csv` will set `file_path` to `"home/user/data.csv"`.
|
data/tech_docs/fastapi_query_params.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Query Parameters in FastAPI
|
| 2 |
+
|
| 3 |
+
Query parameters are the key-value pairs that appear after the `?` in a URL (e.g., `/items?skip=0&limit=10`). In FastAPI, any function parameter that is not part of the path is automatically interpreted as a query parameter.
|
| 4 |
+
|
| 5 |
+
## Basic Query Parameters
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
|
| 12 |
+
# Sample data
|
| 13 |
+
fake_items_db = [{"item_name": f"Item {i}"} for i in range(100)]
|
| 14 |
+
|
| 15 |
+
@app.get("/items/")
|
| 16 |
+
async def read_items(skip: int = 0, limit: int = 10):
|
| 17 |
+
return fake_items_db[skip : skip + limit]
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
In this example, both `skip` and `limit` are query parameters with default values. A request to `/items/` uses the defaults (`skip=0`, `limit=10`), while `/items/?skip=20&limit=5` overrides both. FastAPI automatically converts the string values from the URL into their declared Python types.
|
| 21 |
+
|
| 22 |
+
## Required vs Optional Query Parameters
|
| 23 |
+
|
| 24 |
+
The distinction between required and optional query parameters depends on whether a default value is provided:
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
from fastapi import FastAPI, Query
|
| 28 |
+
from typing import Optional
|
| 29 |
+
|
| 30 |
+
app = FastAPI()
|
| 31 |
+
|
| 32 |
+
@app.get("/search/")
|
| 33 |
+
async def search(
|
| 34 |
+
q: str, # Required - no default
|
| 35 |
+
category: str = "all", # Optional - has default
|
| 36 |
+
max_price: Optional[float] = None, # Optional - default is None
|
| 37 |
+
):
|
| 38 |
+
results = {"q": q, "category": category}
|
| 39 |
+
if max_price is not None:
|
| 40 |
+
results["max_price"] = max_price
|
| 41 |
+
return results
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
If a client calls `/search/` without the `q` parameter, FastAPI returns a 422 Unprocessable Entity error. The `category` parameter defaults to `"all"`, and `max_price` defaults to `None`.
|
| 45 |
+
|
| 46 |
+
## Query Parameter Validation with Query()
|
| 47 |
+
|
| 48 |
+
The `Query()` function provides additional validation and metadata:
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
from fastapi import FastAPI, Query
|
| 52 |
+
|
| 53 |
+
app = FastAPI()
|
| 54 |
+
|
| 55 |
+
@app.get("/items/")
|
| 56 |
+
async def read_items(
|
| 57 |
+
q: str = Query(
|
| 58 |
+
default=None,
|
| 59 |
+
min_length=3,
|
| 60 |
+
max_length=50,
|
| 61 |
+
pattern="^[a-zA-Z0-9 ]+$",
|
| 62 |
+
title="Search query",
|
| 63 |
+
description="The search string to filter items by name",
|
| 64 |
+
example="laptop",
|
| 65 |
+
)
|
| 66 |
+
):
|
| 67 |
+
results = {"items": []}
|
| 68 |
+
if q:
|
| 69 |
+
results["q"] = q
|
| 70 |
+
return results
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
The `Query()` function supports the following validation parameters for strings:
|
| 74 |
+
|
| 75 |
+
- `min_length` -- minimum character length (default: `None`, no minimum)
|
| 76 |
+
- `max_length` -- maximum character length (default: `None`, no maximum)
|
| 77 |
+
- `pattern` -- a regular expression the value must match
|
| 78 |
+
|
| 79 |
+
For numeric query parameters, `Query()` supports the same `gt`, `ge`, `lt`, and `le` constraints as `Path()`.
|
| 80 |
+
|
| 81 |
+
## Multiple Values for a Single Query Parameter
|
| 82 |
+
|
| 83 |
+
To accept a list of values for one query parameter (e.g., `/items/?tag=food&tag=drink`), declare the parameter as a `list`:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
from typing import List
|
| 87 |
+
from fastapi import FastAPI, Query
|
| 88 |
+
|
| 89 |
+
app = FastAPI()
|
| 90 |
+
|
| 91 |
+
@app.get("/items/")
|
| 92 |
+
async def read_items(
|
| 93 |
+
tags: List[str] = Query(default=[], description="Filter by tags")
|
| 94 |
+
):
|
| 95 |
+
return {"tags": tags}
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
A request to `/items/?tags=food&tags=drink` yields `tags=["food", "drink"]`. The default is an empty list if no tags are provided.
|
| 99 |
+
|
| 100 |
+
## Combining Path and Query Parameters
|
| 101 |
+
|
| 102 |
+
Path and query parameters work together seamlessly. FastAPI distinguishes them based on whether the parameter name appears in the path template:
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
@app.get("/users/{user_id}/items/")
|
| 106 |
+
async def read_user_items(
|
| 107 |
+
user_id: int, # Path parameter (in URL path)
|
| 108 |
+
skip: int = 0, # Query parameter (not in path)
|
| 109 |
+
limit: int = 10, # Query parameter (not in path)
|
| 110 |
+
include_archived: bool = False, # Query parameter
|
| 111 |
+
):
|
| 112 |
+
return {
|
| 113 |
+
"user_id": user_id,
|
| 114 |
+
"skip": skip,
|
| 115 |
+
"limit": limit,
|
| 116 |
+
"include_archived": include_archived,
|
| 117 |
+
}
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
A request to `/users/42/items/?skip=5&limit=20&include_archived=true` passes `user_id=42` from the path and all other values from the query string. Boolean query parameters accept `true`, `false`, `1`, `0`, `yes`, `no`, `on`, and `off` (case-insensitive). FastAPI converts all these values to Python `bool`.
|
| 121 |
+
|
| 122 |
+
## Deprecating Query Parameters
|
| 123 |
+
|
| 124 |
+
You can mark a query parameter as deprecated to signal to API consumers that it will be removed in a future version:
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
@app.get("/items/")
|
| 128 |
+
async def read_items(
|
| 129 |
+
q: str = Query(default=None, deprecated=True)
|
| 130 |
+
):
|
| 131 |
+
return {"q": q}
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
The parameter still functions normally, but it appears as deprecated in the generated OpenAPI documentation and Swagger UI.
|
data/tech_docs/fastapi_request_body.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Request Body in FastAPI
|
| 2 |
+
|
| 3 |
+
A request body is data sent by the client to your API, typically as JSON in POST, PUT, or PATCH requests. FastAPI uses Pydantic models to declare, validate, and serialize request bodies with full type safety.
|
| 4 |
+
|
| 5 |
+
## Defining a Request Body with Pydantic
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
|
| 11 |
+
app = FastAPI()
|
| 12 |
+
|
| 13 |
+
class Item(BaseModel):
|
| 14 |
+
name: str
|
| 15 |
+
description: str | None = None
|
| 16 |
+
price: float
|
| 17 |
+
tax: float = 0.0
|
| 18 |
+
|
| 19 |
+
@app.post("/items/")
|
| 20 |
+
async def create_item(item: Item):
|
| 21 |
+
item_dict = item.model_dump()
|
| 22 |
+
if item.tax > 0:
|
| 23 |
+
price_with_tax = item.price + item.tax
|
| 24 |
+
item_dict.update({"price_with_tax": price_with_tax})
|
| 25 |
+
return item_dict
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
When a client sends a POST request with a JSON body like `{"name": "Widget", "price": 35.99, "tax": 3.60}`, FastAPI automatically parses the JSON, validates it against the `Item` model, and passes the validated object to the handler. If `description` is omitted, it defaults to `None`. If `tax` is omitted, it defaults to `0.0`. If `name` or `price` is missing, a 422 Unprocessable Entity response is returned.
|
| 29 |
+
|
| 30 |
+
## Field Validation
|
| 31 |
+
|
| 32 |
+
The `Field()` function from Pydantic lets you add constraints and metadata to individual model fields:
|
| 33 |
+
|
| 34 |
+
```python
|
| 35 |
+
from pydantic import BaseModel, Field
|
| 36 |
+
|
| 37 |
+
class Item(BaseModel):
|
| 38 |
+
name: str = Field(
|
| 39 |
+
min_length=1,
|
| 40 |
+
max_length=100,
|
| 41 |
+
description="The name of the item",
|
| 42 |
+
)
|
| 43 |
+
description: str | None = Field(
|
| 44 |
+
default=None,
|
| 45 |
+
max_length=500,
|
| 46 |
+
description="An optional text description",
|
| 47 |
+
)
|
| 48 |
+
price: float = Field(
|
| 49 |
+
gt=0,
|
| 50 |
+
le=1_000_000,
|
| 51 |
+
description="Price must be greater than 0 and at most 1,000,000",
|
| 52 |
+
)
|
| 53 |
+
quantity: int = Field(
|
| 54 |
+
default=1,
|
| 55 |
+
ge=1,
|
| 56 |
+
le=9999,
|
| 57 |
+
description="Quantity between 1 and 9999",
|
| 58 |
+
)
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Pydantic validates all constraints at request time. The `gt`, `ge`, `lt`, `le` parameters mirror the same semantics as FastAPI's `Path()` and `Query()`. The `min_length` and `max_length` parameters work on string fields.
|
| 62 |
+
|
| 63 |
+
## Nested Models
|
| 64 |
+
|
| 65 |
+
Pydantic models can contain other models, lists, and complex nested structures:
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
from pydantic import BaseModel, HttpUrl
|
| 69 |
+
|
| 70 |
+
class Image(BaseModel):
|
| 71 |
+
url: HttpUrl
|
| 72 |
+
name: str
|
| 73 |
+
width: int = Field(ge=1, le=10000)
|
| 74 |
+
height: int = Field(ge=1, le=10000)
|
| 75 |
+
|
| 76 |
+
class Item(BaseModel):
|
| 77 |
+
name: str
|
| 78 |
+
description: str | None = None
|
| 79 |
+
price: float
|
| 80 |
+
tags: list[str] = []
|
| 81 |
+
images: list[Image] = []
|
| 82 |
+
|
| 83 |
+
class Offer(BaseModel):
|
| 84 |
+
name: str
|
| 85 |
+
description: str | None = None
|
| 86 |
+
items: list[Item]
|
| 87 |
+
discount_percent: float = Field(ge=0, le=100)
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
FastAPI validates the entire nested structure recursively. If any nested field fails validation, the error response includes the exact path to the invalid field (e.g., `body -> items -> 0 -> images -> 1 -> url`).
|
| 91 |
+
|
| 92 |
+
## Combining Body, Path, and Query Parameters
|
| 93 |
+
|
| 94 |
+
You can accept all three parameter types in a single endpoint:
|
| 95 |
+
|
| 96 |
+
```python
|
| 97 |
+
from fastapi import FastAPI, Path, Query
|
| 98 |
+
from pydantic import BaseModel
|
| 99 |
+
|
| 100 |
+
app = FastAPI()
|
| 101 |
+
|
| 102 |
+
class Item(BaseModel):
|
| 103 |
+
name: str
|
| 104 |
+
price: float
|
| 105 |
+
|
| 106 |
+
@app.put("/items/{item_id}")
|
| 107 |
+
async def update_item(
|
| 108 |
+
item_id: int = Path(ge=1, le=10000),
|
| 109 |
+
q: str | None = Query(default=None, max_length=50),
|
| 110 |
+
item: Item = ...,
|
| 111 |
+
):
|
| 112 |
+
result = {"item_id": item_id, **item.model_dump()}
|
| 113 |
+
if q:
|
| 114 |
+
result["q"] = q
|
| 115 |
+
return result
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
FastAPI determines the source of each parameter by these rules: if the parameter name appears in the path string, it is a path parameter; if the type is a Pydantic model (or annotated with `Body()`), it comes from the request body; otherwise, it is a query parameter.
|
| 119 |
+
|
| 120 |
+
## Multiple Body Parameters
|
| 121 |
+
|
| 122 |
+
When you need multiple distinct objects in the request body, declare multiple Pydantic model parameters:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
from fastapi import Body
|
| 126 |
+
|
| 127 |
+
class Item(BaseModel):
|
| 128 |
+
name: str
|
| 129 |
+
price: float
|
| 130 |
+
|
| 131 |
+
class User(BaseModel):
|
| 132 |
+
username: str
|
| 133 |
+
email: str
|
| 134 |
+
|
| 135 |
+
@app.put("/items/{item_id}")
|
| 136 |
+
async def update_item(
|
| 137 |
+
item_id: int,
|
| 138 |
+
item: Item,
|
| 139 |
+
user: User,
|
| 140 |
+
importance: int = Body(gt=0, le=5),
|
| 141 |
+
):
|
| 142 |
+
return {"item_id": item_id, "item": item, "user": user, "importance": importance}
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
The expected JSON body becomes `{"item": {...}, "user": {...}, "importance": 3}`. Each model is keyed by its parameter name. The `Body()` function embeds a singular value inside the body alongside the models, rather than treating it as a query parameter. The maximum request body size is controlled by the ASGI server; Uvicorn defaults to approximately 1 MB.
|
data/tech_docs/fastapi_response_model.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Response Model in FastAPI
|
| 2 |
+
|
| 3 |
+
The `response_model` parameter on route decorators lets you declare the shape of the data your endpoint returns. FastAPI uses it to validate, serialize, and document the response -- filtering out any fields not defined in the model and generating accurate OpenAPI schemas.
|
| 4 |
+
|
| 5 |
+
## Basic Response Model
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
|
| 11 |
+
app = FastAPI()
|
| 12 |
+
|
| 13 |
+
class UserIn(BaseModel):
|
| 14 |
+
username: str
|
| 15 |
+
email: str
|
| 16 |
+
password: str
|
| 17 |
+
|
| 18 |
+
class UserOut(BaseModel):
|
| 19 |
+
username: str
|
| 20 |
+
email: str
|
| 21 |
+
|
| 22 |
+
@app.post("/users/", response_model=UserOut, status_code=201)
|
| 23 |
+
async def create_user(user: UserIn):
|
| 24 |
+
# In a real app, hash the password and save to DB
|
| 25 |
+
return user # password is automatically filtered out
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
Even though the handler returns the full `UserIn` object (which includes `password`), the `response_model=UserOut` declaration ensures that only `username` and `email` appear in the response. This is a critical security pattern -- it prevents accidental leakage of sensitive fields like passwords, tokens, or internal IDs.
|
| 29 |
+
|
| 30 |
+
## Status Codes
|
| 31 |
+
|
| 32 |
+
FastAPI provides the `status_code` parameter to set the HTTP response status code. Common codes include:
|
| 33 |
+
|
| 34 |
+
| Code | Constant | Usage |
|
| 35 |
+
|------|------------------------------------|----------------------|
|
| 36 |
+
| 200 | `status.HTTP_200_OK` | Successful GET |
|
| 37 |
+
| 201 | `status.HTTP_201_CREATED` | Successful creation |
|
| 38 |
+
| 204 | `status.HTTP_204_NO_CONTENT` | Successful deletion |
|
| 39 |
+
| 400 | `status.HTTP_400_BAD_REQUEST` | Client error |
|
| 40 |
+
| 404 | `status.HTTP_404_NOT_FOUND` | Resource not found |
|
| 41 |
+
| 422 | `status.HTTP_422_UNPROCESSABLE_ENTITY` | Validation error |
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
from fastapi import status
|
| 45 |
+
|
| 46 |
+
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 47 |
+
async def delete_item(item_id: int):
|
| 48 |
+
# delete logic
|
| 49 |
+
return None
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
The default `status_code` for all route decorators is `200`.
|
| 53 |
+
|
| 54 |
+
## Filtering Fields with response_model_include and response_model_exclude
|
| 55 |
+
|
| 56 |
+
You can dynamically control which fields appear in the response without creating a separate model:
|
| 57 |
+
|
| 58 |
+
```python
|
| 59 |
+
class Item(BaseModel):
|
| 60 |
+
name: str
|
| 61 |
+
description: str | None = None
|
| 62 |
+
price: float
|
| 63 |
+
tax: float = 0.0
|
| 64 |
+
internal_code: str = "N/A"
|
| 65 |
+
|
| 66 |
+
@app.get(
|
| 67 |
+
"/items/{item_id}",
|
| 68 |
+
response_model=Item,
|
| 69 |
+
response_model_exclude={"internal_code"},
|
| 70 |
+
)
|
| 71 |
+
async def read_item(item_id: int):
|
| 72 |
+
return {
|
| 73 |
+
"name": "Widget",
|
| 74 |
+
"description": "A useful widget",
|
| 75 |
+
"price": 35.99,
|
| 76 |
+
"tax": 3.60,
|
| 77 |
+
"internal_code": "WDG-001",
|
| 78 |
+
}
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
The `response_model_exclude` parameter accepts a `set` of field names to strip from the output. Similarly, `response_model_include` accepts a `set` of field names to keep -- all others are excluded. If both are provided, `response_model_include` is applied first, then `response_model_exclude` removes fields from that subset.
|
| 82 |
+
|
| 83 |
+
## Excluding Unset and Default Values
|
| 84 |
+
|
| 85 |
+
Two additional parameters control whether default or unset values appear in the response:
|
| 86 |
+
|
| 87 |
+
```python
|
| 88 |
+
@app.get(
|
| 89 |
+
"/items/{item_id}",
|
| 90 |
+
response_model=Item,
|
| 91 |
+
response_model_exclude_unset=True,
|
| 92 |
+
)
|
| 93 |
+
async def read_item(item_id: int):
|
| 94 |
+
return Item(name="Widget", price=35.99)
|
| 95 |
+
# Response: {"name": "Widget", "price": 35.99}
|
| 96 |
+
# Fields with defaults (description, tax) are omitted
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
- `response_model_exclude_unset=True` -- omits fields the user did not explicitly set (default: `False`)
|
| 100 |
+
- `response_model_exclude_defaults=True` -- omits fields whose value matches the default (default: `False`)
|
| 101 |
+
- `response_model_exclude_none=True` -- omits fields with `None` values (default: `False`)
|
| 102 |
+
|
| 103 |
+
## Multiple Response Models
|
| 104 |
+
|
| 105 |
+
Use `Union` types or the `responses` parameter to document endpoints that may return different shapes:
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
from typing import Union
|
| 109 |
+
|
| 110 |
+
class ItemPublic(BaseModel):
|
| 111 |
+
name: str
|
| 112 |
+
price: float
|
| 113 |
+
|
| 114 |
+
class ItemAdmin(BaseModel):
|
| 115 |
+
name: str
|
| 116 |
+
price: float
|
| 117 |
+
internal_code: str
|
| 118 |
+
profit_margin: float
|
| 119 |
+
|
| 120 |
+
@app.get("/items/{item_id}", response_model=Union[ItemAdmin, ItemPublic])
|
| 121 |
+
async def read_item(item_id: int, is_admin: bool = False):
|
| 122 |
+
item_data = get_item(item_id)
|
| 123 |
+
if is_admin:
|
| 124 |
+
return ItemAdmin(**item_data)
|
| 125 |
+
return ItemPublic(**item_data)
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
When using `Union`, Pydantic validates the response against each model in order and uses the first match. Place the more specific model first (the one with more fields) to avoid premature matching.
|
data/tech_docs/fastapi_security.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security and Authentication in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI provides integrated security utilities built on top of OpenAPI standards. It supports OAuth2, API keys, HTTP Basic/Bearer authentication, and OpenID Connect, with each scheme automatically reflected in the interactive documentation.
|
| 4 |
+
|
| 5 |
+
## OAuth2 with Password Flow
|
| 6 |
+
|
| 7 |
+
The most common authentication pattern uses OAuth2 "password" flow with JWT tokens:
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from fastapi import FastAPI, Depends, HTTPException, status
|
| 12 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 13 |
+
from jose import JWTError, jwt
|
| 14 |
+
from pydantic import BaseModel
|
| 15 |
+
|
| 16 |
+
app = FastAPI()
|
| 17 |
+
|
| 18 |
+
SECRET_KEY = "your-secret-key-at-least-32-characters-long"
|
| 19 |
+
ALGORITHM = "HS256"
|
| 20 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 21 |
+
|
| 22 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
| 23 |
+
|
| 24 |
+
class Token(BaseModel):
|
| 25 |
+
access_token: str
|
| 26 |
+
token_type: str
|
| 27 |
+
|
| 28 |
+
class User(BaseModel):
|
| 29 |
+
username: str
|
| 30 |
+
email: str | None = None
|
| 31 |
+
disabled: bool = False
|
| 32 |
+
|
| 33 |
+
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
| 34 |
+
to_encode = data.copy()
|
| 35 |
+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
|
| 36 |
+
to_encode.update({"exp": expire})
|
| 37 |
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 38 |
+
|
| 39 |
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
| 40 |
+
credentials_exception = HTTPException(
|
| 41 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 42 |
+
detail="Could not validate credentials",
|
| 43 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 44 |
+
)
|
| 45 |
+
try:
|
| 46 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 47 |
+
username: str = payload.get("sub")
|
| 48 |
+
if username is None:
|
| 49 |
+
raise credentials_exception
|
| 50 |
+
except JWTError:
|
| 51 |
+
raise credentials_exception
|
| 52 |
+
user = get_user_from_db(username)
|
| 53 |
+
if user is None:
|
| 54 |
+
raise credentials_exception
|
| 55 |
+
return user
|
| 56 |
+
|
| 57 |
+
@app.post("/token", response_model=Token)
|
| 58 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
| 59 |
+
user = authenticate_user(form_data.username, form_data.password)
|
| 60 |
+
if not user:
|
| 61 |
+
raise HTTPException(
|
| 62 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 63 |
+
detail="Incorrect username or password",
|
| 64 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 65 |
+
)
|
| 66 |
+
access_token = create_access_token(
|
| 67 |
+
data={"sub": user.username},
|
| 68 |
+
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
|
| 69 |
+
)
|
| 70 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
The `OAuth2PasswordBearer(tokenUrl="token")` declaration tells FastAPI that the client obtains a token by sending credentials to the `/token` endpoint. The `tokenUrl` is relative to the application root. The token is then sent in subsequent requests via the `Authorization: Bearer <token>` header.
|
| 74 |
+
|
| 75 |
+
## HTTP Bearer Authentication
|
| 76 |
+
|
| 77 |
+
For simpler token-based auth without the full OAuth2 flow:
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 81 |
+
|
| 82 |
+
security = HTTPBearer()
|
| 83 |
+
|
| 84 |
+
@app.get("/protected/")
|
| 85 |
+
async def protected_route(
|
| 86 |
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 87 |
+
):
|
| 88 |
+
token = credentials.credentials
|
| 89 |
+
# validate token
|
| 90 |
+
return {"token": token, "scheme": credentials.scheme}
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
`HTTPBearer` extracts the token from the `Authorization: Bearer <token>` header. If the header is missing or does not use the Bearer scheme, FastAPI returns a 403 Forbidden response automatically.
|
| 94 |
+
|
| 95 |
+
## API Key Authentication
|
| 96 |
+
|
| 97 |
+
API keys can be passed via headers, query parameters, or cookies:
|
| 98 |
+
|
| 99 |
+
```python
|
| 100 |
+
from fastapi.security import APIKeyHeader, APIKeyQuery
|
| 101 |
+
|
| 102 |
+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True)
|
| 103 |
+
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
|
| 104 |
+
|
| 105 |
+
async def get_api_key(
|
| 106 |
+
header_key: str | None = Depends(api_key_header),
|
| 107 |
+
query_key: str | None = Depends(api_key_query),
|
| 108 |
+
):
|
| 109 |
+
if header_key == "valid-api-key-12345":
|
| 110 |
+
return header_key
|
| 111 |
+
if query_key == "valid-api-key-12345":
|
| 112 |
+
return query_key
|
| 113 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 114 |
+
|
| 115 |
+
@app.get("/data/", dependencies=[Depends(get_api_key)])
|
| 116 |
+
async def read_data():
|
| 117 |
+
return {"data": "sensitive information"}
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
The `auto_error=True` parameter (the default) causes FastAPI to return an automatic 403 error when the key is missing. Setting `auto_error=False` allows the dependency to return `None` instead, letting you check multiple sources.
|
| 121 |
+
|
| 122 |
+
## OAuth2 Scopes
|
| 123 |
+
|
| 124 |
+
Scopes provide fine-grained permission control:
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
from fastapi.security import SecurityScopes
|
| 128 |
+
|
| 129 |
+
oauth2_scheme = OAuth2PasswordBearer(
|
| 130 |
+
tokenUrl="token",
|
| 131 |
+
scopes={
|
| 132 |
+
"items:read": "Read items",
|
| 133 |
+
"items:write": "Create and update items",
|
| 134 |
+
"admin": "Full administrative access",
|
| 135 |
+
},
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
async def get_current_user(
|
| 139 |
+
security_scopes: SecurityScopes,
|
| 140 |
+
token: str = Depends(oauth2_scheme),
|
| 141 |
+
):
|
| 142 |
+
# Decode token and verify required scopes
|
| 143 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 144 |
+
token_scopes = payload.get("scopes", [])
|
| 145 |
+
for scope in security_scopes.scopes:
|
| 146 |
+
if scope not in token_scopes:
|
| 147 |
+
raise HTTPException(status_code=403, detail="Not enough permissions")
|
| 148 |
+
return get_user_from_db(payload.get("sub"))
|
| 149 |
+
|
| 150 |
+
@app.get("/items/", dependencies=[Depends(Security(get_current_user, scopes=["items:read"]))])
|
| 151 |
+
async def read_items():
|
| 152 |
+
return [{"item": "Widget"}]
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
Each endpoint declares the scopes it requires, and the dependency verifies the token contains all necessary permissions before allowing access.
|
data/tech_docs/fastapi_testing.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing FastAPI Applications
|
| 2 |
+
|
| 3 |
+
FastAPI applications are tested using the `TestClient` class, which provides a synchronous interface for sending requests to your application without running an actual server. For async testing, use `httpx.AsyncClient`.
|
| 4 |
+
|
| 5 |
+
## Basic Testing with TestClient
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI
|
| 9 |
+
from fastapi.testclient import TestClient
|
| 10 |
+
|
| 11 |
+
app = FastAPI()
|
| 12 |
+
|
| 13 |
+
@app.get("/items/{item_id}")
|
| 14 |
+
async def read_item(item_id: int, q: str = None):
|
| 15 |
+
result = {"item_id": item_id}
|
| 16 |
+
if q:
|
| 17 |
+
result["q"] = q
|
| 18 |
+
return result
|
| 19 |
+
|
| 20 |
+
client = TestClient(app)
|
| 21 |
+
|
| 22 |
+
def test_read_item():
|
| 23 |
+
response = client.get("/items/42?q=test")
|
| 24 |
+
assert response.status_code == 200
|
| 25 |
+
assert response.json() == {"item_id": 42, "q": "test"}
|
| 26 |
+
|
| 27 |
+
def test_read_item_not_found():
|
| 28 |
+
response = client.get("/items/abc")
|
| 29 |
+
assert response.status_code == 422 # Validation error
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
The `TestClient` is built on top of `httpx` (which replaced `requests` as of Starlette 0.20.0). It supports all HTTP methods: `client.get()`, `client.post()`, `client.put()`, `client.delete()`, `client.patch()`, `client.options()`, and `client.head()`.
|
| 33 |
+
|
| 34 |
+
## Pytest Fixtures
|
| 35 |
+
|
| 36 |
+
Use fixtures to share the `TestClient` and set up test data:
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
import pytest
|
| 40 |
+
from fastapi import FastAPI
|
| 41 |
+
from fastapi.testclient import TestClient
|
| 42 |
+
|
| 43 |
+
from myapp.main import app
|
| 44 |
+
from myapp.database import Base, engine
|
| 45 |
+
|
| 46 |
+
@pytest.fixture(scope="module")
|
| 47 |
+
def client():
|
| 48 |
+
Base.metadata.create_all(bind=engine)
|
| 49 |
+
with TestClient(app) as c:
|
| 50 |
+
yield c
|
| 51 |
+
Base.metadata.drop_all(bind=engine)
|
| 52 |
+
|
| 53 |
+
@pytest.fixture
|
| 54 |
+
def auth_headers():
|
| 55 |
+
return {"Authorization": "Bearer test-token-12345"}
|
| 56 |
+
|
| 57 |
+
def test_create_item(client, auth_headers):
|
| 58 |
+
response = client.post(
|
| 59 |
+
"/items/",
|
| 60 |
+
json={"name": "Widget", "price": 35.99},
|
| 61 |
+
headers=auth_headers,
|
| 62 |
+
)
|
| 63 |
+
assert response.status_code == 201
|
| 64 |
+
data = response.json()
|
| 65 |
+
assert data["name"] == "Widget"
|
| 66 |
+
assert "id" in data
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Using `scope="module"` means the fixture is created once per test module rather than once per test function, improving performance when database setup is expensive. The `with` statement ensures proper cleanup of the test client's underlying transport.
|
| 70 |
+
|
| 71 |
+
## Overriding Dependencies in Tests
|
| 72 |
+
|
| 73 |
+
Override dependencies to inject mock services or test databases:
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
from fastapi import FastAPI, Depends
|
| 77 |
+
|
| 78 |
+
app = FastAPI()
|
| 79 |
+
|
| 80 |
+
async def get_db():
|
| 81 |
+
db = ProductionDatabase()
|
| 82 |
+
try:
|
| 83 |
+
yield db
|
| 84 |
+
finally:
|
| 85 |
+
db.close()
|
| 86 |
+
|
| 87 |
+
@app.get("/items/")
|
| 88 |
+
async def read_items(db=Depends(get_db)):
|
| 89 |
+
return db.query_all_items()
|
| 90 |
+
|
| 91 |
+
# In your test file:
|
| 92 |
+
def get_test_db():
|
| 93 |
+
db = TestDatabase()
|
| 94 |
+
try:
|
| 95 |
+
yield db
|
| 96 |
+
finally:
|
| 97 |
+
db.close()
|
| 98 |
+
|
| 99 |
+
app.dependency_overrides[get_db] = get_test_db
|
| 100 |
+
|
| 101 |
+
client = TestClient(app)
|
| 102 |
+
|
| 103 |
+
def test_read_items():
|
| 104 |
+
response = client.get("/items/")
|
| 105 |
+
assert response.status_code == 200
|
| 106 |
+
|
| 107 |
+
# Clean up overrides after tests
|
| 108 |
+
app.dependency_overrides.clear()
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
The `app.dependency_overrides` dictionary maps original dependencies to their replacements. This works for any dependency in the chain, including sub-dependencies. Always call `app.dependency_overrides.clear()` after tests to prevent overrides from leaking between test modules.
|
| 112 |
+
|
| 113 |
+
## Async Testing with httpx
|
| 114 |
+
|
| 115 |
+
For testing async-specific behavior (e.g., async database calls, WebSocket-related setup), use `httpx.AsyncClient` with `pytest-asyncio`:
|
| 116 |
+
|
| 117 |
+
```python
|
| 118 |
+
import pytest
|
| 119 |
+
from httpx import AsyncClient, ASGITransport
|
| 120 |
+
from myapp.main import app
|
| 121 |
+
|
| 122 |
+
@pytest.mark.anyio
|
| 123 |
+
async def test_read_items_async():
|
| 124 |
+
transport = ASGITransport(app=app)
|
| 125 |
+
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
| 126 |
+
response = await client.get("/items/")
|
| 127 |
+
assert response.status_code == 200
|
| 128 |
+
|
| 129 |
+
@pytest.mark.anyio
|
| 130 |
+
async def test_create_item_async():
|
| 131 |
+
transport = ASGITransport(app=app)
|
| 132 |
+
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
| 133 |
+
response = await client.post(
|
| 134 |
+
"/items/",
|
| 135 |
+
json={"name": "Widget", "price": 35.99},
|
| 136 |
+
)
|
| 137 |
+
assert response.status_code == 201
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
The `ASGITransport` connects `httpx` directly to the ASGI application without network overhead. The `base_url` parameter is required but can be any valid URL since no real network requests are made. Install the async test dependencies with `pip install httpx pytest-asyncio` (or use `anyio` with the `@pytest.mark.anyio` marker).
|
| 141 |
+
|
| 142 |
+
## Testing WebSockets
|
| 143 |
+
|
| 144 |
+
```python
|
| 145 |
+
def test_websocket():
|
| 146 |
+
client = TestClient(app)
|
| 147 |
+
with client.websocket_connect("/ws") as websocket:
|
| 148 |
+
websocket.send_text("hello")
|
| 149 |
+
data = websocket.receive_text()
|
| 150 |
+
assert data == "Message received: hello"
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
The `websocket_connect` context manager establishes a WebSocket connection. It supports `send_text()`, `send_json()`, `send_bytes()`, `receive_text()`, `receive_json()`, and `receive_bytes()` methods.
|
data/tech_docs/fastapi_websockets.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# WebSockets in FastAPI
|
| 2 |
+
|
| 3 |
+
FastAPI supports WebSocket connections through Starlette's WebSocket implementation, enabling full-duplex, bidirectional communication between clients and servers. WebSockets are ideal for real-time features such as chat applications, live dashboards, and streaming updates.
|
| 4 |
+
|
| 5 |
+
## Basic WebSocket Endpoint
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
from fastapi import FastAPI, WebSocket
|
| 9 |
+
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
|
| 12 |
+
@app.websocket("/ws")
|
| 13 |
+
async def websocket_endpoint(ws: WebSocket):
|
| 14 |
+
await ws.accept()
|
| 15 |
+
while True:
|
| 16 |
+
data = await ws.receive_text()
|
| 17 |
+
await ws.send_text(f"Echo: {data}")
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
The `@app.websocket()` decorator registers a WebSocket route. The handler receives a `WebSocket` object, which must be explicitly accepted by calling `await ws.accept()` before any data can be sent or received. The `accept()` method sends the HTTP 101 Switching Protocols response to the client.
|
| 21 |
+
|
| 22 |
+
## Send and Receive Methods
|
| 23 |
+
|
| 24 |
+
The `WebSocket` object provides several methods for communication:
|
| 25 |
+
|
| 26 |
+
| Method | Description |
|
| 27 |
+
|---------------------|------------------------------------------|
|
| 28 |
+
| `receive_text()` | Receive a text (string) message |
|
| 29 |
+
| `receive_bytes()` | Receive a binary message |
|
| 30 |
+
| `receive_json()` | Receive and parse a JSON message |
|
| 31 |
+
| `send_text(data)` | Send a text message |
|
| 32 |
+
| `send_bytes(data)` | Send binary data |
|
| 33 |
+
| `send_json(data)` | Serialize and send a JSON message |
|
| 34 |
+
| `close(code=1000)` | Close the connection with a status code |
|
| 35 |
+
|
| 36 |
+
The default close code is `1000` (normal closure). Other common codes are `1001` (going away), `1008` (policy violation), and `1011` (unexpected condition). The maximum WebSocket message size defaults to 16 MB in Uvicorn, configurable via the `--ws-max-size` flag.
|
| 37 |
+
|
| 38 |
+
## Handling Disconnects
|
| 39 |
+
|
| 40 |
+
Clients can disconnect at any time. Handle this with `WebSocketDisconnect`:
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
| 44 |
+
|
| 45 |
+
app = FastAPI()
|
| 46 |
+
|
| 47 |
+
class ConnectionManager:
|
| 48 |
+
def __init__(self):
|
| 49 |
+
self.active_connections: list[WebSocket] = []
|
| 50 |
+
|
| 51 |
+
async def connect(self, websocket: WebSocket):
|
| 52 |
+
await websocket.accept()
|
| 53 |
+
self.active_connections.append(websocket)
|
| 54 |
+
|
| 55 |
+
def disconnect(self, websocket: WebSocket):
|
| 56 |
+
self.active_connections.remove(websocket)
|
| 57 |
+
|
| 58 |
+
async def broadcast(self, message: str):
|
| 59 |
+
for connection in self.active_connections:
|
| 60 |
+
await connection.send_text(message)
|
| 61 |
+
|
| 62 |
+
manager = ConnectionManager()
|
| 63 |
+
|
| 64 |
+
@app.websocket("/ws/chat")
|
| 65 |
+
async def chat_endpoint(ws: WebSocket):
|
| 66 |
+
await manager.connect(ws)
|
| 67 |
+
try:
|
| 68 |
+
while True:
|
| 69 |
+
data = await ws.receive_text()
|
| 70 |
+
await manager.broadcast(f"User says: {data}")
|
| 71 |
+
except WebSocketDisconnect:
|
| 72 |
+
manager.disconnect(ws)
|
| 73 |
+
await manager.broadcast("A user has left the chat")
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
The `WebSocketDisconnect` exception is raised when `receive_text()`, `receive_bytes()`, or `receive_json()` detects that the client has closed the connection. The exception has a `code` attribute containing the close code sent by the client.
|
| 77 |
+
|
| 78 |
+
## WebSocket with Path Parameters and Dependencies
|
| 79 |
+
|
| 80 |
+
WebSocket endpoints support path parameters, query parameters, and dependency injection:
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
from fastapi import FastAPI, WebSocket, Depends, Query, Path, Cookie, Header
|
| 84 |
+
|
| 85 |
+
app = FastAPI()
|
| 86 |
+
|
| 87 |
+
async def get_token(
|
| 88 |
+
websocket: WebSocket,
|
| 89 |
+
token: str | None = Query(default=None),
|
| 90 |
+
x_token: str | None = Header(default=None),
|
| 91 |
+
):
|
| 92 |
+
if token is None and x_token is None:
|
| 93 |
+
await websocket.close(code=1008)
|
| 94 |
+
return None
|
| 95 |
+
return token or x_token
|
| 96 |
+
|
| 97 |
+
@app.websocket("/ws/{room_id}")
|
| 98 |
+
async def room_websocket(
|
| 99 |
+
ws: WebSocket,
|
| 100 |
+
room_id: int = Path(ge=1, le=1000),
|
| 101 |
+
token: str | None = Depends(get_token),
|
| 102 |
+
):
|
| 103 |
+
if token is None:
|
| 104 |
+
return
|
| 105 |
+
await ws.accept()
|
| 106 |
+
await ws.send_text(f"Connected to room {room_id}")
|
| 107 |
+
try:
|
| 108 |
+
while True:
|
| 109 |
+
data = await ws.receive_text()
|
| 110 |
+
await ws.send_text(f"[Room {room_id}] {data}")
|
| 111 |
+
except WebSocketDisconnect:
|
| 112 |
+
pass
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Dependencies for WebSocket endpoints work the same as for HTTP endpoints, including `Depends()`, `Path()`, `Query()`, `Header()`, and `Cookie()`. However, WebSocket endpoints do not support `Body()` parameters since WebSocket communication uses its own message protocol rather than HTTP request bodies.
|
| 116 |
+
|
| 117 |
+
## WebSocket with JSON Messages
|
| 118 |
+
|
| 119 |
+
For structured communication, use JSON messages with Pydantic validation:
|
| 120 |
+
|
| 121 |
+
```python
|
| 122 |
+
from pydantic import BaseModel, ValidationError
|
| 123 |
+
|
| 124 |
+
class ChatMessage(BaseModel):
|
| 125 |
+
username: str
|
| 126 |
+
content: str
|
| 127 |
+
channel: str = "general"
|
| 128 |
+
|
| 129 |
+
@app.websocket("/ws/json")
|
| 130 |
+
async def json_websocket(ws: WebSocket):
|
| 131 |
+
await ws.accept()
|
| 132 |
+
try:
|
| 133 |
+
while True:
|
| 134 |
+
raw_data = await ws.receive_json()
|
| 135 |
+
try:
|
| 136 |
+
message = ChatMessage(**raw_data)
|
| 137 |
+
await ws.send_json({
|
| 138 |
+
"status": "ok",
|
| 139 |
+
"echo": message.model_dump(),
|
| 140 |
+
})
|
| 141 |
+
except ValidationError as e:
|
| 142 |
+
await ws.send_json({
|
| 143 |
+
"status": "error",
|
| 144 |
+
"errors": e.errors(),
|
| 145 |
+
})
|
| 146 |
+
except WebSocketDisconnect:
|
| 147 |
+
pass
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
The `receive_json()` method parses the incoming text message as JSON. If the message is not valid JSON, it raises a `json.JSONDecodeError`. Pydantic validation is applied manually since FastAPI does not automatically validate WebSocket message payloads the way it validates HTTP request bodies.
|
pyproject.toml
CHANGED
|
@@ -11,7 +11,7 @@ dependencies = [
|
|
| 11 |
"pydantic>=2.9.0",
|
| 12 |
"pydantic-settings>=2.5.0",
|
| 13 |
"pyyaml>=6.0",
|
| 14 |
-
"sentence-transformers>=3.0.0",
|
| 15 |
"faiss-cpu>=1.8.0",
|
| 16 |
"rank-bm25>=0.2.2",
|
| 17 |
"structlog>=24.0.0",
|
|
|
|
| 11 |
"pydantic>=2.9.0",
|
| 12 |
"pydantic-settings>=2.5.0",
|
| 13 |
"pyyaml>=6.0",
|
| 14 |
+
"sentence-transformers>=3.0.0,<5.0.0",
|
| 15 |
"faiss-cpu>=1.8.0",
|
| 16 |
"rank-bm25>=0.2.2",
|
| 17 |
"structlog>=24.0.0",
|
scripts/ingest.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ingest documents into the hybrid vector store.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
python scripts/ingest.py --config configs/tasks/tech_docs.yaml
|
| 5 |
+
python scripts/ingest.py --doc-dir data/tech_docs/ --store-path .cache/store
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import sys
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
# Ensure the package is importable when running as a script
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 16 |
+
|
| 17 |
+
from agent_bench.rag.chunker import chunk_text
|
| 18 |
+
from agent_bench.rag.embedder import Embedder
|
| 19 |
+
from agent_bench.rag.store import HybridStore
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def ingest(
|
| 23 |
+
doc_dir: str,
|
| 24 |
+
store_path: str,
|
| 25 |
+
chunk_strategy: str = "recursive",
|
| 26 |
+
chunk_size: int = 512,
|
| 27 |
+
chunk_overlap: int = 64,
|
| 28 |
+
model_name: str = "all-MiniLM-L6-v2",
|
| 29 |
+
cache_dir: str = ".cache/embeddings",
|
| 30 |
+
) -> None:
|
| 31 |
+
"""Ingest all markdown files from doc_dir into a HybridStore."""
|
| 32 |
+
doc_path = Path(doc_dir)
|
| 33 |
+
if not doc_path.exists():
|
| 34 |
+
print(f"Error: document directory {doc_dir} does not exist")
|
| 35 |
+
sys.exit(1)
|
| 36 |
+
|
| 37 |
+
md_files = sorted(doc_path.glob("*.md"))
|
| 38 |
+
if not md_files:
|
| 39 |
+
print(f"Error: no markdown files found in {doc_dir}")
|
| 40 |
+
sys.exit(1)
|
| 41 |
+
|
| 42 |
+
print(f"Found {len(md_files)} markdown files in {doc_dir}")
|
| 43 |
+
|
| 44 |
+
# Chunk all documents
|
| 45 |
+
all_chunks = []
|
| 46 |
+
for md_file in md_files:
|
| 47 |
+
text = md_file.read_text(encoding="utf-8")
|
| 48 |
+
source = md_file.name # bare filename
|
| 49 |
+
chunks = chunk_text(
|
| 50 |
+
text, source, strategy=chunk_strategy, chunk_size=chunk_size, chunk_overlap=chunk_overlap
|
| 51 |
+
)
|
| 52 |
+
print(f" {source}: {len(chunks)} chunks")
|
| 53 |
+
all_chunks.extend(chunks)
|
| 54 |
+
|
| 55 |
+
print(f"Total chunks: {len(all_chunks)}")
|
| 56 |
+
|
| 57 |
+
# Embed
|
| 58 |
+
print(f"Embedding with {model_name}...")
|
| 59 |
+
embedder = Embedder(model_name=model_name, cache_dir=cache_dir)
|
| 60 |
+
texts = [c.content for c in all_chunks]
|
| 61 |
+
embeddings = embedder.embed_batch(texts)
|
| 62 |
+
print(f"Embeddings shape: {embeddings.shape}")
|
| 63 |
+
|
| 64 |
+
# Store
|
| 65 |
+
store = HybridStore(dimension=embeddings.shape[1])
|
| 66 |
+
store.add(all_chunks, embeddings)
|
| 67 |
+
store.save(store_path)
|
| 68 |
+
|
| 69 |
+
stats = store.stats()
|
| 70 |
+
print(f"Store saved to {store_path}")
|
| 71 |
+
print(f" Chunks: {stats.total_chunks}")
|
| 72 |
+
print(f" FAISS index size: {stats.faiss_index_size}")
|
| 73 |
+
print(f" Unique sources: {stats.unique_sources}")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def main() -> None:
|
| 77 |
+
parser = argparse.ArgumentParser(description="Ingest documents into vector store")
|
| 78 |
+
parser.add_argument("--doc-dir", default="data/tech_docs/", help="Document directory")
|
| 79 |
+
parser.add_argument("--store-path", default=".cache/store", help="Store output path")
|
| 80 |
+
parser.add_argument("--chunk-strategy", default="recursive", choices=["recursive", "fixed"])
|
| 81 |
+
parser.add_argument("--chunk-size", type=int, default=512)
|
| 82 |
+
parser.add_argument("--chunk-overlap", type=int, default=64)
|
| 83 |
+
parser.add_argument("--model", default="all-MiniLM-L6-v2", help="Embedding model name")
|
| 84 |
+
parser.add_argument("--cache-dir", default=".cache/embeddings", help="Embedding cache dir")
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--config", default=None, help="Task config YAML (overrides other args for doc-dir)"
|
| 87 |
+
)
|
| 88 |
+
args = parser.parse_args()
|
| 89 |
+
|
| 90 |
+
doc_dir = args.doc_dir
|
| 91 |
+
if args.config:
|
| 92 |
+
from agent_bench.core.config import load_task_config
|
| 93 |
+
|
| 94 |
+
task = load_task_config(Path(args.config).stem, path=Path(args.config))
|
| 95 |
+
doc_dir = task.document_dir
|
| 96 |
+
|
| 97 |
+
ingest(
|
| 98 |
+
doc_dir=doc_dir,
|
| 99 |
+
store_path=args.store_path,
|
| 100 |
+
chunk_strategy=args.chunk_strategy,
|
| 101 |
+
chunk_size=args.chunk_size,
|
| 102 |
+
chunk_overlap=args.chunk_overlap,
|
| 103 |
+
model_name=args.model,
|
| 104 |
+
cache_dir=args.cache_dir,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
main()
|