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.
Files changed (45) hide show
  1. {ttasks-0.2.1 → ttasks-0.3.0}/.github/workflows/docs.yml +4 -1
  2. ttasks-0.3.0/PKG-INFO +79 -0
  3. ttasks-0.3.0/README.md +71 -0
  4. ttasks-0.3.0/docs/index.md +24 -0
  5. ttasks-0.3.0/docs/patterns/finally-tasks.md +115 -0
  6. ttasks-0.3.0/docs/patterns/progress-and-output.md +60 -0
  7. ttasks-0.3.0/docs/patterns/retries-and-cancellation.md +59 -0
  8. ttasks-0.3.0/docs/quickstart.md +51 -0
  9. ttasks-0.3.0/docs/reference/api.md +13 -0
  10. ttasks-0.3.0/docs/tutorials/async-execution.md +48 -0
  11. ttasks-0.3.0/docs/tutorials/graph-workflows.md +70 -0
  12. ttasks-0.3.0/docs/tutorials/task-execution.md +116 -0
  13. ttasks-0.3.0/examples/finally_tasks.py +71 -0
  14. ttasks-0.3.0/mkdocs.yml +43 -0
  15. {ttasks-0.2.1 → ttasks-0.3.0}/pyproject.toml +2 -0
  16. {ttasks-0.2.1 → ttasks-0.3.0}/scripts/preflight.py +3 -2
  17. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/__init__.py +2 -0
  18. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_events.py +9 -0
  19. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_executor.py +254 -15
  20. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_graph.py +42 -0
  21. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_version.py +2 -2
  22. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_e2e.py +105 -0
  23. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_executor.py +738 -1
  24. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_public_api.py +19 -0
  25. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_workflow.py +77 -1
  26. {ttasks-0.2.1 → ttasks-0.3.0}/uv.lock +375 -0
  27. ttasks-0.2.1/PKG-INFO +0 -509
  28. ttasks-0.2.1/README.md +0 -501
  29. {ttasks-0.2.1 → ttasks-0.3.0}/.github/workflows/publish.yml +0 -0
  30. {ttasks-0.2.1 → ttasks-0.3.0}/.gitignore +0 -0
  31. {ttasks-0.2.1 → ttasks-0.3.0}/.python-version +0 -0
  32. {ttasks-0.2.1 → ttasks-0.3.0}/.vscode/launch.json +0 -0
  33. {ttasks-0.2.1 → ttasks-0.3.0}/.vscode/settings.json +0 -0
  34. {ttasks-0.2.1 → ttasks-0.3.0}/main.py +0 -0
  35. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_exceptions.py +0 -0
  36. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_sqlite.py +0 -0
  37. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_store.py +0 -0
  38. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/_task.py +0 -0
  39. {ttasks-0.2.1 → ttasks-0.3.0}/src/ttasks/py.typed +0 -0
  40. {ttasks-0.2.1 → ttasks-0.3.0}/tests/conftest.py +0 -0
  41. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_events.py +0 -0
  42. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_sqlite_store.py +0 -0
  43. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_store.py +0 -0
  44. {ttasks-0.2.1 → ttasks-0.3.0}/tests/test_task.py +0 -0
  45. {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.