typact 0.2.2a1__tar.gz → 0.2.2a2__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 (63) hide show
  1. {typact-0.2.2a1 → typact-0.2.2a2}/CHANGELOG.md +8 -1
  2. {typact-0.2.2a1 → typact-0.2.2a2}/CONTRIBUTING.md +2 -2
  3. {typact-0.2.2a1 → typact-0.2.2a2}/MANIFEST.in +1 -1
  4. typact-0.2.2a2/PKG-INFO +324 -0
  5. typact-0.2.2a2/README.md +292 -0
  6. typact-0.2.2a1/README.md → typact-0.2.2a2/README.zh-CN.md +4 -2
  7. {typact-0.2.2a1 → typact-0.2.2a2}/pyproject.toml +15 -5
  8. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/http_client.py +5 -3
  9. typact-0.2.2a2/src/typact.egg-info/PKG-INFO +324 -0
  10. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/SOURCES.txt +1 -0
  11. {typact-0.2.2a1 → typact-0.2.2a2}/tests/test_runtime_integration.py +16 -3
  12. typact-0.2.2a1/PKG-INFO +0 -714
  13. typact-0.2.2a1/src/typact.egg-info/PKG-INFO +0 -714
  14. {typact-0.2.2a1 → typact-0.2.2a2}/LICENSE +0 -0
  15. {typact-0.2.2a1 → typact-0.2.2a2}/RELEASING.md +0 -0
  16. {typact-0.2.2a1 → typact-0.2.2a2}/setup.cfg +0 -0
  17. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/__init__.py +0 -0
  18. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/__init__.py +0 -0
  19. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/base.py +0 -0
  20. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/body.py +0 -0
  21. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/cookie.py +0 -0
  22. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/file.py +0 -0
  23. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/form.py +0 -0
  24. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/header.py +0 -0
  25. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/path.py +0 -0
  26. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/query.py +0 -0
  27. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/__init__.py +0 -0
  28. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/multipart_builder.py +0 -0
  29. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/request_builder.py +0 -0
  30. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/url_builder.py +0 -0
  31. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/__init__.py +0 -0
  32. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/decorator.py +0 -0
  33. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/metadata.py +0 -0
  34. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/__init__.py +0 -0
  35. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/file_converter.py +0 -0
  36. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/json_converter.py +0 -0
  37. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/response_converter.py +0 -0
  38. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/sse_converter.py +0 -0
  39. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/stream_converter.py +0 -0
  40. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/__init__.py +0 -0
  41. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/errors.py +0 -0
  42. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/events.py +0 -0
  43. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/retry.py +0 -0
  44. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/types.py +0 -0
  45. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/__init__.py +0 -0
  46. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/auth.py +0 -0
  47. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/base.py +0 -0
  48. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/log.py +0 -0
  49. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/trace.py +0 -0
  50. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/py.typed +0 -0
  51. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/__init__.py +0 -0
  52. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/aiohttp.py +0 -0
  53. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/base.py +0 -0
  54. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/httpx.py +0 -0
  55. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/mock.py +0 -0
  56. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/urllib.py +0 -0
  57. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/testing/__init__.py +0 -0
  58. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/testing/mock_runtime.py +0 -0
  59. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/dependency_links.txt +0 -0
  60. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/requires.txt +0 -0
  61. {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/top_level.txt +0 -0
  62. {typact-0.2.2a1 → typact-0.2.2a2}/tests/test_http_client_request.py +0 -0
  63. {typact-0.2.2a1 → typact-0.2.2a2}/tests/test_response_retry.py +0 -0
@@ -1,9 +1,16 @@
1
1
  # Changelog
2
2
 
3
- 本文件区分未发布改动与已在 PyPI 发布的版本。主分支版本目前为 0.2.2a1,尚未发布。
3
+ 本文件区分未发布改动与已在 PyPI 发布的版本。当前预发布版本为 0.2.2a2,正式版 0.2.2 尚未发布。
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.2a2] - 2026-09-22
8
+
9
+ ### 变更
10
+
11
+ - 最低 Python 版本从 3.13 下调至 3.10,并在 Linux 与 Windows CI 中覆盖 Python 3.10 至 3.14。
12
+ - 英文 README 和文档成为默认入口,原中文 README 保留为 `README.zh-CN.md`。
13
+
7
14
  ## [0.2.2a1] - 2026-09-14
