start-vibing-stacks 2.16.0 → 2.18.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/package.json +1 -1
- package/stacks/php/skills/composer-workflow/SKILL.md +118 -34
- package/stacks/php/skills/external-api-patterns/SKILL.md +171 -89
- package/stacks/php/skills/laravel-octane/SKILL.md +74 -2
- package/stacks/php/skills/mariadb-octane/SKILL.md +96 -4
- package/stacks/php/skills/php-patterns/SKILL.md +98 -5
- package/stacks/php/skills/phpstan-analysis/SKILL.md +136 -30
- package/stacks/php/skills/phpunit-testing/SKILL.md +247 -61
- package/stacks/php/skills/security-scan-php/SKILL.md +96 -3
- package/stacks/python/skills/api-security-python/SKILL.md +118 -15
- package/stacks/python/skills/async-patterns/SKILL.md +166 -62
- package/stacks/python/skills/django-patterns/SKILL.md +102 -11
- package/stacks/python/skills/fastapi-patterns/SKILL.md +277 -62
- package/stacks/python/skills/pydantic-validation/SKILL.md +106 -11
- package/stacks/python/skills/pytest-testing/SKILL.md +172 -54
- package/stacks/python/skills/python-patterns/SKILL.md +49 -7
- package/stacks/python/skills/python-performance/SKILL.md +183 -3
- package/stacks/python/skills/scripting-automation/SKILL.md +205 -119
|
@@ -1,103 +1,207 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: async-patterns
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Async Python concurrency patterns for Python 3.13 / 3.14. Covers structured concurrency with `asyncio.TaskGroup` (3.11+), the new `asyncio.timeout()` context manager (3.11+, replaces `wait_for`), `asyncio.Runner` for one-shot scripts, AnyIO for backend-agnostic code (asyncio + trio), httpx.AsyncClient connection pooling, semaphore-based rate limiting, producer/consumer with backpressure, free-threaded mode (PEP 779 — officially supported in 3.14) for true CPU parallelism. Invoke when writing any async/await code, fan-out/fan-in flows, or deciding between threads and async."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Async
|
|
7
|
+
# Async Patterns — Python 3.13/3.14
|
|
7
8
|
|
|
8
|
-
**ALWAYS invoke when writing async/await code or
|
|
9
|
+
**ALWAYS invoke when writing async/await code, fan-out flows, or rate-limited callers.**
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## Decision Map — when to reach for what
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Job kind?
|
|
15
|
+
├── I/O-bound (HTTP, DB, file, queue) → asyncio (one event loop)
|
|
16
|
+
├── CPU-bound, single-threaded → just call it; don't async
|
|
17
|
+
├── CPU-bound, multi-thread, GIL build → multiprocessing (workaround for GIL)
|
|
18
|
+
└── CPU-bound, multi-thread, 3.14 free-th → threading on python3.14t (real parallelism)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Free-threaded mode is now **officially supported** in Python 3.14 (PEP 779). Ships as a separate binary (`python3.14t`); per-thread overhead ~10–15% slower than the GIL build, so only switch when you actually need parallel CPU.
|
|
22
|
+
|
|
23
|
+
## Structured Concurrency — `TaskGroup` over `gather`
|
|
11
24
|
|
|
12
25
|
```python
|
|
13
26
|
import asyncio
|
|
14
27
|
|
|
15
|
-
# Gather — run multiple coroutines concurrently
|
|
16
|
-
async def fetch_all():
|
|
17
|
-
results = await asyncio.gather(
|
|
18
|
-
fetch_users(),
|
|
19
|
-
fetch_products(),
|
|
20
|
-
fetch_orders(),
|
|
21
|
-
)
|
|
22
|
-
users, products, orders = results
|
|
23
|
-
|
|
24
|
-
# TaskGroup (Python 3.11+) — structured concurrency with proper cancellation
|
|
25
28
|
async def fetch_all_safe():
|
|
26
29
|
async with asyncio.TaskGroup() as tg:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# If
|
|
30
|
+
users = tg.create_task(fetch_users())
|
|
31
|
+
products = tg.create_task(fetch_products())
|
|
32
|
+
orders = tg.create_task(fetch_orders())
|
|
33
|
+
# If ANY task raises, all siblings are cancelled, exception(s) re-raised as ExceptionGroup
|
|
34
|
+
return users.result(), products.result(), orders.result()
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
`TaskGroup` (3.11+) is the modern default. It propagates exceptions as `ExceptionGroup` and cancels siblings on failure — exactly what you want. `asyncio.gather(...)` only swallows errors when you pass `return_exceptions=True`, which is rarely correct.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# OK only when partial failure is the desired semantics
|
|
41
|
+
results = await asyncio.gather(*coros, return_exceptions=True)
|
|
42
|
+
ok = [r for r in results if not isinstance(r, Exception)]
|
|
43
|
+
errors = [r for r in results if isinstance(r, Exception)]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Timeouts — `asyncio.timeout()` over `wait_for`
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# 3.11+ — context manager, composes inside TaskGroup, supports deadlines
|
|
50
|
+
async def fetch_with_budget():
|
|
51
|
+
try:
|
|
52
|
+
async with asyncio.timeout(5):
|
|
53
|
+
return await fetch_data()
|
|
54
|
+
except TimeoutError:
|
|
55
|
+
return default_value
|
|
56
|
+
|
|
57
|
+
# Deadline-style — useful for chained operations sharing a budget
|
|
58
|
+
async def call_with_deadline(deadline_ts: float):
|
|
59
|
+
async with asyncio.timeout_at(deadline_ts):
|
|
60
|
+
await step1()
|
|
61
|
+
await step2()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Old `asyncio.wait_for(coro, 5)` still works but doesn't compose well inside `TaskGroup` — prefer `timeout()`.
|
|
65
|
+
|
|
66
|
+
## HTTP Client — httpx (reuse the client)
|
|
34
67
|
|
|
35
68
|
```python
|
|
36
69
|
import httpx
|
|
37
70
|
|
|
38
|
-
# REUSE
|
|
39
|
-
async def fetch_data():
|
|
40
|
-
async with httpx.AsyncClient(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
# REUSE — connection pooling, http/2, keepalive
|
|
72
|
+
async def fetch_data(url: str) -> dict:
|
|
73
|
+
async with httpx.AsyncClient(
|
|
74
|
+
timeout=httpx.Timeout(connect=5, read=10, write=10, pool=5),
|
|
75
|
+
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
76
|
+
http2=True,
|
|
77
|
+
) as client:
|
|
78
|
+
r = await client.get(url)
|
|
79
|
+
r.raise_for_status()
|
|
80
|
+
return r.json()
|
|
81
|
+
|
|
82
|
+
# In FastAPI — store on app.state in lifespan, reuse for the whole process
|
|
83
|
+
async def lifespan(app):
|
|
84
|
+
app.state.http = httpx.AsyncClient(timeout=10.0)
|
|
85
|
+
yield
|
|
86
|
+
await app.state.http.aclose()
|
|
51
87
|
```
|
|
52
88
|
|
|
53
|
-
|
|
89
|
+
`httpx.AsyncClient` opened per-request is **wrong** — you lose the pool, pay TCP+TLS handshake every call. Reuse one per process.
|
|
90
|
+
|
|
91
|
+
## Concurrent calls with bounded fan-out
|
|
54
92
|
|
|
55
93
|
```python
|
|
56
|
-
|
|
57
|
-
sem = asyncio.Semaphore(10)
|
|
94
|
+
async def fetch_many(urls: list[str], client: httpx.AsyncClient) -> list[dict]:
|
|
95
|
+
sem = asyncio.Semaphore(10) # max 10 in flight at once
|
|
58
96
|
|
|
59
|
-
async def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
async def one(url):
|
|
98
|
+
async with sem:
|
|
99
|
+
r = await client.get(url)
|
|
100
|
+
r.raise_for_status()
|
|
101
|
+
return r.json()
|
|
102
|
+
|
|
103
|
+
async with asyncio.TaskGroup() as tg:
|
|
104
|
+
tasks = [tg.create_task(one(u)) for u in urls]
|
|
105
|
+
return [t.result() for t in tasks]
|
|
63
106
|
```
|
|
64
107
|
|
|
65
|
-
|
|
108
|
+
Always cap fan-out — unbounded `gather`/`TaskGroup` over thousands of URLs will exhaust file descriptors, TLS sessions, or the remote rate limit.
|
|
109
|
+
|
|
110
|
+
## Producer / Consumer — with backpressure
|
|
66
111
|
|
|
67
112
|
```python
|
|
68
|
-
async def producer(queue: asyncio.Queue):
|
|
69
|
-
for item in
|
|
70
|
-
await queue.put(item)
|
|
71
|
-
|
|
113
|
+
async def producer(queue: asyncio.Queue, items):
|
|
114
|
+
for item in items:
|
|
115
|
+
await queue.put(item) # blocks if queue full → backpressure
|
|
116
|
+
for _ in range(N_CONSUMERS):
|
|
117
|
+
await queue.put(None) # one sentinel per consumer
|
|
72
118
|
|
|
73
119
|
async def consumer(queue: asyncio.Queue):
|
|
74
120
|
while True:
|
|
75
121
|
item = await queue.get()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
try:
|
|
123
|
+
if item is None:
|
|
124
|
+
return
|
|
125
|
+
await process(item)
|
|
126
|
+
finally:
|
|
127
|
+
queue.task_done()
|
|
128
|
+
|
|
129
|
+
async def main():
|
|
130
|
+
queue = asyncio.Queue(maxsize=100) # the cap is the backpressure
|
|
131
|
+
async with asyncio.TaskGroup() as tg:
|
|
132
|
+
tg.create_task(producer(queue, items))
|
|
133
|
+
for _ in range(N_CONSUMERS):
|
|
134
|
+
tg.create_task(consumer(queue))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Entry-points — `asyncio.Runner` (3.11+)
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import asyncio
|
|
141
|
+
|
|
142
|
+
async def main(): ...
|
|
143
|
+
|
|
144
|
+
# Modern — explicit lifecycle, works repeatedly, handles uvloop substitution
|
|
145
|
+
with asyncio.Runner() as runner:
|
|
146
|
+
runner.run(main())
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`asyncio.run(main())` is fine for one-shot scripts; `Runner` is better when you need to call multiple top-level coroutines or swap event loops (uvloop in production).
|
|
150
|
+
|
|
151
|
+
## AnyIO — when the library must support both backends
|
|
152
|
+
|
|
153
|
+
If you ship a library used by both asyncio and trio consumers, write against `anyio`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import anyio
|
|
157
|
+
from anyio import create_task_group, fail_after
|
|
158
|
+
|
|
159
|
+
async def fetch(url): ...
|
|
80
160
|
|
|
81
161
|
async def main():
|
|
82
|
-
|
|
83
|
-
|
|
162
|
+
async with create_task_group() as tg:
|
|
163
|
+
with fail_after(5): # AnyIO's timeout
|
|
164
|
+
for url in urls:
|
|
165
|
+
tg.start_soon(fetch, url)
|
|
166
|
+
|
|
167
|
+
anyio.run(main) # works on asyncio AND trio
|
|
84
168
|
```
|
|
85
169
|
|
|
86
|
-
##
|
|
170
|
+
## Threads + async — the right escape hatch
|
|
171
|
+
|
|
172
|
+
Sync code that you can't replace (legacy SDKs, blocking C extensions) must run **off the event loop**:
|
|
87
173
|
|
|
88
174
|
```python
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
# Run blocking CPU/IO in the default thread pool
|
|
178
|
+
result = await asyncio.to_thread(blocking_function, arg1, arg2)
|
|
179
|
+
|
|
180
|
+
# Bigger pool for many concurrent blocking calls
|
|
181
|
+
loop = asyncio.get_running_loop()
|
|
182
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=32) as pool:
|
|
183
|
+
result = await loop.run_in_executor(pool, blocking_function, arg)
|
|
95
184
|
```
|
|
96
185
|
|
|
186
|
+
For pure-CPU work on the standard GIL build, prefer `multiprocessing` or `concurrent.futures.ProcessPoolExecutor` — threads won't go faster.
|
|
187
|
+
|
|
97
188
|
## FORBIDDEN
|
|
98
189
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
190
|
+
| Pattern | Why |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `time.sleep(x)` inside `async def` | Blocks the entire event loop — use `await asyncio.sleep(x)` |
|
|
193
|
+
| `requests.get(...)` inside `async def` | Blocks event loop — use `httpx.AsyncClient` |
|
|
194
|
+
| Bare `asyncio.gather(...)` without error handling | Swallows exceptions of completed tasks; use `TaskGroup` |
|
|
195
|
+
| `asyncio.wait_for` instead of `asyncio.timeout()` | Older API, doesn't compose inside TaskGroup |
|
|
196
|
+
| New `httpx.AsyncClient()` per request | Loses pool/keepalive — reuse |
|
|
197
|
+
| Unbounded `TaskGroup` over user-provided list | DoS yourself — use Semaphore + cap |
|
|
198
|
+
| Mixing `asyncio` and `trio` directly | Use `anyio` to bridge or pick one |
|
|
199
|
+
| `asyncio.run` inside an existing loop | RuntimeError — use `asyncio.get_running_loop()` and `await` |
|
|
200
|
+
| Banking on free-threading for an I/O-bound API | asyncio is faster; free-threading buys CPU parallelism only |
|
|
201
|
+
|
|
202
|
+
## See Also
|
|
203
|
+
|
|
204
|
+
- `python-patterns` — async vs sync decision; free-threading rules
|
|
205
|
+
- `fastapi-patterns` — lifespan-managed http client, timeouts in routes
|
|
206
|
+
- `python-performance` — when to actually reach for `python3.14t`
|
|
207
|
+
- `pytest-testing` — testing async code (pytest-asyncio 1.0 + AnyIO)
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: django-patterns
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Django 5.2 LTS (April 2025, supported through April 2028) patterns for full-stack Python. Covers Composite Primary Keys (new in 5.2), `select_related`/`prefetch_related` for N+1 prevention, fat-model/thin-view discipline with custom managers, Django REST Framework viewsets/serializers/permissions, **async views** (5.0+) plus the honest caveat that the ORM remains synchronous — `sync_to_async()` is required when calling ORM from `async def`. Includes `pyproject.toml` + uv setup, migration discipline, and the upgrade timeline (Django 4.2 LTS support ends April 2026 — projects must be on 5.2)."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Django Patterns —
|
|
7
|
+
# Django Patterns — Django 5.2 LTS (2025–2028)
|
|
7
8
|
|
|
8
9
|
**ALWAYS invoke when writing Django models, views, or serializers.**
|
|
9
10
|
|
|
11
|
+
## Version Policy (2026)
|
|
12
|
+
|
|
13
|
+
| Branch | Released | Support ends | Status |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **5.2 LTS** | April 2025 | **April 2028** | ✅ Use this for new projects |
|
|
16
|
+
| 5.1 | Aug 2024 | Apr 2025 | EOL |
|
|
17
|
+
| 4.2 LTS | Apr 2023 | **April 2026** | ⚠️ Plan upgrade NOW if still on it |
|
|
18
|
+
|
|
19
|
+
Django 5.2 brings **Composite Primary Keys**, automatic model imports in `manage.py shell`, and simplified `BoundField` overrides. It's the LTS to standardise on for the next 2 years.
|
|
20
|
+
|
|
21
|
+
## Setup with uv
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv init my-django-app && cd my-django-app
|
|
25
|
+
uv add django>=5.2 djangorestframework psycopg[binary] django-environ
|
|
26
|
+
uv add --dev pytest pytest-django ruff mypy django-stubs
|
|
27
|
+
|
|
28
|
+
uv run django-admin startproject config .
|
|
29
|
+
uv run python manage.py startapp users
|
|
30
|
+
```
|
|
31
|
+
|
|
10
32
|
## Model Design (Fat Models, Thin Views)
|
|
11
33
|
|
|
12
34
|
```python
|
|
@@ -77,16 +99,43 @@ class UserViewSet(viewsets.ModelViewSet):
|
|
|
77
99
|
pagination_class = PageNumberPagination
|
|
78
100
|
```
|
|
79
101
|
|
|
80
|
-
##
|
|
102
|
+
## Composite Primary Keys (new in 5.2)
|
|
81
103
|
|
|
82
104
|
```python
|
|
83
|
-
|
|
105
|
+
from django.db.models import CompositePrimaryKey
|
|
106
|
+
|
|
107
|
+
class OrderLine(models.Model):
|
|
108
|
+
pk = CompositePrimaryKey("order", "product")
|
|
109
|
+
order = models.ForeignKey(Order, on_delete=models.CASCADE)
|
|
110
|
+
product = models.ForeignKey(Product, on_delete=models.PROTECT)
|
|
111
|
+
qty = models.PositiveIntegerField()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use composite PKs for join tables that have no business identity of their own — saves an autoincrement column and an index.
|
|
115
|
+
|
|
116
|
+
## Async Views (5.0+) — and the ORM caveat
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import httpx
|
|
120
|
+
from django.http import JsonResponse
|
|
121
|
+
from asgiref.sync import sync_to_async
|
|
122
|
+
|
|
123
|
+
# OK — purely external I/O, no ORM
|
|
84
124
|
async def fetch_external_data(request):
|
|
85
|
-
async with httpx.AsyncClient() as client:
|
|
86
|
-
response = await client.get(
|
|
125
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
126
|
+
response = await client.get("https://api.example.com/data")
|
|
87
127
|
return JsonResponse(response.json())
|
|
128
|
+
|
|
129
|
+
# ORM is still SYNCHRONOUS — you MUST adapt it
|
|
130
|
+
async def list_users(request):
|
|
131
|
+
users = await sync_to_async(list, thread_sensitive=True)(
|
|
132
|
+
User.objects.filter(is_active=True)[:50]
|
|
133
|
+
)
|
|
134
|
+
return JsonResponse({"users": [u.email for u in users]})
|
|
88
135
|
```
|
|
89
136
|
|
|
137
|
+
Django itself notes: *"We're still working on async support for the ORM and other parts of Django."* Until that lands, calling ORM in `async def` without `sync_to_async()` will either deadlock or raise `SynchronousOnlyOperation`. **For DB-heavy apps in 2026, sync views remain the default**; reach for async only when most of the work is external I/O.
|
|
138
|
+
|
|
90
139
|
## Migrations Best Practices
|
|
91
140
|
|
|
92
141
|
```bash
|
|
@@ -97,10 +146,52 @@ python manage.py migrate
|
|
|
97
146
|
python manage.py makemigrations --check --dry-run
|
|
98
147
|
```
|
|
99
148
|
|
|
149
|
+
## Settings — env-driven, no surprises
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# config/settings.py
|
|
153
|
+
import environ
|
|
154
|
+
from pathlib import Path
|
|
155
|
+
|
|
156
|
+
env = environ.Env(DEBUG=(bool, False))
|
|
157
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
158
|
+
environ.Env.read_env(BASE_DIR / ".env")
|
|
159
|
+
|
|
160
|
+
SECRET_KEY = env("SECRET_KEY")
|
|
161
|
+
DEBUG = env("DEBUG")
|
|
162
|
+
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
|
|
163
|
+
|
|
164
|
+
DATABASES = {"default": env.db()} # parses DATABASE_URL
|
|
165
|
+
CACHES = {"default": env.cache()} # parses CACHE_URL
|
|
166
|
+
|
|
167
|
+
# Security defaults — see api-security-python
|
|
168
|
+
SECURE_HSTS_SECONDS = 31_536_000
|
|
169
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
170
|
+
SECURE_HSTS_PRELOAD = True
|
|
171
|
+
SESSION_COOKIE_SECURE = True
|
|
172
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
173
|
+
CSRF_COOKIE_SECURE = True
|
|
174
|
+
```
|
|
175
|
+
|
|
100
176
|
## FORBIDDEN
|
|
101
177
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
178
|
+
| Anti-pattern | Reason |
|
|
179
|
+
|---|---|
|
|
180
|
+
| Logic in views | Fat models, thin views — keeps logic testable & reusable |
|
|
181
|
+
| N+1 queries | Always `select_related` (FK) / `prefetch_related` (M2M) |
|
|
182
|
+
| `Model.objects.all()` without `.only()`/pagination | Loads whole table into memory |
|
|
183
|
+
| Raw SQL with f-strings | SQL injection — use ORM or `params=[...]` |
|
|
184
|
+
| Skipping migrations | `makemigrations --check --dry-run` in CI |
|
|
185
|
+
| ORM in `async def` without `sync_to_async()` | `SynchronousOnlyOperation` / deadlock |
|
|
186
|
+
| Disabling `CsrfViewMiddleware` globally | Wide-open CSRF |
|
|
187
|
+
| Plain `pip` for new projects | Use `uv` |
|
|
188
|
+
| Staying on Django 4.2 LTS past April 2026 | Out of security support |
|
|
189
|
+
| New project on Django < 5.2 | 5.2 is the current LTS — start here |
|
|
190
|
+
|
|
191
|
+
## See Also
|
|
192
|
+
|
|
193
|
+
- `api-security-python` — Django settings checklist (HSTS, CSRF, sessions, CORS)
|
|
194
|
+
- `pydantic-validation` — DRF-Spectacular + Pydantic for OpenAPI (when DRF serializers aren't enough)
|
|
195
|
+
- `pytest-testing` — `pytest-django` setup
|
|
196
|
+
- `_shared/skills/postgres-patterns` — `uuidv7()`, virtual generated columns (PG18) usable from Django
|
|
197
|
+
- `_shared/skills/observability` — Django + structlog + OpenTelemetry
|