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.
- {typact-0.2.2a1 → typact-0.2.2a2}/CHANGELOG.md +8 -1
- {typact-0.2.2a1 → typact-0.2.2a2}/CONTRIBUTING.md +2 -2
- {typact-0.2.2a1 → typact-0.2.2a2}/MANIFEST.in +1 -1
- typact-0.2.2a2/PKG-INFO +324 -0
- typact-0.2.2a2/README.md +292 -0
- typact-0.2.2a1/README.md → typact-0.2.2a2/README.zh-CN.md +4 -2
- {typact-0.2.2a1 → typact-0.2.2a2}/pyproject.toml +15 -5
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/http_client.py +5 -3
- typact-0.2.2a2/src/typact.egg-info/PKG-INFO +324 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/SOURCES.txt +1 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/tests/test_runtime_integration.py +16 -3
- typact-0.2.2a1/PKG-INFO +0 -714
- typact-0.2.2a1/src/typact.egg-info/PKG-INFO +0 -714
- {typact-0.2.2a1 → typact-0.2.2a2}/LICENSE +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/RELEASING.md +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/setup.cfg +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/base.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/body.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/cookie.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/file.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/form.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/header.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/path.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/annotations/query.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/multipart_builder.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/request_builder.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/builder/url_builder.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/decorator.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/client/metadata.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/file_converter.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/json_converter.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/response_converter.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/sse_converter.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/converter/stream_converter.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/errors.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/events.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/retry.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/core/types.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/auth.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/base.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/log.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/interceptor/trace.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/py.typed +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/aiohttp.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/base.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/httpx.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/mock.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/runtime/urllib.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/testing/__init__.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact/testing/mock_runtime.py +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/dependency_links.txt +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/requires.txt +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/src/typact.egg-info/top_level.txt +0 -0
- {typact-0.2.2a1 → typact-0.2.2a2}/tests/test_http_client_request.py +0 -0
- {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
|
|
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.
|
|
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
|
|
14
|
+
CI 在 Windows 和 Linux 上运行上述 Python 版本的完整测试,包含 urllib、httpx 和 aiohttp。Python 元数据允许更新版本,但未经 CI 验证的版本不在当前保证范围内。
|
|
15
15
|
|
|
16
16
|
文档使用 Node.js 22,版本记录在 `docs-site/.node-version`:
|
|
17
17
|
|
typact-0.2.2a2/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://typact.zzq.jl.cn)
|
|
36
|
+
[](https://pypi.org/project/typact/)
|
|
37
|
+
[](https://pypi.org/project/typact/)
|
|
38
|
+
[](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml)
|
|
39
|
+
[](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).
|
typact-0.2.2a2/README.md
ADDED
|
@@ -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
|
+
[](https://typact.zzq.jl.cn)
|
|
4
|
+
[](https://pypi.org/project/typact/)
|
|
5
|
+
[](https://pypi.org/project/typact/)
|
|
6
|
+
[](https://github.com/zzqjlcn/typact/actions/workflows/ci.yml)
|
|
7
|
+
[](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
|
[](https://pypi.org/project/typact/)
|
|
6
6
|
[](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
|
|
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
|
-
-
|
|
669
|
+
- 0.2.2a1 预发布版:生命周期事件、响应内容重试;详见 [变更记录](CHANGELOG.md)。
|
|
668
670
|
- 近期:完善请求生命周期、取消、上传重放和不同 Runtime 的行为约定。
|
|
669
671
|
- 后续按实际使用需求推进:Record / Replay、OpenTelemetry 适配、OpenAPI 生成器。
|
|
670
672
|
|