Nomearod Claude Opus 4.6 (1M context) commited on
Commit
a152b95
·
1 Parent(s): 77bdc95

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 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()