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.
Files changed (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. 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/)