codeforge-dev 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Pydantic v2 Models -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. Computed Fields
|
|
4
|
+
|
|
5
|
+
Computed fields are derived from other model fields at serialization time. They appear in JSON output but are not accepted in input:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pydantic import BaseModel, computed_field
|
|
9
|
+
|
|
10
|
+
class Product(BaseModel):
|
|
11
|
+
price: float
|
|
12
|
+
tax_rate: float = 0.08
|
|
13
|
+
|
|
14
|
+
@computed_field
|
|
15
|
+
@property
|
|
16
|
+
def total(self) -> float:
|
|
17
|
+
return self.price * (1 + self.tax_rate)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
p = Product(price=100)
|
|
22
|
+
p.model_dump()
|
|
23
|
+
# {"price": 100.0, "tax_rate": 0.08, "total": 108.0}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Computed fields participate in JSON Schema generation, appearing in OpenAPI docs with their return type.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Model Inheritance
|
|
31
|
+
|
|
32
|
+
### Shared Base Models
|
|
33
|
+
|
|
34
|
+
Factor common fields into a base class. Input and output models inherit from the base and add their specific fields:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
class UserBase(BaseModel):
|
|
38
|
+
email: str
|
|
39
|
+
display_name: str | None = None
|
|
40
|
+
|
|
41
|
+
class UserCreate(UserBase):
|
|
42
|
+
password: str
|
|
43
|
+
|
|
44
|
+
class UserUpdate(BaseModel):
|
|
45
|
+
email: str | None = None
|
|
46
|
+
display_name: str | None = None
|
|
47
|
+
|
|
48
|
+
class UserResponse(UserBase):
|
|
49
|
+
model_config = {"from_attributes": True}
|
|
50
|
+
|
|
51
|
+
id: int
|
|
52
|
+
created_at: datetime
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Partial Models for PATCH
|
|
56
|
+
|
|
57
|
+
Create update models where all fields are optional. This supports partial updates without requiring the client to send the full object:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pydantic import BaseModel
|
|
61
|
+
|
|
62
|
+
class ItemBase(BaseModel):
|
|
63
|
+
name: str
|
|
64
|
+
description: str
|
|
65
|
+
price: float
|
|
66
|
+
|
|
67
|
+
class ItemUpdate(BaseModel):
|
|
68
|
+
name: str | None = None
|
|
69
|
+
description: str | None = None
|
|
70
|
+
price: float | None = None
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `model.model_dump(exclude_unset=True)` to get only the fields the client explicitly provided:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
@router.patch("/{item_id}")
|
|
77
|
+
async def update_item(item_id: int, updates: ItemUpdate, db: DB):
|
|
78
|
+
update_data = updates.model_dump(exclude_unset=True)
|
|
79
|
+
await db.execute(
|
|
80
|
+
update(Item).where(Item.id == item_id).values(**update_data)
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 3. Custom JSON Encoders
|
|
87
|
+
|
|
88
|
+
Pydantic v2 uses `model_serializer` and `field_serializer` for custom encoding:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pydantic import BaseModel, field_serializer
|
|
92
|
+
from datetime import datetime
|
|
93
|
+
|
|
94
|
+
class Event(BaseModel):
|
|
95
|
+
name: str
|
|
96
|
+
timestamp: datetime
|
|
97
|
+
|
|
98
|
+
@field_serializer("timestamp")
|
|
99
|
+
def serialize_timestamp(self, dt: datetime, _info) -> str:
|
|
100
|
+
return dt.isoformat()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For model-wide custom serialization:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pydantic import model_serializer
|
|
107
|
+
|
|
108
|
+
class Point(BaseModel):
|
|
109
|
+
x: float
|
|
110
|
+
y: float
|
|
111
|
+
|
|
112
|
+
@model_serializer
|
|
113
|
+
def serialize_model(self) -> dict:
|
|
114
|
+
return {"coordinates": [self.x, self.y]}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 4. Discriminated Unions
|
|
120
|
+
|
|
121
|
+
Use a literal discriminator field to enable efficient parsing of polymorphic types. Pydantic checks the discriminator value first, then validates against the matching model:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from typing import Literal, Union, Annotated
|
|
125
|
+
from pydantic import BaseModel, Field
|
|
126
|
+
|
|
127
|
+
class TextBlock(BaseModel):
|
|
128
|
+
type: Literal["text"] = "text"
|
|
129
|
+
content: str
|
|
130
|
+
|
|
131
|
+
class ImageBlock(BaseModel):
|
|
132
|
+
type: Literal["image"] = "image"
|
|
133
|
+
url: str
|
|
134
|
+
alt_text: str | None = None
|
|
135
|
+
|
|
136
|
+
class CodeBlock(BaseModel):
|
|
137
|
+
type: Literal["code"] = "code"
|
|
138
|
+
language: str
|
|
139
|
+
source: str
|
|
140
|
+
|
|
141
|
+
Block = Annotated[
|
|
142
|
+
Union[TextBlock, ImageBlock, CodeBlock],
|
|
143
|
+
Field(discriminator="type"),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
class Document(BaseModel):
|
|
147
|
+
title: str
|
|
148
|
+
blocks: list[Block]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Benefits of discriminated unions:
|
|
152
|
+
- Validation errors reference the specific model variant, not a generic union failure.
|
|
153
|
+
- Performance is constant-time (dict lookup by discriminator), not linear (try each variant).
|
|
154
|
+
- OpenAPI schema uses `oneOf` with `discriminator`, enabling typed client generation.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 5. Validators and Constraints
|
|
159
|
+
|
|
160
|
+
### Field-Level Validators
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pydantic import field_validator
|
|
164
|
+
|
|
165
|
+
class Registration(BaseModel):
|
|
166
|
+
username: str
|
|
167
|
+
age: int
|
|
168
|
+
|
|
169
|
+
@field_validator("username")
|
|
170
|
+
@classmethod
|
|
171
|
+
def username_alphanumeric(cls, v: str) -> str:
|
|
172
|
+
if not v.isalnum():
|
|
173
|
+
raise ValueError("must be alphanumeric")
|
|
174
|
+
return v
|
|
175
|
+
|
|
176
|
+
@field_validator("age")
|
|
177
|
+
@classmethod
|
|
178
|
+
def age_range(cls, v: int) -> int:
|
|
179
|
+
if v < 13 or v > 120:
|
|
180
|
+
raise ValueError("must be between 13 and 120")
|
|
181
|
+
return v
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Model-Level Validators
|
|
185
|
+
|
|
186
|
+
Validate relationships between fields using `model_validator`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from pydantic import model_validator
|
|
190
|
+
|
|
191
|
+
class DateRange(BaseModel):
|
|
192
|
+
start: datetime
|
|
193
|
+
end: datetime
|
|
194
|
+
|
|
195
|
+
@model_validator(mode="after")
|
|
196
|
+
def validate_range(self) -> "DateRange":
|
|
197
|
+
if self.end <= self.start:
|
|
198
|
+
raise ValueError("end must be after start")
|
|
199
|
+
return self
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Field Constraints
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from pydantic import Field
|
|
206
|
+
|
|
207
|
+
class Product(BaseModel):
|
|
208
|
+
name: str = Field(min_length=1, max_length=200)
|
|
209
|
+
price: float = Field(gt=0, description="Price in USD")
|
|
210
|
+
sku: str = Field(pattern=r"^[A-Z]{2}-\d{4}$")
|
|
211
|
+
tags: list[str] = Field(default_factory=list, max_length=10)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 6. BaseSettings for Configuration
|
|
217
|
+
|
|
218
|
+
`BaseSettings` reads values from environment variables, `.env` files, and constructor arguments with a defined priority:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pydantic_settings import BaseSettings
|
|
222
|
+
|
|
223
|
+
class Settings(BaseSettings):
|
|
224
|
+
model_config = {"env_file": ".env", "env_prefix": "APP_"}
|
|
225
|
+
|
|
226
|
+
database_url: str
|
|
227
|
+
redis_url: str = "redis://localhost:6379"
|
|
228
|
+
debug: bool = False
|
|
229
|
+
allowed_origins: list[str] = ["http://localhost:3000"]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# Usage in FastAPI
|
|
234
|
+
from functools import lru_cache
|
|
235
|
+
|
|
236
|
+
@lru_cache
|
|
237
|
+
def get_settings() -> Settings:
|
|
238
|
+
return Settings()
|
|
239
|
+
|
|
240
|
+
@app.get("/info")
|
|
241
|
+
async def info(settings: Settings = Depends(get_settings)):
|
|
242
|
+
return {"debug": settings.debug}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Priority order (highest to lowest): constructor arguments, environment variables, `.env` file, field defaults. Use `lru_cache` to parse settings once rather than on every request.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Routing and Dependencies -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. APIRouter Organization
|
|
4
|
+
|
|
5
|
+
Structure routers by domain, one per module. Each router declares its own prefix, tags, and shared dependencies:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# app/routers/users.py
|
|
9
|
+
from fastapi import APIRouter, Depends
|
|
10
|
+
from app.dependencies import get_current_user
|
|
11
|
+
|
|
12
|
+
router = APIRouter(
|
|
13
|
+
prefix="/users",
|
|
14
|
+
tags=["users"],
|
|
15
|
+
dependencies=[Depends(get_current_user)],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@router.get("/me")
|
|
19
|
+
async def read_current_user(user: User = Depends(get_current_user)):
|
|
20
|
+
return user
|
|
21
|
+
|
|
22
|
+
@router.get("/{user_id}")
|
|
23
|
+
async def read_user(user_id: int):
|
|
24
|
+
return await fetch_user(user_id)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# app/main.py
|
|
29
|
+
from fastapi import FastAPI
|
|
30
|
+
from app.routers import users, items, orders
|
|
31
|
+
|
|
32
|
+
app = FastAPI()
|
|
33
|
+
app.include_router(users.router)
|
|
34
|
+
app.include_router(items.router)
|
|
35
|
+
app.include_router(orders.router)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Router-Level Dependencies
|
|
39
|
+
|
|
40
|
+
Dependencies declared at the router level apply to every endpoint in that router. This is ideal for authentication guards -- every endpoint in the router requires a valid user without repeating the dependency:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
router = APIRouter(
|
|
44
|
+
prefix="/admin",
|
|
45
|
+
dependencies=[Depends(require_admin_role)],
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Override a router-level dependency at the endpoint level by re-declaring the same parameter type with a different `Depends()`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
@router.get("/public", dependencies=[])
|
|
53
|
+
async def public_endpoint():
|
|
54
|
+
return {"message": "no auth required"}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Path Operation Configuration
|
|
60
|
+
|
|
61
|
+
### Response Model Filtering
|
|
62
|
+
|
|
63
|
+
Use `response_model_exclude_unset` to omit fields the client did not send, useful for PATCH-style partial updates:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
@router.patch("/{item_id}", response_model=ItemResponse)
|
|
67
|
+
async def update_item(
|
|
68
|
+
item_id: int,
|
|
69
|
+
updates: ItemUpdate,
|
|
70
|
+
response_model_exclude_unset=True,
|
|
71
|
+
):
|
|
72
|
+
return await apply_updates(item_id, updates)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Multiple Response Models
|
|
76
|
+
|
|
77
|
+
Declare alternative responses for documentation and client generation:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from fastapi import status
|
|
81
|
+
|
|
82
|
+
@router.post(
|
|
83
|
+
"/",
|
|
84
|
+
response_model=ItemResponse,
|
|
85
|
+
status_code=status.HTTP_201_CREATED,
|
|
86
|
+
responses={
|
|
87
|
+
409: {"model": ErrorResponse, "description": "Item already exists"},
|
|
88
|
+
422: {"model": ValidationErrorResponse},
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
async def create_item(item: ItemCreate):
|
|
92
|
+
...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Tags and Deprecation
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
@router.get("/legacy", deprecated=True, tags=["legacy"])
|
|
99
|
+
async def legacy_endpoint():
|
|
100
|
+
...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 3. Nested and Overridden Dependencies
|
|
106
|
+
|
|
107
|
+
### Nested Dependencies
|
|
108
|
+
|
|
109
|
+
Dependencies can depend on other dependencies. FastAPI resolves the full graph, caching each dependency once per request:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
async def get_db():
|
|
113
|
+
db = SessionLocal()
|
|
114
|
+
try:
|
|
115
|
+
yield db
|
|
116
|
+
finally:
|
|
117
|
+
await db.close()
|
|
118
|
+
|
|
119
|
+
async def get_user_repo(db: AsyncSession = Depends(get_db)):
|
|
120
|
+
return UserRepository(db)
|
|
121
|
+
|
|
122
|
+
async def get_current_user(
|
|
123
|
+
token: str = Depends(oauth2_scheme),
|
|
124
|
+
repo: UserRepository = Depends(get_user_repo),
|
|
125
|
+
):
|
|
126
|
+
user = await repo.find_by_token(token)
|
|
127
|
+
if not user:
|
|
128
|
+
raise HTTPException(status_code=401, detail="Invalid token")
|
|
129
|
+
return user
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The dependency graph resolves as: `oauth2_scheme` + `get_db` -> `get_user_repo` -> `get_current_user`. Each node executes once per request regardless of how many endpoints declare it.
|
|
133
|
+
|
|
134
|
+
### Disabling Cache
|
|
135
|
+
|
|
136
|
+
Force a dependency to re-execute per injection point by setting `use_cache=False`:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
async def get_timestamp():
|
|
140
|
+
return datetime.utcnow()
|
|
141
|
+
|
|
142
|
+
@router.get("/")
|
|
143
|
+
async def handler(
|
|
144
|
+
start: datetime = Depends(get_timestamp),
|
|
145
|
+
end: datetime = Depends(get_timestamp, use_cache=False),
|
|
146
|
+
):
|
|
147
|
+
# start and end are different timestamps
|
|
148
|
+
...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 4. WebSocket Endpoints
|
|
154
|
+
|
|
155
|
+
WebSocket routes use `@app.websocket()` and receive a `WebSocket` object for bidirectional communication:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
159
|
+
|
|
160
|
+
@app.websocket("/ws/{client_id}")
|
|
161
|
+
async def websocket_endpoint(websocket: WebSocket, client_id: str):
|
|
162
|
+
await websocket.accept()
|
|
163
|
+
try:
|
|
164
|
+
while True:
|
|
165
|
+
data = await websocket.receive_text()
|
|
166
|
+
await websocket.send_text(f"Echo: {data}")
|
|
167
|
+
except WebSocketDisconnect:
|
|
168
|
+
pass
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Dependencies work with WebSocket endpoints. Inject shared resources the same way as HTTP handlers:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
@app.websocket("/ws")
|
|
175
|
+
async def ws_with_deps(websocket: WebSocket, db: DB):
|
|
176
|
+
await websocket.accept()
|
|
177
|
+
...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Connection Management
|
|
181
|
+
|
|
182
|
+
Track active connections for broadcasting:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
class ConnectionManager:
|
|
186
|
+
def __init__(self):
|
|
187
|
+
self.active: list[WebSocket] = []
|
|
188
|
+
|
|
189
|
+
async def connect(self, ws: WebSocket):
|
|
190
|
+
await ws.accept()
|
|
191
|
+
self.active.append(ws)
|
|
192
|
+
|
|
193
|
+
def disconnect(self, ws: WebSocket):
|
|
194
|
+
self.active.remove(ws)
|
|
195
|
+
|
|
196
|
+
async def broadcast(self, message: str):
|
|
197
|
+
for ws in self.active:
|
|
198
|
+
await ws.send_text(message)
|
|
199
|
+
|
|
200
|
+
manager = ConnectionManager()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 5. Testing Endpoints
|
|
206
|
+
|
|
207
|
+
Use `httpx.AsyncClient` with FastAPI's built-in test support. Override dependencies to inject test doubles:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
import pytest
|
|
211
|
+
from httpx import AsyncClient, ASGITransport
|
|
212
|
+
from app.main import app
|
|
213
|
+
from app.dependencies import get_db
|
|
214
|
+
|
|
215
|
+
async def mock_db():
|
|
216
|
+
yield FakeDatabase()
|
|
217
|
+
|
|
218
|
+
app.dependency_overrides[get_db] = mock_db
|
|
219
|
+
|
|
220
|
+
@pytest.fixture
|
|
221
|
+
async def client():
|
|
222
|
+
transport = ASGITransport(app=app)
|
|
223
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
224
|
+
yield ac
|
|
225
|
+
|
|
226
|
+
@pytest.mark.anyio
|
|
227
|
+
async def test_create_item(client: AsyncClient):
|
|
228
|
+
response = await client.post("/items/", json={"name": "Test", "price": 9.99})
|
|
229
|
+
assert response.status_code == 201
|
|
230
|
+
assert response.json()["name"] == "Test"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Testing with Real Dependencies
|
|
234
|
+
|
|
235
|
+
For integration tests, use the actual dependency graph but point to a test database:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
@pytest.fixture(autouse=True)
|
|
239
|
+
async def setup_test_db():
|
|
240
|
+
await create_test_tables()
|
|
241
|
+
yield
|
|
242
|
+
await drop_test_tables()
|
|
243
|
+
app.dependency_overrides.clear()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Testing Authentication
|
|
247
|
+
|
|
248
|
+
Override the auth dependency to bypass token validation in tests:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
async def mock_current_user():
|
|
252
|
+
return User(id=1, email="test@example.com", role="admin")
|
|
253
|
+
|
|
254
|
+
app.dependency_overrides[get_current_user] = mock_current_user
|
|
255
|
+
```
|