@robhowley/py-pit-skills 3.1.1

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.
@@ -0,0 +1,500 @@
1
+ ---
2
+ name: pydantic-schemas
3
+ description: Opinionated schema architecture for Python API projects
4
+ using Pydantic v2. Defines request, response, query, command, batch,
5
+ and pagination schema patterns to prevent schema drift in new projects.
6
+ disable-model-invocation: false
7
+ ---
8
+
9
+ # pydantic-schemas
10
+
11
+ Opinionated guidance for designing **API request/response schemas**
12
+ using **Pydantic v2** in new Python backend projects.
13
+
14
+ This skill is optimized for **0→1 API projects** where schema
15
+ conventions have not yet been established.\
16
+ Its purpose is to prevent **schema drift**, eliminate repeated
17
+ architectural decisions, and enforce a consistent API boundary between
18
+ domain models and external representations.
19
+
20
+ This skill **does not teach Pydantic basics**.\
21
+ It constrains schema architecture so the model consistently generates
22
+ predictable request, response, query, and command structures.
23
+
24
+ ------------------------------------------------------------------------
25
+
26
+ # Apply this Skill When
27
+
28
+ Apply this skill when the user:
29
+
30
+ - asks how to structure API schemas
31
+ - asks for request or response models
32
+ - asks about Pydantic models in a FastAPI or API backend
33
+ - is creating a new resource schema
34
+ - is designing pagination or response envelopes
35
+ - is implementing create/update/read models
36
+ - is designing command endpoints, search/filter endpoints, batch
37
+ endpoints, or aggregate/reporting responses
38
+
39
+ **Trigger phrases (user language):**
40
+
41
+ > "add a schema for orders", "how should I structure my Pydantic
42
+ > models?", "I need a request body for creating a user", "what's the
43
+ > right way to do partial updates?", "how do I return paginated
44
+ > results?", "should I use the same model for create and update?",
45
+ > "how do I model a cancel order action?", "I need a search endpoint
46
+ > with filters"
47
+
48
+ Do **not** apply this skill when:
49
+
50
+ - working with internal-only DTOs that never cross an API boundary
51
+ - using Pydantic for local parsing scripts
52
+ - the repository already uses a different schema architecture and the
53
+ user did not request a refactor
54
+ - working on non-API validation tasks
55
+
56
+ ------------------------------------------------------------------------
57
+
58
+ # Mission
59
+
60
+ When this skill is active, the model's job is to **produce or extend
61
+ schema code that follows this taxonomy without deviation**, ask no
62
+ unnecessary questions, and leave the caller with working, importable
63
+ schema classes.
64
+
65
+ ------------------------------------------------------------------------
66
+
67
+ # Hard Rules
68
+
69
+ These rules prevent the most common schema mistakes in early-stage API
70
+ projects.
71
+
72
+ **MUST NOT:**
73
+
74
+ - Use ORM models as request or response schemas.
75
+ - Reuse a single schema for create, update, and read roles.
76
+ - Accept unknown request fields silently.
77
+ - Apply partial updates from full model dumps.
78
+ - Invent new pagination or envelope formats per endpoint.
79
+ - Place validation or normalization logic in routers when it belongs
80
+ in schema validators.
81
+ - Introduce advanced configuration (alias generators, global strict
82
+ mode, etc.) unless explicitly required.
83
+ - Create multiple competing base schema classes without clear
84
+ responsibility boundaries.
85
+ - Use `Optional[T]` to mean both "field may be omitted" and "field
86
+ may be null" without being deliberate about which behavior is
87
+ intended.
88
+
89
+ If the repository already has established schema conventions, follow
90
+ those conventions unless the user asks to refactor toward this
91
+ structure.
92
+
93
+ ------------------------------------------------------------------------
94
+
95
+ # Standard Operating Procedure
96
+
97
+ **Step 1 — Inspect existing schemas**
98
+
99
+ Before generating anything, check whether the repo already has a
100
+ `schemas/` directory or existing Pydantic models. If it does:
101
+
102
+ - Identify the base class in use (if any).
103
+ - Check whether Create/Update/Read roles are already separated.
104
+ - Extend that pattern if it is coherent; do not introduce a parallel
105
+ taxonomy on top of it.
106
+ - Only propose migration toward this skill's taxonomy if the user
107
+ explicitly asks or the existing pattern violates a Hard Rule.
108
+
109
+ **Step 2 — Generate or extend schemas**
110
+
111
+ - If starting fresh: create `schemas/base.py` first, then the
112
+ resource module.
113
+ - If extending: add only the new schema classes needed; do not
114
+ rewrite existing ones without being asked.
115
+ - Apply the appropriate role (Create, Update, Read, Filter, Command,
116
+ Batch, Summary) and base classes from the sections below.
117
+ - Include `model_dump(exclude_unset=True)` usage in the service layer
118
+ whenever an `Update` schema is generated.
119
+ - Use explicit command/filter/batch/summary schemas — do not inline
120
+ these as untyped dicts or ad hoc parameter groups.
121
+
122
+ **Step 3 — Verify and present**
123
+
124
+ - Confirm code is importable with no circular dependencies.
125
+ - Show only the files that changed or were created.
126
+ - Call out any deviation from defaults and why it was made.
127
+
128
+ ------------------------------------------------------------------------
129
+
130
+ # Default Schema Layout
131
+
132
+ For new projects schemas SHOULD follow this layout:
133
+
134
+ ```
135
+ schemas/
136
+ base.py
137
+ user.py
138
+ order.py
139
+ pagination.py
140
+ common.py
141
+ ```
142
+
143
+ `base.py` defines shared base schema types:
144
+
145
+ - APIModel
146
+ - CreateModel
147
+ - UpdateModel
148
+ - ReadModel
149
+ - QueryModel
150
+ - CommandModel
151
+ - BatchModel
152
+
153
+ Resource schemas should live in their own module (e.g. `schemas/user.py`).
154
+
155
+ ------------------------------------------------------------------------
156
+
157
+ # Schema Taxonomy
158
+
159
+ Each API should use explicit schema roles. Do not collapse unrelated
160
+ endpoint types into CRUD-only naming.
161
+
162
+ | Schema Role | Purpose |
163
+ |--------------------|--------------------------------------|
164
+ | Create | Request body for resource creation |
165
+ | Update | Partial update payload |
166
+ | Read | Response serialization for a resource |
167
+ | Filter / Query | Search, list, and filtering inputs |
168
+ | CommandRequest | Action-oriented request body |
169
+ | CommandResponse | Action-oriented response body |
170
+ | BatchRequest | Batch operation request body |
171
+ | Page | Paginated response envelope |
172
+ | Summary / Report | Aggregate or computed response shape |
173
+ | Internal (optional) | Service-layer DTO |
174
+
175
+ Rules:
176
+
177
+ - ORM models MUST NOT cross the API boundary
178
+ - Read models MUST NOT be reused for input
179
+ - Create models MUST declare required fields explicitly
180
+ - Update models MUST represent partial updates
181
+ - Command endpoints SHOULD use explicit command schemas even for small
182
+ payloads
183
+ - Aggregate/reporting endpoints MUST use explicit response schemas
184
+ rather than raw dictionaries
185
+
186
+ ------------------------------------------------------------------------
187
+
188
+ # Base Model Configuration
189
+
190
+ All schemas MUST inherit from a shared base class.
191
+
192
+ Example:
193
+
194
+ ```python
195
+ from pydantic import BaseModel, ConfigDict
196
+
197
+ class APIModel(BaseModel):
198
+ model_config = ConfigDict(
199
+ extra="forbid",
200
+ str_strip_whitespace=True,
201
+ validate_assignment=True,
202
+ use_enum_values=True,
203
+ populate_by_name=True,
204
+ )
205
+ ```
206
+
207
+ These defaults enforce:
208
+
209
+ - strict request validation
210
+ - consistent whitespace normalization
211
+ - safe mutation in service-layer logic
212
+ - predictable enum serialization
213
+
214
+ ------------------------------------------------------------------------
215
+
216
+ # Read Model (ORM Serialization)
217
+
218
+ Response models MUST support serialization from ORM objects.
219
+
220
+ ```python
221
+ from pydantic import ConfigDict
222
+
223
+ class ReadModel(APIModel):
224
+ model_config = ConfigDict(
225
+ extra="forbid",
226
+ str_strip_whitespace=True,
227
+ validate_assignment=True,
228
+ use_enum_values=True,
229
+ populate_by_name=True,
230
+ from_attributes=True,
231
+ )
232
+ ```
233
+
234
+ ORM objects SHOULD be converted using:
235
+
236
+ ```python
237
+ UserRead.model_validate(user)
238
+ ```
239
+
240
+ Avoid:
241
+
242
+ - manual dict conversion
243
+ - returning ORM objects directly
244
+ - mixing serialization logic in routers
245
+
246
+ ------------------------------------------------------------------------
247
+
248
+ # Create Models
249
+
250
+ Create models represent **new resource creation**.
251
+
252
+ Rules:
253
+
254
+ - required fields MUST be explicit
255
+ - optional fields SHOULD only be used when necessary
256
+ - defaults belong in the schema, not router logic
257
+
258
+ Example:
259
+
260
+ ```python
261
+ class UserCreate(CreateModel):
262
+ email: EmailStr
263
+ name: str
264
+ ```
265
+
266
+ ------------------------------------------------------------------------
267
+
268
+ # Update Models
269
+
270
+ Update models represent **partial updates**.
271
+
272
+ Fields SHOULD be optional when omission means "leave unchanged":
273
+
274
+ ```python
275
+ class UserUpdate(UpdateModel):
276
+ name: str | None = None
277
+ email: EmailStr | None = None
278
+ ```
279
+
280
+ Updates MUST apply only provided fields:
281
+
282
+ ```python
283
+ updates = update.model_dump(exclude_unset=True)
284
+
285
+ for field, value in updates.items():
286
+ setattr(user, field, value)
287
+ ```
288
+
289
+ Full model dumps MUST NOT be used for partial updates.
290
+
291
+ Be explicit about the difference between:
292
+
293
+ - omitted field: do not change existing value
294
+ - explicit `null`: clear the value, if the API allows it
295
+
296
+ Do not blur these two behaviors accidentally.
297
+
298
+ ------------------------------------------------------------------------
299
+
300
+ # Query / Filter Schemas
301
+
302
+ Search and listing endpoints SHOULD use explicit query/filter schemas
303
+ instead of ad hoc parameter groupings.
304
+
305
+ Example:
306
+
307
+ ```python
308
+ class UserFilter(QueryModel):
309
+ email: str | None = None
310
+ active: bool | None = None
311
+ created_after: datetime | None = None
312
+ ```
313
+
314
+ Use filter/query schemas for:
315
+
316
+ - list endpoints
317
+ - search endpoints
318
+ - reporting filters
319
+ - complex query parameter sets
320
+
321
+ Do not invent different filter naming conventions per endpoint.
322
+
323
+ ------------------------------------------------------------------------
324
+
325
+ # Command Request / Response Schemas
326
+
327
+ Non-CRUD actions SHOULD use explicit command request/response schemas.
328
+
329
+ Examples of command endpoints:
330
+
331
+ - activate user
332
+ - cancel order
333
+ - refund payment
334
+ - generate report
335
+
336
+ Example:
337
+
338
+ ```python
339
+ class RefundPaymentRequest(CommandModel):
340
+ reason: str
341
+ notify_customer: bool = True
342
+
343
+ class RefundPaymentResponse(APIModel):
344
+ payment_id: int
345
+ status: str
346
+ refunded_at: datetime
347
+ ```
348
+
349
+ Do not inline command payloads as untyped dictionaries.
350
+
351
+ Naming pattern — pick one and use it consistently:
352
+
353
+ - `{Action}{Resource}Request`, or
354
+ - `{Resource}{Action}Request`
355
+
356
+ ------------------------------------------------------------------------
357
+
358
+ # Batch Request Schemas
359
+
360
+ Batch operations SHOULD use explicit batch request models.
361
+
362
+ Example:
363
+
364
+ ```python
365
+ class BulkDisableUsersRequest(BatchModel):
366
+ user_ids: list[int]
367
+ ```
368
+
369
+ Do not pass bare lists as request bodies when the payload has semantic
370
+ meaning.
371
+
372
+ ------------------------------------------------------------------------
373
+
374
+ # Aggregate / Summary Response Schemas
375
+
376
+ Computed endpoints MUST use explicit response schemas.
377
+
378
+ Examples:
379
+
380
+ - analytics
381
+ - reports
382
+ - summaries
383
+ - stats
384
+
385
+ Example:
386
+
387
+ ```python
388
+ class RevenueSummary(APIModel):
389
+ total_revenue: Decimal
390
+ total_orders: int
391
+ average_order_value: Decimal
392
+ ```
393
+
394
+ Do not return raw dictionaries for stable API responses.
395
+
396
+ ------------------------------------------------------------------------
397
+
398
+ # Response Envelopes
399
+
400
+ APIs SHOULD use a consistent pagination structure.
401
+
402
+ ```python
403
+ from typing import Generic, TypeVar
404
+ from pydantic import BaseModel
405
+
406
+ T = TypeVar("T")
407
+
408
+ class Page(BaseModel, Generic[T]):
409
+ items: list[T]
410
+ total: int
411
+ limit: int
412
+ offset: int
413
+ ```
414
+
415
+ Endpoints SHOULD return:
416
+
417
+ ```python
418
+ Page[UserRead]
419
+ ```
420
+
421
+ Resource-specific pagination types MUST NOT be created unless necessary.
422
+
423
+ ------------------------------------------------------------------------
424
+
425
+ # Validation Location
426
+
427
+ Normalization and validation SHOULD live in **Pydantic validators**, not
428
+ routers or services.
429
+
430
+ Example:
431
+
432
+ ```python
433
+ from pydantic import field_validator
434
+
435
+ class UserCreate(CreateModel):
436
+ email: EmailStr
437
+
438
+ @field_validator("email")
439
+ @classmethod
440
+ def normalize_email(cls, value: str) -> str:
441
+ return value.lower()
442
+ ```
443
+
444
+ Routers and services SHOULD assume validated input.
445
+
446
+ ------------------------------------------------------------------------
447
+
448
+ # Naming Conventions
449
+
450
+ Schemas MUST follow explicit naming patterns.
451
+
452
+ Recommended patterns:
453
+
454
+ - `{Resource}Create`
455
+ - `{Resource}Update`
456
+ - `{Resource}Read`
457
+ - `{Resource}Filter`
458
+ - `{Action}{Resource}Request`
459
+ - `{Action}{Resource}Response`
460
+ - `{Resource}Summary`
461
+
462
+ Avoid ambiguous names such as:
463
+
464
+ - UserSchema
465
+ - UserDTO
466
+ - UserResponse
467
+ - UserAction
468
+ - ProcessUser
469
+
470
+ Schema names should clearly communicate their role.
471
+
472
+ ------------------------------------------------------------------------
473
+
474
+ # Completion Checklist
475
+
476
+ Before responding, verify:
477
+
478
+ - [ ] Existing schemas were inspected before generating new ones
479
+ - [ ] Create, Update, and Read roles are separated (no shared schemas)
480
+ - [ ] All schemas inherit from `APIModel` or `ReadModel`
481
+ - [ ] Update logic uses `model_dump(exclude_unset=True)`
482
+ - [ ] Omit-vs-null semantics are explicit in Update models
483
+ - [ ] No ORM models cross the API boundary
484
+ - [ ] Command/filter/batch/summary endpoints use explicit schemas, not dicts
485
+ - [ ] Pagination uses `Page[T]`, not a resource-specific type
486
+ - [ ] No new base classes were introduced without clear purpose
487
+ - [ ] Code shown is importable as written
488
+
489
+ ------------------------------------------------------------------------
490
+
491
+ # Outcome
492
+
493
+ Applying this skill ensures:
494
+
495
+ - predictable schema architecture
496
+ - strict request validation
497
+ - safe ORM serialization
498
+ - explicit command/query/batch/summary models
499
+ - consistent response structures
500
+ - maintainable API evolution
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: pytest-service
3
+ description: Write disciplined backend tests for FastAPI services with pytest. Use
4
+ this skill when adding tests to a Python/FastAPI service, setting up SQLAlchemy
5
+ test fixtures, wiring TestClient with DI overrides, mocking external clients, or
6
+ improving test maintainability. Covers fixture design, factory patterns, SQLite
7
+ in-memory databases, app.dependency_overrides, and avoiding fragile or redundant
8
+ tests.
9
+ disable-model-invocation: false
10
+ ---
11
+
12
+ # Skill: pytest-service
13
+
14
+ ## Core position
15
+
16
+ This skill establishes **clean, reliable backend testing using pytest**.
17
+
18
+ The goal is to produce tests that are:
19
+
20
+ - fast
21
+ - deterministic
22
+ - easy to read
23
+ - easy to extend
24
+ - free of infrastructure dependencies
25
+
26
+ Tests should **optimize for scanability and maintainability**, not
27
+ clever abstractions.
28
+
29
+ ## Core testing stance
30
+
31
+ Prefer **simple local tests with minimal infrastructure**.
32
+
33
+ Rules:
34
+
35
+ - Prefer **`TestClient`** for FastAPI endpoint tests whenever possible
36
+ - Use **async clients only when the test genuinely requires async behavior**
37
+ - Default test databases to **SQLite via SQLAlchemy fixtures**
38
+ - Do **not introduce Docker databases** unless the repository already uses them for testing
39
+ - External clients must be **mocked**, not called
40
+ - **No test classes** — top-level `test_*` functions only; fixtures handle all setup
41
+ - Each test covers a **distinct behavior** — no redundant assertions across tests
42
+ - Avoid mutable default arguments in fixtures and helpers
43
+
44
+ Tests must run reliably with a simple `pytest`. No external services required.
45
+
46
+ ## Project structure
47
+
48
+ Tests should live in a `tests/` directory.
49
+
50
+ tests/
51
+ conftest.py
52
+ test_health.py
53
+ test_users.py
54
+ factories/
55
+ user_factory.py
56
+
57
+ - `conftest.py` holds shared fixtures
58
+ - test files mirror application modules when possible
59
+ - factory fixtures may live in `tests/factories/` or `conftest.py`
60
+
61
+ ## Fixture discipline
62
+
63
+ Shared setup belongs in **pytest fixtures**, not duplicated code.
64
+
65
+ - Use `@pytest.fixture` for reusable setup
66
+ - Inspect existing fixtures **before creating new ones**
67
+ - Prefer **composing fixtures** instead of duplicating setup logic
68
+ - Keep fixture responsibilities narrow and clearly named
69
+
70
+ Avoid inline setup repeated across tests.
71
+
72
+ ## Fixture scope
73
+
74
+ Choose fixture scope based on what the fixture creates and how expensive it is.
75
+
76
+ - `scope="session"` — for things that are expensive to create and safe to share (e.g., the SQLAlchemy engine, schema creation)
77
+ - `scope="function"` (default) — for anything that holds mutable state or must be isolated per test (e.g., sessions, clients, mocks)
78
+
79
+ A common pattern: session-scoped engine, function-scoped session with rollback.
80
+
81
+ ``` python
82
+ @pytest.fixture(scope="session")
83
+ def engine():
84
+ eng = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
85
+ Base.metadata.create_all(eng)
86
+ yield eng
87
+ Base.metadata.drop_all(eng)
88
+
89
+
90
+ @pytest.fixture
91
+ def db_session(engine) -> Session:
92
+ connection = engine.connect()
93
+ transaction = connection.begin()
94
+ TestSession = sessionmaker(bind=connection)
95
+ session = TestSession()
96
+ yield session
97
+ session.close()
98
+ transaction.rollback()
99
+ connection.close()
100
+ ```
101
+
102
+ The rollback in teardown means tests never bleed into each other even when they write data.
103
+
104
+ ## Factory fixtures
105
+
106
+ Use **factory fixtures** when tests require variations of objects. Factories are appropriate for sample data, request payloads, ORM objects, mocked clients, and service configurations — not just data.
107
+
108
+ ``` python
109
+ @pytest.fixture
110
+ def user_payload_factory():
111
+ def _factory(**overrides):
112
+ payload = {
113
+ "email": "test@example.com",
114
+ "name": "Test User",
115
+ }
116
+ payload.update(overrides)
117
+ return payload
118
+
119
+ return _factory
120
+ ```
121
+
122
+ ## Avoid branching logic in fixtures
123
+
124
+ Fixtures should configure objects, not contain decision trees.
125
+
126
+ Bad:
127
+
128
+ ``` python
129
+ if status == "ok":
130
+ client.charge.return_value = ...
131
+ elif status == "declined":
132
+ ...
133
+ ```
134
+
135
+ Good:
136
+
137
+ ``` python
138
+ @pytest.fixture
139
+ def payment_client_factory():
140
+ def _factory(charge_response=None):
141
+ charge_response = charge_response or {"status": "ok"}
142
+
143
+ client = MagicMock(spec=PaymentClient)
144
+ client.charge.return_value = charge_response
145
+ return client
146
+
147
+ return _factory
148
+ ```
149
+
150
+ Tests should control behavior explicitly rather than relying on fixture branching.
151
+
152
+ ## FastAPI dependency overrides
153
+
154
+ Use **`app.dependency_overrides`** to swap FastAPI dependencies in tests. Do not patch internals directly.
155
+
156
+ ``` python
157
+ @pytest.fixture
158
+ def client(db_session):
159
+ def override_get_db():
160
+ yield db_session
161
+
162
+ app.dependency_overrides[get_db] = override_get_db
163
+ with TestClient(app) as c:
164
+ yield c
165
+ app.dependency_overrides.clear()
166
+ ```
167
+
168
+ Always clear `dependency_overrides` in teardown so overrides don't leak between tests.
169
+
170
+ ## FastAPI testing
171
+
172
+ Prefer `TestClient` for endpoint tests.
173
+
174
+ ``` python
175
+ def test_health_endpoint(client):
176
+ response = client.get("/health")
177
+
178
+ assert response.status_code == 200
179
+ assert response.json()["status"] == "ok"
180
+ ```
181
+
182
+ ## External client mocking
183
+
184
+ External services must be mocked.
185
+
186
+ - use `MagicMock(spec=ClientType)` or `create_autospec`
187
+ - do not make real network calls in unit tests
188
+ - assert the application's behavior, not the external service behavior
189
+
190
+ Shared mock setups should be implemented as **fixtures or factory fixtures**.
191
+
192
+ ## Parametrization
193
+
194
+ Use pytest parametrization when only inputs vary.
195
+
196
+ ``` python
197
+ @pytest.mark.parametrize(
198
+ "email",
199
+ [
200
+ "user@example.com",
201
+ "admin@example.com",
202
+ ],
203
+ )
204
+ def test_email_validation(email):
205
+ ...
206
+ ```
207
+
208
+ ## Deterministic time and data
209
+
210
+ Tests should not depend on the system clock or random values. Freeze time with `freezegun`; use explicit, stable inputs rather than `uuid4()` or `random`.
211
+
212
+ ``` python
213
+ @freeze_time("2025-01-01")
214
+ def test_token_expiry():
215
+ ...
216
+ ```