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.
Files changed (51) hide show
  1. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/PKG-INFO +1 -1
  2. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/__init__.py +1 -1
  3. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/inflight.py +6 -2
  4. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/scheduler.py +1 -1
  5. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/store.py +10 -10
  6. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/state.py +13 -4
  7. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/injective.py +53 -14
  8. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/PKG-INFO +1 -1
  9. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/LICENSE +0 -0
  10. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/README.md +0 -0
  11. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/pyproject.toml +0 -0
  12. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/setup.cfg +0 -0
  13. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/__init__.py +0 -0
  14. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/base.py +0 -0
  15. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/memory.py +0 -0
  16. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/backend/sqlite_backend.py +0 -0
  17. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/contrib/__init__.py +0 -0
  18. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/__init__.py +0 -0
  19. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/channel.py +0 -0
  20. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/completion.py +0 -0
  21. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/config.py +0 -0
  22. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/console.py +0 -0
  23. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/dispatch.py +0 -0
  24. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/governor.py +0 -0
  25. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/pacing.py +0 -0
  26. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/policy.py +0 -0
  27. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/recovery.py +0 -0
  28. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/resource.py +0 -0
  29. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/runtime.py +0 -0
  30. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/session.py +0 -0
  31. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/engine/types.py +0 -0
  32. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/exceptions.py +0 -0
  33. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/hooks.py +0 -0
  34. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/__init__.py +0 -0
  35. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/context.py +0 -0
  36. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/models/job.py +0 -0
  37. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/pipeline.py +0 -0
  38. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/py.typed +0 -0
  39. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/taxonomy.py +0 -0
  40. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/__init__.py +0 -0
  41. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/ipc.py +0 -0
  42. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/jsonutil.py +0 -0
  43. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/utils/lockfile.py +0 -0
  44. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/__init__.py +0 -0
  45. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/discovery.py +0 -0
  46. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite/wrappers/http.py +0 -0
  47. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/SOURCES.txt +0 -0
  48. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/dependency_links.txt +0 -0
  49. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/requires.txt +0 -0
  50. {tasklite_engine-1.2.0 → tasklite_engine-1.2.1}/tasklite_engine.egg-info/top_level.txt +0 -0
  51. {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.0
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 @@ A lightweight, battle-hardened task orchestration engine with zero external
4
4
  dependencies, process isolation, and ACID persistence.
5
5
  """
6
6
 
7
- __version__ = "1.2.0"
7
+ __version__ = "1.2.1"
8
8
 
9
9
 
10
10
  # Public API - Core
@@ -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
- state.unregister_in_flight(uid)
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
- return self._state.wall_uids
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 - self._rerun_active_uids), (
339
- f"uid in wall/failed and in_flight: {done_overlap - self._rerun_active_uids!r}"
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 - self._rerun_active_uids), (
347
- f"uid in wall/failed and queue: {done_queue - self._rerun_active_uids!r}"
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
- 3. 超长截断单射兜底:截断时附加 SHA-256 摘要(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 = "untitled",
96
+ fallback: str = EMPTY_SENTINEL,
75
97
  ) -> str:
76
- """净化任意值为确定性、单射安全的标识符(如 job_id 或 content_id)。
77
-
78
- - 空值返回 fallback(默认 "untitled");
79
- - 禁止字符与不可打印字符可逆单射转义;
80
- - 超长输入(转义后 > max_len):截断 + 8 位 SHA-256 指纹后缀。
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
- digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:8]
89
- base_limit = max(max_len - 9, 0)
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}_{digest}"
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
- 单射转义与长度截断统一委托底层 sanitize_identifier
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 "untitled"
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.0
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