hayate 0.3.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 (79) hide show
  1. hayate-0.3.0/.github/workflows/ci.yml +48 -0
  2. hayate-0.3.0/.github/workflows/release.yml +38 -0
  3. hayate-0.3.0/.gitignore +10 -0
  4. hayate-0.3.0/CHANGELOG.md +65 -0
  5. hayate-0.3.0/DESIGN.md +460 -0
  6. hayate-0.3.0/PKG-INFO +90 -0
  7. hayate-0.3.0/README.md +71 -0
  8. hayate-0.3.0/accel/Cargo.toml +14 -0
  9. hayate-0.3.0/accel/pyproject.toml +13 -0
  10. hayate-0.3.0/accel/src/lib.rs +128 -0
  11. hayate-0.3.0/benchmarks/bench.py +170 -0
  12. hayate-0.3.0/docs/benchmarks.md +66 -0
  13. hayate-0.3.0/docs/conformance.md +57 -0
  14. hayate-0.3.0/docs/research/cloudflare.md +112 -0
  15. hayate-0.3.0/examples/chat.py +58 -0
  16. hayate-0.3.0/pyproject.toml +50 -0
  17. hayate-0.3.0/src/hayate/__init__.py +46 -0
  18. hayate-0.3.0/src/hayate/abort.py +62 -0
  19. hayate-0.3.0/src/hayate/adapters/__init__.py +5 -0
  20. hayate-0.3.0/src/hayate/adapters/asgi.py +220 -0
  21. hayate-0.3.0/src/hayate/adapters/aws.py +116 -0
  22. hayate-0.3.0/src/hayate/adapters/workers.py +94 -0
  23. hayate-0.3.0/src/hayate/app.py +325 -0
  24. hayate-0.3.0/src/hayate/body.py +78 -0
  25. hayate-0.3.0/src/hayate/context.py +159 -0
  26. hayate-0.3.0/src/hayate/cookies.py +90 -0
  27. hayate-0.3.0/src/hayate/exceptions.py +85 -0
  28. hayate-0.3.0/src/hayate/formdata.py +117 -0
  29. hayate-0.3.0/src/hayate/headers.py +179 -0
  30. hayate-0.3.0/src/hayate/jsonutil.py +28 -0
  31. hayate-0.3.0/src/hayate/middleware/__init__.py +27 -0
  32. hayate-0.3.0/src/hayate/middleware/_internal.py +27 -0
  33. hayate-0.3.0/src/hayate/middleware/basic_auth.py +37 -0
  34. hayate-0.3.0/src/hayate/middleware/body_limit.py +43 -0
  35. hayate-0.3.0/src/hayate/middleware/cache.py +53 -0
  36. hayate-0.3.0/src/hayate/middleware/compress.py +88 -0
  37. hayate-0.3.0/src/hayate/middleware/cors.py +78 -0
  38. hayate-0.3.0/src/hayate/middleware/etag.py +40 -0
  39. hayate-0.3.0/src/hayate/middleware/logger.py +26 -0
  40. hayate-0.3.0/src/hayate/middleware/secure_headers.py +43 -0
  41. hayate-0.3.0/src/hayate/middleware/static_files.py +136 -0
  42. hayate-0.3.0/src/hayate/middleware/timeout.py +21 -0
  43. hayate-0.3.0/src/hayate/py.typed +1 -0
  44. hayate-0.3.0/src/hayate/request.py +210 -0
  45. hayate-0.3.0/src/hayate/response.py +54 -0
  46. hayate-0.3.0/src/hayate/router.py +88 -0
  47. hayate-0.3.0/src/hayate/sse.py +43 -0
  48. hayate-0.3.0/src/hayate/url.py +451 -0
  49. hayate-0.3.0/src/hayate/urlpattern.py +211 -0
  50. hayate-0.3.0/src/hayate/validator.py +64 -0
  51. hayate-0.3.0/src/hayate/websocket.py +78 -0
  52. hayate-0.3.0/tests/_ws.py +54 -0
  53. hayate-0.3.0/tests/conftest.py +7 -0
  54. hayate-0.3.0/tests/test_accel.py +58 -0
  55. hayate-0.3.0/tests/test_app.py +345 -0
  56. hayate-0.3.0/tests/test_asgi.py +194 -0
  57. hayate-0.3.0/tests/test_chat_example.py +28 -0
  58. hayate-0.3.0/tests/test_cookies.py +71 -0
  59. hayate-0.3.0/tests/test_headers.py +87 -0
  60. hayate-0.3.0/tests/test_jsonutil.py +15 -0
  61. hayate-0.3.0/tests/test_lambda_adapter.py +114 -0
  62. hayate-0.3.0/tests/test_middleware.py +225 -0
  63. hayate-0.3.0/tests/test_middleware_http.py +123 -0
  64. hayate-0.3.0/tests/test_request.py +123 -0
  65. hayate-0.3.0/tests/test_response.py +61 -0
  66. hayate-0.3.0/tests/test_sse.py +36 -0
  67. hayate-0.3.0/tests/test_static_and_cache.py +158 -0
  68. hayate-0.3.0/tests/test_todo_api.py +116 -0
  69. hayate-0.3.0/tests/test_url.py +134 -0
  70. hayate-0.3.0/tests/test_urlpattern.py +87 -0
  71. hayate-0.3.0/tests/test_validator.py +121 -0
  72. hayate-0.3.0/tests/test_websocket.py +85 -0
  73. hayate-0.3.0/tests/test_workers_adapter.py +165 -0
  74. hayate-0.3.0/tests/test_wpt_url.py +89 -0
  75. hayate-0.3.0/tests/test_wpt_urlpattern.py +123 -0
  76. hayate-0.3.0/tests/wpt/README.md +22 -0
  77. hayate-0.3.0/tests/wpt/urlpatterntestdata.json +3181 -0
  78. hayate-0.3.0/tests/wpt/urltestdata.json +10631 -0
  79. hayate-0.3.0/uv.lock +213 -0
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python: ["3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - run: uv sync
21
+ - run: uv run ruff check src tests benchmarks
22
+ - run: uv run ruff format --check src tests benchmarks
23
+ - run: uv run pytest -q
24
+
25
+ # Free-threaded build: the core must stay free of global mutable state.
26
+ test-free-threaded:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ python-version: "3.14t"
33
+ - run: uv sync
34
+ - run: uv run pytest -q
35
+
36
+ # Tier 2 accelerator: build the Rust wheel and verify behavioral identity.
37
+ accel:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: dtolnay/rust-toolchain@stable
42
+ - uses: astral-sh/setup-uv@v5
43
+ with:
44
+ python-version: "3.12"
45
+ - run: uv sync
46
+ - run: uv run --with maturin maturin build --release -m accel/Cargo.toml -o dist-accel
47
+ - run: uv pip install dist-accel/*.whl
48
+ - run: uv run --no-sync pytest -q
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: uv sync
17
+ - run: uv run pytest -q
18
+ - run: uv build
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ # PyPI Trusted Publishing (OIDC). Requires the "hayate" project (or a
25
+ # pending publisher) on PyPI configured with:
26
+ # owner: haya-inc, repo: hayate, workflow: release.yml, environment: pypi
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ dist-accel/
8
+ *.egg-info/
9
+ accel/target/
10
+ Cargo.lock
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to hayate are documented here.
4
+
5
+ ## [0.3.0] - 2026-07-22
6
+
7
+ First public release. hayate is a web-standards-first Python web
8
+ framework inspired by Hono: the Fetch API model (Request / Response /
9
+ Headers / URL / URLPattern) is the user-facing surface, and Python-local
10
+ protocols (WSGI/ASGI) are demoted to adapter-level details.
11
+
12
+ ### Core
13
+
14
+ - Fetch-semantics `Request` / `Response` / `Headers` (one-shot bodies,
15
+ `clone()` with stream tee, immutability guard)
16
+ - WHATWG `URL` / `URLSearchParams` / `URLPattern` as documented subsets,
17
+ measured against vendored web-platform-tests on every run
18
+ (URLPattern: zero behavioral mismatches within the supported subset;
19
+ URL: 66% of in-scope cases — see docs/conformance.md)
20
+ - Routing via URLPattern syntax (`/books/:id(\d+)`, `*`, `:name?`),
21
+ onion middleware, automatic 405 + `Allow`, HEAD→GET fallback
22
+ - Errors as RFC 9457 `application/problem+json`, everywhere
23
+ - Context helpers: `c.json/text/html/body/redirect`, `c.set_cookie`,
24
+ `c.wait_until` (Workers `ctx.waitUntil` semantics), `c.event_stream`
25
+ - `app.request()` — test the app with no server and no test client
26
+
27
+ ### Realtime
28
+
29
+ - WebSocket routes (`@app.ws`, auto-accept, async iteration)
30
+ - Server-Sent Events (`c.event_stream`, WHATWG HTML format)
31
+
32
+ ### Middleware (all zero-dependency)
33
+
34
+ `logger`, `cors`, `etag`, `compress` (gzip; zstd on 3.14+),
35
+ `basic_auth`, `body_limit`, `timeout`, `secure_headers`, `cache`,
36
+ `static_files` (single Range / 304 / 416, traversal-safe)
37
+
38
+ ### Validation
39
+
40
+ - `validator(target, callable)` + `c.req.valid(target)` — msgspec and
41
+ pydantic plug in directly (`msgspec.convert`,
42
+ `TypeAdapter(...).validate_python`); no adapter packages needed
43
+
44
+ ### Runtimes
45
+
46
+ - ASGI (`uvicorn main:app` works as-is; http + websocket + lifespan)
47
+ - Cloudflare Python Workers: `Default = to_workers(app)`
48
+ - AWS Lambda (API Gateway HTTP API v2.0 / Function URLs):
49
+ `handler = to_lambda(app)`
50
+
51
+ ### Performance
52
+
53
+ - Lazy materialization over eager Fetch objects: framework overhead at
54
+ or below Starlette's (static 1.20x, 64 routes 1.93x, stock-middleware
55
+ scenario far ahead) — methodology and caveats in docs/benchmarks.md
56
+ - Optional Rust accelerator (`hayate-accel`, source in `accel/`);
57
+ prebuilt wheels land on PyPI in a future release
58
+
59
+ ### Known limits (documented, enforced by tests)
60
+
61
+ - URL: no IDNA/punycode (non-ASCII hosts raise), no IPv4/IPv6
62
+ canonicalization
63
+ - URLPattern: `{}` groups and `+`/`*` modifiers rejected explicitly
64
+ - Workers adapter: bodies buffered across FFI; streaming bridge and
65
+ on-platform verification tracked in docs/research/cloudflare.md
hayate-0.3.0/DESIGN.md ADDED
@@ -0,0 +1,460 @@
1
+ # hayate 設計ドキュメント
2
+
3
+ > Hono を参考に、「最新の Web 標準に即すこと」だけを設計の中心に置いた Python Web フレームワーク。
4
+ > 本書は実装前の設計判断を観点別にまとめた内部設計メモ(日本語)。公開ドキュメントは英語先行(§15)。
5
+ > 各節は「決定 / 理由 / 却下した代替案」の形を基本とする。
6
+
7
+ ## TL;DR
8
+
9
+ - **コンセプトは一文で「Fetch API を Python の第一級市民にする」**。ユーザーが触る API はすべて WHATWG / IETF 標準の概念に 1:1 対応させ、WSGI / ASGI という Python ローカル規格は実装詳細(アダプタ層)に格下げする。
10
+ - アプリケーションの本体は `fetch(Request) -> Response` の純粋な非同期関数。ASGI サーバー、Cloudflare Python Workers、AWS Lambda はすべて「アダプタ」。
11
+ - ルーティング構文は独自 DSL ではなく **WHATWG URLPattern 標準**を採用(Hono の `/:id` 構文は URLPattern のサブセットなのでそのまま動く)。
12
+ - コアは**ゼロ依存**(標準ライブラリのみ)、async-first、PEP 695 世代の型付け。
13
+ - バリデーション・テンプレート・ORM・DI はコアに入れない。Hono 同様、薄いフックと公式ミドルウェアで対応する。
14
+ - **命名**: hayate(疾風)。Hono(炎)と同じ「自然現象の日本語一語」の系譜で、haya-inc の haya(速)を含む。PyPI 空き確認済み(2026-07-22 時点)。
15
+
16
+ ```python
17
+ from hayate import Hayate, Context
18
+
19
+ app = Hayate()
20
+
21
+ @app.get("/books/:id")
22
+ async def show_book(c: Context):
23
+ book = await find_book(c.req.param("id"))
24
+ if book is None:
25
+ raise HTTPException(404, title="Book not found")
26
+ return c.json(book)
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 1. なぜ作るか
32
+
33
+ ### 1.1 JS 世界で起きたこと(前提認識)
34
+
35
+ Hono の成功の本質は「速さ」ではなく「**Node 独自 API を捨てて Web 標準(Request / Response / URL / Streams)だけに依存した**」ことにある。その結果:
36
+
37
+ - Cloudflare Workers / Deno / Bun / Node / Lambda で**同一コードが動く**(ランタイム側が標準を実装しているから)
38
+ - 学習コストが MDN のドキュメントに外部化される(フレームワーク独自概念が最小)
39
+ - `app.request()` でサーバー起動なしにテストできる(ハンドラが純関数だから)
40
+
41
+ この動きは Ecma TC55(WinterTC)の「Minimum Common Web API」として標準化され、2025 年に第 1 版が発行された。「サーバーランタイムが実装すべき Web API のサブセット」という共通認識が JS 世界には確立している。
42
+
43
+ ### 1.2 Python 世界のギャップ
44
+
45
+ | フレームワーク | リクエスト/レスポンスモデル | 問題 |
46
+ |---|---|---|
47
+ | FastAPI / Starlette | ASGI scope の独自ラッパー | Web 標準と語彙が無関係。`request.url` は独自クラス、Headers 意味論も独自 |
48
+ | Django / Flask | WSGI 世代の独自オブジェクト | 同上 + 同期前提の歴史的経緯 |
49
+ | Litestar / BlackSheep | 独自モデル | 同上 |
50
+ | Cloudflare Python Workers | JS の Request/Response を FFI で露出 | Fetch モデルだが Workers 専用。汎用フレームワークではない |
51
+
52
+ つまり「**JS 開発者が Hono / Workers / Deno で身につけた概念がそのまま通用する Python フレームワーク**」は存在しない。WSGI(2003)/ ASGI(2018)は Python ローカルの規格であり、HTTP そのものの標準(RFC 9110 系)や Fetch 標準とは独立に進化した方言である。
53
+
54
+ ### 1.3 賭け(なぜ今か)
55
+
56
+ - サーバーレス / エッジ / AI エージェントの時代、HTTP ハンドラは「Request → Response の純関数」に収束しつつある。Python にもその形の器が要る。
57
+ - Cloudflare が Python Workers(Pyodide)で Fetch モデルを Python に持ち込み始めた。標準準拠のフレームワークがあれば同一コードでエッジと自前サーバーの両方を狙える。
58
+ - Python 3.12+(PEP 695 generics)、3.13+(free-threading)、3.14(`compression.zstd`)でコアをモダンに保つ条件が揃った。
59
+
60
+ **勝負しない領域**: 生のスループットで Rust 系(Granian/Robyn)には勝てないし勝負しない。土俵は「標準準拠」「移植性」「テスト容易性」「学習コストの外部化」。ただし pure Python として Starlette 同等の性能は必達ラインとする。
61
+
62
+ ---
63
+
64
+ ## 2. 規範とする標準(Normative References)
65
+
66
+ 「Web 標準に対応物がない機能はコアに入れない」を機能追加の門番ルールとする。
67
+
68
+ | 標準 | 発行元 | hayate での対応 |
69
+ |---|---|---|
70
+ | Fetch Standard(Request / Response / Headers / Body) | WHATWG | コアオブジェクトモデル(§4) |
71
+ | URL Standard(URL / URLSearchParams) | WHATWG | `hayate.URL` / `hayate.URLSearchParams` |
72
+ | **URLPattern Standard** | WHATWG | ルーティング構文の唯一の基盤(§6) |
73
+ | Minimum Common Web API(2025 年版) | Ecma TC55 (WinterTC) | 提供 API の選定基準 |
74
+ | HTTP Semantics — RFC 9110 | IETF | メソッド / ステータス / ネゴシエーション / 条件付きリクエスト |
75
+ | HTTP Caching — RFC 9111 | IETF | `cache` ミドルウェア、Cache-Control ヘルパー |
76
+ | HTTP/1.1, /2, /3 — RFC 9112–9114 | IETF | **アダプタ/サーバー層の責務**。コアはワイヤ形式に非依存 |
77
+ | Cookies — RFC 6265bis | IETF | cookie ヘルパー(SameSite / `__Host-` 接頭辞 / 署名) |
78
+ | Problem Details — RFC 9457 | IETF | エラーレスポンスの既定形式(§11) |
79
+ | Structured Field Values — RFC 9651 | IETF | ヘッダーパーサユーティリティ `hayate.http.sfv` |
80
+ | multipart/form-data — RFC 7578 | IETF | `await request.form_data()` |
81
+ | Server-Sent Events | WHATWG HTML | SSE ヘルパー(§10) |
82
+ | WebSocket — RFC 6455 / WebSocket API | IETF / WHATWG | `app.ws()`(v0.2) |
83
+ | Early Hints — RFC 8297 | IETF | 将来(ASGI 拡張の普及待ち) |
84
+ | Trace Context(traceparent) | W3C | `request_id` ミドルウェア / OTel 連携 |
85
+ | Fetch Metadata(Sec-Fetch-*) | W3C | `secure_headers` / CSRF 対策 |
86
+
87
+ ---
88
+
89
+ ## 3. アーキテクチャ
90
+
91
+ ### 3.1 層構造
92
+
93
+ ```
94
+ ユーザーコード: handler / middleware ← Web 標準の語彙だけで書く
95
+ ─────────────────────────────────────
96
+ hayate コア: App / Router / Context
97
+ Request / Response / Headers / URL / URLPattern
98
+ ─────────────────────────────────────
99
+ アダプタ: ASGI | Cloudflare Workers | AWS Lambda | testing
100
+ ─────────────────────────────────────
101
+ 実行環境: uvicorn / granian / hypercorn | workerd | Lambda
102
+ ```
103
+
104
+ ### 3.2 心臓部は `fetch`
105
+
106
+ コアの唯一のエントリポイントは以下のシグネチャ(Hono の `app.fetch` / Workers の `on_fetch` と同型):
107
+
108
+ ```python
109
+ async def fetch(self, request: Request, env: Env | None = None) -> Response: ...
110
+ ```
111
+
112
+ - コアは I/O を一切行わない(ソケットもイベントループ起動も知らない)。**「Request を受け取り Response を返す」以外の責務を持たない**。
113
+ - この純粋性が、テスト容易性(§13)とマルチランタイム(§12)の両方を無料で手に入れる根拠。
114
+ - ASGI ⇔ Fetch の変換はアダプタが行う。ユーザーは `scope` / `receive` / `send` を見ることは一生ない。
115
+
116
+ **却下した代替案**: Starlette 型「ASGI を薄くラップ」— ASGI の語彙がユーザー API に漏れ、Workers 等の非 ASGI ランタイムでコードが再利用できない。本プロジェクトの存在意義と正面衝突するため却下。
117
+
118
+ ---
119
+
120
+ ## 4. コアオブジェクトモデル
121
+
122
+ Fetch 標準の**意味論**(不変条件・状態遷移・エラー条件)に準拠した自前実装。ゼロ依存。
123
+
124
+ ### 4.1 Request
125
+
126
+ ```python
127
+ class Request:
128
+ method: str # 正規化済み大文字
129
+ url: URL # WHATWG URL
130
+ headers: Headers
131
+ body: AsyncIterable[bytes] | None
132
+ signal: AbortSignal # クライアント切断の通知
133
+
134
+ async def bytes(self) -> bytes # Fetch 標準の .bytes()
135
+ async def text(self) -> str
136
+ async def json(self) -> Any
137
+ async def form_data(self) -> FormData # urlencoded / multipart 両対応
138
+ def clone(self) -> Request
139
+ @property
140
+ def body_used(self) -> bool # 消費は一回(Fetch 準拠)
141
+ ```
142
+
143
+ - Body の二重読み取りはエラー(Fetch の `bodyUsed` 意味論)。`clone()` は内部バッファ共有で両方読めるようにする(`tee()` 相当)。
144
+ - ルートパラメータ等のサーバー固有情報は Request を汚さず Context 側に置く(Fetch 標準にないものを標準オブジェクトに生やさない)。
145
+
146
+ ### 4.2 Response
147
+
148
+ ```python
149
+ Response(body=None, status=200, headers=None) # Fetch のコンストラクタ形状
150
+ Response.redirect(location, status=302)
151
+ ```
152
+
153
+ - body に許すのは `None | bytes | str | AsyncIterable[bytes]` の 4 型のみ。ストリーミングは async iterable を渡すだけ(専用 API を増やさない)。
154
+ - Fetch の静的 `Response.json()` は**提供しない**(実装で確定): Python ではボディ読み取りの `await res.json()` と同名のクラスメソッドを共存できない。JSON ビルダーは `c.json()` が担う。
155
+
156
+ ### 4.3 Headers
157
+
158
+ - 大文字小文字非区別・挿入順保持・複数値対応(内部はタプルのリスト)。
159
+ - `get()` はカンマ結合(Fetch 準拠)、`Set-Cookie` だけは結合してはならないので `set_cookie_list()`(Fetch の `getSetCookie()` 対応)を用意。
160
+ - Fetch の immutable guard 意味論を採用: アダプタから来た `request.headers` は不変。
161
+
162
+ ### 4.4 URL / URLSearchParams / URLPattern
163
+
164
+ - **URL**: WHATWG URL 意味論の実用部分集合を自前実装。`urllib.parse` は RFC 3986 系で WHATWG パーサと挙動が異なるため、内部利用に留めて表面には出さない。準拠度は wpt(web-platform-tests)の該当テストベクタをベンダリングして CI で計測する(§13)。
165
+ - **URLPattern**: ルーティングの基盤(§6)。`pathname` 成分を中心に実装し、`exec()` / `test()` を提供。
166
+
167
+ ### 4.5 AbortSignal
168
+
169
+ - `request.signal` でクライアント切断を観測可能にする。実装は asyncio の cancellation とブリッジ。
170
+ - 既定ではハンドラを強制キャンセルしない(Hono と同じ安全側)。切断時中断は `timeout` / `abort_on_disconnect` ミドルウェアで opt-in。
171
+
172
+ ### 4.6 Streams の扱い(意図的な簡略化)
173
+
174
+ WHATWG ReadableStream のフル実装は**しない**。Python には `AsyncIterable[bytes]` という慣用表現があり、ストリーム消費・逐次処理というスペックの目的を満たす。`tee` / `cancel` に相当する最小機能は `clone()` と `signal` に内包する。「標準の意味論に従うが、言語慣用と冗長に競合する API 形状までは輸入しない」— この線引きは §5 の命名方針と同じ原則。
175
+
176
+ ---
177
+
178
+ ## 5. 命名規則: 意味論は標準、表記は PEP 8
179
+
180
+ **決定**: プロパティ / メソッド名は snake_case(`search_params`, `form_data()`)。ただし概念・意味論・状態遷移は Fetch 標準に厳密対応させ、ドキュメントに **camelCase ⇔ snake_case 対応表**を必ず併記する。
181
+
182
+ | Web 標準 | hayate |
183
+ |---|---|
184
+ | `url.searchParams` | `url.search_params` |
185
+ | `await req.arrayBuffer()` / `await req.bytes()` | `await req.bytes()` |
186
+ | `req.bodyUsed` | `req.body_used` |
187
+ | `headers.getSetCookie()` | `headers.set_cookie_list()` |
188
+ | `URLPattern.exec()` | `pattern.exec()`(予約語でないのでそのまま) |
189
+
190
+ **理由**: Pyodide のように camelCase を露出すると Python エコシステム(linter, 慣習)と衝突し「Python のフリをした JS」になる。標準準拠の価値は**名前の字面**ではなく**挙動の予測可能性**(MDN を読めば hayate の挙動がわかる)にある。
191
+
192
+ ---
193
+
194
+ ## 6. ルーティング
195
+
196
+ ### 6.1 構文 = URLPattern 標準
197
+
198
+ 独自ルーティング DSL を**発明しない**。WHATWG URLPattern の pathname 構文をそのまま使う:
199
+
200
+ ```python
201
+ @app.get("/books/:id") # 名前付きパラメータ
202
+ @app.get("/books/:id(\\d+)") # 正規表現制約
203
+ @app.get("/files/*") # ワイルドカード
204
+ @app.get("/users{/:lang}?") # オプショナルグループ
205
+ ```
206
+
207
+ - Hono の `/:id` 構文は URLPattern のサブセットなので、Hono ユーザーの知識がそのまま使える。
208
+ - Node / Deno / Bun / ブラウザに実装済みの標準なので、構文の学習・議論・エッジケース定義をすべて標準に外部化できる。
209
+ - v0 では pathname のみ対象(method × pathname でルーティング)。hostname 等の他成分マッチは需要が出るまでやらない(YAGNI)。
210
+
211
+ ### 6.2 ルーターアルゴリズム
212
+
213
+ - **v0 は単一実装**: 登録時に全ルートをコンパイル → 静的パスは dict 完全一致、動的パスは事前コンパイル済み正規表現の登録順走査(セグメント trie 化はベンチマークが必要性を示したら — 証拠駆動)。
214
+ - Hono の SmartRouter(複数ルーター切替)は**採用しない**。KISS 違反(利用者が選択を迫られる)であり、Python では起動時コンパイル一本で十分。プロファイルで問題が出たら初めて検討する(証拠駆動)。
215
+ - `405 Method Not Allowed`(+ `Allow` ヘッダー)と `HEAD` の自動処理は RFC 9110 準拠でコアが面倒を見る。
216
+
217
+ ---
218
+
219
+ ## 7. アプリケーション API(Context 設計)
220
+
221
+ ### 7.1 ハンドラは `Context` を 1 つ受け取る(Hono 踏襲)
222
+
223
+ ```python
224
+ @app.get("/todos/:id")
225
+ async def show(c: Context):
226
+ todo = await repo.find(c.req.param("id"))
227
+ return c.json(todo) # ヘルパー経由
228
+ # return Response.json(todo) でも良い # 生 Response も常に有効
229
+ ```
230
+
231
+ - `c.req: Request`(+ `c.req.param()` / `c.req.query()` などサーバー側拡張)
232
+ - `c.env`: 実行環境バインディング(Workers の env / ASGI では lifespan state + 環境変数)。ジェネリクスで型付け
233
+ - `c.get()` / `c.set()`: ミドルウェア → ハンドラ間の型付き受け渡し(Hono の `c.var`)
234
+ - `c.header()`: レスポンスヘッダーの積み上げ
235
+ - `c.wait_until(coro)`: レスポンス返却後に完了を保証する後処理の登録。Workers の `ctx.waitUntil` に 1:1 対応し、ASGI ではレスポンス送信後のタスク実行にマップ(エッジ調査より昇格 → docs/research/cloudflare.md §3.4)
236
+ - ヘルパー: `c.json()` `c.text()` `c.html()` `c.body()` `c.redirect()` `c.not_found()`
237
+
238
+ **却下した代替案**:
239
+ - FastAPI 型のシグネチャ検査 + 引数注入 — 魔法が多く、実行パスが複数になる(単一経路原則違反)。バリデーションと DI をコアに引き込む重力が働くのも避けたい。
240
+ - 素の `handler(request) -> Response` 一本 — 最も標準純粋だが、ミドルウェアからの値受け渡しとレスポンスヘルパーの置き場がなくなり、実用 DX で Hono に遠く及ばない。なお `app.mount_fetch(handler)` で素の fetch ハンドラのマウントは可能にする(Workers 互換コードの受け入れ口)。
241
+
242
+ ### 7.2 ミドルウェア: onion モデル
243
+
244
+ ```python
245
+ @app.use
246
+ async def server_timing(c: Context, next):
247
+ start = time.perf_counter()
248
+ await next()
249
+ dur = (time.perf_counter() - start) * 1000
250
+ c.res.headers.append("server-timing", f"app;dur={dur:.1f}")
251
+ ```
252
+
253
+ - `await next()` の前後に処理を書く Koa / Hono スタイル。`app.use(pattern, mw)` でパス限定適用。
254
+ - ミドルウェアもハンドラも同一シグネチャ側で正規化し、内部の実行機構は 1 本(合成された coroutine チェーン)。
255
+
256
+ ### 7.3 標準添付ミドルウェア(バッテリー)
257
+
258
+ Hono のミドルウェア群に対応。すべてゼロ依存で書けるものから:
259
+
260
+ `logger` / `cors`(Fetch 標準の CORS 意味論) / `etag`(RFC 9110 条件付きリクエスト) / `compress`(gzip・deflate は stdlib、zstd は 3.14+ の `compression.zstd`、brotli は extra) / `basic_auth` / `bearer_auth` / `body_limit` / `timeout` / `secure_headers`(CSP, Sec-Fetch-* 検査) / `request_id`(W3C Trace Context) / `cache`(RFC 9111) / `trailing_slash`
261
+
262
+ JWT(HS256 は stdlib の hmac で可、RS256 等は `hayate-jwt` extra)のように暗号依存が出るものはコアから分離する。
263
+
264
+ ---
265
+
266
+ ## 8. 実行モデル(並行性)
267
+
268
+ | 論点 | 決定 | 理由 |
269
+ |---|---|---|
270
+ | 非同期基盤 | **asyncio のみ**(anyio 非依存) | ゼロ依存方針。内部で使うのは await / TaskGroup / timeout の最小サブセットなので、将来 anyio 対応が必要になっても表面 API は変わらない |
271
+ | sync ハンドラ | 許容し、登録時に検出して `asyncio.to_thread` 実行に**正規化**。ただし **ASGI 系アダプタ限定機能**(Pyodide にはスレッドがないため Workers では async ハンドラのみ) | 実行機構は coroutine チェーン 1 本のまま(単一経路)。free-threading 時代に価値が上がる |
272
+ | タイムアウト | `asyncio.timeout` ベースの `timeout` ミドルウェア | 標準機構の再利用 |
273
+ | キャンセル | `request.signal` ⇔ asyncio cancellation のブリッジ。既定は継続、opt-in で中断 | §4.5 |
274
+ | free-threading(3.13t/3.14t) | コアをグローバル可変状態ゼロで設計し、CI に free-threaded ビルドを含める | 「対応」は設計制約であって機能ではない |
275
+ | lifecycle | `app.on_start` / `app.on_stop`(ASGI lifespan にマップ) | Workers など lifespan のない環境ではアダプタが no-op |
276
+
277
+ ---
278
+
279
+ ## 9. 型付けとバリデーション
280
+
281
+ ### 9.1 型付け
282
+
283
+ - Python 3.12+ を最低ラインとし、PEP 695 構文で書く。`Hayate[Env, Vars]` で env / 変数辞書を型付け。
284
+ - pyright / ty の strict モードを CI で強制。**「型が通らない API は設計が悪い」を原則にする**。
285
+ - TS の Hono が誇るパスパラメータのリテラル型推論(`/:id` → `{id: string}`)は Python の型システムでは不可能。**率直に諦めて**ドキュメントに明記し、実行時 validator(下記)に委ねる。中途半端な型スタブ生成などはしない。
286
+
287
+ ### 9.2 バリデーション(コアに入れない)
288
+
289
+ Hono が zod をコアに入れなかったのと同じ判断。コアは**フックだけ**を持つ:
290
+
291
+ ```python
292
+ from hayate.validator import validator
293
+ import msgspec
294
+
295
+ class BookIn(msgspec.Struct):
296
+ title: str
297
+ year: int
298
+
299
+ @app.post("/books", validator("json", BookIn)) # msgspec/pydantic アダプタは extra
300
+ async def create(c: Context):
301
+ book = c.req.valid("json") # 検証済み・型付きの値
302
+ ...
303
+ ```
304
+
305
+ - `hayate-msgspec` / `hayate-pydantic` を別配布(コアのゼロ依存を守る)。
306
+ - 失敗時のレスポンスは RFC 9457 Problem Details + `errors` 拡張メンバー(§11)で統一。
307
+
308
+ ---
309
+
310
+ ## 10. HTTP 機能マップ(RFC → 提供形態)
311
+
312
+ | 機能 | 根拠標準 | 提供形態 |
313
+ |---|---|---|
314
+ | コンテントネゴシエーション | RFC 9110 §12 | `c.req.accepts(...)` ヘルパー |
315
+ | 条件付きリクエスト(ETag / If-None-Match) | RFC 9110 §13 | `etag` ミドルウェア |
316
+ | Range リクエスト | RFC 9110 §14 | 静的ファイルヘルパー(v0.2) |
317
+ | キャッシュ制御 | RFC 9111 | `cache` ミドルウェア + `CacheControl` ビルダー |
318
+ | Cookie(SameSite, `__Host-`) | RFC 6265bis | `c.req.cookies` / `c.set_cookie()`(HMAC 署名は opt-in) |
319
+ | 圧縮(gzip / zstd / brotli) | RFC 1952 / 8878 / 7932 | `compress` ミドルウェア |
320
+ | エラー表現 | RFC 9457 | `HTTPException` → `application/problem+json` |
321
+ | 構造化ヘッダー | RFC 9651 | `hayate.http.sfv` パース/シリアライズ |
322
+ | SSE | WHATWG HTML | `c.event_stream()`(async generator を返すだけ) |
323
+ | WebSocket | RFC 6455 | `app.ws()`(v0.2、WHATWG WebSocket API 形状) |
324
+ | 103 Early Hints | RFC 8297 | 将来。ASGI 拡張の普及を待つ |
325
+
326
+ ## 11. エラー処理
327
+
328
+ ```python
329
+ raise HTTPException(404, title="Book not found", detail=f"id={id} does not exist")
330
+ ```
331
+
332
+ - 既定の直列化は **RFC 9457 Problem Details**(`application/problem+json`)。独自エラー JSON 形式を発明しない。
333
+ - `app.on_error(handler)` / `app.not_found(handler)` で上書き可能(Hono 互換)。
334
+ - 未捕捉例外は 500 + ログ。デバッグモード時のみトレースバックを含める。
335
+
336
+ ## 12. マルチランタイム(アダプタ)
337
+
338
+ | ターゲット | 形態 | 備考 |
339
+ |---|---|---|
340
+ | ASGI(uvicorn / granian / hypercorn) | `app` 自体が ASGI callable(`__call__` がアダプタに委譲) | `uvicorn main:app` がそのまま動く。v0.1 の主力 |
341
+ | テスト | `await app.request(...)` | アダプタなしでコアを直接呼ぶ(§13) |
342
+ | Cloudflare Python Workers | `Default = to_workers(app)`(`WorkerEntrypoint` サブクラスを生成) | JS Request → hayate Request の 1 段変換で **fetch 直結**(公式推奨の FastAPI は ASGI 変換を挟む 2 段構成 — ここが差別化)。`env` は JsProxy を `c.env` に素通し、`ctx.waitUntil` は `c.wait_until()` にマップ。scheduled / Queues / Durable Objects は素の `workers` API と同居(Hono の `{ fetch: app.fetch, scheduled }` と同型)。ベータのため tier-2。詳細: docs/research/cloudflare.md |
343
+ | AWS Lambda(Function URL / API GW) | `handler = to_lambda(app)` | Mangum 相当を内蔵 |
344
+ | WSGI | **提供しない** | 同期世界への逆行。既存ブリッジで足りる |
345
+
346
+ コアがゼロ依存・純粋関数であることが、アダプタを「変換だけの薄い層」に保つ鍵。逆に言えば、コアに I/O や ASGI 概念が漏れた瞬間にこの表は崩れる。
347
+
348
+ ## 13. テスト戦略
349
+
350
+ ### 13.1 ユーザー向け: `app.request()`
351
+
352
+ ```python
353
+ async def test_create_book():
354
+ res = await app.request("/books", method="POST", json={"title": "SICP"})
355
+ assert res.status == 201
356
+ assert (await res.json())["title"] == "SICP"
357
+ ```
358
+
359
+ サーバー起動もテストクライアントライブラリも不要(Hono の DX をそのまま輸入)。fetch モデルの直接的な配当。
360
+
361
+ ### 13.2 フレームワーク自身の準拠テスト
362
+
363
+ - **wpt(web-platform-tests)の URLPattern / URL / Headers テストベクタをベンダリング**して CI で実行。「準拠している」を主張ではなく数値(pass rate)で示す。これが「標準準拠」を名乗る資格の担保。
364
+ - HTTP 意味論(405/Allow、HEAD、条件付きリクエスト等)は RFC の MUST 条項をテスト名に引用したテストスイートを作る。
365
+
366
+ ## 14. パフォーマンス方針(2026-07-22 改訂: 理論限界を攻める)
367
+
368
+ 目標を「Starlette 同等以上」から「**Fetch モデルを保った理論限界**」へ引き上げる。実測(docs/benchmarks.md): 素の ASGI 関数の床は 0.51µs/req、フレームワーク税は hayate/Starlette とも約 4.5µs。CPython はインタープリタであり、税はほぼ「オブジェクト生成数 + 関数呼び出し数 + バイトコード量」に線形 — 削る対象はこの 3 つ。
369
+
370
+ ### 14.1 設計原則
371
+
372
+ 1. **意味論は eager、実体化は lazy**: Fetch 標準は観測的意味論。`c.req.headers` が触られた瞬間に正しければ準拠であり、触られなかった Request 構成要素(Headers 実体・URL・signal・変数辞書)は作らない。
373
+ 2. **起動時に計算できるものは起動時に**: ルートコンパイル、ミドルウェアチェーン合成、定数のエンコード。Pyodide のメモリスナップショットは起動時計算を**ランタイムコストゼロ**にするため、この原則は Workers で二重に効く。
374
+ 3. **wire-native 内部表現**: サーバーは bytes を渡してくる。公開 API は Fetch の文字列意味論を維持し、内部は bytes を保持して**境界で遅延変換**する(ASGI はヘッダー名の小文字化を保証しており再正規化も不要)。
375
+
376
+ ### 14.2 3 層アーキテクチャ
377
+
378
+ | Tier | 内容 | 動作環境 |
379
+ |---|---|---|
380
+ | 0: 参照実装 | pure Python。意味論の正であり、全機能のフォールバック | 全環境(Pyodide 含む) |
381
+ | 1: 内部最適化 | 遅延実体化・bytes 内部表現・事前合成(Tier 0 と同一コードベース) | 全環境 |
382
+ | 2: native accelerator | Rust(maturin)による **opt-in** 拡張(`accel/` = `hayate-accel`)。初弾は compact JSON encoder(dynamic-json を 0.99x → 1.22x に改善)。次候補は計測で選定(multipart、SFV) | native CPython(abi3 wheel、pyo3 0.26)。**Workers は PyEmscripten wheel をサポートするため、emscripten ターゲットもビルドできれば Pyodide でも有効** |
383
+
384
+ Tier 2 の受け入れ条件: ① pure Python フォールバックと挙動同一(同一テストスイートを両実装で実行)、② 意味論コードと加速コードの分離、③ Pyodide の 6 ヶ月ごとの ABI 追随コストを負えること。デメリット(ビルドチェーン複雑化・デバッグ困難化・供給網リスク)は①②で封じ込める。
385
+
386
+ ### 14.3 JIT との関係
387
+
388
+ - CPython 3.13+ の copy-and-patch JIT は実験的・デフォルト無効で、**Pyodide(WASM)では実行時マシンコード生成が原理的に不可**。「JIT 前提の設計」は Cloudflare 対応と正面衝突するため採らない。
389
+ - 代わりに **specializing adaptive interpreter(PEP 659、3.11+ で常時有効、Pyodide でも効く)前提**をコーディング規約とする: 呼び出しサイトの単相化、ホットパスの型安定、動的分岐の削減。この規約は将来 JIT が既定化したときそのまま JIT に有利に働く。
390
+ - ベンチ体制: 床(素の ASGI)との差 =「フレームワーク税」を主指標とし、リグレッションを CI で検出する。
391
+
392
+ ## 15. リポジトリ構成とツールチェーン
393
+
394
+ ```
395
+ hayate/
396
+ pyproject.toml # uv + hatchling, PEP 621
397
+ src/hayate/
398
+ __init__.py # Hayate, Context, Request, Response, HTTPException
399
+ request.py response.py headers.py url.py urlpattern.py
400
+ context.py router.py app.py
401
+ http/ # sfv.py, cookies.py, negotiation.py など RFC 実装
402
+ middleware/ # logger.py, cors.py, etag.py, compress.py, ...
403
+ adapters/ # asgi.py, workers.py, aws.py
404
+ testing.py
405
+ tests/
406
+ wpt/ # ベンダリングした wpt テストベクタ
407
+ docs/
408
+ ```
409
+
410
+ - ツール: uv / ruff(lint + format)/ pyright or ty(strict)/ pytest / GitHub Actions(3.12–3.14 + free-threaded)
411
+ - **言語ポリシー(決定)**: 公開ドキュメント・README・docstring・コード内コメントは**英語を第一言語**とする(OSS として国際的に使われることを想定)。本書のような内部設計メモは日本語で良い。
412
+ - ライセンス: MIT(Hono と同じ)を推奨
413
+
414
+ ## 16. スコープ外(YAGNI リスト)
415
+
416
+ v1 まで**やらない**と明示するもの:
417
+
418
+ | やらないこと | 理由 |
419
+ |---|---|
420
+ | テンプレートエンジン / JSX 相当 | `c.html()` に文字列を渡せば足りる。API ファースト |
421
+ | ORM / DB 統合 / DI コンテナ | フレームワークの仕事ではない |
422
+ | fetch クライアント(`hayate.fetch()`) | サーバー側が先。需要(BFF / プロキシ用途)の証拠が出たら検討 |
423
+ | OpenAPI 自動生成 | validator エコシステムが固まる v0.3 以降に判断 |
424
+ | SmartRouter / 複数ルーター | 単一実装で十分(§6.2) |
425
+ | WSGI アダプタ / Python 3.11 以前 | 過去との互換はこのプロジェクトの目的に無い |
426
+ | HTTP/2 Server Push | 標準側で事実上廃止済み |
427
+
428
+ ## 17. リスクと対応
429
+
430
+ | リスク | 対応 |
431
+ |---|---|
432
+ | WHATWG URL / URLPattern 完全準拠の泥沼(仕様が巨大) | 「実用部分集合」を最初に明文化し、wpt pass rate を公開して準拠範囲を誠実に示す |
433
+ | 性能で Rust 系に見劣り | 土俵を明示(§1.3)。Starlette 同等を CI で担保 |
434
+ | 「snake_case は標準じゃない」批判 | §5 の対応表と設計原則文書で先回りして立場を宣言 |
435
+ | Python Workers がベータのまま停滞 | 2025 年時点で DO / Workflows / scheduled の Python 対応、メモリスナップショットで cold start 10 倍改善と進展は前向き(docs/research/cloudflare.md)。それでも Workers アダプタは tier-2 とし、コア価値(ASGI + テスト DX + 標準準拠)は単独で成立する設計にしてある |
436
+ | PyPI 名 `hayate` が公開前に第三者に取られる | private 開始のため公開まで名前を確保できない(PyPI は placeholder 登録を規約で禁止)。v0.1 が形になり次第、最小実装で早期に 0.0.x を公開して確保する |
437
+
438
+ ## 18. 決定事項・マイルストーン・未決事項
439
+
440
+ ### 決定済み(2026-07-22)
441
+
442
+ | 項目 | 決定 |
443
+ |---|---|
444
+ | 名前 | **hayate**(疾風)。配布名 = import 名 = `hayate`、アプリクラスは `Hayate`。GitHub リポジトリも `haya-inc/para` → `haya-inc/hayate` へのリネームを推奨 |
445
+ | ドキュメント言語 | **英語先行**(公開ドキュメント・README・docstring・コード内コメント)。内部設計メモは日本語可 |
446
+ | 公開戦略 | **private で開始**。v0.1 完成時に公開(PyPI 名確保を兼ねる)を判断 |
447
+ | 最低 Python | **3.12**(PEP 695 対応と採用の広さのバランス。Pyodide 現行の 3.12/3.13 系とも整合) |
448
+
449
+ ### マイルストーン
450
+
451
+ | 版 | 内容 | 受け入れ基準 |
452
+ |---|---|---|
453
+ | **v0.1 コア** | Headers → URL/URLSearchParams → URLPattern → Request/Response → Context/Router/App → ASGI アダプタ → testing → 初期ミドルウェア(logger, cors, etag, basic_auth, compress) | TODO API がテスト付きで書ける。wpt サブセット合格。Starlette 比ベンチ公開 |
454
+ | **v0.2** | SSE / WebSocket / secure_headers / 署名 cookie / body_limit / timeout / 静的ファイル(Range, 304, 416)/ cache(マイクロキャッシュ + Cache-Control/Age)— **すべて実装済み** | リアルタイムチャットのサンプルが動く ✅(examples/chat.py + tests/test_chat_example.py) |
455
+ | **v0.3** | validator フック(実装済み — callable プロトコルにより msgspec / pydantic が**アダプタパッケージなしで直結**、専用 extra は YAGNI で不要と判明)/ Workers アダプタ(実装済み、モックテスト済み)/ Lambda アダプタ(実装済み、API GW v2.0)/ ドキュメントサイト(未) | 同一アプリが uvicorn と Workers で無変更動作 — 残タスクは Pyodide 実機検証(docs/research/cloudflare.md §5)とストリーミングブリッジ |
456
+ | v1.0 | API 凍結、OpenAPI 等は証拠駆動で判断 | — |
457
+
458
+ ### 未決事項(要判断)
459
+
460
+ 現時点でなし。次の判断ポイントは v0.1 完成時の「公開タイミング」(§17 の PyPI 名確保と連動)。