ttasks 0.2.1__tar.gz → 0.3.0__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.
- {ttasks-0.2.1 → ttasks-0.3.0}/.github/workflows/docs.yml +4 -1
- ttasks-0.3.0/PKG-INFO +79 -0
- ttasks-0.3.0/README.md +71 -0
- ttasks-0.3.0/docs/index.md +24 -0
- ttasks-0.3.0/docs/patterns/finally-tasks.md +115 -0
- ttasks-0.3.0/docs/patterns/progress-and-output.md +60 -0
- ttasks-0.3.0/docs/patterns/retries-and-cancellation.md +59 -0
- ttasks-0.3.0/docs/quickstart.md +51 -0
- ttasks-0.3.0/docs/reference/api.md +13 -0
- ttasks-0.3.0/docs/tutorials/async-execution.md +48 -0
- ttasks-0.3.0/docs/tutorials/graph-workflows.md +70 -0
- ttasks-0.3.0/docs/tutorials/task-execution.md +116 -0
- ttasks-0.3.0/examples/finally_tasks.py +71 -0
- ttasks-0.3.0/mkdocs.yml +43 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/pyproject.toml +2 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/scripts/preflight.py +3 -2
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/__init__.py +2 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_events.py +9 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_executor.py +254 -15
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_graph.py +42 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_version.py +2 -2
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_e2e.py +105 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_executor.py +738 -1
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_public_api.py +19 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_workflow.py +77 -1
- {ttasks-0.2.1 → ttasks-0.3.0}/uv.lock +375 -0
- ttasks-0.2.1/PKG-INFO +0 -509
- ttasks-0.2.1/README.md +0 -501
- {ttasks-0.2.1 → ttasks-0.3.0}/.github/workflows/publish.yml +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/.gitignore +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/.python-version +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/.vscode/launch.json +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/.vscode/settings.json +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/main.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_exceptions.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_sqlite.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_store.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_task.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/py.typed +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/conftest.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_events.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_sqlite_store.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_store.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_task.py +0 -0
- {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_task_factories.py +0 -0
|
@@ -35,8 +35,11 @@ jobs:
|
|
|
35
35
|
- name: Install dependencies
|
|
36
36
|
run: uv sync --group dev
|
|
37
37
|
|
|
38
|
+
- name: Build user documentation
|
|
39
|
+
run: uv run mkdocs build --strict --site-dir site
|
|
40
|
+
|
|
38
41
|
- name: Build API documentation
|
|
39
|
-
run: uv run pdoc ttasks --output-directory site
|
|
42
|
+
run: uv run pdoc ttasks --output-directory site/api
|
|
40
43
|
|
|
41
44
|
- name: Upload Pages artifact
|
|
42
45
|
uses: actions/upload-pages-artifact@v3
|
ttasks-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ttasks
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: github-copilot-sdk>=0.1.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# ttasks
|
|
10
|
+
|
|
11
|
+
A small Python task ledger, executor, and DAG workflow library.
|
|
12
|
+
|
|
13
|
+
`ttasks` models work as `Task` objects, executes them through a configurable
|
|
14
|
+
`TaskExecutor`, persists them through an optional `Store`, and runs dependency
|
|
15
|
+
graphs with `TaskGraph`.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Python 3.12+
|
|
20
|
+
- `uv` for the development workflow used by this repository
|
|
21
|
+
- GitHub Copilot authentication for `TaskType.PROMPT` and `TaskType.AGENT` tasks
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from ttasks import Task, TaskExecutor
|
|
27
|
+
|
|
28
|
+
executor = TaskExecutor()
|
|
29
|
+
task = Task.bash("echo hello", title="Say hello")
|
|
30
|
+
|
|
31
|
+
result = executor.execute(task)
|
|
32
|
+
|
|
33
|
+
assert task.is_done
|
|
34
|
+
assert result.output == "hello\n"
|
|
35
|
+
assert task.result is result
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What it provides
|
|
39
|
+
|
|
40
|
+
- `Task` and `TaskResult` domain objects for tracking work and outcomes.
|
|
41
|
+
- `TaskExecutor` for running Bash, PowerShell, Copilot prompt, Copilot agent, or
|
|
42
|
+
custom handler tasks.
|
|
43
|
+
- Event streams for lifecycle, progress, and subprocess output updates.
|
|
44
|
+
- `TaskGraph` for dependency-ordered DAG workflows with finally tasks.
|
|
45
|
+
- In-memory and SQLite stores for task and graph persistence.
|
|
46
|
+
- Async single-task submission, graceful shutdown, cancellation, timeouts, and
|
|
47
|
+
opt-in single-task retries.
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
User docs are published with the generated API reference on GitHub Pages:
|
|
52
|
+
|
|
53
|
+
- [Quickstart](https://ipdelete.github.io/ttasks/quickstart/)
|
|
54
|
+
- [Tutorials](https://ipdelete.github.io/ttasks/tutorials/task-execution/)
|
|
55
|
+
- [Patterns](https://ipdelete.github.io/ttasks/patterns/finally-tasks/)
|
|
56
|
+
- [API reference](https://ipdelete.github.io/ttasks/api/)
|
|
57
|
+
|
|
58
|
+
Build the docs locally:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv run mkdocs build --strict --site-dir site
|
|
62
|
+
uv run pdoc ttasks --output-directory site/api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
Run the project checks:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run ruff check .
|
|
71
|
+
uv run ty check
|
|
72
|
+
uv run pytest
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run the full suite including live tests:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv run pytest -o addopts='' --cov=ttasks --cov-report=term-missing --cov-fail-under=100
|
|
79
|
+
```
|
ttasks-0.3.0/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ttasks
|
|
2
|
+
|
|
3
|
+
A small Python task ledger, executor, and DAG workflow library.
|
|
4
|
+
|
|
5
|
+
`ttasks` models work as `Task` objects, executes them through a configurable
|
|
6
|
+
`TaskExecutor`, persists them through an optional `Store`, and runs dependency
|
|
7
|
+
graphs with `TaskGraph`.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Python 3.12+
|
|
12
|
+
- `uv` for the development workflow used by this repository
|
|
13
|
+
- GitHub Copilot authentication for `TaskType.PROMPT` and `TaskType.AGENT` tasks
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from ttasks import Task, TaskExecutor
|
|
19
|
+
|
|
20
|
+
executor = TaskExecutor()
|
|
21
|
+
task = Task.bash("echo hello", title="Say hello")
|
|
22
|
+
|
|
23
|
+
result = executor.execute(task)
|
|
24
|
+
|
|
25
|
+
assert task.is_done
|
|
26
|
+
assert result.output == "hello\n"
|
|
27
|
+
assert task.result is result
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What it provides
|
|
31
|
+
|
|
32
|
+
- `Task` and `TaskResult` domain objects for tracking work and outcomes.
|
|
33
|
+
- `TaskExecutor` for running Bash, PowerShell, Copilot prompt, Copilot agent, or
|
|
34
|
+
custom handler tasks.
|
|
35
|
+
- Event streams for lifecycle, progress, and subprocess output updates.
|
|
36
|
+
- `TaskGraph` for dependency-ordered DAG workflows with finally tasks.
|
|
37
|
+
- In-memory and SQLite stores for task and graph persistence.
|
|
38
|
+
- Async single-task submission, graceful shutdown, cancellation, timeouts, and
|
|
39
|
+
opt-in single-task retries.
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
User docs are published with the generated API reference on GitHub Pages:
|
|
44
|
+
|
|
45
|
+
- [Quickstart](https://ipdelete.github.io/ttasks/quickstart/)
|
|
46
|
+
- [Tutorials](https://ipdelete.github.io/ttasks/tutorials/task-execution/)
|
|
47
|
+
- [Patterns](https://ipdelete.github.io/ttasks/patterns/finally-tasks/)
|
|
48
|
+
- [API reference](https://ipdelete.github.io/ttasks/api/)
|
|
49
|
+
|
|
50
|
+
Build the docs locally:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run mkdocs build --strict --site-dir site
|
|
54
|
+
uv run pdoc ttasks --output-directory site/api
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
Run the project checks:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run ruff check .
|
|
63
|
+
uv run ty check
|
|
64
|
+
uv run pytest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run the full suite including live tests:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run pytest -o addopts='' --cov=ttasks --cov-report=term-missing --cov-fail-under=100
|
|
71
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ttasks
|
|
2
|
+
|
|
3
|
+
`ttasks` is a small Python task ledger, executor, and DAG workflow library.
|
|
4
|
+
|
|
5
|
+
Use it when you want to model units of work as durable Python objects, execute
|
|
6
|
+
them through explicit handlers, observe progress and output, and compose them
|
|
7
|
+
into dependency graphs.
|
|
8
|
+
|
|
9
|
+
## Start here
|
|
10
|
+
|
|
11
|
+
- [Quickstart](quickstart.md): run one task and one graph.
|
|
12
|
+
- [Task execution tutorial](tutorials/task-execution.md): learn tasks, handlers,
|
|
13
|
+
results, events, and persistence.
|
|
14
|
+
- [Graph workflows tutorial](tutorials/graph-workflows.md): build DAGs with
|
|
15
|
+
dependencies and outcome views.
|
|
16
|
+
- [Finally tasks pattern](patterns/finally-tasks.md): run cleanup, reporting, and
|
|
17
|
+
artifact tasks after success or failure.
|
|
18
|
+
|
|
19
|
+
## API reference
|
|
20
|
+
|
|
21
|
+
The API reference is generated from docstrings with `pdoc` and published
|
|
22
|
+
alongside these guides.
|
|
23
|
+
|
|
24
|
+
<a href="api/">Open the API reference</a>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Finally tasks
|
|
2
|
+
|
|
3
|
+
Finally tasks are graph-scheduling semantics for work that should run after one
|
|
4
|
+
or more upstream tasks become inactive. They are useful for cleanup, reporting,
|
|
5
|
+
artifact collection, and recommendations.
|
|
6
|
+
|
|
7
|
+
They are not Python `finally` blocks, and they are not graph-level retry. They
|
|
8
|
+
are regular tasks with special readiness rules inside `TaskGraph`.
|
|
9
|
+
|
|
10
|
+
## Basic form
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from ttasks import Task, TaskGraph
|
|
14
|
+
|
|
15
|
+
lint = Task.bash("ruff check .", title="Lint")
|
|
16
|
+
test = Task.bash("pytest", title="Tests")
|
|
17
|
+
report = Task.bash("python scripts/report.py", title="Write report")
|
|
18
|
+
|
|
19
|
+
graph = TaskGraph(title="preflight")
|
|
20
|
+
graph.add(lint)
|
|
21
|
+
graph.add(test)
|
|
22
|
+
graph.add(report, after=[lint, test], finally_=True)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The finally task becomes ready once every listed `after` task is no longer
|
|
26
|
+
active, even if one failed, was cancelled, or became blocked.
|
|
27
|
+
|
|
28
|
+
## Cleanup after failed work
|
|
29
|
+
|
|
30
|
+
Use a required finally task for cleanup that must succeed for the graph to be
|
|
31
|
+
healthy.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
build = Task.bash("make build", title="Build")
|
|
35
|
+
cleanup = Task.bash("rm -rf .tmp-build", title="Clean temporary files")
|
|
36
|
+
|
|
37
|
+
graph.add(build)
|
|
38
|
+
graph.add(cleanup, after=[build], finally_=True)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If `build` fails, `cleanup` still runs. If `cleanup` fails, it is a required
|
|
42
|
+
task by default, so `graph.ok` is false.
|
|
43
|
+
|
|
44
|
+
## Reports and artifacts
|
|
45
|
+
|
|
46
|
+
A finally task receives the listed `after` tasks through `context.upstream`, just
|
|
47
|
+
like a normal dependency. Use that to summarize results or collect artifact
|
|
48
|
+
paths.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
def write_report(context):
|
|
52
|
+
lines = []
|
|
53
|
+
for upstream in context.upstream.values():
|
|
54
|
+
result = upstream.result
|
|
55
|
+
lines.append(f"{upstream.title}: {upstream.status.value}")
|
|
56
|
+
if result and result.error:
|
|
57
|
+
lines.append(result.error)
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Only direct dependencies are included. If a report needs an earlier ancestor,
|
|
62
|
+
add that ancestor as an explicit dependency.
|
|
63
|
+
|
|
64
|
+
## Optional recommendations
|
|
65
|
+
|
|
66
|
+
Use `required=False` for best-effort reporting, artifact collection, or Copilot
|
|
67
|
+
recommendation tasks. Their failures are visible, but they do not make
|
|
68
|
+
`graph.ok` false by themselves.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
recommend = Task.prompt(
|
|
72
|
+
"Summarize preflight output and recommend the next action.",
|
|
73
|
+
title="Copilot recommendation",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
graph.add(
|
|
77
|
+
recommend,
|
|
78
|
+
after=[lint, test, report],
|
|
79
|
+
finally_=True,
|
|
80
|
+
required=False,
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This is useful when the primary work should remain authoritative, but optional
|
|
85
|
+
diagnostics should still appear in the run summary.
|
|
86
|
+
|
|
87
|
+
## Required vs optional
|
|
88
|
+
|
|
89
|
+
| Setting | Failure effect | Good use |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `required=True` | Failure makes `graph.ok` false | Cleanup that must complete |
|
|
92
|
+
| `required=False` | Failure is reported but does not make `graph.ok` false | Reports, artifact collection, AI recommendations |
|
|
93
|
+
|
|
94
|
+
Required and optional finally tasks are available through introspection views:
|
|
95
|
+
|
|
96
|
+
- `graph.finally_tasks`
|
|
97
|
+
- `graph.optional_tasks`
|
|
98
|
+
- `graph.required_tasks`
|
|
99
|
+
- `graph.optional_failed`
|
|
100
|
+
- `graph.required_failed`
|
|
101
|
+
- `graph.required_blocked`
|
|
102
|
+
|
|
103
|
+
`graph.ok` remains the authoritative success predicate.
|
|
104
|
+
|
|
105
|
+
## Runnable example
|
|
106
|
+
|
|
107
|
+
The repository includes a local deterministic example:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv run python examples/finally_tasks.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
It demonstrates failed primary work, cleanup that still runs, report generation,
|
|
114
|
+
and an optional recommendation task whose failure is visible without changing
|
|
115
|
+
the required outcome.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Progress and output
|
|
2
|
+
|
|
3
|
+
Every executor has an `EventBus` for task lifecycle events.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from ttasks import TaskEvent, TaskEventType
|
|
7
|
+
|
|
8
|
+
seen: list[TaskEvent] = []
|
|
9
|
+
|
|
10
|
+
with executor.events.subscribed(seen.append):
|
|
11
|
+
executor.execute(task)
|
|
12
|
+
|
|
13
|
+
assert [event.type for event in seen] == [
|
|
14
|
+
TaskEventType.STARTED,
|
|
15
|
+
TaskEventType.SUCCEEDED,
|
|
16
|
+
]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For long-lived subscribers, use `executor.events.subscribe(callback)`, which
|
|
20
|
+
returns an idempotent unsubscribe callable.
|
|
21
|
+
|
|
22
|
+
## Event payloads
|
|
23
|
+
|
|
24
|
+
Events include:
|
|
25
|
+
|
|
26
|
+
- `type`: `STARTED`, `PROGRESS`, `OUTPUT`, `SUCCEEDED`, `FAILED`, `CANCELLED`,
|
|
27
|
+
`BLOCKED`, or `PERSISTENCE_FAILED`
|
|
28
|
+
- `task_id`
|
|
29
|
+
- `task`
|
|
30
|
+
- `previous_status`
|
|
31
|
+
- `status`
|
|
32
|
+
- `timestamp`
|
|
33
|
+
- `error`, when relevant
|
|
34
|
+
- `progress_percent` and `progress_message`, when `type` is `PROGRESS`
|
|
35
|
+
- `output_stream` and `output_chunk`, when `type` is `OUTPUT`
|
|
36
|
+
|
|
37
|
+
Subscriber exceptions do not fail task execution. They are recorded on
|
|
38
|
+
`executor.events.errors` so observers cannot break the work they observe.
|
|
39
|
+
|
|
40
|
+
## Progress events
|
|
41
|
+
|
|
42
|
+
Handlers can report progress without changing task lifecycle state:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def handler(context):
|
|
46
|
+
context.emit_progress(25, "warming up")
|
|
47
|
+
context.emit_progress(message="still working")
|
|
48
|
+
return "done"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Progress percentages are optional finite values from 0 through 100. They are not
|
|
52
|
+
required to be monotonic.
|
|
53
|
+
|
|
54
|
+
## Streaming subprocess output
|
|
55
|
+
|
|
56
|
+
Built-in subprocess handlers emit `OUTPUT` events as stdout and stderr lines are
|
|
57
|
+
read. Complete stdout and stderr are still retained on the terminal
|
|
58
|
+
`TaskResult`.
|
|
59
|
+
|
|
60
|
+
Output subscribers run synchronously on reader threads, so keep callbacks fast.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Retries and cancellation
|
|
2
|
+
|
|
3
|
+
## Single-task retries
|
|
4
|
+
|
|
5
|
+
Single-task retries are opt-in with `RetryPolicy`.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from ttasks import RetryPolicy, Task, TaskExecutor
|
|
9
|
+
|
|
10
|
+
executor = TaskExecutor()
|
|
11
|
+
task = Task.bash("./flaky-command", title="Flaky command")
|
|
12
|
+
|
|
13
|
+
result = executor.execute(
|
|
14
|
+
task,
|
|
15
|
+
retry_policy=RetryPolicy(max_attempts=3, backoff=0.5),
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`max_attempts` is the total attempt count, including the first run. `backoff` is
|
|
20
|
+
the number of seconds to sleep between failed attempts.
|
|
21
|
+
|
|
22
|
+
Retries apply to single-task `execute()` and `submit()` calls. Graph-level retry
|
|
23
|
+
is not provided by this policy.
|
|
24
|
+
|
|
25
|
+
Each attempt emits its normal lifecycle events, and `task.result` reflects only
|
|
26
|
+
the final attempt.
|
|
27
|
+
|
|
28
|
+
## Cancellation is not retried
|
|
29
|
+
|
|
30
|
+
Cancellation is terminal for retry policy purposes. Handlers may cooperatively
|
|
31
|
+
abort by raising `TaskCancelled`; the executor owns the transition to
|
|
32
|
+
`CANCELLED` and records the terminal `TaskResult`.
|
|
33
|
+
|
|
34
|
+
Cancel a task through the executor to also terminate any active subprocess:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
executor.cancel(task)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For asynchronous work, prefer `executor.cancel(task)` over `future.cancel()`.
|
|
41
|
+
`future.cancel()` can only cancel work that has not started yet.
|
|
42
|
+
|
|
43
|
+
## Timeouts
|
|
44
|
+
|
|
45
|
+
`Task.timeout` defaults to `None`, which means no automatic timeout is applied.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
Task.bash("sleep 30", title="Long task")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use a positive timeout for bounded subprocess execution:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
Task.bash("sleep 30", title="Bounded task", timeout=5)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the timeout is exceeded, the executor terminates the subprocess, marks the
|
|
58
|
+
task failed, stores the timeout message in `task.error`, attaches a failed
|
|
59
|
+
`TaskResult`, and raises `TaskTimeoutError`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
## Run one task
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from ttasks import Task, TaskExecutor
|
|
7
|
+
|
|
8
|
+
executor = TaskExecutor()
|
|
9
|
+
task = Task.bash("echo hello", title="Say hello")
|
|
10
|
+
|
|
11
|
+
result = executor.execute(task)
|
|
12
|
+
|
|
13
|
+
assert task.is_done
|
|
14
|
+
assert result.output == "hello\n"
|
|
15
|
+
assert task.result is result
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`TaskExecutor` registers built-in handlers for Bash, PowerShell, Copilot prompt,
|
|
19
|
+
and Copilot agent tasks. You can override any handler or create an executor with
|
|
20
|
+
no defaults by using `TaskExecutor.empty()`.
|
|
21
|
+
|
|
22
|
+
## Run a graph
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from ttasks import Task, TaskExecutor, TaskGraph
|
|
26
|
+
|
|
27
|
+
build = Task.bash("echo build", title="Build")
|
|
28
|
+
test = Task.bash("echo test", title="Test")
|
|
29
|
+
package = Task.bash("echo package", title="Package")
|
|
30
|
+
|
|
31
|
+
graph = TaskGraph(title="build pipeline")
|
|
32
|
+
graph.add(build)
|
|
33
|
+
graph.add(test, after=[build])
|
|
34
|
+
graph.add(package, after=[test])
|
|
35
|
+
|
|
36
|
+
graph.run(TaskExecutor())
|
|
37
|
+
|
|
38
|
+
assert graph.ok
|
|
39
|
+
assert graph.succeeded == [build, test, package]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If a task fails or is cancelled, downstream tasks are marked blocked and are not
|
|
43
|
+
submitted. Use [finally tasks](patterns/finally-tasks.md) for cleanup and
|
|
44
|
+
reporting work that should still run after failure.
|
|
45
|
+
|
|
46
|
+
## Build docs locally
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv run mkdocs build --strict --site-dir site
|
|
50
|
+
uv run pdoc ttasks --output-directory site/api
|
|
51
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
The API reference is generated from docstrings with `pdoc` as part of the docs
|
|
4
|
+
build.
|
|
5
|
+
|
|
6
|
+
<a href="../../api/">Open the generated API reference</a>
|
|
7
|
+
|
|
8
|
+
Local build:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv run mkdocs build --strict --site-dir site
|
|
12
|
+
uv run pdoc ttasks --output-directory site/api
|
|
13
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Async execution
|
|
2
|
+
|
|
3
|
+
Use `submit()` for asynchronous single-task execution.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from ttasks import Task, TaskExecutor
|
|
7
|
+
|
|
8
|
+
with TaskExecutor() as executor:
|
|
9
|
+
task = Task.bash("echo async", title="Async example")
|
|
10
|
+
future = executor.submit(task)
|
|
11
|
+
result = future.result()
|
|
12
|
+
|
|
13
|
+
assert result.output == "async\n"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`submit()` runs the same `execute()` path, so lifecycle events,
|
|
17
|
+
auto-persistence, progress events, output events, results, and handler errors
|
|
18
|
+
behave the same way as synchronous execution.
|
|
19
|
+
|
|
20
|
+
## Shutdown
|
|
21
|
+
|
|
22
|
+
The worker pool is created lazily on the first `submit()` call.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
executor.shutdown()
|
|
26
|
+
assert executor.is_shutdown
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`shutdown()` is idempotent, rejects later `submit()` calls, and waits for
|
|
30
|
+
already-submitted tasks to finish, including queued tasks that have not started
|
|
31
|
+
yet. It does not cancel running or queued tasks.
|
|
32
|
+
|
|
33
|
+
`close()` is an alias for `shutdown()`, and the context-manager protocol closes
|
|
34
|
+
the executor on exit.
|
|
35
|
+
|
|
36
|
+
## Cancellation
|
|
37
|
+
|
|
38
|
+
Use `executor.cancel(task)` for cooperative cancellation because
|
|
39
|
+
`future.cancel()` can only cancel work that has not started yet.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
future = executor.submit(task)
|
|
43
|
+
executor.cancel(task)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Graph execution owns graph-level scheduling. The async submit API is a
|
|
47
|
+
single-task primitive; graph dependencies and finally-task semantics remain on
|
|
48
|
+
`TaskGraph`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Graph workflows
|
|
2
|
+
|
|
3
|
+
`TaskGraph` runs tasks as a directed acyclic graph. Dependencies must be
|
|
4
|
+
registered in the graph before `run()`.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from ttasks import Task, TaskExecutor, TaskGraph
|
|
8
|
+
|
|
9
|
+
build = Task.bash("echo build", title="Build")
|
|
10
|
+
test = Task.bash("echo test", title="Test")
|
|
11
|
+
package = Task.bash("echo package", title="Package")
|
|
12
|
+
|
|
13
|
+
graph = TaskGraph(title="build pipeline")
|
|
14
|
+
graph.add(build)
|
|
15
|
+
graph.add(test, after=[build])
|
|
16
|
+
graph.add(package, after=[test])
|
|
17
|
+
|
|
18
|
+
graph.run(TaskExecutor())
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Reading outcomes
|
|
22
|
+
|
|
23
|
+
Useful graph views include:
|
|
24
|
+
|
|
25
|
+
- `graph.succeeded`
|
|
26
|
+
- `graph.failed`
|
|
27
|
+
- `graph.cancelled`
|
|
28
|
+
- `graph.blocked`
|
|
29
|
+
- `graph.finally_tasks`
|
|
30
|
+
- `graph.optional_tasks`
|
|
31
|
+
- `graph.required_tasks`
|
|
32
|
+
- `graph.optional_failed`
|
|
33
|
+
- `graph.required_failed`
|
|
34
|
+
- `graph.required_blocked`
|
|
35
|
+
- `graph.errors`
|
|
36
|
+
- `graph.roots()`
|
|
37
|
+
- `graph.leaves()`
|
|
38
|
+
|
|
39
|
+
`graph.ok` is the authoritative success predicate. Optional finally-task
|
|
40
|
+
failures are visible through outcome views without making `graph.ok` false.
|
|
41
|
+
|
|
42
|
+
## Failure behavior
|
|
43
|
+
|
|
44
|
+
If a task fails or is cancelled, downstream tasks are blocked and not submitted.
|
|
45
|
+
Executor or setup errors raised by submitted futures are available in
|
|
46
|
+
`graph.errors`, keyed by task ID.
|
|
47
|
+
|
|
48
|
+
Already-succeeded tasks count as satisfied dependencies, so a graph can be rerun
|
|
49
|
+
or extended after partial completion.
|
|
50
|
+
|
|
51
|
+
## Upstream context
|
|
52
|
+
|
|
53
|
+
When a graph submits a task, its handler receives direct dependency task refs in
|
|
54
|
+
`context.upstream`. The refs come from the graph itself and are keyed by task ID:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
def handler(context):
|
|
58
|
+
parent = context.upstream[build.id]
|
|
59
|
+
assert parent.result is not None
|
|
60
|
+
return parent.result.output.upper()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Only direct dependencies are included. If a task needs an earlier ancestor, add
|
|
64
|
+
that ancestor as an explicit graph dependency.
|
|
65
|
+
|
|
66
|
+
## Finally tasks
|
|
67
|
+
|
|
68
|
+
Use [finally tasks](../patterns/finally-tasks.md) for cleanup, reporting, and
|
|
69
|
+
artifact collection work that should run after dependencies are inactive even if
|
|
70
|
+
they failed or were blocked.
|