ttasks 0.2.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.
- ttasks/__init__.py +53 -0
- ttasks/_events.py +96 -0
- ttasks/_exceptions.py +45 -0
- ttasks/_executor.py +645 -0
- ttasks/_graph.py +409 -0
- ttasks/_sqlite.py +587 -0
- ttasks/_store.py +174 -0
- ttasks/_task.py +398 -0
- ttasks/_version.py +24 -0
- ttasks/py.typed +0 -0
- ttasks-0.2.0.dist-info/METADATA +511 -0
- ttasks-0.2.0.dist-info/RECORD +13 -0
- ttasks-0.2.0.dist-info/WHEEL +4 -0
ttasks/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""ttasks — a small task runner with DAG support.
|
|
2
|
+
|
|
3
|
+
The public surface is intentionally flat: ``from ttasks import X`` is the
|
|
4
|
+
canonical and only supported import path for every name in ``__all__``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._events import EventBus, TaskEvent, TaskEventType
|
|
8
|
+
from ._exceptions import TaskCancelled, TaskExecutionError, TaskTimeoutError
|
|
9
|
+
from ._executor import (
|
|
10
|
+
TaskContext,
|
|
11
|
+
TaskExecutor,
|
|
12
|
+
make_copilot_agent_handler,
|
|
13
|
+
make_copilot_prompt_handler,
|
|
14
|
+
)
|
|
15
|
+
from ._graph import TaskGraph
|
|
16
|
+
from ._sqlite import SQLiteStore
|
|
17
|
+
from ._store import (
|
|
18
|
+
InMemoryStore,
|
|
19
|
+
Store,
|
|
20
|
+
)
|
|
21
|
+
from ._task import Task, TaskResult, TaskStatus, TaskType
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"EventBus",
|
|
25
|
+
"InMemoryStore",
|
|
26
|
+
"SQLiteStore",
|
|
27
|
+
"Store",
|
|
28
|
+
"Task",
|
|
29
|
+
"TaskCancelled",
|
|
30
|
+
"TaskContext",
|
|
31
|
+
"TaskExecutionError",
|
|
32
|
+
"TaskEvent",
|
|
33
|
+
"TaskEventType",
|
|
34
|
+
"TaskExecutor",
|
|
35
|
+
"TaskGraph",
|
|
36
|
+
"TaskResult",
|
|
37
|
+
"TaskTimeoutError",
|
|
38
|
+
"TaskStatus",
|
|
39
|
+
"TaskType",
|
|
40
|
+
"make_copilot_agent_handler",
|
|
41
|
+
"make_copilot_prompt_handler",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Rewrite ``__module__`` on every public name so tracebacks and ``repr()`` show
|
|
45
|
+
# ``ttasks.TaskTimeoutError`` rather than the internal submodule path. This is
|
|
46
|
+
# the same idiom used verbatim by httpx, openai-python, and anthropic-python.
|
|
47
|
+
import contextlib as _contextlib
|
|
48
|
+
|
|
49
|
+
_locals = locals()
|
|
50
|
+
for _name in __all__:
|
|
51
|
+
with _contextlib.suppress(TypeError, AttributeError):
|
|
52
|
+
_locals[_name].__module__ = "ttasks"
|
|
53
|
+
del _locals, _name, _contextlib
|
ttasks/_events.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Task event types and publish/subscribe helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from threading import RLock
|
|
11
|
+
|
|
12
|
+
from ._task import Task, TaskStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskEventType(Enum):
|
|
16
|
+
"""Kinds of task events emitted during execution."""
|
|
17
|
+
|
|
18
|
+
STARTED = "started"
|
|
19
|
+
SUCCEEDED = "succeeded"
|
|
20
|
+
FAILED = "failed"
|
|
21
|
+
CANCELLED = "cancelled"
|
|
22
|
+
BLOCKED = "blocked"
|
|
23
|
+
PERSISTENCE_FAILED = "persistence_failed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class TaskEvent:
|
|
28
|
+
"""A task execution event delivered to event subscribers."""
|
|
29
|
+
|
|
30
|
+
type: TaskEventType
|
|
31
|
+
task_id: str
|
|
32
|
+
task: Task
|
|
33
|
+
timestamp: datetime
|
|
34
|
+
previous_status: TaskStatus | None
|
|
35
|
+
status: TaskStatus
|
|
36
|
+
error: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
TaskEventHandler = Callable[[TaskEvent], None]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EventBus:
|
|
43
|
+
"""Thread-safe publish/subscribe bus for task events."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
"""Create an event bus with no subscribers or recorded errors."""
|
|
47
|
+
self._subscribers: list[TaskEventHandler] = []
|
|
48
|
+
self._errors: list[BaseException] = []
|
|
49
|
+
self._lock = RLock()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def errors(self) -> list[BaseException]:
|
|
53
|
+
"""Return subscriber errors recorded while emitting events."""
|
|
54
|
+
with self._lock:
|
|
55
|
+
return list(self._errors)
|
|
56
|
+
|
|
57
|
+
def subscribe(self, subscriber: TaskEventHandler) -> Callable[[], None]:
|
|
58
|
+
"""Register subscriber and return an idempotent unsubscribe callback."""
|
|
59
|
+
if not callable(subscriber):
|
|
60
|
+
raise TypeError("subscriber must be callable")
|
|
61
|
+
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._subscribers.append(subscriber)
|
|
64
|
+
|
|
65
|
+
def unsubscribe() -> None:
|
|
66
|
+
"""Remove subscriber if it is still registered."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
if subscriber in self._subscribers:
|
|
69
|
+
self._subscribers.remove(subscriber)
|
|
70
|
+
|
|
71
|
+
return unsubscribe
|
|
72
|
+
|
|
73
|
+
@contextmanager
|
|
74
|
+
def subscribed(self, subscriber: TaskEventHandler) -> Iterator[None]:
|
|
75
|
+
"""Subscribe for the duration of a ``with`` block, then auto-unsubscribe.
|
|
76
|
+
|
|
77
|
+
The callback is unsubscribed on normal exit and on exception so a
|
|
78
|
+
short-lived observer cannot leak past its scope.
|
|
79
|
+
"""
|
|
80
|
+
unsubscribe = self.subscribe(subscriber)
|
|
81
|
+
try:
|
|
82
|
+
yield
|
|
83
|
+
finally:
|
|
84
|
+
unsubscribe()
|
|
85
|
+
|
|
86
|
+
def emit(self, event: TaskEvent) -> None:
|
|
87
|
+
"""Publish event to subscribers without letting observers fail execution."""
|
|
88
|
+
with self._lock:
|
|
89
|
+
subscribers = list(self._subscribers)
|
|
90
|
+
|
|
91
|
+
for subscriber in subscribers:
|
|
92
|
+
try:
|
|
93
|
+
subscriber(event)
|
|
94
|
+
except BaseException as error:
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._errors.append(error)
|
ttasks/_exceptions.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Exceptions raised by task execution.
|
|
2
|
+
|
|
3
|
+
Kept in a dedicated module so users can catch the full hierarchy without
|
|
4
|
+
importing executor machinery and so the executor module stays focused on
|
|
5
|
+
execution logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskCancelled(RuntimeError):
|
|
14
|
+
"""Signal that task execution was cancelled.
|
|
15
|
+
|
|
16
|
+
Handlers should not mutate Task lifecycle state directly. They may raise
|
|
17
|
+
TaskCancelled to cooperatively abort; TaskExecutor owns the transition to
|
|
18
|
+
CANCELLED and records the terminal TaskResult.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskExecutionError(RuntimeError):
|
|
23
|
+
"""Raised when a subprocess exits unsuccessfully.
|
|
24
|
+
|
|
25
|
+
completed preserves stdout, stderr, and returncode so TaskExecutor can
|
|
26
|
+
attach structured failure details to Task.result instead of keeping only
|
|
27
|
+
the exception string.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, completed: subprocess.CompletedProcess[str]):
|
|
31
|
+
"""Create an execution error for completed."""
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.completed = completed
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskTimeoutError(TimeoutError):
|
|
37
|
+
"""Raised when a subprocess exceeds its timeout.
|
|
38
|
+
|
|
39
|
+
completed preserves any output collected after terminating the process.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, completed: subprocess.CompletedProcess[str]):
|
|
43
|
+
"""Create a timeout error for completed."""
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.completed = completed
|