@eltonssouza/development-utility-kit 0.10.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/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: python/django-5
|
|
3
|
+
versions_covered: "5.0.x — 5.2.x"
|
|
4
|
+
last_validated: 2026-05-28
|
|
5
|
+
validated_against: "reference pack — Python 3.13 + Django 5.2 LTS + DRF 3.15"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-28
|
|
9
|
+
next_review_due: 2027-05-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Python 3.13 + Django 5.x
|
|
13
|
+
|
|
14
|
+
Canonical knowledge pack for greenfield projects on Python 3.12+/3.13 + Django 5.x (5.0, 5.1, 5.2 LTS). Django 5 raised the floor to Python 3.10+ and brought significant async view maturity, generated model fields, declarative DRF serializers, and Python 3.13 free-threaded compatibility on Django 5.2. For projects still on Django 4.2 LTS, use `django-4.md` (not yet published — open issue if you need it).
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: Python 3.12+ + Django 5.x` in `## Project Identity`.
|
|
19
|
+
- `pyproject.toml` (or `requirements.txt`) declares `django>=5.0,<6.0` and `python_requires = ">=3.12"`.
|
|
20
|
+
- Greenfield: **prefer this pack** unless your hosting platform pins Python 3.10/3.11.
|
|
21
|
+
- API-heavy projects: pair Django with Django REST Framework (DRF) for the API layer; `## Code patterns` below assumes DRF.
|
|
22
|
+
- For projects that are HTTP-API-only (no admin, no templates), consider FastAPI (`python/fastapi-*.md`) — Django shines when the admin + ORM + auth + forms ecosystem is valuable.
|
|
23
|
+
|
|
24
|
+
## 2. Stack baseline (what this pack assumes)
|
|
25
|
+
|
|
26
|
+
| Component | Version range | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Python | 3.12 (min) / 3.13 (recommended) | 3.13 brings free-threaded build; Django 5.2 verified on 3.13 |
|
|
29
|
+
| Django | 5.0.x — 5.2.x | 5.2 is LTS (April 2025 → April 2028); 5.0/5.1 are non-LTS |
|
|
30
|
+
| Django REST Framework | 3.15.x | Declarative serializer mode; `@extend_schema` for OpenAPI via drf-spectacular |
|
|
31
|
+
| ORM / migrations | Django ORM (native) + django-migrations | Never raw SQL outside `RunSQL` migrations; never auto-makemigrations in CI |
|
|
32
|
+
| Build / packaging | `uv` (recommended) or `pip` + `pyproject.toml` | `uv` is 10–100× faster; `uv lock` produces deterministic lockfiles |
|
|
33
|
+
| Async runtime | ASGI via `uvicorn` workers + `gunicorn` master | WSGI via `gunicorn` if no async views needed |
|
|
34
|
+
| Tests | pytest 8.x + pytest-django 4.x + factory-boy 3.x + Faker | NEVER SQLite for tests if prod is Postgres — use Postgres in CI via service container |
|
|
35
|
+
| Mutation | mutmut 3.x (or cosmic-ray) | Target ≥70% on `domain/` + `application/` |
|
|
36
|
+
| Coverage | coverage.py 7.x with `branch = True` | Target ≥85% lines, ≥80% branches |
|
|
37
|
+
| Static analysis | `ruff` (replaces flake8/black/isort) + `mypy` + `django-stubs` | `ruff check --fix .` + `ruff format .` + `mypy --strict` on `domain/` and `application/` |
|
|
38
|
+
| Security scan | `pip-audit` + `bandit` | 0 CVE with CVSS ≥7.0; `bandit -ll` for medium+ |
|
|
39
|
+
| Observability | OpenTelemetry SDK + `opentelemetry-instrumentation-django` | W3C Trace Context; auto-instrumented requests, DB, cache |
|
|
40
|
+
| Background jobs | Celery 5.x + Redis 7 | Or Django-Q2 for simpler needs |
|
|
41
|
+
| Static files | `whitenoise` | Behind gunicorn; no separate Nginx for static needed |
|
|
42
|
+
| Env config | `django-environ` or `pydantic-settings` | `.env` file gitignored; `env.example` committed |
|
|
43
|
+
|
|
44
|
+
## 3. Project structure (DDD-flavored Django)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
backend/
|
|
48
|
+
├── pyproject.toml # uv-managed; PEP 621 metadata
|
|
49
|
+
├── uv.lock # committed
|
|
50
|
+
├── manage.py
|
|
51
|
+
├── env.example # committed; .env gitignored
|
|
52
|
+
├── config/ # Django project (settings)
|
|
53
|
+
│ ├── __init__.py
|
|
54
|
+
│ ├── asgi.py
|
|
55
|
+
│ ├── wsgi.py
|
|
56
|
+
│ ├── urls.py
|
|
57
|
+
│ └── settings/
|
|
58
|
+
│ ├── __init__.py
|
|
59
|
+
│ ├── base.py
|
|
60
|
+
│ ├── dev.py
|
|
61
|
+
│ ├── prod.py
|
|
62
|
+
│ └── test.py
|
|
63
|
+
├── domain/ # pure Python; no Django imports
|
|
64
|
+
│ ├── products/
|
|
65
|
+
│ │ ├── entities.py # dataclasses, value objects
|
|
66
|
+
│ │ ├── repository.py # ABC port; impl lives in infrastructure
|
|
67
|
+
│ │ └── services.py # pure domain services
|
|
68
|
+
│ └── shared/
|
|
69
|
+
├── application/ # use cases (1 public callable each)
|
|
70
|
+
│ ├── products/
|
|
71
|
+
│ │ ├── create_product.py
|
|
72
|
+
│ │ ├── list_products.py
|
|
73
|
+
│ │ └── dto.py # request/response dataclasses
|
|
74
|
+
│ └── shared/
|
|
75
|
+
├── infrastructure/ # Django ORM adapters, external clients
|
|
76
|
+
│ ├── products/
|
|
77
|
+
│ │ ├── models.py # @models.Model — JPA-equivalent layer
|
|
78
|
+
│ │ ├── repository_impl.py # adapts ORM to the domain port
|
|
79
|
+
│ │ └── migrations/
|
|
80
|
+
│ └── shared/
|
|
81
|
+
├── api/ # DRF views + serializers + urls
|
|
82
|
+
│ ├── products/
|
|
83
|
+
│ │ ├── serializers.py
|
|
84
|
+
│ │ ├── views.py # APIView / ViewSet
|
|
85
|
+
│ │ ├── urls.py
|
|
86
|
+
│ │ └── permissions.py
|
|
87
|
+
│ └── shared/
|
|
88
|
+
│ └── exception_handler.py # DRF custom exception handler -> RFC 9457
|
|
89
|
+
└── tests/
|
|
90
|
+
├── unit/ # domain + application
|
|
91
|
+
├── integration/ # ORM + DRF with TestContainers Postgres
|
|
92
|
+
└── e2e/ # Playwright (optional, if there is a UI)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Rule**: `domain/` and `application/` contain **zero Django imports**. They are pure Python testable without a database, without `pytest-django`, in milliseconds. `infrastructure/` is the only layer that imports `django.db.models`. `api/` is the only layer that imports `rest_framework`.
|
|
96
|
+
|
|
97
|
+
## 4. Code patterns
|
|
98
|
+
|
|
99
|
+
### Domain entity as `@dataclass(frozen=True)` (no Django dependency)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# domain/products/entities.py
|
|
103
|
+
from dataclasses import dataclass
|
|
104
|
+
from decimal import Decimal
|
|
105
|
+
from uuid import UUID
|
|
106
|
+
from datetime import datetime
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class Product:
|
|
110
|
+
id: UUID
|
|
111
|
+
name: str
|
|
112
|
+
price: Decimal
|
|
113
|
+
stock: int
|
|
114
|
+
created_at: datetime
|
|
115
|
+
|
|
116
|
+
def reserve(self, qty: int) -> "Product":
|
|
117
|
+
if qty <= 0:
|
|
118
|
+
raise ValueError("qty must be positive")
|
|
119
|
+
if qty > self.stock:
|
|
120
|
+
raise ValueError("insufficient stock")
|
|
121
|
+
return Product(self.id, self.name, self.price, self.stock - qty, self.created_at)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Rule**: domain entities are immutable (`frozen=True`), live in `domain/`, and have **zero Django imports**. Business invariants are methods on the entity, not service-layer code. Tests for this file run without `pytest-django`.
|
|
125
|
+
|
|
126
|
+
### Infrastructure ORM model (separate file, separate type)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# infrastructure/products/models.py
|
|
130
|
+
from django.db import models
|
|
131
|
+
import uuid
|
|
132
|
+
|
|
133
|
+
class ProductORM(models.Model):
|
|
134
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
135
|
+
name = models.CharField(max_length=120, db_index=True)
|
|
136
|
+
price = models.DecimalField(max_digits=12, decimal_places=2)
|
|
137
|
+
stock = models.PositiveIntegerField()
|
|
138
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
139
|
+
|
|
140
|
+
class Meta:
|
|
141
|
+
db_table = "products"
|
|
142
|
+
indexes = [models.Index(fields=["created_at"])]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Rule**: ORM models live in `infrastructure/`, never `domain/`. Naming: `<Entity>ORM` to mark the boundary. Translation between domain entity and ORM model happens in `repository_impl.py`. UUID primary key by default — sequential integers are an anti-pattern for distributed systems.
|
|
146
|
+
|
|
147
|
+
### Repository implementation (DDD-style adapter)
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# infrastructure/products/repository_impl.py
|
|
151
|
+
from typing import Optional
|
|
152
|
+
from uuid import UUID
|
|
153
|
+
from domain.products.entities import Product
|
|
154
|
+
from domain.products.repository import ProductRepository
|
|
155
|
+
from infrastructure.products.models import ProductORM
|
|
156
|
+
|
|
157
|
+
class DjangoProductRepository(ProductRepository):
|
|
158
|
+
def get(self, id: UUID) -> Optional[Product]:
|
|
159
|
+
row = ProductORM.objects.filter(id=id).first()
|
|
160
|
+
return self._to_domain(row) if row else None
|
|
161
|
+
|
|
162
|
+
def save(self, product: Product) -> None:
|
|
163
|
+
ProductORM.objects.update_or_create(
|
|
164
|
+
id=product.id,
|
|
165
|
+
defaults={
|
|
166
|
+
"name": product.name,
|
|
167
|
+
"price": product.price,
|
|
168
|
+
"stock": product.stock,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _to_domain(row: ProductORM) -> Product:
|
|
174
|
+
return Product(
|
|
175
|
+
id=row.id, name=row.name, price=row.price,
|
|
176
|
+
stock=row.stock, created_at=row.created_at,
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Rule**: domain port (`ProductRepository`) is an ABC defined in `domain/`. Implementation imports Django. Application use cases depend on the ABC, not the impl — wired by DI in `apps.py` or via simple module-level singleton.
|
|
181
|
+
|
|
182
|
+
### DRF serializer + view (declarative mode, Django 5.x)
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# api/products/serializers.py
|
|
186
|
+
from rest_framework import serializers
|
|
187
|
+
from decimal import Decimal
|
|
188
|
+
|
|
189
|
+
class CreateProductRequest(serializers.Serializer):
|
|
190
|
+
name = serializers.CharField(max_length=120)
|
|
191
|
+
price = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=Decimal("0.01"))
|
|
192
|
+
stock = serializers.IntegerField(min_value=0)
|
|
193
|
+
|
|
194
|
+
class ProductResponse(serializers.Serializer):
|
|
195
|
+
id = serializers.UUIDField(read_only=True)
|
|
196
|
+
name = serializers.CharField(read_only=True)
|
|
197
|
+
price = serializers.DecimalField(max_digits=12, decimal_places=2, read_only=True)
|
|
198
|
+
stock = serializers.IntegerField(read_only=True)
|
|
199
|
+
created_at = serializers.DateTimeField(read_only=True)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# api/products/views.py
|
|
204
|
+
from rest_framework.views import APIView
|
|
205
|
+
from rest_framework.response import Response
|
|
206
|
+
from rest_framework import status
|
|
207
|
+
from application.products.create_product import CreateProductUseCase
|
|
208
|
+
from .serializers import CreateProductRequest, ProductResponse
|
|
209
|
+
|
|
210
|
+
class ProductCreateView(APIView):
|
|
211
|
+
def __init__(self, use_case: CreateProductUseCase, **kwargs):
|
|
212
|
+
super().__init__(**kwargs)
|
|
213
|
+
self.use_case = use_case
|
|
214
|
+
|
|
215
|
+
def post(self, request):
|
|
216
|
+
req = CreateProductRequest(data=request.data)
|
|
217
|
+
req.is_valid(raise_exception=True)
|
|
218
|
+
product = self.use_case.execute(**req.validated_data)
|
|
219
|
+
return Response(ProductResponse(product).data, status=status.HTTP_201_CREATED)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Rule**: serializers are request/response DTOs, never `ModelSerializer` from ORM directly (it leaks the data model into the API contract). Views are thin — they parse, validate, call the use case, serialize the response. Business logic lives in `application/`.
|
|
223
|
+
|
|
224
|
+
### Async view (Django 5.x, when a real reason exists)
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# api/products/async_views.py
|
|
228
|
+
from adrf.views import APIView # async DRF
|
|
229
|
+
from rest_framework.response import Response
|
|
230
|
+
import httpx
|
|
231
|
+
|
|
232
|
+
class ProductPriceCheckView(APIView):
|
|
233
|
+
async def get(self, request, product_id):
|
|
234
|
+
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
235
|
+
r = await client.get(f"https://price-api.example.com/{product_id}")
|
|
236
|
+
return Response({"upstream_price": r.json()["price"]})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Rule**: async views only when the view does I/O that benefits from concurrency (external HTTP, large concurrent reads). Sync views are simpler and have less footgun (no `sync_to_async` mismatch on ORM). When in doubt, sync.
|
|
240
|
+
|
|
241
|
+
## 5. Testing
|
|
242
|
+
|
|
243
|
+
### Unit (pytest, no Django bootstrap)
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# tests/unit/domain/products/test_product.py
|
|
247
|
+
import pytest
|
|
248
|
+
from decimal import Decimal
|
|
249
|
+
from uuid import uuid4
|
|
250
|
+
from datetime import datetime
|
|
251
|
+
from domain.products.entities import Product
|
|
252
|
+
|
|
253
|
+
def test_reserve_decreases_stock():
|
|
254
|
+
p = Product(uuid4(), "x", Decimal("9.90"), stock=10, created_at=datetime.now())
|
|
255
|
+
p2 = p.reserve(3)
|
|
256
|
+
assert p2.stock == 7
|
|
257
|
+
|
|
258
|
+
def test_reserve_refuses_zero():
|
|
259
|
+
p = Product(uuid4(), "x", Decimal("9.90"), stock=10, created_at=datetime.now())
|
|
260
|
+
with pytest.raises(ValueError):
|
|
261
|
+
p.reserve(0)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Runs in milliseconds. No `pytest-django`, no DB.
|
|
265
|
+
|
|
266
|
+
### Integration (pytest-django + Testcontainers Postgres)
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
# conftest.py
|
|
270
|
+
import pytest
|
|
271
|
+
from testcontainers.postgres import PostgresContainer
|
|
272
|
+
|
|
273
|
+
@pytest.fixture(scope="session")
|
|
274
|
+
def postgres_container():
|
|
275
|
+
with PostgresContainer("postgres:16-alpine") as pg:
|
|
276
|
+
yield pg
|
|
277
|
+
|
|
278
|
+
@pytest.fixture(scope="session")
|
|
279
|
+
def django_db_setup(django_db_setup, postgres_container):
|
|
280
|
+
pass # connection URL injected via env in pytest.ini
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
# tests/integration/infrastructure/products/test_repository.py
|
|
285
|
+
import pytest
|
|
286
|
+
from decimal import Decimal
|
|
287
|
+
from uuid import uuid4
|
|
288
|
+
from datetime import datetime, timezone
|
|
289
|
+
from domain.products.entities import Product
|
|
290
|
+
from infrastructure.products.repository_impl import DjangoProductRepository
|
|
291
|
+
|
|
292
|
+
@pytest.mark.django_db
|
|
293
|
+
def test_save_and_get():
|
|
294
|
+
repo = DjangoProductRepository()
|
|
295
|
+
p = Product(uuid4(), "widget", Decimal("9.90"), 100, datetime.now(timezone.utc))
|
|
296
|
+
repo.save(p)
|
|
297
|
+
fetched = repo.get(p.id)
|
|
298
|
+
assert fetched == p
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Rule**: never SQLite for integration tests if production is Postgres. Use Testcontainers Postgres 16. Migrations run automatically via `pytest-django`.
|
|
302
|
+
|
|
303
|
+
### Mutation (mutmut)
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
mutmut run --paths-to-mutate=domain,application
|
|
307
|
+
mutmut results
|
|
308
|
+
# Target: killed/total >= 70% on domain/ and application/
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### E2E (Playwright, optional)
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
# tests/e2e/test_create_product_flow.py
|
|
315
|
+
from playwright.sync_api import Page, expect
|
|
316
|
+
|
|
317
|
+
def test_user_creates_product(page: Page, live_server):
|
|
318
|
+
page.goto(f"{live_server.url}/admin/login/")
|
|
319
|
+
# ... login + navigation + assertions
|
|
320
|
+
expect(page.get_by_text("widget")).to_be_visible()
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## 6. Build & run commands
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
# Setup (uv recommended)
|
|
327
|
+
uv venv
|
|
328
|
+
uv pip sync requirements.lock # or: uv pip install -e ".[dev]"
|
|
329
|
+
|
|
330
|
+
# Run dev
|
|
331
|
+
python manage.py migrate
|
|
332
|
+
python manage.py runserver 0.0.0.0:8000
|
|
333
|
+
|
|
334
|
+
# Run prod (ASGI)
|
|
335
|
+
uvicorn config.asgi:application --workers 4 --host 0.0.0.0 --port 8000
|
|
336
|
+
|
|
337
|
+
# Lint + format (ruff replaces flake8 + black + isort)
|
|
338
|
+
ruff check --fix .
|
|
339
|
+
ruff format .
|
|
340
|
+
|
|
341
|
+
# Type check
|
|
342
|
+
mypy --strict domain/ application/
|
|
343
|
+
|
|
344
|
+
# Tests
|
|
345
|
+
pytest # all
|
|
346
|
+
pytest tests/unit/ # fast loop
|
|
347
|
+
pytest --cov=domain --cov=application --cov-branch --cov-fail-under=85
|
|
348
|
+
pytest -m "not slow" # skip Testcontainers in tight loop
|
|
349
|
+
|
|
350
|
+
# Mutation
|
|
351
|
+
mutmut run --paths-to-mutate=domain,application
|
|
352
|
+
|
|
353
|
+
# Security scan
|
|
354
|
+
pip-audit
|
|
355
|
+
bandit -ll -r domain/ application/ infrastructure/ api/
|
|
356
|
+
|
|
357
|
+
# Migrations (always review before commit)
|
|
358
|
+
python manage.py makemigrations
|
|
359
|
+
python manage.py showmigrations
|
|
360
|
+
python manage.py migrate
|
|
361
|
+
|
|
362
|
+
# Production-only checks before deploy
|
|
363
|
+
python manage.py check --deploy
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
367
|
+
|
|
368
|
+
### 7.1 Authentication & Authorization
|
|
369
|
+
|
|
370
|
+
- **Sessions for admin/web**: Django's built-in session auth. Cookie flags: `SESSION_COOKIE_SECURE=True`, `SESSION_COOKIE_HTTPONLY=True`, `SESSION_COOKIE_SAMESITE="Lax"` (or `Strict`).
|
|
371
|
+
- **API auth**: DRF + `djangorestframework-simplejwt` for stateless JWT, OR `dj-rest-auth` + session for cookie-based SPAs. Choose one — do not mix without ADR.
|
|
372
|
+
- **Password hashing**: Django uses Argon2 (preferred) or PBKDF2-SHA256. Add `argon2-cffi` and set `PASSWORD_HASHERS` with Argon2 first. Bcrypt with cost 12 acceptable.
|
|
373
|
+
- **MFA**: `django-otp` + `django-two-factor-auth` for TOTP. Always enabled for staff users.
|
|
374
|
+
- **Authorization**: `permissions.py` per app, using DRF `BasePermission` subclasses. Object-level via `get_object()` + check. IDOR mitigation: every "owned resource" read/write validates `request.user.id == obj.owner_id`.
|
|
375
|
+
|
|
376
|
+
### 7.2 CORS
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
# settings/prod.py
|
|
380
|
+
INSTALLED_APPS += ["corsheaders"]
|
|
381
|
+
MIDDLEWARE.insert(0, "corsheaders.middleware.CorsMiddleware")
|
|
382
|
+
CORS_ALLOWED_ORIGINS = ["https://app.example.com"] # NEVER ["*"] in prod
|
|
383
|
+
CORS_ALLOW_CREDENTIALS = True
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 7.3 Validation & input sanitization
|
|
387
|
+
|
|
388
|
+
- **SQL injection**: ORM parameterizes by default. NEVER use `raw()` or `extra()` with f-strings/concatenation. If raw SQL is needed, use `cursor.execute(sql, params)` with `%s` placeholders.
|
|
389
|
+
- **XSS**: Django templates auto-escape. DRF serializers do not auto-escape — sanitize via `bleach` if rendering user HTML in any frontend.
|
|
390
|
+
- **Path traversal**: never join user input directly into `FileField` `upload_to`. Use `secrets.token_urlsafe()` for filenames.
|
|
391
|
+
- **Mass assignment**: DRF serializers explicit; never `Serializer(Meta).fields = "__all__"` on writable serializers — list fields explicitly.
|
|
392
|
+
|
|
393
|
+
### 7.4 Secrets management
|
|
394
|
+
|
|
395
|
+
- `.env` file (loaded by `django-environ`) gitignored; `env.example` committed with placeholder values.
|
|
396
|
+
- `SECRET_KEY`: env var; rotate via `python manage.py rotate_secret_key` ritual (manual; Django does not auto-rotate). On rotation, invalidate sessions.
|
|
397
|
+
- AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault preferred over `.env` for prod.
|
|
398
|
+
|
|
399
|
+
### 7.5 Rate limiting
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
# pip install django-ratelimit
|
|
403
|
+
from django_ratelimit.decorators import ratelimit
|
|
404
|
+
|
|
405
|
+
@ratelimit(key="ip", rate="5/m", method="POST", block=True)
|
|
406
|
+
def login_view(request):
|
|
407
|
+
...
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
- `5/m` on `/api/auth/login`, `/api/auth/password-reset`, `/api/auth/signup`.
|
|
411
|
+
- `100/m` on regular GET endpoints by user.
|
|
412
|
+
- For DRF: `rest_framework.throttling.AnonRateThrottle` + `UserRateThrottle` with `DEFAULT_THROTTLE_RATES`.
|
|
413
|
+
- Brute-force lockout: `django-axes` for admin and login views — lock account after 5 failed attempts within 10 minutes.
|
|
414
|
+
|
|
415
|
+
### 7.6 OWASP Top 10 mapping
|
|
416
|
+
|
|
417
|
+
| OWASP | Mitigation in Django 5.x |
|
|
418
|
+
|---|---|
|
|
419
|
+
| A01 Broken Access Control | `LoginRequiredMiddleware` (Django 5.0+); per-view DRF permissions; object-level `get_object_or_404(Model, owner=request.user, id=...)` pattern |
|
|
420
|
+
| A02 Cryptographic Failures | Argon2 password hasher; `DJANGO_SETTINGS_MODULE=config.settings.prod` enforces TLS; `SECURE_HSTS_SECONDS=31536000`, `SECURE_HSTS_INCLUDE_SUBDOMAINS=True`, `SECURE_HSTS_PRELOAD=True` |
|
|
421
|
+
| A03 Injection | ORM parameterization; never `raw()`/`extra()` with f-strings; DRF input validation via serializers |
|
|
422
|
+
| A04 Insecure Design | DDD layering enforced (`domain/` no Django); use cases as single-responsibility classes; ADRs document deviations |
|
|
423
|
+
| A05 Security Misconfiguration | `python manage.py check --deploy` in CI; `DEBUG=False` in prod via env split; `ALLOWED_HOSTS` set to actual domains; `X_FRAME_OPTIONS="DENY"` |
|
|
424
|
+
| A06 Vulnerable Components | `pip-audit` in CI; `dependabot` or `renovate` for PR-based bumps; `bandit` for code-level patterns |
|
|
425
|
+
| A07 Auth Failures | `django-axes` brute-force lock; Argon2 hashing; `django-two-factor-auth` for staff; session rotation on password change |
|
|
426
|
+
| A08 Data Integrity | Verify upstream packages via `uv pip install --require-hashes`; SBOM via `cyclonedx-py`; signed releases for internal libs |
|
|
427
|
+
| A09 Logging Failures | Structured JSON logs via `python-json-logger`; correlation ID via `django-guid` middleware; **NEVER** log `request.body` raw (PII) |
|
|
428
|
+
| A10 SSRF | Allowlist outbound HTTP via `httpx` client with explicit allowed hosts; validate URLs server-side before fetching; never trust user-supplied URLs as fetch targets |
|
|
429
|
+
|
|
430
|
+
### 7.7 LGPD / GDPR / compliance specifics
|
|
431
|
+
|
|
432
|
+
- **PII tagging**: a `pii=True` model field convention enforced by `django-stubs` + custom lint (or naming convention `<field>__pii`).
|
|
433
|
+
- **Soft delete**: `is_deleted` boolean + `deleted_at` timestamp; never hard delete user-owned data without an explicit erasure request workflow.
|
|
434
|
+
- **Data subject access**: `python manage.py export_user_data <user_id>` custom command exports all PII as JSON for GDPR Article 15.
|
|
435
|
+
- **Erasure (Article 17)**: `python manage.py erase_user_data <user_id>` redacts PII fields while preserving foreign keys (e.g. orders kept with `customer=None`).
|
|
436
|
+
- **Encryption at rest**: PostgreSQL TDE or column-level via `django-cryptography` for sensitive fields (CPF, ID documents).
|
|
437
|
+
|
|
438
|
+
## 8. Anti-patterns (block in code-review)
|
|
439
|
+
|
|
440
|
+
| ❌ Bad | ✅ Good | Why |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `Product.objects.filter(...).first()` inside DRF view | Repository in `infrastructure/`, called from `application/` use case | View should be thin; logic + persistence belong in layers |
|
|
443
|
+
| `ModelSerializer` exposing all ORM fields | Request/response `Serializer` with explicit fields | API contract leaks the data model otherwise |
|
|
444
|
+
| `auto_now_add=True` on `created_at` + `auto_now=True` on `updated_at` mixed with manual sets | Pick one — automatic OR explicit, never both | Race conditions in batch operations |
|
|
445
|
+
| `SECRET_KEY` in `settings/base.py` | `env("SECRET_KEY")` via django-environ | Secret in code = secret in git history |
|
|
446
|
+
| `DEBUG=True` reachable in prod | `DJANGO_SETTINGS_MODULE=config.settings.prod` + `python manage.py check --deploy` gate | DEBUG exposes stack traces with code and DB queries |
|
|
447
|
+
| Migrations auto-applied in CI without review | `makemigrations` runs locally, dev reviews, commits, then CI runs `migrate` | Schema changes need human eyes |
|
|
448
|
+
| SQLite for tests when prod is Postgres | Testcontainers Postgres 16 in CI | Subtle SQL differences cause prod-only bugs |
|
|
449
|
+
| `.objects.all()` without `select_related` / `prefetch_related` in templates/serializers | Explicit `.select_related("category").prefetch_related("tags")` | N+1 queries kill performance under load |
|
|
450
|
+
| Catching `Exception:` in views | Catch specific domain exceptions; let DRF exception handler do the rest | Hides bugs, breaks 500 telemetry |
|
|
451
|
+
| Sync view with `requests.get(...)` blocking external call | Async view + `httpx.AsyncClient` OR move external call to Celery task | Blocks worker thread; cascade failures under load |
|
|
452
|
+
| `User = get_user_model()` at module level + circular import | Lazy import via `django.contrib.auth.get_user_model()` inside the function | Solves the AppRegistryNotReady bootstrap issue |
|
|
453
|
+
|
|
454
|
+
## 9. Migration hints — Django 4.2 → 5.x
|
|
455
|
+
|
|
456
|
+
Breaking changes worth flagging when `migrator` agent runs Django 4 → 5:
|
|
457
|
+
|
|
458
|
+
- **Python 3.10 minimum** (Django 5.0); recommend 3.12 or 3.13.
|
|
459
|
+
- **`USE_L10N` removed** — was deprecated since 4.0. Localization is always on; format strings via `formats.py`.
|
|
460
|
+
- **`pytz` removed** — use `zoneinfo` from stdlib. Replace `pytz.timezone("America/Sao_Paulo")` with `zoneinfo.ZoneInfo("America/Sao_Paulo")`.
|
|
461
|
+
- **Form rendering**: default template-based form rendering (Django 4.0+) is now the only mode. If you have legacy `as_p()` overrides, review.
|
|
462
|
+
- **Async ORM**: `aget`, `aupdate`, `acreate` are first-class in 5.x; if you wrote `sync_to_async(Model.objects.get)` workarounds, swap them for native async calls.
|
|
463
|
+
- **DRF on Django 5**: requires DRF 3.15+. Older DRF versions have async-related deprecations.
|
|
464
|
+
- **`USE_TZ = True` enforced**: timezone-aware datetimes everywhere. If your code does `datetime.now()` without tzinfo, Django 5 issues warnings and may raise in 6.0.
|
|
465
|
+
- **`LoginRequiredMiddleware`** (5.0+): when enabled, every view is login-required by default; mark public views with `@login_not_required`. Simpler than per-view decorators for protected apps.
|
|
466
|
+
- **Generated model fields** (5.0+): `GeneratedField` allows DB-computed columns; consider migrating computed properties into the DB layer where appropriate.
|
|
467
|
+
|
|
468
|
+
Hand off to `migrator` agent with: target version, current Django/Python versions from `pyproject.toml`, list of installed third-party Django apps (some need bumps).
|
|
469
|
+
|
|
470
|
+
## 10. References
|
|
471
|
+
|
|
472
|
+
- [Django 5.0 release notes](https://docs.djangoproject.com/en/5.0/releases/5.0/)
|
|
473
|
+
- [Django 5.2 LTS release notes](https://docs.djangoproject.com/en/5.2/releases/5.2/)
|
|
474
|
+
- [Django Security Documentation](https://docs.djangoproject.com/en/stable/topics/security/)
|
|
475
|
+
- [DRF official tutorial](https://www.django-rest-framework.org/)
|
|
476
|
+
- [OWASP Django Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Django_REST_Framework_Cheat_Sheet.html)
|
|
477
|
+
- [pytest-django docs](https://pytest-django.readthedocs.io/)
|
|
478
|
+
- [django-axes (brute-force lockout)](https://github.com/jazzband/django-axes)
|
|
479
|
+
- [django-environ (env var config)](https://github.com/joke2k/django-environ)
|
|
480
|
+
- ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
|
|
481
|
+
- ADR-026 (Generic agents + stack packs architecture)
|
|
482
|
+
- ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
|
|
483
|
+
- ADR-029 (Canonical pack format — this document follows it)
|