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,268 @@
|
|
|
1
|
+
# One-liner Scripts (PEP 723 + uv)
|
|
2
|
+
|
|
3
|
+
Self-contained Python scripts with declared dependencies, run with no environment setup. The combination eliminates the historical reason to write small tools in Go or Bash.
|
|
4
|
+
|
|
5
|
+
**Rule: EVERY `.py` script — even throwaway — MUST use PEP 723 inline metadata with the usage comment block.** No venv, no requirements.txt, no setup.py. The script IS the environment spec.
|
|
6
|
+
|
|
7
|
+
## The two patterns
|
|
8
|
+
|
|
9
|
+
### Pattern 1: inline `uv run` invocation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv run --with httpx2 --with rich python -c "
|
|
13
|
+
import httpx2
|
|
14
|
+
from rich import print
|
|
15
|
+
print(httpx2.get('https://api.github.com').json())
|
|
16
|
+
"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use for terminal one-shots that you don't want to save. `--with PKG` may be repeated.
|
|
20
|
+
|
|
21
|
+
### Pattern 2: PEP 723 script with shebang (THE CANONICAL PATTERN)
|
|
22
|
+
|
|
23
|
+
A regular `.py` file with metadata in a comment block. uv reads the metadata, materialises a disposable venv (cached), and runs the script.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
#!/usr/bin/env -S uv run --script
|
|
27
|
+
# /// script
|
|
28
|
+
# requires-python = ">=3.13"
|
|
29
|
+
# dependencies = [
|
|
30
|
+
# "httpx2[http2,brotli,zstd]",
|
|
31
|
+
# "rich",
|
|
32
|
+
# ]
|
|
33
|
+
# ///
|
|
34
|
+
|
|
35
|
+
# ─── How to run ───
|
|
36
|
+
# 1. Install uv (if not installed):
|
|
37
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
38
|
+
# 2. Run directly (no venv, no pip install needed):
|
|
39
|
+
# uv run my_script.py
|
|
40
|
+
# 3. Or make executable and run:
|
|
41
|
+
# chmod +x my_script.py && ./my_script.py
|
|
42
|
+
# ──────────────────
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import httpx2
|
|
47
|
+
from rich import print as rprint
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
with httpx2.Client(http2=True, follow_redirects=True) as client:
|
|
52
|
+
resp = client.get("https://api.github.com")
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
rprint(resp.json())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Mandatory elements
|
|
62
|
+
|
|
63
|
+
Every PEP 723 script MUST include these, in order:
|
|
64
|
+
|
|
65
|
+
1. **Shebang**: `#!/usr/bin/env -S uv run --script`
|
|
66
|
+
2. **PEP 723 metadata block**: `# /// script` ... `# ///` with `requires-python` and `dependencies`
|
|
67
|
+
3. **Usage comment block**: How to install uv + how to run the script. Copy the template above verbatim.
|
|
68
|
+
4. **`from __future__ import annotations`**: Always first import.
|
|
69
|
+
5. **`if __name__ == "__main__": main()`**: Entry point guard.
|
|
70
|
+
|
|
71
|
+
### The usage comment block (NON-NEGOTIABLE)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# ─── How to run ───
|
|
75
|
+
# 1. Install uv (if not installed):
|
|
76
|
+
# curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
77
|
+
# 2. Run directly (no venv, no pip install needed):
|
|
78
|
+
# uv run <SCRIPT_NAME>.py [ARGS]
|
|
79
|
+
# 3. Or make executable and run:
|
|
80
|
+
# chmod +x <SCRIPT_NAME>.py && ./<SCRIPT_NAME>.py
|
|
81
|
+
# ──────────────────
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Replace `<SCRIPT_NAME>` with the actual filename. Add argument descriptions if the script takes CLI args. This block goes immediately after the `# ///` closing line, before any imports.
|
|
85
|
+
|
|
86
|
+
**Why mandatory**: Anyone who receives this script — colleague, CI, future you — must know how to run it without reading docs. The comment IS the docs.
|
|
87
|
+
|
|
88
|
+
## Template generator
|
|
89
|
+
|
|
90
|
+
Use `scripts/new-script.py` to scaffold a new PEP 723 script with all boilerplate pre-filled:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Generate to temp directory (default)
|
|
94
|
+
uv run scripts/new-script.py my_tool
|
|
95
|
+
|
|
96
|
+
# Generate to specific path
|
|
97
|
+
uv run scripts/new-script.py my_tool --output ./scripts/my_tool.py
|
|
98
|
+
|
|
99
|
+
# With extra dependencies
|
|
100
|
+
uv run scripts/new-script.py my_tool --deps "polars" "duckdb" "rich"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Common dependency sets
|
|
104
|
+
|
|
105
|
+
| Use case | Dependencies line |
|
|
106
|
+
|---|---|
|
|
107
|
+
| API client | `"httpx2[http2,brotli,zstd]"` |
|
|
108
|
+
| Data processing | `"polars"`, `"duckdb"` |
|
|
109
|
+
| CLI tool | `"typer"`, `"rich"` |
|
|
110
|
+
| Web scraping | `"httpx2[http2,brotli,zstd]"`, `"selectolax"` |
|
|
111
|
+
| File watcher | `"watchfiles"` |
|
|
112
|
+
| JSON pretty | `"rich"` |
|
|
113
|
+
| AI / LLM | `"pydantic-ai"`, `"httpx2[http2,brotli,zstd]"` |
|
|
114
|
+
|
|
115
|
+
## Real-world examples
|
|
116
|
+
|
|
117
|
+
### Fetch + print JSON
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
#!/usr/bin/env -S uv run --script
|
|
121
|
+
# /// script
|
|
122
|
+
# requires-python = ">=3.13"
|
|
123
|
+
# dependencies = [
|
|
124
|
+
# "httpx2[http2,brotli,zstd]",
|
|
125
|
+
# "rich",
|
|
126
|
+
# ]
|
|
127
|
+
# ///
|
|
128
|
+
|
|
129
|
+
# ─── How to run ───
|
|
130
|
+
# 1. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
131
|
+
# 2. Run: uv run fetch_json.py https://api.github.com/repos/pydantic/httpx2
|
|
132
|
+
# ──────────────────
|
|
133
|
+
|
|
134
|
+
from __future__ import annotations
|
|
135
|
+
|
|
136
|
+
import sys
|
|
137
|
+
|
|
138
|
+
import httpx2
|
|
139
|
+
from rich import print as rprint
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> None:
|
|
143
|
+
url = sys.argv[1] if len(sys.argv) > 1 else "https://api.github.com"
|
|
144
|
+
with httpx2.Client(http2=True, follow_redirects=True) as client:
|
|
145
|
+
resp = client.get(url)
|
|
146
|
+
resp.raise_for_status()
|
|
147
|
+
rprint(resp.json())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### CSV → Parquet conversion
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
#!/usr/bin/env -S uv run --script
|
|
158
|
+
# /// script
|
|
159
|
+
# requires-python = ">=3.13"
|
|
160
|
+
# dependencies = [
|
|
161
|
+
# "polars",
|
|
162
|
+
# "typer",
|
|
163
|
+
# "rich",
|
|
164
|
+
# ]
|
|
165
|
+
# ///
|
|
166
|
+
|
|
167
|
+
# ─── How to run ───
|
|
168
|
+
# 1. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
169
|
+
# 2. Run: uv run csv2parquet.py input.csv output.parquet
|
|
170
|
+
# ──────────────────
|
|
171
|
+
|
|
172
|
+
from __future__ import annotations
|
|
173
|
+
|
|
174
|
+
from pathlib import Path
|
|
175
|
+
|
|
176
|
+
import polars as pl
|
|
177
|
+
import typer
|
|
178
|
+
from rich import print as rprint
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(input_path: Path, output_path: Path | None = None) -> None:
|
|
182
|
+
"""Convert CSV to Parquet."""
|
|
183
|
+
out = output_path or input_path.with_suffix(".parquet")
|
|
184
|
+
df = pl.read_csv(input_path)
|
|
185
|
+
df.write_parquet(out)
|
|
186
|
+
rprint(f"[green]✓[/green] {input_path} → {out} ({len(df)} rows)")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
typer.run(main)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Quick benchmark
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
#!/usr/bin/env -S uv run --script
|
|
197
|
+
# /// script
|
|
198
|
+
# requires-python = ">=3.13"
|
|
199
|
+
# dependencies = [
|
|
200
|
+
# "httpx2[http2,brotli,zstd]",
|
|
201
|
+
# "rich",
|
|
202
|
+
# "anyio",
|
|
203
|
+
# ]
|
|
204
|
+
# ///
|
|
205
|
+
|
|
206
|
+
# ─── How to run ───
|
|
207
|
+
# 1. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
208
|
+
# 2. Run: uv run bench.py https://api.example.com/health 50
|
|
209
|
+
# ──────────────────
|
|
210
|
+
|
|
211
|
+
from __future__ import annotations
|
|
212
|
+
|
|
213
|
+
import socket
|
|
214
|
+
import sys
|
|
215
|
+
import time
|
|
216
|
+
|
|
217
|
+
import anyio
|
|
218
|
+
import httpx2
|
|
219
|
+
from rich import print as rprint
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def main() -> None:
|
|
223
|
+
url = sys.argv[1] if len(sys.argv) > 1 else "https://api.github.com"
|
|
224
|
+
n = int(sys.argv[2]) if len(sys.argv) > 2 else 20
|
|
225
|
+
|
|
226
|
+
limits = httpx2.Limits(max_connections=200, max_keepalive_connections=40, keepalive_expiry=30.0)
|
|
227
|
+
timeout = httpx2.Timeout(connect=5.0, read=30.0, write=10.0, pool=10.0)
|
|
228
|
+
transport = httpx2.AsyncHTTPTransport(
|
|
229
|
+
http2=True, retries=3, limits=limits,
|
|
230
|
+
socket_options=[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async with httpx2.AsyncClient(transport=transport, timeout=timeout, follow_redirects=True) as client:
|
|
234
|
+
# warmup
|
|
235
|
+
for _ in range(3):
|
|
236
|
+
await client.get(url)
|
|
237
|
+
|
|
238
|
+
start = time.perf_counter()
|
|
239
|
+
for _ in range(n):
|
|
240
|
+
r = await client.get(url)
|
|
241
|
+
assert r.status_code == 200
|
|
242
|
+
elapsed = time.perf_counter() - start
|
|
243
|
+
|
|
244
|
+
avg_ms = (elapsed / n) * 1000
|
|
245
|
+
rprint(f"[bold]{url}[/bold]: {avg_ms:.1f}ms avg over {n} requests ({elapsed:.2f}s total, {r.http_version})")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
anyio.run(main)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Anti-patterns
|
|
253
|
+
|
|
254
|
+
| ❌ Don't | ✅ Do |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `pip install httpx2 && python script.py` | `uv run script.py` |
|
|
257
|
+
| `requirements.txt` alongside script | PEP 723 inline metadata |
|
|
258
|
+
| `python -m venv .venv && ...` | `uv run --script` handles it |
|
|
259
|
+
| Script without usage comment | Always include the "How to run" block |
|
|
260
|
+
| `import asyncio; asyncio.run(main())` | `import anyio; anyio.run(main)` |
|
|
261
|
+
| Bare `httpx2.AsyncClient()` | Full production defaults (see `references/httpx2-optimization.md`) |
|
|
262
|
+
|
|
263
|
+
## Sources
|
|
264
|
+
|
|
265
|
+
- PEP 723 - Inline script metadata: <https://peps.python.org/pep-0723/>
|
|
266
|
+
- uv `run --script` docs: <https://docs.astral.sh/uv/guides/scripts/>
|
|
267
|
+
- Original article: <https://www.cottongeeks.com/articles/2025-06-24-fun-with-uv-and-pep-723>
|
|
268
|
+
- Simon Willison on one-shot Python tools: <https://simonwillison.net/2024/Dec/19/one-shot-python-tools/>
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# orjson — When to Use, How to Integrate
|
|
2
|
+
|
|
3
|
+
`orjson` is the fastest JSON library on PyPI — written in Rust, 6–11× faster than stdlib `json` on serialization, 1.5–4× faster on deserialization. It also supports types the stdlib refuses to serialize: `datetime`, `date`, `UUID`, `numpy` arrays, `dataclass`, Pydantic models (via a small bridge).
|
|
4
|
+
|
|
5
|
+
This document covers the production patterns. **Not every project needs orjson.** The decision tree is in §1.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Decision tree — should you adopt orjson?
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Are you serializing/deserializing JSON in a hot path?
|
|
13
|
+
├─ NO → stdlib `json` is fine. Stop here.
|
|
14
|
+
└─ YES ↓
|
|
15
|
+
|
|
16
|
+
Is the project FastAPI?
|
|
17
|
+
├─ YES ↓
|
|
18
|
+
│
|
|
19
|
+
│ Is your response body fully described by a Pydantic v2 model?
|
|
20
|
+
│ ├─ YES → Use FastAPI's default JSON response (uses Pydantic's
|
|
21
|
+
│ │ Rust-backed serializer; orjson saves nothing in this path).
|
|
22
|
+
│ │ Adopt orjson only for *non-Pydantic* responses below.
|
|
23
|
+
│ └─ NO → Use `ORJSONResponse` for endpoints that return dicts,
|
|
24
|
+
│ lists, or arbitrary structures.
|
|
25
|
+
│
|
|
26
|
+
└─ NOT FastAPI ↓
|
|
27
|
+
|
|
28
|
+
Are you serializing Pydantic v2 models repeatedly?
|
|
29
|
+
├─ YES → Use `model.model_dump_json()` directly — backed by pydantic-core
|
|
30
|
+
│ (Rust), within ~10% of orjson on the same payload, and respects
|
|
31
|
+
│ every Pydantic feature (computed fields, aliases, validators).
|
|
32
|
+
└─ NO ↓
|
|
33
|
+
|
|
34
|
+
Are you serializing dicts / lists / dataclasses / datetime / UUID?
|
|
35
|
+
├─ YES → orjson is the right answer.
|
|
36
|
+
└─ NO → stdlib `json`.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**The crucial 2026 fact**: with Pydantic v2's `model_dump_json()`, **Pydantic-shaped responses no longer need orjson**. Adopt orjson where you are still going through `dict` / `list` / `dataclass`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Install
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
# pyproject.toml
|
|
47
|
+
dependencies = [
|
|
48
|
+
"orjson>=3.10",
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
orjson wheels are published for every major CPython version and platform (macOS, Linux glibc/musl, Windows, ARM64). No compilation step on install.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Basic usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import orjson
|
|
60
|
+
|
|
61
|
+
# Serialization — returns bytes, not str
|
|
62
|
+
raw: bytes = orjson.dumps({"hello": "world", "ts": datetime.now(UTC)})
|
|
63
|
+
|
|
64
|
+
# Deserialization
|
|
65
|
+
data = orjson.loads(raw)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Two things to internalize:
|
|
69
|
+
|
|
70
|
+
1. **`orjson.dumps` returns `bytes`**, not `str`. Stdlib `json.dumps` returns `str`. This is by design — most JSON destinations (sockets, files in binary mode, HTTP bodies) want bytes anyway, and skipping the encode/decode round trip is part of the speedup.
|
|
71
|
+
2. **No `indent` arg.** orjson supports `OPT_INDENT_2` (and only 2-space indent) via flags. If you need other indentation, use stdlib `json`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 4. The option flags you actually use
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import orjson
|
|
79
|
+
|
|
80
|
+
orjson.dumps(
|
|
81
|
+
payload,
|
|
82
|
+
option=(
|
|
83
|
+
orjson.OPT_NAIVE_UTC # treat naive datetimes as UTC (recommended)
|
|
84
|
+
| orjson.OPT_UTC_Z # render UTC as "...Z" instead of "+00:00"
|
|
85
|
+
| orjson.OPT_SERIALIZE_NUMPY # serialize numpy arrays natively
|
|
86
|
+
| orjson.OPT_SERIALIZE_DATACLASS # serialize @dataclass instances
|
|
87
|
+
| orjson.OPT_NON_STR_KEYS # allow int / UUID / datetime dict keys
|
|
88
|
+
# | orjson.OPT_SORT_KEYS # only when you need deterministic output
|
|
89
|
+
# | orjson.OPT_INDENT_2 # only for human-readable output (slower)
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each flag is opt-in for a reason — orjson defaults to spec-strict JSON.
|
|
95
|
+
|
|
96
|
+
The flag combination above is a sensible "production default" for application code. The `OPT_NAIVE_UTC | OPT_UTC_Z` pair is especially important: it produces RFC 3339 timestamps that every parser on earth accepts.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 5. orjson + FastAPI
|
|
101
|
+
|
|
102
|
+
### 5.1 The legacy pattern: `ORJSONResponse`
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from fastapi import FastAPI
|
|
106
|
+
from fastapi.responses import ORJSONResponse
|
|
107
|
+
|
|
108
|
+
app = FastAPI(default_response_class=ORJSONResponse)
|
|
109
|
+
|
|
110
|
+
@app.get("/items")
|
|
111
|
+
async def get_items() -> dict[str, list[dict[str, int]]]:
|
|
112
|
+
return {"items": [{"id": i, "qty": i * 2} for i in range(1000)]}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`default_response_class=ORJSONResponse` swaps the global JSON encoder for orjson. **This affects only the response body serialization**, not request parsing — for request parsing, FastAPI still uses Pydantic.
|
|
116
|
+
|
|
117
|
+
### 5.2 The 2026 reality — Pydantic v2 vs orjson
|
|
118
|
+
|
|
119
|
+
With FastAPI 0.100+ on Pydantic v2:
|
|
120
|
+
|
|
121
|
+
- If your response is annotated with a Pydantic model, FastAPI calls `model_dump_json()` directly. **orjson is bypassed** even with `default_response_class=ORJSONResponse`, because the Pydantic serializer is already Rust-backed.
|
|
122
|
+
- If your response is a raw `dict` / `list` / Python object, `ORJSONResponse` does kick in and saves real time.
|
|
123
|
+
|
|
124
|
+
The benchmark in `tiangolo/fastapi#11728` (Apr 2024) showed `model_dump_json()` is ~10–15% faster than `ORJSONResponse + model_dump()` for Pydantic-shaped responses. The shape of the data matters; on mixed-shape APIs, keep `ORJSONResponse` as the default and trust Pydantic's path for typed responses.
|
|
125
|
+
|
|
126
|
+
### 5.3 Recommended setup
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from fastapi import FastAPI
|
|
130
|
+
from fastapi.responses import ORJSONResponse
|
|
131
|
+
|
|
132
|
+
app = FastAPI(
|
|
133
|
+
default_response_class=ORJSONResponse, # benefits dict/list returns
|
|
134
|
+
# Pydantic-typed returns automatically use pydantic-core serialization
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Do NOT** wrap Pydantic models manually:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# BAD — defeats Pydantic's optimized path
|
|
142
|
+
@app.get("/users/{id}", response_class=ORJSONResponse)
|
|
143
|
+
async def get_user(id: int) -> ORJSONResponse:
|
|
144
|
+
user = await fetch_user(id)
|
|
145
|
+
return ORJSONResponse(content=user.model_dump()) # extra dict trip
|
|
146
|
+
|
|
147
|
+
# GOOD — let FastAPI serialize the model
|
|
148
|
+
@app.get("/users/{id}")
|
|
149
|
+
async def get_user(id: int) -> User:
|
|
150
|
+
return await fetch_user(id)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 5.4 Streaming responses
|
|
154
|
+
|
|
155
|
+
`ORJSONResponse` does not stream — it buffers the whole response. For SSE, NDJSON, or chunked JSON, use `StreamingResponse` and call `orjson.dumps` per chunk:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from fastapi.responses import StreamingResponse
|
|
159
|
+
import orjson
|
|
160
|
+
|
|
161
|
+
async def ndjson_stream():
|
|
162
|
+
async for row in fetch_rows():
|
|
163
|
+
yield orjson.dumps(row) + b"\n"
|
|
164
|
+
|
|
165
|
+
@app.get("/export")
|
|
166
|
+
async def export():
|
|
167
|
+
return StreamingResponse(ndjson_stream(), media_type="application/x-ndjson")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is where orjson shines — per-chunk serialization in a tight loop, zero buffering.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 6. orjson + Pydantic v2 (no FastAPI)
|
|
175
|
+
|
|
176
|
+
When you have a Pydantic model and want orjson's output for non-FastAPI contexts:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pydantic import BaseModel
|
|
180
|
+
import orjson
|
|
181
|
+
|
|
182
|
+
class User(BaseModel):
|
|
183
|
+
id: int
|
|
184
|
+
email: str
|
|
185
|
+
created: datetime
|
|
186
|
+
|
|
187
|
+
user = User(id=1, email="a@b.com", created=datetime.now(UTC))
|
|
188
|
+
|
|
189
|
+
# Option A — Pydantic's built-in Rust serializer (USE THIS by default)
|
|
190
|
+
raw: bytes = user.model_dump_json().encode()
|
|
191
|
+
# 2026: ~1.2× faster than orjson on the same payload, supports
|
|
192
|
+
# every Pydantic feature (aliases, computed fields, json_schema_extra, etc.)
|
|
193
|
+
|
|
194
|
+
# Option B — orjson bridge for cases Pydantic does not cover
|
|
195
|
+
raw: bytes = orjson.dumps(
|
|
196
|
+
user,
|
|
197
|
+
default=lambda obj: obj.model_dump() if isinstance(obj, BaseModel) else None,
|
|
198
|
+
)
|
|
199
|
+
# Useful when serializing nested non-Pydantic structures that contain
|
|
200
|
+
# BaseModels — e.g. a list of dicts that each may contain a BaseModel.
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
For routine "serialize one Pydantic model to JSON", `model_dump_json()` wins on speed AND feature parity. Reach for orjson only at the *container* level (a dict of mixed types).
|
|
204
|
+
|
|
205
|
+
### Custom `default=` callback — the universal extension point
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import orjson
|
|
209
|
+
from decimal import Decimal
|
|
210
|
+
from pydantic import BaseModel
|
|
211
|
+
|
|
212
|
+
def _default(obj):
|
|
213
|
+
if isinstance(obj, BaseModel):
|
|
214
|
+
return obj.model_dump()
|
|
215
|
+
if isinstance(obj, Decimal):
|
|
216
|
+
return str(obj)
|
|
217
|
+
if isinstance(obj, set):
|
|
218
|
+
return list(obj)
|
|
219
|
+
raise TypeError(f"orjson: cannot serialize {type(obj).__name__}")
|
|
220
|
+
|
|
221
|
+
orjson.dumps(payload, default=_default, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The `default=` callback runs once per unrecognized type, then orjson caches the path. Performance impact on subsequent calls is negligible.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 7. Caching, queues, logging — the prime orjson use cases
|
|
229
|
+
|
|
230
|
+
These are where orjson pays off most clearly because there is no Pydantic in the loop:
|
|
231
|
+
|
|
232
|
+
### Redis cache
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import orjson
|
|
236
|
+
import redis.asyncio as redis
|
|
237
|
+
|
|
238
|
+
r = redis.from_url("redis://localhost")
|
|
239
|
+
|
|
240
|
+
async def set_cache(key: str, value: dict) -> None:
|
|
241
|
+
await r.set(key, orjson.dumps(value), ex=3600)
|
|
242
|
+
|
|
243
|
+
async def get_cache(key: str) -> dict | None:
|
|
244
|
+
raw = await r.get(key)
|
|
245
|
+
return orjson.loads(raw) if raw else None
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`orjson` over stdlib `json` here saves ~5–10× on the serialize step for typical cache payloads. Multiply by request rate.
|
|
249
|
+
|
|
250
|
+
### Task queue payloads (Celery, RQ, dramatiq)
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# Celery custom serializer
|
|
254
|
+
from kombu.serialization import register
|
|
255
|
+
import orjson
|
|
256
|
+
|
|
257
|
+
def _orjson_dumps(obj):
|
|
258
|
+
return orjson.dumps(obj, option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z).decode()
|
|
259
|
+
|
|
260
|
+
def _orjson_loads(s):
|
|
261
|
+
return orjson.loads(s)
|
|
262
|
+
|
|
263
|
+
register("orjson", _orjson_dumps, _orjson_loads,
|
|
264
|
+
content_type="application/x-orjson",
|
|
265
|
+
content_encoding="utf-8")
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Same speedup, applied to every task payload encode/decode.
|
|
269
|
+
|
|
270
|
+
### Structured logging (structlog, custom slog)
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
import structlog
|
|
274
|
+
import orjson
|
|
275
|
+
|
|
276
|
+
structlog.configure(
|
|
277
|
+
processors=[
|
|
278
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
279
|
+
structlog.processors.add_log_level,
|
|
280
|
+
structlog.processors.JSONRenderer(serializer=orjson.dumps),
|
|
281
|
+
],
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
structlog's `JSONRenderer` accepts any callable; orjson is the obvious default. Logging hot paths benefit dramatically — every log line at info level becomes ~5× cheaper to render.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 8. Gotchas
|
|
290
|
+
|
|
291
|
+
### `orjson.dumps` returns bytes, not str
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
# BAD — concatenating bytes and str
|
|
295
|
+
log.info("payload: " + orjson.dumps(data)) # TypeError
|
|
296
|
+
|
|
297
|
+
# GOOD
|
|
298
|
+
log.info("payload: %s", orjson.dumps(data).decode())
|
|
299
|
+
# or
|
|
300
|
+
log.info("payload: %s", orjson.dumps(data)) # let the formatter handle it
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### No `cls=` argument for custom encoders
|
|
304
|
+
|
|
305
|
+
orjson uses `default=` only. If you have a custom `JSONEncoder` subclass from stdlib `json`, port its `default()` method to a `default=` callable.
|
|
306
|
+
|
|
307
|
+
### Subclasses of `dict` / `list` are NOT serialized as their parent
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
class StrictDict(dict): ...
|
|
311
|
+
d = StrictDict({"k": "v"})
|
|
312
|
+
|
|
313
|
+
import json
|
|
314
|
+
json.dumps(d) # OK — stdlib walks subclasses
|
|
315
|
+
orjson.dumps(d) # TypeError — orjson is strict by design
|
|
316
|
+
orjson.dumps(d, option=orjson.OPT_PASSTHROUGH_SUBCLASS) # then route via default=
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Set `OPT_PASSTHROUGH_SUBCLASS` and handle the subclass in `default=`. The design discourages accidental subclass usage that breaks elsewhere.
|
|
320
|
+
|
|
321
|
+
### `int` overflow
|
|
322
|
+
|
|
323
|
+
orjson refuses to encode integers larger than 2⁵³ - 1 by default (the IEEE-754 double-precision safe-integer limit — what JavaScript can round-trip). For larger ints, opt in:
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
orjson.dumps(huge_int, option=orjson.OPT_STRICT_INTEGER) # error
|
|
327
|
+
orjson.dumps(huge_int) # default — int is encoded as JSON number
|
|
328
|
+
# JavaScript clients lose precision past 2^53; consider sending as string
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This is more spec-strict than stdlib `json`, which silently emits ints of any size.
|
|
332
|
+
|
|
333
|
+
### Timezone-naive datetimes
|
|
334
|
+
|
|
335
|
+
By default, orjson treats naive `datetime` as the system local timezone — almost never what you want. **Always set `OPT_NAIVE_UTC`** to treat naive datetimes as UTC, or use timezone-aware datetimes (which is the better long-term habit).
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 9. Benchmark — should I actually adopt this?
|
|
340
|
+
|
|
341
|
+
The numbers below are 2024–2026 averages from `tiangolo/fastapi#11728` and orjson's own benchmark suite, on Python 3.13, modern x86_64:
|
|
342
|
+
|
|
343
|
+
| Payload | stdlib `json` | `orjson` | `model_dump_json()` (Pydantic v2) |
|
|
344
|
+
|---|---|---|---|
|
|
345
|
+
| Small dict (100 fields) | 1.0× | **8×** | n/a |
|
|
346
|
+
| List of 10k dicts | 1.0× | **11×** | n/a |
|
|
347
|
+
| Pydantic model with 20 fields | 1.0× (after `model_dump()`) | 5× (with `default=` bridge) | **6×** |
|
|
348
|
+
| Datetime-heavy payload | 1.0× (after manual ISO conv) | **9×** | 6× |
|
|
349
|
+
| numpy array (1M floats) | impossible without manual conv | **20×** vs json+tolist | n/a |
|
|
350
|
+
|
|
351
|
+
The takeaways:
|
|
352
|
+
|
|
353
|
+
- For raw dict/list/datetime, **orjson is dramatically faster**.
|
|
354
|
+
- For Pydantic models, **`model_dump_json()` is already faster than orjson+bridge**.
|
|
355
|
+
- For numpy, orjson is the only sane choice.
|
|
356
|
+
|
|
357
|
+
In production, the actual measured win on a FastAPI app with mixed payloads is typically 5–15% reduction in p99 latency. Worth the one-line `default_response_class=ORJSONResponse` switch.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 10. When NOT to adopt orjson
|
|
362
|
+
|
|
363
|
+
- The codebase is small, JSON is not a bottleneck, and you have no measured perf concern.
|
|
364
|
+
- You depend on stdlib `json`'s `cls=` arg or its lax tolerance for non-spec input (NaN, Infinity, comments).
|
|
365
|
+
- You need pretty-printed JSON with custom indent — orjson only supports 2-space indent via the flag.
|
|
366
|
+
- You need pure-Python portability (e.g., MicroPython, no-wheel platforms) — orjson is a compiled Rust extension.
|
|
367
|
+
|
|
368
|
+
If the choice is "add a dependency that does 5–10× the speed on serialization for free", the answer is almost always yes. The "almost" is in the bullets above.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Sources
|
|
373
|
+
|
|
374
|
+
- orjson: https://github.com/ijl/orjson
|
|
375
|
+
- Pydantic v2 `model_dump_json`: https://docs.pydantic.dev/latest/concepts/serialization/#modelmodel_dump_json
|
|
376
|
+
- FastAPI `ORJSONResponse`: https://fastapi.tiangolo.com/advanced/custom-response/#use-orjsonresponse
|
|
377
|
+
- "FastAPI + orjson vs Pydantic v2" benchmark: https://github.com/fastapi/fastapi/discussions/11728
|
|
378
|
+
- structlog JSON rendering: https://www.structlog.org/en/stable/api.html#structlog.processors.JSONRenderer
|