tasklite-engine 1.0.0__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.
@@ -0,0 +1,177 @@
1
+ """Payload validation utilities."""
2
+ import math
3
+ import types
4
+ import typing
5
+
6
+ _UnionType = getattr(types, "UnionType", None)
7
+
8
+
9
+ def validate_resource_amounts(resources: dict, where: str) -> None:
10
+ """校验资源 amount 数值(Job.__init__ 与 pipeline 注册路径共用单点)。
11
+
12
+ 拒绝三态:非数值(含 bool——bool 是 int 子类)/ NaN·Inf(毒化
13
+ CapacityResource used 账目 → livelock)/ 负值(acquire 时抛 ValueError
14
+ → try 之外资源永久泄漏)。``where`` 用于错误消息定位调用来源。
15
+ """
16
+ for res_name, amount in resources.items():
17
+ if not isinstance(amount, (int, float)) or isinstance(amount, bool):
18
+ raise TypeError(
19
+ f"resource '{res_name}' amount in {where} must be a number, "
20
+ f"got {type(amount).__name__} ({amount!r})"
21
+ )
22
+ try:
23
+ finite = math.isfinite(amount)
24
+ except OverflowError:
25
+ # 超大 int(>1.8e308,如 10**400)让 math.isfinite
26
+ # 抛 OverflowError 而非返回 False——用户拿到无定位的原始异常,
27
+ # 且逃逸调度器 malformed 兜底(捕获元组不含 OverflowError)
28
+ # → 磁盘脏数据可崩整轮扫描。转为标准 ValueError(与有限性
29
+ # 校验同语义:超出 float 表示范围的量不可用)。
30
+ raise ValueError(
31
+ f"resource '{res_name}' amount in {where} is too large "
32
+ f"(exceeds float range), got {amount!r}"
33
+ )
34
+ if not finite:
35
+ raise ValueError(
36
+ f"resource '{res_name}' amount in {where} must be finite, got {amount!r}"
37
+ )
38
+ if amount < 0:
39
+ raise ValueError(
40
+ f"resource '{res_name}' amount in {where} must be non-negative, got {amount!r}"
41
+ )
42
+
43
+
44
+ def validate_payload(payload: dict, schema: type) -> list:
45
+ """Validate payload against a TypedDict schema using runtime type hints.
46
+
47
+ Returns a list of error strings. Empty list means valid.
48
+ Uses typing.get_type_hints() for runtime type resolution (Appendix D.1).
49
+ Handles parameterized generics (e.g. list[str]) via typing.get_origin(),
50
+ and Union/Optional types (e.g. str | None) by checking against member types.
51
+
52
+ Note: Validation is intentionally shallow — nested TypedDict fields are
53
+ checked only at the top level (isinstance dict), not recursively. This
54
+ keeps validation fast and avoids deep-type-resolution complexity.
55
+
56
+ (never-raise 契约):本函数**永不抛异常**——schema 畸形(Union 含
57
+ Any/TypeVar 等不可 isinstance 的成员)或 payload 形状异常时,返回
58
+ ``["schema error: ..."]`` 而非上抛。调用链(_dispatch_job → except
59
+ Exception → requeue + raise)会让校验系统自身的故障打崩整个 run 并
60
+ 形成无限崩溃循环;校验故障应视为校验失败(job 进 DLQ),绝不崩 run。
61
+ """
62
+ try:
63
+ return _validate_payload_impl(payload, schema)
64
+ except Exception as e:
65
+ # 兜底:任何未预期异常(含未来 schema 类型演化)→ 校验失败
66
+ return [f"schema error: {type(e).__name__}: {e}"]
67
+
68
+
69
+ def _validate_payload_impl(payload: dict, schema: type) -> list:
70
+ errors = []
71
+ # payload 必须为 dict——非 dict(str/int/None)会在下方
72
+ # `key not in payload`/`payload[key]` 处抛 TypeError(str 索引 int、
73
+ # int 不可迭代),上抛到 _dispatch_job → 整个 run 崩溃。入口返回错误串。
74
+ if not isinstance(payload, dict):
75
+ return [f"payload must be a dict, got {type(payload).__name__}"]
76
+ try:
77
+ hints = typing.get_type_hints(schema)
78
+ except Exception as e:
79
+ return [f"schema resolution failed: {e}"]
80
+ # TypedDict(total=False) 的可选字段允许缺失;无 __required_keys__
81
+ # 属性时视为全必填。
82
+ required_keys = getattr(schema, "__required_keys__", None)
83
+ for key, expected_type in hints.items():
84
+ if key not in payload:
85
+ if required_keys is not None and key not in required_keys:
86
+ continue
87
+ errors.append(f"missing required field '{key}'")
88
+ else:
89
+ value = payload[key]
90
+ origin = typing.get_origin(expected_type)
91
+ if origin is typing.Literal:
92
+ # Literal[...] 不能传给 isinstance,直接比较取值范围。
93
+ # True == 1 会让 bool 值穿过 Literal[1, 2];
94
+ # 判定用「值相等且类型一致」——True 拒绝、1 接受。
95
+ if not any(
96
+ value == allowed and type(value) is type(allowed)
97
+ for allowed in typing.get_args(expected_type)
98
+ ):
99
+ type_name = str(expected_type)
100
+ errors.append(
101
+ f"field '{key}' expected {type_name}, "
102
+ f"got {type(value).__name__}"
103
+ )
104
+ continue
105
+ # bool 是 int 的子类:int 字段显式拒绝 True/False(防止载荷类型静默错位)
106
+ if origin is None and expected_type is int and isinstance(value, bool):
107
+ errors.append(
108
+ f"field '{key}' expected int, got bool"
109
+ )
110
+ continue
111
+ # Unwrap parameterized generics (e.g. list[str] -> list).
112
+ # Union types (str | None / Optional[str]) must be checked against
113
+ # their member types so None is accepted for Optional fields.
114
+ is_union = (origin is typing.Union or (_UnionType is not None and origin is _UnionType))
115
+ if is_union:
116
+ check_type = tuple(
117
+ (typing.get_origin(a) or a) for a in typing.get_args(expected_type)
118
+ )
119
+ else:
120
+ check_type = origin or expected_type
121
+ try:
122
+ valid = isinstance(value, check_type)
123
+ except TypeError:
124
+ # union 含 Literal 成员时 isinstance(value, Literal[...])
125
+ # 抛 TypeError 会被误吞(valid=True → 校验形同虚设)——对
126
+ # Literal 成员做值比较(与顶层 Literal 分支同款),
127
+ # 其余成员仍走 isinstance。
128
+ literal_members = [
129
+ a for a in typing.get_args(expected_type)
130
+ if typing.get_origin(a) is typing.Literal
131
+ ]
132
+ if literal_members:
133
+ non_literal = [
134
+ (typing.get_origin(a) or a) for a in typing.get_args(expected_type)
135
+ if typing.get_origin(a) is not typing.Literal
136
+ ]
137
+ literal_ok = any(
138
+ value == allowed and type(value) is type(allowed)
139
+ for m in literal_members
140
+ for allowed in typing.get_args(m)
141
+ )
142
+ # non_literal 可能含 Any/TypeVar 等不可 isinstance 的
143
+ # 成员——过滤掉(isinstance 会再抛 TypeError)。
144
+ # 不可 isinstance 的成员按「放行」处理(无法静态校验)。
145
+ isinst_ok = True
146
+ for t in non_literal:
147
+ if t is typing.Any or isinstance(t, typing.TypeVar):
148
+ continue # 不可 isinstance,放行
149
+ try:
150
+ if not isinstance(value, t):
151
+ isinst_ok = False
152
+ break
153
+ except TypeError:
154
+ continue # 仍不可 isinstance,放行
155
+ valid = literal_ok or isinst_ok
156
+ else:
157
+ valid = True
158
+ if valid and is_union and isinstance(value, bool):
159
+ # isinstance(True, int) 为 True,Union 含 int
160
+ # 成员时 Optional[int] 会放行 True/False。与 int 字段的
161
+ # bool 拒绝语义对齐:成员含 int 但无 bool 时拒绝 bool 值
162
+ # (Optional/None 的接受不受影响)。
163
+ member_types = tuple(
164
+ typing.get_origin(a) or a for a in typing.get_args(expected_type)
165
+ )
166
+ if int in member_types and bool not in member_types:
167
+ valid = False
168
+ if not valid:
169
+ type_name = getattr(expected_type, '__name__', str(expected_type))
170
+ errors.append(
171
+ f"field '{key}' expected {type_name}, "
172
+ f"got {type(value).__name__}"
173
+ )
174
+ for key in payload:
175
+ if key not in hints:
176
+ errors.append(f"unexpected field '{key}'")
177
+ return errors
@@ -0,0 +1,26 @@
1
+ """通用编排模式封装(wrappers):discovery。
2
+
3
+ - discovery:通用增量扫描回调封装(register_discovery + DiscoveryHandler + 协议)。
4
+
5
+ 公共脚手架见 ``tasklite.pipeline_util``。
6
+ """
7
+
8
+ from . import discovery
9
+ from .discovery import (
10
+ DiscoveryContext,
11
+ DiscoveryHandler,
12
+ DiscoveryHost,
13
+ DiscoveryJob,
14
+ register_discovery,
15
+ sanitize_content_id,
16
+ )
17
+
18
+ __all__ = [
19
+ "discovery",
20
+ "DiscoveryContext",
21
+ "DiscoveryHandler",
22
+ "DiscoveryHost",
23
+ "DiscoveryJob",
24
+ "register_discovery",
25
+ "sanitize_content_id",
26
+ ]