pyronova 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. pyronova-2.0.0/.claude/commands/test.md +10 -0
  2. pyronova-2.0.0/.claude/projects/-Users-hucao-projects-pyre/memory/MEMORY.md +1 -0
  3. pyronova-2.0.0/.claude/projects/-Users-hucao-projects-pyre/memory/feedback_y_hash_fix.md +16 -0
  4. pyronova-2.0.0/.github/workflows/ci.yml +108 -0
  5. pyronova-2.0.0/.github/workflows/release.yml +87 -0
  6. pyronova-2.0.0/.gitignore +12 -0
  7. pyronova-2.0.0/CHANGELOG.md +369 -0
  8. pyronova-2.0.0/CLAUDE.md +138 -0
  9. pyronova-2.0.0/Cargo.lock +2806 -0
  10. pyronova-2.0.0/Cargo.toml +77 -0
  11. pyronova-2.0.0/LICENSE +199 -0
  12. pyronova-2.0.0/PKG-INFO +804 -0
  13. pyronova-2.0.0/README.md +777 -0
  14. pyronova-2.0.0/ROADMAP.md +421 -0
  15. pyronova-2.0.0/arena_submission/Dockerfile +45 -0
  16. pyronova-2.0.0/arena_submission/README.md +55 -0
  17. pyronova-2.0.0/arena_submission/app.py +235 -0
  18. pyronova-2.0.0/arena_submission/launcher.py +108 -0
  19. pyronova-2.0.0/arena_submission/meta.json +23 -0
  20. pyronova-2.0.0/benchmarks/BENCH_PLAN.en.md +154 -0
  21. pyronova-2.0.0/benchmarks/BENCH_PLAN.md +154 -0
  22. pyronova-2.0.0/benchmarks/baseline.json +9 -0
  23. pyronova-2.0.0/benchmarks/bench.py +36 -0
  24. pyronova-2.0.0/benchmarks/bench_compression.py +85 -0
  25. pyronova-2.0.0/benchmarks/bench_gil.py +46 -0
  26. pyronova-2.0.0/benchmarks/bench_plaintext.py +65 -0
  27. pyronova-2.0.0/benchmarks/bench_subinterp.py +28 -0
  28. pyronova-2.0.0/benchmarks/benchmark-1-reason.en.md +96 -0
  29. pyronova-2.0.0/benchmarks/benchmark-1-reason.md +96 -0
  30. pyronova-2.0.0/benchmarks/benchmark-10-final-verdict.en.md +87 -0
  31. pyronova-2.0.0/benchmarks/benchmark-10-final-verdict.md +87 -0
  32. pyronova-2.0.0/benchmarks/benchmark-11-vs-fastapi.md +68 -0
  33. pyronova-2.0.0/benchmarks/benchmark-12-stability.md +155 -0
  34. pyronova-2.0.0/benchmarks/benchmark-13-logging.en.md +104 -0
  35. pyronova-2.0.0/benchmarks/benchmark-13-logging.md +104 -0
  36. pyronova-2.0.0/benchmarks/benchmark-14-channel-dispatch.en.md +131 -0
  37. pyronova-2.0.0/benchmarks/benchmark-14-channel-dispatch.md +131 -0
  38. pyronova-2.0.0/benchmarks/benchmark-14-linux.en.md +245 -0
  39. pyronova-2.0.0/benchmarks/benchmark-14-linux.md +245 -0
  40. pyronova-2.0.0/benchmarks/benchmark-15-vs-actix-compression.md +299 -0
  41. pyronova-2.0.0/benchmarks/benchmark-16-v1.6-regression-audit.md +169 -0
  42. pyronova-2.0.0/benchmarks/benchmark-17-arena-head-to-head.md +231 -0
  43. pyronova-2.0.0/benchmarks/benchmark-2-results.en.md +59 -0
  44. pyronova-2.0.0/benchmarks/benchmark-2-results.md +59 -0
  45. pyronova-2.0.0/benchmarks/benchmark-3-full-stack.en.md +41 -0
  46. pyronova-2.0.0/benchmarks/benchmark-3-full-stack.md +41 -0
  47. pyronova-2.0.0/benchmarks/benchmark-4-features.en.md +30 -0
  48. pyronova-2.0.0/benchmarks/benchmark-4-features.md +30 -0
  49. pyronova-2.0.0/benchmarks/benchmark-5-refactor.en.md +46 -0
  50. pyronova-2.0.0/benchmarks/benchmark-5-refactor.md +46 -0
  51. pyronova-2.0.0/benchmarks/benchmark-6-full-suite.en.md +84 -0
  52. pyronova-2.0.0/benchmarks/benchmark-6-full-suite.md +84 -0
  53. pyronova-2.0.0/benchmarks/benchmark-7-shared-state.en.md +106 -0
  54. pyronova-2.0.0/benchmarks/benchmark-7-shared-state.md +108 -0
  55. pyronova-2.0.0/benchmarks/benchmark-8-sse-regression.en.md +77 -0
  56. pyronova-2.0.0/benchmarks/benchmark-8-sse-regression.md +78 -0
  57. pyronova-2.0.0/benchmarks/benchmark-9-gil-memory.en.md +105 -0
  58. pyronova-2.0.0/benchmarks/benchmark-9-gil-memory.md +99 -0
  59. pyronova-2.0.0/benchmarks/fastapi_bench_app.py +53 -0
  60. pyronova-2.0.0/benchmarks/mem_bench.sh +80 -0
  61. pyronova-2.0.0/benchmarks/pyronova_bench_app.py +56 -0
  62. pyronova-2.0.0/benchmarks/results/2026-03-24_113354/raw.json +1306 -0
  63. pyronova-2.0.0/benchmarks/results/2026-03-24_113354/summary.md +43 -0
  64. pyronova-2.0.0/benchmarks/results/2026-03-24_130737/raw.json +1306 -0
  65. pyronova-2.0.0/benchmarks/results/2026-03-24_130737/summary.md +43 -0
  66. pyronova-2.0.0/benchmarks/results/2026-03-24_140616/raw.json +1306 -0
  67. pyronova-2.0.0/benchmarks/results/2026-03-24_140616/summary.md +43 -0
  68. pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/latency.png +0 -0
  69. pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/pyre_vs_robyn.png +0 -0
  70. pyronova-2.0.0/benchmarks/results/2026-03-24_144558/charts/throughput.png +0 -0
  71. pyronova-2.0.0/benchmarks/results/2026-03-24_144558/raw.json +1306 -0
  72. pyronova-2.0.0/benchmarks/results/2026-03-24_144558/summary.md +43 -0
  73. pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/latency.png +0 -0
  74. pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/pyre_vs_robyn.png +0 -0
  75. pyronova-2.0.0/benchmarks/results/2026-03-24_161056/charts/throughput.png +0 -0
  76. pyronova-2.0.0/benchmarks/results/2026-03-24_161056/raw.json +1630 -0
  77. pyronova-2.0.0/benchmarks/results/2026-03-24_161056/summary.md +43 -0
  78. pyronova-2.0.0/benchmarks/results/2026-03-24_171330/raw.json +1630 -0
  79. pyronova-2.0.0/benchmarks/results/2026-03-24_171330/summary.md +43 -0
  80. pyronova-2.0.0/benchmarks/robyn_app.py +57 -0
  81. pyronova-2.0.0/benchmarks/run_bench.sh +66 -0
  82. pyronova-2.0.0/benchmarks/run_comparison.sh +99 -0
  83. pyronova-2.0.0/benchmarks/run_compression_bench.sh +96 -0
  84. pyronova-2.0.0/benchmarks/run_full_bench.sh +281 -0
  85. pyronova-2.0.0/benchmarks/run_logging_bench.sh +176 -0
  86. pyronova-2.0.0/benchmarks/run_vs_robyn.sh +208 -0
  87. pyronova-2.0.0/benchmarks/suite/charts.py +186 -0
  88. pyronova-2.0.0/benchmarks/suite/payloads/large.json +1 -0
  89. pyronova-2.0.0/benchmarks/suite/payloads/medium.json +1 -0
  90. pyronova-2.0.0/benchmarks/suite/payloads/small.json +1 -0
  91. pyronova-2.0.0/benchmarks/suite/post.lua +12 -0
  92. pyronova-2.0.0/benchmarks/suite/runner.py +384 -0
  93. pyronova-2.0.0/benchmarks/suite/servers/pyronova_gil.py +79 -0
  94. pyronova-2.0.0/benchmarks/suite/servers/pyronova_hybrid.py +77 -0
  95. pyronova-2.0.0/benchmarks/suite/servers/pyronova_subinterp.py +72 -0
  96. pyronova-2.0.0/benchmarks/suite/servers/robyn_server.py +77 -0
  97. pyronova-2.0.0/docs/advisor-triage-2026-04-19.md +52 -0
  98. pyronova-2.0.0/docs/developer-experience.md +293 -0
  99. pyronova-2.0.0/docs/dual-engine-design.md +81 -0
  100. pyronova-2.0.0/docs/features.md +170 -0
  101. pyronova-2.0.0/docs/gc-optimization-guide.md +108 -0
  102. pyronova-2.0.0/docs/gil-monitor-design.md +301 -0
  103. pyronova-2.0.0/docs/http-arena-preflight.md +138 -0
  104. pyronova-2.0.0/docs/logging-design.en.md +201 -0
  105. pyronova-2.0.0/docs/logging-design.md +201 -0
  106. pyronova-2.0.0/docs/memory-leak-investigation-2026-04-19.md +575 -0
  107. pyronova-2.0.0/docs/phase-7.2-async-bridge.md +165 -0
  108. pyronova-2.0.0/docs/positioning-and-roadmap.md +167 -0
  109. pyronova-2.0.0/docs/release-pipeline.md +140 -0
  110. pyronova-2.0.0/docs/rpc-engine-design.md +113 -0
  111. pyronova-2.0.0/docs/subinterp-c-extension-compat.md +140 -0
  112. pyronova-2.0.0/docs/subinterp-safe-ecosystem.md +224 -0
  113. pyronova-2.0.0/docs/why-not-multiprocess.md +145 -0
  114. pyronova-2.0.0/docs/zero-copy-design.md +82 -0
  115. pyronova-2.0.0/examples/README.md +39 -0
  116. pyronova-2.0.0/examples/ai_agent_server.py +179 -0
  117. pyronova-2.0.0/examples/fullstack_api.py +325 -0
  118. pyronova-2.0.0/examples/hello.py +77 -0
  119. pyronova-2.0.0/examples/hello_subinterp.py +33 -0
  120. pyronova-2.0.0/examples/sse_demo.py +50 -0
  121. pyronova-2.0.0/examples/trading_api.py +274 -0
  122. pyronova-2.0.0/examples/websocket_demo.py +41 -0
  123. pyronova-2.0.0/justfile +265 -0
  124. pyronova-2.0.0/pyproject.toml +49 -0
  125. pyronova-2.0.0/python/pyronova/__init__.py +34 -0
  126. pyronova-2.0.0/python/pyronova/__main__.py +5 -0
  127. pyronova-2.0.0/python/pyronova/_async_engine.py +146 -0
  128. pyronova-2.0.0/python/pyronova/_bootstrap.py +384 -0
  129. pyronova-2.0.0/python/pyronova/app.py +950 -0
  130. pyronova-2.0.0/python/pyronova/cli.py +161 -0
  131. pyronova-2.0.0/python/pyronova/config.py +66 -0
  132. pyronova-2.0.0/python/pyronova/context.py +87 -0
  133. pyronova-2.0.0/python/pyronova/cookies.py +125 -0
  134. pyronova-2.0.0/python/pyronova/crud.py +223 -0
  135. pyronova-2.0.0/python/pyronova/db.py +50 -0
  136. pyronova-2.0.0/python/pyronova/engine.pyi +78 -0
  137. pyronova-2.0.0/python/pyronova/health.py +97 -0
  138. pyronova-2.0.0/python/pyronova/mcp.py +313 -0
  139. pyronova-2.0.0/python/pyronova/observability.py +171 -0
  140. pyronova-2.0.0/python/pyronova/rpc.py +150 -0
  141. pyronova-2.0.0/python/pyronova/testing.py +286 -0
  142. pyronova-2.0.0/python/pyronova/uploads.py +118 -0
  143. pyronova-2.0.0/src/app.rs +921 -0
  144. pyronova-2.0.0/src/body_stream.rs +177 -0
  145. pyronova-2.0.0/src/compression.rs +463 -0
  146. pyronova-2.0.0/src/db.rs +664 -0
  147. pyronova-2.0.0/src/handlers.rs +954 -0
  148. pyronova-2.0.0/src/interp.rs +2449 -0
  149. pyronova-2.0.0/src/json.rs +348 -0
  150. pyronova-2.0.0/src/leak_detect.rs +195 -0
  151. pyronova-2.0.0/src/lib.rs +78 -0
  152. pyronova-2.0.0/src/logging.rs +175 -0
  153. pyronova-2.0.0/src/monitor.rs +213 -0
  154. pyronova-2.0.0/src/pyronova_request_type.rs +423 -0
  155. pyronova-2.0.0/src/response.rs +336 -0
  156. pyronova-2.0.0/src/router.rs +152 -0
  157. pyronova-2.0.0/src/state.rs +137 -0
  158. pyronova-2.0.0/src/static_fs.rs +351 -0
  159. pyronova-2.0.0/src/stream.rs +119 -0
  160. pyronova-2.0.0/src/tls.rs +139 -0
  161. pyronova-2.0.0/src/types.rs +353 -0
  162. pyronova-2.0.0/src/websocket.rs +362 -0
  163. pyronova-2.0.0/tests/conftest.py +138 -0
  164. pyronova-2.0.0/tests/e2e/README.md +21 -0
  165. pyronova-2.0.0/tests/e2e/ws_binary_client.py +35 -0
  166. pyronova-2.0.0/tests/e2e/ws_binary_server.py +23 -0
  167. pyronova-2.0.0/tests/test_admission_control.py +77 -0
  168. pyronova-2.0.0/tests/test_async.py +29 -0
  169. pyronova-2.0.0/tests/test_async_bridge.py +28 -0
  170. pyronova-2.0.0/tests/test_async_isolation.py +96 -0
  171. pyronova-2.0.0/tests/test_async_shutdown.py +44 -0
  172. pyronova-2.0.0/tests/test_async_timeout.py +137 -0
  173. pyronova-2.0.0/tests/test_c_extensions.py +42 -0
  174. pyronova-2.0.0/tests/test_capi_hygiene.py +357 -0
  175. pyronova-2.0.0/tests/test_cli.py +157 -0
  176. pyronova-2.0.0/tests/test_client_ip.py +106 -0
  177. pyronova-2.0.0/tests/test_compression.py +186 -0
  178. pyronova-2.0.0/tests/test_config_context.py +128 -0
  179. pyronova-2.0.0/tests/test_cookies_e2e.py +52 -0
  180. pyronova-2.0.0/tests/test_cookies_unit.py +76 -0
  181. pyronova-2.0.0/tests/test_cors_404.py +101 -0
  182. pyronova-2.0.0/tests/test_cors_e2e.py +69 -0
  183. pyronova-2.0.0/tests/test_crud.py +192 -0
  184. pyronova-2.0.0/tests/test_db_pg.py +332 -0
  185. pyronova-2.0.0/tests/test_env_var_worker.py +46 -0
  186. pyronova-2.0.0/tests/test_fast_response.py +120 -0
  187. pyronova-2.0.0/tests/test_ffi_panic_safety.py +52 -0
  188. pyronova-2.0.0/tests/test_fixes.py +34 -0
  189. pyronova-2.0.0/tests/test_health.py +165 -0
  190. pyronova-2.0.0/tests/test_hybrid.py +42 -0
  191. pyronova-2.0.0/tests/test_json_serialization.py +649 -0
  192. pyronova-2.0.0/tests/test_lifecycle.py +163 -0
  193. pyronova-2.0.0/tests/test_logging.py +233 -0
  194. pyronova-2.0.0/tests/test_mcp.py +133 -0
  195. pyronova-2.0.0/tests/test_middleware_hooks.py +200 -0
  196. pyronova-2.0.0/tests/test_observability.py +245 -0
  197. pyronova-2.0.0/tests/test_passive_gil_metrics.py +83 -0
  198. pyronova-2.0.0/tests/test_pyerr_no_stderr.py +126 -0
  199. pyronova-2.0.0/tests/test_request_fields.py +260 -0
  200. pyronova-2.0.0/tests/test_routing_e2e.py +101 -0
  201. pyronova-2.0.0/tests/test_rpc.py +104 -0
  202. pyronova-2.0.0/tests/test_shared_state.py +95 -0
  203. pyronova-2.0.0/tests/test_static_files.py +153 -0
  204. pyronova-2.0.0/tests/test_stream_backpressure.py +69 -0
  205. pyronova-2.0.0/tests/test_stream_lifecycle.py +128 -0
  206. pyronova-2.0.0/tests/test_subinterp_features.py +278 -0
  207. pyronova-2.0.0/tests/test_subinterp_hooks.py +32 -0
  208. pyronova-2.0.0/tests/test_subinterp_memory_regression.py +509 -0
  209. pyronova-2.0.0/tests/test_subinterp_timeout.py +112 -0
  210. pyronova-2.0.0/tests/test_testclient.py +145 -0
  211. pyronova-2.0.0/tests/test_testclient_v2.py +238 -0
  212. pyronova-2.0.0/tests/test_tls.py +156 -0
  213. pyronova-2.0.0/tests/test_tls_slowloris.py +30 -0
  214. pyronova-2.0.0/tests/test_upload_streaming.py +141 -0
  215. pyronova-2.0.0/tests/test_uploads_e2e.py +57 -0
  216. pyronova-2.0.0/tests/test_websocket.py +113 -0