8
15
 
9
16
  ### 新增
@@ -4,14 +4,14 @@ Typact 是开源 HTTP 客户端库。设计变更应考虑通用场景、公共
4
4
 
5
5
  ## 开发环境
6
6
 
7
- Python 3.13 或 3.14;使用 uv 管理环境:
7
+ Python 3.10 至 3.14;使用 uv 管理环境:
8
8
 
9
9
  ```sh
10
10
  uv sync --all-extras --group dev
11
11
  uv run --no-sync pytest -q
12
12
  ```
13
13
 
14
- CI 在 Windows 和 Linux 上运行上述两个 Python 版本的完整测试,包含 urllib、httpx 和 aiohttp。Python 元数据允许更新版本,但未经 CI 验证的版本不在当前保证范围内。
14
+ CI 在 Windows 和 Linux 上运行上述 Python 版本的完整测试,包含 urllib、httpx 和 aiohttp。Python 元数据允许更新版本,但未经 CI 验证的版本不在当前保证范围内。
15
15
 
16
16
  文档使用 Node.js 22,版本记录在 `docs-site/.node-version`:
17
17
 
@@ -1,4 +1,4 @@
1
- include README.md LICENSE CHANGELOG.md CONTRIBUTING.md RELEASING.md
1
+ include README.md README.zh-CN.md LICENSE CHANGELOG.md CONTRIBUTING.md RELEASING.md
2
2
  recursive-include src/typact *.py py.typed
3
3
  recursive-include tests *.py
4
4
  prune docs-site
