xparse-client 0.2.11__py3-none-any.whl → 0.3.0b3__py3-none-any.whl
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.
- example/1_basic_api_usage.py +198 -0
- example/2_async_job.py +210 -0
- example/3_local_workflow.py +300 -0
- example/4_advanced_workflow.py +327 -0
- example/README.md +128 -0
- example/config_example.json +95 -0
- tests/conftest.py +310 -0
- tests/unit/__init__.py +1 -0
- tests/unit/api/__init__.py +1 -0
- tests/unit/api/test_extract.py +232 -0
- tests/unit/api/test_local.py +231 -0
- tests/unit/api/test_parse.py +374 -0
- tests/unit/api/test_pipeline.py +369 -0
- tests/unit/api/test_workflows.py +108 -0
- tests/unit/connectors/test_ftp.py +525 -0
- tests/unit/connectors/test_local_connectors.py +324 -0
- tests/unit/connectors/test_milvus.py +368 -0
- tests/unit/connectors/test_qdrant.py +399 -0
- tests/unit/connectors/test_s3.py +598 -0
- tests/unit/connectors/test_smb.py +442 -0
- tests/unit/connectors/test_utils.py +335 -0
- tests/unit/models/test_local.py +54 -0
- tests/unit/models/test_pipeline_stages.py +144 -0
- tests/unit/models/test_workflows.py +55 -0
- tests/unit/test_base.py +437 -0
- tests/unit/test_client.py +110 -0
- tests/unit/test_config.py +160 -0
- tests/unit/test_exceptions.py +182 -0
- tests/unit/test_http.py +562 -0
- xparse_client/__init__.py +111 -20
- xparse_client/_base.py +179 -0
- xparse_client/_client.py +218 -0
- xparse_client/_config.py +221 -0
- xparse_client/_http.py +350 -0
- xparse_client/api/__init__.py +14 -0
- xparse_client/api/extract.py +109 -0
- xparse_client/api/local.py +215 -0
- xparse_client/api/parse.py +209 -0
- xparse_client/api/pipeline.py +134 -0
- xparse_client/api/workflows.py +204 -0
- xparse_client/connectors/__init__.py +45 -0
- xparse_client/connectors/_utils.py +138 -0
- xparse_client/connectors/destinations/__init__.py +45 -0
- xparse_client/connectors/destinations/base.py +116 -0
- xparse_client/connectors/destinations/local.py +91 -0
- xparse_client/connectors/destinations/milvus.py +229 -0
- xparse_client/connectors/destinations/qdrant.py +238 -0
- xparse_client/connectors/destinations/s3.py +163 -0
- xparse_client/connectors/sources/__init__.py +45 -0
- xparse_client/connectors/sources/base.py +74 -0
- xparse_client/connectors/sources/ftp.py +278 -0
- xparse_client/connectors/sources/local.py +176 -0
- xparse_client/connectors/sources/s3.py +232 -0
- xparse_client/connectors/sources/smb.py +259 -0
- xparse_client/exceptions.py +398 -0
- xparse_client/models/__init__.py +60 -0
- xparse_client/models/chunk.py +39 -0
- xparse_client/models/embed.py +62 -0
- xparse_client/models/extract.py +41 -0
- xparse_client/models/local.py +38 -0
- xparse_client/models/parse.py +136 -0
- xparse_client/models/pipeline.py +134 -0
- xparse_client/models/workflows.py +74 -0
- xparse_client-0.3.0b3.dist-info/METADATA +1075 -0
- xparse_client-0.3.0b3.dist-info/RECORD +68 -0
- {xparse_client-0.2.11.dist-info → xparse_client-0.3.0b3.dist-info}/WHEEL +1 -1
- {xparse_client-0.2.11.dist-info → xparse_client-0.3.0b3.dist-info}/licenses/LICENSE +1 -1
- {xparse_client-0.2.11.dist-info → xparse_client-0.3.0b3.dist-info}/top_level.txt +1 -0
- example/run_pipeline.py +0 -506
- example/run_pipeline_test.py +0 -458
- xparse_client/pipeline/__init__.py +0 -3
- xparse_client/pipeline/config.py +0 -129
- xparse_client/pipeline/destinations.py +0 -487
- xparse_client/pipeline/pipeline.py +0 -622
- xparse_client/pipeline/sources.py +0 -585
- xparse_client-0.2.11.dist-info/METADATA +0 -1050
- xparse_client-0.2.11.dist-info/RECORD +0 -13
tests/unit/test_base.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""BaseAPI 测试
|
|
2
|
+
|
|
3
|
+
测试 xparse_client._base 模块的 BaseAPI 基类。
|
|
4
|
+
|
|
5
|
+
运行方式:
|
|
6
|
+
pytest tests/unit/test_base.py -v
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
import respx
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from xparse_client._base import BaseAPI
|
|
16
|
+
from xparse_client._config import SDKConfiguration
|
|
17
|
+
from xparse_client._http import HTTPClient
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# 测试用 Pydantic 模型
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
class SampleModel(BaseModel):
|
|
24
|
+
"""测试用模型"""
|
|
25
|
+
id: str
|
|
26
|
+
name: str
|
|
27
|
+
value: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConcreteAPI(BaseAPI):
|
|
31
|
+
"""用于测试的具体 API 类"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# Fixtures
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def config():
|
|
41
|
+
"""创建测试配置"""
|
|
42
|
+
return SDKConfiguration(
|
|
43
|
+
app_id="test_app",
|
|
44
|
+
secret_code="test_secret",
|
|
45
|
+
server_url="https://api.test.com"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def http_client(config):
|
|
51
|
+
"""创建测试 HTTP 客户端"""
|
|
52
|
+
return HTTPClient(config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def api(config, http_client):
|
|
57
|
+
"""创建测试 API 实例"""
|
|
58
|
+
return ConcreteAPI(config, http_client)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# 初始化测试
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
def test_base_api_initialization(config, http_client):
|
|
66
|
+
"""测试 BaseAPI 初始化"""
|
|
67
|
+
api = ConcreteAPI(config, http_client)
|
|
68
|
+
|
|
69
|
+
assert api._config == config
|
|
70
|
+
assert api._http == http_client
|
|
71
|
+
assert api._base_path == "/api/xparse"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# HTTP 请求方法测试
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
@respx.mock
|
|
79
|
+
def test_base_api_get_request(api):
|
|
80
|
+
"""测试 _get 方法"""
|
|
81
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
82
|
+
return_value=httpx.Response(200, json={"success": True})
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response = api._get("/test")
|
|
86
|
+
|
|
87
|
+
assert response.status_code == 200
|
|
88
|
+
assert response.json() == {"success": True}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@respx.mock
|
|
92
|
+
def test_base_api_post_request(api):
|
|
93
|
+
"""测试 _post 方法"""
|
|
94
|
+
respx.post("https://api.test.com/api/xparse/test").mock(
|
|
95
|
+
return_value=httpx.Response(200, json={"success": True})
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
response = api._post("/test", json={"data": "value"})
|
|
99
|
+
|
|
100
|
+
assert response.status_code == 200
|
|
101
|
+
assert response.json() == {"success": True}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@respx.mock
|
|
105
|
+
def test_base_api_put_request(api):
|
|
106
|
+
"""测试 _put 方法"""
|
|
107
|
+
respx.put("https://api.test.com/api/xparse/test/123").mock(
|
|
108
|
+
return_value=httpx.Response(200, json={"success": True})
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
response = api._put("/test/123", json={"data": "updated"})
|
|
112
|
+
|
|
113
|
+
assert response.status_code == 200
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@respx.mock
|
|
117
|
+
def test_base_api_delete_request(api):
|
|
118
|
+
"""测试 _delete 方法"""
|
|
119
|
+
respx.delete("https://api.test.com/api/xparse/test/123").mock(
|
|
120
|
+
return_value=httpx.Response(204)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
response = api._delete("/test/123")
|
|
124
|
+
|
|
125
|
+
assert response.status_code == 204
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@respx.mock
|
|
129
|
+
def test_base_api_request_with_params(api):
|
|
130
|
+
"""测试带查询参数的请求"""
|
|
131
|
+
respx.get("https://api.test.com/api/xparse/test?page=1&limit=10").mock(
|
|
132
|
+
return_value=httpx.Response(200, json={"items": []})
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
response = api._get("/test", params={"page": 1, "limit": 10})
|
|
136
|
+
|
|
137
|
+
assert response.status_code == 200
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@respx.mock
|
|
141
|
+
def test_base_api_request_with_headers(api):
|
|
142
|
+
"""测试带自定义请求头的请求"""
|
|
143
|
+
route = respx.get("https://api.test.com/api/xparse/test").mock(
|
|
144
|
+
return_value=httpx.Response(200, json={})
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
api._get("/test", headers={"X-Custom-Header": "value"})
|
|
148
|
+
|
|
149
|
+
# 验证请求头
|
|
150
|
+
assert route.called
|
|
151
|
+
request = route.calls.last.request
|
|
152
|
+
assert "X-Custom-Header" in request.headers
|
|
153
|
+
assert request.headers["X-Custom-Header"] == "value"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@respx.mock
|
|
157
|
+
def test_base_api_request_with_timeout(api):
|
|
158
|
+
"""测试带超时的请求"""
|
|
159
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
160
|
+
return_value=httpx.Response(200, json={})
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# 不会抛出异常
|
|
164
|
+
response = api._get("/test", timeout=5.0)
|
|
165
|
+
assert response.status_code == 200
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# 响应解析测试
|
|
170
|
+
# ============================================================================
|
|
171
|
+
|
|
172
|
+
@respx.mock
|
|
173
|
+
def test_parse_response_standard_format(api):
|
|
174
|
+
"""测试解析标准格式响应 {"code": 200, "data": {...}}"""
|
|
175
|
+
response_data = {
|
|
176
|
+
"code": 200,
|
|
177
|
+
"data": {
|
|
178
|
+
"id": "123",
|
|
179
|
+
"name": "test",
|
|
180
|
+
"value": 42
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
185
|
+
return_value=httpx.Response(200, json=response_data)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
response = api._get("/test")
|
|
189
|
+
result = api._parse_response(response, SampleModel)
|
|
190
|
+
|
|
191
|
+
assert isinstance(result, SampleModel)
|
|
192
|
+
assert result.id == "123"
|
|
193
|
+
assert result.name == "test"
|
|
194
|
+
assert result.value == 42
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@respx.mock
|
|
198
|
+
def test_parse_response_direct_format(api):
|
|
199
|
+
"""测试解析直接返回格式 {...}"""
|
|
200
|
+
response_data = {
|
|
201
|
+
"id": "123",
|
|
202
|
+
"name": "test",
|
|
203
|
+
"value": 42
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
207
|
+
return_value=httpx.Response(200, json=response_data)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
response = api._get("/test")
|
|
211
|
+
result = api._parse_response(response, SampleModel)
|
|
212
|
+
|
|
213
|
+
assert isinstance(result, SampleModel)
|
|
214
|
+
assert result.id == "123"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@respx.mock
|
|
218
|
+
def test_parse_response_with_default_values(api):
|
|
219
|
+
"""测试解析响应使用模型默认值"""
|
|
220
|
+
response_data = {
|
|
221
|
+
"code": 200,
|
|
222
|
+
"data": {
|
|
223
|
+
"id": "123",
|
|
224
|
+
"name": "test"
|
|
225
|
+
# value 缺失,应使用默认值 0
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
230
|
+
return_value=httpx.Response(200, json=response_data)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
response = api._get("/test")
|
|
234
|
+
result = api._parse_response(response, SampleModel)
|
|
235
|
+
|
|
236
|
+
assert result.value == 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@respx.mock
|
|
240
|
+
def test_parse_list_response_standard_format(api):
|
|
241
|
+
"""测试解析标准格式的列表响应"""
|
|
242
|
+
response_data = {
|
|
243
|
+
"code": 200,
|
|
244
|
+
"data": [
|
|
245
|
+
{"id": "1", "name": "first", "value": 10},
|
|
246
|
+
{"id": "2", "name": "second", "value": 20},
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
251
|
+
return_value=httpx.Response(200, json=response_data)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
response = api._get("/test")
|
|
255
|
+
result = api._parse_list_response(response, SampleModel)
|
|
256
|
+
|
|
257
|
+
assert isinstance(result, list)
|
|
258
|
+
assert len(result) == 2
|
|
259
|
+
assert all(isinstance(item, SampleModel) for item in result)
|
|
260
|
+
assert result[0].id == "1"
|
|
261
|
+
assert result[1].id == "2"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@respx.mock
|
|
265
|
+
def test_parse_list_response_direct_format(api):
|
|
266
|
+
"""测试解析直接返回的列表"""
|
|
267
|
+
response_data = [
|
|
268
|
+
{"id": "1", "name": "first", "value": 10},
|
|
269
|
+
{"id": "2", "name": "second", "value": 20},
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
273
|
+
return_value=httpx.Response(200, json=response_data)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
response = api._get("/test")
|
|
277
|
+
result = api._parse_list_response(response, SampleModel)
|
|
278
|
+
|
|
279
|
+
assert len(result) == 2
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@respx.mock
|
|
283
|
+
def test_parse_list_response_single_item_as_list(api):
|
|
284
|
+
"""测试解析单个对象为列表"""
|
|
285
|
+
response_data = {
|
|
286
|
+
"code": 200,
|
|
287
|
+
"data": {"id": "1", "name": "single", "value": 10}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
291
|
+
return_value=httpx.Response(200, json=response_data)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
response = api._get("/test")
|
|
295
|
+
result = api._parse_list_response(response, SampleModel)
|
|
296
|
+
|
|
297
|
+
# 单个对象应该被包装成列表
|
|
298
|
+
assert isinstance(result, list)
|
|
299
|
+
assert len(result) == 1
|
|
300
|
+
assert result[0].id == "1"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@respx.mock
|
|
304
|
+
def test_parse_list_response_empty_list(api):
|
|
305
|
+
"""测试解析空列表"""
|
|
306
|
+
response_data = {
|
|
307
|
+
"code": 200,
|
|
308
|
+
"data": []
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
312
|
+
return_value=httpx.Response(200, json=response_data)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
response = api._get("/test")
|
|
316
|
+
result = api._parse_list_response(response, SampleModel)
|
|
317
|
+
|
|
318
|
+
assert isinstance(result, list)
|
|
319
|
+
assert len(result) == 0
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@respx.mock
|
|
323
|
+
def test_parse_raw_response_standard_format(api):
|
|
324
|
+
"""测试解析原始响应(标准格式)"""
|
|
325
|
+
response_data = {
|
|
326
|
+
"code": 200,
|
|
327
|
+
"data": {
|
|
328
|
+
"key1": "value1",
|
|
329
|
+
"key2": 123,
|
|
330
|
+
"nested": {"inner": "data"}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
335
|
+
return_value=httpx.Response(200, json=response_data)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
response = api._get("/test")
|
|
339
|
+
result = api._parse_raw_response(response)
|
|
340
|
+
|
|
341
|
+
assert result == response_data["data"]
|
|
342
|
+
assert result["key1"] == "value1"
|
|
343
|
+
assert result["nested"]["inner"] == "data"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@respx.mock
|
|
347
|
+
def test_parse_raw_response_direct_format(api):
|
|
348
|
+
"""测试解析原始响应(直接格式)"""
|
|
349
|
+
response_data = {
|
|
350
|
+
"key1": "value1",
|
|
351
|
+
"key2": 123
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
355
|
+
return_value=httpx.Response(200, json=response_data)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
response = api._get("/test")
|
|
359
|
+
result = api._parse_raw_response(response)
|
|
360
|
+
|
|
361
|
+
assert result == response_data
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ============================================================================
|
|
365
|
+
# 路径构造测试
|
|
366
|
+
# ============================================================================
|
|
367
|
+
|
|
368
|
+
@respx.mock
|
|
369
|
+
def test_base_api_path_construction(api):
|
|
370
|
+
"""测试 API 路径构造"""
|
|
371
|
+
route = respx.get("https://api.test.com/api/xparse/v1/resource/123").mock(
|
|
372
|
+
return_value=httpx.Response(200, json={})
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
api._get("/v1/resource/123")
|
|
376
|
+
|
|
377
|
+
assert route.called
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@respx.mock
|
|
381
|
+
def test_base_api_path_without_leading_slash(api):
|
|
382
|
+
"""测试路径不以斜杠开头"""
|
|
383
|
+
route = respx.get("https://api.test.com/api/xparseresource").mock(
|
|
384
|
+
return_value=httpx.Response(200, json={})
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
api._get("resource")
|
|
388
|
+
|
|
389
|
+
assert route.called
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ============================================================================
|
|
393
|
+
# 错误处理测试
|
|
394
|
+
# ============================================================================
|
|
395
|
+
|
|
396
|
+
@respx.mock
|
|
397
|
+
def test_parse_response_validation_error(api):
|
|
398
|
+
"""测试响应数据验证错误"""
|
|
399
|
+
response_data = {
|
|
400
|
+
"code": 200,
|
|
401
|
+
"data": {
|
|
402
|
+
"id": "123"
|
|
403
|
+
# name 缺失且没有默认值,应该抛出验证错误
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
408
|
+
return_value=httpx.Response(200, json=response_data)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
response = api._get("/test")
|
|
412
|
+
|
|
413
|
+
from pydantic import ValidationError
|
|
414
|
+
with pytest.raises(ValidationError):
|
|
415
|
+
api._parse_response(response, SampleModel)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@respx.mock
|
|
419
|
+
def test_parse_list_response_validation_error(api):
|
|
420
|
+
"""测试列表响应中的验证错误"""
|
|
421
|
+
response_data = {
|
|
422
|
+
"code": 200,
|
|
423
|
+
"data": [
|
|
424
|
+
{"id": "1", "name": "valid", "value": 10},
|
|
425
|
+
{"id": "2"} # name 缺失
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
respx.get("https://api.test.com/api/xparse/test").mock(
|
|
430
|
+
return_value=httpx.Response(200, json=response_data)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
response = api._get("/test")
|
|
434
|
+
|
|
435
|
+
from pydantic import ValidationError
|
|
436
|
+
with pytest.raises(ValidationError):
|
|
437
|
+
api._parse_list_response(response, SampleModel)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""XParseClient 测试"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xparse_client import XParseClient
|
|
6
|
+
from xparse_client._config import SDKConfiguration
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestXParseClient:
|
|
10
|
+
"""XParseClient 测试"""
|
|
11
|
+
|
|
12
|
+
def test_create_with_credentials(self, app_id, secret_code):
|
|
13
|
+
"""测试使用凭证创建客户端"""
|
|
14
|
+
client = XParseClient(
|
|
15
|
+
app_id=app_id,
|
|
16
|
+
secret_code=secret_code,
|
|
17
|
+
)
|
|
18
|
+
assert client.config.app_id == app_id
|
|
19
|
+
assert client.config.secret_code == secret_code
|
|
20
|
+
|
|
21
|
+
def test_create_with_config(self, app_id, secret_code):
|
|
22
|
+
"""测试使用配置对象创建客户端"""
|
|
23
|
+
config = SDKConfiguration(
|
|
24
|
+
app_id=app_id,
|
|
25
|
+
secret_code=secret_code,
|
|
26
|
+
)
|
|
27
|
+
client = XParseClient(config=config)
|
|
28
|
+
assert client.config is config
|
|
29
|
+
|
|
30
|
+
def test_create_with_custom_settings(self, app_id, secret_code, server_url):
|
|
31
|
+
"""测试自定义设置"""
|
|
32
|
+
client = XParseClient(
|
|
33
|
+
app_id=app_id,
|
|
34
|
+
secret_code=secret_code,
|
|
35
|
+
server_url=server_url,
|
|
36
|
+
timeout=60.0,
|
|
37
|
+
max_retries=5,
|
|
38
|
+
)
|
|
39
|
+
assert client.config.server_url == server_url
|
|
40
|
+
assert client.config.timeout == 60.0
|
|
41
|
+
assert client.config.max_retries == 5
|
|
42
|
+
|
|
43
|
+
def test_from_env(self, app_id, secret_code, monkeypatch):
|
|
44
|
+
"""测试从环境变量创建"""
|
|
45
|
+
monkeypatch.setenv("TEXTIN_APP_ID", app_id)
|
|
46
|
+
monkeypatch.setenv("TEXTIN_SECRET_CODE", secret_code)
|
|
47
|
+
|
|
48
|
+
client = XParseClient.from_env()
|
|
49
|
+
assert client.config.app_id == app_id
|
|
50
|
+
assert client.config.secret_code == secret_code
|
|
51
|
+
|
|
52
|
+
def test_lazy_load_parse_api(self, app_id, secret_code):
|
|
53
|
+
"""测试懒加载 Parse API"""
|
|
54
|
+
client = XParseClient(
|
|
55
|
+
app_id=app_id,
|
|
56
|
+
secret_code=secret_code,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# 访问 parse 属性应该返回 Parse 实例
|
|
60
|
+
parse_api = client.parse
|
|
61
|
+
assert parse_api is not None
|
|
62
|
+
|
|
63
|
+
# 再次访问应该返回同一个实例(缓存)
|
|
64
|
+
assert client.parse is parse_api
|
|
65
|
+
|
|
66
|
+
def test_lazy_load_extract_api(self, app_id, secret_code):
|
|
67
|
+
"""测试懒加载 Extract API"""
|
|
68
|
+
client = XParseClient(
|
|
69
|
+
app_id=app_id,
|
|
70
|
+
secret_code=secret_code,
|
|
71
|
+
)
|
|
72
|
+
extract_api = client.extract
|
|
73
|
+
assert extract_api is not None
|
|
74
|
+
|
|
75
|
+
def test_lazy_load_pipeline_api(self, app_id, secret_code):
|
|
76
|
+
"""测试懒加载 Pipeline API"""
|
|
77
|
+
client = XParseClient(
|
|
78
|
+
app_id=app_id,
|
|
79
|
+
secret_code=secret_code,
|
|
80
|
+
)
|
|
81
|
+
pipeline_api = client.pipeline
|
|
82
|
+
assert pipeline_api is not None
|
|
83
|
+
|
|
84
|
+
def test_unknown_attribute(self, app_id, secret_code):
|
|
85
|
+
"""测试访问未知属性"""
|
|
86
|
+
client = XParseClient(
|
|
87
|
+
app_id=app_id,
|
|
88
|
+
secret_code=secret_code,
|
|
89
|
+
)
|
|
90
|
+
with pytest.raises(AttributeError):
|
|
91
|
+
_ = client.unknown_api
|
|
92
|
+
|
|
93
|
+
def test_context_manager(self, app_id, secret_code):
|
|
94
|
+
"""测试上下文管理器"""
|
|
95
|
+
with XParseClient(app_id=app_id, secret_code=secret_code) as client:
|
|
96
|
+
assert client is not None
|
|
97
|
+
|
|
98
|
+
def test_close(self, app_id, secret_code):
|
|
99
|
+
"""测试关闭客户端"""
|
|
100
|
+
client = XParseClient(
|
|
101
|
+
app_id=app_id,
|
|
102
|
+
secret_code=secret_code,
|
|
103
|
+
)
|
|
104
|
+
# 预热 API 缓存
|
|
105
|
+
_ = client.parse
|
|
106
|
+
assert len(client._api_cache) > 0
|
|
107
|
+
|
|
108
|
+
# 关闭应该清空缓存
|
|
109
|
+
client.close()
|
|
110
|
+
assert len(client._api_cache) == 0
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""配置类测试"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from xparse_client._config import (
|
|
6
|
+
DEFAULT_MAX_RETRIES,
|
|
7
|
+
DEFAULT_SERVER_URL,
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
RetryConfiguration,
|
|
10
|
+
SDKConfiguration,
|
|
11
|
+
)
|
|
12
|
+
from xparse_client.exceptions import ConfigurationError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSDKConfiguration:
|
|
16
|
+
"""SDK 配置测试"""
|
|
17
|
+
|
|
18
|
+
def test_basic_config(self, app_id, secret_code):
|
|
19
|
+
"""测试基本配置"""
|
|
20
|
+
config = SDKConfiguration(
|
|
21
|
+
app_id=app_id,
|
|
22
|
+
secret_code=secret_code,
|
|
23
|
+
)
|
|
24
|
+
assert config.app_id == app_id
|
|
25
|
+
assert config.secret_code == secret_code
|
|
26
|
+
assert config.server_url == DEFAULT_SERVER_URL
|
|
27
|
+
assert config.timeout == DEFAULT_TIMEOUT
|
|
28
|
+
assert config.max_retries == DEFAULT_MAX_RETRIES
|
|
29
|
+
|
|
30
|
+
def test_custom_config(self, app_id, secret_code):
|
|
31
|
+
"""测试自定义配置"""
|
|
32
|
+
config = SDKConfiguration(
|
|
33
|
+
app_id=app_id,
|
|
34
|
+
secret_code=secret_code,
|
|
35
|
+
server_url="https://custom.api.com",
|
|
36
|
+
timeout=60.0,
|
|
37
|
+
max_retries=5,
|
|
38
|
+
)
|
|
39
|
+
assert config.server_url == "https://custom.api.com"
|
|
40
|
+
assert config.timeout == 60.0
|
|
41
|
+
assert config.max_retries == 5
|
|
42
|
+
|
|
43
|
+
def test_trailing_slash_removed(self, app_id, secret_code):
|
|
44
|
+
"""测试移除末尾斜杠"""
|
|
45
|
+
config = SDKConfiguration(
|
|
46
|
+
app_id=app_id,
|
|
47
|
+
secret_code=secret_code,
|
|
48
|
+
server_url="https://api.com/",
|
|
49
|
+
)
|
|
50
|
+
assert config.server_url == "https://api.com"
|
|
51
|
+
|
|
52
|
+
def test_missing_app_id(self, secret_code):
|
|
53
|
+
"""测试缺少 app_id"""
|
|
54
|
+
with pytest.raises(ConfigurationError) as exc_info:
|
|
55
|
+
SDKConfiguration(app_id="", secret_code=secret_code)
|
|
56
|
+
assert "app_id" in str(exc_info.value)
|
|
57
|
+
|
|
58
|
+
def test_missing_secret_code(self, app_id):
|
|
59
|
+
"""测试缺少 secret_code"""
|
|
60
|
+
with pytest.raises(ConfigurationError) as exc_info:
|
|
61
|
+
SDKConfiguration(app_id=app_id, secret_code="")
|
|
62
|
+
assert "secret_code" in str(exc_info.value)
|
|
63
|
+
|
|
64
|
+
def test_invalid_timeout(self, app_id, secret_code):
|
|
65
|
+
"""测试无效的 timeout"""
|
|
66
|
+
with pytest.raises(ConfigurationError) as exc_info:
|
|
67
|
+
SDKConfiguration(
|
|
68
|
+
app_id=app_id,
|
|
69
|
+
secret_code=secret_code,
|
|
70
|
+
timeout=-1,
|
|
71
|
+
)
|
|
72
|
+
assert "timeout" in str(exc_info.value)
|
|
73
|
+
|
|
74
|
+
def test_invalid_max_retries(self, app_id, secret_code):
|
|
75
|
+
"""测试无效的 max_retries"""
|
|
76
|
+
with pytest.raises(ConfigurationError) as exc_info:
|
|
77
|
+
SDKConfiguration(
|
|
78
|
+
app_id=app_id,
|
|
79
|
+
secret_code=secret_code,
|
|
80
|
+
max_retries=-1,
|
|
81
|
+
)
|
|
82
|
+
assert "max_retries" in str(exc_info.value)
|
|
83
|
+
|
|
84
|
+
def test_get_auth_headers(self, app_id, secret_code):
|
|
85
|
+
"""测试获取认证头"""
|
|
86
|
+
config = SDKConfiguration(
|
|
87
|
+
app_id=app_id,
|
|
88
|
+
secret_code=secret_code,
|
|
89
|
+
)
|
|
90
|
+
headers = config.get_auth_headers()
|
|
91
|
+
assert headers["x-ti-app-id"] == app_id
|
|
92
|
+
assert headers["x-ti-secret-code"] == secret_code
|
|
93
|
+
|
|
94
|
+
def test_extra_headers(self, app_id, secret_code):
|
|
95
|
+
"""测试额外请求头"""
|
|
96
|
+
config = SDKConfiguration(
|
|
97
|
+
app_id=app_id,
|
|
98
|
+
secret_code=secret_code,
|
|
99
|
+
extra_headers={"X-Custom": "value"},
|
|
100
|
+
)
|
|
101
|
+
headers = config.get_auth_headers()
|
|
102
|
+
assert headers["X-Custom"] == "value"
|
|
103
|
+
|
|
104
|
+
def test_from_env(self, app_id, secret_code, monkeypatch):
|
|
105
|
+
"""测试从环境变量创建配置"""
|
|
106
|
+
monkeypatch.setenv("TEXTIN_APP_ID", app_id)
|
|
107
|
+
monkeypatch.setenv("TEXTIN_SECRET_CODE", secret_code)
|
|
108
|
+
|
|
109
|
+
config = SDKConfiguration.from_env()
|
|
110
|
+
assert config.app_id == app_id
|
|
111
|
+
assert config.secret_code == secret_code
|
|
112
|
+
|
|
113
|
+
def test_from_env_missing(self, monkeypatch):
|
|
114
|
+
"""测试环境变量缺失"""
|
|
115
|
+
monkeypatch.delenv("TEXTIN_APP_ID", raising=False)
|
|
116
|
+
monkeypatch.delenv("TEXTIN_SECRET_CODE", raising=False)
|
|
117
|
+
|
|
118
|
+
with pytest.raises(ConfigurationError):
|
|
119
|
+
SDKConfiguration.from_env()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestRetryConfiguration:
|
|
123
|
+
"""重试配置测试"""
|
|
124
|
+
|
|
125
|
+
def test_default_config(self):
|
|
126
|
+
"""测试默认配置"""
|
|
127
|
+
config = RetryConfiguration()
|
|
128
|
+
assert config.max_retries == DEFAULT_MAX_RETRIES
|
|
129
|
+
assert 429 in config.retry_status_codes
|
|
130
|
+
assert 500 in config.retry_status_codes
|
|
131
|
+
|
|
132
|
+
def test_calculate_backoff(self):
|
|
133
|
+
"""测试退避时间计算"""
|
|
134
|
+
config = RetryConfiguration(backoff_base=2.0, backoff_max=60.0)
|
|
135
|
+
|
|
136
|
+
# 第0次重试: 2^0 = 1
|
|
137
|
+
assert config.calculate_backoff(0) == 1.0
|
|
138
|
+
# 第1次重试: 2^1 = 2
|
|
139
|
+
assert config.calculate_backoff(1) == 2.0
|
|
140
|
+
# 第2次重试: 2^2 = 4
|
|
141
|
+
assert config.calculate_backoff(2) == 4.0
|
|
142
|
+
# 第6次重试: 2^6 = 64 > 60,所以返回 60
|
|
143
|
+
assert config.calculate_backoff(6) == 60.0
|
|
144
|
+
|
|
145
|
+
def test_should_retry(self):
|
|
146
|
+
"""测试是否应该重试"""
|
|
147
|
+
config = RetryConfiguration(max_retries=3)
|
|
148
|
+
|
|
149
|
+
# 可重试状态码
|
|
150
|
+
assert config.should_retry(429, 0) is True
|
|
151
|
+
assert config.should_retry(500, 0) is True
|
|
152
|
+
assert config.should_retry(503, 0) is True
|
|
153
|
+
|
|
154
|
+
# 不可重试状态码
|
|
155
|
+
assert config.should_retry(400, 0) is False
|
|
156
|
+
assert config.should_retry(401, 0) is False
|
|
157
|
+
assert config.should_retry(404, 0) is False
|
|
158
|
+
|
|
159
|
+
# 超过最大重试次数
|
|
160
|
+
assert config.should_retry(500, 3) is False
|