autoworkflow 3.1.5 → 3.6.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Prisma Skill
|
|
2
|
+
|
|
3
|
+
## Schema Design
|
|
4
|
+
\`\`\`prisma
|
|
5
|
+
// schema.prisma
|
|
6
|
+
generator client {
|
|
7
|
+
provider = "prisma-client-js"
|
|
8
|
+
previewFeatures = ["fullTextSearch", "filteredRelationCount"]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
datasource db {
|
|
12
|
+
provider = "postgresql"
|
|
13
|
+
url = env("DATABASE_URL")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model User {
|
|
17
|
+
id String @id @default(cuid())
|
|
18
|
+
email String @unique
|
|
19
|
+
name String?
|
|
20
|
+
role Role @default(USER)
|
|
21
|
+
posts Post[]
|
|
22
|
+
profile Profile?
|
|
23
|
+
accounts Account[]
|
|
24
|
+
createdAt DateTime @default(now())
|
|
25
|
+
updatedAt DateTime @updatedAt
|
|
26
|
+
deletedAt DateTime? // Soft delete
|
|
27
|
+
|
|
28
|
+
@@index([email])
|
|
29
|
+
@@index([createdAt])
|
|
30
|
+
@@map("users") // Table name
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
model Post {
|
|
34
|
+
id String @id @default(cuid())
|
|
35
|
+
title String
|
|
36
|
+
content String?
|
|
37
|
+
published Boolean @default(false)
|
|
38
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
39
|
+
authorId String
|
|
40
|
+
categories Category[]
|
|
41
|
+
createdAt DateTime @default(now())
|
|
42
|
+
|
|
43
|
+
@@index([authorId])
|
|
44
|
+
@@index([published, createdAt])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
model Category {
|
|
48
|
+
id String @id @default(cuid())
|
|
49
|
+
name String @unique
|
|
50
|
+
posts Post[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// One-to-one
|
|
54
|
+
model Profile {
|
|
55
|
+
id String @id @default(cuid())
|
|
56
|
+
bio String?
|
|
57
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
58
|
+
userId String @unique
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum Role {
|
|
62
|
+
USER
|
|
63
|
+
ADMIN
|
|
64
|
+
MODERATOR
|
|
65
|
+
}
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
## Query Patterns
|
|
69
|
+
\`\`\`typescript
|
|
70
|
+
// CRUD Operations
|
|
71
|
+
const user = await prisma.user.create({
|
|
72
|
+
data: {
|
|
73
|
+
email: 'user@example.com',
|
|
74
|
+
name: 'John',
|
|
75
|
+
profile: { create: { bio: 'Developer' } }, // Nested create
|
|
76
|
+
},
|
|
77
|
+
include: { profile: true },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Find operations
|
|
81
|
+
const user = await prisma.user.findUnique({
|
|
82
|
+
where: { id },
|
|
83
|
+
select: { id: true, email: true, name: true }, // Only select needed fields
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const user = await prisma.user.findUniqueOrThrow({ where: { id } }); // Throws if not found
|
|
87
|
+
|
|
88
|
+
// Filter with multiple conditions
|
|
89
|
+
const users = await prisma.user.findMany({
|
|
90
|
+
where: {
|
|
91
|
+
AND: [
|
|
92
|
+
{ email: { contains: '@company.com' } },
|
|
93
|
+
{ role: 'USER' },
|
|
94
|
+
{ deletedAt: null },
|
|
95
|
+
],
|
|
96
|
+
OR: [
|
|
97
|
+
{ name: { startsWith: 'A' } },
|
|
98
|
+
{ name: { startsWith: 'B' } },
|
|
99
|
+
],
|
|
100
|
+
NOT: { role: 'ADMIN' },
|
|
101
|
+
},
|
|
102
|
+
orderBy: { createdAt: 'desc' },
|
|
103
|
+
skip: 0,
|
|
104
|
+
take: 20,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Include relations with filtering
|
|
108
|
+
const usersWithPosts = await prisma.user.findMany({
|
|
109
|
+
include: {
|
|
110
|
+
posts: {
|
|
111
|
+
where: { published: true },
|
|
112
|
+
orderBy: { createdAt: 'desc' },
|
|
113
|
+
take: 5,
|
|
114
|
+
},
|
|
115
|
+
_count: { select: { posts: true } }, // Count relations
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update operations
|
|
120
|
+
const updated = await prisma.user.update({
|
|
121
|
+
where: { id },
|
|
122
|
+
data: {
|
|
123
|
+
name: 'New Name',
|
|
124
|
+
posts: {
|
|
125
|
+
updateMany: { where: { published: false }, data: { published: true } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Upsert
|
|
131
|
+
const user = await prisma.user.upsert({
|
|
132
|
+
where: { email: 'user@example.com' },
|
|
133
|
+
update: { name: 'Updated Name' },
|
|
134
|
+
create: { email: 'user@example.com', name: 'New User' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Delete with cascade (defined in schema)
|
|
138
|
+
await prisma.user.delete({ where: { id } });
|
|
139
|
+
|
|
140
|
+
// Soft delete pattern
|
|
141
|
+
await prisma.user.update({
|
|
142
|
+
where: { id },
|
|
143
|
+
data: { deletedAt: new Date() },
|
|
144
|
+
});
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
## Transactions
|
|
148
|
+
\`\`\`typescript
|
|
149
|
+
// Sequential transactions (array)
|
|
150
|
+
const [user, post] = await prisma.$transaction([
|
|
151
|
+
prisma.user.create({ data: userData }),
|
|
152
|
+
prisma.post.create({ data: postData }),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Interactive transactions (function)
|
|
156
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
157
|
+
const user = await tx.user.findUnique({ where: { id } });
|
|
158
|
+
if (!user) throw new Error('User not found');
|
|
159
|
+
|
|
160
|
+
const updatedUser = await tx.user.update({
|
|
161
|
+
where: { id },
|
|
162
|
+
data: { balance: { decrement: 100 } },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await tx.transaction.create({
|
|
166
|
+
data: { userId: id, amount: -100, type: 'WITHDRAWAL' },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return updatedUser;
|
|
170
|
+
}, {
|
|
171
|
+
maxWait: 5000, // Max wait to start transaction
|
|
172
|
+
timeout: 10000, // Max transaction duration
|
|
173
|
+
isolationLevel: 'Serializable', // Optional isolation level
|
|
174
|
+
});
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
## Middleware
|
|
178
|
+
\`\`\`typescript
|
|
179
|
+
// Logging middleware
|
|
180
|
+
prisma.$use(async (params, next) => {
|
|
181
|
+
const before = Date.now();
|
|
182
|
+
const result = await next(params);
|
|
183
|
+
const after = Date.now();
|
|
184
|
+
|
|
185
|
+
console.log(\`Query \${params.model}.\${params.action} took \${after - before}ms\`);
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Soft delete middleware
|
|
190
|
+
prisma.$use(async (params, next) => {
|
|
191
|
+
if (params.model === 'User') {
|
|
192
|
+
// Intercept delete and convert to soft delete
|
|
193
|
+
if (params.action === 'delete') {
|
|
194
|
+
params.action = 'update';
|
|
195
|
+
params.args.data = { deletedAt: new Date() };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Filter out soft-deleted in find queries
|
|
199
|
+
if (params.action === 'findMany' || params.action === 'findFirst') {
|
|
200
|
+
params.args.where = { ...params.args.where, deletedAt: null };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return next(params);
|
|
204
|
+
});
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
## Raw Queries
|
|
208
|
+
\`\`\`typescript
|
|
209
|
+
// Raw SQL queries
|
|
210
|
+
const users = await prisma.$queryRaw\`
|
|
211
|
+
SELECT * FROM users
|
|
212
|
+
WHERE email LIKE \${pattern}
|
|
213
|
+
LIMIT \${limit}
|
|
214
|
+
\`;
|
|
215
|
+
|
|
216
|
+
// Raw SQL with typed result
|
|
217
|
+
const users = await prisma.$queryRaw<User[]>\`
|
|
218
|
+
SELECT id, email, name FROM users WHERE role = \${role}
|
|
219
|
+
\`;
|
|
220
|
+
|
|
221
|
+
// Execute raw (INSERT, UPDATE, DELETE)
|
|
222
|
+
const count = await prisma.$executeRaw\`
|
|
223
|
+
UPDATE users SET updated_at = NOW() WHERE id = \${id}
|
|
224
|
+
\`;
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
## Prisma Client Extensions
|
|
228
|
+
\`\`\`typescript
|
|
229
|
+
// Extend Prisma Client (v4.16+)
|
|
230
|
+
const prisma = new PrismaClient().$extends({
|
|
231
|
+
model: {
|
|
232
|
+
user: {
|
|
233
|
+
async findByEmail(email: string) {
|
|
234
|
+
return prisma.user.findUnique({ where: { email } });
|
|
235
|
+
},
|
|
236
|
+
async softDelete(id: string) {
|
|
237
|
+
return prisma.user.update({
|
|
238
|
+
where: { id },
|
|
239
|
+
data: { deletedAt: new Date() },
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
result: {
|
|
245
|
+
user: {
|
|
246
|
+
fullName: {
|
|
247
|
+
needs: { firstName: true, lastName: true },
|
|
248
|
+
compute(user) {
|
|
249
|
+
return \`\${user.firstName} \${user.lastName}\`;
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Usage
|
|
257
|
+
const user = await prisma.user.findByEmail('test@example.com');
|
|
258
|
+
console.log(user?.fullName);
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
## Migrations
|
|
262
|
+
\`\`\`bash
|
|
263
|
+
# Create migration from schema changes
|
|
264
|
+
npx prisma migrate dev --name add_user_role
|
|
265
|
+
|
|
266
|
+
# Apply migrations in production
|
|
267
|
+
npx prisma migrate deploy
|
|
268
|
+
|
|
269
|
+
# Reset database (dangerous!)
|
|
270
|
+
npx prisma migrate reset
|
|
271
|
+
|
|
272
|
+
# Generate Prisma Client
|
|
273
|
+
npx prisma generate
|
|
274
|
+
|
|
275
|
+
# View database in browser
|
|
276
|
+
npx prisma studio
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
## ❌ DON'T
|
|
280
|
+
- Use \`findMany()\` without pagination on large tables
|
|
281
|
+
- Fetch relations you don't need (use select/include wisely)
|
|
282
|
+
- Ignore the N+1 problem with nested queries
|
|
283
|
+
- Use raw queries when Prisma Client suffices
|
|
284
|
+
- Skip migrations in production
|
|
285
|
+
|
|
286
|
+
## ✅ DO
|
|
287
|
+
- Use \`select\` to fetch only needed fields
|
|
288
|
+
- Use \`take\` and \`skip\` for pagination (or cursor-based)
|
|
289
|
+
- Use transactions for related operations
|
|
290
|
+
- Use middleware for cross-cutting concerns
|
|
291
|
+
- Use extensions for reusable model methods
|
|
292
|
+
- Use \`findUniqueOrThrow\` when record must exist
|
|
293
|
+
- Run migrations in CI/CD pipeline
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Pydantic Skill
|
|
2
|
+
|
|
3
|
+
## Basic Model with Validators
|
|
4
|
+
\`\`\`python
|
|
5
|
+
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
class UserBase(BaseModel):
|
|
10
|
+
"""Base model with shared fields and configuration."""
|
|
11
|
+
email: EmailStr
|
|
12
|
+
name: Annotated[str, Field(min_length=1, max_length=100)]
|
|
13
|
+
|
|
14
|
+
model_config = {
|
|
15
|
+
"from_attributes": True, # ORM mode
|
|
16
|
+
"str_strip_whitespace": True,
|
|
17
|
+
"validate_assignment": True,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class UserCreate(UserBase):
|
|
21
|
+
password: str = Field(min_length=8)
|
|
22
|
+
password_confirm: str
|
|
23
|
+
|
|
24
|
+
@field_validator('name')
|
|
25
|
+
@classmethod
|
|
26
|
+
def name_must_not_be_empty(cls, v: str) -> str:
|
|
27
|
+
if not v.strip():
|
|
28
|
+
raise ValueError('Name cannot be empty')
|
|
29
|
+
return v.title()
|
|
30
|
+
|
|
31
|
+
@model_validator(mode='after')
|
|
32
|
+
def passwords_match(self) -> 'UserCreate':
|
|
33
|
+
if self.password != self.password_confirm:
|
|
34
|
+
raise ValueError('Passwords do not match')
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
class UserResponse(UserBase):
|
|
38
|
+
id: int
|
|
39
|
+
created_at: datetime
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
## Computed Fields
|
|
43
|
+
\`\`\`python
|
|
44
|
+
from pydantic import BaseModel, computed_field
|
|
45
|
+
from datetime import datetime, date
|
|
46
|
+
|
|
47
|
+
class User(BaseModel):
|
|
48
|
+
first_name: str
|
|
49
|
+
last_name: str
|
|
50
|
+
birth_date: date
|
|
51
|
+
|
|
52
|
+
@computed_field
|
|
53
|
+
@property
|
|
54
|
+
def full_name(self) -> str:
|
|
55
|
+
return f"{self.first_name} {self.last_name}"
|
|
56
|
+
|
|
57
|
+
@computed_field
|
|
58
|
+
@property
|
|
59
|
+
def age(self) -> int:
|
|
60
|
+
today = date.today()
|
|
61
|
+
return today.year - self.birth_date.year - (
|
|
62
|
+
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
class Order(BaseModel):
|
|
66
|
+
items: list[dict]
|
|
67
|
+
tax_rate: float = 0.08
|
|
68
|
+
|
|
69
|
+
@computed_field
|
|
70
|
+
@property
|
|
71
|
+
def subtotal(self) -> float:
|
|
72
|
+
return sum(item['price'] * item['quantity'] for item in self.items)
|
|
73
|
+
|
|
74
|
+
@computed_field
|
|
75
|
+
@property
|
|
76
|
+
def total(self) -> float:
|
|
77
|
+
return self.subtotal * (1 + self.tax_rate)
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Custom Types and Validators
|
|
81
|
+
\`\`\`python
|
|
82
|
+
from pydantic import BaseModel, BeforeValidator, AfterValidator, PlainValidator
|
|
83
|
+
from typing import Annotated
|
|
84
|
+
import re
|
|
85
|
+
|
|
86
|
+
# Custom type with validation
|
|
87
|
+
def validate_phone(v: str) -> str:
|
|
88
|
+
digits = re.sub(r'\\D', '', v)
|
|
89
|
+
if len(digits) != 10:
|
|
90
|
+
raise ValueError('Phone must be 10 digits')
|
|
91
|
+
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
|
|
92
|
+
|
|
93
|
+
PhoneNumber = Annotated[str, AfterValidator(validate_phone)]
|
|
94
|
+
|
|
95
|
+
# Coercing validator
|
|
96
|
+
def to_lowercase(v: str) -> str:
|
|
97
|
+
return v.lower().strip()
|
|
98
|
+
|
|
99
|
+
LowercaseStr = Annotated[str, BeforeValidator(to_lowercase)]
|
|
100
|
+
|
|
101
|
+
# Multiple validators
|
|
102
|
+
def validate_username(v: str) -> str:
|
|
103
|
+
if not re.match(r'^[a-z0-9_]+$', v):
|
|
104
|
+
raise ValueError('Username must be alphanumeric with underscores')
|
|
105
|
+
if len(v) < 3:
|
|
106
|
+
raise ValueError('Username must be at least 3 characters')
|
|
107
|
+
return v
|
|
108
|
+
|
|
109
|
+
Username = Annotated[str, BeforeValidator(to_lowercase), AfterValidator(validate_username)]
|
|
110
|
+
|
|
111
|
+
class Contact(BaseModel):
|
|
112
|
+
username: Username
|
|
113
|
+
email: LowercaseStr
|
|
114
|
+
phone: PhoneNumber
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
## Discriminated Unions
|
|
118
|
+
\`\`\`python
|
|
119
|
+
from pydantic import BaseModel, Field
|
|
120
|
+
from typing import Literal, Union
|
|
121
|
+
from datetime import datetime
|
|
122
|
+
|
|
123
|
+
class EmailNotification(BaseModel):
|
|
124
|
+
type: Literal["email"] = "email"
|
|
125
|
+
recipient: str
|
|
126
|
+
subject: str
|
|
127
|
+
body: str
|
|
128
|
+
|
|
129
|
+
class SMSNotification(BaseModel):
|
|
130
|
+
type: Literal["sms"] = "sms"
|
|
131
|
+
phone_number: str
|
|
132
|
+
message: str
|
|
133
|
+
|
|
134
|
+
class PushNotification(BaseModel):
|
|
135
|
+
type: Literal["push"] = "push"
|
|
136
|
+
device_token: str
|
|
137
|
+
title: str
|
|
138
|
+
body: str
|
|
139
|
+
|
|
140
|
+
# Discriminated union - Pydantic uses 'type' field to determine which model
|
|
141
|
+
Notification = Union[EmailNotification, SMSNotification, PushNotification]
|
|
142
|
+
|
|
143
|
+
class NotificationRequest(BaseModel):
|
|
144
|
+
notification: Annotated[Notification, Field(discriminator='type')]
|
|
145
|
+
scheduled_at: datetime | None = None
|
|
146
|
+
|
|
147
|
+
# Usage
|
|
148
|
+
req = NotificationRequest(
|
|
149
|
+
notification={"type": "email", "recipient": "user@example.com", "subject": "Hi", "body": "Hello!"}
|
|
150
|
+
)
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Settings Management
|
|
154
|
+
\`\`\`python
|
|
155
|
+
from pydantic import Field, SecretStr, PostgresDsn
|
|
156
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
157
|
+
from functools import lru_cache
|
|
158
|
+
|
|
159
|
+
class Settings(BaseSettings):
|
|
160
|
+
"""Application settings loaded from environment variables."""
|
|
161
|
+
|
|
162
|
+
model_config = SettingsConfigDict(
|
|
163
|
+
env_file=".env",
|
|
164
|
+
env_file_encoding="utf-8",
|
|
165
|
+
env_prefix="APP_",
|
|
166
|
+
case_sensitive=False,
|
|
167
|
+
extra="ignore",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# App settings
|
|
171
|
+
app_name: str = "MyApp"
|
|
172
|
+
debug: bool = False
|
|
173
|
+
environment: Literal["development", "staging", "production"] = "development"
|
|
174
|
+
|
|
175
|
+
# Database
|
|
176
|
+
database_url: PostgresDsn
|
|
177
|
+
db_pool_size: int = Field(default=5, ge=1, le=100)
|
|
178
|
+
|
|
179
|
+
# Secrets
|
|
180
|
+
secret_key: SecretStr
|
|
181
|
+
api_key: SecretStr
|
|
182
|
+
|
|
183
|
+
# External services
|
|
184
|
+
redis_url: str = "redis://localhost:6379"
|
|
185
|
+
|
|
186
|
+
@computed_field
|
|
187
|
+
@property
|
|
188
|
+
def is_production(self) -> bool:
|
|
189
|
+
return self.environment == "production"
|
|
190
|
+
|
|
191
|
+
@lru_cache
|
|
192
|
+
def get_settings() -> Settings:
|
|
193
|
+
"""Cached settings instance."""
|
|
194
|
+
return Settings()
|
|
195
|
+
|
|
196
|
+
# Usage in FastAPI
|
|
197
|
+
# settings = get_settings()
|
|
198
|
+
# app = FastAPI(title=settings.app_name, debug=settings.debug)
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
## Serialization Options
|
|
202
|
+
\`\`\`python
|
|
203
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
204
|
+
from datetime import datetime
|
|
205
|
+
from enum import Enum
|
|
206
|
+
|
|
207
|
+
class Status(Enum):
|
|
208
|
+
ACTIVE = "active"
|
|
209
|
+
INACTIVE = "inactive"
|
|
210
|
+
|
|
211
|
+
class User(BaseModel):
|
|
212
|
+
model_config = ConfigDict(
|
|
213
|
+
use_enum_values=True,
|
|
214
|
+
populate_by_name=True, # Allow alias or field name
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
id: int
|
|
218
|
+
email: str
|
|
219
|
+
internal_code: str = Field(exclude=True) # Never serialized
|
|
220
|
+
display_name: str = Field(alias="displayName")
|
|
221
|
+
status: Status
|
|
222
|
+
created_at: datetime
|
|
223
|
+
|
|
224
|
+
user = User(
|
|
225
|
+
id=1,
|
|
226
|
+
email="user@example.com",
|
|
227
|
+
internal_code="SECRET123",
|
|
228
|
+
displayName="John Doe",
|
|
229
|
+
status=Status.ACTIVE,
|
|
230
|
+
created_at=datetime.now()
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Serialization methods
|
|
234
|
+
user.model_dump() # Dict with all fields (except excluded)
|
|
235
|
+
user.model_dump(by_alias=True) # Use aliases in keys
|
|
236
|
+
user.model_dump(exclude={"email"}) # Exclude specific fields
|
|
237
|
+
user.model_dump(include={"id", "email"}) # Only include specific fields
|
|
238
|
+
user.model_dump(exclude_none=True) # Exclude None values
|
|
239
|
+
user.model_dump(mode='json') # JSON-compatible types
|
|
240
|
+
|
|
241
|
+
user.model_dump_json() # JSON string
|
|
242
|
+
user.model_dump_json(indent=2) # Formatted JSON
|
|
243
|
+
|
|
244
|
+
# Round-trip
|
|
245
|
+
User.model_validate(user.model_dump()) # From dict
|
|
246
|
+
User.model_validate_json(user.model_dump_json()) # From JSON string
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
## Generic Models
|
|
250
|
+
\`\`\`python
|
|
251
|
+
from pydantic import BaseModel
|
|
252
|
+
from typing import Generic, TypeVar
|
|
253
|
+
|
|
254
|
+
T = TypeVar('T')
|
|
255
|
+
|
|
256
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
257
|
+
items: list[T]
|
|
258
|
+
total: int
|
|
259
|
+
page: int
|
|
260
|
+
per_page: int
|
|
261
|
+
|
|
262
|
+
@computed_field
|
|
263
|
+
@property
|
|
264
|
+
def total_pages(self) -> int:
|
|
265
|
+
return (self.total + self.per_page - 1) // self.per_page
|
|
266
|
+
|
|
267
|
+
@computed_field
|
|
268
|
+
@property
|
|
269
|
+
def has_next(self) -> bool:
|
|
270
|
+
return self.page < self.total_pages
|
|
271
|
+
|
|
272
|
+
class User(BaseModel):
|
|
273
|
+
id: int
|
|
274
|
+
name: str
|
|
275
|
+
|
|
276
|
+
class Product(BaseModel):
|
|
277
|
+
id: int
|
|
278
|
+
title: str
|
|
279
|
+
price: float
|
|
280
|
+
|
|
281
|
+
# Usage
|
|
282
|
+
users_response: PaginatedResponse[User] = PaginatedResponse(
|
|
283
|
+
items=[User(id=1, name="John")],
|
|
284
|
+
total=50,
|
|
285
|
+
page=1,
|
|
286
|
+
per_page=10
|
|
287
|
+
)
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
## ✅ DO
|
|
291
|
+
- Use \`model_config\` instead of nested Config class (Pydantic v2)
|
|
292
|
+
- Use \`@field_validator\` with \`@classmethod\` decorator
|
|
293
|
+
- Use \`computed_field\` for derived properties
|
|
294
|
+
- Use \`Annotated\` types for reusable validation
|
|
295
|
+
- Use \`pydantic-settings\` for environment configuration
|
|
296
|
+
- Use discriminated unions for polymorphic data
|
|
297
|
+
- Use \`SecretStr\` for sensitive data (won't be logged)
|
|
298
|
+
|
|
299
|
+
## ❌ DON'T
|
|
300
|
+
- Don't use \`@validator\` (Pydantic v1 syntax, deprecated)
|
|
301
|
+
- Don't use \`class Config:\` (use \`model_config\` dict)
|
|
302
|
+
- Don't store secrets as plain strings
|
|
303
|
+
- Don't use \`.dict()\` (use \`.model_dump()\`)
|
|
304
|
+
- Don't use \`.json()\` (use \`.model_dump_json()\`)
|