xparse-client 0.2.19__py3-none-any.whl → 0.3.0b8__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.
Files changed (75) hide show
  1. example/1_basic_api_usage.py +198 -0
  2. example/2_async_job.py +210 -0
  3. example/3_local_workflow.py +300 -0
  4. example/4_advanced_workflow.py +327 -0
  5. example/README.md +128 -0
  6. example/config_example.json +95 -0
  7. tests/conftest.py +310 -0
  8. tests/unit/__init__.py +1 -0
  9. tests/unit/api/__init__.py +1 -0
  10. tests/unit/api/test_extract.py +232 -0
  11. tests/unit/api/test_local.py +231 -0
  12. tests/unit/api/test_parse.py +374 -0
  13. tests/unit/api/test_pipeline.py +369 -0
  14. tests/unit/api/test_workflows.py +108 -0
  15. tests/unit/connectors/test_ftp.py +525 -0
  16. tests/unit/connectors/test_local_connectors.py +324 -0
  17. tests/unit/connectors/test_milvus.py +368 -0
  18. tests/unit/connectors/test_qdrant.py +399 -0
  19. tests/unit/connectors/test_s3.py +598 -0
  20. tests/unit/connectors/test_smb.py +442 -0
  21. tests/unit/connectors/test_utils.py +335 -0
  22. tests/unit/models/test_local.py +54 -0
  23. tests/unit/models/test_pipeline_stages.py +144 -0
  24. tests/unit/models/test_workflows.py +55 -0
  25. tests/unit/test_base.py +437 -0
  26. tests/unit/test_client.py +110 -0
  27. tests/unit/test_config.py +160 -0
  28. tests/unit/test_exceptions.py +182 -0
  29. tests/unit/test_http.py +562 -0
  30. xparse_client/__init__.py +111 -20
  31. xparse_client/_base.py +188 -0
  32. xparse_client/_client.py +218 -0
  33. xparse_client/_config.py +221 -0
  34. xparse_client/_http.py +351 -0
  35. xparse_client/api/__init__.py +14 -0
  36. xparse_client/api/extract.py +109 -0
  37. xparse_client/api/local.py +225 -0
  38. xparse_client/api/parse.py +209 -0
  39. xparse_client/api/pipeline.py +134 -0
  40. xparse_client/api/workflows.py +204 -0
  41. xparse_client/connectors/__init__.py +45 -0
  42. xparse_client/connectors/_utils.py +138 -0
  43. xparse_client/connectors/destinations/__init__.py +45 -0
  44. xparse_client/connectors/destinations/base.py +116 -0
  45. xparse_client/connectors/destinations/local.py +91 -0
  46. xparse_client/connectors/destinations/milvus.py +229 -0
  47. xparse_client/connectors/destinations/qdrant.py +238 -0
  48. xparse_client/connectors/destinations/s3.py +163 -0
  49. xparse_client/connectors/sources/__init__.py +45 -0
  50. xparse_client/connectors/sources/base.py +74 -0
  51. xparse_client/connectors/sources/ftp.py +278 -0
  52. xparse_client/connectors/sources/local.py +176 -0
  53. xparse_client/connectors/sources/s3.py +232 -0
  54. xparse_client/connectors/sources/smb.py +259 -0
  55. xparse_client/exceptions.py +398 -0
  56. xparse_client/models/__init__.py +60 -0
  57. xparse_client/models/chunk.py +39 -0
  58. xparse_client/models/embed.py +62 -0
  59. xparse_client/models/extract.py +41 -0
  60. xparse_client/models/local.py +38 -0
  61. xparse_client/models/parse.py +132 -0
  62. xparse_client/models/pipeline.py +134 -0
  63. xparse_client/models/workflows.py +74 -0
  64. xparse_client-0.3.0b8.dist-info/METADATA +1075 -0
  65. xparse_client-0.3.0b8.dist-info/RECORD +68 -0
  66. {xparse_client-0.2.19.dist-info → xparse_client-0.3.0b8.dist-info}/WHEEL +1 -1
  67. {xparse_client-0.2.19.dist-info → xparse_client-0.3.0b8.dist-info}/licenses/LICENSE +1 -1
  68. {xparse_client-0.2.19.dist-info → xparse_client-0.3.0b8.dist-info}/top_level.txt +2 -0
  69. xparse_client/pipeline/__init__.py +0 -3
  70. xparse_client/pipeline/config.py +0 -129
  71. xparse_client/pipeline/destinations.py +0 -489
  72. xparse_client/pipeline/pipeline.py +0 -690
  73. xparse_client/pipeline/sources.py +0 -583
  74. xparse_client-0.2.19.dist-info/METADATA +0 -1050
  75. xparse_client-0.2.19.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
+ # 序列化检查(使用 by_alias=True 来确保 schema_ 序列化为 schema)
93
+ dumped = stage.model_dump(by_alias=True)
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"