@coralai/sps-cli 0.41.2 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Python Async / Concurrency
|
|
2
|
+
|
|
3
|
+
`asyncio` patterns for I/O-bound concurrency. For CPU-bound work, use processes, not async.
|
|
4
|
+
|
|
5
|
+
## When to use what
|
|
6
|
+
|
|
7
|
+
| Workload | Tool | Why |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| I/O-bound (network, files, DB) | `asyncio` | One thread, thousands of connections |
|
|
10
|
+
| CPU-bound (crunching, crypto) | `multiprocessing` / `concurrent.futures.ProcessPoolExecutor` | GIL blocks threads |
|
|
11
|
+
| Blocking library you can't async-ify | `asyncio.to_thread()` | Offload to the default thread pool |
|
|
12
|
+
| Parallelizing blocking syscalls | `concurrent.futures.ThreadPoolExecutor` | Simple, no asyncio required |
|
|
13
|
+
|
|
14
|
+
`asyncio` without I/O is pointless — a `async def` that only does CPU work gives you no concurrency.
|
|
15
|
+
|
|
16
|
+
## `async def` / `await` basics
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
async def fetch(url: str) -> str:
|
|
23
|
+
async with httpx.AsyncClient() as client:
|
|
24
|
+
r = await client.get(url)
|
|
25
|
+
return r.text
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
html = await fetch("https://example.com")
|
|
29
|
+
print(len(html))
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Rule: never call `asyncio.run()` from inside already-running async code. It's the entry point, not a utility.
|
|
35
|
+
|
|
36
|
+
## Concurrency — run coroutines in parallel
|
|
37
|
+
|
|
38
|
+
### `asyncio.gather` (legacy, still common)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
async def fetch_all(urls: list[str]) -> list[str]:
|
|
42
|
+
return await asyncio.gather(*(fetch(u) for u in urls))
|
|
43
|
+
|
|
44
|
+
# With return_exceptions — failures don't cancel siblings
|
|
45
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
46
|
+
for r in results:
|
|
47
|
+
if isinstance(r, Exception):
|
|
48
|
+
log.warning("one failed: %s", r)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `asyncio.TaskGroup` (Python 3.11+) — preferred
|
|
52
|
+
|
|
53
|
+
Structured concurrency: if any task raises, siblings are cancelled and errors are aggregated into an `ExceptionGroup`.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
async def fetch_all(urls: list[str]) -> list[str]:
|
|
57
|
+
async with asyncio.TaskGroup() as tg:
|
|
58
|
+
tasks = [tg.create_task(fetch(u)) for u in urls]
|
|
59
|
+
return [t.result() for t in tasks]
|
|
60
|
+
|
|
61
|
+
# Error handling
|
|
62
|
+
try:
|
|
63
|
+
await fetch_all(urls)
|
|
64
|
+
except* httpx.ConnectError as eg:
|
|
65
|
+
log.warning("network failures: %d", len(eg.exceptions))
|
|
66
|
+
except* httpx.HTTPStatusError as eg:
|
|
67
|
+
log.error("HTTP errors: %s", [e.response.status_code for e in eg.exceptions])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Prefer `TaskGroup` over `gather` in new code — cancellation is correct by default.
|
|
71
|
+
|
|
72
|
+
## Timeouts
|
|
73
|
+
|
|
74
|
+
### Python 3.11+: `asyncio.timeout`
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
async def with_deadline():
|
|
78
|
+
async with asyncio.timeout(5.0):
|
|
79
|
+
return await slow_operation()
|
|
80
|
+
|
|
81
|
+
# Nested / reschedulable
|
|
82
|
+
async with asyncio.timeout(None) as cm:
|
|
83
|
+
cm.reschedule(asyncio.get_running_loop().time() + 10)
|
|
84
|
+
...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Older: `asyncio.wait_for`
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
result = await asyncio.wait_for(slow_operation(), timeout=5.0)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Cancellation
|
|
94
|
+
|
|
95
|
+
Cancellation is a `CancelledError` injected at the next `await`. Rules:
|
|
96
|
+
|
|
97
|
+
- **Never swallow `CancelledError`** except to run cleanup; always re-raise.
|
|
98
|
+
- Cleanup in `finally` must itself be fast and cancellation-safe.
|
|
99
|
+
- `asyncio.shield()` protects a critical section from outer cancellation.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
async def worker():
|
|
103
|
+
try:
|
|
104
|
+
while True:
|
|
105
|
+
await do_work()
|
|
106
|
+
except asyncio.CancelledError:
|
|
107
|
+
await cleanup() # fast, idempotent
|
|
108
|
+
raise # REQUIRED — don't swallow
|
|
109
|
+
|
|
110
|
+
async def critical_write(path, data):
|
|
111
|
+
# Outer cancel won't interrupt the write
|
|
112
|
+
await asyncio.shield(write_file(path, data))
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Offloading blocking code
|
|
116
|
+
|
|
117
|
+
Never call blocking code from an async function — it stalls the event loop for everyone.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Wrong: blocks the event loop
|
|
121
|
+
async def handler(req):
|
|
122
|
+
data = requests.get(req.url).text # ❌ blocking library in async code
|
|
123
|
+
return data
|
|
124
|
+
|
|
125
|
+
# Right: offload to thread pool
|
|
126
|
+
async def handler(req):
|
|
127
|
+
data = await asyncio.to_thread(requests.get, req.url)
|
|
128
|
+
return data.text
|
|
129
|
+
|
|
130
|
+
# Or use an async-native library
|
|
131
|
+
async def handler(req):
|
|
132
|
+
async with httpx.AsyncClient() as client:
|
|
133
|
+
r = await client.get(req.url)
|
|
134
|
+
return r.text
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`time.sleep()` in async code is always a bug — use `await asyncio.sleep()`.
|
|
138
|
+
|
|
139
|
+
## Async iteration & generators
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Async iterator
|
|
143
|
+
async def stream_lines(url: str) -> AsyncIterator[str]:
|
|
144
|
+
async with httpx.AsyncClient() as client:
|
|
145
|
+
async with client.stream("GET", url) as resp:
|
|
146
|
+
async for line in resp.aiter_lines():
|
|
147
|
+
yield line
|
|
148
|
+
|
|
149
|
+
async for line in stream_lines(url):
|
|
150
|
+
process(line)
|
|
151
|
+
|
|
152
|
+
# Async comprehension
|
|
153
|
+
urls = [u async for u in stream_urls() if u.startswith("https://")]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Async context managers
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from contextlib import asynccontextmanager
|
|
160
|
+
|
|
161
|
+
@asynccontextmanager
|
|
162
|
+
async def db_transaction(conn):
|
|
163
|
+
tx = await conn.begin()
|
|
164
|
+
try:
|
|
165
|
+
yield tx
|
|
166
|
+
except Exception:
|
|
167
|
+
await tx.rollback()
|
|
168
|
+
raise
|
|
169
|
+
else:
|
|
170
|
+
await tx.commit()
|
|
171
|
+
|
|
172
|
+
async with db_transaction(conn) as tx:
|
|
173
|
+
await tx.execute(...)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Backpressure with semaphores
|
|
177
|
+
|
|
178
|
+
Limit in-flight concurrency when the downstream can't take unlimited load.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
async def fetch_bounded(urls: list[str], limit: int = 10) -> list[str]:
|
|
182
|
+
sem = asyncio.Semaphore(limit)
|
|
183
|
+
|
|
184
|
+
async def one(u):
|
|
185
|
+
async with sem:
|
|
186
|
+
return await fetch(u)
|
|
187
|
+
|
|
188
|
+
async with asyncio.TaskGroup() as tg:
|
|
189
|
+
tasks = [tg.create_task(one(u)) for u in urls]
|
|
190
|
+
return [t.result() for t in tasks]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Anti-patterns
|
|
194
|
+
|
|
195
|
+
| Anti-pattern | Fix |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `asyncio.run()` inside async code | Pass awaitables up; `asyncio.run()` is the entry point only |
|
|
198
|
+
| `time.sleep()` in async | `await asyncio.sleep()` |
|
|
199
|
+
| `requests` / blocking HTTP in async | Use `httpx.AsyncClient` or `asyncio.to_thread` |
|
|
200
|
+
| Swallowing `CancelledError` | Always re-raise after cleanup |
|
|
201
|
+
| Fire-and-forget `asyncio.create_task(x)` without keeping a reference | Reference dropped → task can be garbage-collected mid-flight |
|
|
202
|
+
| Mixing threads and asyncio naively | Use `asyncio.to_thread` / `loop.run_in_executor`, not raw `threading` |
|
|
203
|
+
| Using async just because "it's modern" | Async has real overhead; pure CPU or simple scripts don't need it |
|
|
204
|
+
|
|
205
|
+
## Running background tasks correctly
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# Wrong: task may be GC'd before it runs
|
|
209
|
+
asyncio.create_task(background())
|
|
210
|
+
|
|
211
|
+
# Right: keep a reference
|
|
212
|
+
_background_tasks: set[asyncio.Task] = set()
|
|
213
|
+
|
|
214
|
+
def schedule(coro):
|
|
215
|
+
t = asyncio.create_task(coro)
|
|
216
|
+
_background_tasks.add(t)
|
|
217
|
+
t.add_done_callback(_background_tasks.discard)
|
|
218
|
+
```
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Python Error Handling
|
|
2
|
+
|
|
3
|
+
Exception design and handling patterns.
|
|
4
|
+
|
|
5
|
+
## Specific exceptions only
|
|
6
|
+
|
|
7
|
+
Never `except:` (bare). Never `except Exception:` except at the top of a daemon/server loop.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# Good: specific exceptions, each with its own response
|
|
11
|
+
def load_config(path: str) -> Config:
|
|
12
|
+
try:
|
|
13
|
+
with open(path) as f:
|
|
14
|
+
return Config.from_json(f.read())
|
|
15
|
+
except FileNotFoundError as e:
|
|
16
|
+
raise ConfigError(f"Config not found: {path}") from e
|
|
17
|
+
except json.JSONDecodeError as e:
|
|
18
|
+
raise ConfigError(f"Invalid JSON in {path}: {e}") from e
|
|
19
|
+
except PermissionError as e:
|
|
20
|
+
raise ConfigError(f"No read permission: {path}") from e
|
|
21
|
+
|
|
22
|
+
# Bad: silent catch-all
|
|
23
|
+
def load_config(path: str) -> Config:
|
|
24
|
+
try:
|
|
25
|
+
with open(path) as f:
|
|
26
|
+
return Config.from_json(f.read())
|
|
27
|
+
except:
|
|
28
|
+
return None # caller has no idea what went wrong
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Exception chaining (`from`)
|
|
32
|
+
|
|
33
|
+
Always preserve the original traceback when re-raising.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
def process(data: str) -> Result:
|
|
37
|
+
try:
|
|
38
|
+
parsed = json.loads(data)
|
|
39
|
+
except json.JSONDecodeError as e:
|
|
40
|
+
# `from e` attaches the original exception
|
|
41
|
+
raise ValueError(f"Bad data: {data!r}") from e
|
|
42
|
+
|
|
43
|
+
# Output traceback shows both:
|
|
44
|
+
# ValueError: Bad data: '...'
|
|
45
|
+
# The above exception was the direct cause of the following:
|
|
46
|
+
# json.JSONDecodeError: ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use `raise ... from None` to **hide** the chain (rare — only when the chain is noise).
|
|
50
|
+
|
|
51
|
+
## Custom exception hierarchy
|
|
52
|
+
|
|
53
|
+
One base class per module/service. Subclasses for specific failures.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# errors.py
|
|
57
|
+
class AppError(Exception):
|
|
58
|
+
"""Base exception for this application."""
|
|
59
|
+
|
|
60
|
+
class ValidationError(AppError):
|
|
61
|
+
"""Input validation failed."""
|
|
62
|
+
|
|
63
|
+
class NotFoundError(AppError):
|
|
64
|
+
"""Resource not found."""
|
|
65
|
+
|
|
66
|
+
class AuthError(AppError):
|
|
67
|
+
"""Authentication or authorization failed."""
|
|
68
|
+
|
|
69
|
+
class ExternalServiceError(AppError):
|
|
70
|
+
"""Upstream dependency failed."""
|
|
71
|
+
|
|
72
|
+
# Usage
|
|
73
|
+
def get_user(user_id: str) -> User:
|
|
74
|
+
user = db.find_user(user_id)
|
|
75
|
+
if not user:
|
|
76
|
+
raise NotFoundError(f"User {user_id}")
|
|
77
|
+
return user
|
|
78
|
+
|
|
79
|
+
# Callers catch at the right level
|
|
80
|
+
try:
|
|
81
|
+
user = get_user(uid)
|
|
82
|
+
except NotFoundError:
|
|
83
|
+
return 404
|
|
84
|
+
except AppError:
|
|
85
|
+
return 500 # any other app error
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception("unexpected")
|
|
88
|
+
return 500
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
| Rule | Why |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Catch only exceptions you can handle | If you can't recover, let it propagate |
|
|
96
|
+
| Never catch `BaseException` | That includes `KeyboardInterrupt` and `SystemExit` |
|
|
97
|
+
| Log before re-raising | Otherwise the log line says "unexpected" when you actually expected it |
|
|
98
|
+
| Specific exception types | Callers can pattern-match; `Exception` forces logs to be the only debugging aid |
|
|
99
|
+
| Use `try/except/else` for 2-phase operations | `else` runs only if no exception — clearer than nesting |
|
|
100
|
+
|
|
101
|
+
## `try/except/else/finally`
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
def fetch(url: str) -> Response:
|
|
105
|
+
try:
|
|
106
|
+
response = http.get(url)
|
|
107
|
+
except TimeoutError:
|
|
108
|
+
return fallback_response()
|
|
109
|
+
except HTTPError as e:
|
|
110
|
+
logger.error("http error: %s", e)
|
|
111
|
+
raise
|
|
112
|
+
else:
|
|
113
|
+
# runs only on success — cleaner than putting this in try
|
|
114
|
+
cache.set(url, response)
|
|
115
|
+
return response
|
|
116
|
+
finally:
|
|
117
|
+
# always runs
|
|
118
|
+
release_connection()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Context manager for error handling
|
|
122
|
+
|
|
123
|
+
For repeated try/except patterns, wrap in a context manager.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from contextlib import contextmanager
|
|
127
|
+
|
|
128
|
+
@contextmanager
|
|
129
|
+
def as_domain_error(target_type: type[Exception], msg: str):
|
|
130
|
+
"""Re-raise any exception as target_type(msg)."""
|
|
131
|
+
try:
|
|
132
|
+
yield
|
|
133
|
+
except Exception as e:
|
|
134
|
+
raise target_type(msg) from e
|
|
135
|
+
|
|
136
|
+
# Usage
|
|
137
|
+
with as_domain_error(ConfigError, "Failed to load config"):
|
|
138
|
+
with open('config.json') as f:
|
|
139
|
+
return json.load(f)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Validation vs exceptions
|
|
143
|
+
|
|
144
|
+
For user-input validation, prefer returning a result object over raising.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from dataclasses import dataclass
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class ValidationResult:
|
|
151
|
+
valid: bool
|
|
152
|
+
errors: list[str]
|
|
153
|
+
|
|
154
|
+
def validate_user(data: dict) -> ValidationResult:
|
|
155
|
+
errors = []
|
|
156
|
+
if not data.get('email'):
|
|
157
|
+
errors.append('email required')
|
|
158
|
+
if 'age' in data and not 0 <= data['age'] <= 150:
|
|
159
|
+
errors.append('age must be 0-150')
|
|
160
|
+
return ValidationResult(valid=not errors, errors=errors)
|
|
161
|
+
|
|
162
|
+
# Caller
|
|
163
|
+
result = validate_user(input)
|
|
164
|
+
if not result.valid:
|
|
165
|
+
return {"errors": result.errors}, 400
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Raise exceptions for **exceptional** conditions. Validation failure is **expected**.
|
|
169
|
+
|
|
170
|
+
## Exception groups (Python 3.11+)
|
|
171
|
+
|
|
172
|
+
For reporting multiple failures at once — concurrent tasks, batch operations, validation aggregates. Use `ExceptionGroup` and `except*`.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
def import_batch(records: list[dict]) -> None:
|
|
176
|
+
errors = []
|
|
177
|
+
for r in records:
|
|
178
|
+
try:
|
|
179
|
+
import_one(r)
|
|
180
|
+
except (ValidationError, DBError) as e:
|
|
181
|
+
errors.append(e)
|
|
182
|
+
if errors:
|
|
183
|
+
raise ExceptionGroup("batch import failed", errors)
|
|
184
|
+
|
|
185
|
+
# Caller: except* matches by type across the group
|
|
186
|
+
try:
|
|
187
|
+
import_batch(data)
|
|
188
|
+
except* ValidationError as eg:
|
|
189
|
+
for e in eg.exceptions:
|
|
190
|
+
log.warning("invalid: %s", e)
|
|
191
|
+
except* DBError as eg:
|
|
192
|
+
for e in eg.exceptions:
|
|
193
|
+
log.error("db failure: %s", e)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`asyncio.TaskGroup` (3.11+) raises `ExceptionGroup` natively when multiple child tasks fail.
|
|
197
|
+
|
|
198
|
+
## Logging exceptions
|
|
199
|
+
|
|
200
|
+
Use `logger.exception()` inside an except block — it auto-includes the traceback.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import logging
|
|
204
|
+
logger = logging.getLogger(__name__)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
risky_operation()
|
|
208
|
+
except ExternalServiceError:
|
|
209
|
+
# logger.exception is equivalent to logger.error(exc_info=True)
|
|
210
|
+
logger.exception("External service failed")
|
|
211
|
+
# Re-raise or handle
|
|
212
|
+
raise
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Never** use `print(e)` in production — print goes to stdout, not your log pipeline, and loses the traceback.
|
|
216
|
+
|
|
217
|
+
## Common mistakes
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# ❌ Mutating `except` clause without `from`
|
|
221
|
+
try: ...
|
|
222
|
+
except ValueError:
|
|
223
|
+
raise TypeError("wrong type") # original traceback lost
|
|
224
|
+
|
|
225
|
+
# ✅
|
|
226
|
+
try: ...
|
|
227
|
+
except ValueError as e:
|
|
228
|
+
raise TypeError("wrong type") from e
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ❌ Catching to "convert to None"
|
|
232
|
+
try:
|
|
233
|
+
return lookup(key)
|
|
234
|
+
except KeyError:
|
|
235
|
+
return None # if this is your pattern, use dict.get(key) — simpler
|
|
236
|
+
|
|
237
|
+
# ✅
|
|
238
|
+
return mapping.get(key)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ❌ Overcatching hides bugs
|
|
242
|
+
try:
|
|
243
|
+
data = get_user(uid)
|
|
244
|
+
data.name = new_name # if this raises, you misclassify it as "user not found"
|
|
245
|
+
except Exception:
|
|
246
|
+
return 404
|
|
247
|
+
|
|
248
|
+
# ✅
|
|
249
|
+
try:
|
|
250
|
+
data = get_user(uid)
|
|
251
|
+
except NotFoundError:
|
|
252
|
+
return 404
|
|
253
|
+
data.name = new_name # let real bugs propagate
|
|
254
|
+
```
|