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.
- package/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/architect-lib/generators.ts +2 -0
- package/templates/pi/extensions/architect-lib/helpers.ts +21 -0
- package/templates/pi/extensions/architect.ts +28 -1
- package/templates/pi/preflight_report.json +2 -2
- package/templates/pi/skills/agents/go-codegen.md +42 -0
- package/templates/pi/skills/agents/java-codegen.md +42 -0
- package/templates/pi/skills/agents/python-codegen.md +41 -0
- package/templates/pi/skills/agents/typescript-codegen.md +40 -0
- package/templates/pi/skills/go-enterprise-codegen.md +450 -0
- package/templates/pi/skills/java-spring-enterprise-codegen.md +498 -0
- package/templates/pi/skills/python-enterprise-codegen.md +381 -0
- package/templates/pi/skills/typescript-enterprise-codegen.md +411 -0
|
@@ -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)*
|