qqmusic-api-python 0.4.1__tar.gz → 0.5.0__tar.gz
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.
- qqmusic_api_python-0.5.0/.agents/skills/pydantic/SKILL.md +1439 -0
- qqmusic_api_python-0.5.0/.agents/skills/python-standards/SKILL.md +282 -0
- qqmusic_api_python-0.5.0/.agents/skills/tarsio/SKILL.md +247 -0
- qqmusic_api_python-0.5.0/.agents/skills/tarsio/references/api-reference.md +246 -0
- qqmusic_api_python-0.5.0/.agents/skills/uv-package-manager/SKILL.md +836 -0
- qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/bug.yml +79 -0
- qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
- qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/feature.yml +34 -0
- qqmusic_api_python-0.5.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- qqmusic_api_python-0.5.0/.github/renovate.json +38 -0
- qqmusic_api_python-0.5.0/.github/workflows/checking.yaml +14 -0
- qqmusic_api_python-0.5.0/.github/workflows/docs.yml +69 -0
- qqmusic_api_python-0.5.0/.github/workflows/release.yml +58 -0
- qqmusic_api_python-0.5.0/.github/workflows/testing.yml +22 -0
- {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/.gitignore +6 -0
- qqmusic_api_python-0.5.0/.markdownlint-cli2.yaml +33 -0
- qqmusic_api_python-0.5.0/AGENTS.md +54 -0
- qqmusic_api_python-0.5.0/LICENSE +674 -0
- {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/PKG-INFO +41 -56
- qqmusic_api_python-0.5.0/README.md +56 -0
- qqmusic_api_python-0.5.0/assets/qq-music.svg +1 -0
- qqmusic_api_python-0.5.0/cliff.toml +94 -0
- qqmusic_api_python-0.5.0/docs/coding.md +237 -0
- qqmusic_api_python-0.5.0/docs/contributing.md +99 -0
- qqmusic_api_python-0.5.0/docs/index.md +83 -0
- qqmusic_api_python-0.5.0/docs/reference/core/client.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/core/exception.md +21 -0
- qqmusic_api_python-0.5.0/docs/reference/core/request.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/core/versioning.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/album.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/base.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/comment.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/login.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/lyric.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/mv.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/recommend.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/request.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/search.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/singer.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/song.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/songlist.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/top.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/model/user.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/album.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/comment.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/login.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/lyric.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/mv.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/recommend.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/search.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/singer.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/song.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/songlist.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/top.md +3 -0
- qqmusic_api_python-0.5.0/docs/reference/modules/user.md +3 -0
- qqmusic_api_python-0.5.0/docs/release-notes.md +606 -0
- qqmusic_api_python-0.5.0/docs/tutorial/client.md +68 -0
- qqmusic_api_python-0.5.0/docs/tutorial/credential.md +83 -0
- qqmusic_api_python-0.5.0/docs/tutorial/start.md +69 -0
- qqmusic_api_python-0.5.0/examples/download_song.py +30 -0
- qqmusic_api_python-0.5.0/examples/phone_login.py +41 -0
- qqmusic_api_python-0.5.0/examples/qrcode_login.py +74 -0
- qqmusic_api_python-0.5.0/prek.toml +74 -0
- {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/pyproject.toml +92 -65
- qqmusic_api_python-0.5.0/qqmusic_api/__init__.py +32 -0
- qqmusic_api_python-0.5.0/qqmusic_api/algorithms/__init__.py +48 -0
- qqmusic_api_python-0.5.0/qqmusic_api/algorithms/sign.py +58 -0
- {qqmusic_api_python-0.4.1/qqmusic_api/utils → qqmusic_api_python-0.5.0/qqmusic_api/algorithms}/tripledes.py +18 -19
- qqmusic_api_python-0.5.0/qqmusic_api/core/__init__.py +47 -0
- qqmusic_api_python-0.5.0/qqmusic_api/core/client.py +749 -0
- qqmusic_api_python-0.5.0/qqmusic_api/core/exceptions.py +289 -0
- qqmusic_api_python-0.5.0/qqmusic_api/core/request.py +357 -0
- qqmusic_api_python-0.5.0/qqmusic_api/core/versioning.py +238 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/__init__.py +7 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/album.py +72 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/base.py +307 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/comment.py +205 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/login.py +116 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/lyric.py +41 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/mv.py +118 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/recommend.py +174 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/request.py +179 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/search.py +276 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/singer.py +471 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/song.py +389 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/songlist.py +74 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/top.py +118 -0
- qqmusic_api_python-0.5.0/qqmusic_api/models/user.py +418 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/__init__.py +29 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/_base.py +198 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/album.py +53 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/comment.py +154 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/login.py +590 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/login_utils.py +222 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/lyric.py +50 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/mv.py +70 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/recommend.py +81 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/search.py +144 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/singer.py +290 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/song.py +359 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/songlist.py +169 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/top.py +49 -0
- qqmusic_api_python-0.5.0/qqmusic_api/modules/user.py +306 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/__init__.py +15 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/common.py +110 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/device.py +188 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/mqtt.py +661 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/qimei.py +250 -0
- qqmusic_api_python-0.5.0/qqmusic_api/utils/retry.py +116 -0
- qqmusic_api_python-0.5.0/scripts/ag-1.py +37 -0
- qqmusic_api_python-0.5.0/tests/conftest.py +96 -0
- qqmusic_api_python-0.5.0/tests/test_album.py +54 -0
- qqmusic_api_python-0.5.0/tests/test_comment.py +38 -0
- qqmusic_api_python-0.5.0/tests/test_login.py +97 -0
- qqmusic_api_python-0.5.0/tests/test_login_utils.py +71 -0
- qqmusic_api_python-0.5.0/tests/test_lyric.py +39 -0
- qqmusic_api_python-0.5.0/tests/test_mv.py +27 -0
- qqmusic_api_python-0.5.0/tests/test_recommend.py +37 -0
- qqmusic_api_python-0.5.0/tests/test_search.py +59 -0
- qqmusic_api_python-0.5.0/tests/test_singer.py +177 -0
- qqmusic_api_python-0.5.0/tests/test_song.py +106 -0
- qqmusic_api_python-0.5.0/tests/test_songlist.py +59 -0
- qqmusic_api_python-0.5.0/tests/test_top.py +55 -0
- qqmusic_api_python-0.5.0/tests/test_user.py +103 -0
- qqmusic_api_python-0.5.0/uv.lock +1487 -0
- qqmusic_api_python-0.5.0/zensical.toml +154 -0
- qqmusic_api_python-0.4.1/LICENSE +0 -21
- qqmusic_api_python-0.4.1/README.md +0 -82
- qqmusic_api_python-0.4.1/qqmusic_api/__init__.py +0 -36
- qqmusic_api_python-0.4.1/qqmusic_api/album.py +0 -58
- qqmusic_api_python-0.4.1/qqmusic_api/comment.py +0 -147
- qqmusic_api_python-0.4.1/qqmusic_api/exceptions/__init__.py +0 -17
- qqmusic_api_python-0.4.1/qqmusic_api/exceptions/api_exception.py +0 -67
- qqmusic_api_python-0.4.1/qqmusic_api/login.py +0 -556
- qqmusic_api_python-0.4.1/qqmusic_api/lyric.py +0 -68
- qqmusic_api_python-0.4.1/qqmusic_api/mv.py +0 -79
- qqmusic_api_python-0.4.1/qqmusic_api/recommend.py +0 -52
- qqmusic_api_python-0.4.1/qqmusic_api/search.py +0 -138
- qqmusic_api_python-0.4.1/qqmusic_api/singer.py +0 -411
- qqmusic_api_python-0.4.1/qqmusic_api/song.py +0 -319
- qqmusic_api_python-0.4.1/qqmusic_api/songlist.py +0 -146
- qqmusic_api_python-0.4.1/qqmusic_api/top.py +0 -34
- qqmusic_api_python-0.4.1/qqmusic_api/user.py +0 -206
- qqmusic_api_python-0.4.1/qqmusic_api/utils/__init__.py +0 -0
- qqmusic_api_python-0.4.1/qqmusic_api/utils/common.py +0 -96
- qqmusic_api_python-0.4.1/qqmusic_api/utils/credential.py +0 -137
- qqmusic_api_python-0.4.1/qqmusic_api/utils/device.py +0 -101
- qqmusic_api_python-0.4.1/qqmusic_api/utils/mqtt.py +0 -347
- qqmusic_api_python-0.4.1/qqmusic_api/utils/network.py +0 -458
- qqmusic_api_python-0.4.1/qqmusic_api/utils/qimei.py +0 -165
- qqmusic_api_python-0.4.1/qqmusic_api/utils/session.py +0 -99
- qqmusic_api_python-0.4.1/qqmusic_api/utils/sign.py +0 -36
- qqmusic_api_python-0.4.1/tests/test_album.py +0 -13
- qqmusic_api_python-0.4.1/tests/test_comment.py +0 -38
- qqmusic_api_python-0.4.1/tests/test_login.py +0 -42
- qqmusic_api_python-0.4.1/tests/test_lyric.py +0 -18
- qqmusic_api_python-0.4.1/tests/test_mv.py +0 -13
- qqmusic_api_python-0.4.1/tests/test_qimei.py +0 -5
- qqmusic_api_python-0.4.1/tests/test_recommend.py +0 -25
- qqmusic_api_python-0.4.1/tests/test_search.py +0 -33
- qqmusic_api_python-0.4.1/tests/test_session.py +0 -94
- qqmusic_api_python-0.4.1/tests/test_sign.py +0 -13
- qqmusic_api_python-0.4.1/tests/test_singer.py +0 -62
- qqmusic_api_python-0.4.1/tests/test_song.py +0 -55
- qqmusic_api_python-0.4.1/tests/test_songlist.py +0 -18
- qqmusic_api_python-0.4.1/tests/test_top.py +0 -13
- qqmusic_api_python-0.4.1/tests/test_user.py +0 -86
- qqmusic_api_python-0.4.1/web/README.md +0 -46
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pydantic
|
|
3
|
+
description: Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation in FastAPI, Django, and configuration management.
|
|
4
|
+
progressive_disclosure:
|
|
5
|
+
entry_point:
|
|
6
|
+
- summary
|
|
7
|
+
- when_to_use
|
|
8
|
+
- quick_start
|
|
9
|
+
full_content: all
|
|
10
|
+
token_estimates:
|
|
11
|
+
entry_point: 70
|
|
12
|
+
full: 5500
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Pydantic Validation Skill
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation.
|
|
20
|
+
|
|
21
|
+
## When to Use
|
|
22
|
+
|
|
23
|
+
* API request/response validation (FastAPI, Django)
|
|
24
|
+
* Settings and configuration management (env variables, config files)
|
|
25
|
+
* ORM model validation (SQLAlchemy integration)
|
|
26
|
+
* Data parsing and serialization (JSON, dict, custom formats)
|
|
27
|
+
* Type-safe data classes with automatic validation
|
|
28
|
+
* CLI argument parsing with type safety
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pydantic import BaseModel, Field, EmailStr
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
|
|
36
|
+
class User(BaseModel):
|
|
37
|
+
id: int
|
|
38
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
39
|
+
email: EmailStr
|
|
40
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
41
|
+
is_active: bool = True
|
|
42
|
+
|
|
43
|
+
# Validate data
|
|
44
|
+
user = User(id=1, name="Alice", email="alice@example.com")
|
|
45
|
+
print(user.model_dump()) # {'id': 1, 'name': 'Alice', ...}
|
|
46
|
+
|
|
47
|
+
# Automatic type coercion
|
|
48
|
+
user2 = User(id="2", name="Bob", email="bob@example.com")
|
|
49
|
+
assert user2.id == 2 # String "2" coerced to int
|
|
50
|
+
|
|
51
|
+
# Validation error
|
|
52
|
+
try:
|
|
53
|
+
User(id=3, name="", email="invalid")
|
|
54
|
+
except ValidationError as e:
|
|
55
|
+
print(e.errors())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Core Concepts
|
|
61
|
+
|
|
62
|
+
### BaseModel Foundation
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from pydantic import BaseModel, ConfigDict
|
|
66
|
+
|
|
67
|
+
class Product(BaseModel):
|
|
68
|
+
model_config = ConfigDict(
|
|
69
|
+
str_strip_whitespace=True,
|
|
70
|
+
validate_assignment=True,
|
|
71
|
+
use_enum_values=True,
|
|
72
|
+
arbitrary_types_allowed=False
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
price: float
|
|
77
|
+
quantity: int = 0
|
|
78
|
+
|
|
79
|
+
# Usage
|
|
80
|
+
product = Product(name=" Widget ", price=19.99)
|
|
81
|
+
assert product.name == "Widget" # Whitespace stripped
|
|
82
|
+
|
|
83
|
+
# Validate on assignment
|
|
84
|
+
product.price = "29.99" # Auto-converts to float
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Field Configuration
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pydantic import Field, field_validator
|
|
91
|
+
from typing import Annotated
|
|
92
|
+
|
|
93
|
+
class Item(BaseModel):
|
|
94
|
+
# Field constraints
|
|
95
|
+
sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
|
|
96
|
+
price: float = Field(gt=0, le=10000)
|
|
97
|
+
stock: int = Field(ge=0, default=0)
|
|
98
|
+
|
|
99
|
+
# Annotated types (Pydantic v2)
|
|
100
|
+
quantity: Annotated[int, Field(ge=1, le=100)]
|
|
101
|
+
|
|
102
|
+
# Descriptions and examples
|
|
103
|
+
description: str = Field(
|
|
104
|
+
...,
|
|
105
|
+
description="Product description",
|
|
106
|
+
examples=["High-quality widget"]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Deprecated fields
|
|
110
|
+
old_field: str | None = Field(None, deprecated=True)
|
|
111
|
+
|
|
112
|
+
@field_validator('sku')
|
|
113
|
+
@classmethod
|
|
114
|
+
def validate_sku(cls, v: str) -> str:
|
|
115
|
+
if not v.startswith('ABC'):
|
|
116
|
+
raise ValueError('SKU must start with ABC')
|
|
117
|
+
return v
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Pydantic v2 Improvements
|
|
121
|
+
|
|
122
|
+
### Migration from v1
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Pydantic v1
|
|
126
|
+
class OldModel(BaseModel):
|
|
127
|
+
class Config:
|
|
128
|
+
validate_assignment = True
|
|
129
|
+
json_encoders = {datetime: lambda v: v.isoformat()}
|
|
130
|
+
|
|
131
|
+
# Pydantic v2
|
|
132
|
+
class NewModel(BaseModel):
|
|
133
|
+
model_config = ConfigDict(
|
|
134
|
+
validate_assignment=True,
|
|
135
|
+
# json_encoders replaced by serializers
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@model_serializer
|
|
139
|
+
def ser_model(self) -> dict:
|
|
140
|
+
return {...}
|
|
141
|
+
|
|
142
|
+
# Key changes:
|
|
143
|
+
# - .dict() → .model_dump()
|
|
144
|
+
# - .json() → .model_dump_json()
|
|
145
|
+
# - .parse_obj() → .model_validate()
|
|
146
|
+
# - .parse_raw() → .model_validate_json()
|
|
147
|
+
# - @validator → @field_validator
|
|
148
|
+
# - @root_validator → @model_validator
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Performance Improvements
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# v2 uses Rust core (pydantic-core) for 5-50x speedup
|
|
155
|
+
from pydantic import BaseModel
|
|
156
|
+
import time
|
|
157
|
+
|
|
158
|
+
class Data(BaseModel):
|
|
159
|
+
values: list[int]
|
|
160
|
+
names: list[str]
|
|
161
|
+
|
|
162
|
+
# Benchmark
|
|
163
|
+
data = {'values': list(range(10000)), 'names': ['item'] * 10000}
|
|
164
|
+
start = time.perf_counter()
|
|
165
|
+
for _ in range(1000):
|
|
166
|
+
Data.model_validate(data)
|
|
167
|
+
elapsed = time.perf_counter() - start
|
|
168
|
+
print(f"Validated 1000 iterations in {elapsed:.2f}s")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Field Types
|
|
172
|
+
|
|
173
|
+
### Built-in Types
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from pydantic import (
|
|
177
|
+
BaseModel, EmailStr, HttpUrl, UUID4,
|
|
178
|
+
FilePath, DirectoryPath, Json, SecretStr,
|
|
179
|
+
PositiveInt, NegativeFloat, conint, constr
|
|
180
|
+
)
|
|
181
|
+
from typing import Literal
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
class Example(BaseModel):
|
|
185
|
+
# Email validation
|
|
186
|
+
email: EmailStr
|
|
187
|
+
|
|
188
|
+
# URL validation
|
|
189
|
+
website: HttpUrl
|
|
190
|
+
|
|
191
|
+
# UUID
|
|
192
|
+
id: UUID4
|
|
193
|
+
|
|
194
|
+
# File system paths
|
|
195
|
+
config_file: FilePath
|
|
196
|
+
data_dir: DirectoryPath
|
|
197
|
+
|
|
198
|
+
# JSON string → parsed object
|
|
199
|
+
metadata: Json[dict[str, str]]
|
|
200
|
+
|
|
201
|
+
# Secret (won't print in logs)
|
|
202
|
+
api_key: SecretStr
|
|
203
|
+
|
|
204
|
+
# Constrained types
|
|
205
|
+
age: PositiveInt
|
|
206
|
+
balance: NegativeFloat
|
|
207
|
+
username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
|
|
208
|
+
code: conint(ge=1000, le=9999)
|
|
209
|
+
|
|
210
|
+
# Literal types
|
|
211
|
+
status: Literal['pending', 'approved', 'rejected']
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Custom Types
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
|
|
218
|
+
from pydantic_core import core_schema
|
|
219
|
+
from typing import Any
|
|
220
|
+
|
|
221
|
+
class Color:
|
|
222
|
+
def __init__(self, r: int, g: int, b: int):
|
|
223
|
+
self.r, self.g, self.b = r, g, b
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def __get_pydantic_core_schema__(
|
|
227
|
+
cls, source_type: Any, handler: GetCoreSchemaHandler
|
|
228
|
+
) -> core_schema.CoreSchema:
|
|
229
|
+
return core_schema.no_info_after_validator_function(
|
|
230
|
+
cls.validate,
|
|
231
|
+
core_schema.str_schema()
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def validate(cls, v: str) -> 'Color':
|
|
236
|
+
if not v.startswith('#') or len(v) != 7:
|
|
237
|
+
raise ValueError('Invalid hex color')
|
|
238
|
+
r = int(v[1:3], 16)
|
|
239
|
+
g = int(v[3:5], 16)
|
|
240
|
+
b = int(v[5:7], 16)
|
|
241
|
+
return cls(r, g, b)
|
|
242
|
+
|
|
243
|
+
class Design(BaseModel):
|
|
244
|
+
primary_color: Color
|
|
245
|
+
|
|
246
|
+
# Usage
|
|
247
|
+
design = Design(primary_color='#FF5733')
|
|
248
|
+
assert design.primary_color.r == 255
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Validators
|
|
252
|
+
|
|
253
|
+
### Field Validators
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pydantic import field_validator, model_validator
|
|
257
|
+
|
|
258
|
+
class Account(BaseModel):
|
|
259
|
+
username: str
|
|
260
|
+
password: str
|
|
261
|
+
password_confirm: str
|
|
262
|
+
|
|
263
|
+
@field_validator('username')
|
|
264
|
+
@classmethod
|
|
265
|
+
def username_alphanumeric(cls, v: str) -> str:
|
|
266
|
+
if not v.isalnum():
|
|
267
|
+
raise ValueError('must be alphanumeric')
|
|
268
|
+
return v
|
|
269
|
+
|
|
270
|
+
@field_validator('password')
|
|
271
|
+
@classmethod
|
|
272
|
+
def password_strong(cls, v: str) -> str:
|
|
273
|
+
if len(v) < 8:
|
|
274
|
+
raise ValueError('must be at least 8 characters')
|
|
275
|
+
if not any(c.isupper() for c in v):
|
|
276
|
+
raise ValueError('must contain uppercase letter')
|
|
277
|
+
return v
|
|
278
|
+
|
|
279
|
+
# Validate multiple fields
|
|
280
|
+
@field_validator('username', 'password')
|
|
281
|
+
@classmethod
|
|
282
|
+
def not_empty(cls, v: str) -> str:
|
|
283
|
+
if not v or not v.strip():
|
|
284
|
+
raise ValueError('must not be empty')
|
|
285
|
+
return v.strip()
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Model Validators
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from pydantic import model_validator
|
|
292
|
+
from typing import Self
|
|
293
|
+
|
|
294
|
+
class DateRange(BaseModel):
|
|
295
|
+
start_date: datetime
|
|
296
|
+
end_date: datetime
|
|
297
|
+
|
|
298
|
+
@model_validator(mode='after')
|
|
299
|
+
def check_dates(self) -> Self:
|
|
300
|
+
if self.end_date < self.start_date:
|
|
301
|
+
raise ValueError('end_date must be after start_date')
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
class Order(BaseModel):
|
|
305
|
+
items: list[str]
|
|
306
|
+
total: float
|
|
307
|
+
discount: float = 0
|
|
308
|
+
|
|
309
|
+
@model_validator(mode='before')
|
|
310
|
+
@classmethod
|
|
311
|
+
def calculate_total(cls, data: dict) -> dict:
|
|
312
|
+
# Pre-processing before validation
|
|
313
|
+
if isinstance(data, dict) and 'total' not in data:
|
|
314
|
+
data['total'] = len(data.get('items', [])) * 10.0
|
|
315
|
+
return data
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Root Validators (Wrap)
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from pydantic import model_validator, ValidationInfo
|
|
322
|
+
|
|
323
|
+
class Config(BaseModel):
|
|
324
|
+
env: Literal['dev', 'prod']
|
|
325
|
+
debug: bool = False
|
|
326
|
+
|
|
327
|
+
@model_validator(mode='wrap')
|
|
328
|
+
@classmethod
|
|
329
|
+
def validate_config(cls, values: Any, handler, info: ValidationInfo):
|
|
330
|
+
# Call default validation
|
|
331
|
+
result = handler(values)
|
|
332
|
+
|
|
333
|
+
# Post-validation logic
|
|
334
|
+
if result.env == 'prod' and result.debug:
|
|
335
|
+
raise ValueError('debug cannot be True in production')
|
|
336
|
+
|
|
337
|
+
return result
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Type Coercion and Strict Mode
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
344
|
+
|
|
345
|
+
# Coercive mode (default)
|
|
346
|
+
class CoerciveModel(BaseModel):
|
|
347
|
+
count: int
|
|
348
|
+
price: float
|
|
349
|
+
|
|
350
|
+
data = CoerciveModel(count="42", price="19.99")
|
|
351
|
+
assert data.count == 42 # String → int
|
|
352
|
+
assert data.price == 19.99 # String → float
|
|
353
|
+
|
|
354
|
+
# Strict mode
|
|
355
|
+
class StrictModel(BaseModel):
|
|
356
|
+
model_config = ConfigDict(strict=True)
|
|
357
|
+
|
|
358
|
+
count: int
|
|
359
|
+
price: float
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
StrictModel(count="42", price="19.99") # Raises ValidationError
|
|
363
|
+
except ValidationError as e:
|
|
364
|
+
print("Strict mode: no coercion allowed")
|
|
365
|
+
|
|
366
|
+
# Per-field strict mode
|
|
367
|
+
class MixedModel(BaseModel):
|
|
368
|
+
flexible: int # Allows coercion
|
|
369
|
+
strict: Annotated[int, Field(strict=True)] # No coercion
|
|
370
|
+
|
|
371
|
+
MixedModel(flexible="1", strict=2) # OK
|
|
372
|
+
# MixedModel(flexible="1", strict="2") # ValidationError
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Nested Models and Recursive Types
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
from pydantic import BaseModel
|
|
379
|
+
from typing import ForwardRef
|
|
380
|
+
|
|
381
|
+
# Nested models
|
|
382
|
+
class Address(BaseModel):
|
|
383
|
+
street: str
|
|
384
|
+
city: str
|
|
385
|
+
country: str
|
|
386
|
+
|
|
387
|
+
class Company(BaseModel):
|
|
388
|
+
name: str
|
|
389
|
+
address: Address
|
|
390
|
+
|
|
391
|
+
company = Company(
|
|
392
|
+
name="ACME Corp",
|
|
393
|
+
address={'street': '123 Main St', 'city': 'NYC', 'country': 'USA'}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Recursive types (tree structure)
|
|
397
|
+
class TreeNode(BaseModel):
|
|
398
|
+
value: int
|
|
399
|
+
children: list['TreeNode'] = []
|
|
400
|
+
|
|
401
|
+
TreeNode.model_rebuild() # Required for forward references
|
|
402
|
+
|
|
403
|
+
tree = TreeNode(
|
|
404
|
+
value=1,
|
|
405
|
+
children=[
|
|
406
|
+
TreeNode(value=2, children=[TreeNode(value=4)]),
|
|
407
|
+
TreeNode(value=3)
|
|
408
|
+
]
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Self-referencing with ForwardRef
|
|
412
|
+
class Category(BaseModel):
|
|
413
|
+
name: str
|
|
414
|
+
parent: 'Category | None' = None
|
|
415
|
+
subcategories: list['Category'] = []
|
|
416
|
+
|
|
417
|
+
Category.model_rebuild()
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Generic Models
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from pydantic import BaseModel
|
|
424
|
+
from typing import Generic, TypeVar
|
|
425
|
+
|
|
426
|
+
T = TypeVar('T')
|
|
427
|
+
|
|
428
|
+
class Response(BaseModel, Generic[T]):
|
|
429
|
+
success: bool
|
|
430
|
+
data: T
|
|
431
|
+
message: str = ''
|
|
432
|
+
|
|
433
|
+
class User(BaseModel):
|
|
434
|
+
id: int
|
|
435
|
+
name: str
|
|
436
|
+
|
|
437
|
+
# Usage with concrete type
|
|
438
|
+
user_response = Response[User](
|
|
439
|
+
success=True,
|
|
440
|
+
data=User(id=1, name='Alice')
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# List response
|
|
444
|
+
list_response = Response[list[User]](
|
|
445
|
+
success=True,
|
|
446
|
+
data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Generic repository pattern
|
|
450
|
+
class Repository(BaseModel, Generic[T]):
|
|
451
|
+
items: list[T]
|
|
452
|
+
|
|
453
|
+
def add(self, item: T) -> None:
|
|
454
|
+
self.items.append(item)
|
|
455
|
+
|
|
456
|
+
user_repo = Repository[User](items=[])
|
|
457
|
+
user_repo.add(User(id=1, name='Alice'))
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Serialization
|
|
461
|
+
|
|
462
|
+
### Model Dump
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from pydantic import BaseModel, Field, field_serializer
|
|
466
|
+
|
|
467
|
+
class Article(BaseModel):
|
|
468
|
+
title: str
|
|
469
|
+
content: str
|
|
470
|
+
tags: list[str]
|
|
471
|
+
metadata: dict[str, Any] = {}
|
|
472
|
+
|
|
473
|
+
# Serialization customization
|
|
474
|
+
@field_serializer('tags')
|
|
475
|
+
def serialize_tags(self, tags: list[str]) -> str:
|
|
476
|
+
return ','.join(tags)
|
|
477
|
+
|
|
478
|
+
article = Article(
|
|
479
|
+
title='Pydantic Guide',
|
|
480
|
+
content='...',
|
|
481
|
+
tags=['python', 'validation']
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Dump to dict
|
|
485
|
+
data = article.model_dump()
|
|
486
|
+
# {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}
|
|
487
|
+
|
|
488
|
+
# Exclude fields
|
|
489
|
+
data = article.model_dump(exclude={'metadata'})
|
|
490
|
+
|
|
491
|
+
# Include only specific fields
|
|
492
|
+
data = article.model_dump(include={'title', 'tags'})
|
|
493
|
+
|
|
494
|
+
# Exclude unset fields
|
|
495
|
+
article2 = Article(title='Test', content='...', tags=[])
|
|
496
|
+
data = article2.model_dump(exclude_unset=True) # metadata excluded
|
|
497
|
+
|
|
498
|
+
# By alias
|
|
499
|
+
class AliasModel(BaseModel):
|
|
500
|
+
internal_name: str = Field(alias='externalName')
|
|
501
|
+
|
|
502
|
+
model = AliasModel(externalName='value')
|
|
503
|
+
model.model_dump(by_alias=True) # {'externalName': 'value'}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### JSON Serialization
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
from datetime import datetime
|
|
510
|
+
from pydantic import BaseModel, field_serializer
|
|
511
|
+
|
|
512
|
+
class Event(BaseModel):
|
|
513
|
+
name: str
|
|
514
|
+
timestamp: datetime
|
|
515
|
+
|
|
516
|
+
@field_serializer('timestamp')
|
|
517
|
+
def serialize_dt(self, dt: datetime) -> str:
|
|
518
|
+
return dt.isoformat()
|
|
519
|
+
|
|
520
|
+
event = Event(name='Deploy', timestamp=datetime.now())
|
|
521
|
+
|
|
522
|
+
# Dump to JSON string
|
|
523
|
+
json_str = event.model_dump_json()
|
|
524
|
+
# '{"name":"Deploy","timestamp":"2025-11-30T..."}'
|
|
525
|
+
|
|
526
|
+
# Pretty print
|
|
527
|
+
json_str = event.model_dump_json(indent=2)
|
|
528
|
+
|
|
529
|
+
# Parse from JSON
|
|
530
|
+
event2 = Event.model_validate_json(json_str)
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Custom Serializers
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from pydantic import model_serializer
|
|
537
|
+
|
|
538
|
+
class User(BaseModel):
|
|
539
|
+
id: int
|
|
540
|
+
username: str
|
|
541
|
+
password: SecretStr
|
|
542
|
+
|
|
543
|
+
@model_serializer
|
|
544
|
+
def ser_model(self) -> dict[str, Any]:
|
|
545
|
+
return {
|
|
546
|
+
'id': self.id,
|
|
547
|
+
'username': self.username,
|
|
548
|
+
# Never serialize password
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
user = User(id=1, username='alice', password='secret123')
|
|
552
|
+
assert 'password' not in user.model_dump()
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Settings Management
|
|
556
|
+
|
|
557
|
+
### BaseSettings
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
561
|
+
from pydantic import Field
|
|
562
|
+
|
|
563
|
+
class AppSettings(BaseSettings):
|
|
564
|
+
model_config = SettingsConfigDict(
|
|
565
|
+
env_file='.env',
|
|
566
|
+
env_file_encoding='utf-8',
|
|
567
|
+
env_prefix='APP_',
|
|
568
|
+
case_sensitive=False
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Environment variables
|
|
572
|
+
database_url: str
|
|
573
|
+
redis_url: str = 'redis://localhost:6379'
|
|
574
|
+
secret_key: SecretStr
|
|
575
|
+
debug: bool = False
|
|
576
|
+
|
|
577
|
+
# Nested settings
|
|
578
|
+
class SMTPSettings(BaseModel):
|
|
579
|
+
host: str
|
|
580
|
+
port: int = 587
|
|
581
|
+
username: str
|
|
582
|
+
password: SecretStr
|
|
583
|
+
|
|
584
|
+
smtp: SMTPSettings
|
|
585
|
+
|
|
586
|
+
# Reads from environment variables:
|
|
587
|
+
# APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
|
|
588
|
+
# APP_SMTP__HOST, APP_SMTP__PORT, etc.
|
|
589
|
+
|
|
590
|
+
settings = AppSettings()
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Multi-Environment Settings
|
|
594
|
+
|
|
595
|
+
```python
|
|
596
|
+
from functools import lru_cache
|
|
597
|
+
|
|
598
|
+
class Settings(BaseSettings):
|
|
599
|
+
environment: Literal['dev', 'staging', 'prod'] = 'dev'
|
|
600
|
+
database_url: str
|
|
601
|
+
api_key: SecretStr
|
|
602
|
+
|
|
603
|
+
model_config = SettingsConfigDict(
|
|
604
|
+
env_file='.env',
|
|
605
|
+
extra='ignore'
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def is_production(self) -> bool:
|
|
610
|
+
return self.environment == 'prod'
|
|
611
|
+
|
|
612
|
+
@lru_cache
|
|
613
|
+
def get_settings() -> Settings:
|
|
614
|
+
return Settings()
|
|
615
|
+
|
|
616
|
+
# Usage in FastAPI
|
|
617
|
+
from fastapi import Depends
|
|
618
|
+
|
|
619
|
+
@app.get('/config')
|
|
620
|
+
def get_config(settings: Settings = Depends(get_settings)):
|
|
621
|
+
return {'env': settings.environment}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
## FastAPI Integration
|
|
625
|
+
|
|
626
|
+
### Request/Response Models
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
from fastapi import FastAPI, HTTPException
|
|
630
|
+
from pydantic import BaseModel, EmailStr
|
|
631
|
+
|
|
632
|
+
app = FastAPI()
|
|
633
|
+
|
|
634
|
+
class UserCreate(BaseModel):
|
|
635
|
+
username: str = Field(min_length=3, max_length=50)
|
|
636
|
+
email: EmailStr
|
|
637
|
+
password: str = Field(min_length=8)
|
|
638
|
+
|
|
639
|
+
class UserResponse(BaseModel):
|
|
640
|
+
id: int
|
|
641
|
+
username: str
|
|
642
|
+
email: EmailStr
|
|
643
|
+
|
|
644
|
+
model_config = ConfigDict(from_attributes=True)
|
|
645
|
+
|
|
646
|
+
@app.post('/users', response_model=UserResponse)
|
|
647
|
+
def create_user(user: UserCreate):
|
|
648
|
+
# FastAPI auto-validates request body
|
|
649
|
+
# Returns only fields in UserResponse (password excluded)
|
|
650
|
+
return UserResponse(
|
|
651
|
+
id=1,
|
|
652
|
+
username=user.username,
|
|
653
|
+
email=user.email
|
|
654
|
+
)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Query Parameters
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
from pydantic import BaseModel, Field
|
|
661
|
+
from fastapi import Query
|
|
662
|
+
|
|
663
|
+
class PaginationParams(BaseModel):
|
|
664
|
+
skip: int = Field(0, ge=0)
|
|
665
|
+
limit: int = Field(10, ge=1, le=100)
|
|
666
|
+
|
|
667
|
+
class SearchParams(BaseModel):
|
|
668
|
+
q: str = Field(..., min_length=1)
|
|
669
|
+
category: str | None = None
|
|
670
|
+
sort_by: Literal['date', 'relevance'] = 'relevance'
|
|
671
|
+
|
|
672
|
+
@app.get('/search')
|
|
673
|
+
def search(params: SearchParams = Query()):
|
|
674
|
+
return {'query': params.q, 'sort': params.sort_by}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Response Model Customization
|
|
678
|
+
|
|
679
|
+
```python
|
|
680
|
+
class DetailedUser(BaseModel):
|
|
681
|
+
id: int
|
|
682
|
+
username: str
|
|
683
|
+
email: EmailStr
|
|
684
|
+
created_at: datetime
|
|
685
|
+
last_login: datetime | None
|
|
686
|
+
|
|
687
|
+
@app.get('/users/{user_id}', response_model=DetailedUser)
|
|
688
|
+
def get_user(user_id: int, include_dates: bool = False):
|
|
689
|
+
user = DetailedUser(
|
|
690
|
+
id=user_id,
|
|
691
|
+
username='alice',
|
|
692
|
+
email='alice@example.com',
|
|
693
|
+
created_at=datetime.now(),
|
|
694
|
+
last_login=None
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
if not include_dates:
|
|
698
|
+
return user.model_dump(exclude={'created_at', 'last_login'})
|
|
699
|
+
return user
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## SQLAlchemy Integration
|
|
703
|
+
|
|
704
|
+
### ORM Models with Pydantic
|
|
705
|
+
|
|
706
|
+
```python
|
|
707
|
+
from sqlalchemy import Column, Integer, String, DateTime
|
|
708
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
709
|
+
from pydantic import BaseModel, ConfigDict
|
|
710
|
+
|
|
711
|
+
class Base(DeclarativeBase):
|
|
712
|
+
pass
|
|
713
|
+
|
|
714
|
+
# SQLAlchemy ORM model
|
|
715
|
+
class UserDB(Base):
|
|
716
|
+
__tablename__ = 'users'
|
|
717
|
+
|
|
718
|
+
id = Column(Integer, primary_key=True)
|
|
719
|
+
username = Column(String(50), unique=True)
|
|
720
|
+
email = Column(String(100))
|
|
721
|
+
created_at = Column(DateTime, default=datetime.utcnow)
|
|
722
|
+
|
|
723
|
+
# Pydantic model for validation
|
|
724
|
+
class UserSchema(BaseModel):
|
|
725
|
+
model_config = ConfigDict(from_attributes=True)
|
|
726
|
+
|
|
727
|
+
id: int
|
|
728
|
+
username: str
|
|
729
|
+
email: EmailStr
|
|
730
|
+
created_at: datetime
|
|
731
|
+
|
|
732
|
+
# Usage
|
|
733
|
+
from sqlalchemy.orm import Session
|
|
734
|
+
|
|
735
|
+
def get_user(db: Session, user_id: int) -> UserSchema:
|
|
736
|
+
user = db.query(UserDB).filter(UserDB.id == user_id).first()
|
|
737
|
+
return UserSchema.model_validate(user) # ORM → Pydantic
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Hybrid Approach
|
|
741
|
+
|
|
742
|
+
```python
|
|
743
|
+
from pydantic import BaseModel
|
|
744
|
+
|
|
745
|
+
class UserBase(BaseModel):
|
|
746
|
+
username: str
|
|
747
|
+
email: EmailStr
|
|
748
|
+
|
|
749
|
+
class UserCreate(UserBase):
|
|
750
|
+
password: str
|
|
751
|
+
|
|
752
|
+
class UserUpdate(BaseModel):
|
|
753
|
+
username: str | None = None
|
|
754
|
+
email: EmailStr | None = None
|
|
755
|
+
password: str | None = None
|
|
756
|
+
|
|
757
|
+
class UserInDB(UserBase):
|
|
758
|
+
model_config = ConfigDict(from_attributes=True)
|
|
759
|
+
|
|
760
|
+
id: int
|
|
761
|
+
created_at: datetime
|
|
762
|
+
password_hash: str
|
|
763
|
+
|
|
764
|
+
# CRUD operations
|
|
765
|
+
def create_user(db: Session, user: UserCreate) -> UserInDB:
|
|
766
|
+
db_user = UserDB(
|
|
767
|
+
username=user.username,
|
|
768
|
+
email=user.email,
|
|
769
|
+
password_hash=hash_password(user.password)
|
|
770
|
+
)
|
|
771
|
+
db.add(db_user)
|
|
772
|
+
db.commit()
|
|
773
|
+
db.refresh(db_user)
|
|
774
|
+
return UserInDB.model_validate(db_user)
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## Django Integration
|
|
778
|
+
|
|
779
|
+
### Django Model Validation
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
from django.db import models
|
|
783
|
+
from pydantic import BaseModel, field_validator
|
|
784
|
+
|
|
785
|
+
# Django model
|
|
786
|
+
class Article(models.Model):
|
|
787
|
+
title = models.CharField(max_length=200)
|
|
788
|
+
content = models.TextField()
|
|
789
|
+
published = models.BooleanField(default=False)
|
|
790
|
+
|
|
791
|
+
# Pydantic schema
|
|
792
|
+
class ArticleSchema(BaseModel):
|
|
793
|
+
model_config = ConfigDict(from_attributes=True)
|
|
794
|
+
|
|
795
|
+
title: str = Field(max_length=200)
|
|
796
|
+
content: str
|
|
797
|
+
published: bool = False
|
|
798
|
+
|
|
799
|
+
@field_validator('content')
|
|
800
|
+
@classmethod
|
|
801
|
+
def validate_content(cls, v: str) -> str:
|
|
802
|
+
if len(v) < 100:
|
|
803
|
+
raise ValueError('Content too short')
|
|
804
|
+
return v
|
|
805
|
+
|
|
806
|
+
# Usage in Django views
|
|
807
|
+
from django.http import JsonResponse
|
|
808
|
+
from django.views.decorators.http import require_http_methods
|
|
809
|
+
|
|
810
|
+
@require_http_methods(['POST'])
|
|
811
|
+
def create_article(request):
|
|
812
|
+
try:
|
|
813
|
+
data = ArticleSchema.model_validate_json(request.body)
|
|
814
|
+
article = Article.objects.create(**data.model_dump())
|
|
815
|
+
return JsonResponse({'id': article.id})
|
|
816
|
+
except ValidationError as e:
|
|
817
|
+
return JsonResponse({'errors': e.errors()}, status=400)
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
## Computed Fields
|
|
821
|
+
|
|
822
|
+
```python
|
|
823
|
+
from pydantic import computed_field
|
|
824
|
+
|
|
825
|
+
class Rectangle(BaseModel):
|
|
826
|
+
width: float
|
|
827
|
+
height: float
|
|
828
|
+
|
|
829
|
+
@computed_field
|
|
830
|
+
@property
|
|
831
|
+
def area(self) -> float:
|
|
832
|
+
return self.width * self.height
|
|
833
|
+
|
|
834
|
+
@computed_field
|
|
835
|
+
@property
|
|
836
|
+
def perimeter(self) -> float:
|
|
837
|
+
return 2 * (self.width + self.height)
|
|
838
|
+
|
|
839
|
+
rect = Rectangle(width=10, height=5)
|
|
840
|
+
assert rect.area == 50
|
|
841
|
+
assert rect.perimeter == 30
|
|
842
|
+
|
|
843
|
+
# Computed fields in serialization
|
|
844
|
+
data = rect.model_dump()
|
|
845
|
+
# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
## Custom Errors
|
|
849
|
+
|
|
850
|
+
```python
|
|
851
|
+
from pydantic import BaseModel, field_validator, ValidationError
|
|
852
|
+
from pydantic_core import PydanticCustomError
|
|
853
|
+
|
|
854
|
+
class StrictUser(BaseModel):
|
|
855
|
+
username: str
|
|
856
|
+
age: int
|
|
857
|
+
|
|
858
|
+
@field_validator('username')
|
|
859
|
+
@classmethod
|
|
860
|
+
def validate_username(cls, v: str) -> str:
|
|
861
|
+
if len(v) < 3:
|
|
862
|
+
raise PydanticCustomError(
|
|
863
|
+
'username_too_short',
|
|
864
|
+
'Username must be at least 3 characters',
|
|
865
|
+
{'min_length': 3, 'actual_length': len(v)}
|
|
866
|
+
)
|
|
867
|
+
return v
|
|
868
|
+
|
|
869
|
+
@field_validator('age')
|
|
870
|
+
@classmethod
|
|
871
|
+
def validate_age(cls, v: int) -> int:
|
|
872
|
+
if v < 18:
|
|
873
|
+
raise PydanticCustomError(
|
|
874
|
+
'underage',
|
|
875
|
+
'User must be at least 18 years old',
|
|
876
|
+
{'age': v, 'minimum_age': 18}
|
|
877
|
+
)
|
|
878
|
+
return v
|
|
879
|
+
|
|
880
|
+
# Custom error handling
|
|
881
|
+
try:
|
|
882
|
+
StrictUser(username='ab', age=16)
|
|
883
|
+
except ValidationError as e:
|
|
884
|
+
for error in e.errors():
|
|
885
|
+
print(f"{error['type']}: {error['msg']}")
|
|
886
|
+
print(f"Context: {error.get('ctx')}")
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
## Performance Optimization
|
|
890
|
+
|
|
891
|
+
### V2 Rust Core Benefits
|
|
892
|
+
|
|
893
|
+
```python
|
|
894
|
+
# Pydantic v2 uses pydantic-core (Rust) for:
|
|
895
|
+
# - 5-50x faster validation
|
|
896
|
+
# - Lower memory usage
|
|
897
|
+
# - Better error messages
|
|
898
|
+
# - Improved JSON parsing
|
|
899
|
+
|
|
900
|
+
import timeit
|
|
901
|
+
from pydantic import BaseModel
|
|
902
|
+
|
|
903
|
+
class Data(BaseModel):
|
|
904
|
+
values: list[int]
|
|
905
|
+
names: list[str]
|
|
906
|
+
metadata: dict[str, Any]
|
|
907
|
+
|
|
908
|
+
# Benchmark
|
|
909
|
+
data_dict = {
|
|
910
|
+
'values': list(range(1000)),
|
|
911
|
+
'names': ['item'] * 1000,
|
|
912
|
+
'metadata': {'key': 'value'}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
def validate():
|
|
916
|
+
Data.model_validate(data_dict)
|
|
917
|
+
|
|
918
|
+
time_taken = timeit.timeit(validate, number=10000)
|
|
919
|
+
print(f"10000 validations: {time_taken:.2f}s")
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Optimization Techniques
|
|
923
|
+
|
|
924
|
+
```python
|
|
925
|
+
from pydantic import BaseModel, ConfigDict
|
|
926
|
+
|
|
927
|
+
class OptimizedModel(BaseModel):
|
|
928
|
+
model_config = ConfigDict(
|
|
929
|
+
# Validate assignment only when needed
|
|
930
|
+
validate_assignment=False,
|
|
931
|
+
|
|
932
|
+
# Disable validation for internal use
|
|
933
|
+
validate_default=False,
|
|
934
|
+
|
|
935
|
+
# Use slots for memory efficiency
|
|
936
|
+
# (Not available in Pydantic v2 BaseModel directly)
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
data: list[int]
|
|
940
|
+
|
|
941
|
+
# Reuse validators
|
|
942
|
+
from functools import lru_cache
|
|
943
|
+
|
|
944
|
+
@lru_cache(maxsize=128)
|
|
945
|
+
def get_validator(model_class):
|
|
946
|
+
return model_class.model_validate
|
|
947
|
+
|
|
948
|
+
# Bulk validation
|
|
949
|
+
def validate_bulk(items: list[dict]) -> list[Data]:
|
|
950
|
+
validator = get_validator(Data)
|
|
951
|
+
return [validator(item) for item in items]
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
## JSON Schema Generation
|
|
955
|
+
|
|
956
|
+
```python
|
|
957
|
+
from pydantic import BaseModel, Field
|
|
958
|
+
|
|
959
|
+
class Product(BaseModel):
|
|
960
|
+
"""Product model for catalog"""
|
|
961
|
+
|
|
962
|
+
id: int = Field(description="Unique product identifier")
|
|
963
|
+
name: str = Field(description="Product name", examples=["Widget"])
|
|
964
|
+
price: float = Field(gt=0, description="Price in USD")
|
|
965
|
+
tags: list[str] = Field(default=[], description="Product tags")
|
|
966
|
+
|
|
967
|
+
# Generate JSON Schema
|
|
968
|
+
schema = Product.model_json_schema()
|
|
969
|
+
print(json.dumps(schema, indent=2))
|
|
970
|
+
# {
|
|
971
|
+
# "title": "Product",
|
|
972
|
+
# "description": "Product model for catalog",
|
|
973
|
+
# "type": "object",
|
|
974
|
+
# "properties": {
|
|
975
|
+
# "id": {"type": "integer", "description": "Unique product identifier"},
|
|
976
|
+
# "name": {"type": "string", "description": "Product name"},
|
|
977
|
+
# ...
|
|
978
|
+
# },
|
|
979
|
+
# "required": ["id", "name", "price"]
|
|
980
|
+
# }
|
|
981
|
+
|
|
982
|
+
# OpenAPI compatible
|
|
983
|
+
from fastapi import FastAPI
|
|
984
|
+
|
|
985
|
+
app = FastAPI()
|
|
986
|
+
|
|
987
|
+
@app.post('/products')
|
|
988
|
+
def create_product(product: Product):
|
|
989
|
+
return product
|
|
990
|
+
|
|
991
|
+
# FastAPI auto-generates OpenAPI schema from Pydantic models
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
## Dataclass Integration
|
|
995
|
+
|
|
996
|
+
```python
|
|
997
|
+
from pydantic.dataclasses import dataclass
|
|
998
|
+
from pydantic import Field
|
|
999
|
+
|
|
1000
|
+
@dataclass
|
|
1001
|
+
class User:
|
|
1002
|
+
id: int
|
|
1003
|
+
name: str = Field(min_length=1)
|
|
1004
|
+
email: str = Field(pattern=r'.+@.+\..+')
|
|
1005
|
+
|
|
1006
|
+
# Works like Pydantic BaseModel with validation
|
|
1007
|
+
user = User(id=1, name='Alice', email='alice@example.com')
|
|
1008
|
+
|
|
1009
|
+
# Validation on construction
|
|
1010
|
+
try:
|
|
1011
|
+
User(id=2, name='', email='invalid')
|
|
1012
|
+
except ValidationError as e:
|
|
1013
|
+
print(e.errors())
|
|
1014
|
+
|
|
1015
|
+
# Convert to Pydantic BaseModel
|
|
1016
|
+
from pydantic import BaseModel
|
|
1017
|
+
|
|
1018
|
+
class UserModel(BaseModel):
|
|
1019
|
+
model_config = ConfigDict(from_attributes=True)
|
|
1020
|
+
|
|
1021
|
+
id: int
|
|
1022
|
+
name: str
|
|
1023
|
+
email: str
|
|
1024
|
+
|
|
1025
|
+
user_model = UserModel.model_validate(user)
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
## Testing Strategies
|
|
1029
|
+
|
|
1030
|
+
### Unit Testing Models
|
|
1031
|
+
|
|
1032
|
+
```python
|
|
1033
|
+
import pytest
|
|
1034
|
+
from pydantic import ValidationError
|
|
1035
|
+
|
|
1036
|
+
def test_user_validation():
|
|
1037
|
+
# Valid data
|
|
1038
|
+
user = User(id=1, name='Alice', email='alice@example.com')
|
|
1039
|
+
assert user.name == 'Alice'
|
|
1040
|
+
|
|
1041
|
+
# Invalid data
|
|
1042
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
1043
|
+
User(id='invalid', name='Bob', email='bob@example.com')
|
|
1044
|
+
|
|
1045
|
+
errors = exc_info.value.errors()
|
|
1046
|
+
assert errors[0]['type'] == 'int_parsing'
|
|
1047
|
+
|
|
1048
|
+
def test_user_serialization():
|
|
1049
|
+
user = User(id=1, name='Alice', email='alice@example.com')
|
|
1050
|
+
data = user.model_dump()
|
|
1051
|
+
|
|
1052
|
+
assert data == {
|
|
1053
|
+
'id': 1,
|
|
1054
|
+
'name': 'Alice',
|
|
1055
|
+
'email': 'alice@example.com'
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
def test_nested_validation():
|
|
1059
|
+
company = Company(
|
|
1060
|
+
name='ACME',
|
|
1061
|
+
address={'street': '123 Main', 'city': 'NYC', 'country': 'USA'}
|
|
1062
|
+
)
|
|
1063
|
+
assert company.address.city == 'NYC'
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### Testing with Fixtures
|
|
1067
|
+
|
|
1068
|
+
```python
|
|
1069
|
+
@pytest.fixture
|
|
1070
|
+
def sample_user_data():
|
|
1071
|
+
return {
|
|
1072
|
+
'id': 1,
|
|
1073
|
+
'name': 'Alice',
|
|
1074
|
+
'email': 'alice@example.com'
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
@pytest.fixture
|
|
1078
|
+
def sample_user(sample_user_data):
|
|
1079
|
+
return User(**sample_user_data)
|
|
1080
|
+
|
|
1081
|
+
def test_with_fixtures(sample_user):
|
|
1082
|
+
assert sample_user.name == 'Alice'
|
|
1083
|
+
|
|
1084
|
+
def test_invalid_email(sample_user_data):
|
|
1085
|
+
sample_user_data['email'] = 'invalid'
|
|
1086
|
+
with pytest.raises(ValidationError):
|
|
1087
|
+
User(**sample_user_data)
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Property-Based Testing
|
|
1091
|
+
|
|
1092
|
+
```python
|
|
1093
|
+
from hypothesis import given, strategies as st
|
|
1094
|
+
|
|
1095
|
+
@given(
|
|
1096
|
+
id=st.integers(min_value=1),
|
|
1097
|
+
name=st.text(min_size=1, max_size=100),
|
|
1098
|
+
email=st.emails()
|
|
1099
|
+
)
|
|
1100
|
+
def test_user_always_valid(id, name, email):
|
|
1101
|
+
user = User(id=id, name=name, email=email)
|
|
1102
|
+
assert user.id == id
|
|
1103
|
+
assert user.name == name
|
|
1104
|
+
assert user.email == email
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
## Migration Guide (v1 → v2)
|
|
1108
|
+
|
|
1109
|
+
### Key Changes
|
|
1110
|
+
|
|
1111
|
+
```python
|
|
1112
|
+
# v1
|
|
1113
|
+
from pydantic import BaseModel
|
|
1114
|
+
|
|
1115
|
+
class OldModel(BaseModel):
|
|
1116
|
+
class Config:
|
|
1117
|
+
validate_assignment = True
|
|
1118
|
+
arbitrary_types_allowed = True
|
|
1119
|
+
|
|
1120
|
+
# Validators
|
|
1121
|
+
@validator('field')
|
|
1122
|
+
def validate_field(cls, v):
|
|
1123
|
+
return v
|
|
1124
|
+
|
|
1125
|
+
@root_validator
|
|
1126
|
+
def validate_model(cls, values):
|
|
1127
|
+
return values
|
|
1128
|
+
|
|
1129
|
+
# Serialization
|
|
1130
|
+
data = model.dict()
|
|
1131
|
+
json_str = model.json()
|
|
1132
|
+
|
|
1133
|
+
# Parsing
|
|
1134
|
+
model = OldModel.parse_obj(data)
|
|
1135
|
+
model = OldModel.parse_raw(json_str)
|
|
1136
|
+
|
|
1137
|
+
# v2
|
|
1138
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
1139
|
+
|
|
1140
|
+
class NewModel(BaseModel):
|
|
1141
|
+
model_config = ConfigDict(
|
|
1142
|
+
validate_assignment=True,
|
|
1143
|
+
arbitrary_types_allowed=True
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# Field validators
|
|
1147
|
+
@field_validator('field')
|
|
1148
|
+
@classmethod
|
|
1149
|
+
def validate_field(cls, v):
|
|
1150
|
+
return v
|
|
1151
|
+
|
|
1152
|
+
# Model validators
|
|
1153
|
+
@model_validator(mode='after')
|
|
1154
|
+
def validate_model(self):
|
|
1155
|
+
return self
|
|
1156
|
+
|
|
1157
|
+
# Serialization
|
|
1158
|
+
data = model.model_dump()
|
|
1159
|
+
json_str = model.model_dump_json()
|
|
1160
|
+
|
|
1161
|
+
# Parsing
|
|
1162
|
+
model = NewModel.model_validate(data)
|
|
1163
|
+
model = NewModel.model_validate_json(json_str)
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### Migration Checklist
|
|
1167
|
+
|
|
1168
|
+
* [ ] Replace `class Config` with `model_config = ConfigDict()`
|
|
1169
|
+
* [ ] Update `.dict()` → `.model_dump()`
|
|
1170
|
+
* [ ] Update `.json()` → `.model_dump_json()`
|
|
1171
|
+
* [ ] Update `.parse_obj()` → `.model_validate()`
|
|
1172
|
+
* [ ] Update `.parse_raw()` → `.model_validate_json()`
|
|
1173
|
+
* [ ] Update `@validator` → `@field_validator` with `@classmethod`
|
|
1174
|
+
* [ ] Update `@root_validator` → `@model_validator(mode='after')`
|
|
1175
|
+
* [ ] Review `json_encoders` → use `@field_serializer`
|
|
1176
|
+
* [ ] Test strict mode behavior changes
|
|
1177
|
+
* [ ] Update custom types to use `__get_pydantic_core_schema__`
|
|
1178
|
+
|
|
1179
|
+
## Best Practices
|
|
1180
|
+
|
|
1181
|
+
### Model Organization
|
|
1182
|
+
|
|
1183
|
+
```python
|
|
1184
|
+
# Separate schemas by use case
|
|
1185
|
+
class UserBase(BaseModel):
|
|
1186
|
+
"""Shared fields"""
|
|
1187
|
+
username: str
|
|
1188
|
+
email: EmailStr
|
|
1189
|
+
|
|
1190
|
+
class UserCreate(UserBase):
|
|
1191
|
+
"""API request for creating user"""
|
|
1192
|
+
password: str
|
|
1193
|
+
|
|
1194
|
+
class UserUpdate(BaseModel):
|
|
1195
|
+
"""API request for updating user (all optional)"""
|
|
1196
|
+
username: str | None = None
|
|
1197
|
+
email: EmailStr | None = None
|
|
1198
|
+
password: str | None = None
|
|
1199
|
+
|
|
1200
|
+
class UserInDB(UserBase):
|
|
1201
|
+
"""Database representation"""
|
|
1202
|
+
model_config = ConfigDict(from_attributes=True)
|
|
1203
|
+
|
|
1204
|
+
id: int
|
|
1205
|
+
password_hash: str
|
|
1206
|
+
created_at: datetime
|
|
1207
|
+
|
|
1208
|
+
class UserResponse(UserBase):
|
|
1209
|
+
"""API response (excludes sensitive data)"""
|
|
1210
|
+
id: int
|
|
1211
|
+
created_at: datetime
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### Validation Best Practices
|
|
1215
|
+
|
|
1216
|
+
```python
|
|
1217
|
+
# Use Field for constraints, not validators
|
|
1218
|
+
class Good(BaseModel):
|
|
1219
|
+
age: int = Field(ge=0, le=150)
|
|
1220
|
+
email: EmailStr
|
|
1221
|
+
|
|
1222
|
+
class Bad(BaseModel):
|
|
1223
|
+
age: int
|
|
1224
|
+
email: str
|
|
1225
|
+
|
|
1226
|
+
@field_validator('age')
|
|
1227
|
+
@classmethod
|
|
1228
|
+
def validate_age(cls, v):
|
|
1229
|
+
if v < 0 or v > 150:
|
|
1230
|
+
raise ValueError('invalid age')
|
|
1231
|
+
return v
|
|
1232
|
+
|
|
1233
|
+
# Prefer composition over inheritance
|
|
1234
|
+
class TimestampMixin(BaseModel):
|
|
1235
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
1236
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
1237
|
+
|
|
1238
|
+
class User(TimestampMixin):
|
|
1239
|
+
username: str
|
|
1240
|
+
email: EmailStr
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
### Error Handling
|
|
1244
|
+
|
|
1245
|
+
```python
|
|
1246
|
+
from pydantic import ValidationError
|
|
1247
|
+
|
|
1248
|
+
def safe_validate(data: dict) -> User | None:
|
|
1249
|
+
try:
|
|
1250
|
+
return User.model_validate(data)
|
|
1251
|
+
except ValidationError as e:
|
|
1252
|
+
# Log validation errors
|
|
1253
|
+
logger.error(f"Validation failed: {e.errors()}")
|
|
1254
|
+
return None
|
|
1255
|
+
|
|
1256
|
+
def validate_with_details(data: dict):
|
|
1257
|
+
try:
|
|
1258
|
+
return User.model_validate(data)
|
|
1259
|
+
except ValidationError as e:
|
|
1260
|
+
# Return user-friendly errors
|
|
1261
|
+
return {
|
|
1262
|
+
'success': False,
|
|
1263
|
+
'errors': [
|
|
1264
|
+
{
|
|
1265
|
+
'field': '.'.join(str(loc) for loc in err['loc']),
|
|
1266
|
+
'message': err['msg'],
|
|
1267
|
+
'type': err['type']
|
|
1268
|
+
}
|
|
1269
|
+
for err in e.errors()
|
|
1270
|
+
]
|
|
1271
|
+
}
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
## Common Patterns
|
|
1275
|
+
|
|
1276
|
+
### API Response Wrapper
|
|
1277
|
+
|
|
1278
|
+
```python
|
|
1279
|
+
from typing import Generic, TypeVar
|
|
1280
|
+
|
|
1281
|
+
T = TypeVar('T')
|
|
1282
|
+
|
|
1283
|
+
class APIResponse(BaseModel, Generic[T]):
|
|
1284
|
+
success: bool
|
|
1285
|
+
data: T | None = None
|
|
1286
|
+
error: str | None = None
|
|
1287
|
+
metadata: dict[str, Any] = {}
|
|
1288
|
+
|
|
1289
|
+
# Usage
|
|
1290
|
+
user_response = APIResponse[User](
|
|
1291
|
+
success=True,
|
|
1292
|
+
data=User(id=1, name='Alice', email='alice@example.com')
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
error_response = APIResponse[User](
|
|
1296
|
+
success=False,
|
|
1297
|
+
error='User not found'
|
|
1298
|
+
)
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### Pagination
|
|
1302
|
+
|
|
1303
|
+
```python
|
|
1304
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
1305
|
+
items: list[T]
|
|
1306
|
+
total: int
|
|
1307
|
+
page: int
|
|
1308
|
+
page_size: int
|
|
1309
|
+
|
|
1310
|
+
@computed_field
|
|
1311
|
+
@property
|
|
1312
|
+
def total_pages(self) -> int:
|
|
1313
|
+
return (self.total + self.page_size - 1) // self.page_size
|
|
1314
|
+
|
|
1315
|
+
users = PaginatedResponse[User](
|
|
1316
|
+
items=[...],
|
|
1317
|
+
total=100,
|
|
1318
|
+
page=1,
|
|
1319
|
+
page_size=10
|
|
1320
|
+
)
|
|
1321
|
+
assert users.total_pages == 10
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
### Audit Fields
|
|
1325
|
+
|
|
1326
|
+
```python
|
|
1327
|
+
class AuditMixin(BaseModel):
|
|
1328
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
1329
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
1330
|
+
created_by: int | None = None
|
|
1331
|
+
updated_by: int | None = None
|
|
1332
|
+
|
|
1333
|
+
class Document(AuditMixin):
|
|
1334
|
+
title: str
|
|
1335
|
+
content: str
|
|
1336
|
+
|
|
1337
|
+
@model_validator(mode='before')
|
|
1338
|
+
@classmethod
|
|
1339
|
+
def update_timestamp(cls, data: dict) -> dict:
|
|
1340
|
+
if isinstance(data, dict):
|
|
1341
|
+
data['updated_at'] = datetime.utcnow()
|
|
1342
|
+
return data
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
## Related Skills
|
|
1346
|
+
|
|
1347
|
+
When using Pydantic, consider these complementary skills:
|
|
1348
|
+
|
|
1349
|
+
* **fastapi-local-dev**: FastAPI development server patterns with Pydantic integration
|
|
1350
|
+
* **sqlalchemy**: SQLAlchemy ORM patterns for database models with Pydantic validation
|
|
1351
|
+
* **django**: Django framework integration with Pydantic schemas
|
|
1352
|
+
* **pytest**: Testing strategies for Pydantic models and validation
|
|
1353
|
+
|
|
1354
|
+
### Quick FastAPI Integration Reference (Inlined for Standalone Use)
|
|
1355
|
+
|
|
1356
|
+
```python
|
|
1357
|
+
# FastAPI with Pydantic (basic pattern)
|
|
1358
|
+
from fastapi import FastAPI, HTTPException
|
|
1359
|
+
from pydantic import BaseModel, EmailStr
|
|
1360
|
+
|
|
1361
|
+
app = FastAPI()
|
|
1362
|
+
|
|
1363
|
+
class UserCreate(BaseModel):
|
|
1364
|
+
username: str
|
|
1365
|
+
email: EmailStr
|
|
1366
|
+
password: str
|
|
1367
|
+
|
|
1368
|
+
class UserResponse(BaseModel):
|
|
1369
|
+
id: int
|
|
1370
|
+
username: str
|
|
1371
|
+
email: EmailStr
|
|
1372
|
+
|
|
1373
|
+
model_config = ConfigDict(from_attributes=True)
|
|
1374
|
+
|
|
1375
|
+
@app.post('/users', response_model=UserResponse)
|
|
1376
|
+
def create_user(user: UserCreate):
|
|
1377
|
+
# FastAPI auto-validates using Pydantic
|
|
1378
|
+
# response_model filters out password
|
|
1379
|
+
return UserResponse(id=1, username=user.username, email=user.email)
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
### Quick SQLAlchemy Integration Reference (Inlined for Standalone Use)
|
|
1383
|
+
|
|
1384
|
+
```python
|
|
1385
|
+
# SQLAlchemy 2.0 with Pydantic validation
|
|
1386
|
+
from sqlalchemy import Column, Integer, String
|
|
1387
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
1388
|
+
from pydantic import BaseModel, ConfigDict
|
|
1389
|
+
|
|
1390
|
+
class Base(DeclarativeBase):
|
|
1391
|
+
pass
|
|
1392
|
+
|
|
1393
|
+
class UserDB(Base):
|
|
1394
|
+
__tablename__ = 'users'
|
|
1395
|
+
id = Column(Integer, primary_key=True)
|
|
1396
|
+
username = Column(String(50))
|
|
1397
|
+
email = Column(String(100))
|
|
1398
|
+
|
|
1399
|
+
class UserSchema(BaseModel):
|
|
1400
|
+
model_config = ConfigDict(from_attributes=True)
|
|
1401
|
+
id: int
|
|
1402
|
+
username: str
|
|
1403
|
+
email: str
|
|
1404
|
+
|
|
1405
|
+
# Convert ORM to Pydantic
|
|
1406
|
+
user_orm = db.query(UserDB).first()
|
|
1407
|
+
user_validated = UserSchema.model_validate(user_orm)
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
### Quick Pytest Testing Reference (Inlined for Standalone Use)
|
|
1411
|
+
|
|
1412
|
+
```python
|
|
1413
|
+
# Testing Pydantic models with pytest
|
|
1414
|
+
import pytest
|
|
1415
|
+
from pydantic import ValidationError
|
|
1416
|
+
|
|
1417
|
+
def test_user_validation():
|
|
1418
|
+
user = User(id=1, name='Alice', email='alice@example.com')
|
|
1419
|
+
assert user.name == 'Alice'
|
|
1420
|
+
|
|
1421
|
+
def test_validation_error():
|
|
1422
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
1423
|
+
User(id='invalid', name='Bob', email='bob@example.com')
|
|
1424
|
+
errors = exc_info.value.errors()
|
|
1425
|
+
assert errors[0]['type'] == 'int_parsing'
|
|
1426
|
+
|
|
1427
|
+
@pytest.fixture
|
|
1428
|
+
def sample_user():
|
|
1429
|
+
return User(id=1, name='Alice', email='alice@example.com')
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
[Full integration patterns available in respective skills if deployed together]
|
|
1433
|
+
|
|
1434
|
+
## Additional Resources
|
|
1435
|
+
|
|
1436
|
+
* [Pydantic Documentation](https://docs.pydantic.dev/)
|
|
1437
|
+
* [Migration Guide v1→v2](https://docs.pydantic.dev/latest/migration/)
|
|
1438
|
+
* [Performance Benchmarks](https://docs.pydantic.dev/latest/concepts/performance/)
|
|
1439
|
+
* [JSON Schema Integration](https://docs.pydantic.dev/latest/concepts/json_schema/)
|