@martinffx/atelier 1.4.0 → 3.0.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/CHANGELOG.md +38 -0
- package/README.md +95 -220
- package/agents/architect.md +6 -6
- package/agents/oracle.md +1 -1
- package/agents/{recon.md → sentinel.md} +3 -3
- package/dist/atelier.js +167 -26
- package/package.json +2 -1
- package/skills/atelier-orchestrator/SKILL.md +180 -0
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/SKILL.md +2 -2
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +9 -9
- package/skills/code-subagents/SKILL.md +44 -75
- package/skills/code-subagents/references/batch-reviewer-prompt.md +44 -0
- package/skills/code-subagents/references/implementor-prompt.md +10 -11
- package/skills/spec-brainstorm/SKILL.md +21 -16
- package/skills/spec-finish/SKILL.md +14 -14
- package/skills/spec-implement/SKILL.md +42 -42
- package/skills/spec-plan/SKILL.md +72 -26
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +0 -48
- package/skills/code-subagents/references/spec-reviewer-prompt.md +0 -62
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/spec-orchestrator/SKILL.md +0 -145
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
# Pydantic Validation in FastAPI
|
|
2
|
-
|
|
3
|
-
FastAPI uses Pydantic for request/response validation. Comprehensive validation patterns.
|
|
4
|
-
|
|
5
|
-
## Field Constraints
|
|
6
|
-
|
|
7
|
-
### Numeric Constraints
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
from pydantic import BaseModel, Field
|
|
11
|
-
from decimal import Decimal
|
|
12
|
-
|
|
13
|
-
class Product(BaseModel):
|
|
14
|
-
price: Decimal = Field(gt=0, decimal_places=2) # Greater than 0, 2 decimals
|
|
15
|
-
discount: float = Field(ge=0, le=1) # Between 0 and 1
|
|
16
|
-
stock: int = Field(ge=0) # Greater than or equal to 0
|
|
17
|
-
rating: float = Field(ge=0, le=5) # Rating from 0 to 5
|
|
18
|
-
|
|
19
|
-
# Field constraints:
|
|
20
|
-
# gt: greater than
|
|
21
|
-
# ge: greater than or equal
|
|
22
|
-
# lt: less than
|
|
23
|
-
# le: less than or equal
|
|
24
|
-
# multiple_of: must be multiple of value
|
|
25
|
-
# decimal_places: max decimal places
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### String Constraints
|
|
29
|
-
|
|
30
|
-
```python
|
|
31
|
-
class User(BaseModel):
|
|
32
|
-
username: str = Field(min_length=3, max_length=50)
|
|
33
|
-
email: str = Field(pattern=r"^[\w\.\+\-]+@[\w\.\-]+\.\w+$")
|
|
34
|
-
password: str = Field(min_length=8, max_length=100)
|
|
35
|
-
bio: str | None = Field(default=None, max_length=500)
|
|
36
|
-
|
|
37
|
-
# String constraints:
|
|
38
|
-
# min_length: minimum length
|
|
39
|
-
# max_length: maximum length
|
|
40
|
-
# pattern: regex pattern
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Collection Constraints
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
class Order(BaseModel):
|
|
47
|
-
items: list[str] = Field(min_length=1, max_length=100) # 1-100 items
|
|
48
|
-
tags: set[str] = Field(max_length=10) # Max 10 unique tags
|
|
49
|
-
|
|
50
|
-
# Collection constraints:
|
|
51
|
-
# min_length: minimum items
|
|
52
|
-
# max_length: maximum items
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Field Types
|
|
56
|
-
|
|
57
|
-
### Special Pydantic Types
|
|
58
|
-
|
|
59
|
-
```python
|
|
60
|
-
from pydantic import BaseModel, EmailStr, HttpUrl, UUID4, constr, conint
|
|
61
|
-
|
|
62
|
-
class User(BaseModel):
|
|
63
|
-
id: UUID4 # Valid UUID4
|
|
64
|
-
email: EmailStr # Valid email
|
|
65
|
-
website: HttpUrl # Valid HTTP URL
|
|
66
|
-
username: constr(min_length=3, max_length=50) # Constrained string
|
|
67
|
-
age: conint(ge=18, le=120) # Constrained int
|
|
68
|
-
|
|
69
|
-
# Install email-validator for EmailStr:
|
|
70
|
-
# pip install email-validator
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Datetime Types
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
from datetime import datetime, date, time
|
|
77
|
-
from pydantic import BaseModel, Field
|
|
78
|
-
|
|
79
|
-
class Event(BaseModel):
|
|
80
|
-
name: str
|
|
81
|
-
start_date: date # YYYY-MM-DD
|
|
82
|
-
start_time: time # HH:MM:SS
|
|
83
|
-
created_at: datetime # ISO 8601 datetime
|
|
84
|
-
|
|
85
|
-
class Appointment(BaseModel):
|
|
86
|
-
scheduled_at: datetime = Field(
|
|
87
|
-
description="Appointment datetime in ISO 8601 format"
|
|
88
|
-
)
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## Custom Validators
|
|
92
|
-
|
|
93
|
-
### Field Validators
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
from pydantic import BaseModel, field_validator
|
|
97
|
-
|
|
98
|
-
class User(BaseModel):
|
|
99
|
-
email: str
|
|
100
|
-
age: int
|
|
101
|
-
|
|
102
|
-
@field_validator("email")
|
|
103
|
-
@classmethod
|
|
104
|
-
def validate_email(cls, v):
|
|
105
|
-
"""Validate email domain"""
|
|
106
|
-
if not v.endswith("@example.com"):
|
|
107
|
-
raise ValueError("Only @example.com emails allowed")
|
|
108
|
-
return v
|
|
109
|
-
|
|
110
|
-
@field_validator("age")
|
|
111
|
-
@classmethod
|
|
112
|
-
def validate_age(cls, v):
|
|
113
|
-
"""Validate age range"""
|
|
114
|
-
if v < 18:
|
|
115
|
-
raise ValueError("Must be at least 18 years old")
|
|
116
|
-
if v > 120:
|
|
117
|
-
raise ValueError("Age must be realistic")
|
|
118
|
-
return v
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Model Validators
|
|
122
|
-
|
|
123
|
-
Validate across multiple fields:
|
|
124
|
-
|
|
125
|
-
```python
|
|
126
|
-
from pydantic import BaseModel, model_validator
|
|
127
|
-
|
|
128
|
-
class DateRange(BaseModel):
|
|
129
|
-
start_date: date
|
|
130
|
-
end_date: date
|
|
131
|
-
|
|
132
|
-
@model_validator(mode="after")
|
|
133
|
-
def validate_date_range(self):
|
|
134
|
-
"""Validate start before end"""
|
|
135
|
-
if self.start_date >= self.end_date:
|
|
136
|
-
raise ValueError("start_date must be before end_date")
|
|
137
|
-
return self
|
|
138
|
-
|
|
139
|
-
class Order(BaseModel):
|
|
140
|
-
items: list[str]
|
|
141
|
-
discount: float
|
|
142
|
-
|
|
143
|
-
@model_validator(mode="after")
|
|
144
|
-
def validate_discount_eligibility(self):
|
|
145
|
-
"""Discount requires minimum items"""
|
|
146
|
-
if self.discount > 0 and len(self.items) < 2:
|
|
147
|
-
raise ValueError("Discount requires at least 2 items")
|
|
148
|
-
return self
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Transform Values
|
|
152
|
-
|
|
153
|
-
```python
|
|
154
|
-
from pydantic import field_validator
|
|
155
|
-
|
|
156
|
-
class User(BaseModel):
|
|
157
|
-
email: str
|
|
158
|
-
username: str
|
|
159
|
-
|
|
160
|
-
@field_validator("email")
|
|
161
|
-
@classmethod
|
|
162
|
-
def lowercase_email(cls, v):
|
|
163
|
-
"""Normalize email to lowercase"""
|
|
164
|
-
return v.lower()
|
|
165
|
-
|
|
166
|
-
@field_validator("username")
|
|
167
|
-
@classmethod
|
|
168
|
-
def strip_username(cls, v):
|
|
169
|
-
"""Remove whitespace from username"""
|
|
170
|
-
return v.strip()
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Complex Validation
|
|
174
|
-
|
|
175
|
-
### Conditional Validation
|
|
176
|
-
|
|
177
|
-
```python
|
|
178
|
-
from pydantic import BaseModel, model_validator
|
|
179
|
-
|
|
180
|
-
class Payment(BaseModel):
|
|
181
|
-
method: str # "card" or "bank"
|
|
182
|
-
card_number: str | None = None
|
|
183
|
-
bank_account: str | None = None
|
|
184
|
-
|
|
185
|
-
@model_validator(mode="after")
|
|
186
|
-
def validate_payment_method(self):
|
|
187
|
-
"""Validate required fields based on method"""
|
|
188
|
-
if self.method == "card":
|
|
189
|
-
if not self.card_number:
|
|
190
|
-
raise ValueError("card_number required for card payment")
|
|
191
|
-
elif self.method == "bank":
|
|
192
|
-
if not self.bank_account:
|
|
193
|
-
raise ValueError("bank_account required for bank payment")
|
|
194
|
-
else:
|
|
195
|
-
raise ValueError("Invalid payment method")
|
|
196
|
-
return self
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### List Item Validation
|
|
200
|
-
|
|
201
|
-
```python
|
|
202
|
-
from pydantic import BaseModel, field_validator
|
|
203
|
-
|
|
204
|
-
class Order(BaseModel):
|
|
205
|
-
items: list[str]
|
|
206
|
-
|
|
207
|
-
@field_validator("items")
|
|
208
|
-
@classmethod
|
|
209
|
-
def validate_items(cls, v):
|
|
210
|
-
"""Validate each item"""
|
|
211
|
-
if not v:
|
|
212
|
-
raise ValueError("Order must have at least one item")
|
|
213
|
-
|
|
214
|
-
# Check for duplicates
|
|
215
|
-
if len(v) != len(set(v)):
|
|
216
|
-
raise ValueError("Duplicate items not allowed")
|
|
217
|
-
|
|
218
|
-
# Validate each item
|
|
219
|
-
for item in v:
|
|
220
|
-
if len(item) < 3:
|
|
221
|
-
raise ValueError(f"Item name too short: {item}")
|
|
222
|
-
|
|
223
|
-
return v
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Nested Models
|
|
227
|
-
|
|
228
|
-
```python
|
|
229
|
-
from pydantic import BaseModel, Field
|
|
230
|
-
|
|
231
|
-
class Address(BaseModel):
|
|
232
|
-
street: str
|
|
233
|
-
city: str
|
|
234
|
-
country: str = Field(pattern=r"^[A-Z]{2}$") # Country code
|
|
235
|
-
|
|
236
|
-
class User(BaseModel):
|
|
237
|
-
name: str
|
|
238
|
-
email: EmailStr
|
|
239
|
-
address: Address # Nested model
|
|
240
|
-
|
|
241
|
-
# Request body:
|
|
242
|
-
# {
|
|
243
|
-
# "name": "Alice",
|
|
244
|
-
# "email": "alice@example.com",
|
|
245
|
-
# "address": {
|
|
246
|
-
# "street": "123 Main St",
|
|
247
|
-
# "city": "New York",
|
|
248
|
-
# "country": "US"
|
|
249
|
-
# }
|
|
250
|
-
# }
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## Optional Fields
|
|
254
|
-
|
|
255
|
-
```python
|
|
256
|
-
from typing import Optional
|
|
257
|
-
|
|
258
|
-
class User(BaseModel):
|
|
259
|
-
name: str
|
|
260
|
-
email: str
|
|
261
|
-
bio: str | None = None # Optional, defaults to None
|
|
262
|
-
age: int | None = None # Optional
|
|
263
|
-
website: HttpUrl | None = None # Optional URL
|
|
264
|
-
|
|
265
|
-
# Or use Optional (older syntax)
|
|
266
|
-
class User(BaseModel):
|
|
267
|
-
name: str
|
|
268
|
-
bio: Optional[str] = None
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
## Default Values
|
|
272
|
-
|
|
273
|
-
```python
|
|
274
|
-
from datetime import datetime
|
|
275
|
-
from uuid import uuid4
|
|
276
|
-
|
|
277
|
-
class User(BaseModel):
|
|
278
|
-
id: UUID = Field(default_factory=uuid4) # Generate new UUID
|
|
279
|
-
created_at: datetime = Field(default_factory=datetime.now)
|
|
280
|
-
is_active: bool = True # Static default
|
|
281
|
-
role: str = "user" # Static default
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## Union Types
|
|
285
|
-
|
|
286
|
-
```python
|
|
287
|
-
from typing import Union
|
|
288
|
-
from pydantic import field_validator
|
|
289
|
-
|
|
290
|
-
class FlexibleField(BaseModel):
|
|
291
|
-
value: int | str # Can be int or string
|
|
292
|
-
|
|
293
|
-
@field_validator("value")
|
|
294
|
-
@classmethod
|
|
295
|
-
def validate_value(cls, v):
|
|
296
|
-
"""Validate based on type"""
|
|
297
|
-
if isinstance(v, int):
|
|
298
|
-
if v < 0:
|
|
299
|
-
raise ValueError("Int value must be positive")
|
|
300
|
-
elif isinstance(v, str):
|
|
301
|
-
if len(v) < 3:
|
|
302
|
-
raise ValueError("String value too short")
|
|
303
|
-
return v
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
## Discriminated Unions
|
|
307
|
-
|
|
308
|
-
For handling multiple types with type field:
|
|
309
|
-
|
|
310
|
-
```python
|
|
311
|
-
from typing import Literal, Union
|
|
312
|
-
from pydantic import BaseModel, Field
|
|
313
|
-
|
|
314
|
-
class CardPayment(BaseModel):
|
|
315
|
-
type: Literal["card"]
|
|
316
|
-
card_number: str
|
|
317
|
-
cvv: str
|
|
318
|
-
|
|
319
|
-
class BankPayment(BaseModel):
|
|
320
|
-
type: Literal["bank"]
|
|
321
|
-
account_number: str
|
|
322
|
-
routing_number: str
|
|
323
|
-
|
|
324
|
-
class Payment(BaseModel):
|
|
325
|
-
payment: Union[CardPayment, BankPayment] = Field(discriminator="type")
|
|
326
|
-
|
|
327
|
-
# FastAPI automatically validates based on "type" field
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## Custom Error Messages
|
|
331
|
-
|
|
332
|
-
```python
|
|
333
|
-
from pydantic import BaseModel, Field, field_validator, ValidationError
|
|
334
|
-
|
|
335
|
-
class User(BaseModel):
|
|
336
|
-
age: int = Field(
|
|
337
|
-
ge=18,
|
|
338
|
-
le=120,
|
|
339
|
-
description="User age in years",
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
@field_validator("age")
|
|
343
|
-
@classmethod
|
|
344
|
-
def validate_age(cls, v):
|
|
345
|
-
if v < 18:
|
|
346
|
-
raise ValueError("You must be at least 18 years old to register")
|
|
347
|
-
return v
|
|
348
|
-
|
|
349
|
-
# Customize validation error response in FastAPI
|
|
350
|
-
from fastapi import Request, status
|
|
351
|
-
from fastapi.responses import JSONResponse
|
|
352
|
-
from fastapi.exceptions import RequestValidationError
|
|
353
|
-
|
|
354
|
-
@app.exception_handler(RequestValidationError)
|
|
355
|
-
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
356
|
-
"""Custom validation error format"""
|
|
357
|
-
errors = []
|
|
358
|
-
for error in exc.errors():
|
|
359
|
-
errors.append({
|
|
360
|
-
"field": ".".join(str(loc) for loc in error["loc"]),
|
|
361
|
-
"message": error["msg"],
|
|
362
|
-
"type": error["type"],
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
return JSONResponse(
|
|
366
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
367
|
-
content={"errors": errors},
|
|
368
|
-
)
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
## Reusable Validation
|
|
372
|
-
|
|
373
|
-
```python
|
|
374
|
-
from pydantic import field_validator
|
|
375
|
-
|
|
376
|
-
def validate_positive(v: int) -> int:
|
|
377
|
-
"""Reusable validator"""
|
|
378
|
-
if v <= 0:
|
|
379
|
-
raise ValueError("Must be positive")
|
|
380
|
-
return v
|
|
381
|
-
|
|
382
|
-
class Product(BaseModel):
|
|
383
|
-
price: int
|
|
384
|
-
stock: int
|
|
385
|
-
|
|
386
|
-
_validate_price = field_validator("price")(classmethod(validate_positive))
|
|
387
|
-
_validate_stock = field_validator("stock")(classmethod(validate_positive))
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
## Config Options
|
|
391
|
-
|
|
392
|
-
```python
|
|
393
|
-
from pydantic import BaseModel, ConfigDict
|
|
394
|
-
|
|
395
|
-
class User(BaseModel):
|
|
396
|
-
model_config = ConfigDict(
|
|
397
|
-
str_strip_whitespace=True, # Strip whitespace from strings
|
|
398
|
-
str_min_length=1, # Minimum string length
|
|
399
|
-
validate_assignment=True, # Validate on assignment
|
|
400
|
-
validate_default=True, # Validate default values
|
|
401
|
-
from_attributes=True, # Enable ORM mode
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
name: str
|
|
405
|
-
email: str
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
## Validation in Route
|
|
409
|
-
|
|
410
|
-
```python
|
|
411
|
-
from fastapi import HTTPException
|
|
412
|
-
|
|
413
|
-
@app.post("/users")
|
|
414
|
-
def create_user(user: CreateUserRequest):
|
|
415
|
-
"""Pydantic validation happens automatically"""
|
|
416
|
-
# If validation fails, FastAPI returns 422 with error details
|
|
417
|
-
# If successful, user is validated CreateUserRequest instance
|
|
418
|
-
|
|
419
|
-
# Additional business validation
|
|
420
|
-
if user_exists(user.email):
|
|
421
|
-
raise HTTPException(
|
|
422
|
-
status_code=400,
|
|
423
|
-
detail="User with this email already exists",
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
return create_user_in_db(user)
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
## Testing Validation
|
|
430
|
-
|
|
431
|
-
```python
|
|
432
|
-
import pytest
|
|
433
|
-
from pydantic import ValidationError
|
|
434
|
-
|
|
435
|
-
def test_valid_user():
|
|
436
|
-
"""Test valid user creation"""
|
|
437
|
-
user = CreateUserRequest(
|
|
438
|
-
email="test@example.com",
|
|
439
|
-
name="Test User",
|
|
440
|
-
age=25,
|
|
441
|
-
)
|
|
442
|
-
assert user.email == "test@example.com"
|
|
443
|
-
|
|
444
|
-
def test_invalid_email():
|
|
445
|
-
"""Test invalid email"""
|
|
446
|
-
with pytest.raises(ValidationError) as exc_info:
|
|
447
|
-
CreateUserRequest(
|
|
448
|
-
email="invalid-email",
|
|
449
|
-
name="Test User",
|
|
450
|
-
age=25,
|
|
451
|
-
)
|
|
452
|
-
errors = exc_info.value.errors()
|
|
453
|
-
assert any(error["loc"] == ("email",) for error in errors)
|
|
454
|
-
|
|
455
|
-
def test_age_validation():
|
|
456
|
-
"""Test age must be >= 18"""
|
|
457
|
-
with pytest.raises(ValidationError) as exc_info:
|
|
458
|
-
CreateUserRequest(
|
|
459
|
-
email="test@example.com",
|
|
460
|
-
name="Test User",
|
|
461
|
-
age=15,
|
|
462
|
-
)
|
|
463
|
-
errors = exc_info.value.errors()
|
|
464
|
-
assert "age" in str(errors)
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
## Best Practices
|
|
468
|
-
|
|
469
|
-
1. **Use Field constraints**: Prefer `Field(gt=0)` over custom validators for simple cases
|
|
470
|
-
2. **Descriptive error messages**: Make validation errors helpful
|
|
471
|
-
3. **Validate early**: Pydantic validates at API boundary
|
|
472
|
-
4. **Normalize data**: Use validators to transform (lowercase email, strip whitespace)
|
|
473
|
-
5. **Test validators**: Write tests for complex validation logic
|
|
474
|
-
6. **Reuse validators**: Extract common validators to functions
|
|
475
|
-
7. **Model validators for cross-field**: Use `@model_validator` when validating multiple fields
|
|
476
|
-
|
|
477
|
-
## Summary
|
|
478
|
-
|
|
479
|
-
**Field constraints:**
|
|
480
|
-
- Numeric: `gt`, `ge`, `lt`, `le`, `multiple_of`
|
|
481
|
-
- String: `min_length`, `max_length`, `pattern`
|
|
482
|
-
- Collection: `min_length`, `max_length`
|
|
483
|
-
|
|
484
|
-
**Validators:**
|
|
485
|
-
- `@field_validator`: Validate single field
|
|
486
|
-
- `@model_validator`: Validate across fields
|
|
487
|
-
- Transform values in validators
|
|
488
|
-
|
|
489
|
-
**Special types:**
|
|
490
|
-
- `EmailStr`: Valid email
|
|
491
|
-
- `HttpUrl`: Valid URL
|
|
492
|
-
- `UUID4`: Valid UUID
|
|
493
|
-
|
|
494
|
-
**Error handling:**
|
|
495
|
-
- Automatic 422 responses
|
|
496
|
-
- Custom error messages
|
|
497
|
-
- Exception handlers
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: python-modern-python
|
|
3
|
-
description: Modern Python language features and typing patterns. Use when writing type hints, using generics, implementing pattern matching, working with async/await, or leveraging Python 3.10+ features.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Modern Python Features
|
|
8
|
-
|
|
9
|
-
Modern Python 3.10+ language features, type hints, and patterns.
|
|
10
|
-
|
|
11
|
-
## Type Hints
|
|
12
|
-
|
|
13
|
-
### Basic Types
|
|
14
|
-
|
|
15
|
-
```python
|
|
16
|
-
def greet(name: str) -> str:
|
|
17
|
-
return f"Hello {name}"
|
|
18
|
-
|
|
19
|
-
age: int = 25
|
|
20
|
-
prices: list[float] = [9.99, 19.99]
|
|
21
|
-
mapping: dict[str, int] = {"a": 1}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Union Types (Python 3.10+)
|
|
25
|
-
|
|
26
|
-
```python
|
|
27
|
-
# Modern syntax
|
|
28
|
-
def process(value: int | str) -> bool:
|
|
29
|
-
...
|
|
30
|
-
|
|
31
|
-
# Optional
|
|
32
|
-
def get_user(id: int) -> User | None:
|
|
33
|
-
...
|
|
34
|
-
|
|
35
|
-
# Multiple types
|
|
36
|
-
Result = int | str | bool
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Generic Types
|
|
40
|
-
|
|
41
|
-
```python
|
|
42
|
-
from typing import TypeVar, Generic
|
|
43
|
-
|
|
44
|
-
T = TypeVar("T")
|
|
45
|
-
|
|
46
|
-
class Repository(Generic[T]):
|
|
47
|
-
def get(self, id: int) -> T | None:
|
|
48
|
-
...
|
|
49
|
-
|
|
50
|
-
def save(self, entity: T) -> T:
|
|
51
|
-
...
|
|
52
|
-
|
|
53
|
-
user_repo = Repository[User]()
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Protocol (Structural Typing)
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
from typing import Protocol
|
|
60
|
-
|
|
61
|
-
class Drawable(Protocol):
|
|
62
|
-
"""Structural type - any class with draw()"""
|
|
63
|
-
def draw(self) -> None:
|
|
64
|
-
...
|
|
65
|
-
|
|
66
|
-
def render(obj: Drawable) -> None:
|
|
67
|
-
"""Works with any object that has draw()"""
|
|
68
|
-
obj.draw()
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Pattern Matching (Python 3.10+)
|
|
72
|
-
|
|
73
|
-
### Basic Matching
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
def handle_command(command: str):
|
|
77
|
-
match command:
|
|
78
|
-
case "start":
|
|
79
|
-
start_process()
|
|
80
|
-
case "stop":
|
|
81
|
-
stop_process()
|
|
82
|
-
case "status":
|
|
83
|
-
return get_status()
|
|
84
|
-
case _:
|
|
85
|
-
raise ValueError("Unknown command")
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Matching with Values
|
|
89
|
-
|
|
90
|
-
```python
|
|
91
|
-
def handle_http_status(status: int):
|
|
92
|
-
match status:
|
|
93
|
-
case 200:
|
|
94
|
-
return "OK"
|
|
95
|
-
case 404:
|
|
96
|
-
return "Not Found"
|
|
97
|
-
case 500 | 502 | 503:
|
|
98
|
-
return "Server Error"
|
|
99
|
-
case _:
|
|
100
|
-
return "Unknown"
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Structural Pattern Matching
|
|
104
|
-
|
|
105
|
-
```python
|
|
106
|
-
def process_point(point):
|
|
107
|
-
match point:
|
|
108
|
-
case (0, 0):
|
|
109
|
-
return "Origin"
|
|
110
|
-
case (x, 0):
|
|
111
|
-
return f"On X-axis at {x}"
|
|
112
|
-
case (0, y):
|
|
113
|
-
return f"On Y-axis at {y}"
|
|
114
|
-
case (x, y):
|
|
115
|
-
return f"At ({x}, {y})"
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Async/Await
|
|
119
|
-
|
|
120
|
-
### Async Functions
|
|
121
|
-
|
|
122
|
-
```python
|
|
123
|
-
async def fetch_data(url: str) -> dict:
|
|
124
|
-
"""Async function"""
|
|
125
|
-
async with httpx.AsyncClient() as client:
|
|
126
|
-
response = await client.get(url)
|
|
127
|
-
return response.json()
|
|
128
|
-
|
|
129
|
-
# Call async function
|
|
130
|
-
data = await fetch_data("https://api.example.com")
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Concurrent Execution
|
|
134
|
-
|
|
135
|
-
```python
|
|
136
|
-
import asyncio
|
|
137
|
-
|
|
138
|
-
async def process_all(urls: list[str]):
|
|
139
|
-
"""Process multiple URLs concurrently"""
|
|
140
|
-
tasks = [fetch_data(url) for url in urls]
|
|
141
|
-
results = await asyncio.gather(*tasks)
|
|
142
|
-
return results
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Async Context Managers
|
|
146
|
-
|
|
147
|
-
```python
|
|
148
|
-
class AsyncDatabase:
|
|
149
|
-
async def __aenter__(self):
|
|
150
|
-
await self.connect()
|
|
151
|
-
return self
|
|
152
|
-
|
|
153
|
-
async def __aexit__(self, *args):
|
|
154
|
-
await self.disconnect()
|
|
155
|
-
|
|
156
|
-
async with AsyncDatabase() as db:
|
|
157
|
-
await db.query(...)
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Dataclasses and attrs
|
|
161
|
-
|
|
162
|
-
```python
|
|
163
|
-
from dataclasses import dataclass
|
|
164
|
-
|
|
165
|
-
@dataclass(frozen=True)
|
|
166
|
-
class Point:
|
|
167
|
-
x: float
|
|
168
|
-
y: float
|
|
169
|
-
|
|
170
|
-
def distance_from_origin(self) -> float:
|
|
171
|
-
return (self.x ** 2 + self.y ** 2) ** 0.5
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Walrus Operator
|
|
175
|
-
|
|
176
|
-
```python
|
|
177
|
-
# Assign and use in one expression
|
|
178
|
-
if (n := len(items)) > 10:
|
|
179
|
-
print(f"Too many items: {n}")
|
|
180
|
-
|
|
181
|
-
# In comprehensions
|
|
182
|
-
results = [y for x in items if (y := process(x)) is not None]
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Modern String Formatting
|
|
186
|
-
|
|
187
|
-
```python
|
|
188
|
-
name = "Alice"
|
|
189
|
-
age = 30
|
|
190
|
-
|
|
191
|
-
# f-strings
|
|
192
|
-
message = f"Hello {name}, you are {age} years old"
|
|
193
|
-
|
|
194
|
-
# f-strings with expressions
|
|
195
|
-
message = f"In 5 years you'll be {age + 5}"
|
|
196
|
-
|
|
197
|
-
# Debug f-strings (Python 3.8+)
|
|
198
|
-
print(f"{name=}") # name='Alice'
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
See references/ for advanced typing, async patterns, and pattern matching.
|