guardian-framework 0.1.11 → 0.1.12

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,381 @@
1
+ # Python Enterprise Code Generation — DDD + Clean Architecture
2
+
3
+ > Canonical skill for generating production-grade Python code following Clean Architecture with DDD.
4
+ > All code MUST follow these patterns. Validators enforce compliance.
5
+ >
6
+ > Source: Clean Architecture principles + Python best practices + DDD patterns from context7.
7
+
8
+ ---
9
+
10
+ ## 1. Project Structure — Clean Architecture with DDD
11
+
12
+ Every bounded context follows the same 4-layer structure:
13
+
14
+ ```
15
+ module/
16
+ ├── domain/ # Pure domain entities, value objects, events
17
+ │ ├── __init__.py
18
+ │ ├── entity.py # Aggregate roots and entities
19
+ │ ├── value.py # Value objects (dataclasses)
20
+ │ ├── event.py # Domain event payloads
21
+ │ ├── repository.py # Repository interfaces (ABC)
22
+ │ └── errors.py # Typed exceptions
23
+ ├── application/ # Service interfaces and DTOs
24
+ │ ├── __init__.py
25
+ │ ├── service.py # Service interface definitions
26
+ │ └── dto.py # Input/Output DTOs (pydantic)
27
+ ├── infrastructure/ # Repository implementations, external adapters
28
+ │ ├── __init__.py
29
+ │ ├── repository/ # Repository implementations
30
+ │ │ ├── sqlalchemy_repo.py
31
+ │ │ └── in_memory_repo.py
32
+ │ └── database.py # DB connection, migrations
33
+ └── interfaces/ # API contracts (HTTP)
34
+ ├── __init__.py
35
+ ├── controller.py # FastAPI route handlers
36
+ ├── middleware.py # Auth, validation middleware
37
+ └── dto.py # Request/Response schemas (pydantic)
38
+ ```
39
+
40
+ ### Dependency Direction Rule (Inward Dependency)
41
+
42
+ ```
43
+ domain → application → infrastructure → interfaces
44
+ ↑ ↑
45
+ └── inner layers never depend on outer layers
46
+ ```
47
+
48
+ - **domain/** — depends on nothing except stdlib
49
+ - **application/** — depends on domain
50
+ - **infrastructure/** — depends on domain + application
51
+ - **interfaces/** — depends on application
52
+
53
+ ---
54
+
55
+ ## 2. Domain Layer — Entities & Value Objects
56
+
57
+ ### Entity
58
+
59
+ ```python
60
+ # domain/entity.py
61
+
62
+ from __future__ import annotations
63
+ from dataclasses import dataclass, field
64
+ from datetime import datetime, timezone
65
+ from uuid import uuid4, UUID
66
+ from enum import Enum
67
+ from .errors import DomainError, ErrorCode
68
+
69
+
70
+ class Status(Enum):
71
+ PENDING = "pending"
72
+ ACTIVE = "active"
73
+ COMPLETED = "completed"
74
+ FAILED = "failed"
75
+
76
+ def can_transition_to(self, target: Status) -> bool:
77
+ transitions = {
78
+ Status.PENDING: {Status.ACTIVE},
79
+ Status.ACTIVE: {Status.COMPLETED, Status.FAILED},
80
+ Status.COMPLETED: set(),
81
+ Status.FAILED: set(),
82
+ }
83
+ return target in transitions[self]
84
+
85
+
86
+ @dataclass
87
+ class Entity:
88
+ id: UUID = field(default_factory=uuid4)
89
+ status: Status = Status.PENDING
90
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
91
+ updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
92
+
93
+ def transition(self, new_status: Status) -> None:
94
+ if not self.status.can_transition_to(new_status):
95
+ raise DomainError(
96
+ ErrorCode.INVALID_TRANSITION,
97
+ f"Cannot transition from {self.status.value} to {new_status.value}",
98
+ )
99
+ self.status = new_status
100
+ self.updated_at = datetime.now(timezone.utc)
101
+ ```
102
+
103
+ ### Value Object
104
+
105
+ ```python
106
+ # domain/value.py
107
+
108
+ from __future__ import annotations
109
+ from dataclasses import dataclass
110
+ from .errors import DomainError, ErrorCode
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class Money:
115
+ amount: int # Stored in smallest currency unit (cents)
116
+ currency: str
117
+
118
+ def __post_init__(self) -> None:
119
+ if self.amount < 0:
120
+ raise DomainError(ErrorCode.VALIDATION, "Amount must be non-negative")
121
+ if len(self.currency) != 3:
122
+ raise DomainError(ErrorCode.VALIDATION, "Currency must be ISO 4217")
123
+
124
+ def __add__(self, other: Money) -> Money:
125
+ if self.currency != other.currency:
126
+ raise DomainError(ErrorCode.VALIDATION, "Currency mismatch")
127
+ return Money(self.amount + other.amount, self.currency)
128
+ ```
129
+
130
+ ### Repository Interface
131
+
132
+ ```python
133
+ # domain/repository.py
134
+
135
+ from abc import ABC, abstractmethod
136
+ from uuid import UUID
137
+ from .entity import Entity, Status
138
+
139
+
140
+ class Repository(ABC):
141
+ @abstractmethod
142
+ async def find_by_id(self, id: UUID) -> Entity | None: ...
143
+
144
+ @abstractmethod
145
+ async def save(self, entity: Entity) -> None: ...
146
+
147
+ @abstractmethod
148
+ async def delete(self, id: UUID) -> None: ...
149
+
150
+ @abstractmethod
151
+ async def find_by_status(self, status: Status) -> list[Entity]: ...
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 3. Domain Error Handling
157
+
158
+ ```python
159
+ # domain/errors.py
160
+
161
+ from enum import Enum
162
+
163
+
164
+ class ErrorCode(Enum):
165
+ NOT_FOUND = "NOT_FOUND"
166
+ INVALID_STATE = "INVALID_STATE"
167
+ VALIDATION = "VALIDATION"
168
+ DUPLICATE = "DUPLICATE"
169
+ INVALID_TRANSITION = "INVALID_TRANSITION"
170
+
171
+
172
+ class DomainError(Exception):
173
+ def __init__(self, code: ErrorCode, message: str, cause: Exception | None = None) -> None:
174
+ self.code = code
175
+ self.cause = cause
176
+ super().__init__(f"[{code.value}] {message}")
177
+
178
+
179
+ class NotFoundError(DomainError):
180
+ def __init__(self, id: str) -> None:
181
+ super().__init__(ErrorCode.NOT_FOUND, f"Resource {id} not found")
182
+
183
+
184
+ class ValidationError(DomainError):
185
+ def __init__(self, message: str) -> None:
186
+ super().__init__(ErrorCode.VALIDATION, message)
187
+ ```
188
+
189
+ ### Rules
190
+ - ✅ Use typed exception classes with `ErrorCode`
191
+ - ✅ Inherit from `DomainError` for all domain exceptions
192
+ - ✅ Use `@dataclass(frozen=True)` for value objects
193
+ - ✅ Use `ABC` for repository interfaces
194
+ - ❌ Don't use bare `Exception` for domain errors
195
+ - ❌ Don't leak ORM models into domain layer
196
+
197
+ ---
198
+
199
+ ## 4. Application Layer — Services
200
+
201
+ ```python
202
+ # application/service.py
203
+
204
+ from uuid import UUID
205
+ from ..domain.entity import Entity, Status
206
+ from ..domain.repository import Repository
207
+
208
+
209
+ class Service:
210
+ def __init__(self, repo: Repository, logger: Logger) -> None:
211
+ self._repo = repo
212
+ self._logger = logger
213
+
214
+ async def execute(self, cmd: Command) -> Entity:
215
+ entity = Entity()
216
+ entity.transition(cmd.desired_status)
217
+ await self._repo.save(entity)
218
+ self._logger.info("Entity executed", extra={"id": str(entity.id)})
219
+ return entity
220
+
221
+ async def get_by_id(self, id: UUID) -> Entity:
222
+ entity = await self._repo.find_by_id(id)
223
+ if entity is None:
224
+ raise NotFoundError(str(id))
225
+ return entity
226
+ ```
227
+
228
+ ---
229
+
230
+ ## 5. Infrastructure Layer — Repository Implementation
231
+
232
+ ```python
233
+ # infrastructure/repository/sqlalchemy_repo.py
234
+
235
+ from uuid import UUID
236
+ from sqlalchemy.ext.asyncio import AsyncSession
237
+ from ...domain.entity import Entity, Status
238
+ from ...domain.repository import Repository
239
+
240
+
241
+ class SQLAlchemyRepository(Repository):
242
+ def __init__(self, session: AsyncSession) -> None:
243
+ self._session = session
244
+
245
+ async def find_by_id(self, id: UUID) -> Entity | None:
246
+ model = await self._session.get(EntityModel, id)
247
+ return model.to_domain() if model else None
248
+
249
+ async def save(self, entity: Entity) -> None:
250
+ model = EntityModel.from_domain(entity)
251
+ self._session.add(model)
252
+ await self._session.flush()
253
+ ```
254
+
255
+ ---
256
+
257
+ ## 6. Interfaces Layer — FastAPI Controllers
258
+
259
+ ```python
260
+ # interfaces/controller.py
261
+
262
+ from fastapi import APIRouter, HTTPException, status
263
+ from pydantic import BaseModel
264
+ from ..application.service import Service
265
+ from ..domain.errors import DomainError, ErrorCode
266
+
267
+ router = APIRouter()
268
+
269
+ def create_controller(svc: Service) -> APIRouter:
270
+ @router.post("/execute", status_code=status.HTTP_201_CREATED)
271
+ async def execute(cmd: ExecuteRequest) -> EntityResponse:
272
+ try:
273
+ entity = await svc.execute(cmd.to_domain())
274
+ return EntityResponse.from_domain(entity)
275
+ except DomainError as e:
276
+ raise HTTPException(
277
+ status_code=_error_http_status(e.code),
278
+ detail=e.message,
279
+ )
280
+
281
+ return router
282
+
283
+
284
+ def _error_http_status(code: ErrorCode) -> int:
285
+ mapping = {
286
+ ErrorCode.NOT_FOUND: 404,
287
+ ErrorCode.VALIDATION: 400,
288
+ ErrorCode.DUPLICATE: 409,
289
+ ErrorCode.INVALID_STATE: 409,
290
+ }
291
+ return mapping.get(code, 500)
292
+ ```
293
+
294
+ ---
295
+
296
+ ## 7. Testing Patterns
297
+
298
+ ```python
299
+ # tests/test_entity.py
300
+ import pytest
301
+ from uuid import uuid4
302
+ from module.domain.entity import Entity, Status
303
+ from module.domain.errors import DomainError
304
+
305
+
306
+ class TestEntity:
307
+ def test_transitions_pending_to_active(self) -> None:
308
+ entity = Entity()
309
+ entity.transition(Status.ACTIVE)
310
+ assert entity.status == Status.ACTIVE
311
+
312
+ def test_invalid_transition_raises(self) -> None:
313
+ entity = Entity()
314
+ entity.transition(Status.ACTIVE)
315
+ with pytest.raises(DomainError, match="INVALID_TRANSITION"):
316
+ entity.transition(Status.PENDING)
317
+ ```
318
+
319
+ ---
320
+
321
+ ## 8. Anti-Patterns — NEVER DO
322
+
323
+ ```python
324
+ # ❌ Business logic in __init__.py
325
+ # ❌ Global state at module level
326
+ db = Session() # BAD
327
+
328
+ # ❌ ORM models in domain
329
+ from sqlalchemy import Column # BAD — domain is ORM-free
330
+
331
+ # ❌ Mutable default arguments
332
+ def process(items=[]): # BAD — use None
333
+
334
+ # ❌ Broad exception handling
335
+ try:
336
+ ...
337
+ except Exception: # BAD — catch specific exceptions
338
+ pass
339
+
340
+ # ❌ Business logic in FastAPI routes
341
+ @router.post("/")
342
+ async def handler(payload: dict): # BAD — use pydantic models + service layer
343
+ ```
344
+
345
+ ---
346
+
347
+ ## 9. Project-Level Structure
348
+
349
+ ```
350
+ src/
351
+ module/ # One directory per bounded context
352
+ domain/
353
+ __init__.py
354
+ entity.py
355
+ value.py
356
+ repository.py
357
+ errors.py
358
+ application/
359
+ __init__.py
360
+ service.py
361
+ dto.py
362
+ infrastructure/
363
+ __init__.py
364
+ repository/
365
+ interfaces/
366
+ __init__.py
367
+ controller.py
368
+ dto.py
369
+ shared/
370
+ logger.py
371
+ database.py
372
+ main.py
373
+ tests/
374
+ pyproject.toml
375
+ ```
376
+
377
+ ---
378
+
379
+ *Version: 1.0.0*
380
+ *Last updated: 2026-07-03*
381
+ *Source: Guardian DDD patterns + Python best practices + context7 (/fastapi/fastapi)*