xparse-client 0.2.20__py3-none-any.whl → 0.3.0b1__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 +110 -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 +185 -0
- xparse_client/api/parse.py +209 -0
- xparse_client/api/pipeline.py +132 -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 +132 -0
- xparse_client/models/workflows.py +74 -0
- xparse_client-0.3.0b1.dist-info/METADATA +1075 -0
- xparse_client-0.3.0b1.dist-info/RECORD +68 -0
- {xparse_client-0.2.20.dist-info → xparse_client-0.3.0b1.dist-info}/WHEEL +1 -1
- {xparse_client-0.2.20.dist-info → xparse_client-0.3.0b1.dist-info}/licenses/LICENSE +1 -1
- {xparse_client-0.2.20.dist-info → xparse_client-0.3.0b1.dist-info}/top_level.txt +2 -0
- xparse_client/pipeline/__init__.py +0 -3
- xparse_client/pipeline/config.py +0 -163
- xparse_client/pipeline/destinations.py +0 -489
- xparse_client/pipeline/pipeline.py +0 -860
- xparse_client/pipeline/sources.py +0 -583
- xparse_client-0.2.20.dist-info/METADATA +0 -1050
- xparse_client-0.2.20.dist-info/RECORD +0 -11
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Connectors 工具函数测试
|
|
2
|
+
|
|
3
|
+
测试 xparse_client.connectors._utils 模块的所有工具函数。
|
|
4
|
+
|
|
5
|
+
运行方式:
|
|
6
|
+
pytest tests/unit/connectors/test_utils.py -v
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import timezone
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from xparse_client.connectors._utils import (
|
|
16
|
+
flatten_dict,
|
|
17
|
+
get_current_millis_timestamp,
|
|
18
|
+
match_file_pattern,
|
|
19
|
+
normalize_wildcard_patterns,
|
|
20
|
+
to_millis_timestamp,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ============================================================================
|
|
24
|
+
# normalize_wildcard_patterns 测试
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
def test_normalize_wildcard_patterns_none():
|
|
28
|
+
"""测试 None 输入返回 None"""
|
|
29
|
+
result = normalize_wildcard_patterns(None)
|
|
30
|
+
assert result is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_normalize_wildcard_patterns_valid_list():
|
|
34
|
+
"""测试有效的模式列表"""
|
|
35
|
+
result = normalize_wildcard_patterns(["*.pdf", "*.docx"])
|
|
36
|
+
assert result == ["*.pdf", "*.docx"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_normalize_wildcard_patterns_with_whitespace():
|
|
40
|
+
"""测试包含空格的模式"""
|
|
41
|
+
result = normalize_wildcard_patterns([" *.pdf ", " *.docx "])
|
|
42
|
+
assert result == ["*.pdf", "*.docx"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_normalize_wildcard_patterns_with_empty_strings():
|
|
46
|
+
"""测试包含空字符串的模式列表"""
|
|
47
|
+
result = normalize_wildcard_patterns(["*.pdf", "", " ", "*.docx"])
|
|
48
|
+
assert result == ["*.pdf", "*.docx"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_normalize_wildcard_patterns_empty_list():
|
|
52
|
+
"""测试空列表返回 None(匹配所有)"""
|
|
53
|
+
result = normalize_wildcard_patterns([])
|
|
54
|
+
assert result is None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_normalize_wildcard_patterns_wildcard_all():
|
|
58
|
+
"""测试包含 * 通配符返回 None(匹配所有)"""
|
|
59
|
+
result = normalize_wildcard_patterns(["*.pdf", "*", "*.docx"])
|
|
60
|
+
assert result is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_normalize_wildcard_patterns_only_spaces():
|
|
64
|
+
"""测试只包含空格的列表返回 None"""
|
|
65
|
+
result = normalize_wildcard_patterns([" ", " "])
|
|
66
|
+
assert result is None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_normalize_wildcard_patterns_invalid_type():
|
|
70
|
+
"""测试无效类型抛出 ValueError"""
|
|
71
|
+
with pytest.raises(ValueError, match="pattern 必须是列表类型"):
|
|
72
|
+
normalize_wildcard_patterns("*.pdf")
|
|
73
|
+
|
|
74
|
+
with pytest.raises(ValueError, match="pattern 必须是列表类型"):
|
|
75
|
+
normalize_wildcard_patterns({"pattern": "*.pdf"})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ============================================================================
|
|
79
|
+
# match_file_pattern 测试
|
|
80
|
+
# ============================================================================
|
|
81
|
+
|
|
82
|
+
def test_match_file_pattern_none_patterns():
|
|
83
|
+
"""测试 None patterns 匹配所有文件"""
|
|
84
|
+
assert match_file_pattern("test.pdf", None) is True
|
|
85
|
+
assert match_file_pattern("any_file.txt", None) is True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_match_file_pattern_basic_match():
|
|
89
|
+
"""测试基本模式匹配"""
|
|
90
|
+
assert match_file_pattern("document.pdf", ["*.pdf"]) is True
|
|
91
|
+
assert match_file_pattern("document.docx", ["*.pdf"]) is False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_match_file_pattern_multiple_patterns():
|
|
95
|
+
"""测试多个模式匹配"""
|
|
96
|
+
patterns = ["*.pdf", "*.docx", "*.txt"]
|
|
97
|
+
assert match_file_pattern("file.pdf", patterns) is True
|
|
98
|
+
assert match_file_pattern("file.docx", patterns) is True
|
|
99
|
+
assert match_file_pattern("file.txt", patterns) is True
|
|
100
|
+
assert match_file_pattern("file.xlsx", patterns) is False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_match_file_pattern_with_path():
|
|
104
|
+
"""测试带路径的文件匹配(只匹配文件名)"""
|
|
105
|
+
assert match_file_pattern("dir/subdir/file.pdf", ["*.pdf"]) is True
|
|
106
|
+
assert match_file_pattern("dir/file.txt", ["*.txt"]) is True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_match_file_pattern_complex_patterns():
|
|
110
|
+
"""测试复杂通配符模式"""
|
|
111
|
+
assert match_file_pattern("report_2024.pdf", ["report_*.pdf"]) is True
|
|
112
|
+
assert match_file_pattern("test_file.txt", ["test_*"]) is True
|
|
113
|
+
assert match_file_pattern("document.pdf", ["doc*"]) is True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_match_file_pattern_case_sensitive():
|
|
117
|
+
"""测试大小写敏感匹配"""
|
|
118
|
+
# fnmatch 默认是大小写敏感的(在大多数系统上)
|
|
119
|
+
assert match_file_pattern("file.PDF", ["*.pdf"]) is False
|
|
120
|
+
assert match_file_pattern("file.PDF", ["*.PDF"]) is True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ============================================================================
|
|
124
|
+
# to_millis_timestamp 测试
|
|
125
|
+
# ============================================================================
|
|
126
|
+
|
|
127
|
+
def test_to_millis_timestamp_none():
|
|
128
|
+
"""测试 None 输入返回空字符串"""
|
|
129
|
+
result = to_millis_timestamp(None)
|
|
130
|
+
assert result == ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_to_millis_timestamp_seconds():
|
|
134
|
+
"""测试秒级时间戳转换为毫秒"""
|
|
135
|
+
# 2024-01-28 12:00:00 UTC
|
|
136
|
+
timestamp = 1706443200.0
|
|
137
|
+
result = to_millis_timestamp(timestamp)
|
|
138
|
+
assert result == "1706443200000"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_to_millis_timestamp_millis():
|
|
142
|
+
"""测试毫秒级时间戳直接返回"""
|
|
143
|
+
# 已经是毫秒
|
|
144
|
+
timestamp = 1706443200000.0
|
|
145
|
+
result = to_millis_timestamp(timestamp)
|
|
146
|
+
assert result == "1706443200000"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_to_millis_timestamp_with_decimals():
|
|
150
|
+
"""测试带小数的时间戳"""
|
|
151
|
+
timestamp = 1706443200.123
|
|
152
|
+
result = to_millis_timestamp(timestamp)
|
|
153
|
+
assert result == "1706443200123"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_to_millis_timestamp_zero():
|
|
157
|
+
"""测试零值时间戳"""
|
|
158
|
+
result = to_millis_timestamp(0)
|
|
159
|
+
assert result == "0"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_to_millis_timestamp_boundary():
|
|
163
|
+
"""测试边界值 (1e12)"""
|
|
164
|
+
# 接近边界但仍是秒
|
|
165
|
+
result = to_millis_timestamp(1e11)
|
|
166
|
+
assert result == str(int(1e11 * 1000))
|
|
167
|
+
|
|
168
|
+
# 刚好超过边界,是毫秒
|
|
169
|
+
result = to_millis_timestamp(1e12 + 1)
|
|
170
|
+
assert result == str(int(1e12 + 1))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ============================================================================
|
|
174
|
+
# get_current_millis_timestamp 测试
|
|
175
|
+
# ============================================================================
|
|
176
|
+
|
|
177
|
+
@patch('xparse_client.connectors._utils.datetime')
|
|
178
|
+
def test_get_current_millis_timestamp(mock_datetime):
|
|
179
|
+
"""测试获取当前毫秒时间戳"""
|
|
180
|
+
# Mock datetime.now() 返回固定时间
|
|
181
|
+
mock_now = MagicMock()
|
|
182
|
+
mock_now.timestamp.return_value = 1706443200.0
|
|
183
|
+
mock_datetime.now.return_value = mock_now
|
|
184
|
+
mock_datetime.timezone = timezone
|
|
185
|
+
|
|
186
|
+
result = get_current_millis_timestamp()
|
|
187
|
+
|
|
188
|
+
# 验证调用
|
|
189
|
+
mock_datetime.now.assert_called_once_with(timezone.utc)
|
|
190
|
+
|
|
191
|
+
# 验证结果
|
|
192
|
+
assert result == "1706443200000"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_get_current_millis_timestamp_real():
|
|
196
|
+
"""测试实际获取当前时间戳(不 mock)"""
|
|
197
|
+
result = get_current_millis_timestamp()
|
|
198
|
+
|
|
199
|
+
# 验证返回字符串
|
|
200
|
+
assert isinstance(result, str)
|
|
201
|
+
|
|
202
|
+
# 验证是有效的数字
|
|
203
|
+
timestamp = int(result)
|
|
204
|
+
assert timestamp > 0
|
|
205
|
+
|
|
206
|
+
# 验证长度(毫秒时间戳应该是 13 位)
|
|
207
|
+
assert len(result) == 13
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ============================================================================
|
|
211
|
+
# flatten_dict 测试
|
|
212
|
+
# ============================================================================
|
|
213
|
+
|
|
214
|
+
def test_flatten_dict_simple():
|
|
215
|
+
"""测试简单字典展平"""
|
|
216
|
+
data = {"a": 1, "b": 2}
|
|
217
|
+
result = flatten_dict(data)
|
|
218
|
+
assert result == {"a": 1, "b": 2}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_flatten_dict_nested():
|
|
222
|
+
"""测试嵌套字典展平"""
|
|
223
|
+
data = {"a": {"b": 1, "c": 2}, "d": 3}
|
|
224
|
+
result = flatten_dict(data)
|
|
225
|
+
assert result == {"a_b": 1, "a_c": 2, "d": 3}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_flatten_dict_with_prefix():
|
|
229
|
+
"""测试带前缀的展平"""
|
|
230
|
+
data = {"a": 1, "b": 2}
|
|
231
|
+
result = flatten_dict(data, prefix="meta")
|
|
232
|
+
assert result == {"meta_a": 1, "meta_b": 2}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_flatten_dict_nested_with_prefix():
|
|
236
|
+
"""测试嵌套字典带前缀展平"""
|
|
237
|
+
data = {"user": {"name": "Alice", "age": 30}}
|
|
238
|
+
result = flatten_dict(data, prefix="doc")
|
|
239
|
+
assert result == {"doc_user_name": "Alice", "doc_user_age": 30}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_flatten_dict_with_list():
|
|
243
|
+
"""测试包含列表的字典展平"""
|
|
244
|
+
data = {"tags": ["python", "test"], "count": 2}
|
|
245
|
+
result = flatten_dict(data)
|
|
246
|
+
|
|
247
|
+
assert result["count"] == 2
|
|
248
|
+
assert result["tags"] == json.dumps(["python", "test"], ensure_ascii=False)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_flatten_dict_with_list_chinese():
|
|
252
|
+
"""测试包含中文列表的展平(ensure_ascii=False)"""
|
|
253
|
+
data = {"tags": ["测试", "文档"]}
|
|
254
|
+
result = flatten_dict(data)
|
|
255
|
+
|
|
256
|
+
# 验证中文不被转义
|
|
257
|
+
assert result["tags"] == '["测试", "文档"]'
|
|
258
|
+
assert "\\u" not in result["tags"]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_flatten_dict_deep_nested():
|
|
262
|
+
"""测试深层嵌套展平"""
|
|
263
|
+
data = {
|
|
264
|
+
"level1": {
|
|
265
|
+
"level2": {
|
|
266
|
+
"level3": {
|
|
267
|
+
"value": 42
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
result = flatten_dict(data)
|
|
273
|
+
assert result == {"level1_level2_level3_value": 42}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_flatten_dict_exclude_fields():
|
|
277
|
+
"""测试排除字段"""
|
|
278
|
+
data = {"a": 1, "b": 2, "c": 3}
|
|
279
|
+
result = flatten_dict(data, exclude_fields={"b"})
|
|
280
|
+
assert result == {"a": 1, "c": 3}
|
|
281
|
+
assert "b" not in result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_flatten_dict_exclude_nested_fields():
|
|
285
|
+
"""测试排除嵌套字段"""
|
|
286
|
+
data = {"user": {"name": "Alice", "age": 30}, "count": 5}
|
|
287
|
+
result = flatten_dict(data, exclude_fields={"user_age"})
|
|
288
|
+
|
|
289
|
+
assert result == {"user_name": "Alice", "count": 5}
|
|
290
|
+
assert "user_age" not in result
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_flatten_dict_exclude_with_prefix():
|
|
294
|
+
"""测试带前缀时排除字段"""
|
|
295
|
+
data = {"a": 1, "b": 2}
|
|
296
|
+
result = flatten_dict(data, prefix="meta", exclude_fields={"meta_b"})
|
|
297
|
+
|
|
298
|
+
assert result == {"meta_a": 1}
|
|
299
|
+
assert "meta_b" not in result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_flatten_dict_empty():
|
|
303
|
+
"""测试空字典"""
|
|
304
|
+
result = flatten_dict({})
|
|
305
|
+
assert result == {}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_flatten_dict_mixed_types():
|
|
309
|
+
"""测试混合类型"""
|
|
310
|
+
data = {
|
|
311
|
+
"string": "text",
|
|
312
|
+
"number": 42,
|
|
313
|
+
"float": 3.14,
|
|
314
|
+
"bool": True,
|
|
315
|
+
"null": None,
|
|
316
|
+
"list": [1, 2, 3],
|
|
317
|
+
"dict": {"nested": "value"}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
result = flatten_dict(data)
|
|
321
|
+
|
|
322
|
+
assert result["string"] == "text"
|
|
323
|
+
assert result["number"] == 42
|
|
324
|
+
assert result["float"] == 3.14
|
|
325
|
+
assert result["bool"] is True
|
|
326
|
+
assert result["null"] is None
|
|
327
|
+
assert result["list"] == "[1, 2, 3]"
|
|
328
|
+
assert result["dict_nested"] == "value"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_flatten_dict_exclude_none():
|
|
332
|
+
"""测试 exclude_fields 为 None(默认)"""
|
|
333
|
+
data = {"a": 1, "b": 2}
|
|
334
|
+
result = flatten_dict(data, exclude_fields=None)
|
|
335
|
+
assert result == {"a": 1, "b": 2}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""测试 Local API 的数据模型"""
|
|
2
|
+
|
|
3
|
+
from xparse_client.models.local import FailedFile, WorkflowResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_failed_file_creation():
|
|
7
|
+
"""测试 FailedFile 创建"""
|
|
8
|
+
failed = FailedFile(
|
|
9
|
+
file_path="/path/to/file.pdf",
|
|
10
|
+
error="Connection timeout",
|
|
11
|
+
retry_count=3
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
assert failed.file_path == "/path/to/file.pdf"
|
|
15
|
+
assert failed.error == "Connection timeout"
|
|
16
|
+
assert failed.retry_count == 3
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_workflow_result_creation():
|
|
20
|
+
"""测试 WorkflowResult 创建"""
|
|
21
|
+
result = WorkflowResult(
|
|
22
|
+
total=10,
|
|
23
|
+
success=8,
|
|
24
|
+
failed=2,
|
|
25
|
+
failed_files=[
|
|
26
|
+
FailedFile(
|
|
27
|
+
file_path="/path/to/file1.pdf",
|
|
28
|
+
error="Parse error",
|
|
29
|
+
retry_count=1
|
|
30
|
+
)
|
|
31
|
+
],
|
|
32
|
+
duration=125.5
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert result.total == 10
|
|
36
|
+
assert result.success == 8
|
|
37
|
+
assert result.failed == 2
|
|
38
|
+
assert len(result.failed_files) == 1
|
|
39
|
+
assert result.duration == 125.5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_workflow_result_all_success():
|
|
43
|
+
"""测试全部成功的 WorkflowResult"""
|
|
44
|
+
result = WorkflowResult(
|
|
45
|
+
total=5,
|
|
46
|
+
success=5,
|
|
47
|
+
failed=0,
|
|
48
|
+
failed_files=[],
|
|
49
|
+
duration=60.0
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert result.total == result.success
|
|
53
|
+
assert result.failed == 0
|
|
54
|
+
assert len(result.failed_files) == 0
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""测试 PipelineStage Config 类型安全"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from xparse_client.models import (
|
|
7
|
+
ChunkConfig,
|
|
8
|
+
ChunkStage,
|
|
9
|
+
EmbedConfig,
|
|
10
|
+
EmbedStage,
|
|
11
|
+
ExtractConfig,
|
|
12
|
+
ExtractStage,
|
|
13
|
+
ParseConfig,
|
|
14
|
+
ParseStage,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_stage_with_config():
|
|
19
|
+
"""测试 ParseStage 使用 ParseConfig"""
|
|
20
|
+
stage = ParseStage(config=ParseConfig(provider="textin"))
|
|
21
|
+
assert stage.type == "parse"
|
|
22
|
+
assert stage.config.provider == "textin"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_stage_with_extra_fields():
|
|
26
|
+
"""测试 ParseConfig 支持额外字段"""
|
|
27
|
+
config = ParseConfig(provider="textin", custom_field="value")
|
|
28
|
+
stage = ParseStage(config=config)
|
|
29
|
+
|
|
30
|
+
# 序列化应该包含额外字段
|
|
31
|
+
dumped = stage.model_dump()
|
|
32
|
+
assert dumped["config"]["custom_field"] == "value"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_chunk_stage_with_config():
|
|
36
|
+
"""测试 ChunkStage 使用 ChunkConfig"""
|
|
37
|
+
stage = ChunkStage(
|
|
38
|
+
config=ChunkConfig(
|
|
39
|
+
strategy="by_title",
|
|
40
|
+
max_characters=2048,
|
|
41
|
+
overlap=100
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
assert stage.type == "chunk"
|
|
45
|
+
assert stage.config.strategy == "by_title"
|
|
46
|
+
assert stage.config.max_characters == 2048
|
|
47
|
+
assert stage.config.overlap == 100
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_embed_stage_with_config():
|
|
51
|
+
"""测试 EmbedStage 使用 EmbedConfig"""
|
|
52
|
+
stage = EmbedStage(
|
|
53
|
+
config=EmbedConfig(
|
|
54
|
+
provider="qwen",
|
|
55
|
+
model_name="text-embedding-v3"
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
assert stage.type == "embed"
|
|
59
|
+
assert stage.config.provider == "qwen"
|
|
60
|
+
assert stage.config.model_name == "text-embedding-v3"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_embed_config_validation():
|
|
64
|
+
"""测试 EmbedConfig 验证 provider 和 model 匹配"""
|
|
65
|
+
# 正确的组合
|
|
66
|
+
EmbedConfig(provider="qwen", model_name="text-embedding-v3")
|
|
67
|
+
EmbedConfig(provider="doubao", model_name="doubao-embedding-large-text-250515")
|
|
68
|
+
|
|
69
|
+
# 错误的组合应该抛出异常
|
|
70
|
+
with pytest.raises(ValidationError):
|
|
71
|
+
EmbedConfig(provider="qwen", model_name="doubao-embedding-large-text-250515")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_extract_stage_with_config():
|
|
75
|
+
"""测试 ExtractStage 使用 ExtractConfig"""
|
|
76
|
+
schema = {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"name": {"type": "string"},
|
|
80
|
+
"age": {"type": "number"}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
stage = ExtractStage(
|
|
84
|
+
config=ExtractConfig(
|
|
85
|
+
schema=schema,
|
|
86
|
+
generate_citations=True
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
assert stage.type == "extract"
|
|
90
|
+
assert stage.config.generate_citations is True
|
|
91
|
+
|
|
92
|
+
# 序列化检查
|
|
93
|
+
dumped = stage.model_dump()
|
|
94
|
+
assert "schema" in dumped["config"]
|
|
95
|
+
assert dumped["config"]["schema"] == schema
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_default_configs():
|
|
99
|
+
"""测试使用默认配置"""
|
|
100
|
+
parse_stage = ParseStage()
|
|
101
|
+
assert parse_stage.config.provider == "textin"
|
|
102
|
+
|
|
103
|
+
chunk_stage = ChunkStage()
|
|
104
|
+
assert chunk_stage.config.strategy == "basic"
|
|
105
|
+
assert chunk_stage.config.max_characters == 1024
|
|
106
|
+
|
|
107
|
+
embed_stage = EmbedStage()
|
|
108
|
+
assert embed_stage.config.provider == "qwen"
|
|
109
|
+
assert embed_stage.config.model_name == "text-embedding-v3"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_stage_json_serialization():
|
|
113
|
+
"""测试 Stage 的 JSON 序列化"""
|
|
114
|
+
stage = ParseStage(config=ParseConfig(provider="mineru", custom="value"))
|
|
115
|
+
|
|
116
|
+
# 序列化
|
|
117
|
+
dumped = stage.model_dump()
|
|
118
|
+
assert dumped == {
|
|
119
|
+
"type": "parse",
|
|
120
|
+
"config": {
|
|
121
|
+
"provider": "mineru",
|
|
122
|
+
"custom": "value"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# 反序列化
|
|
127
|
+
restored = ParseStage.model_validate(dumped)
|
|
128
|
+
assert restored.config.provider == "mineru"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_chunk_config_constraints():
|
|
132
|
+
"""测试 ChunkConfig 的约束"""
|
|
133
|
+
# 正常值
|
|
134
|
+
ChunkConfig(new_after_n_chars=100, max_characters=500, overlap=10)
|
|
135
|
+
|
|
136
|
+
# 负数应该失败
|
|
137
|
+
with pytest.raises(ValidationError):
|
|
138
|
+
ChunkConfig(new_after_n_chars=-1)
|
|
139
|
+
|
|
140
|
+
with pytest.raises(ValidationError):
|
|
141
|
+
ChunkConfig(max_characters=0)
|
|
142
|
+
|
|
143
|
+
with pytest.raises(ValidationError):
|
|
144
|
+
ChunkConfig(overlap=-10)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""测试 Workflows API 的数据模型"""
|
|
2
|
+
|
|
3
|
+
from xparse_client.models import ParseConfig, ParseStage
|
|
4
|
+
from xparse_client.models.workflows import Schedule, WorkflowInformation, WorkflowState
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_schedule_creation():
|
|
8
|
+
"""测试 Schedule 创建"""
|
|
9
|
+
schedule = Schedule(cron="0 0 * * *")
|
|
10
|
+
assert schedule.cron == "0 0 * * *"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_workflow_information_creation():
|
|
14
|
+
"""测试 WorkflowInformation 创建"""
|
|
15
|
+
workflow = WorkflowInformation(
|
|
16
|
+
workflow_id="wf_123",
|
|
17
|
+
name="daily-processing",
|
|
18
|
+
source_id="src_456",
|
|
19
|
+
destination_id="dst_789",
|
|
20
|
+
stages=[],
|
|
21
|
+
schedule=Schedule(cron="0 0 * * *"),
|
|
22
|
+
state=WorkflowState.ACTIVE,
|
|
23
|
+
created_at="2026-01-27T10:00:00Z",
|
|
24
|
+
updated_at="2026-01-27T10:00:00Z"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert workflow.workflow_id == "wf_123"
|
|
28
|
+
assert workflow.name == "daily-processing"
|
|
29
|
+
assert workflow.state == WorkflowState.ACTIVE
|
|
30
|
+
assert workflow.schedule is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_workflow_with_stages():
|
|
34
|
+
"""测试带 stages 的工作流"""
|
|
35
|
+
workflow = WorkflowInformation(
|
|
36
|
+
workflow_id="wf_123",
|
|
37
|
+
name="parse-workflow",
|
|
38
|
+
source_id="src_456",
|
|
39
|
+
destination_id="dst_789",
|
|
40
|
+
stages=[ParseStage(config=ParseConfig(provider="textin"))],
|
|
41
|
+
schedule=None,
|
|
42
|
+
state=WorkflowState.ACTIVE,
|
|
43
|
+
created_at="2026-01-27T10:00:00Z",
|
|
44
|
+
updated_at="2026-01-27T10:00:00Z"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert len(workflow.stages) == 1
|
|
48
|
+
assert workflow.schedule is None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_workflow_states():
|
|
52
|
+
"""测试工作流状态枚举"""
|
|
53
|
+
assert WorkflowState.ACTIVE == "active"
|
|
54
|
+
assert WorkflowState.PAUSED == "paused"
|
|
55
|
+
assert WorkflowState.ARCHIVED == "archived"
|