@martinffx/atelier 0.1.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 (130) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +285 -0
  4. package/agents/architect.md +38 -0
  5. package/agents/oracle.md +40 -0
  6. package/agents/recon.md +39 -0
  7. package/dist/atelier.js +33082 -0
  8. package/package.json +44 -0
  9. package/skills/code-conventional-commit/SKILL.md +134 -0
  10. package/skills/code-conventional-commit/evals/evals.json +23 -0
  11. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
  12. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
  13. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
  14. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
  15. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
  16. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
  17. package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
  18. package/skills/code-debug/SKILL.md +271 -0
  19. package/skills/code-debug/references/language-tools.md +139 -0
  20. package/skills/code-debug/references/techniques.md +117 -0
  21. package/skills/code-docs/SKILL.md +171 -0
  22. package/skills/code-docs/references/api-docs.md +116 -0
  23. package/skills/code-docs/references/changelog.md +64 -0
  24. package/skills/code-docs/references/readme-structure.md +94 -0
  25. package/skills/code-git-worktrees/SKILL.md +218 -0
  26. package/skills/code-perf/SKILL.md +163 -0
  27. package/skills/code-perf/references/common-bottlenecks.md +124 -0
  28. package/skills/code-perf/references/optimization-patterns.md +167 -0
  29. package/skills/code-perf/references/profiling-tools.md +126 -0
  30. package/skills/code-review/SKILL.md +78 -0
  31. package/skills/code-review/references/output.md +200 -0
  32. package/skills/code-review/references/reviewers.md +719 -0
  33. package/skills/code-review/references/rq.md +357 -0
  34. package/skills/code-review/references/rs.md +30 -0
  35. package/skills/code-security/SKILL.md +183 -0
  36. package/skills/code-security/references/owasp-top-10.md +129 -0
  37. package/skills/code-security/references/security-tools.md +102 -0
  38. package/skills/code-security/references/vulnerability-patterns.md +129 -0
  39. package/skills/code-stacked-commit/SKILL.md +155 -0
  40. package/skills/code-subagents/SKILL.md +184 -0
  41. package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
  42. package/skills/code-subagents/references/implementor-prompt.md +79 -0
  43. package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
  44. package/skills/oracle-architect/SKILL.md +246 -0
  45. package/skills/oracle-architect/references/api-design.md +345 -0
  46. package/skills/oracle-architect/references/data-modeling.md +318 -0
  47. package/skills/oracle-architect/references/ddd-patterns.md +247 -0
  48. package/skills/oracle-challenge/SKILL.md +126 -0
  49. package/skills/oracle-testing/SKILL.md +237 -0
  50. package/skills/oracle-testing/references/anti-patterns.md +618 -0
  51. package/skills/oracle-testing/references/boundaries.md +579 -0
  52. package/skills/oracle-testing/references/core-testing.md +691 -0
  53. package/skills/oracle-testing/references/edge-testing.md +794 -0
  54. package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
  55. package/skills/oracle-thinkdeep/SKILL.md +164 -0
  56. package/skills/python-architecture/SKILL.md +148 -0
  57. package/skills/python-architecture/references/data-modeling.md +558 -0
  58. package/skills/python-architecture/references/ddd.md +569 -0
  59. package/skills/python-architecture/references/functional-core.md +337 -0
  60. package/skills/python-build-tools/SKILL.md +153 -0
  61. package/skills/python-build-tools/references/basedpyright.md +321 -0
  62. package/skills/python-build-tools/references/ruff.md +225 -0
  63. package/skills/python-build-tools/references/uv.md +257 -0
  64. package/skills/python-fastapi/SKILL.md +184 -0
  65. package/skills/python-fastapi/references/api-design.md +563 -0
  66. package/skills/python-fastapi/references/dependencies.md +479 -0
  67. package/skills/python-fastapi/references/middleware.md +475 -0
  68. package/skills/python-fastapi/references/validation.md +497 -0
  69. package/skills/python-modern-python/SKILL.md +201 -0
  70. package/skills/python-modern-python/references/async.md +76 -0
  71. package/skills/python-modern-python/references/pattern-matching.md +89 -0
  72. package/skills/python-modern-python/references/typing.md +96 -0
  73. package/skills/python-monorepo/SKILL.md +146 -0
  74. package/skills/python-monorepo/references/docker.md +515 -0
  75. package/skills/python-monorepo/references/namespace-packages.md +426 -0
  76. package/skills/python-monorepo/references/workspace-config.md +341 -0
  77. package/skills/python-sqlalchemy/SKILL.md +119 -0
  78. package/skills/python-sqlalchemy/references/async.md +51 -0
  79. package/skills/python-sqlalchemy/references/models.md +67 -0
  80. package/skills/python-sqlalchemy/references/queries.md +57 -0
  81. package/skills/python-temporal/SKILL.md +128 -0
  82. package/skills/python-temporal/references/patterns.md +87 -0
  83. package/skills/python-temporal/references/testing.md +54 -0
  84. package/skills/python-testing/SKILL.md +161 -0
  85. package/skills/python-testing/references/boundaries.md +449 -0
  86. package/skills/python-testing/references/mocking.md +449 -0
  87. package/skills/python-testing/references/pytest.md +524 -0
  88. package/skills/spec-brainstorm/SKILL.md +325 -0
  89. package/skills/spec-finish/SKILL.md +159 -0
  90. package/skills/spec-implement/SKILL.md +296 -0
  91. package/skills/spec-orchestrator/SKILL.md +156 -0
  92. package/skills/spec-plan/SKILL.md +248 -0
  93. package/skills/typescript-api-design/SKILL.md +396 -0
  94. package/skills/typescript-api-design/references/error-responses.md +469 -0
  95. package/skills/typescript-build-tools/SKILL.md +447 -0
  96. package/skills/typescript-build-tools/references/biome.md +436 -0
  97. package/skills/typescript-build-tools/references/bun.md +432 -0
  98. package/skills/typescript-build-tools/references/turborepo.md +426 -0
  99. package/skills/typescript-build-tools/references/vitest.md +518 -0
  100. package/skills/typescript-drizzle-orm/SKILL.md +398 -0
  101. package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
  102. package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
  103. package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
  104. package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
  105. package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
  106. package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
  107. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
  108. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
  109. package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
  110. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
  111. package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
  112. package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
  113. package/skills/typescript-effect-ts/SKILL.md +341 -0
  114. package/skills/typescript-effect-ts/references/core.md +412 -0
  115. package/skills/typescript-effect-ts/references/error-handling.md +468 -0
  116. package/skills/typescript-effect-ts/references/resources.md +476 -0
  117. package/skills/typescript-effect-ts/references/schema.md +536 -0
  118. package/skills/typescript-effect-ts/references/services.md +461 -0
  119. package/skills/typescript-fastify/SKILL.md +405 -0
  120. package/skills/typescript-fastify/references/plugins.md +329 -0
  121. package/skills/typescript-fastify/references/typeid.md +315 -0
  122. package/skills/typescript-functional-patterns/SKILL.md +402 -0
  123. package/skills/typescript-functional-patterns/references/adts.md +497 -0
  124. package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
  125. package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
  126. package/skills/typescript-functional-patterns/references/option-result.md +585 -0
  127. package/skills/typescript-testing/SKILL.md +353 -0
  128. package/skills/typescript-testing/references/mocking.md +675 -0
  129. package/skills/typescript-testing/references/msw.md +852 -0
  130. package/skills/typescript-testing/references/snapshot-testing.md +774 -0
@@ -0,0 +1,558 @@
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