@martinffx/atelier 1.4.0 → 3.0.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 (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +95 -220
  3. package/agents/architect.md +6 -6
  4. package/agents/oracle.md +1 -1
  5. package/agents/{recon.md → sentinel.md} +3 -3
  6. package/dist/atelier.js +167 -26
  7. package/package.json +2 -1
  8. package/skills/atelier-orchestrator/SKILL.md +180 -0
  9. package/skills/atelier-setup/SKILL.md +80 -0
  10. package/skills/code-review/SKILL.md +2 -2
  11. package/skills/code-review/references/reviewers.md +3 -3
  12. package/skills/code-review/references/rq.md +9 -9
  13. package/skills/code-subagents/SKILL.md +44 -75
  14. package/skills/code-subagents/references/batch-reviewer-prompt.md +44 -0
  15. package/skills/code-subagents/references/implementor-prompt.md +10 -11
  16. package/skills/spec-brainstorm/SKILL.md +21 -16
  17. package/skills/spec-finish/SKILL.md +14 -14
  18. package/skills/spec-implement/SKILL.md +42 -42
  19. package/skills/spec-plan/SKILL.md +72 -26
  20. package/skills/spec-plan/references/plan_template.md +77 -0
  21. package/skills/code-subagents/references/code-quality-reviewer-prompt.md +0 -48
  22. package/skills/code-subagents/references/spec-reviewer-prompt.md +0 -62
  23. package/skills/python-architecture/SKILL.md +0 -148
  24. package/skills/python-architecture/references/data-modeling.md +0 -558
  25. package/skills/python-architecture/references/ddd.md +0 -569
  26. package/skills/python-architecture/references/functional-core.md +0 -337
  27. package/skills/python-build-tools/SKILL.md +0 -153
  28. package/skills/python-build-tools/references/basedpyright.md +0 -321
  29. package/skills/python-build-tools/references/ruff.md +0 -225
  30. package/skills/python-build-tools/references/uv.md +0 -257
  31. package/skills/python-fastapi/SKILL.md +0 -184
  32. package/skills/python-fastapi/references/api-design.md +0 -563
  33. package/skills/python-fastapi/references/dependencies.md +0 -479
  34. package/skills/python-fastapi/references/middleware.md +0 -475
  35. package/skills/python-fastapi/references/validation.md +0 -497
  36. package/skills/python-modern-python/SKILL.md +0 -201
  37. package/skills/python-modern-python/references/async.md +0 -76
  38. package/skills/python-modern-python/references/pattern-matching.md +0 -89
  39. package/skills/python-modern-python/references/typing.md +0 -96
  40. package/skills/python-monorepo/SKILL.md +0 -146
  41. package/skills/python-monorepo/references/docker.md +0 -515
  42. package/skills/python-monorepo/references/namespace-packages.md +0 -426
  43. package/skills/python-monorepo/references/workspace-config.md +0 -341
  44. package/skills/python-sqlalchemy/SKILL.md +0 -119
  45. package/skills/python-sqlalchemy/references/async.md +0 -51
  46. package/skills/python-sqlalchemy/references/models.md +0 -67
  47. package/skills/python-sqlalchemy/references/queries.md +0 -57
  48. package/skills/python-temporal/SKILL.md +0 -128
  49. package/skills/python-temporal/references/patterns.md +0 -87
  50. package/skills/python-temporal/references/testing.md +0 -54
  51. package/skills/python-testing/SKILL.md +0 -161
  52. package/skills/python-testing/references/boundaries.md +0 -449
  53. package/skills/python-testing/references/mocking.md +0 -449
  54. package/skills/python-testing/references/pytest.md +0 -524
  55. package/skills/spec-orchestrator/SKILL.md +0 -145
  56. package/skills/typescript-api-design/SKILL.md +0 -396
  57. package/skills/typescript-api-design/references/error-responses.md +0 -469
  58. package/skills/typescript-build-tools/SKILL.md +0 -447
  59. package/skills/typescript-build-tools/references/biome.md +0 -436
  60. package/skills/typescript-build-tools/references/bun.md +0 -432
  61. package/skills/typescript-build-tools/references/turborepo.md +0 -426
  62. package/skills/typescript-build-tools/references/vitest.md +0 -518
  63. package/skills/typescript-drizzle-orm/SKILL.md +0 -398
  64. package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
  65. package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
  66. package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
  67. package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
  68. package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
  69. package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
  70. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
  71. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
  72. package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
  73. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
  74. package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
  75. package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
  76. package/skills/typescript-effect-ts/SKILL.md +0 -341
  77. package/skills/typescript-effect-ts/references/core.md +0 -412
  78. package/skills/typescript-effect-ts/references/error-handling.md +0 -468
  79. package/skills/typescript-effect-ts/references/resources.md +0 -476
  80. package/skills/typescript-effect-ts/references/schema.md +0 -536
  81. package/skills/typescript-effect-ts/references/services.md +0 -461
  82. package/skills/typescript-fastify/SKILL.md +0 -405
  83. package/skills/typescript-fastify/references/plugins.md +0 -329
  84. package/skills/typescript-fastify/references/typeid.md +0 -315
  85. package/skills/typescript-functional-patterns/SKILL.md +0 -402
  86. package/skills/typescript-functional-patterns/references/adts.md +0 -497
  87. package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
  88. package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
  89. package/skills/typescript-functional-patterns/references/option-result.md +0 -585
  90. package/skills/typescript-testing/SKILL.md +0 -353
  91. package/skills/typescript-testing/references/mocking.md +0 -675
  92. package/skills/typescript-testing/references/msw.md +0 -852
  93. package/skills/typescript-testing/references/snapshot-testing.md +0 -774
@@ -1,148 +0,0 @@
1
- ---
2
- name: python-architecture
3
- description: Python application architecture with functional core, effectful shell, DDD, and data modeling. Use when designing application layers, separating pure business logic from IO, defining domain models, implementing validation, or structuring bounded contexts.
4
- user-invocable: false
5
- ---
6
-
7
- # Python Application Architecture
8
-
9
- Modern Python application architecture following functional core / imperative shell pattern, Domain-Driven Design, and type-safe data modeling.
10
-
11
- ## Core Principle: Functional Core / Imperative Shell
12
-
13
- Separate pure business logic from side effects:
14
-
15
- - **Functional Core**: Pure functions, business logic, no IO
16
- - **Imperative Shell**: Coordinates external dependencies, handles side effects
17
-
18
- See [references/functional-core.md](references/functional-core.md) for detailed patterns and examples.
19
-
20
- ## Layered Architecture
21
-
22
- Follow bottom-up dependency flow:
23
-
24
- ```
25
- Router/Handler → Service → Repository → Entity → Database
26
- ```
27
-
28
- Each layer depends only on layers below.
29
-
30
- **Responsibilities:**
31
- - **Entity**: Domain models, validation, business rules, data transformations (fromRequest, toRecord, toResponse)
32
- - **Repository**: Abstract storage interface, returns domain entities
33
- - **Service**: Business workflows, orchestrates entities and repositories
34
- - **Router/Handler**: HTTP handling, delegates to services
35
-
36
- ## Domain Models
37
-
38
- ### Entity Example
39
-
40
- ```python
41
- from dataclasses import dataclass
42
- from uuid import UUID
43
- from decimal import Decimal
44
-
45
- @dataclass
46
- class Order:
47
- """Entity - has identity and encapsulated behavior"""
48
- id: UUID
49
- customer_id: UUID
50
- total: Decimal
51
- status: str
52
-
53
- def apply_discount(self, rate: Decimal) -> None:
54
- """Business rule - encapsulated in entity"""
55
- if self.status == "pending":
56
- self.total = self.total * (1 - rate)
57
-
58
- @classmethod
59
- def from_request(cls, req, customer_id: UUID) -> "Order":
60
- """Transform API request → entity"""
61
- return cls(id=uuid4(), customer_id=customer_id, total=Decimal("0"), status="pending")
62
-
63
- def to_response(self):
64
- """Transform entity → API response"""
65
- return {"id": self.id, "total": self.total, "status": self.status}
66
- ```
67
-
68
- ### Value Object Example
69
-
70
- ```python
71
- from dataclasses import dataclass
72
-
73
- @dataclass(frozen=True)
74
- class Money:
75
- """Value object - immutable, no identity"""
76
- amount: Decimal
77
- currency: str
78
-
79
- def add(self, other: "Money") -> "Money":
80
- if self.currency != other.currency:
81
- raise ValueError("Cannot add different currencies")
82
- return Money(self.amount + other.amount, self.currency)
83
- ```
84
-
85
- See [references/ddd.md](references/ddd.md) for aggregates, bounded contexts, and domain services.
86
-
87
- ## Repository Pattern
88
-
89
- Abstract storage behind interface:
90
-
91
- ```python
92
- from abc import ABC, abstractmethod
93
- from typing import Optional
94
-
95
- class OrderRepository(ABC):
96
- """Abstract repository - interface only"""
97
-
98
- @abstractmethod
99
- def get(self, order_id: UUID) -> Optional[Order]:
100
- pass
101
-
102
- @abstractmethod
103
- def save(self, order: Order) -> None:
104
- pass
105
-
106
- class PostgresOrderRepository(OrderRepository):
107
- """Concrete implementation"""
108
-
109
- def get(self, order_id: UUID) -> Optional[Order]:
110
- record = self.session.get(OrderRecord, order_id)
111
- return Order.from_record(record) if record else None
112
-
113
- def save(self, order: Order) -> None:
114
- record = order.to_record()
115
- self.session.merge(record)
116
- self.session.commit()
117
- ```
118
-
119
- ## Data Modeling
120
-
121
- - **dataclasses**: Domain models and internal logic (lightweight, standard library)
122
- - **Pydantic**: API boundaries (validation, JSON schema, OpenAPI)
123
- - **Entity transformations**: `from_request()`, `to_response()`, `from_record()`, `to_record()`
124
-
125
- See [references/data-modeling.md](references/data-modeling.md) for validation patterns, Pydantic features, and transformation examples.
126
-
127
- ## Best Practices
128
-
129
- 1. **Pure functions first** - Write business logic without IO dependencies
130
- 2. **Entity encapsulation** - Keep business rules inside entities
131
- 3. **Repository abstraction** - Hide storage details, work with domain entities
132
- 4. **Validate at boundaries** - Use Pydantic at API edges, simple validation in entities
133
- 5. **Immutable value objects** - Always use `frozen=True`
134
- 6. **Single Responsibility** - Each layer has one reason to change
135
- 7. **Dependency direction** - Always depend on abstractions, not implementations
136
-
137
- ## Anti-Patterns
138
-
139
- ❌ **Anemic Domain Model** - Entities with only getters/setters, all logic in services
140
- ❌ **Transaction Script** - All logic in service layer, entities just data
141
- ❌ **Leaky Abstraction** - Repository exposing database details
142
- ❌ **God Object** - Entity with too many responsibilities
143
- ❌ **Mixed Concerns** - Business logic calling IO directly
144
-
145
- For detailed examples, patterns, and decision trees, see the reference materials:
146
- - [references/functional-core.md](references/functional-core.md) - Core vs shell separation
147
- - [references/ddd.md](references/ddd.md) - DDD patterns, aggregates, bounded contexts
148
- - [references/data-modeling.md](references/data-modeling.md) - dataclasses, Pydantic, transformations
@@ -1,558 +0,0 @@
1
- # Data Modeling in Python
2
-
3
- Modern Python provides multiple tools for data modeling: `dataclasses`, `Pydantic`, and `attrs`. Each has specific use cases.
4
-
5
- ## When to Use What
6
-
7
- **dataclasses** - Domain models and internal data
8
- - Part of standard library (Python 3.7+)
9
- - Fast and lightweight
10
- - Good for internal domain logic
11
- - Use when you control all data
12
-
13
- **Pydantic** - API boundaries and validation
14
- - Runtime validation
15
- - JSON schema generation
16
- - OpenAPI integration
17
- - Use at system edges (HTTP requests/responses)
18
-
19
- **attrs** - Advanced use cases
20
- - More features than dataclasses
21
- - Validators, converters, evolve
22
- - Use when dataclasses aren't enough
23
-
24
- ## dataclasses
25
-
26
- ### Basic Usage
27
-
28
- ```python
29
- from dataclasses import dataclass
30
- from uuid import UUID
31
- from decimal import Decimal
32
-
33
- @dataclass
34
- class Product:
35
- """Basic dataclass"""
36
- id: UUID
37
- name: str
38
- price: Decimal
39
- in_stock: bool = True # Default value
40
- ```
41
-
42
- **Features:**
43
- - Auto-generated `__init__`, `__repr__`, `__eq__`
44
- - Type hints required
45
- - Default values supported
46
- - Minimal boilerplate
47
-
48
- ### Immutable dataclasses
49
-
50
- Use `frozen=True` for value objects:
51
-
52
- ```python
53
- @dataclass(frozen=True)
54
- class Money:
55
- """Immutable value object"""
56
- amount: Decimal
57
- currency: str
58
-
59
- def add(self, other: "Money") -> "Money":
60
- """Return new instance instead of mutating"""
61
- if self.currency != other.currency:
62
- raise ValueError("Currency mismatch")
63
- return Money(self.amount + other.amount, self.currency)
64
-
65
- # Immutable - these fail:
66
- # money.amount = 100 # FrozenInstanceError
67
- # money.currency = "EUR" # FrozenInstanceError
68
- ```
69
-
70
- ### field() for Advanced Configuration
71
-
72
- ```python
73
- from dataclasses import dataclass, field
74
- from datetime import datetime
75
- from uuid import uuid4
76
-
77
- @dataclass
78
- class Order:
79
- id: UUID = field(default_factory=uuid4) # Call function for default
80
- created_at: datetime = field(default_factory=datetime.now)
81
- items: list[str] = field(default_factory=list) # Mutable default
82
- _internal: str = field(default="", repr=False) # Exclude from repr
83
- metadata: dict = field(default_factory=dict, compare=False) # Exclude from equality
84
- ```
85
-
86
- **field() parameters:**
87
- - `default`: Static default value
88
- - `default_factory`: Callable for default (use for mutable defaults)
89
- - `init`: Include in `__init__` (default True)
90
- - `repr`: Include in `__repr__` (default True)
91
- - `compare`: Include in `__eq__` (default True)
92
- - `hash`: Include in `__hash__` (default None)
93
-
94
- ### Validation with __post_init__
95
-
96
- ```python
97
- @dataclass
98
- class User:
99
- email: str
100
- age: int
101
-
102
- def __post_init__(self):
103
- """Validate after initialization"""
104
- if "@" not in self.email:
105
- raise ValueError("Invalid email address")
106
- if self.age < 0:
107
- raise ValueError("Age cannot be negative")
108
- if self.age < 18:
109
- raise ValueError("User must be at least 18")
110
-
111
- # Usage
112
- user = User("test@example.com", 25) # OK
113
- # User("invalid", 25) # ValueError: Invalid email address
114
- # User("test@example.com", 15) # ValueError: User must be at least 18
115
- ```
116
-
117
- ### Computed Fields
118
-
119
- ```python
120
- @dataclass
121
- class Rectangle:
122
- width: float
123
- height: float
124
-
125
- @property
126
- def area(self) -> float:
127
- """Computed property"""
128
- return self.width * self.height
129
-
130
- @property
131
- def perimeter(self) -> float:
132
- """Another computed property"""
133
- return 2 * (self.width + self.height)
134
-
135
- rect = Rectangle(width=10, height=5)
136
- print(rect.area) # 50.0
137
- print(rect.perimeter) # 30.0
138
- ```
139
-
140
- ### Inheritance
141
-
142
- ```python
143
- @dataclass
144
- class Person:
145
- name: str
146
- age: int
147
-
148
- @dataclass
149
- class Employee(Person):
150
- """Inherits name and age fields"""
151
- employee_id: str
152
- salary: Decimal
153
-
154
- emp = Employee(name="Alice", age=30, employee_id="E123", salary=Decimal("75000"))
155
- ```
156
-
157
- ## Pydantic
158
-
159
- ### Basic Models
160
-
161
- ```python
162
- from pydantic import BaseModel, EmailStr, Field
163
- from decimal import Decimal
164
- from uuid import UUID
165
-
166
- class CreateUserRequest(BaseModel):
167
- """Request validation at API boundary"""
168
- email: EmailStr # Validates email format
169
- age: int = Field(ge=18, le=120) # Min/max constraints
170
- name: str = Field(min_length=1, max_length=100)
171
-
172
- class UserResponse(BaseModel):
173
- """Response serialization"""
174
- id: UUID
175
- email: str
176
- name: str
177
- created_at: str
178
-
179
- model_config = {"from_attributes": True} # Enable ORM mode
180
- ```
181
-
182
- **Key features:**
183
- - Runtime validation
184
- - Automatic type coercion
185
- - Rich error messages
186
- - JSON schema generation
187
-
188
- ### Field Constraints
189
-
190
- ```python
191
- from pydantic import BaseModel, Field, field_validator
192
- from typing import Annotated
193
-
194
- class Product(BaseModel):
195
- name: str = Field(min_length=1, max_length=100)
196
- price: Decimal = Field(gt=0, decimal_places=2) # Greater than 0, 2 decimals
197
- quantity: int = Field(ge=0, le=10000) # Between 0 and 10000
198
- tags: list[str] = Field(max_length=10) # Max 10 tags
199
-
200
- # Alternative: Annotated types
201
- Price = Annotated[Decimal, Field(gt=0, decimal_places=2)]
202
-
203
- class Product(BaseModel):
204
- name: str
205
- price: Price # Reusable constraint
206
- ```
207
-
208
- ### Custom Validators
209
-
210
- ```python
211
- from pydantic import BaseModel, field_validator, model_validator
212
-
213
- class Order(BaseModel):
214
- items: list[str]
215
- discount: float
216
-
217
- @field_validator("items")
218
- @classmethod
219
- def items_not_empty(cls, v):
220
- """Field validator - validates single field"""
221
- if not v:
222
- raise ValueError("Order must have at least one item")
223
- return v
224
-
225
- @field_validator("discount")
226
- @classmethod
227
- def discount_range(cls, v):
228
- """Validate discount is percentage"""
229
- if not 0 <= v <= 1:
230
- raise ValueError("Discount must be between 0 and 1")
231
- return v
232
-
233
- @model_validator(mode="after")
234
- def validate_discount_with_items(self):
235
- """Model validator - validates across fields"""
236
- if self.discount > 0 and len(self.items) < 2:
237
- raise ValueError("Discount requires at least 2 items")
238
- return self
239
- ```
240
-
241
- ### Serialization and Deserialization
242
-
243
- ```python
244
- from pydantic import BaseModel
245
- from uuid import UUID
246
- from datetime import datetime
247
-
248
- class User(BaseModel):
249
- id: UUID
250
- email: str
251
- created_at: datetime
252
-
253
- # From JSON/dict
254
- data = {"id": "123e4567-e89b-12d3-a456-426614174000", "email": "test@example.com", "created_at": "2024-01-01T12:00:00"}
255
- user = User.model_validate(data) # Pydantic v2
256
- # user = User(**data) # Alternative
257
-
258
- # To dict
259
- user_dict = user.model_dump()
260
-
261
- # To JSON
262
- user_json = user.model_dump_json()
263
-
264
- # Exclude fields
265
- user_dict = user.model_dump(exclude={"created_at"})
266
-
267
- # Include only specific fields
268
- user_dict = user.model_dump(include={"id", "email"})
269
- ```
270
-
271
- ### ORM Integration
272
-
273
- ```python
274
- from pydantic import BaseModel
275
- from sqlalchemy.orm import Session
276
-
277
- class UserResponse(BaseModel):
278
- id: int
279
- email: str
280
- name: str
281
-
282
- model_config = {"from_attributes": True} # Enable ORM mode
283
-
284
- # Convert SQLAlchemy model to Pydantic
285
- def get_user(session: Session, user_id: int) -> UserResponse:
286
- db_user = session.get(UserModel, user_id)
287
- return UserResponse.model_validate(db_user) # Reads from ORM attributes
288
- ```
289
-
290
- ### Nested Models
291
-
292
- ```python
293
- class Address(BaseModel):
294
- street: str
295
- city: str
296
- country: str
297
-
298
- class OrderItem(BaseModel):
299
- product_id: UUID
300
- quantity: int
301
- price: Decimal
302
-
303
- class CreateOrderRequest(BaseModel):
304
- customer_email: EmailStr
305
- shipping_address: Address # Nested model
306
- items: list[OrderItem] # List of nested models
307
-
308
- @field_validator("items")
309
- @classmethod
310
- def validate_items(cls, v):
311
- if not v:
312
- raise ValueError("Order must have items")
313
- return v
314
- ```
315
-
316
- ### Generic Models
317
-
318
- ```python
319
- from pydantic import BaseModel
320
- from typing import TypeVar, Generic
321
-
322
- T = TypeVar("T")
323
-
324
- class PaginatedResponse(BaseModel, Generic[T]):
325
- """Generic response wrapper"""
326
- items: list[T]
327
- total: int
328
- page: int
329
- page_size: int
330
-
331
- class UserResponse(BaseModel):
332
- id: int
333
- email: str
334
-
335
- # Type-safe pagination
336
- response = PaginatedResponse[UserResponse](
337
- items=[UserResponse(id=1, email="test@example.com")],
338
- total=1,
339
- page=1,
340
- page_size=10
341
- )
342
- ```
343
-
344
- ## Entity Transformations
345
-
346
- Entities should handle all data transformations.
347
-
348
- ### Pattern: from_request, to_response, from_record, to_record
349
-
350
- ```python
351
- from dataclasses import dataclass
352
- from uuid import UUID, uuid4
353
- from decimal import Decimal
354
- from pydantic import BaseModel
355
-
356
- # ===== API Layer (Pydantic) =====
357
- class CreateProductRequest(BaseModel):
358
- name: str
359
- price: Decimal
360
-
361
- class ProductResponse(BaseModel):
362
- id: UUID
363
- name: str
364
- price: Decimal
365
- in_stock: bool
366
-
367
- # ===== Database Layer (SQLAlchemy or similar) =====
368
- @dataclass
369
- class ProductRecord:
370
- """Database representation"""
371
- id: UUID
372
- name: str
373
- price: Decimal
374
- in_stock: bool
375
-
376
- # ===== Domain Layer (dataclass) =====
377
- @dataclass
378
- class Product:
379
- """Domain entity with transformation methods"""
380
- id: UUID
381
- name: str
382
- price: Decimal
383
- in_stock: bool = True
384
-
385
- @classmethod
386
- def from_request(cls, request: CreateProductRequest) -> "Product":
387
- """Transform API request → domain entity"""
388
- return cls(
389
- id=uuid4(),
390
- name=request.name,
391
- price=request.price,
392
- in_stock=True
393
- )
394
-
395
- def to_response(self) -> ProductResponse:
396
- """Transform domain entity → API response"""
397
- return ProductResponse(
398
- id=self.id,
399
- name=self.name,
400
- price=self.price,
401
- in_stock=self.in_stock
402
- )
403
-
404
- @classmethod
405
- def from_record(cls, record: ProductRecord) -> "Product":
406
- """Transform database record → domain entity"""
407
- return cls(
408
- id=record.id,
409
- name=record.name,
410
- price=record.price,
411
- in_stock=record.in_stock
412
- )
413
-
414
- def to_record(self) -> ProductRecord:
415
- """Transform domain entity → database record"""
416
- return ProductRecord(
417
- id=self.id,
418
- name=self.name,
419
- price=self.price,
420
- in_stock=self.in_stock
421
- )
422
- ```
423
-
424
- **Usage in router:**
425
-
426
- ```python
427
- from fastapi import APIRouter
428
-
429
- router = APIRouter()
430
-
431
- @router.post("/products", response_model=ProductResponse)
432
- def create_product(request: CreateProductRequest, service: ProductService):
433
- """Router handles HTTP, delegates to service"""
434
- product = Product.from_request(request) # API → domain
435
- saved_product = service.create(product) # Domain logic
436
- return saved_product.to_response() # Domain → API
437
- ```
438
-
439
- **Usage in repository:**
440
-
441
- ```python
442
- class ProductRepository:
443
- def save(self, product: Product) -> Product:
444
- """Repository handles persistence"""
445
- record = product.to_record() # Domain → database
446
- db.save(record)
447
- return Product.from_record(record) # Database → domain
448
- ```
449
-
450
- ## Best Practices
451
-
452
- ### 1. Use dataclasses for Domain Layer
453
-
454
- ```python
455
- @dataclass
456
- class Order:
457
- """Internal domain model"""
458
- id: UUID
459
- customer_id: UUID
460
- total: Decimal
461
- status: str
462
-
463
- def submit(self) -> None:
464
- """Business logic in entity"""
465
- if self.status != "draft":
466
- raise ValueError("Only draft orders can be submitted")
467
- self.status = "submitted"
468
- ```
469
-
470
- ### 2. Use Pydantic for API Boundaries
471
-
472
- ```python
473
- class CreateOrderRequest(BaseModel):
474
- """Validate incoming requests"""
475
- customer_id: UUID
476
- items: list[OrderItemRequest]
477
-
478
- @field_validator("items")
479
- @classmethod
480
- def validate_items(cls, v):
481
- if not v:
482
- raise ValueError("Order must have items")
483
- return v
484
-
485
- class OrderResponse(BaseModel):
486
- """Serialize responses"""
487
- id: UUID
488
- total: Decimal
489
- status: str
490
- ```
491
-
492
- ### 3. Immutable Value Objects
493
-
494
- ```python
495
- @dataclass(frozen=True)
496
- class Money:
497
- """Value objects are immutable"""
498
- amount: Decimal
499
- currency: str
500
- ```
501
-
502
- ### 4. Validate in __post_init__ for dataclasses
503
-
504
- ```python
505
- @dataclass
506
- class User:
507
- email: str
508
- age: int
509
-
510
- def __post_init__(self):
511
- if "@" not in self.email:
512
- raise ValueError("Invalid email")
513
- if self.age < 18:
514
- raise ValueError("Must be 18+")
515
- ```
516
-
517
- ### 5. Keep Transformations in Entities
518
-
519
- ```python
520
- @dataclass
521
- class Product:
522
- @classmethod
523
- def from_request(cls, req: CreateProductRequest) -> "Product":
524
- """Entity handles transformation"""
525
- return cls(id=uuid4(), name=req.name, price=req.price)
526
-
527
- def to_response(self) -> ProductResponse:
528
- """Entity handles serialization"""
529
- return ProductResponse(id=self.id, name=self.name, price=self.price)
530
- ```
531
-
532
- ## Anti-Patterns
533
-
534
- ❌ **Using Pydantic everywhere**: Pydantic is for validation, not domain logic
535
- ❌ **Mutable value objects**: Value objects must be immutable
536
- ❌ **Transformation in service layer**: Entities should handle transformations
537
- ❌ **Exposing database models**: Always convert to domain entities
538
- ❌ **Validation in service layer**: Validate at boundaries (Pydantic) or in entities
539
-
540
- ## Summary
541
-
542
- **dataclasses:**
543
- - Domain models and internal data
544
- - Lightweight and fast
545
- - `frozen=True` for immutability
546
- - `__post_init__` for validation
547
-
548
- **Pydantic:**
549
- - API boundaries (requests/responses)
550
- - Runtime validation
551
- - JSON schema generation
552
- - OpenAPI integration
553
-
554
- **Entity transformations:**
555
- - `from_request`: API → domain
556
- - `to_response`: domain → API
557
- - `from_record`: database → domain
558
- - `to_record`: domain → database