@@ -0,0 +1,10 @@
1
+ Run all Pyre tests: Rust compile check + Python unit + integration.
2
+
3
+ Steps:
4
+ 1. `cargo test` — Rust compilation check (catches type errors, unused imports, etc.)
5
+ 2. `source .venv/bin/activate && maturin develop --release` — build Python extension
6
+ 3. `pytest tests/test_mcp.py tests/test_cookies_unit.py tests/test_testclient.py tests/test_rpc.py tests/test_static_files.py tests/test_websocket.py tests/test_async_isolation.py tests/test_logging.py -v` — pytest unit tests
7
+ 4. `python tests/test_all_features.py` — integration tests (starts real servers, 22 end-to-end tests)
8
+ 5. Report a summary table of results (pass/fail counts per stage)
9
+
10
+ If any test fails, investigate the failure and report what went wrong. Do NOT auto-fix — just report.
@@ -0,0 +1 @@
1
+ - [feedback_y_hash_fix.md](feedback_y_hash_fix.md) — Phase 7.2: use y# not z# for bytes in PyArg_ParseTuple
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Phase 7.2 FFI format string fix
3
+ description: When implementing pyre_send_cfunc, use y# (not z#) for Python bytes args. z# is for str/None only.
4
+ type: feedback
5
+ ---
6
+
7
+ When implementing Phase 7.2 async bridge (`pyre_send_cfunc` in interp.rs):
8
+ - Use `c"nKHsy#"` format string for `PyArg_ParseTuple` (not `z#z#`)
9
+ - `y#` = bytes, `z#` = str/None, `s` = str
10
+ - Don't pass `&mut body_len` twice (pointer overwrite bug)
11
+ - Always `PyErr_Print()` on parse failure, never silently return null
12
+ - Consider replacing global `Mutex<HashMap>` with `OnceLock<Vec<WorkerState>>` for zero-lock at 200k+ QPS
13
+
14
+ **Why:** Python side sends `json.dumps(res).encode('utf-8')` which is `bytes`, not `str`. Wrong format causes silent parse failure → tx.send never fires → request hangs → Tokio pool exhaustion.
15
+
16
+ **How to apply:** When writing `pyre_send_cfunc` for Phase 7.2 implementation.
@@ -0,0 +1,108 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ CARGO_TERM_COLOR: always
11
+
12
+ jobs:
13
+ # ── Build + unit tests ────────────────────────────────────────────
14
+ test-unit:
15
+ name: Unit tests (Python ${{ matrix.python-version }})
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version: ["3.13", "3.14"]
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+ allow-prereleases: true
27
+ - uses: dtolnay/rust-toolchain@stable
28
+ - uses: Swatinem/rust-cache@v2
29
+
30
+ - name: Install dependencies
31
+ run: |
32
+ python -m venv .venv
33
+ source .venv/bin/activate
34
+ pip install maturin pytest pytest-asyncio websockets httpx pydantic numpy msgpack
35
+
36
+ - name: cargo test (Rust compile check)
37
+ env:
38
+ PYO3_PYTHON: ${{ env.pythonLocation }}/bin/python
39
+ run: cargo test
40
+
41
+ - name: Build (release)
42
+ run: |
43
+ source .venv/bin/activate
44
+ maturin develop --release
45
+
46
+ - name: Run pytest (unit)
47
+ run: |
48
+ source .venv/bin/activate
49
+ pytest tests/test_mcp.py tests/test_cookies_unit.py tests/test_testclient.py tests/test_rpc.py tests/test_static_files.py tests/test_websocket.py tests/test_async_isolation.py tests/test_logging.py -v
50
+
51
+ # ── Integration tests ─────────────────────────────────────────────
52
+ test-integration:
53
+ name: Integration tests (Python ${{ matrix.python-version }})
54
+ runs-on: ubuntu-latest
55
+ needs: test-unit
56
+ strategy:
57
+ fail-fast: false
58
+ matrix:
59
+ python-version: ["3.13", "3.14"]
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+ - uses: actions/setup-python@v5
63
+ with:
64
+ python-version: ${{ matrix.python-version }}
65
+ allow-prereleases: true
66
+ - uses: dtolnay/rust-toolchain@stable
67
+ - uses: Swatinem/rust-cache@v2
68
+
69
+ - name: Install dependencies
70
+ run: |
71
+ python -m venv .venv
72
+ source .venv/bin/activate
73
+ pip install maturin pytest pytest-asyncio websockets httpx pydantic numpy msgpack
74
+
75
+ - name: Build (release)
76
+ run: |
77
+ source .venv/bin/activate
78
+ maturin develop --release
79
+
80
+ - name: Integration — full pytest suite (GIL + sub-interp)
81
+ run: |
82
+ source .venv/bin/activate
83
+ # E2E drivers live under tests/e2e/ (not collected by pytest).
84
+ pytest tests/ -q --ignore=tests/e2e
85
+
86
+ # ── Rust lint (blocking) ──────────────────────────────────────────
87
+ rust-lint:
88
+ name: Rust lint
89
+ runs-on: ubuntu-latest
90
+ steps:
91
+ - uses: actions/checkout@v4
92
+ # pyo3-ffi gates PyThreadState_GetUnchecked on cfg(Py_3_13); Pyronova
93
+ # requires it. Without pinning an interpreter, the runner's
94
+ # default python3 (typically 3.11/3.12) is picked up and clippy
95
+ # fails with E0425 on a missing ffi symbol.
96
+ - uses: actions/setup-python@v5
97
+ with:
98
+ python-version: "3.13"
99
+ - uses: dtolnay/rust-toolchain@stable
100
+ with:
101
+ components: clippy, rustfmt
102
+ - uses: Swatinem/rust-cache@v2
103
+ - name: cargo fmt
104
+ run: cargo fmt --check
105
+ - name: cargo clippy
106
+ env:
107
+ PYO3_PYTHON: ${{ env.pythonLocation }}/bin/python
108
+ run: cargo clippy --all-targets -- -D warnings
@@ -0,0 +1,87 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ env:
12
+ CARGO_TERM_COLOR: always
13
+
14
+ jobs:
15
+ # ── Build wheels ────────────────────────────────────────────────────
16
+ build:
17
+ name: Build (${{ matrix.target }})
18
+ runs-on: ${{ matrix.os }}
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ include:
23
+ # Linux x86_64 (manylinux via Docker)
24
+ - os: ubuntu-latest
25
+ target: x86_64-unknown-linux-gnu
26
+ # macOS ARM (Apple Silicon)
27
+ - os: macos-14
28
+ target: aarch64-apple-darwin
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.13"
35
+
36
+ - name: Build wheels
37
+ uses: PyO3/maturin-action@v1
38
+ with:
39
+ target: ${{ matrix.target }}
40
+ args: --release --out dist -i python3.13
41
+ manylinux: auto
42
+
43
+ - uses: actions/upload-artifact@v4
44
+ with:
45
+ name: wheels-${{ matrix.target }}
46
+ path: dist/
47
+
48
+ # ── Build source distribution ───────────────────────────────────────
49
+ sdist:
50
+ name: Build sdist
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+
55
+ - name: Build sdist
56
+ uses: PyO3/maturin-action@v1
57
+ with:
58
+ command: sdist
59
+ args: --out dist
60
+
61
+ - uses: actions/upload-artifact@v4
62
+ with:
63
+ name: wheels-sdist
64
+ path: dist/
65
+
66
+ # ── Publish to PyPI ─────────────────────────────────────────────────
67
+ publish:
68
+ name: Publish to PyPI
69
+ needs: [build, sdist]
70
+ runs-on: ubuntu-latest
71
+ environment:
72
+ name: pypi
73
+ url: https://pypi.org/p/pyronova
74
+ permissions:
75
+ id-token: write
76
+ steps:
77
+ - uses: actions/download-artifact@v4
78
+ with:
79
+ pattern: wheels-*
80
+ merge-multiple: true
81
+ path: dist/
82
+
83
+ - name: Publish to PyPI
84
+ uses: PyO3/maturin-action@v1
85
+ with:
86
+ command: upload
87
+ args: --non-interactive --skip-existing dist/*
@@ -0,0 +1,12 @@
1
+ target/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.so
6
+ *.dylib
7
+ *.egg-info/
8
+ dist/
9
+ .venv/
10
+ *.lock
11
+ .arc/
12
+ arc.log.jsonl
@@ -0,0 +1,369 @@
1
+ # Changelog
2
+
3
+ ## v2.0.0 (2026-04-21) — Rename to Pyronova
4
+
5
+ Project rename — `pyreframework` → `pyronova`, brand `Pyre` → `Pyronova`.
6
+ Clean break, no alias. All downstream users must update imports. This is
7
+ a rename-only release — no behavioral changes.
8
+
9
+ ### Why
10
+
11
+ "Pyre" collided with Meta's widely-used Pyre type checker. After a round
12
+ of name-collision research (PyPI availability + GitHub hit count), we
13
+ settled on **Pyronova** — retains the fire theme, encodes "Python +
14
+ Rust + nova (explosive newness)", single GitHub collision, free on
15
+ PyPI. See the rename PR description for the full candidate shortlist.
16
+
17
+ ### Breaking — import surface
18
+
19
+ - PyPI package: `pip install pyronova` (was `pyreframework`).
20
+ - Main class: `from pyronova import Pyronova` (was `from pyreframework
21
+ import Pyre`).
22
+ - Type names switch to unprefixed ("FastAPI style"): `Request`,
23
+ `Response`, `WebSocket`, `Stream`, `BodyStream`, `RPCClient`,
24
+ `Settings`. The `PyreRequest` / `PyreResponse` / … prefixed forms
25
+ are gone.
26
+ - CLI: `pyronova run/dev/routes` (was `pyre ...`).
27
+ - Env vars: `PYRONOVA_HOST`, `PYRONOVA_PORT`, `PYRONOVA_LOG`,
28
+ `PYRONOVA_WORKERS`, `PYRONOVA_IO_WORKERS`, `PYRONOVA_TLS_CERT`,
29
+ `PYRONOVA_TLS_KEY`, `PYRONOVA_RELOAD`, `PYRONOVA_LOG_LEVEL`,
30
+ `PYRONOVA_TEST_PG_DSN`.
31
+ - Rust crate: `pyronova-engine`, lib name `pyronova_engine` (was
32
+ `pyreframework-engine` / `pyreframework_engine`).
33
+ - Rust FFI symbols injected into sub-interpreter globals: `_pyronova_recv`,
34
+ `_pyronova_send`, `_pyronova_emit_log`, `_pyronova_pool_id`.
35
+ - Log target names: `pyronova::server`, `pyronova::access`,
36
+ `pyronova::app`.
37
+
38
+ ### Migration
39
+
40
+ Mechanical — two find-and-replace passes handle the common shape:
41
+
42
+ ```bash
43
+ git grep -l pyreframework | xargs sed -i 's/pyreframework/pyronova/g'
44
+ git grep -l '\bPyre\b' | xargs sed -i 's/\bPyre\b/Pyronova/g'
45
+ # Type names lost their prefix — handle per-class:
46
+ git grep -l PyreRequest | xargs sed -i 's/PyreRequest/Request/g'
47
+ git grep -l PyreResponse | xargs sed -i 's/PyreResponse/Response/g'
48
+ # …and the same for WebSocket, Stream, BodyStream, RPCClient, Settings.
49
+ # Env vars:
50
+ git grep -l PYRE_ | xargs sed -i 's/PYRE_/PYRONOVA_/g'
51
+ ```
52
+
53
+ Historical release notes, benchmark reports, and incident post-mortems
54
+ under `benchmarks/benchmark-*.md`, `docs/memory-leak-investigation-*.md`,
55
+ `docs/advisor-triage-*.md` were deliberately left unrenamed — the
56
+ framework was called Pyre when those events happened, and rewriting the
57
+ history is dishonest.
58
+
59
+ ## v1.5.0 (2026-04-19)
60
+
61
+ Memory-leak root cause fix + hardening pass. Minor-bump because Python
62
+ 3.13+ is now required (dropped 3.10-3.12 support, see "Breaking").
63
+
64
+ ### Headline — sub-interpreter memory leak closed
65
+
66
+ Pyre sub-interpreters had a long-standing unbounded RSS growth under
67
+ sustained load (~128 B/req at 400k rps → OOM in ~10 min). Root cause
68
+ isolated via a pure-C reproducer to cross-thread `PyThreadState` reuse:
69
+ `SubInterpreterWorker::new` ran on the main thread, `Py_NewInterpreterFromConfig`
70
+ bound the tstate to that OS thread, then the worker pthread attach/detached
71
+ that tstate on every request. CPython's per-OS-thread tstate bookkeeping
72
+ accumulates when the attaching thread differs from the creator — measured
73
+ at ~1 KB per iteration in isolation.
74
+
75
+ Fix (`fc45a7f`): new `rebind_tstate_to_current_thread` helper that each
76
+ worker calls on entry. Creates a fresh tstate via `PyThreadState_New(interp)`
77
+ bound to the worker's OS thread, swaps it in, and disposes of the creator
78
+ tstate. All subsequent request dispatch runs against the thread-local
79
+ tstate. **Measured result**: 73.8M requests @ 410k rps over 180s = total
80
+ RSS growth of **4 MB** (0.057 B/req, below /proc sampling noise).
81
+
82
+ Side effect: Python `__del__` / `tp_finalize` now fires correctly in
83
+ sub-interp handlers (previously silently broken, was xfail'd).
84
+
85
+ ### Architecture — raw C-API `_PyreRequest` type
86
+
87
+ Replaced the Python-defined `_PyreRequest` class with a custom heap
88
+ type built via `PyType_FromSpec` + `PyMemberDef` in
89
+ `src/pyre_request_type.rs`. Custom `tp_dealloc` synchronously DECREFs
90
+ all seven slot fields. pyo3's `#[pyclass]` can't be used — pyo3 0.28
91
+ hard-rejects sub-interpreters.
92
+
93
+ Two invariants learned the hard way:
94
+
95
+ - **No `Py_TPFLAGS_BASETYPE`**: Python subclassing triggers CPython's
96
+ `subtype_dealloc` fallback which silently bypasses our `tp_dealloc`.
97
+ Helper methods (`.text()`, `.json()`, `.body`, `.query_params`) are
98
+ monkey-patched onto the heap type at sub-interp init instead.
99
+ - **No `Py_TPFLAGS_HAVE_GC`**: empirically made per-request leak 5×
100
+ worse. Our workload has no cycles; GC tracking costs without benefit.
101
+
102
+ ### Correctness & hygiene — 22 fixes triaged from adversarial review
103
+
104
+ Full triage record (31 claims reviewed, 21 real / 10 rejected) lives in
105
+ `docs/advisor-triage-2026-04-19.md`.
106
+
107
+ C-API hygiene: `PyDict_Next` + `PyObject_Str` re-entrancy in
108
+ `parse_sky_response`; `PyObject_IsInstance == 1` now handles `-1`
109
+ (error); `py_str_dict` clears pending exceptions on OOM; `PyDict_SetItem`
110
+ return value checked; `_PyreRequest.__init__` path type-checks dict
111
+ slots via `PyDict_Check` before `PyDict_Clear`.
112
+
113
+ Lifecycle: `LoopGuard` drop-after-`Py_Finalize` segfault fixed via
114
+ `std::mem::forget`; Hyper graceful shutdown via `TaskTracker` waits
115
+ up to 30s for in-flight connections before runtime drop (was
116
+ RST-on-shutdown); `InterpreterPool::drop` bounds worker-thread join
117
+ at 5s; `spawn_rss_sampler` JoinHandle now joined on shutdown;
118
+ `WORKER_STATES` `OnceLock` → `RwLock<Vec>` so repeated `app.run()`
119
+ gets fresh channels.
120
+
121
+ WebSocket: async WS handlers now driven via `asyncio.run` (was
122
+ silently dropped); explicit `drop(handler)` under GIL.
123
+
124
+ Routing: path params URL-decoded (`/user/john%20doe` → `"john doe"`);
125
+ async middleware coroutines driven through `resolve_coroutine` in
126
+ both sub-interp and GIL-mode paths; CORS `origin="*"` + credentials
127
+ emits W3C-violation warning.
128
+
129
+ Static files: `canonicalize → File::open` TOCTOU closed via
130
+ `O_NOFOLLOW` on Unix (refuses post-check symlink swap).
131
+
132
+ ### Performance
133
+
134
+ Awaitable detection in `resolve_coroutine` moved from
135
+ `PyObject_HasAttrString(obj, "__await__")` (μs per call — interns the
136
+ string, walks the MRO, runs descriptor protocol) to a direct
137
+ `Py_TYPE(obj)->tp_as_async->am_await` pointer probe (ns, L1-resident).
138
+
139
+ ### Benchmark
140
+
141
+ Bench targets split cleanly. `benchmarks/bench_plaintext.py` (new,
142
+ feature-light) is the target for `just bench-record` / `bench-compare`.
143
+ `examples/hello.py` (restored to v1.4.0 content) is the feature demo,
144
+ run via `just bench-features`. `just bench-tfb-plaintext` runs
145
+ TechEmpower-style `wrk -t8 -c256 -d15s --pipeline 16`.
146
+
147
+ Numbers (AMD Ryzen 7 7840HS, 8C/16T, Python 3.14.4, performance
148
+ governor, wrk 4.1.0):
149
+
150
+ | Workload | Config | Req/s |
151
+ |---|---|---|
152
+ | Plaintext baseline | `wrk -t4 -c100 -d10s` | **422,976** |
153
+ | Feature demo | `wrk -t4 -c100 -d10s` on `hello.py` | 381,123 |
154
+ | **TFB Plaintext** | `wrk -t8 -c256 -d15s --pipeline 16` | **902,213** |
155
+ | TFB JSON (`/hello/{name}`) | `wrk -t8 -c512 -d15s` | ~536,000 |
156
+
157
+ Plaintext baseline vs v1.4.0's published 419,730 on the same machine:
158
+ **+0.8% — zero regression**. The ~10% gap on `hello.py` reflects the
159
+ added cost of async-correct middleware + access log (intentional
160
+ hygiene tax, opt-in).
161
+
162
+ ### Tests
163
+
164
+ - `test_sustained_concurrent_load_no_leak` — 12s soak, fails if RSS
165
+ grows >15 MB. Regression guard for the tstate fix.
166
+ - `test_subinterp_python_finalizers_fire` — xfail removed.
167
+ - `test_capi_hygiene.py` — 5 tests (reentrancy, malformed init,
168
+ URL-decode, async hook, instancecheck raise).
169
+ - `test_static_symlink_out_of_root_refused` — O_NOFOLLOW regression.
170
+ - `worker_states_can_be_reinstalled` — Rust unit test for hot-reload
171
+ of sub-interp pool.
172
+ - `TestClient(port=None)` auto-allocates port (new).
173
+ - Port collision fixes (19878, 19883).
174
+
175
+ Total: 235 passed / 2 full-suite runs / 0 flakes / 0 regressions.
176
+
177
+ ### Breaking
178
+
179
+ - **`requires-python = ">=3.13"`** (was `>=3.10`). Users on 3.10-3.12
180
+ should stay on v1.4.x.
181
+
182
+ ### Deferred to future releases
183
+
184
+ - SSE dedicated asyncio background thread.
185
+ - `response_map` active-sweep GC for deadlocked-worker orphans.
186
+ - PyBuffer-based zero-copy body write on the send path.
187
+ - `query_params_all()` additive API for HPP-correct multivalue access.
188
+
189
+ ---
190
+
191
+ ## v1.4.5 (2026-04-19)
192
+
193
+ Security + correctness hardening from an adversarial review pass. 23 fixes
194
+ (6 critical + 17 error).
195
+
196
+ **Same-day same-machine benchmark** (AMD 7840HS, `wrk -t4 -c100 -d10s`
197
+ on `examples/hello.py` hybrid mode):
198
+
199
+ | Build | Requests/sec (avg of 3) |
200
+ |---|---|
201
+ | d0ce481 (v1.4.4 pre-hotfix) | ~260k |
202
+ | v1.4.5 (this release) | ~320k |
203
+
204
+ No regression; observed a small net gain within noise. Both runs are
205
+ below the 419k historical baseline from v1.4.0's benchmark day — the
206
+ machine is in a different thermal / load state today, not a code
207
+ change. The relative comparison is what matters and it is clean.
208
+
209
+ ### Critical (already shipped in hotfix)
210
+ - `accept()` loop classifies errno (EMFILE / ENFILE / ENOBUFS / ENOMEM)
211
+ and backs off — was 100% CPU spin on FD exhaustion
212
+ - Sub-interpreter RAII: `SubInterpreterWorker::new` no longer leaks the
213
+ sub-interp on any of 5+ fallible init steps
214
+ - WebSocket: `py_handle.join()` moved off the Tokio worker pool;
215
+ `recv/recv_bytes/recv_message` release the GIL via `py.detach()`
216
+ - MCP: reject non-object JSON-RPC payloads with -32600 instead of
217
+ crashing through `AttributeError`
218
+
219
+ ### Security
220
+ - Cookies: reject CRLF / NUL in name / value / domain / path / expires
221
+ (HTTP Response Splitting)
222
+ - Error responses: `serde_json` for `{"error": msg}` — hand-rolled
223
+ escape handled only `"`, leaving backslash / control-char injection
224
+ open
225
+ - Static files: open-once design removes a TOCTOU where a rename
226
+ between `metadata()` and `read()` bypassed the `MAX_STATIC_FILE_BYTES`
227
+ cap; `.take(cap)` adds belt-and-braces
228
+ - `before_request` hook that raises now fails the request with 500
229
+ — previously fell through to the unprotected main handler, a
230
+ critical auth-bypass for deny-via-raise auth hooks
231
+ - Sub-interp path: CORS now applied on the `Err` branch so browsers
232
+ show the real 5xx instead of an opaque CORS error
233
+ - Interp FFI: `PyTuple_SetItem` never embeds a NULL — every leaf
234
+ allocation NULL-checked up front, partial failures cleaned up
235
+ atomically (was a latent segfault on OOM)
236
+
237
+ ### Correctness
238
+ - `logging::init_logger`: `(writer, guard)` stored atomically in
239
+ `OnceLock<LoggerState>` — previous design could drop the guard of
240
+ whichever caller won `try_init()`, silently killing the log thread
241
+ - `router::lookup` uppercases the method to match `insert` — HTTP is
242
+ case-insensitive per RFC 9110 §9.1, lowercase / `Get` was silently
243
+ missing routes
244
+ - `SharedState::incr` raises `TypeError` / `OverflowError` instead of
245
+ silently resetting non-numeric values to 0
246
+ - Bounded channels: `PyreStream` (1024) + WebSocket outgoing (1024)
247
+ with `try_send` — unbounded was an OOM DoS under slow-client
248
+ backpressure
249
+ - `handlers::handle_request` error path: full PyErr logged server-side
250
+ (`e.display(py)` + `tracing::error!`); client gets a generic "handler
251
+ error" instead of a leaked one-line repr
252
+
253
+ ### Python
254
+ - `rpc.py` / `_async_engine.py`: `log.exception` before the error
255
+ envelope so server-side stack traces survive RPC / async failures
256
+ - `_bootstrap.py` `PyreRustHandler.emit`: `self.handleError(record)`
257
+ instead of `pass` — stdlib logging's standard "I failed to log" hook
258
+ - `mcp._extract_schema`: `typing.get_type_hints(fn)` so tools defined
259
+ in modules with `from __future__ import annotations` don't silently
260
+ regress to "string" for every argument
261
+ - `UploadFile`: `@dataclass(frozen=True, slots=True)` — shares memory
262
+ with the raw multipart buffer, mutation would corrupt replay
263
+
264
+ ## v1.4.0 (2026-04-01)
265
+
266
+ ### Performance — Linux 42万 QPS
267
+ - **SO_REUSEPORT multi-accept** — N=io_workers 个独立 accept loop,Linux 内核级四元组哈希负载均衡,macOS 自动降级为 1
268
+ - **M:N scheduling** — `io_workers` (Tokio I/O threads) 和 `workers` (Python sub-interpreters) 独立配置,解耦网络层与计算层
269
+ - **LTO fat + codegen-units=1** — 编译期全局优化,+4% JSON/params 路由
270
+ - **TCP_QUICKACK** — Linux 禁用延迟 ACK,降低首字节延迟
271
+ - **Headers OnceLock lazy view** — 不访问 headers 时零开销,延迟转换
272
+ - **serde_json + pythonize** — Rust 侧 JSON 序列化,替代 Python json.loads
273
+ - **SharedState Bytes** — 零拷贝 clone
274
+ - **Arc\<str\> method/path** — 请求路径零分配
275
+ - **IpAddr lazy eval** — 不访问时不解析
276
+ - **Bytes zero-copy body** — 请求体零拷贝
277
+ - **mimalloc global allocator** — 高并发分配性能
278
+
279
+ ### Features
280
+ - `io_workers` parameter — `app.run(workers=24, io_workers=16)` 或 `PYRE_IO_WORKERS=16`
281
+ - `client_ip` — 请求客户端 IP 地址
282
+ - Lifecycle hooks — `on_startup` / `on_shutdown`
283
+ - Zero-cost logging — Rust tracing engine + Python→Rust FFI bridge, OFF 级别原子跳过
284
+
285
+ ### Benchmarks (Linux, AMD Ryzen 7 7840HS 8C/16T)
286
+ - **GET /: 420k req/s** (P99 571μs) — vs macOS v1.2.0 214k (+96%)
287
+ - **300s sustained: 401k req/s**, 1.2 亿请求, 0 错误, 内存仅 +27 MB
288
+ - **vs Robyn: 14-16x faster** across all routes
289
+
290
+ ## v1.3.0 (2026-03-31)
291
+
292
+ ### Features
293
+ - **Zero-cost logging system** — Rust `tracing` + `EnvFilter`, three targets, Python logging bridge via C-FFI
294
+ - **client_ip** property on PyreRequest
295
+ - **on_startup / on_shutdown** lifecycle hooks
296
+
297
+ ### Performance
298
+ - IpAddr lazy evaluation
299
+ - Bytes zero-copy request body
300
+ - Arc\<str\> method/path to eliminate allocations
301
+ - Vec params (from HashMap)
302
+ - Zero-allocation hook iteration
303
+ - Sync Python log level with Rust EnvFilter
304
+
305
+ ### Docs
306
+ - Sub-interpreter C extension compatibility guide (30/30 libs confirmed)
307
+ - English translations for all benchmark reports
308
+
309
+ ## v1.2.0 (2026-03-25)
310
+
311
+ ### Features
312
+ - **Dual async/sync worker pool** — `async def` handlers auto-route to asyncio event loops, `def` handlers to sync sub-interpreters. Zero config, zero performance loss.
313
+ - **Native async bridge (C-FFI)** — `pyre_recv`/`pyre_send` release GIL during channel wait, enabling true async in sub-interpreters.
314
+ - **MCP Server** — JSON-RPC 2.0 with `@app.mcp.tool()`, `@app.mcp.resource()`, `@app.mcp.prompt()` decorators.
315
+ - **MsgPack RPC** — `@app.rpc()` with content negotiation (MsgPack/JSON) + `PyreRPCClient` magic client.
316
+ - **SSE Streaming** — `PyreStream` with mpsc channel, returned directly from handlers.
317
+ - **SharedState** — Cross-worker `app.state` backed by `Arc<DashMap>`, nanosecond latency.
318
+ - **GIL Watchdog** — Monitor GIL contention, hold time, queue depth, memory RSS.
319
+ - **Backpressure** — Bounded channels with `try_send()`, returns 503 on overload.
320
+ - **Request timeout** — 30s zombie reaper in sub-interpreter mode (504 Gateway Timeout).
321
+ - **mimalloc** — Global allocator for high-concurrency allocation performance.
322
+ - **Hybrid dispatch** — `gil=True` routes auto-dispatch to main interpreter for C extension compatibility.
323
+
324
+ ### Code Quality
325
+ - Extracted bootstrap script from Rust string to `python/pyreframework/_bootstrap.py` (`include_str!`).
326
+ - Removed dead `filter_script_ast` code.
327
+ - Moved CORS/logging from global statics to per-instance `PyreApp` fields.
328
+ - Added `debug_assert!(PyGILState_Check())` in `PyObjRef::Drop`.
329
+ - Full `cargo fmt` + zero clippy warnings.
330
+ - Migrated deprecated PyO3 `downcast` → `cast` calls.
331
+
332
+ ### Bug Fixes
333
+ - **Fixed segfault on Ctrl+C** — `InterpreterPool::Drop` now joins worker threads before `Py_Finalize`.
334
+ - **Fixed KeyboardInterrupt noise** — Guard `signal.signal()` for main thread only.
335
+ - Hot reload fallback skips `.venv`/`node_modules`/`__pycache__`.
336
+
337
+ ### Testing
338
+ - 21 Rust unit tests (response builders, MIME detection, header extraction, query params).
339
+ - 54 Python pytest tests (MCP, cookies, TestClient, RPC, static files, WebSocket, async isolation, logging).
340
+ - 22 integration tests (GIL + sub-interp modes, all features end-to-end).
341
+ - 5-minute stability benchmark: 64M requests, zero memory leaks, zero crashes.
342
+
343
+ ### CI/CD
344
+ - GitHub Actions: cargo test → pytest → integration tests on Python 3.13/3.14.
345
+ - Blocking `cargo fmt --check` + `cargo clippy -- -D warnings`.
346
+
347
+ ## v1.1.0 (2026-03-24)
348
+
349
+ ### Features
350
+ - WebSocket support (text + binary) via tokio-tungstenite.
351
+ - Cookie utilities (`get_cookie`, `set_cookie`, `delete_cookie`).
352
+ - Multipart file upload parser.
353
+ - Redirect helper.
354
+ - TestClient for testing without a running server.
355
+ - Env var configuration (`PYRE_HOST`, `PYRE_PORT`, `PYRE_WORKERS`, `PYRE_LOG`).
356
+ - Hot reload (`reload=True` or `PYRE_RELOAD=1`).
357
+
358
+ ## v1.0.0 (2026-03-23)
359
+
360
+ ### Initial Release
361
+ - Rust core with Tokio + Hyper HTTP server.
362
+ - Per-Interpreter GIL (PEP 684) sub-interpreter pool.
363
+ - Decorator routing (`@app.get`, `@app.post`, etc.).
364
+ - Path params, query params, JSON parsing.
365
+ - CORS middleware.
366
+ - Static file serving with MIME detection + path traversal protection.
367
+ - Pydantic validation via `model=` parameter.
368
+ - Before/after request hooks.
369
+ - Graceful shutdown via Ctrl+C.