tasklite-engine 1.2.0__tar.gz → 1.2.1__tar.gz
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.
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/PKG-INFO +1 -1
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/__init__.py +1 -1
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/inflight.py +6 -2
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/scheduler.py +1 -1
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/store.py +10 -10
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/state.py +13 -4
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/injective.py +53 -14
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/PKG-INFO +1 -1
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/LICENSE +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/README.md +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/pyproject.toml +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/setup.cfg +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/base.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/memory.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/sqlite_backend.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/contrib/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/channel.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/completion.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/config.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/console.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/dispatch.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/governor.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/pacing.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/policy.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/recovery.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/resource.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/runtime.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/session.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/types.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/exceptions.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/hooks.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/context.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/job.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/pipeline.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/py.typed +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/taxonomy.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/ipc.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/jsonutil.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/lockfile.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/__init__.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/discovery.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/http.py +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/SOURCES.txt +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/dependency_links.txt +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/requires.txt +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/top_level.txt +0 -0
- {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tests/test_packaging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasklite-engine
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: A lightweight task orchestration engine with zero external dependencies, process isolation, and ACID persistence
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/CaptainLi321/tasklite
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from collections.abc import MutableMapping
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import (
|
|
7
|
-
Any, Dict, Iterator, List, Mapping, Optional, Tuple, Union, TYPE_CHECKING
|
|
7
|
+
Any, Dict, FrozenSet, Iterator, List, Mapping, Optional, Tuple, Union, TYPE_CHECKING
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -144,7 +144,11 @@ class InFlightTracker(MutableMapping[str, InFlightJob]):
|
|
|
144
144
|
"""语义化结算接缝:注销在途任务并同步内存状态(单一真相源出口)。"""
|
|
145
145
|
entry = self._entries.pop(uid, None)
|
|
146
146
|
if state is not None and hasattr(state, "unregister_in_flight"):
|
|
147
|
-
|
|
147
|
+
# 三态提交内部已注销 in-flight(retry 分支注销后立即 requeue 并
|
|
148
|
+
# 重建 rerun 豁免);此处仅在 uid 仍在途索引时才兜底转发注销,
|
|
149
|
+
# 防止二次注销误删刚重建的豁免(安全网语义不变)。
|
|
150
|
+
if uid in state.in_flight_uids:
|
|
151
|
+
state.unregister_in_flight(uid)
|
|
148
152
|
return entry
|
|
149
153
|
|
|
150
154
|
def unregister(
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import logging
|
|
10
10
|
import time
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Dict, FrozenSet, List, Optional, Tuple, Union, TYPE_CHECKING
|
|
12
|
+
from typing import Any, Dict, FrozenSet, List, Optional, Tuple, Union, TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from .types import HandlerEntry
|
|
@@ -139,10 +139,6 @@ class StateStore:
|
|
|
139
139
|
def stats(self) -> Any:
|
|
140
140
|
return self._stats
|
|
141
141
|
|
|
142
|
-
@property
|
|
143
|
-
def backend(self) -> AbstractStateBackend:
|
|
144
|
-
return self._backend
|
|
145
|
-
|
|
146
142
|
def set_stats(self, stats: Any) -> None:
|
|
147
143
|
self._stats = stats
|
|
148
144
|
|
|
@@ -255,18 +251,22 @@ class StateStore:
|
|
|
255
251
|
|
|
256
252
|
@property
|
|
257
253
|
def wall_uids(self) -> FrozenSet[str]:
|
|
258
|
-
"""已成功作业 UID 集合快照。
|
|
259
|
-
|
|
254
|
+
"""已成功作业 UID 集合快照。
|
|
255
|
+
|
|
256
|
+
不变式:对外只交不可变快照——PipelineState 内部活索引绝不被
|
|
257
|
+
调用方持有引用(误改会静默破坏 wall 去重一致性)。
|
|
258
|
+
"""
|
|
259
|
+
return frozenset(self._state.wall_uids)
|
|
260
260
|
|
|
261
261
|
@property
|
|
262
262
|
def failed_uids(self) -> FrozenSet[str]:
|
|
263
|
-
"""已失败作业 UID
|
|
264
|
-
return self._state.failed_uids
|
|
263
|
+
"""已失败作业 UID 集合快照(不可变契约同 wall_uids)。"""
|
|
264
|
+
return frozenset(self._state.failed_uids)
|
|
265
265
|
|
|
266
266
|
@property
|
|
267
267
|
def queue_uids(self) -> FrozenSet[str]:
|
|
268
|
-
"""排队作业 UID
|
|
269
|
-
return self._state.queue_uids
|
|
268
|
+
"""排队作业 UID 集合快照(不可变契约同 wall_uids)。"""
|
|
269
|
+
return frozenset(self._state.queue_uids)
|
|
270
270
|
|
|
271
271
|
def pop_job(self, idx: int) -> dict:
|
|
272
272
|
"""弹出指定位置作业并同步 UID 索引。"""
|
|
@@ -311,6 +311,14 @@ class PipelineState:
|
|
|
311
311
|
|
|
312
312
|
# 内部 ---------------------------------------------------------
|
|
313
313
|
|
|
314
|
+
def _rerun_exempt_uids_from_queue(self) -> set:
|
|
315
|
+
"""从 queue 事实源直接取重跑豁免集合,不依赖派生缓存
|
|
316
|
+
``_rerun_active_uids``(缓存被瞬态路径误删时断言不得误报)。"""
|
|
317
|
+
return {
|
|
318
|
+
uid_from_job_dict(j) for j in self._queue
|
|
319
|
+
if j.get("rerun") in ("every_run", "on_failure", "on_input_change")
|
|
320
|
+
}
|
|
321
|
+
|
|
314
322
|
def _assert_uids_consistent(self) -> None:
|
|
315
323
|
"""DEBUG 不变式:_queue_uids 与 _queue 完全一致。"""
|
|
316
324
|
recomputed = {uid_from_job_dict(j) for j in self._queue}
|
|
@@ -334,17 +342,18 @@ class PipelineState:
|
|
|
334
342
|
f"uid in both queue and in_flight: {overlap!r} "
|
|
335
343
|
f"(queue={self._queue_uids!r} in_flight={self._in_flight_uids!r})"
|
|
336
344
|
)
|
|
345
|
+
exempt = self._rerun_active_uids | self._rerun_exempt_uids_from_queue()
|
|
337
346
|
done_overlap = (set(self._wall) | set(self._failed)) & self._in_flight_uids
|
|
338
|
-
assert not (done_overlap -
|
|
339
|
-
f"uid in wall/failed and in_flight: {done_overlap -
|
|
347
|
+
assert not (done_overlap - exempt), (
|
|
348
|
+
f"uid in wall/failed and in_flight: {done_overlap - exempt!r}"
|
|
340
349
|
)
|
|
341
350
|
wall_failed = set(self._wall) & set(self._failed)
|
|
342
351
|
assert not wall_failed, (
|
|
343
352
|
f"uid in both wall and failed: {wall_failed!r}"
|
|
344
353
|
)
|
|
345
354
|
done_queue = (set(self._wall) | set(self._failed)) & self._queue_uids
|
|
346
|
-
assert not (done_queue -
|
|
347
|
-
f"uid in wall/failed and queue: {done_queue -
|
|
355
|
+
assert not (done_queue - exempt), (
|
|
356
|
+
f"uid in wall/failed and queue: {done_queue - exempt!r}"
|
|
348
357
|
)
|
|
349
358
|
self._assert_uids_consistent()
|
|
350
359
|
self._assert_terminal_uids_consistent()
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
任何非单射净化(如丢弃字符、简单多对一替换)在拼装 UID 或文件路径时会导致碰撞,
|
|
6
6
|
进而导致 wall 去重静默吞任务或锁竞争错乱。
|
|
7
7
|
2. 转义符优先转义:% 必须首先转义为 %25,因此输出中的 % 唯一且确定地引导一个 %XX 序列。
|
|
8
|
-
|
|
8
|
+
推论:含孤立 %(% 后不跟两位大写 hex)的串不在像集中——哨兵与截断标记均利用此性质
|
|
9
|
+
选取「任何输入都映射不出」的形态。
|
|
10
|
+
3. 超长截断兜底:截断输出 = 前缀 + "%_" + 64 位 SHA-256 指纹。标记 "%_" 使截断输出
|
|
11
|
+
落在转义像集之外,与「转义后 ≤ max_len 直通域」结构性不相交(无定点自碰撞);
|
|
12
|
+
同前缀长输入之间依赖 64 位指纹概率防撞(生日界约 2^32 条)。有界输出对无限定义域
|
|
13
|
+
的全域单射数学上不可能,此处为工程实用界而非数学保证。
|
|
9
14
|
"""
|
|
10
15
|
from __future__ import annotations
|
|
11
16
|
|
|
@@ -21,6 +26,23 @@ DEFAULT_FORBIDDEN = "/\\:%"
|
|
|
21
26
|
# 内容 ID 允许字符集(仅字母、数字、短横线、下划线、点)
|
|
22
27
|
CONTENT_ID_ALLOWED = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
|
|
23
28
|
|
|
29
|
+
# 空值哨兵:含孤立 %(% 后不跟两位大写 hex)。不变式:单射转义的像集中 % 只以
|
|
30
|
+
# %XX 成对出现,故任何非空输入都不可能映射出该形态——空值与真实输入(含字面
|
|
31
|
+
# "untitled")零碰撞。哨兵为固定最短安全形态,不随 max_len 收缩。
|
|
32
|
+
EMPTY_SENTINEL = "%untitled"
|
|
33
|
+
|
|
34
|
+
# 截断指纹宽度:64 位 hex。同前缀长输入之间为概率防撞,生日界约 2^32 条(50% 碰撞
|
|
35
|
+
# 概率所需的同前缀长 ID 量级),工程上视为实用安全界而非数学保证。
|
|
36
|
+
_TRUNC_DIGEST_HEX = 16
|
|
37
|
+
|
|
38
|
+
# 截断标记:含孤立 %(_ 非 hex 位),处于转义像集之外。不变式:直通域输出继承
|
|
39
|
+
# escape_injective 的「% 只以 %XX 成对出现」形态,故截断输出与直通域结构性不相交,
|
|
40
|
+
# 杜绝「截断输出自身作为输入再次净化」时的定点自碰撞。
|
|
41
|
+
_TRUNC_MARKER = "%_"
|
|
42
|
+
|
|
43
|
+
# 截断形态的固定开销(标记 + 指纹),max_len 低于此值时截断无法在不破上限的前提下完成
|
|
44
|
+
_TRUNC_FIXED_LEN = len(_TRUNC_MARKER) + _TRUNC_DIGEST_HEX
|
|
45
|
+
|
|
24
46
|
# 文件系统危险字符映射(POSIX /、Windows \、NUL、glob *?[]、冒号 :)
|
|
25
47
|
FS_ESCAPE_CHARS: Dict[str, str] = {
|
|
26
48
|
"/": "%2F",
|
|
@@ -71,13 +93,20 @@ def sanitize_identifier(
|
|
|
71
93
|
max_len: int = 120,
|
|
72
94
|
forbidden: Union[str, Sequence[str], None] = DEFAULT_FORBIDDEN,
|
|
73
95
|
allowed: Union[str, Sequence[str], None] = None,
|
|
74
|
-
fallback: str =
|
|
96
|
+
fallback: str = EMPTY_SENTINEL,
|
|
75
97
|
) -> str:
|
|
76
|
-
"""净化任意值为确定性、单射安全的标识符(如 job_id 或 content_id
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
"""净化任意值为确定性、单射安全的标识符(如 job_id 或 content_id)。单射契约分级:
|
|
99
|
+
|
|
100
|
+
- 空值(None/"")返回 fallback;默认哨兵 EMPTY_SENTINEL 处于单射转义像集之外,
|
|
101
|
+
任何非空输入都映射不出该值(零碰撞)。自定义 fallback 必须同样选用像集外
|
|
102
|
+
形态(含孤立 %),否则与字面输入的碰撞由调用方自负;哨兵形态固定,不随
|
|
103
|
+
max_len 收缩;
|
|
104
|
+
- 转义后 ≤ max_len 的输入:直通输出,全域单射(继承 escape_injective);
|
|
105
|
+
- 转义后 > max_len 的输入:截断 + "%_" + 64 位 SHA-256 全文指纹。输出带像集外
|
|
106
|
+
标记,与直通域结构性不相交;同前缀长输入之间为概率防撞(生日界约 2^32 条
|
|
107
|
+
同前缀长 ID),非数学全域保证;
|
|
108
|
+
- 需截断而 max_len < _TRUNC_FIXED_LEN 时抛 ValueError(截断形态无法压入上限,
|
|
109
|
+
fail-loud);直通输入对任意 max_len 均可用。
|
|
81
110
|
"""
|
|
82
111
|
text = str(value) if value is not None else ""
|
|
83
112
|
if not text:
|
|
@@ -85,29 +114,39 @@ def sanitize_identifier(
|
|
|
85
114
|
escaped = escape_injective(text, forbidden=forbidden, allowed=allowed)
|
|
86
115
|
if len(escaped) <= max_len:
|
|
87
116
|
return escaped
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
# 指纹覆盖全文(非截断前缀),同前缀不同尾部的长输入由此区分
|
|
118
|
+
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:_TRUNC_DIGEST_HEX]
|
|
119
|
+
base_limit = max_len - _TRUNC_FIXED_LEN
|
|
120
|
+
if base_limit < 0:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"max_len={max_len} 不足以容纳截断指纹形态"
|
|
123
|
+
f"(标记与指纹固定占 {_TRUNC_FIXED_LEN} 字符),拒绝静默突破长度上限"
|
|
124
|
+
)
|
|
90
125
|
prefix = escaped[:base_limit]
|
|
91
126
|
if prefix.endswith("%"):
|
|
92
127
|
prefix = prefix[:-1]
|
|
93
128
|
elif len(prefix) >= 2 and prefix[-2] == "%":
|
|
94
129
|
prefix = prefix[:-2]
|
|
95
|
-
return f"{prefix}
|
|
130
|
+
return f"{prefix}{_TRUNC_MARKER}{digest}"
|
|
96
131
|
|
|
97
132
|
|
|
98
133
|
def sanitize_job_component(value: Any, *, max_len: int = 120) -> str:
|
|
99
|
-
r"""把任意字符串**转义**净化为可作 job_id
|
|
134
|
+
r"""把任意字符串**转义**净化为可作 job_id 成分的串(不删字符)。
|
|
100
135
|
|
|
101
|
-
|
|
136
|
+
单射契约分级见 sanitize_identifier:直通域全域单射;截断输出带像集外标记,
|
|
137
|
+
与直通域结构性不相交,截断域内为 64 位指纹概率防撞。
|
|
102
138
|
"""
|
|
103
139
|
return sanitize_identifier(value, max_len=max_len)
|
|
104
140
|
|
|
105
141
|
|
|
106
142
|
def sanitize_content_id(content_id: str, *, max_len: int = 120) -> str:
|
|
107
|
-
"""按严密 allowlist 净化 content_id(如用于 discovery 子任务派发)。
|
|
143
|
+
"""按严密 allowlist 净化 content_id(如用于 discovery 子任务派发)。
|
|
144
|
+
|
|
145
|
+
空值返回像集外哨兵;单射契约分级与截断行为同 sanitize_identifier。
|
|
146
|
+
"""
|
|
108
147
|
text = str(content_id)
|
|
109
148
|
if not text:
|
|
110
|
-
return
|
|
149
|
+
return EMPTY_SENTINEL
|
|
111
150
|
# 若全部是 clean 字符且未超长,直接返回
|
|
112
151
|
if all(c in CONTENT_ID_ALLOWED for c in text) and len(text) <= max_len:
|
|
113
152
|
return text
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasklite-engine
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: A lightweight task orchestration engine with zero external dependencies, process isolation, and ACID persistence
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/CaptainLi321/tasklite
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|