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 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