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.
@@ -1,103 +1,207 @@
1
1
  ---
2
2
  name: async-patterns
3
- version: 1.0.0
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 Python Patterns — Concurrency & Performance
7
+ # Async Patterns — Python 3.13/3.14
7
8
 
8
- **ALWAYS invoke when writing async/await code or concurrent operations.**
9
+ **ALWAYS invoke when writing async/await code, fan-out flows, or rate-limited callers.**
9
10
 
10
- ## Core: asyncio.gather vs TaskGroup
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
- t1 = tg.create_task(fetch_users())
28
- t2 = tg.create_task(fetch_products())
29
- return t1.result(), t2.result()
30
- # If any task fails, ALL are cancelled
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
- ## HTTP Client (httpx)
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 client (connection pooling)
39
- async def fetch_data():
40
- async with httpx.AsyncClient(timeout=10.0) as client:
41
- response = await client.get("https://api.example.com/data")
42
- response.raise_for_status()
43
- return response.json()
44
-
45
- # Concurrent requests
46
- async def fetch_many(urls: list[str]) -> list[dict]:
47
- async with httpx.AsyncClient() as client:
48
- tasks = [client.get(url) for url in urls]
49
- responses = await asyncio.gather(*tasks)
50
- return [r.json() for r in responses]
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
- ## Semaphore (rate limiting)
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
- # Limit concurrent operations
57
- sem = asyncio.Semaphore(10) # Max 10 concurrent
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 fetch_with_limit(url: str):
60
- async with sem:
61
- async with httpx.AsyncClient() as client:
62
- return await client.get(url)
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
- ## Queue Pattern (producer/consumer)
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 data:
70
- await queue.put(item)
71
- await queue.put(None) # Sentinel
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
- if item is None:
77
- break
78
- await process(item)
79
- queue.task_done()
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
- queue = asyncio.Queue(maxsize=100) # Backpressure
83
- await asyncio.gather(producer(queue), consumer(queue))
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
- ## Timeouts
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
- # Always add timeouts to external calls
90
- try:
91
- result = await asyncio.wait_for(fetch_data(), timeout=5.0)
92
- except asyncio.TimeoutError:
93
- logger.warning("External API timed out")
94
- result = default_value
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
- 1. **`time.sleep()` in async code** — use `await asyncio.sleep()`
100
- 2. **Sync HTTP in async context** — use `httpx.AsyncClient`, not `requests`
101
- 3. **No timeout on external calls**always `asyncio.wait_for()` or `httpx.Timeout`
102
- 4. **Creating new client per request**reuse with `async with`
103
- 5. **Bare `asyncio.gather` without error handling** use `return_exceptions=True` or TaskGroup
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: 1.0.0
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 — Full-Stack Python (Django 5+)
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
- ## Async Views (Django 5.0+)
102
+ ## Composite Primary Keys (new in 5.2)
81
103
 
82
104
  ```python
83
- # Use async when calling external APIs or heavy I/O
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('https://api.example.com/data')
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
- 1. **Logic in views** — fat models, thin views
103
- 2. **N+1 queries** — always `select_related`/`prefetch_related`
104
- 3. **`objects.all()` without limit**paginate or `.only()`
105
- 4. **Raw SQL without parameterization** use ORM or `params=[]`
106
- 5. **Skipping migrations** always `makemigrations` after model changes
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