litclaude-ai 0.2.2
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/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# AnyIO Reference: Replacing asyncio Idioms
|
|
2
|
+
|
|
3
|
+
> **Skill mandate**: `import asyncio` is BANNED. Use `import anyio` exclusively.
|
|
4
|
+
> This reference targets AnyIO 4.x (2026 Python projects).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Task Groups (The Core Primitive)
|
|
9
|
+
|
|
10
|
+
AnyIO uses **structured concurrency** via task groups. A task group is an async context manager that guarantees all child tasks finish before the block exits.
|
|
11
|
+
|
|
12
|
+
### `start_soon` — fire-and-forget
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import anyio
|
|
16
|
+
|
|
17
|
+
async def worker(n: int) -> None:
|
|
18
|
+
await anyio.sleep(1)
|
|
19
|
+
print(f"task {n} done")
|
|
20
|
+
|
|
21
|
+
async def main() -> None:
|
|
22
|
+
async with anyio.create_task_group() as tg:
|
|
23
|
+
for i in range(3):
|
|
24
|
+
tg.start_soon(worker, i)
|
|
25
|
+
print("all tasks finished")
|
|
26
|
+
|
|
27
|
+
anyio.run(main)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Signature**: `tg.start_soon(func, *args, name=None)`
|
|
31
|
+
- `func` must be a **coroutine function** (not a coroutine object).
|
|
32
|
+
- `name` is optional, for introspection/debugging.
|
|
33
|
+
- No return value; exceptions propagate as `ExceptionGroup` on exit.
|
|
34
|
+
|
|
35
|
+
### `start` — wait for ready signal
|
|
36
|
+
|
|
37
|
+
Use when a task must initialize before the caller proceeds (e.g., starting a server and then connecting to it).
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from anyio import TASK_STATUS_IGNORED, create_task_group, run
|
|
41
|
+
from anyio.abc import TaskStatus
|
|
42
|
+
|
|
43
|
+
async def start_server(port: int, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED) -> None:
|
|
44
|
+
listener = await anyio.create_tcp_listener(local_host="127.0.0.1", local_port=port)
|
|
45
|
+
task_status.started() # unblocks tg.start()
|
|
46
|
+
await listener.serve(handler)
|
|
47
|
+
|
|
48
|
+
async def main() -> None:
|
|
49
|
+
async with create_task_group() as tg:
|
|
50
|
+
await tg.start(start_server, 8080) # blocks until task_status.started()
|
|
51
|
+
# server is guaranteed ready here
|
|
52
|
+
async with await anyio.connect_tcp("127.0.0.1", 8080) as client:
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
run(main)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Rule of thumb**:
|
|
59
|
+
- Use `start_soon` when you don't need to know when the task is ready.
|
|
60
|
+
- Use `start` when the task must signal readiness before you continue.
|
|
61
|
+
|
|
62
|
+
### `create_task` — retrieving return values (AnyIO 4.14+)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
async def add(x: int, y: int) -> int:
|
|
66
|
+
return x + y
|
|
67
|
+
|
|
68
|
+
async def main() -> None:
|
|
69
|
+
async with anyio.create_task_group() as tg:
|
|
70
|
+
handle = tg.create_task(add(2, 4))
|
|
71
|
+
result = await handle # == 6
|
|
72
|
+
print(handle.return_value) # also 6
|
|
73
|
+
|
|
74
|
+
anyio.run(main)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Signature**: `tg.create_task(coro, *, name=None, context=None) -> TaskHandle[T]`
|
|
78
|
+
- Returns a `TaskHandle` you can `await` for the result.
|
|
79
|
+
- If the task raises, awaiting raises `TaskFailed` (or `TaskCancelled`).
|
|
80
|
+
- This is the canonical replacement for `asyncio.gather` when you need results.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 2. asyncio → anyio Cheat Sheet
|
|
85
|
+
|
|
86
|
+
| asyncio | anyio | Notes |
|
|
87
|
+
|---------|-------|-------|
|
|
88
|
+
| `asyncio.gather(a, b, c)` | `tg.create_task(a); tg.create_task(b); tg.create_task(c); results = [await h for h in handles]` | No direct gather; structured concurrency requires explicit task group scope. For fire-and-forget, use `tg.start_soon`. |
|
|
89
|
+
| `asyncio.create_task(coro)` | `tg.start_soon(func, *args)` or `tg.create_task(coro)` | `start_soon` takes a coroutine **function** + args. `create_task` takes a coroutine **object** and returns a handle. |
|
|
90
|
+
| `asyncio.sleep(n)` | `anyio.sleep(n)` | Identical semantics. |
|
|
91
|
+
| `asyncio.wait_for(coro, timeout)` | `with anyio.fail_after(timeout): await coro` | Raises `TimeoutError`. Use `move_on_after` for silent timeout. |
|
|
92
|
+
| `asyncio.Event()` | `anyio.Event()` | AnyIO events are **not reusable**; create a new one instead of `.clear()`. |
|
|
93
|
+
| `asyncio.Lock()` | `anyio.Lock()` | Use `async with lock:`. Pass `fast_acquire=True` if performance-critical. |
|
|
94
|
+
| `asyncio.Semaphore(n)` | `anyio.Semaphore(n)` | Same. Pass `fast_acquire=True` if performance-critical. |
|
|
95
|
+
| `asyncio.Condition()` | `anyio.Condition()` | Same semantics. |
|
|
96
|
+
| `asyncio.run(main())` | `anyio.run(main)` | Backend-agnostic entry point. |
|
|
97
|
+
| `asyncio.Queue(maxsize=N)` | `anyio.create_memory_object_stream[T](max_buffer_size=N)` | Returns `(send_stream, receive_stream)`. Supports `async for` on receive end. |
|
|
98
|
+
| `asyncio.to_thread(fn, *args)` | `anyio.to_thread.run_sync(fn, *args)` | Supports `abandon_on_cancel=True` and custom `limiter`. |
|
|
99
|
+
| `asyncio.run_coroutine_threadsafe(coro, loop)` | `anyio.from_thread.run(func, *args)` | Call async code from a worker thread. |
|
|
100
|
+
| `loop.call_soon_threadsafe(callback)` | `anyio.from_thread.run_sync(func, *args)` | Call sync code in event loop thread from worker thread, **with return value**. |
|
|
101
|
+
| `asyncio.shield(coro)` | `with anyio.CancelScope(shield=True): ...` | AnyIO shielding does not orphan tasks. |
|
|
102
|
+
| `asyncio.timeout(delay)` | `with anyio.fail_after(delay): ...` | AnyIO uses level cancellation, not edge cancellation. |
|
|
103
|
+
| `asyncio.CancelledError` | `anyio.get_cancelled_exc_class()` | Use this to catch cancellation portably across backends. |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 3. Cancellation & CancelScope
|
|
108
|
+
|
|
109
|
+
AnyIO uses **level cancellation** (inspired by Trio), not asyncio's **edge cancellation**.
|
|
110
|
+
|
|
111
|
+
- **Edge cancellation** (asyncio): A `CancelledError` is injected once. If caught and not re-raised, the task keeps running.
|
|
112
|
+
- **Level cancellation** (anyio): As long as a task is inside an effectively cancelled scope, every yield point raises a new cancellation exception.
|
|
113
|
+
|
|
114
|
+
### Basic CancelScope
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from anyio import CancelScope, create_task_group, get_cancelled_exc_class, sleep, run
|
|
118
|
+
|
|
119
|
+
async def worker() -> None:
|
|
120
|
+
try:
|
|
121
|
+
await sleep(10)
|
|
122
|
+
except get_cancelled_exc_class():
|
|
123
|
+
print("cancelled!")
|
|
124
|
+
raise # ALWAYS re-raise cancellation exceptions
|
|
125
|
+
|
|
126
|
+
async def main() -> None:
|
|
127
|
+
async with create_task_group() as tg:
|
|
128
|
+
tg.start_soon(worker)
|
|
129
|
+
await sleep(0.1)
|
|
130
|
+
tg.cancel_scope.cancel() # cancels all children
|
|
131
|
+
|
|
132
|
+
run(main)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Shielding
|
|
136
|
+
|
|
137
|
+
Shield a block from external cancellation. Essential for cleanup.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from anyio import CancelScope, create_task_group, sleep, run
|
|
141
|
+
|
|
142
|
+
async def main() -> None:
|
|
143
|
+
async with create_task_group() as tg:
|
|
144
|
+
with CancelScope(shield=True):
|
|
145
|
+
tg.start_soon(some_task)
|
|
146
|
+
tg.cancel_scope.cancel() # shielded block is protected
|
|
147
|
+
await sleep(1) # this still runs
|
|
148
|
+
|
|
149
|
+
run(main)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Combine with timeouts for graceful shutdown**:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from anyio import CancelScope, move_on_after
|
|
156
|
+
|
|
157
|
+
async def do_something(resource) -> None:
|
|
158
|
+
try:
|
|
159
|
+
await run_async_stuff()
|
|
160
|
+
except BaseException:
|
|
161
|
+
# Allow up to 10s for cleanup, then move on
|
|
162
|
+
with move_on_after(10, shield=True):
|
|
163
|
+
await resource.aclose()
|
|
164
|
+
raise
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Structured Concurrency Guarantee
|
|
168
|
+
|
|
169
|
+
A task group contains its own `CancelScope`. If any child task raises an exception:
|
|
170
|
+
1. The task group's cancel scope is cancelled.
|
|
171
|
+
2. All other child tasks receive cancellation.
|
|
172
|
+
3. The task group waits for all children to finish.
|
|
173
|
+
4. The original exception (wrapped in `ExceptionGroup` if multiple) is re-raised.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 4. Timeouts
|
|
178
|
+
|
|
179
|
+
Two context managers. Both create a `CancelScope` internally.
|
|
180
|
+
|
|
181
|
+
### `fail_after` — raises on timeout
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from anyio import fail_after, sleep, run
|
|
185
|
+
|
|
186
|
+
async def main() -> None:
|
|
187
|
+
try:
|
|
188
|
+
with fail_after(5) as scope:
|
|
189
|
+
await sleep(10)
|
|
190
|
+
except TimeoutError:
|
|
191
|
+
print("timed out")
|
|
192
|
+
print(scope.cancelled_caught) # True
|
|
193
|
+
|
|
194
|
+
run(main)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `move_on_after` — silent timeout
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from anyio import move_on_after, sleep, run
|
|
201
|
+
|
|
202
|
+
async def main() -> None:
|
|
203
|
+
with move_on_after(5) as scope:
|
|
204
|
+
await sleep(10)
|
|
205
|
+
print("this never prints")
|
|
206
|
+
|
|
207
|
+
print("exited scope, cancelled =", scope.cancelled_caught)
|
|
208
|
+
|
|
209
|
+
run(main)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Combined with shielding
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from anyio import move_on_after
|
|
216
|
+
|
|
217
|
+
# Give cleanup 10 seconds, but don't let outer cancellation interrupt it
|
|
218
|
+
with move_on_after(10, shield=True):
|
|
219
|
+
await resource.aclose()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 5. Memory Object Streams (Queue Replacement)
|
|
225
|
+
|
|
226
|
+
Replaces `asyncio.Queue` with a safer, typed, structured-concurrency-friendly construct.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from anyio import create_task_group, create_memory_object_stream, run
|
|
230
|
+
from anyio.streams.memory import MemoryObjectReceiveStream
|
|
231
|
+
|
|
232
|
+
async def consumer(stream: MemoryObjectReceiveStream[str]) -> None:
|
|
233
|
+
async with stream: # closes receive end on exit
|
|
234
|
+
async for item in stream:
|
|
235
|
+
print("received", item)
|
|
236
|
+
|
|
237
|
+
async def main() -> None:
|
|
238
|
+
# Type-annotated stream creation (AnyIO 4+ syntax)
|
|
239
|
+
send_stream, receive_stream = create_memory_object_stream[str](max_buffer_size=10)
|
|
240
|
+
|
|
241
|
+
async with create_task_group() as tg:
|
|
242
|
+
tg.start_soon(consumer, receive_stream)
|
|
243
|
+
async with send_stream:
|
|
244
|
+
for i in range(5):
|
|
245
|
+
await send_stream.send(f"item {i}")
|
|
246
|
+
# send_stream closed → consumer's async for loop exits naturally
|
|
247
|
+
|
|
248
|
+
run(main)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Key differences from `asyncio.Queue`**:
|
|
252
|
+
- **Bounded by default**: `max_buffer_size=0` means send blocks until a receiver is ready.
|
|
253
|
+
- **Cloneable**: Each producer/consumer can close its own clone. The stream only ends when **all** clones of one end are closed.
|
|
254
|
+
- **Async iterable**: `async for item in receive_stream:` works out of the box.
|
|
255
|
+
- **Type-safe**: Generic `create_memory_object_stream[T]()`.
|
|
256
|
+
- **Synchronous close**: Both `close()` and `async with` work.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 6. Backend Selection
|
|
261
|
+
|
|
262
|
+
AnyIO is backend-agnostic. Code written against AnyIO APIs runs on both asyncio and Trio.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
import anyio
|
|
266
|
+
|
|
267
|
+
async def main() -> None:
|
|
268
|
+
print("running on", anyio.current_async_library())
|
|
269
|
+
await anyio.sleep(1)
|
|
270
|
+
|
|
271
|
+
# Default backend (asyncio)
|
|
272
|
+
anyio.run(main)
|
|
273
|
+
|
|
274
|
+
# Explicit backend
|
|
275
|
+
anyio.run(main, backend="trio")
|
|
276
|
+
anyio.run(main, backend="asyncio", backend_options={"debug": True})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Library design rule**: Never hardcode a backend. Let the application choose via `anyio.run()`. Libraries should only import `anyio` and avoid backend-specific APIs.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 7. Compatibility with asyncio-only libraries
|
|
284
|
+
|
|
285
|
+
### Using asyncio libraries under the asyncio backend
|
|
286
|
+
|
|
287
|
+
If a third-party library exposes only an asyncio interface (returns asyncio coroutine objects), it works directly under the asyncio backend because AnyIO runs on top of asyncio's event loop:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
import anyio
|
|
291
|
+
import some_asyncio_only_lib # returns asyncio.Future/coroutine objects
|
|
292
|
+
|
|
293
|
+
async def main() -> None:
|
|
294
|
+
# This works because under the asyncio backend, await passes through
|
|
295
|
+
result = await some_asyncio_only_lib.fetch_data()
|
|
296
|
+
|
|
297
|
+
anyio.run(main, backend="asyncio")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Important**: This only works on the `asyncio` backend. On the `trio` backend, asyncio-native objects will not work.
|
|
301
|
+
|
|
302
|
+
### When you MUST use asyncio APIs
|
|
303
|
+
|
|
304
|
+
Some APIs have no AnyIO equivalent and require direct event loop access:
|
|
305
|
+
|
|
306
|
+
| Scenario | asyncio API | AnyIO approach |
|
|
307
|
+
|----------|-------------|----------------|
|
|
308
|
+
| Signal handlers | `loop.add_signal_handler()` | `anyio.open_signal_receiver()` |
|
|
309
|
+
| Custom protocols | `asyncio.Protocol` | Use AnyIO streams / sockets |
|
|
310
|
+
| Direct Future manipulation | `asyncio.Future` | Avoid; use AnyIO primitives |
|
|
311
|
+
| Eager task factories | `asyncio.eager_task_factory` | Experimental in AnyIO; avoid |
|
|
312
|
+
|
|
313
|
+
If you absolutely need the running loop:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
import asyncio
|
|
317
|
+
|
|
318
|
+
async def main() -> None:
|
|
319
|
+
loop = asyncio.get_running_loop()
|
|
320
|
+
# ... do something loop-specific ...
|
|
321
|
+
# WARNING: this breaks backend-agnosticism
|
|
322
|
+
|
|
323
|
+
anyio.run(main, backend="asyncio")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Best practice**: Wrap asyncio-only code in a backend-agnostic facade, and document that the feature requires the asyncio backend.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 8. Idiomatic Code Snippets
|
|
331
|
+
|
|
332
|
+
### Snippet 1: Parallel HTTP requests with timeout and cleanup
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
import anyio
|
|
336
|
+
|
|
337
|
+
async def fetch(url: str) -> bytes:
|
|
338
|
+
await anyio.sleep(0.5) # simulate
|
|
339
|
+
return b"data"
|
|
340
|
+
|
|
341
|
+
async def main() -> None:
|
|
342
|
+
urls = ["a", "b", "c"]
|
|
343
|
+
async with anyio.create_task_group() as tg:
|
|
344
|
+
with anyio.move_on_after(5):
|
|
345
|
+
for url in urls:
|
|
346
|
+
tg.start_soon(fetch, url)
|
|
347
|
+
# All tasks are cancelled on timeout; task group waits for cleanup
|
|
348
|
+
|
|
349
|
+
anyio.run(main)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Snippet 2: Producer-consumer with memory object stream
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
import anyio
|
|
356
|
+
from anyio.streams.memory import MemoryObjectReceiveStream
|
|
357
|
+
|
|
358
|
+
async def producer(send_stream: anyio.streams.memory.MemoryObjectSendStream[int]) -> None:
|
|
359
|
+
async with send_stream:
|
|
360
|
+
for i in range(100):
|
|
361
|
+
await send_stream.send(i)
|
|
362
|
+
|
|
363
|
+
async def consumer(receive_stream: MemoryObjectReceiveStream[int]) -> None:
|
|
364
|
+
async with receive_stream:
|
|
365
|
+
async for item in receive_stream:
|
|
366
|
+
print(f"consumed {item}")
|
|
367
|
+
|
|
368
|
+
async def main() -> None:
|
|
369
|
+
send, receive = anyio.create_memory_object_stream[int](max_buffer_size=5)
|
|
370
|
+
async with anyio.create_task_group() as tg:
|
|
371
|
+
tg.start_soon(producer, send)
|
|
372
|
+
tg.start_soon(consumer, receive)
|
|
373
|
+
|
|
374
|
+
anyio.run(main)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Snippet 3: Calling sync code from async
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
import time
|
|
381
|
+
import anyio
|
|
382
|
+
|
|
383
|
+
async def main() -> None:
|
|
384
|
+
# Run blocking function in worker thread
|
|
385
|
+
result = await anyio.to_thread.run_sync(time.sleep, 2)
|
|
386
|
+
print("done")
|
|
387
|
+
|
|
388
|
+
anyio.run(main)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Snippet 4: Calling async code from a worker thread
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
import anyio
|
|
395
|
+
|
|
396
|
+
def blocking_callback() -> None:
|
|
397
|
+
# Inside a worker thread, call back into the event loop
|
|
398
|
+
anyio.from_thread.run(anyio.sleep, 1)
|
|
399
|
+
anyio.from_thread.run_sync(print, "hello from thread")
|
|
400
|
+
|
|
401
|
+
async def main() -> None:
|
|
402
|
+
await anyio.to_thread.run_sync(blocking_callback)
|
|
403
|
+
|
|
404
|
+
anyio.run(main)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Snippet 5: Graceful shutdown with shielded cleanup
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
import anyio
|
|
411
|
+
|
|
412
|
+
async def worker() -> None:
|
|
413
|
+
try:
|
|
414
|
+
await anyio.sleep_forever()
|
|
415
|
+
except anyio.get_cancelled_exc_class():
|
|
416
|
+
with anyio.CancelScope(shield=True):
|
|
417
|
+
await anyio.sleep(0.5) # cleanup
|
|
418
|
+
print("cleaned up")
|
|
419
|
+
raise
|
|
420
|
+
|
|
421
|
+
async def main() -> None:
|
|
422
|
+
async with anyio.create_task_group() as tg:
|
|
423
|
+
tg.start_soon(worker)
|
|
424
|
+
await anyio.sleep(1)
|
|
425
|
+
tg.cancel_scope.cancel()
|
|
426
|
+
|
|
427
|
+
anyio.run(main)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Sources
|
|
433
|
+
|
|
434
|
+
- AnyIO Documentation (stable): https://anyio.readthedocs.io/en/stable/
|
|
435
|
+
- AnyIO GitHub (HEAD `cb245dba`): https://github.com/agronholm/anyio
|
|
436
|
+
- Task Groups: https://anyio.readthedocs.io/en/stable/tasks.html
|
|
437
|
+
- Cancellation & Timeouts: https://anyio.readthedocs.io/en/stable/cancellation.html
|
|
438
|
+
- Streams: https://anyio.readthedocs.io/en/stable/streams.html
|
|
439
|
+
- Synchronization: https://anyio.readthedocs.io/en/stable/synchronization.html
|
|
440
|
+
- Threads: https://anyio.readthedocs.io/en/stable/threads.html
|
|
441
|
+
- Basics / Backends: https://anyio.readthedocs.io/en/stable/basics.html
|
|
442
|
+
- Design Rationale (why asyncio is problematic): https://anyio.readthedocs.io/en/stable/why.html
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Data Modeling
|
|
2
|
+
|
|
3
|
+
Which container to use, how to structure data, and why frozen is the default.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Decision flowchart
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Is it a fixed set of named constants?
|
|
11
|
+
YES → StrEnum / IntEnum
|
|
12
|
+
NO ↓
|
|
13
|
+
Is it just branding a primitive (int, str, float)?
|
|
14
|
+
YES → NewType("X", base)
|
|
15
|
+
NO ↓
|
|
16
|
+
Is it an interface / contract ("this thing can do X")?
|
|
17
|
+
├─ Shape only, no shared code → Protocol
|
|
18
|
+
└─ Shared method implementation needed → ABC
|
|
19
|
+
NO ↓
|
|
20
|
+
Does the data cross a trust boundary (user input, API, file, external DB)?
|
|
21
|
+
YES → pydantic.BaseModel (frozen=True) — validates + serializes
|
|
22
|
+
NO ↓
|
|
23
|
+
Is it a dict shape needed for JSON compat / **kwargs typing?
|
|
24
|
+
YES → TypedDict
|
|
25
|
+
NO ↓
|
|
26
|
+
Is it structured data with named fields?
|
|
27
|
+
YES → @dataclass(frozen=True, slots=True)
|
|
28
|
+
NO ↓
|
|
29
|
+
Is it a tuple with positional semantics (x, y coords / DB row)?
|
|
30
|
+
YES → NamedTuple
|
|
31
|
+
NO → you probably don't need a new type
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Container reference
|
|
37
|
+
|
|
38
|
+
### @dataclass — internal value object
|
|
39
|
+
|
|
40
|
+
The default for structured data inside your codebase. Zero overhead, no framework coupling.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import NewType
|
|
45
|
+
|
|
46
|
+
UserId = NewType("UserId", int)
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class User:
|
|
50
|
+
id: UserId
|
|
51
|
+
name: str
|
|
52
|
+
email: str
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class Point:
|
|
56
|
+
x: float
|
|
57
|
+
y: float
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Always `frozen=True, slots=True`. Mutable only when mutation is the documented purpose — opt out with `# noqa: MUTABLE_OK`.
|
|
61
|
+
|
|
62
|
+
### Pydantic BaseModel — trust boundary guardian
|
|
63
|
+
|
|
64
|
+
Use when data enters or leaves your system. Validates at construction, serializes to JSON, generates OpenAPI schema.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pydantic import BaseModel, ConfigDict, EmailStr
|
|
68
|
+
|
|
69
|
+
class CreateUserRequest(BaseModel):
|
|
70
|
+
model_config = ConfigDict(frozen=True)
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
email: EmailStr
|
|
74
|
+
age: int
|
|
75
|
+
|
|
76
|
+
class UserResponse(BaseModel):
|
|
77
|
+
model_config = ConfigDict(frozen=True)
|
|
78
|
+
|
|
79
|
+
id: int
|
|
80
|
+
name: str
|
|
81
|
+
email: str
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**The one rule**: data crosses a trust boundary → Pydantic. Everything else → dataclass.
|
|
85
|
+
Never use Pydantic for internal-only data just because it's convenient. The validation cost is real.
|
|
86
|
+
|
|
87
|
+
### TypedDict — dict that knows its shape
|
|
88
|
+
|
|
89
|
+
Use when the value must stay a `dict` at runtime — JSON blobs, `**kwargs`, third-party APIs expecting dicts.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from typing import TypedDict, NotRequired
|
|
93
|
+
|
|
94
|
+
class Headers(TypedDict):
|
|
95
|
+
content_type: str
|
|
96
|
+
authorization: NotRequired[str]
|
|
97
|
+
|
|
98
|
+
def make_request(url: str, headers: Headers) -> None: ...
|
|
99
|
+
|
|
100
|
+
make_request("https://api.example.com", {"content_type": "application/json"})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Protocol — structural interface
|
|
104
|
+
|
|
105
|
+
"Anything that has method X" — no inheritance required.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from typing import Protocol
|
|
109
|
+
|
|
110
|
+
class Renderable(Protocol):
|
|
111
|
+
def render(self) -> str: ...
|
|
112
|
+
|
|
113
|
+
class Saveable(Protocol):
|
|
114
|
+
async def save(self) -> None: ...
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class MarkdownDoc:
|
|
118
|
+
content: str
|
|
119
|
+
def render(self) -> str:
|
|
120
|
+
return self.content
|
|
121
|
+
|
|
122
|
+
def publish(doc: Renderable) -> None:
|
|
123
|
+
print(doc.render()) # MarkdownDoc works — no inheritance needed
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Default to Protocol for interfaces. ABC only when you need shared method implementations.
|
|
127
|
+
|
|
128
|
+
### ABC — interface with shared code
|
|
129
|
+
|
|
130
|
+
Only when Protocol isn't enough.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from abc import ABC, abstractmethod
|
|
134
|
+
|
|
135
|
+
class BaseRepository(ABC):
|
|
136
|
+
@abstractmethod
|
|
137
|
+
async def get(self, id: int) -> Model | None: ...
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
async def save(self, model: Model) -> None: ...
|
|
141
|
+
|
|
142
|
+
async def get_or_raise(self, id: int) -> Model:
|
|
143
|
+
result = await self.get(id)
|
|
144
|
+
if result is None:
|
|
145
|
+
msg = f"{type(self).__name__}: id {id} not found"
|
|
146
|
+
raise LookupError(msg)
|
|
147
|
+
return result
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### NamedTuple — positional + named (rare)
|
|
151
|
+
|
|
152
|
+
Only when you need tuple protocol (unpacking, indexing).
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from typing import NamedTuple
|
|
156
|
+
|
|
157
|
+
class Coordinate(NamedTuple):
|
|
158
|
+
x: float
|
|
159
|
+
y: float
|
|
160
|
+
|
|
161
|
+
x, y = Coordinate(1.0, 2.0) # tuple unpacking
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
99% of the time, `@dataclass(frozen=True, slots=True)` is better.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Quick lookup
|
|
169
|
+
|
|
170
|
+
| Situation | Use | Why |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| User input, API request/response | `Pydantic BaseModel` | Validation, JSON schema, serialization |
|
|
173
|
+
| DB row ↔ Python (ORM) | SQLAlchemy `Mapped[]` model | ORM integration, async session |
|
|
174
|
+
| Internal value object | `@dataclass(frozen=True, slots=True)` | Zero overhead, no validation needed |
|
|
175
|
+
| Multiple outcomes from function | Union of frozen dataclasses | Distinct types for `match` |
|
|
176
|
+
| Dict shape for JSON / `**kwargs` | `TypedDict` | Stays a dict at runtime |
|
|
177
|
+
| Fixed constants | `StrEnum` / `IntEnum` | Exhaustive match, no typos |
|
|
178
|
+
| Distinct primitive | `NewType("X", int)` | Zero runtime cost, type-level only |
|
|
179
|
+
| Contract / capability | `Protocol` | Structural typing, no inheritance |
|
|
180
|
+
| Contract + shared impl | `ABC` | When Protocol isn't enough |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Comparison matrix
|
|
185
|
+
|
|
186
|
+
| Feature | dataclass | Pydantic | TypedDict | Protocol | NamedTuple | NewType | Enum |
|
|
187
|
+
|---|---|---|---|---|---|---|---|
|
|
188
|
+
| Validation | - | ✓ | - | - | - | - | - |
|
|
189
|
+
| JSON serialization | manual | built-in | native dict | - | - | - | `.value` |
|
|
190
|
+
| Immutable | frozen=True | frozen=True | - (dict) | N/A | always | N/A | always |
|
|
191
|
+
| Runtime cost | ~zero | validation | zero | zero | ~zero | zero | ~zero |
|
|
192
|
+
| `match` support | ✓ | ✓ | - | - | ✓ | - | ✓ |
|
|
193
|
+
| `slots` support | ✓ | - | - | - | - | - | - |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Parse, don't validate
|
|
198
|
+
|
|
199
|
+
Validate at the boundary. Inside the boundary, types are proof of validity.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# BAD — validate then pass raw data
|
|
203
|
+
def process_email(email: str) -> None:
|
|
204
|
+
if "@" not in email:
|
|
205
|
+
raise ValueError("invalid email")
|
|
206
|
+
# still a raw str everywhere downstream
|
|
207
|
+
|
|
208
|
+
# GOOD — parse into typed value at boundary
|
|
209
|
+
from typing import NewType
|
|
210
|
+
|
|
211
|
+
Email = NewType("Email", str)
|
|
212
|
+
|
|
213
|
+
def parse_email(raw: str) -> Email:
|
|
214
|
+
if "@" not in raw or "." not in raw.split("@")[1]:
|
|
215
|
+
msg = f"invalid email: {raw}"
|
|
216
|
+
raise ValueError(msg)
|
|
217
|
+
return Email(raw.lower().strip())
|
|
218
|
+
|
|
219
|
+
# Downstream only sees Email, never raw str
|
|
220
|
+
def send_welcome(email: Email) -> None: ...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
With Pydantic this happens automatically — `EmailStr` is already a parsed type. Once constructed, `.email` is always valid. No re-validation needed.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Sources
|
|
228
|
+
|
|
229
|
+
- Python docs: [dataclasses](https://docs.python.org/3/library/dataclasses.html)
|
|
230
|
+
- Pydantic v2: [docs.pydantic.dev](https://docs.pydantic.dev/latest/)
|
|
231
|
+
- Python docs: [typing — Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol)
|
|
232
|
+
- Python docs: [typing — TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
|
233
|
+
- Alexis King: [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
|