@@ -0,0 +1,324 @@
1
+ Metadata-Version: 2.4
2
+ Name: typact
3
+ Version: 0.2.2a2
4
+ Summary: Build production-ready Python API clients with FastAPI-style declarations.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://typact.zzq.jl.cn
7
+ Project-URL: Documentation, https://typact.zzq.jl.cn/docs/getting-started/
8
+ Project-URL: GitHub, https://github.com/zzqjlcn/typact
9
+ Project-URL: AtomGit, https://atomgit.com/zhangzhanqi/typact
10
+ Project-URL: Issues, https://github.com/zzqjlcn/typact/issues
11
+ Project-URL: Changelog, https://github.com/zzqjlcn/typact/blob/main/CHANGELOG.md
12
+ Keywords: http,api-client,async,typing,declarative,fastapi,pydantic,sse,retry
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Framework :: AsyncIO
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: pydantic>=2.13.4
27
+ Provides-Extra: aiohttp
28
+ Requires-Dist: aiohttp>=3.14.1; extra == "aiohttp"
29
+ Provides-Extra: httpx
30
+ Requires-Dist: httpx>=0.28.1; extra == "httpx"
31
+ Dynamic: license-file
32
+
33
+ # <img src="https://typact.zzq.jl.cn/favicon.svg" alt="Typact" width="28" height="28" style="vertical-align: middle;"/> Typact
34
+
35
+ [![Documentation](https://img.shields.io/badge/docs-typact.zzq.jl.cn-blue)](https://typact.zzq.jl.cn)
36
+ [![PyPI](https://img.shields.io/pypi/v/typact)](https://pypi.org/project/typact/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/typact)](https://pypi.org/project/typact/)
38
+ [![CI](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml/badge.svg)](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml)
39
+ [![License](https://img.shields.io/pypi/l/typact)](https://github.com/zzqjlcn/typact/blob/main/LICENSE)
40
+
41
+ [Documentation](https://typact.zzq.jl.cn) · [PyPI](https://pypi.org/project/typact/) · [中文 README](https://github.com/zzqjlcn/typact/blob/main/README.zh-CN.md) · [Changelog](https://github.com/zzqjlcn/typact/blob/main/CHANGELOG.md)
42
+
43
+ **Build production-ready Python API clients with FastAPI-style declarations.**
44
+
45
+ Typact turns an annotated Python function into an executable HTTP contract. Declare the request parameters and return type once; Typact handles request building, transport, response validation, retries, authentication, streaming, and test doubles.
46
+
47
+ ```python
48
+ from pydantic import BaseModel
49
+
50
+ from typact import HttpClient, Path
51
+
52
+
53
+ class User(BaseModel):
54
+ id: int
55
+ name: str
56
+
57
+
58
+ client = HttpClient("https://api.example.com")
59
+
60
+
61
+ @client.get("/users/{user_id}")
62
+ async def get_user(user_id: int = Path()) -> User:
63
+ pass
64
+
65
+
66
+ user = await get_user(1)
67
+ ```
68
+
69
+ ## Why Typact?
70
+
71
+ Handwritten clients tend to repeat the same plumbing: assemble URLs and headers, serialize bodies, check status codes, validate JSON, retry transient failures, refresh tokens, and mock the network in tests. Typact keeps that behavior behind one typed contract.
72
+
73
+ - **FastAPI-style declarations** with `Path`, `Query`, `Header`, `Cookie`, `Body`, `Form`, and `File`.
74
+ - **Validated return types** powered by Pydantic v2 `TypeAdapter`.
75
+ - **Production request policies** including timeouts, exponential backoff, response-aware retries, and token refresh.
76
+ - **Streaming support** for raw byte/text streams and typed Server-Sent Events.
77
+ - **Pluggable transports** using the standard library, httpx, aiohttp, or a custom Runtime.
78
+ - **Deterministic tests** through a Mock Runtime that records the request Typact built.
79
+ - **Small core install** with Pydantic as the only required dependency.
80
+
81
+ Typact is async-first and targets Python 3.10 through 3.14.
82
+
83
+ ## Installation
84
+
85
+ The default Runtime uses Python's standard library:
86
+
87
+ ```bash
88
+ pip install typact
89
+ ```
90
+
91
+ Install an optional transport when you need httpx or aiohttp:
92
+
93
+ ```bash
94
+ pip install "typact[httpx]"
95
+ pip install "typact[aiohttp]"
96
+ ```
97
+
98
+ ## Quick start
99
+
100
+ ```python
101
+ import asyncio
102
+
103
+ from pydantic import BaseModel, ConfigDict, Field
104
+
105
+ from typact import Body, HttpClient, Path, Query
106
+
107
+
108
+ class Todo(BaseModel):
109
+ model_config = ConfigDict(populate_by_name=True)
110
+
111
+ user_id: int = Field(alias="userId")
112
+ id: int | None = None
113
+ title: str
114
+ completed: bool
115
+
116
+
117
+ client = HttpClient("https://jsonplaceholder.typicode.com")
118
+
119
+
120
+ @client.get("/todos/{todo_id}")
121
+ async def get_todo(todo_id: int = Path()) -> Todo:
122
+ pass
123
+
124
+
125
+ @client.get("/todos")
126
+ async def list_todos(user_id: int = Query(alias="userId")) -> list[Todo]:
127
+ pass
128
+
129
+
130
+ @client.post("/todos")
131
+ async def create_todo(todo: Todo = Body()) -> Todo:
132
+ pass
133
+
134
+
135
+ async def main():
136
+ todo = await get_todo(1)
137
+ todos = await list_todos(user_id=1)
138
+ created = await create_todo(
139
+ Todo(user_id=1, title="Try Typact", completed=False)
140
+ )
141
+ print(todo, todos[0], created)
142
+ await client.close()
143
+
144
+
145
+ asyncio.run(main())
146
+ ```
147
+
148
+ ## Production request policies
149
+
150
+ Configure shared timeout and retry behavior on the client, then override it per route when an endpoint needs different semantics:
151
+
152
+ ```python
153
+ from typact import HttpClient, Path, RetryConfig
154
+
155
+
156
+ client = HttpClient(
157
+ "https://api.example.com",
158
+ timeout=10,
159
+ retry_config=RetryConfig(max_retries=3, initial_delay=0.5),
160
+ )
161
+
162
+
163
+ @client.get(
164
+ "/reports/{report_id}",
165
+ timeout=60,
166
+ retry_config=RetryConfig(max_retries=2),
167
+ )
168
+ async def get_report(report_id: int = Path()) -> dict:
169
+ pass
170
+ ```
171
+
172
+ Retries are disabled by default. When enabled, the default policy retries network and timeout failures plus `429`, `502`, `503`, and `504` responses for idempotent methods.
173
+
174
+ Typact can also retry a successful HTTP response that carries a transient business error:
175
+
176
+ ```python
177
+ from typact import Response, RetryConfig, default_should_retry_response
178
+
179
+
180
+ def should_retry(response: Response) -> bool:
181
+ if default_should_retry_response(response):
182
+ return True
183
+ data = response.json()
184
+ return (
185
+ response.status_code == 200
186
+ and isinstance(data, dict)
187
+ and data.get("code") in {"SYSTEM_BUSY", "RATE_LIMITED"}
188
+ )
189
+
190
+
191
+ client = HttpClient(
192
+ "https://api.example.com",
193
+ retry_config=RetryConfig(
194
+ max_retries=3,
195
+ should_retry_response=should_retry,
196
+ ),
197
+ )
198
+ ```
199
+
200
+ See the [Runtime guide](https://typact.zzq.jl.cn/docs/runtime/) for inheritance rules, streaming behavior, and lifecycle events.
201
+
202
+ ## Authentication and cross-cutting behavior
203
+
204
+ Interceptors keep authentication, logging, and tracing out of endpoint declarations:
205
+
206
+ ```python
207
+ from typact import BearerTokenInterceptor, HttpClient, InterceptorChain
208
+
209
+
210
+ client = HttpClient(
211
+ "https://api.example.com",
212
+ interceptor_chain=InterceptorChain(
213
+ request_interceptors=[BearerTokenInterceptor("your-token")],
214
+ ),
215
+ )
216
+ ```
217
+
218
+ Built-in interceptors include bearer tokens, refreshable bearer tokens, API keys, trace IDs, and logging.
219
+
220
+ ## Files and streaming
221
+
222
+ Upload multipart files with `FileData`:
223
+
224
+ ```python
225
+ from typact import File, FileData
226
+
227
+
228
+ @client.post("/upload")
229
+ async def upload(file: FileData = File()) -> dict:
230
+ pass
231
+
232
+
233
+ await upload(
234
+ FileData(
235
+ content=b"hello",
236
+ filename="hello.txt",
237
+ content_type="text/plain",
238
+ )
239
+ )
240
+ ```
241
+
242
+ Use `AsyncIterator[bytes]` or `AsyncIterator[str]` for raw streams. Other item types are decoded from Server-Sent Events:
243
+
244
+ ```python
245
+ from collections.abc import AsyncIterator
246
+
247
+
248
+ @client.get("/events")
249
+ async def events() -> AsyncIterator[dict]:
250
+ pass
251
+
252
+
253
+ async for event in events():
254
+ print(event)
255
+ ```
256
+
257
+ Streaming requires the httpx or aiohttp Runtime. Retries stop after the first chunk has been delivered, preventing duplicate data.
258
+
259
+ ## Pluggable Runtimes
260
+
261
+ The default `UrllibRuntime` needs no extra HTTP dependency. Existing httpx and aiohttp clients can be injected when you need their connection settings or ecosystem integrations:
262
+
263
+ ```python
264
+ import httpx
265
+
266
+ from typact import HttpClient, HttpxRuntime
267
+
268
+
269
+ client = HttpClient(
270
+ "https://api.example.com",
271
+ client_runtime=HttpxRuntime(httpx.AsyncClient(timeout=30)),
272
+ )
273
+ ```
274
+
275
+ Endpoint declarations and return types stay unchanged when the Runtime changes.
276
+
277
+ ## Testing without a server
278
+
279
+ The Mock Runtime replaces the transport boundary and records every built request:
280
+
281
+ ```python
282
+ from typact import HttpClient, MockRuntime, Query
283
+
284
+
285
+ runtime = MockRuntime()
286
+ runtime.add_response(
287
+ "GET",
288
+ "https://api.example.com/items",
289
+ json_data={"items": []},
290
+ )
291
+ client = HttpClient("https://api.example.com", client_runtime=runtime)
292
+
293
+
294
+ @client.get("/items")
295
+ async def list_items(page: int = Query(1)) -> dict:
296
+ pass
297
+
298
+
299
+ result = await list_items(page=2)
300
+ assert result == {"items": []}
301
+ assert runtime.requests[0].params == {"page": 2}
302
+ ```
303
+
304
+ ## Documentation and examples
305
+
306
+ - [Getting started](https://typact.zzq.jl.cn/docs/getting-started/)
307
+ - [Parameter annotations](https://typact.zzq.jl.cn/docs/annotations/)
308
+ - [Runtimes, retries, streaming, and events](https://typact.zzq.jl.cn/docs/runtime/)
309
+ - [Interceptors](https://typact.zzq.jl.cn/docs/interceptors/)
310
+ - [Testing](https://typact.zzq.jl.cn/docs/testing/)
311
+ - [`examples/`](https://github.com/zzqjlcn/typact/tree/main/examples)
312
+
313
+ ## Development
314
+
315
+ ```bash
316
+ uv sync --all-extras --group dev
317
+ uv run --no-sync pytest -q
318
+ ```
319
+
320
+ Design changes must account for public API clarity, Runtime consistency, backward compatibility, and migration cost. See [CONTRIBUTING.md](https://github.com/zzqjlcn/typact/blob/main/CONTRIBUTING.md) and [RELEASING.md](https://github.com/zzqjlcn/typact/blob/main/RELEASING.md) before contributing or publishing.
321
+
322
+ ## License
323
+
324
+ Typact is released under the [MIT License](https://github.com/zzqjlcn/typact/blob/main/LICENSE).
@@ -0,0 +1,292 @@
1
+ # <img src="https://typact.zzq.jl.cn/favicon.svg" alt="Typact" width="28" height="28" style="vertical-align: middle;"/> Typact
2
+
3
+ [![Documentation](https://img.shields.io/badge/docs-typact.zzq.jl.cn-blue)](https://typact.zzq.jl.cn)
4
+ [![PyPI](https://img.shields.io/pypi/v/typact)](https://pypi.org/project/typact/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/typact)](https://pypi.org/project/typact/)
6
+ [![CI](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml/badge.svg)](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml)
7
+ [![License](https://img.shields.io/pypi/l/typact)](https://github.com/zzqjlcn/typact/blob/main/LICENSE)
8
+
9
+ [Documentation](https://typact.zzq.jl.cn) · [PyPI](https://pypi.org/project/typact/) · [中文 README](https://github.com/zzqjlcn/typact/blob/main/README.zh-CN.md) · [Changelog](https://github.com/zzqjlcn/typact/blob/main/CHANGELOG.md)
10
+
11
+ **Build production-ready Python API clients with FastAPI-style declarations.**
12
+
13
+ Typact turns an annotated Python function into an executable HTTP contract. Declare the request parameters and return type once; Typact handles request building, transport, response validation, retries, authentication, streaming, and test doubles.
14
+
15
+ ```python
16
+ from pydantic import BaseModel
17
+
18
+ from typact import HttpClient, Path
19
+
20
+
21
+ class User(BaseModel):
22
+ id: int
23
+ name: str
24
+
25
+
26
+ client = HttpClient("https://api.example.com")
27
+
28
+
29
+ @client.get("/users/{user_id}")
30
+ async def get_user(user_id: int = Path()) -> User:
31
+ pass
32
+
33
+
34
+ user = await get_user(1)
35
+ ```
36
+
37
+ ## Why Typact?
38
+
39
+ Handwritten clients tend to repeat the same plumbing: assemble URLs and headers, serialize bodies, check status codes, validate JSON, retry transient failures, refresh tokens, and mock the network in tests. Typact keeps that behavior behind one typed contract.
40
+
41
+ - **FastAPI-style declarations** with `Path`, `Query`, `Header`, `Cookie`, `Body`, `Form`, and `File`.
42
+ - **Validated return types** powered by Pydantic v2 `TypeAdapter`.
43
+ - **Production request policies** including timeouts, exponential backoff, response-aware retries, and token refresh.
44
+ - **Streaming support** for raw byte/text streams and typed Server-Sent Events.
45
+ - **Pluggable transports** using the standard library, httpx, aiohttp, or a custom Runtime.
46
+ - **Deterministic tests** through a Mock Runtime that records the request Typact built.
47
+ - **Small core install** with Pydantic as the only required dependency.
48
+
49
+ Typact is async-first and targets Python 3.10 through 3.14.
50
+
51
+ ## Installation
52
+
53
+ The default Runtime uses Python's standard library:
54
+
55
+ ```bash
56
+ pip install typact
57
+ ```
58
+
59
+ Install an optional transport when you need httpx or aiohttp:
60
+
61
+ ```bash
62
+ pip install "typact[httpx]"
63
+ pip install "typact[aiohttp]"
64
+ ```
65
+
66
+ ## Quick start
67
+
68
+ ```python
69
+ import asyncio
70
+
71
+ from pydantic import BaseModel, ConfigDict, Field
72
+
73
+ from typact import Body, HttpClient, Path, Query
74
+
75
+
76
+ class Todo(BaseModel):
77
+ model_config = ConfigDict(populate_by_name=True)
78
+
79
+ user_id: int = Field(alias="userId")
80
+ id: int | None = None
81
+ title: str
82
+ completed: bool
83
+
84
+
85
+ client = HttpClient("https://jsonplaceholder.typicode.com")
86
+
87
+
88
+ @client.get("/todos/{todo_id}")
89
+ async def get_todo(todo_id: int = Path()) -> Todo:
90
+ pass
91
+
92
+
93
+ @client.get("/todos")
94
+ async def list_todos(user_id: int = Query(alias="userId")) -> list[Todo]:
95
+ pass
96
+
97
+
98
+ @client.post("/todos")
99
+ async def create_todo(todo: Todo = Body()) -> Todo:
100
+ pass
101
+
102
+
103
+ async def main():
104
+ todo = await get_todo(1)
105
+ todos = await list_todos(user_id=1)
106
+ created = await create_todo(
107
+ Todo(user_id=1, title="Try Typact", completed=False)
108
+ )
109
+ print(todo, todos[0], created)
110
+ await client.close()
111
+
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ ## Production request policies
117
+
118
+ Configure shared timeout and retry behavior on the client, then override it per route when an endpoint needs different semantics:
119
+
120
+ ```python
121
+ from typact import HttpClient, Path, RetryConfig
122
+
123
+
124
+ client = HttpClient(
125
+ "https://api.example.com",
126
+ timeout=10,
127
+ retry_config=RetryConfig(max_retries=3, initial_delay=0.5),
128
+ )
129
+
130
+
131
+ @client.get(
132
+ "/reports/{report_id}",
133
+ timeout=60,
134
+ retry_config=RetryConfig(max_retries=2),
135
+ )
136
+ async def get_report(report_id: int = Path()) -> dict:
137
+ pass
138
+ ```
139
+
140
+ Retries are disabled by default. When enabled, the default policy retries network and timeout failures plus `429`, `502`, `503`, and `504` responses for idempotent methods.
141
+
142
+ Typact can also retry a successful HTTP response that carries a transient business error:
143
+
144
+ ```python
145
+ from typact import Response, RetryConfig, default_should_retry_response
146
+
147
+
148
+ def should_retry(response: Response) -> bool:
149
+ if default_should_retry_response(response):
150
+ return True
151
+ data = response.json()
152
+ return (
153
+ response.status_code == 200
154
+ and isinstance(data, dict)
155
+ and data.get("code") in {"SYSTEM_BUSY", "RATE_LIMITED"}
156
+ )
157
+
158
+
159
+ client = HttpClient(
160
+ "https://api.example.com",
161
+ retry_config=RetryConfig(
162
+ max_retries=3,
163
+ should_retry_response=should_retry,
164
+ ),
165
+ )
166
+ ```
167
+
168
+ See the [Runtime guide](https://typact.zzq.jl.cn/docs/runtime/) for inheritance rules, streaming behavior, and lifecycle events.
169
+
170
+ ## Authentication and cross-cutting behavior
171
+
172
+ Interceptors keep authentication, logging, and tracing out of endpoint declarations:
173
+
174
+ ```python
175
+ from typact import BearerTokenInterceptor, HttpClient, InterceptorChain
176
+
177
+
178
+ client = HttpClient(
179
+ "https://api.example.com",
180
+ interceptor_chain=InterceptorChain(
181
+ request_interceptors=[BearerTokenInterceptor("your-token")],
182
+ ),
183
+ )
184
+ ```
185
+
186
+ Built-in interceptors include bearer tokens, refreshable bearer tokens, API keys, trace IDs, and logging.
187
+
188
+ ## Files and streaming
189
+
190
+ Upload multipart files with `FileData`:
191
+
192
+ ```python
193
+ from typact import File, FileData
194
+
195
+
196
+ @client.post("/upload")
197
+ async def upload(file: FileData = File()) -> dict:
198
+ pass
199
+
200
+
201
+ await upload(
202
+ FileData(
203
+ content=b"hello",
204
+ filename="hello.txt",
205
+ content_type="text/plain",
206
+ )
207
+ )
208
+ ```
209
+
210
+ Use `AsyncIterator[bytes]` or `AsyncIterator[str]` for raw streams. Other item types are decoded from Server-Sent Events:
211
+
212
+ ```python
213
+ from collections.abc import AsyncIterator
214
+
215
+
216
+ @client.get("/events")
217
+ async def events() -> AsyncIterator[dict]:
218
+ pass
219
+
220
+
221
+ async for event in events():
222
+ print(event)
223
+ ```
224
+
225
+ Streaming requires the httpx or aiohttp Runtime. Retries stop after the first chunk has been delivered, preventing duplicate data.
226
+
227
+ ## Pluggable Runtimes
228
+
229
+ The default `UrllibRuntime` needs no extra HTTP dependency. Existing httpx and aiohttp clients can be injected when you need their connection settings or ecosystem integrations:
230
+
231
+ ```python
232
+ import httpx
233
+
234
+ from typact import HttpClient, HttpxRuntime
235
+
236
+
237
+ client = HttpClient(
238
+ "https://api.example.com",
239
+ client_runtime=HttpxRuntime(httpx.AsyncClient(timeout=30)),
240
+ )
241
+ ```
242
+
243
+ Endpoint declarations and return types stay unchanged when the Runtime changes.
244
+
245
+ ## Testing without a server
246
+
247
+ The Mock Runtime replaces the transport boundary and records every built request:
248
+
249
+ ```python
250
+ from typact import HttpClient, MockRuntime, Query
251
+
252
+
253
+ runtime = MockRuntime()
254
+ runtime.add_response(
255
+ "GET",
256
+ "https://api.example.com/items",
257
+ json_data={"items": []},
258
+ )
259
+ client = HttpClient("https://api.example.com", client_runtime=runtime)
260
+
261
+
262
+ @client.get("/items")
263
+ async def list_items(page: int = Query(1)) -> dict:
264
+ pass
265
+
266
+
267
+ result = await list_items(page=2)
268
+ assert result == {"items": []}
269
+ assert runtime.requests[0].params == {"page": 2}
270
+ ```
271
+
272
+ ## Documentation and examples
273
+
274
+ - [Getting started](https://typact.zzq.jl.cn/docs/getting-started/)
275
+ - [Parameter annotations](https://typact.zzq.jl.cn/docs/annotations/)
276
+ - [Runtimes, retries, streaming, and events](https://typact.zzq.jl.cn/docs/runtime/)
277
+ - [Interceptors](https://typact.zzq.jl.cn/docs/interceptors/)
278
+ - [Testing](https://typact.zzq.jl.cn/docs/testing/)
279
+ - [`examples/`](https://github.com/zzqjlcn/typact/tree/main/examples)
280
+
281
+ ## Development
282
+
283
+ ```bash
284
+ uv sync --all-extras --group dev
285
+ uv run --no-sync pytest -q
286
+ ```
287
+
288
+ Design changes must account for public API clarity, Runtime consistency, backward compatibility, and migration cost. See [CONTRIBUTING.md](https://github.com/zzqjlcn/typact/blob/main/CONTRIBUTING.md) and [RELEASING.md](https://github.com/zzqjlcn/typact/blob/main/RELEASING.md) before contributing or publishing.
289
+
290
+ ## License
291
+
292
+ Typact is released under the [MIT License](https://github.com/zzqjlcn/typact/blob/main/LICENSE).
@@ -5,6 +5,8 @@
5
5
  [![Python](https://img.shields.io/pypi/pyversions/typact)](https://pypi.org/project/typact/)
6
6
  [![License](https://img.shields.io/pypi/l/typact)](LICENSE)
7
7
 
8
+ [English](README.md) · [文档](https://typact.zzq.jl.cn) · [PyPI](https://pypi.org/project/typact/) · [变更记录](CHANGELOG.md)
9
+
8
10
  源码托管:[AtomGit](https://atomgit.com/zhangzhanqi/typact) · [GitHub](https://github.com/zzqjlcn/typact)。可前往仓库浏览源码、获取代码和查看提交记录。
9
11
 
10
12
  **Typact** 是一个面向 Python 的声明式、类型安全、可插拔 Runtime 的 HTTP 服务调用框架。
@@ -51,7 +53,7 @@ user = await get_user(1)
51
53
  Typact 要求:
52
54
 
53
55
  ```text
54
- Python >= 3.13
56
+ Python 3.10–3.14
55
57
  ```
56
58
 
57
59
  核心安装只依赖 `pydantic`,默认 Runtime 使用 Python 标准库 `urllib`:
@@ -664,7 +666,7 @@ uv run python -m compileall src examples
664
666
  ## 路线图
665
667
 
666
668
  - 已支持:超时与 Retry / Backoff、SSE / Stream 响应转换。
667
- - 主分支未发布:生命周期事件、响应内容重试;详见 [变更记录](CHANGELOG.md)。
669
+ - 0.2.2a1 预发布版:生命周期事件、响应内容重试;详见 [变更记录](CHANGELOG.md)。
668
670
  - 近期:完善请求生命周期、取消、上传重放和不同 Runtime 的行为约定。
669
671
  - 后续按实际使用需求推进:Record / Replay、OpenTelemetry 适配、OpenAPI 生成器。
670
672