Skip to content

Commit 4ca19c1

Browse files
authored
feat: Mock API (#130)
* add mock mode * add ut tests and readme * update readme for mockapi * update ci yaml * support 3.9
1 parent 17b0839 commit 4ca19c1

File tree

15 files changed

+749
-144
lines changed

15 files changed

+749
-144
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ jobs:
2323
version: "latest"
2424
- name: Set up Python
2525
run: uv python install ${{ matrix.python-version }}
26+
- name: Verify Python Version
27+
run: |
28+
uv run python -c "import sys; print(f'Python version: {sys.version}')"
29+
uv run python -c "import sys; assert sys.version_info >= (3, 10), f'Python version {sys.version} is less than 3.10'"
2630
- name: Install Dependencies
2731
run: uv sync --all-extras
28-
- name: Install Dependencies
32+
- name: Install Project Dependencies
2933
run: uv run pip install -r requirements.txt
3034
- name: Test
3135
run: uv run pytest tests/ --cov=pydantic_client --cov-report=term-missing:skip-covered --cov-report=xml

.github/workflows/python-publish.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v3
1616
- name: Set up Python
17-
uses: actions/setup-python@v3
17+
uses: actions/setup-python@v4
1818
with:
19-
python-version: '3.x'
19+
python-version: '3.10' # 明确指定3.10或更高版本,与项目要求一致
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip
2323
pip install build
24+
python -c "import sys; print(f'Python version: {sys.version}')"
25+
python -c "import sys; assert sys.version_info >= (3, 10), f'Python version {sys.version} is less than 3.10'"
2426
- name: Build package
2527
run: python -m build
2628
- name: Publish package

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,93 @@ user = client.get_user("123")
170170

171171
```
172172

173+
## Mock API Responses
174+
175+
You can configure the client to return mock responses instead of making actual API calls. This is useful for testing or development purposes.
176+
177+
### Setting Mock Responses Directly
178+
179+
```python
180+
from pydantic_client import RequestsWebClient, get
181+
182+
class UserResponse(BaseModel):
183+
id: int
184+
name: str
185+
186+
class MyClient(RequestsWebClient):
187+
@get("/users/{user_id}")
188+
def get_user(self, user_id: int) -> UserResponse:
189+
pass
190+
191+
# Create client and configure mocks
192+
client = MyClient(base_url="https://api.example.com")
193+
client.set_mock_config(mock_config=[
194+
{
195+
"name": "get_user",
196+
"output": {
197+
"id": 123,
198+
"name": "Mock User"
199+
}
200+
}
201+
])
202+
203+
# This will return the mock response without making an actual API call
204+
user = client.get_user(1) # Returns UserResponse(id=123, name="Mock User")
205+
```
206+
207+
### Loading Mock Responses from a JSON File
208+
209+
You can also load mock configurations from a JSON file:
210+
211+
```python
212+
# Load mock data from a JSON file
213+
client.set_mock_config(mock_config_path="path/to/mock_data.json")
214+
```
215+
216+
The JSON file should follow this format:
217+
218+
```json
219+
[
220+
{
221+
"name": "get_user",
222+
"output": {
223+
"id": 123,
224+
"name": "Mock User"
225+
}
226+
},
227+
{
228+
"name": "list_users",
229+
"output": {
230+
"users": [
231+
{"id": 1, "name": "User 1"},
232+
{"id": 2, "name": "User 2"}
233+
]
234+
}
235+
}
236+
]
237+
```
238+
239+
### Setting Mock Responses in Client Configuration
240+
241+
You can also include mock configuration when creating a client from configuration:
242+
243+
```python
244+
config = {
245+
"base_url": "https://api.example.com",
246+
"timeout": 10,
247+
"mock_config": [
248+
{
249+
"name": "get_user",
250+
"output": {
251+
"id": 123,
252+
"name": "Mock User"
253+
}
254+
}
255+
]
256+
}
257+
258+
client = MyClient.from_config(config)
259+
```
173260

174261
### Timing Context Manager
175262

@@ -213,6 +300,7 @@ client = MyAPIClient.from_config(config)
213300
The library automatically validates responses against Pydantic models when specified as return types
214301
in the method definitions.
215302

303+
216304
## Error Handling
217305

218306
HTTP errors are raised as exceptions by the underlying HTTP client libraries. Make sure to handle

example/example_mock.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Example for using mock functionality in pydantic-client
3+
"""
4+
import logging
5+
from typing import List, Optional
6+
from pydantic import BaseModel
7+
8+
from pydantic_client.sync_client import RequestsWebClient
9+
from pydantic_client.decorators import get, post, patch
10+
11+
# 设置日志格式和级别
12+
logging.basicConfig(level=logging.INFO,
13+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14+
15+
# 定义API响应模型
16+
class User(BaseModel):
17+
name: str
18+
age: int
19+
email: Optional[str] = None
20+
21+
22+
class UserList(BaseModel):
23+
users: List[User]
24+
total: int
25+
26+
27+
# 定义API客户端
28+
class MockableApiClient(RequestsWebClient):
29+
@get("/users")
30+
def get_users(self) -> UserList:
31+
"""获取用户列表"""
32+
pass
33+
34+
@get("/users/{user_id}")
35+
def get_user(self, user_id: int) -> User:
36+
"""获取单个用户"""
37+
pass
38+
39+
@post("/users")
40+
def create_user(self, user: User) -> User:
41+
"""创建用户"""
42+
pass
43+
44+
@patch("/users/{user_id}")
45+
def update_user(self, user_id: int, user: User) -> User:
46+
"""更新用户"""
47+
pass
48+
49+
50+
def main():
51+
# 创建客户端
52+
client = MockableApiClient(base_url="https://api.example.com")
53+
54+
# 设置mock数据
55+
client.set_mock_config(mock_config=[
56+
{
57+
"name": "get_users",
58+
"output": {
59+
"users": [
60+
{"name": "张三", "age": 30, "email": "zhangsan@example.com"},
61+
{"name": "李四", "age": 25, "email": "lisi@example.com"}
62+
],
63+
"total": 2
64+
}
65+
},
66+
{
67+
"name": "get_user",
68+
"output": {
69+
"name": "张三",
70+
"age": 30,
71+
"email": "zhangsan@example.com"
72+
}
73+
},
74+
{
75+
"name": "create_user",
76+
"output": {
77+
"name": "王五",
78+
"age": 35,
79+
"email": "wangwu@example.com"
80+
}
81+
}
82+
])
83+
84+
# 使用API方法 - 将返回mock数据而不是实际发送请求
85+
users = client.get_users()
86+
print(f"用户列表: {users.users}")
87+
print(f"总用户数: {users.total}")
88+
89+
user = client.get_user(1)
90+
print(f"单个用户: {user.name}, {user.age} 岁")
91+
92+
new_user = User(name="王五", age=35, email="wangwu@example.com")
93+
created_user = client.create_user(new_user)
94+
print(f"创建的用户: {created_user.name}, {created_user.email}")
95+
96+
# 没有mock配置的方法会尝试实际API调用
97+
# 注意: 下面这行代码会尝试实际发送请求,因为我们没有为update_user设置mock
98+
# 在这个示例中会失败,因为我们的API地址不存在
99+
# try:
100+
# updated_user = client.update_user(1, new_user)
101+
# print(f"更新的用户: {updated_user.name}")
102+
# except Exception as e:
103+
# print(f"API调用失败(预期的行为): {e}")
104+
105+
106+
def from_config_example():
107+
"""演示使用from_config创建带mock配置的客户端"""
108+
config = {
109+
"base_url": "https://api.example.com",
110+
"timeout": 10,
111+
"mock_config": [
112+
{
113+
"name": "get_users",
114+
"output": {
115+
"users": [
116+
{"name": "配置张三", "age": 30, "email": "config_zhangsan@example.com"},
117+
{"name": "配置李四", "age": 25, "email": "config_lisi@example.com"}
118+
],
119+
"total": 2
120+
}
121+
}
122+
]
123+
}
124+
125+
client = MockableApiClient.from_config(config)
126+
users = client.get_users()
127+
print("\n从配置创建的客户端:")
128+
print(f"用户列表: {users.users}")
129+
130+
131+
if __name__ == "__main__":
132+
main()
133+
from_config_example()

pydantic_client/async_client.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from typing import Any, Dict, Optional, TypeVar
2+
import logging
23

34
from pydantic import BaseModel
45

56
from .base import BaseWebClient, RequestInfo
67

8+
logger = logging.getLogger(__name__)
9+
710
try:
811
import aiohttp
912
except ImportError:
@@ -25,25 +28,30 @@ def __init__(
2528
super().__init__(base_url, headers, timeout, session, statsd_address)
2629

2730
async def _request(self, request_info: RequestInfo) -> Any:
31+
# Check if there's a mock response for this method
32+
mock_response = self._get_mock_response(request_info)
33+
if mock_response:
34+
return mock_response
35+
36+
import aiohttp
2837
request_params = self.dump_request_params(request_info)
2938
response_model = request_params.pop("response_model")
3039

3140
request_params = self.before_request(request_params)
3241

33-
async with aiohttp.ClientSession() as session:
34-
async with session.request(
35-
**request_params,
36-
timeout=aiohttp.ClientTimeout(total=self.timeout)
37-
) as response:
38-
response.raise_for_status()
42+
if not self.session:
43+
self.session = aiohttp.ClientSession()
44+
45+
async with self.session.request(**request_params) as response:
46+
response.raise_for_status()
3947

40-
if response_model is str:
41-
return await response.text()
42-
elif response_model is bytes:
43-
return await response.content.read()
44-
elif not response_model:
45-
return await response.json()
46-
return response_model.model_validate(await response.json(), from_attributes=True)
48+
if response_model is str:
49+
return await response.text()
50+
elif response_model is bytes:
51+
return await response.content.read()
52+
elif not response_model:
53+
return await response.json()
54+
return response_model.model_validate(await response.json(), from_attributes=True)
4755

4856

4957
class HttpxWebClient(BaseWebClient):
@@ -63,6 +71,12 @@ def __init__(
6371
raise ImportError("please install httpx: `pip install httpx`")
6472

6573
async def _request(self, request_info: RequestInfo) -> Any:
74+
# Check if there's a mock response for this method
75+
mock_response = self._get_mock_response(request_info)
76+
if mock_response:
77+
return mock_response
78+
79+
# 没有mock数据,继续进行正常请求
6680
import httpx
6781
request_params = self.dump_request_params(request_info)
6882
response_model = request_params.pop("response_model")

0 commit comments

Comments
 (0)