@malamute/ai-rules 1.0.0 → 1.2.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.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,229 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Middleware Patterns
7
+
8
+ ## Custom Middleware
9
+
10
+ ```python
11
+ from starlette.middleware.base import BaseHTTPMiddleware
12
+ from starlette.requests import Request
13
+ from starlette.responses import Response
14
+ import time
15
+
16
+ class TimingMiddleware(BaseHTTPMiddleware):
17
+ async def dispatch(self, request: Request, call_next) -> Response:
18
+ start_time = time.perf_counter()
19
+
20
+ response = await call_next(request)
21
+
22
+ process_time = time.perf_counter() - start_time
23
+ response.headers["X-Process-Time"] = f"{process_time:.4f}"
24
+
25
+ return response
26
+
27
+ app.add_middleware(TimingMiddleware)
28
+ ```
29
+
30
+ ## Request ID Middleware
31
+
32
+ ```python
33
+ import uuid
34
+ from contextvars import ContextVar
35
+
36
+ request_id_ctx: ContextVar[str] = ContextVar("request_id", default="")
37
+
38
+ class RequestIDMiddleware(BaseHTTPMiddleware):
39
+ async def dispatch(self, request: Request, call_next) -> Response:
40
+ request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
41
+ request_id_ctx.set(request_id)
42
+
43
+ response = await call_next(request)
44
+ response.headers["X-Request-ID"] = request_id
45
+
46
+ return response
47
+
48
+ def get_request_id() -> str:
49
+ return request_id_ctx.get()
50
+ ```
51
+
52
+ ## Logging Middleware
53
+
54
+ ```python
55
+ import logging
56
+ from typing import Callable
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ class LoggingMiddleware(BaseHTTPMiddleware):
61
+ async def dispatch(self, request: Request, call_next) -> Response:
62
+ # Log request
63
+ logger.info(
64
+ "Request started",
65
+ extra={
66
+ "method": request.method,
67
+ "path": request.url.path,
68
+ "client_ip": request.client.host,
69
+ "request_id": get_request_id(),
70
+ },
71
+ )
72
+
73
+ start_time = time.perf_counter()
74
+
75
+ try:
76
+ response = await call_next(request)
77
+ except Exception as e:
78
+ logger.exception(
79
+ "Request failed",
80
+ extra={
81
+ "path": request.url.path,
82
+ "error": str(e),
83
+ "request_id": get_request_id(),
84
+ },
85
+ )
86
+ raise
87
+
88
+ duration = time.perf_counter() - start_time
89
+
90
+ # Log response
91
+ logger.info(
92
+ "Request completed",
93
+ extra={
94
+ "method": request.method,
95
+ "path": request.url.path,
96
+ "status_code": response.status_code,
97
+ "duration": f"{duration:.4f}s",
98
+ "request_id": get_request_id(),
99
+ },
100
+ )
101
+
102
+ return response
103
+ ```
104
+
105
+ ## Error Handling Middleware
106
+
107
+ ```python
108
+ from fastapi.responses import JSONResponse
109
+
110
+ class ErrorHandlingMiddleware(BaseHTTPMiddleware):
111
+ async def dispatch(self, request: Request, call_next) -> Response:
112
+ try:
113
+ return await call_next(request)
114
+ except ValueError as e:
115
+ return JSONResponse(
116
+ status_code=400,
117
+ content={"error": "Bad Request", "detail": str(e)},
118
+ )
119
+ except PermissionError as e:
120
+ return JSONResponse(
121
+ status_code=403,
122
+ content={"error": "Forbidden", "detail": str(e)},
123
+ )
124
+ except Exception as e:
125
+ logger.exception("Unhandled exception")
126
+ return JSONResponse(
127
+ status_code=500,
128
+ content={"error": "Internal Server Error"},
129
+ )
130
+ ```
131
+
132
+ ## Pure ASGI Middleware
133
+
134
+ ```python
135
+ # More performant than BaseHTTPMiddleware
136
+ class PureASGIMiddleware:
137
+ def __init__(self, app):
138
+ self.app = app
139
+
140
+ async def __call__(self, scope, receive, send):
141
+ if scope["type"] != "http":
142
+ await self.app(scope, receive, send)
143
+ return
144
+
145
+ # Before request
146
+ start_time = time.perf_counter()
147
+
148
+ async def send_wrapper(message):
149
+ if message["type"] == "http.response.start":
150
+ # Add headers
151
+ headers = list(message.get("headers", []))
152
+ duration = time.perf_counter() - start_time
153
+ headers.append((b"x-process-time", f"{duration:.4f}".encode()))
154
+ message["headers"] = headers
155
+ await send(message)
156
+
157
+ await self.app(scope, receive, send_wrapper)
158
+
159
+ app.add_middleware(PureASGIMiddleware)
160
+ ```
161
+
162
+ ## Database Session Middleware
163
+
164
+ ```python
165
+ from contextvars import ContextVar
166
+
167
+ db_session_ctx: ContextVar[AsyncSession | None] = ContextVar("db_session", default=None)
168
+
169
+ class DatabaseSessionMiddleware(BaseHTTPMiddleware):
170
+ async def dispatch(self, request: Request, call_next) -> Response:
171
+ async with async_session() as session:
172
+ db_session_ctx.set(session)
173
+ try:
174
+ response = await call_next(request)
175
+ await session.commit()
176
+ except Exception:
177
+ await session.rollback()
178
+ raise
179
+ finally:
180
+ db_session_ctx.set(None)
181
+
182
+ return response
183
+
184
+ def get_db() -> AsyncSession:
185
+ session = db_session_ctx.get()
186
+ if session is None:
187
+ raise RuntimeError("No database session available")
188
+ return session
189
+ ```
190
+
191
+ ## Middleware Order
192
+
193
+ ```python
194
+ # Order matters! Last added = first executed
195
+ # Request flow: A -> B -> C -> Handler -> C -> B -> A
196
+
197
+ app.add_middleware(ErrorHandlingMiddleware) # 3rd (innermost)
198
+ app.add_middleware(LoggingMiddleware) # 2nd
199
+ app.add_middleware(RequestIDMiddleware) # 1st (outermost)
200
+ ```
201
+
202
+ ## Built-in Middleware
203
+
204
+ ```python
205
+ from fastapi.middleware.cors import CORSMiddleware
206
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
207
+ from fastapi.middleware.gzip import GZipMiddleware
208
+ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
209
+
210
+ # HTTPS redirect
211
+ app.add_middleware(HTTPSRedirectMiddleware)
212
+
213
+ # Trusted hosts
214
+ app.add_middleware(
215
+ TrustedHostMiddleware,
216
+ allowed_hosts=["example.com", "*.example.com"],
217
+ )
218
+
219
+ # GZip compression
220
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
221
+
222
+ # CORS
223
+ app.add_middleware(
224
+ CORSMiddleware,
225
+ allow_origins=["https://example.com"],
226
+ allow_methods=["*"],
227
+ allow_headers=["*"],
228
+ )
229
+ ```
@@ -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
+ ```