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,360 @@
|
|
|
1
|
+
# httpx2 — Production Defaults
|
|
2
|
+
|
|
3
|
+
> **Source**: [pydantic/httpx2](https://github.com/pydantic/httpx2) — next-generation HTTP client for Python 3, continuation of HTTPX under Pydantic stewardship.
|
|
4
|
+
>
|
|
5
|
+
> **Rule**: Every network request MUST use `httpx2`. **ALL optimizations below are ON by default** — HTTP/2, brotli+zstd, tuned connection pool, fine-grained timeouts, transport retries, TCP_NODELAY. This is the baseline, not a stretch goal. A bare `httpx2.AsyncClient()` is a bug.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Installation — all extras, always
|
|
10
|
+
|
|
11
|
+
```toml
|
|
12
|
+
# pyproject.toml
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx2[http2,brotli,zstd]",
|
|
15
|
+
]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Extra | What it enables | Why it's mandatory |
|
|
19
|
+
|-------|----------------|--------------------|
|
|
20
|
+
| `http2` | HTTP/2 multiplexing via `h2` | Single TCP connection handles concurrent requests; eliminates head-of-line blocking |
|
|
21
|
+
| `brotli` | Brotli content decoding (`br`) | ~20% smaller payloads than gzip for text/JSON |
|
|
22
|
+
| `zstd` | Zstandard content decoding | Faster decompression than brotli at similar ratios; stdlib in Python ≥ 3.14 |
|
|
23
|
+
| `socks` | SOCKS5 proxy support via `socksio` | Install only if you route through SOCKS proxies |
|
|
24
|
+
|
|
25
|
+
All three core extras (`http2,brotli,zstd`) are non-negotiable. Omitting any is leaving performance on the table.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. The canonical defaults — ALL ON
|
|
30
|
+
|
|
31
|
+
These are not "optimizations to consider". These are **the correct defaults** that every httpx2 client must use.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import socket
|
|
35
|
+
import httpx2
|
|
36
|
+
|
|
37
|
+
# ── These are the STANDARD values. Use them verbatim. ──
|
|
38
|
+
|
|
39
|
+
LIMITS = httpx2.Limits(
|
|
40
|
+
max_connections=200, # library default 100 is too conservative
|
|
41
|
+
max_keepalive_connections=40, # library default 20 wastes reconnects
|
|
42
|
+
keepalive_expiry=30.0, # library default 5s kills warm connections too fast
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
TIMEOUT = httpx2.Timeout(
|
|
46
|
+
connect=5.0, # TCP + TLS handshake budget
|
|
47
|
+
read=30.0, # time to receive a response chunk
|
|
48
|
+
write=10.0, # time to send a request chunk
|
|
49
|
+
pool=10.0, # time to acquire a connection from pool
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
SOCKET_OPTIONS: list[tuple[int, int, int]] = [
|
|
53
|
+
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1), # disable Nagle — no 40ms delay
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Why each knob is set this way
|
|
58
|
+
|
|
59
|
+
| Setting | Library default | Our default | Why |
|
|
60
|
+
|---------|----------------|-------------|-----|
|
|
61
|
+
| `http2` | `False` | **`True`** | HTTP/2 multiplexing is strictly superior for any modern API |
|
|
62
|
+
| `max_connections` | `100` | `200` | Headroom for fan-out; prevents pool exhaustion under load |
|
|
63
|
+
| `max_keepalive_connections` | `20` | `40` | Keeps warm connections alive; fewer TLS handshakes |
|
|
64
|
+
| `keepalive_expiry` | `5.0s` | `30.0s` | 5s is too aggressive — kills connections between burst requests |
|
|
65
|
+
| `Timeout(5.0)` uniform | `5.0` all | Split | Uniform 5s is too tight for reads, too loose for connects |
|
|
66
|
+
| `read` timeout | `5.0` | `30.0` | Slow APIs and streaming need breathing room |
|
|
67
|
+
| `pool` timeout | `5.0` | `10.0` | Explicit — hitting this means `max_connections` needs raising |
|
|
68
|
+
| `TCP_NODELAY` | off | **on** | Eliminates Nagle's 40ms coalescing delay for small payloads |
|
|
69
|
+
| `retries` | `0` | `3` | Retries on `ConnectError`/`ConnectTimeout` only — safe and resilient |
|
|
70
|
+
| `follow_redirects` | `False` | **`True`** | Most APIs redirect; failing on 3xx is wrong default behavior |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. Factory functions — the ONE correct way to create clients
|
|
75
|
+
|
|
76
|
+
Copy this into your project. This is the canonical pattern.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
"""httpx2 client factory. Always use create_client() / create_async_client()."""
|
|
80
|
+
|
|
81
|
+
from __future__ import annotations
|
|
82
|
+
|
|
83
|
+
import socket
|
|
84
|
+
import typing
|
|
85
|
+
|
|
86
|
+
import httpx2
|
|
87
|
+
|
|
88
|
+
_LIMITS = httpx2.Limits(
|
|
89
|
+
max_connections=200,
|
|
90
|
+
max_keepalive_connections=40,
|
|
91
|
+
keepalive_expiry=30.0,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
_TIMEOUT = httpx2.Timeout(
|
|
95
|
+
connect=5.0,
|
|
96
|
+
read=30.0,
|
|
97
|
+
write=10.0,
|
|
98
|
+
pool=10.0,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
_SOCKET_OPTIONS: list[tuple[int, int, int]] = [
|
|
102
|
+
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_async_client(
|
|
107
|
+
*,
|
|
108
|
+
base_url: str = "",
|
|
109
|
+
http2: bool = True,
|
|
110
|
+
retries: int = 3,
|
|
111
|
+
limits: httpx2.Limits = _LIMITS,
|
|
112
|
+
timeout: httpx2.Timeout = _TIMEOUT,
|
|
113
|
+
headers: dict[str, str] | None = None,
|
|
114
|
+
event_hooks: dict[str, list[typing.Callable[..., typing.Any]]] | None = None,
|
|
115
|
+
**kwargs: typing.Any,
|
|
116
|
+
) -> httpx2.AsyncClient:
|
|
117
|
+
transport = httpx2.AsyncHTTPTransport(
|
|
118
|
+
http2=http2,
|
|
119
|
+
retries=retries,
|
|
120
|
+
limits=limits,
|
|
121
|
+
socket_options=_SOCKET_OPTIONS,
|
|
122
|
+
)
|
|
123
|
+
return httpx2.AsyncClient(
|
|
124
|
+
transport=transport,
|
|
125
|
+
timeout=timeout,
|
|
126
|
+
base_url=base_url,
|
|
127
|
+
headers=headers or {},
|
|
128
|
+
event_hooks=event_hooks or {},
|
|
129
|
+
follow_redirects=True,
|
|
130
|
+
**kwargs,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create_client(
|
|
135
|
+
*,
|
|
136
|
+
base_url: str = "",
|
|
137
|
+
http2: bool = True,
|
|
138
|
+
retries: int = 3,
|
|
139
|
+
limits: httpx2.Limits = _LIMITS,
|
|
140
|
+
timeout: httpx2.Timeout = _TIMEOUT,
|
|
141
|
+
headers: dict[str, str] | None = None,
|
|
142
|
+
event_hooks: dict[str, list[typing.Callable[..., typing.Any]]] | None = None,
|
|
143
|
+
**kwargs: typing.Any,
|
|
144
|
+
) -> httpx2.Client:
|
|
145
|
+
transport = httpx2.HTTPTransport(
|
|
146
|
+
http2=http2,
|
|
147
|
+
retries=retries,
|
|
148
|
+
limits=limits,
|
|
149
|
+
socket_options=_SOCKET_OPTIONS,
|
|
150
|
+
)
|
|
151
|
+
return httpx2.Client(
|
|
152
|
+
transport=transport,
|
|
153
|
+
timeout=timeout,
|
|
154
|
+
base_url=base_url,
|
|
155
|
+
headers=headers or {},
|
|
156
|
+
event_hooks=event_hooks or {},
|
|
157
|
+
follow_redirects=True,
|
|
158
|
+
**kwargs,
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Usage:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Async — the common case
|
|
166
|
+
async with create_async_client(base_url="https://api.example.com") as client:
|
|
167
|
+
r = await client.get("/users")
|
|
168
|
+
|
|
169
|
+
# Sync
|
|
170
|
+
with create_client() as client:
|
|
171
|
+
r = client.get("https://api.example.com/health")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**If you are NOT using this factory pattern, you are doing it wrong.** A bare `httpx2.AsyncClient()` leaves HTTP/2 off, retries off, TCP_NODELAY off, keepalive too short, and timeouts too uniform.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. Special case overrides
|
|
179
|
+
|
|
180
|
+
The factory defaults cover 95% of use cases. Override only when you have a specific reason:
|
|
181
|
+
|
|
182
|
+
| Scenario | Override |
|
|
183
|
+
|----------|----------|
|
|
184
|
+
| LLM streaming endpoints | `timeout=httpx2.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)` — no read timeout on streaming |
|
|
185
|
+
| Single-host API with low concurrency | `limits=httpx2.Limits(max_connections=50, max_keepalive_connections=20, keepalive_expiry=60.0)` |
|
|
186
|
+
| Ephemeral short-lived requests | `keepalive_expiry=5.0` — don't hold connections |
|
|
187
|
+
| Unix domain sockets | `httpx2.AsyncHTTPTransport(uds="/path/to/socket", ...)` |
|
|
188
|
+
| mTLS / client certs | Pass `verify=ssl_ctx` with `ctx.load_cert_chain(certfile=...)` |
|
|
189
|
+
| SOCKS proxy | `httpx2[socks]`, `proxy="socks5://..."` |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 5. Event hooks — always wire observability
|
|
194
|
+
|
|
195
|
+
This is not optional. Every production client should log requests.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
import time
|
|
199
|
+
import logging
|
|
200
|
+
|
|
201
|
+
logger = logging.getLogger(__name__)
|
|
202
|
+
|
|
203
|
+
async def log_request(request: httpx2.Request) -> None:
|
|
204
|
+
request.extensions["request_start"] = time.perf_counter()
|
|
205
|
+
|
|
206
|
+
async def log_response(response: httpx2.Response) -> None:
|
|
207
|
+
start = response.request.extensions.get("request_start", 0)
|
|
208
|
+
elapsed = time.perf_counter() - start
|
|
209
|
+
logger.info(
|
|
210
|
+
"HTTP %s %s → %d (%.3fs, %s)",
|
|
211
|
+
response.request.method,
|
|
212
|
+
response.request.url,
|
|
213
|
+
response.status_code,
|
|
214
|
+
elapsed,
|
|
215
|
+
response.http_version,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Sync versions for Client
|
|
219
|
+
def log_request_sync(request: httpx2.Request) -> None:
|
|
220
|
+
request.extensions["request_start"] = time.perf_counter()
|
|
221
|
+
|
|
222
|
+
def log_response_sync(response: httpx2.Response) -> None:
|
|
223
|
+
start = response.request.extensions.get("request_start", 0)
|
|
224
|
+
elapsed = time.perf_counter() - start
|
|
225
|
+
logger.info(
|
|
226
|
+
"HTTP %s %s → %d (%.3fs, %s)",
|
|
227
|
+
response.request.method,
|
|
228
|
+
response.request.url,
|
|
229
|
+
response.status_code,
|
|
230
|
+
elapsed,
|
|
231
|
+
response.http_version,
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For auto `raise_for_status()`:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
async def raise_on_error(response: httpx2.Response) -> None:
|
|
239
|
+
response.raise_for_status()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 6. Verification script — confirm your setup is fully optimized
|
|
245
|
+
|
|
246
|
+
Run this against your target endpoint to **verify** (not decide) that all optimizations are active:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
"""Verify httpx2 is fully optimized against a target endpoint."""
|
|
250
|
+
|
|
251
|
+
from __future__ import annotations
|
|
252
|
+
|
|
253
|
+
import socket
|
|
254
|
+
import time
|
|
255
|
+
|
|
256
|
+
import anyio
|
|
257
|
+
import httpx2
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
TARGET_URL = "https://api.example.com/health"
|
|
261
|
+
ITERATIONS = 30
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def bench(label: str, client: httpx2.AsyncClient, url: str, n: int) -> float:
|
|
265
|
+
for _ in range(3): # warmup
|
|
266
|
+
await client.get(url)
|
|
267
|
+
start = time.perf_counter()
|
|
268
|
+
for _ in range(n):
|
|
269
|
+
r = await client.get(url)
|
|
270
|
+
assert r.status_code == 200
|
|
271
|
+
elapsed = time.perf_counter() - start
|
|
272
|
+
avg_ms = (elapsed / n) * 1000
|
|
273
|
+
print(f" {label}: {avg_ms:.1f}ms avg ({n} reqs in {elapsed:.2f}s)")
|
|
274
|
+
return avg_ms
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def main() -> None:
|
|
278
|
+
results: dict[str, float] = {}
|
|
279
|
+
|
|
280
|
+
# BAD: bare defaults (this is what we're proving is worse)
|
|
281
|
+
async with httpx2.AsyncClient() as c:
|
|
282
|
+
results["BAD-bare-defaults"] = await bench("BAD-bare-defaults", c, TARGET_URL, ITERATIONS)
|
|
283
|
+
|
|
284
|
+
# GOOD: full production defaults (this is what we always use)
|
|
285
|
+
limits = httpx2.Limits(max_connections=200, max_keepalive_connections=40, keepalive_expiry=30.0)
|
|
286
|
+
timeout = httpx2.Timeout(connect=5.0, read=30.0, write=10.0, pool=10.0)
|
|
287
|
+
transport = httpx2.AsyncHTTPTransport(
|
|
288
|
+
http2=True, retries=3, limits=limits,
|
|
289
|
+
socket_options=[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)],
|
|
290
|
+
)
|
|
291
|
+
async with httpx2.AsyncClient(transport=transport, timeout=timeout, follow_redirects=True) as c:
|
|
292
|
+
results["GOOD-full-production"] = await bench("GOOD-full-production", c, TARGET_URL, ITERATIONS)
|
|
293
|
+
|
|
294
|
+
print("\n--- Proof ---")
|
|
295
|
+
baseline = results["BAD-bare-defaults"]
|
|
296
|
+
for label, avg in results.items():
|
|
297
|
+
delta = ((avg - baseline) / baseline) * 100
|
|
298
|
+
print(f" {label}: {avg:.1f}ms ({delta:+.1f}% vs bare)")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
anyio.run(main)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 7. Quick reference — all knobs
|
|
308
|
+
|
|
309
|
+
### `httpx2.AsyncClient` / `httpx2.Client`
|
|
310
|
+
|
|
311
|
+
| Parameter | Type | Library Default | **Our Default** |
|
|
312
|
+
|-----------|------|-----------------|-----------------|
|
|
313
|
+
| `http1` | `bool` | `True` | `True` |
|
|
314
|
+
| `http2` | `bool` | `False` | **`True`** |
|
|
315
|
+
| `verify` | `ssl.SSLContext \| str \| bool` | `True` | `True` |
|
|
316
|
+
| `cert` | `CertTypes \| None` | `None` | `None` |
|
|
317
|
+
| `proxy` | `str \| Proxy \| None` | `None` | `None` |
|
|
318
|
+
| `mounts` | `dict[str, Transport]` | `None` | `None` |
|
|
319
|
+
| `timeout` | `Timeout \| float \| None` | `Timeout(5.0)` | **Split: 5/30/10/10** |
|
|
320
|
+
| `limits` | `Limits` | `Limits(100, 20, 5.0)` | **`Limits(200, 40, 30.0)`** |
|
|
321
|
+
| `follow_redirects` | `bool` | `False` | **`True`** |
|
|
322
|
+
| `max_redirects` | `int` | `20` | `20` |
|
|
323
|
+
| `event_hooks` | `dict` | `{}` | **Wire logging** |
|
|
324
|
+
| `base_url` | `str` | `""` | Set for single-API clients |
|
|
325
|
+
| `trust_env` | `bool` | `True` | `True` |
|
|
326
|
+
| `default_encoding` | `str \| Callable` | `"utf-8"` | `"utf-8"` |
|
|
327
|
+
|
|
328
|
+
### `httpx2.AsyncHTTPTransport` / `httpx2.HTTPTransport`
|
|
329
|
+
|
|
330
|
+
| Parameter | Type | Library Default | **Our Default** |
|
|
331
|
+
|-----------|------|-----------------|-----------------|
|
|
332
|
+
| `http1` | `bool` | `True` | `True` |
|
|
333
|
+
| `http2` | `bool` | `False` | **`True`** |
|
|
334
|
+
| `retries` | `int` | `0` | **`3`** |
|
|
335
|
+
| `limits` | `Limits` | `Limits(100, 20, 5.0)` | **`Limits(200, 40, 30.0)`** |
|
|
336
|
+
| `uds` | `str \| None` | `None` | `None` |
|
|
337
|
+
| `local_address` | `str \| None` | `None` | `None` |
|
|
338
|
+
| `socket_options` | `Iterable[SOCKET_OPTION]` | `None` | **`[TCP_NODELAY]`** |
|
|
339
|
+
| `proxy` | `str \| Proxy \| None` | `None` | `None` |
|
|
340
|
+
|
|
341
|
+
### `httpx2.Timeout`
|
|
342
|
+
|
|
343
|
+
| Parameter | Library Default | **Our Default** |
|
|
344
|
+
|-----------|-----------------|-----------------|
|
|
345
|
+
| `connect` | `5.0` | `5.0` |
|
|
346
|
+
| `read` | `5.0` | **`30.0`** |
|
|
347
|
+
| `write` | `5.0` | **`10.0`** |
|
|
348
|
+
| `pool` | `5.0` | **`10.0`** |
|
|
349
|
+
|
|
350
|
+
### `httpx2.Limits`
|
|
351
|
+
|
|
352
|
+
| Parameter | Library Default | **Our Default** |
|
|
353
|
+
|-----------|-----------------|-----------------|
|
|
354
|
+
| `max_connections` | `100` | **`200`** |
|
|
355
|
+
| `max_keepalive_connections` | `20` | **`40`** |
|
|
356
|
+
| `keepalive_expiry` | `5.0` | **`30.0`** |
|
|
357
|
+
|
|
358
|
+
### Async backend (httpcore2)
|
|
359
|
+
|
|
360
|
+
httpcore2 uses `anyio` by default (works with both asyncio and trio). No extra config needed if you're already on the anyio stack. For trio, install `httpcore2[trio]`.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Library Defaults — Decision Tree
|
|
2
|
+
|
|
3
|
+
For each domain, the canonical 2026 choice, why, and the canonical usage snippet. The skill enforces these unless the project's `pyproject.toml` explicitly says otherwise.
|
|
4
|
+
|
|
5
|
+
## CLI — typer
|
|
6
|
+
|
|
7
|
+
`typer` builds a CLI from type-annotated function signatures. argparse needs 5x the code; click ignores type annotations; fire is magic that breaks at scale.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import typer
|
|
11
|
+
from rich import print as rprint
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def greet(name: str, count: int = 1, shout: bool = False) -> None:
|
|
17
|
+
"""Print a greeting `count` times."""
|
|
18
|
+
message = f"Hello, {name}!" if not shout else f"HELLO, {name.upper()}!"
|
|
19
|
+
for _ in range(count):
|
|
20
|
+
rprint(message)
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
app()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For a single-function script, `typer.run(main)` skips the `Typer()` boilerplate. Subcommands use `@app.command()`.
|
|
27
|
+
|
|
28
|
+
## Terminal output — rich
|
|
29
|
+
|
|
30
|
+
`rich` produces tables, progress bars, syntax highlighting, traceback rendering. Use it for any structured output. Plain `print` is acceptable for non-interactive log lines (and even those are usually better via `rich.console.Console(stderr=True).log(...)`).
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.table import Table
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
table = Table(title="Users")
|
|
39
|
+
table.add_column("ID", style="cyan")
|
|
40
|
+
table.add_column("Name", style="magenta")
|
|
41
|
+
table.add_row("1", "Alice")
|
|
42
|
+
console.print(table)
|
|
43
|
+
|
|
44
|
+
# Rich tracebacks (call once at process start)
|
|
45
|
+
from rich.traceback import install
|
|
46
|
+
install(show_locals=True)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## HTTP client — [httpx2](https://github.com/pydantic/httpx2)
|
|
50
|
+
|
|
51
|
+
Next-generation HTTP client under Pydantic stewardship. Sync and async in one library, HTTP/2 native, brotli + zstd content decoding, real type stubs. Replaces `requests` (sync only), `aiohttp` (async only), and the original `httpx`.
|
|
52
|
+
|
|
53
|
+
**Install**: `httpx2[http2,brotli,zstd]` — always include all three extras, no exceptions.
|
|
54
|
+
|
|
55
|
+
**A bare `httpx2.AsyncClient()` / `httpx2.Client()` is a bug.** Always use the factory pattern from `references/httpx2-optimization.md` with ALL optimizations enabled by default:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import socket
|
|
59
|
+
import httpx2
|
|
60
|
+
|
|
61
|
+
# ── Production defaults — ALL ON, always. ──
|
|
62
|
+
_LIMITS = httpx2.Limits(max_connections=200, max_keepalive_connections=40, keepalive_expiry=30.0)
|
|
63
|
+
_TIMEOUT = httpx2.Timeout(connect=5.0, read=30.0, write=10.0, pool=10.0)
|
|
64
|
+
_SOCKET_OPTS: list[tuple[int, int, int]] = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
|
|
65
|
+
|
|
66
|
+
# Async (the common case)
|
|
67
|
+
transport = httpx2.AsyncHTTPTransport(http2=True, retries=3, limits=_LIMITS, socket_options=_SOCKET_OPTS)
|
|
68
|
+
async with httpx2.AsyncClient(transport=transport, timeout=_TIMEOUT, follow_redirects=True) as client:
|
|
69
|
+
response = await client.get("https://api.example.com/users")
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
users = response.json()
|
|
72
|
+
|
|
73
|
+
# Sync
|
|
74
|
+
transport = httpx2.HTTPTransport(http2=True, retries=3, limits=_LIMITS, socket_options=_SOCKET_OPTS)
|
|
75
|
+
with httpx2.Client(transport=transport, timeout=_TIMEOUT, follow_redirects=True) as client:
|
|
76
|
+
response = client.get("https://api.example.com/users")
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
users = response.json()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
See `references/httpx2-optimization.md` for the full factory functions (`create_client()` / `create_async_client()`), event hooks, and the rationale behind every setting. **Load that reference whenever you write ANY network code.**
|
|
82
|
+
|
|
83
|
+
## JSON — stdlib `json` (default) or `orjson` (hot paths)
|
|
84
|
+
|
|
85
|
+
Stdlib `json` is fine for cold paths and configs. **Reach for `orjson` when JSON is in the hot path** — cache layers, queue payloads, streaming responses, structured logs, FastAPI endpoints returning raw `dict` / `list`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import orjson
|
|
89
|
+
|
|
90
|
+
# orjson.dumps returns bytes, not str
|
|
91
|
+
raw: bytes = orjson.dumps(
|
|
92
|
+
payload,
|
|
93
|
+
option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z | orjson.OPT_SERIALIZE_DATACLASS,
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Critical 2026 fact**: with Pydantic v2, `model.model_dump_json()` is backed by pydantic-core (Rust) and is faster than `orjson + default=` bridge for Pydantic-shaped responses. **Use `model_dump_json()` for Pydantic; orjson for everything else.**
|
|
98
|
+
|
|
99
|
+
For FastAPI: `app = FastAPI(default_response_class=ORJSONResponse)`. Pydantic-typed responses bypass it (and that's correct — Pydantic's path is faster). Raw `dict`/`list` returns go through orjson.
|
|
100
|
+
|
|
101
|
+
See `references/orjson-stack.md` for the full decision tree, option flag reference, FastAPI integration, Redis/queue/logging patterns, and the `model_dump_json()` vs orjson benchmark.
|
|
102
|
+
|
|
103
|
+
## Validation — pydantic v2
|
|
104
|
+
|
|
105
|
+
Pydantic v2's core is in Rust (~10x faster than v1). It is the de-facto boundary validator. Use it for:
|
|
106
|
+
|
|
107
|
+
- HTTP request/response models (FastAPI uses pydantic natively)
|
|
108
|
+
- Config files (env vars via `pydantic-settings`)
|
|
109
|
+
- Anything entering the program from outside
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pydantic import BaseModel, Field, EmailStr, field_validator
|
|
113
|
+
|
|
114
|
+
class User(BaseModel):
|
|
115
|
+
id: int = Field(ge=1)
|
|
116
|
+
email: EmailStr
|
|
117
|
+
name: str = Field(min_length=1, max_length=100)
|
|
118
|
+
age: int | None = Field(default=None, ge=0, le=150)
|
|
119
|
+
|
|
120
|
+
@field_validator("name")
|
|
121
|
+
@classmethod
|
|
122
|
+
def name_no_digits(cls, v: str) -> str:
|
|
123
|
+
if any(c.isdigit() for c in v):
|
|
124
|
+
raise ValueError("name cannot contain digits")
|
|
125
|
+
return v
|
|
126
|
+
|
|
127
|
+
# Inside the program, use the validated instance with confidence
|
|
128
|
+
user = User.model_validate({"id": 1, "email": "a@b.com", "name": "Alice"})
|
|
129
|
+
print(user.model_dump_json(indent=2))
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`@dataclass` is fine for purely internal records (no validation needed). For anything crossing a process boundary, use Pydantic.
|
|
133
|
+
|
|
134
|
+
## Async — anyio
|
|
135
|
+
|
|
136
|
+
Full reference: [async-anyio.md](async-anyio.md). The summary:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import anyio
|
|
140
|
+
|
|
141
|
+
async def fetch(url: str) -> str:
|
|
142
|
+
await anyio.sleep(0.1)
|
|
143
|
+
return url
|
|
144
|
+
|
|
145
|
+
async def main() -> None:
|
|
146
|
+
async with anyio.create_task_group() as tg:
|
|
147
|
+
for url in ["a", "b", "c"]:
|
|
148
|
+
tg.start_soon(fetch, url)
|
|
149
|
+
|
|
150
|
+
anyio.run(main)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Never `import asyncio` directly. The third-party libraries you call are free to use asyncio internally.
|
|
154
|
+
|
|
155
|
+
## Web framework — fastapi
|
|
156
|
+
|
|
157
|
+
Type-hint-driven HTTP framework. Pydantic models become OpenAPI schemas automatically.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from fastapi import FastAPI
|
|
161
|
+
from pydantic import BaseModel
|
|
162
|
+
|
|
163
|
+
app = FastAPI()
|
|
164
|
+
|
|
165
|
+
class CreateUser(BaseModel):
|
|
166
|
+
name: str
|
|
167
|
+
email: str
|
|
168
|
+
|
|
169
|
+
class User(BaseModel):
|
|
170
|
+
id: int
|
|
171
|
+
name: str
|
|
172
|
+
email: str
|
|
173
|
+
|
|
174
|
+
@app.post("/users", response_model=User)
|
|
175
|
+
async def create_user(payload: CreateUser) -> User:
|
|
176
|
+
return User(id=1, **payload.model_dump())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Full stack with database: [fastapi-stack.md](fastapi-stack.md).
|
|
180
|
+
|
|
181
|
+
## ORM — sqlalchemy 2.x async
|
|
182
|
+
|
|
183
|
+
SQLAlchemy 2.x finally has a real async API. Use the modern declarative `MappedAsDataclass` style with type annotations.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from sqlalchemy import String
|
|
187
|
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
|
188
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, MappedAsDataclass
|
|
189
|
+
|
|
190
|
+
class Base(MappedAsDataclass, DeclarativeBase):
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
class User(Base):
|
|
194
|
+
__tablename__ = "users"
|
|
195
|
+
id: Mapped[int] = mapped_column(primary_key=True, init=False)
|
|
196
|
+
name: Mapped[str] = mapped_column(String(100))
|
|
197
|
+
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
198
|
+
|
|
199
|
+
engine = create_async_engine("postgresql+asyncpg://localhost/myapp")
|
|
200
|
+
SessionFactory = async_sessionmaker(engine, expire_on_commit=False)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Full pattern with FastAPI integration: [fastapi-stack.md](fastapi-stack.md).
|
|
204
|
+
|
|
205
|
+
## Database — postgres + asyncpg
|
|
206
|
+
|
|
207
|
+
For new applications, default to Postgres. SQLite for tests is fine; SQLite for production is not.
|
|
208
|
+
|
|
209
|
+
asyncpg is the fastest Python Postgres driver, native to SQLAlchemy 2.x async, native to FastAPI's lifespan model. URL: `postgresql+asyncpg://user:pass@host:5432/db`.
|
|
210
|
+
|
|
211
|
+
For migrations, use Alembic with `[alembic.context]` configured to use the async engine. Single-step:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
uv add alembic
|
|
215
|
+
uv run alembic init -t async migrations
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## TUI — textual
|
|
219
|
+
|
|
220
|
+
Textual builds rich, mouse-aware, mobile-style TUIs on the rich rendering engine. See [textual-tui.md](textual-tui.md).
|
|
221
|
+
|
|
222
|
+
## AI agents — pydantic-ai
|
|
223
|
+
|
|
224
|
+
The agent framework from the Pydantic team. Type-strict, structured outputs are first-class, model-agnostic. See [pydantic-ai.md](pydantic-ai.md).
|
|
225
|
+
|
|
226
|
+
## DataFrames — polars + numpy
|
|
227
|
+
|
|
228
|
+
Polars is 10-50x faster than pandas, has a real type system, and supports lazy evaluation. Numpy stays in the toolbox for arrays. See [data-processing.md](data-processing.md).
|
|
229
|
+
|
|
230
|
+
## OLAP / SQL — duckdb
|
|
231
|
+
|
|
232
|
+
DuckDB is the SQL engine for analytical workloads. Query CSV/Parquet/JSON files directly without loading into memory; perform joins and aggregations 3-4x faster than Polars; zero-copy interchange with Polars via Arrow. See [data-processing.md](data-processing.md).
|
|
233
|
+
|
|
234
|
+
## Tests — pytest
|
|
235
|
+
|
|
236
|
+
Plain `unittest` is fine for stdlib; everything else uses pytest. Conventions:
|
|
237
|
+
|
|
238
|
+
- File names `test_*.py`, function names `test_*`.
|
|
239
|
+
- Fixtures via `@pytest.fixture`. Async fixtures are anyio-aware (`@pytest.fixture` on an async function works under `pytest-anyio` which is bundled with anyio).
|
|
240
|
+
- Parametrise with `@pytest.mark.parametrize`.
|
|
241
|
+
- Mark async tests with `@pytest.mark.anyio` (provided by anyio's pytest plugin).
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
import pytest
|
|
245
|
+
import anyio
|
|
246
|
+
|
|
247
|
+
@pytest.fixture
|
|
248
|
+
def sample_user() -> dict[str, str]:
|
|
249
|
+
return {"name": "Alice", "email": "a@b.com"}
|
|
250
|
+
|
|
251
|
+
@pytest.mark.parametrize("count,expected", [(1, "Hello"), (2, "Hello, Hello")])
|
|
252
|
+
def test_greet(count: int, expected: str) -> None:
|
|
253
|
+
result = ", ".join(["Hello"] * count)
|
|
254
|
+
assert result == expected
|
|
255
|
+
|
|
256
|
+
@pytest.mark.anyio
|
|
257
|
+
async def test_async_fetch() -> None:
|
|
258
|
+
await anyio.sleep(0)
|
|
259
|
+
assert True
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`pyproject.toml`:
|
|
263
|
+
|
|
264
|
+
```toml
|
|
265
|
+
[tool.pytest.ini_options]
|
|
266
|
+
minversion = "8.0"
|
|
267
|
+
testpaths = ["tests"]
|
|
268
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Settings / config — pydantic-settings
|
|
272
|
+
|
|
273
|
+
Loads env vars and `.env` files into a Pydantic model. Replaces ad-hoc `os.environ.get(...)` everywhere.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from pydantic import Field
|
|
277
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
278
|
+
|
|
279
|
+
class Settings(BaseSettings):
|
|
280
|
+
model_config = SettingsConfigDict(env_file=".env", env_prefix="MYAPP_")
|
|
281
|
+
|
|
282
|
+
database_url: str
|
|
283
|
+
api_key: str = Field(min_length=1)
|
|
284
|
+
debug: bool = False
|
|
285
|
+
|
|
286
|
+
settings = Settings() # loads at import time; raises if any required var is missing
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Logging — stdlib logging + rich handler
|
|
290
|
+
|
|
291
|
+
Stdlib `logging` is fine; it gets a face-lift from `rich.logging.RichHandler`.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
import logging
|
|
295
|
+
from rich.logging import RichHandler
|
|
296
|
+
|
|
297
|
+
logging.basicConfig(
|
|
298
|
+
level=logging.INFO,
|
|
299
|
+
format="%(message)s",
|
|
300
|
+
datefmt="[%X]",
|
|
301
|
+
handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
|
|
302
|
+
)
|
|
303
|
+
log = logging.getLogger(__name__)
|
|
304
|
+
log.info("ready")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For structured logging in production, swap to `structlog` (separate dep). Don't roll your own.
|