@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.
- package/CHANGELOG.md +38 -0
- package/README.md +95 -220
- package/agents/architect.md +6 -6
- package/agents/oracle.md +1 -1
- package/agents/{recon.md → sentinel.md} +3 -3
- package/dist/atelier.js +167 -26
- package/package.json +2 -1
- package/skills/atelier-orchestrator/SKILL.md +180 -0
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/SKILL.md +2 -2
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +9 -9
- package/skills/code-subagents/SKILL.md +44 -75
- package/skills/code-subagents/references/batch-reviewer-prompt.md +44 -0
- package/skills/code-subagents/references/implementor-prompt.md +10 -11
- package/skills/spec-brainstorm/SKILL.md +21 -16
- package/skills/spec-finish/SKILL.md +14 -14
- package/skills/spec-implement/SKILL.md +42 -42
- package/skills/spec-plan/SKILL.md +72 -26
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +0 -48
- package/skills/code-subagents/references/spec-reviewer-prompt.md +0 -62
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/spec-orchestrator/SKILL.md +0 -145
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- 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
|