@malamute/ai-rules 1.0.0 → 1.3.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/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/schemas/**/*.py"
|
|
4
|
+
- "**/models/**/*.py"
|
|
5
|
+
- "**/*_schema.py"
|
|
6
|
+
- "**/*_model.py"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Python Validation (Pydantic)
|
|
10
|
+
|
|
11
|
+
## Basic Models
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# schemas/user.py
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from pydantic import BaseModel, EmailStr, Field, field_validator
|
|
18
|
+
from uuid import UUID
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UserBase(BaseModel):
|
|
22
|
+
email: EmailStr
|
|
23
|
+
name: str = Field(..., min_length=2, max_length=100)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UserCreate(UserBase):
|
|
27
|
+
password: str = Field(..., min_length=8)
|
|
28
|
+
|
|
29
|
+
@field_validator("password")
|
|
30
|
+
@classmethod
|
|
31
|
+
def validate_password(cls, v: str) -> str:
|
|
32
|
+
if not any(c.isupper() for c in v):
|
|
33
|
+
raise ValueError("Password must contain uppercase letter")
|
|
34
|
+
if not any(c.islower() for c in v):
|
|
35
|
+
raise ValueError("Password must contain lowercase letter")
|
|
36
|
+
if not any(c.isdigit() for c in v):
|
|
37
|
+
raise ValueError("Password must contain digit")
|
|
38
|
+
if not any(c in "!@#$%^&*" for c in v):
|
|
39
|
+
raise ValueError("Password must contain special character")
|
|
40
|
+
return v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserUpdate(BaseModel):
|
|
44
|
+
name: Optional[str] = Field(None, min_length=2, max_length=100)
|
|
45
|
+
phone: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserResponse(UserBase):
|
|
49
|
+
id: UUID
|
|
50
|
+
is_active: bool
|
|
51
|
+
created_at: datetime
|
|
52
|
+
|
|
53
|
+
model_config = {"from_attributes": True}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Nested Models
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# schemas/order.py
|
|
60
|
+
from pydantic import BaseModel, Field, model_validator
|
|
61
|
+
from typing import List
|
|
62
|
+
from decimal import Decimal
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OrderItem(BaseModel):
|
|
66
|
+
product_id: UUID
|
|
67
|
+
quantity: int = Field(..., gt=0, le=100)
|
|
68
|
+
price: Decimal = Field(..., gt=0, decimal_places=2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OrderCreate(BaseModel):
|
|
72
|
+
items: List[OrderItem] = Field(..., min_length=1)
|
|
73
|
+
shipping_address: str = Field(..., min_length=10)
|
|
74
|
+
notes: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
@model_validator(mode="after")
|
|
77
|
+
def validate_order(self) -> "OrderCreate":
|
|
78
|
+
total_items = sum(item.quantity for item in self.items)
|
|
79
|
+
if total_items > 100:
|
|
80
|
+
raise ValueError("Maximum 100 items per order")
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OrderResponse(BaseModel):
|
|
85
|
+
id: UUID
|
|
86
|
+
items: List[OrderItem]
|
|
87
|
+
total: Decimal
|
|
88
|
+
status: str
|
|
89
|
+
created_at: datetime
|
|
90
|
+
|
|
91
|
+
model_config = {"from_attributes": True}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Custom Types
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# schemas/types.py
|
|
98
|
+
from typing import Annotated
|
|
99
|
+
from pydantic import AfterValidator, BeforeValidator
|
|
100
|
+
import re
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def validate_phone(v: str) -> str:
|
|
104
|
+
pattern = r"^\+?[1-9]\d{1,14}$"
|
|
105
|
+
if not re.match(pattern, v):
|
|
106
|
+
raise ValueError("Invalid phone number format")
|
|
107
|
+
return v
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_slug(v: str) -> str:
|
|
111
|
+
pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
112
|
+
if not re.match(pattern, v):
|
|
113
|
+
raise ValueError("Invalid slug format")
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def normalize_email(v: str) -> str:
|
|
118
|
+
return v.lower().strip()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
PhoneNumber = Annotated[str, AfterValidator(validate_phone)]
|
|
122
|
+
Slug = Annotated[str, AfterValidator(validate_slug)]
|
|
123
|
+
NormalizedEmail = Annotated[str, BeforeValidator(normalize_email)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Usage
|
|
127
|
+
class UserCreate(BaseModel):
|
|
128
|
+
email: NormalizedEmail
|
|
129
|
+
phone: Optional[PhoneNumber] = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class PostCreate(BaseModel):
|
|
133
|
+
title: str
|
|
134
|
+
slug: Slug
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Enum Validation
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# schemas/enums.py
|
|
141
|
+
from enum import Enum
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UserRole(str, Enum):
|
|
145
|
+
USER = "user"
|
|
146
|
+
ADMIN = "admin"
|
|
147
|
+
MODERATOR = "moderator"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class OrderStatus(str, Enum):
|
|
151
|
+
PENDING = "pending"
|
|
152
|
+
PROCESSING = "processing"
|
|
153
|
+
SHIPPED = "shipped"
|
|
154
|
+
DELIVERED = "delivered"
|
|
155
|
+
CANCELLED = "cancelled"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Usage
|
|
159
|
+
class UserCreate(BaseModel):
|
|
160
|
+
email: EmailStr
|
|
161
|
+
role: UserRole = UserRole.USER
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class OrderUpdate(BaseModel):
|
|
165
|
+
status: OrderStatus
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Computed Fields
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# schemas/product.py
|
|
172
|
+
from pydantic import BaseModel, computed_field
|
|
173
|
+
from decimal import Decimal
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Product(BaseModel):
|
|
177
|
+
name: str
|
|
178
|
+
price: Decimal
|
|
179
|
+
discount_percent: int = 0
|
|
180
|
+
|
|
181
|
+
@computed_field
|
|
182
|
+
@property
|
|
183
|
+
def discounted_price(self) -> Decimal:
|
|
184
|
+
discount = self.price * self.discount_percent / 100
|
|
185
|
+
return self.price - discount
|
|
186
|
+
|
|
187
|
+
@computed_field
|
|
188
|
+
@property
|
|
189
|
+
def display_name(self) -> str:
|
|
190
|
+
if self.discount_percent > 0:
|
|
191
|
+
return f"{self.name} ({self.discount_percent}% off)"
|
|
192
|
+
return self.name
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Conditional Validation
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# schemas/payment.py
|
|
199
|
+
from pydantic import BaseModel, model_validator
|
|
200
|
+
from typing import Optional
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class PaymentMethod(str, Enum):
|
|
204
|
+
CREDIT_CARD = "credit_card"
|
|
205
|
+
BANK_TRANSFER = "bank_transfer"
|
|
206
|
+
PAYPAL = "paypal"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class PaymentCreate(BaseModel):
|
|
210
|
+
method: PaymentMethod
|
|
211
|
+
amount: Decimal
|
|
212
|
+
|
|
213
|
+
# Credit card fields
|
|
214
|
+
card_number: Optional[str] = None
|
|
215
|
+
expiry_date: Optional[str] = None
|
|
216
|
+
cvv: Optional[str] = None
|
|
217
|
+
|
|
218
|
+
# Bank transfer fields
|
|
219
|
+
bank_account: Optional[str] = None
|
|
220
|
+
routing_number: Optional[str] = None
|
|
221
|
+
|
|
222
|
+
# PayPal fields
|
|
223
|
+
paypal_email: Optional[EmailStr] = None
|
|
224
|
+
|
|
225
|
+
@model_validator(mode="after")
|
|
226
|
+
def validate_payment_details(self) -> "PaymentCreate":
|
|
227
|
+
if self.method == PaymentMethod.CREDIT_CARD:
|
|
228
|
+
if not all([self.card_number, self.expiry_date, self.cvv]):
|
|
229
|
+
raise ValueError("Card details required for credit card payment")
|
|
230
|
+
|
|
231
|
+
elif self.method == PaymentMethod.BANK_TRANSFER:
|
|
232
|
+
if not all([self.bank_account, self.routing_number]):
|
|
233
|
+
raise ValueError("Bank details required for bank transfer")
|
|
234
|
+
|
|
235
|
+
elif self.method == PaymentMethod.PAYPAL:
|
|
236
|
+
if not self.paypal_email:
|
|
237
|
+
raise ValueError("PayPal email required")
|
|
238
|
+
|
|
239
|
+
return self
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Generic Pagination
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
# schemas/pagination.py
|
|
246
|
+
from typing import Generic, TypeVar, List
|
|
247
|
+
from pydantic import BaseModel
|
|
248
|
+
|
|
249
|
+
T = TypeVar("T")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
253
|
+
items: List[T]
|
|
254
|
+
total: int
|
|
255
|
+
page: int
|
|
256
|
+
size: int
|
|
257
|
+
pages: int
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def create(
|
|
261
|
+
cls,
|
|
262
|
+
items: List[T],
|
|
263
|
+
total: int,
|
|
264
|
+
page: int,
|
|
265
|
+
size: int,
|
|
266
|
+
) -> "PaginatedResponse[T]":
|
|
267
|
+
return cls(
|
|
268
|
+
items=items,
|
|
269
|
+
total=total,
|
|
270
|
+
page=page,
|
|
271
|
+
size=size,
|
|
272
|
+
pages=(total + size - 1) // size,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class PaginationParams(BaseModel):
|
|
277
|
+
page: int = Field(1, ge=1)
|
|
278
|
+
size: int = Field(10, ge=1, le=100)
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def offset(self) -> int:
|
|
282
|
+
return (self.page - 1) * self.size
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Usage
|
|
286
|
+
# PaginatedResponse[UserResponse]
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## FastAPI Integration
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
# api/users.py
|
|
293
|
+
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
294
|
+
from pydantic import ValidationError
|
|
295
|
+
|
|
296
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@router.post("/", response_model=UserResponse, status_code=201)
|
|
300
|
+
async def create_user(
|
|
301
|
+
user: UserCreate,
|
|
302
|
+
db: AsyncSession = Depends(get_db),
|
|
303
|
+
) -> UserResponse:
|
|
304
|
+
# Validation already done by Pydantic
|
|
305
|
+
existing = await db.execute(
|
|
306
|
+
select(User).where(User.email == user.email)
|
|
307
|
+
)
|
|
308
|
+
if existing.scalar_one_or_none():
|
|
309
|
+
raise HTTPException(status_code=409, detail="Email already registered")
|
|
310
|
+
|
|
311
|
+
new_user = User(**user.model_dump())
|
|
312
|
+
db.add(new_user)
|
|
313
|
+
await db.commit()
|
|
314
|
+
await db.refresh(new_user)
|
|
315
|
+
|
|
316
|
+
return UserResponse.model_validate(new_user)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.get("/", response_model=PaginatedResponse[UserResponse])
|
|
320
|
+
async def list_users(
|
|
321
|
+
pagination: PaginationParams = Depends(),
|
|
322
|
+
search: Optional[str] = Query(None, min_length=1),
|
|
323
|
+
db: AsyncSession = Depends(get_db),
|
|
324
|
+
) -> PaginatedResponse[UserResponse]:
|
|
325
|
+
query = select(User)
|
|
326
|
+
|
|
327
|
+
if search:
|
|
328
|
+
query = query.where(User.name.ilike(f"%{search}%"))
|
|
329
|
+
|
|
330
|
+
total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
|
331
|
+
users = await db.scalars(
|
|
332
|
+
query.offset(pagination.offset).limit(pagination.size)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return PaginatedResponse.create(
|
|
336
|
+
items=[UserResponse.model_validate(u) for u in users],
|
|
337
|
+
total=total,
|
|
338
|
+
page=pagination.page,
|
|
339
|
+
size=pagination.size,
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Custom Error Messages
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# schemas/user.py
|
|
347
|
+
from pydantic import BaseModel, Field
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class UserCreate(BaseModel):
|
|
351
|
+
email: EmailStr = Field(
|
|
352
|
+
...,
|
|
353
|
+
description="User email address",
|
|
354
|
+
json_schema_extra={"example": "user@example.com"},
|
|
355
|
+
)
|
|
356
|
+
password: str = Field(
|
|
357
|
+
...,
|
|
358
|
+
min_length=8,
|
|
359
|
+
max_length=100,
|
|
360
|
+
description="User password (min 8 characters)",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
model_config = {
|
|
364
|
+
"json_schema_extra": {
|
|
365
|
+
"examples": [
|
|
366
|
+
{
|
|
367
|
+
"email": "john@example.com",
|
|
368
|
+
"password": "SecurePass123!",
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Anti-patterns
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
# BAD: Using dict instead of Pydantic model
|
|
379
|
+
@router.post("/users")
|
|
380
|
+
async def create_user(data: dict): # No validation!
|
|
381
|
+
email = data.get("email")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# GOOD: Use Pydantic models
|
|
385
|
+
@router.post("/users")
|
|
386
|
+
async def create_user(data: UserCreate):
|
|
387
|
+
email = data.email # Validated and typed
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# BAD: Manual validation
|
|
391
|
+
class UserCreate(BaseModel):
|
|
392
|
+
email: str
|
|
393
|
+
|
|
394
|
+
def validate_email(self):
|
|
395
|
+
if "@" not in self.email:
|
|
396
|
+
raise ValueError("Invalid email")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# GOOD: Use built-in validators
|
|
400
|
+
class UserCreate(BaseModel):
|
|
401
|
+
email: EmailStr # Automatic validation
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# BAD: Catching all exceptions
|
|
405
|
+
try:
|
|
406
|
+
user = UserCreate(**data)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
return {"error": str(e)}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# GOOD: Catch specific validation errors
|
|
412
|
+
from pydantic import ValidationError
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
user = UserCreate(**data)
|
|
416
|
+
except ValidationError as e:
|
|
417
|
+
return {"errors": e.errors()}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# BAD: Not using model_config for ORM
|
|
421
|
+
class UserResponse(BaseModel):
|
|
422
|
+
id: UUID
|
|
423
|
+
name: str
|
|
424
|
+
# This won't work with SQLAlchemy objects
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# GOOD: Enable from_attributes
|
|
428
|
+
class UserResponse(BaseModel):
|
|
429
|
+
id: UUID
|
|
430
|
+
name: str
|
|
431
|
+
|
|
432
|
+
model_config = {"from_attributes": True}
|
|
433
|
+
```
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Response Patterns
|
|
7
|
+
|
|
8
|
+
## Response Models
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from pydantic import BaseModel, ConfigDict
|
|
12
|
+
|
|
13
|
+
# GOOD - explicit response model
|
|
14
|
+
class UserResponse(BaseModel):
|
|
15
|
+
id: int
|
|
16
|
+
email: str
|
|
17
|
+
name: str
|
|
18
|
+
created_at: datetime
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(from_attributes=True)
|
|
21
|
+
|
|
22
|
+
@router.get("/users/{user_id}", response_model=UserResponse)
|
|
23
|
+
async def get_user(user_id: int, db: DbSession) -> User:
|
|
24
|
+
user = await db.get(User, user_id)
|
|
25
|
+
if not user:
|
|
26
|
+
raise HTTPException(404, "User not found")
|
|
27
|
+
return user # Automatically serialized to UserResponse
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Pagination Response
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from typing import Generic, TypeVar
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
class Page(BaseModel, Generic[T]):
|
|
38
|
+
items: list[T]
|
|
39
|
+
total: int
|
|
40
|
+
page: int
|
|
41
|
+
size: int
|
|
42
|
+
pages: int
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def create(cls, items: list[T], total: int, page: int, size: int) -> "Page[T]":
|
|
46
|
+
return cls(
|
|
47
|
+
items=items,
|
|
48
|
+
total=total,
|
|
49
|
+
page=page,
|
|
50
|
+
size=size,
|
|
51
|
+
pages=(total + size - 1) // size,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@router.get("/users", response_model=Page[UserResponse])
|
|
55
|
+
async def list_users(
|
|
56
|
+
page: int = Query(1, ge=1),
|
|
57
|
+
size: int = Query(20, ge=1, le=100),
|
|
58
|
+
db: DbSession,
|
|
59
|
+
) -> Page[User]:
|
|
60
|
+
total = await db.scalar(select(func.count()).select_from(User))
|
|
61
|
+
|
|
62
|
+
users = await db.scalars(
|
|
63
|
+
select(User)
|
|
64
|
+
.offset((page - 1) * size)
|
|
65
|
+
.limit(size)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return Page.create(list(users), total, page, size)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Response Classes
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from fastapi.responses import (
|
|
75
|
+
JSONResponse,
|
|
76
|
+
HTMLResponse,
|
|
77
|
+
PlainTextResponse,
|
|
78
|
+
RedirectResponse,
|
|
79
|
+
StreamingResponse,
|
|
80
|
+
FileResponse,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# JSON with custom status and headers
|
|
84
|
+
@router.post("/users")
|
|
85
|
+
async def create_user(data: UserCreate) -> JSONResponse:
|
|
86
|
+
user = await user_service.create(data)
|
|
87
|
+
return JSONResponse(
|
|
88
|
+
content={"id": user.id, "message": "User created"},
|
|
89
|
+
status_code=201,
|
|
90
|
+
headers={"X-Custom-Header": "value"},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# HTML response
|
|
94
|
+
@router.get("/page", response_class=HTMLResponse)
|
|
95
|
+
async def get_page() -> str:
|
|
96
|
+
return "<html><body><h1>Hello</h1></body></html>"
|
|
97
|
+
|
|
98
|
+
# Redirect
|
|
99
|
+
@router.get("/old-path")
|
|
100
|
+
async def redirect() -> RedirectResponse:
|
|
101
|
+
return RedirectResponse(url="/new-path", status_code=301)
|
|
102
|
+
|
|
103
|
+
# File download
|
|
104
|
+
@router.get("/download/{filename}")
|
|
105
|
+
async def download_file(filename: str) -> FileResponse:
|
|
106
|
+
return FileResponse(
|
|
107
|
+
path=f"files/{filename}",
|
|
108
|
+
filename=filename,
|
|
109
|
+
media_type="application/octet-stream",
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Streaming Response
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import asyncio
|
|
117
|
+
from typing import AsyncGenerator
|
|
118
|
+
|
|
119
|
+
async def generate_data() -> AsyncGenerator[bytes, None]:
|
|
120
|
+
for i in range(100):
|
|
121
|
+
yield f"data: {i}\n\n".encode()
|
|
122
|
+
await asyncio.sleep(0.1)
|
|
123
|
+
|
|
124
|
+
@router.get("/stream")
|
|
125
|
+
async def stream_data() -> StreamingResponse:
|
|
126
|
+
return StreamingResponse(
|
|
127
|
+
generate_data(),
|
|
128
|
+
media_type="text/event-stream",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Stream large file
|
|
132
|
+
@router.get("/large-file")
|
|
133
|
+
async def stream_file() -> StreamingResponse:
|
|
134
|
+
async def iterfile():
|
|
135
|
+
async with aiofiles.open("large_file.csv", "rb") as f:
|
|
136
|
+
while chunk := await f.read(8192):
|
|
137
|
+
yield chunk
|
|
138
|
+
|
|
139
|
+
return StreamingResponse(
|
|
140
|
+
iterfile(),
|
|
141
|
+
media_type="text/csv",
|
|
142
|
+
headers={"Content-Disposition": "attachment; filename=data.csv"},
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Error Responses
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from fastapi import HTTPException
|
|
150
|
+
from fastapi.responses import JSONResponse
|
|
151
|
+
|
|
152
|
+
# Standard HTTP exception
|
|
153
|
+
@router.get("/users/{user_id}")
|
|
154
|
+
async def get_user(user_id: int) -> UserResponse:
|
|
155
|
+
user = await db.get(User, user_id)
|
|
156
|
+
if not user:
|
|
157
|
+
raise HTTPException(
|
|
158
|
+
status_code=404,
|
|
159
|
+
detail="User not found",
|
|
160
|
+
headers={"X-Error-Code": "USER_NOT_FOUND"},
|
|
161
|
+
)
|
|
162
|
+
return user
|
|
163
|
+
|
|
164
|
+
# Custom error response model
|
|
165
|
+
class ErrorResponse(BaseModel):
|
|
166
|
+
error: str
|
|
167
|
+
detail: str
|
|
168
|
+
request_id: str | None = None
|
|
169
|
+
|
|
170
|
+
@router.get(
|
|
171
|
+
"/users/{user_id}",
|
|
172
|
+
responses={
|
|
173
|
+
200: {"model": UserResponse},
|
|
174
|
+
404: {"model": ErrorResponse, "description": "User not found"},
|
|
175
|
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
async def get_user(user_id: int) -> UserResponse:
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
# Global exception handler
|
|
182
|
+
@app.exception_handler(ValueError)
|
|
183
|
+
async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse:
|
|
184
|
+
return JSONResponse(
|
|
185
|
+
status_code=400,
|
|
186
|
+
content={"error": "Bad Request", "detail": str(exc)},
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Response Headers
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from fastapi import Response
|
|
194
|
+
|
|
195
|
+
@router.get("/data")
|
|
196
|
+
async def get_data(response: Response) -> dict:
|
|
197
|
+
response.headers["X-Custom-Header"] = "custom-value"
|
|
198
|
+
response.headers["Cache-Control"] = "max-age=3600"
|
|
199
|
+
return {"data": "value"}
|
|
200
|
+
|
|
201
|
+
# Set cookies
|
|
202
|
+
@router.post("/login")
|
|
203
|
+
async def login(response: Response) -> dict:
|
|
204
|
+
response.set_cookie(
|
|
205
|
+
key="session_id",
|
|
206
|
+
value="abc123",
|
|
207
|
+
httponly=True,
|
|
208
|
+
secure=True,
|
|
209
|
+
samesite="lax",
|
|
210
|
+
max_age=3600,
|
|
211
|
+
)
|
|
212
|
+
return {"status": "logged_in"}
|
|
213
|
+
|
|
214
|
+
# Delete cookie
|
|
215
|
+
@router.post("/logout")
|
|
216
|
+
async def logout(response: Response) -> dict:
|
|
217
|
+
response.delete_cookie("session_id")
|
|
218
|
+
return {"status": "logged_out"}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Background Response
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from fastapi import BackgroundTasks
|
|
225
|
+
|
|
226
|
+
@router.post("/send-email")
|
|
227
|
+
async def send_email(
|
|
228
|
+
email: EmailSchema,
|
|
229
|
+
background_tasks: BackgroundTasks,
|
|
230
|
+
) -> dict:
|
|
231
|
+
# Return immediately
|
|
232
|
+
background_tasks.add_task(send_email_async, email.to, email.subject, email.body)
|
|
233
|
+
return {"message": "Email queued"}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## No Content Response
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from fastapi import Response, status
|
|
240
|
+
|
|
241
|
+
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
242
|
+
async def delete_user(user_id: int, db: DbSession) -> Response:
|
|
243
|
+
user = await db.get(User, user_id)
|
|
244
|
+
if not user:
|
|
245
|
+
raise HTTPException(404, "User not found")
|
|
246
|
+
|
|
247
|
+
await db.delete(user)
|
|
248
|
+
await db.commit()
|
|
249
|
+
|
|
250
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
251
|
+
```
|