PyTaskWireCheck 0.1.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.
- pytaskwirecheck/__init__.py +32 -0
- pytaskwirecheck/api.py +314 -0
- pytaskwirecheck/cache/__init__.py +0 -0
- pytaskwirecheck/cache/fingerprint.py +176 -0
- pytaskwirecheck/cache/policy.py +49 -0
- pytaskwirecheck/cache/store.py +367 -0
- pytaskwirecheck/cli.py +690 -0
- pytaskwirecheck/config/__init__.py +0 -0
- pytaskwirecheck/config/loader.py +72 -0
- pytaskwirecheck/config/schema.py +170 -0
- pytaskwirecheck/config/validation.py +590 -0
- pytaskwirecheck/diagnostics/__init__.py +31 -0
- pytaskwirecheck/diagnostics/errors.py +86 -0
- pytaskwirecheck/engine/__init__.py +0 -0
- pytaskwirecheck/engine/cancellation.py +38 -0
- pytaskwirecheck/engine/events.py +93 -0
- pytaskwirecheck/engine/executor.py +301 -0
- pytaskwirecheck/engine/scheduler.py +312 -0
- pytaskwirecheck/engine/state.py +123 -0
- pytaskwirecheck/graph/__init__.py +0 -0
- pytaskwirecheck/graph/builder.py +33 -0
- pytaskwirecheck/graph/model.py +104 -0
- pytaskwirecheck/graph/planner.py +155 -0
- pytaskwirecheck/graph/validator.py +92 -0
- pytaskwirecheck/history/__init__.py +0 -0
- pytaskwirecheck/history/store.py +164 -0
- pytaskwirecheck/logging/__init__.py +0 -0
- pytaskwirecheck/logging/artifacts.py +95 -0
- pytaskwirecheck/logging/structured.py +83 -0
- pytaskwirecheck/notifications/__init__.py +0 -0
- pytaskwirecheck/notifications/base.py +42 -0
- pytaskwirecheck/notifications/webhook.py +71 -0
- pytaskwirecheck/processes/__init__.py +0 -0
- pytaskwirecheck/processes/runner.py +265 -0
- pytaskwirecheck/processes/signals.py +65 -0
- pytaskwirecheck/py.typed +0 -0
- pytaskwirecheck/tui/__init__.py +0 -0
- pytaskwirecheck/tui/app.py +204 -0
- pytaskwirecheck/tui/renderer.py +143 -0
- pytaskwirecheck-0.1.0.dist-info/METADATA +316 -0
- pytaskwirecheck-0.1.0.dist-info/RECORD +45 -0
- pytaskwirecheck-0.1.0.dist-info/WHEEL +5 -0
- pytaskwirecheck-0.1.0.dist-info/entry_points.txt +2 -0
- pytaskwirecheck-0.1.0.dist-info/licenses/LICENSE +21 -0
- pytaskwirecheck-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Plain-text console reporting.
|
|
2
|
+
|
|
3
|
+
This renderer provides a useful non-TTY fallback and a CI-friendly mode. It
|
|
4
|
+
never assumes ANSI colour support and keeps concurrent task output labelled.
|
|
5
|
+
It only observes events; it never mutates run state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Dict, Optional, TextIO
|
|
12
|
+
|
|
13
|
+
from pytaskwirecheck.engine.events import (
|
|
14
|
+
Event,
|
|
15
|
+
RunFinished,
|
|
16
|
+
RunStarted,
|
|
17
|
+
TaskAttemptFinished,
|
|
18
|
+
TaskAttemptStarted,
|
|
19
|
+
TaskOutput,
|
|
20
|
+
TaskStateChanged,
|
|
21
|
+
)
|
|
22
|
+
from pytaskwirecheck.engine.state import TaskStatus
|
|
23
|
+
|
|
24
|
+
_STATUS_GLYPH = {
|
|
25
|
+
TaskStatus.SUCCEEDED: "ok",
|
|
26
|
+
TaskStatus.FAILED: "fail",
|
|
27
|
+
TaskStatus.CACHED: "cached",
|
|
28
|
+
TaskStatus.SKIPPED: "skip",
|
|
29
|
+
TaskStatus.CANCELLED: "cancel",
|
|
30
|
+
TaskStatus.TIMED_OUT: "timeout",
|
|
31
|
+
TaskStatus.BLOCKED: "blocked",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_COLORS = {
|
|
35
|
+
TaskStatus.SUCCEEDED: "\033[32m",
|
|
36
|
+
TaskStatus.CACHED: "\033[36m",
|
|
37
|
+
TaskStatus.FAILED: "\033[31m",
|
|
38
|
+
TaskStatus.TIMED_OUT: "\033[31m",
|
|
39
|
+
TaskStatus.BLOCKED: "\033[31m",
|
|
40
|
+
TaskStatus.CANCELLED: "\033[33m",
|
|
41
|
+
}
|
|
42
|
+
_RESET = "\033[0m"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PlainReporter:
|
|
46
|
+
"""A synchronous, line-oriented event reporter."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
stream: Optional[TextIO] = None,
|
|
51
|
+
*,
|
|
52
|
+
color: bool = False,
|
|
53
|
+
quiet: bool = False,
|
|
54
|
+
show_output: bool = True,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._stream = stream if stream is not None else sys.stdout
|
|
57
|
+
self._color = color
|
|
58
|
+
self._quiet = quiet
|
|
59
|
+
self._show_output = show_output
|
|
60
|
+
self._buffers: Dict[tuple, str] = {}
|
|
61
|
+
|
|
62
|
+
def _write(self, text: str) -> None:
|
|
63
|
+
try:
|
|
64
|
+
self._stream.write(text)
|
|
65
|
+
self._stream.flush()
|
|
66
|
+
except (BrokenPipeError, ValueError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def _paint(self, status: TaskStatus, text: str) -> str:
|
|
70
|
+
if not self._color:
|
|
71
|
+
return text
|
|
72
|
+
color = _COLORS.get(status)
|
|
73
|
+
return "%s%s%s" % (color, text, _RESET) if color else text
|
|
74
|
+
|
|
75
|
+
def handle(self, event: Event) -> None:
|
|
76
|
+
if isinstance(event, RunStarted):
|
|
77
|
+
if not self._quiet:
|
|
78
|
+
self._write("Run %s: %d task(s)\n" % (event.run_id, event.total_tasks))
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if isinstance(event, TaskAttemptStarted):
|
|
82
|
+
if event.max_attempts > 1 and not self._quiet:
|
|
83
|
+
self._write(
|
|
84
|
+
" %s: attempt %d/%d\n"
|
|
85
|
+
% (event.task_id, event.attempt, event.max_attempts)
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if isinstance(event, TaskAttemptFinished):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if isinstance(event, TaskStateChanged):
|
|
93
|
+
self._on_state_changed(event)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if isinstance(event, TaskOutput):
|
|
97
|
+
self._on_output(event)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
if isinstance(event, RunFinished):
|
|
101
|
+
self._on_run_finished(event)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
def _on_state_changed(self, event: TaskStateChanged) -> None:
|
|
105
|
+
if event.status is TaskStatus.RUNNING:
|
|
106
|
+
if not self._quiet and not self._show_output:
|
|
107
|
+
self._write(" %s: running\n" % event.task_id)
|
|
108
|
+
return
|
|
109
|
+
if not event.status.is_terminal:
|
|
110
|
+
return
|
|
111
|
+
glyph = _STATUS_GLYPH.get(event.status, event.status.value)
|
|
112
|
+
suffix = ""
|
|
113
|
+
if event.status is TaskStatus.FAILED and event.message:
|
|
114
|
+
suffix = " (%s)" % event.message
|
|
115
|
+
elif event.status in (TaskStatus.TIMED_OUT, TaskStatus.BLOCKED):
|
|
116
|
+
if event.message:
|
|
117
|
+
suffix = " (%s)" % event.message
|
|
118
|
+
label = self._paint(event.status, glyph)
|
|
119
|
+
self._write(" [%s] %s%s\n" % (label, event.task_id, suffix))
|
|
120
|
+
|
|
121
|
+
def _on_output(self, event: TaskOutput) -> None:
|
|
122
|
+
if not self._show_output or self._quiet:
|
|
123
|
+
return
|
|
124
|
+
key = (event.task_id, event.stream)
|
|
125
|
+
buffer = self._buffers.get(key, "")
|
|
126
|
+
buffer += event.data.decode("utf-8", errors="replace")
|
|
127
|
+
while "\n" in buffer:
|
|
128
|
+
line, buffer = buffer.split("\n", 1)
|
|
129
|
+
prefix = event.task_id
|
|
130
|
+
if event.stream == "stderr":
|
|
131
|
+
prefix += "!stderr"
|
|
132
|
+
self._write(" %s | %s\n" % (prefix, line))
|
|
133
|
+
self._buffers[key] = buffer
|
|
134
|
+
|
|
135
|
+
def _on_run_finished(self, event: RunFinished) -> None:
|
|
136
|
+
summary = event.summary or {}
|
|
137
|
+
counts = summary.get("counts", {})
|
|
138
|
+
duration = summary.get("duration_seconds", 0.0)
|
|
139
|
+
status = "succeeded" if event.succeeded else "failed"
|
|
140
|
+
rendered = ", ".join("%d %s" % (counts[k], k) for k in sorted(counts))
|
|
141
|
+
self._write(
|
|
142
|
+
"Run %s %s in %.2fs (%s)\n" % (event.run_id, status, duration, rendered)
|
|
143
|
+
)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyTaskWireCheck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first, dependency-aware task orchestration engine (DAG scheduler + content-addressed cache + CLI).
|
|
5
|
+
Author: Georgi Mullassery
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Mullassery/PyTaskWireCheck
|
|
8
|
+
Project-URL: Repository, https://github.com/Mullassery/PyTaskWireCheck
|
|
9
|
+
Project-URL: Issues, https://github.com/Mullassery/PyTaskWireCheck/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Mullassery/PyTaskWireCheck/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/Mullassery/PyTaskWireCheck#readme
|
|
12
|
+
Keywords: task,tasks,orchestration,dag,workflow,scheduler,pipeline,build,local-first,cli
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
36
|
+
Provides-Extra: release
|
|
37
|
+
Requires-Dist: build>=1.0; extra == "release"
|
|
38
|
+
Requires-Dist: twine>=4.0; extra == "release"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
<div align="center">
|
|
42
|
+
|
|
43
|
+
# PyTaskWireCheck
|
|
44
|
+
|
|
45
|
+
**Local-first, dependency-aware task orchestration for Python.**
|
|
46
|
+
|
|
47
|
+
Describe your tasks and how they depend on each other. PyTaskWireCheck works out the
|
|
48
|
+
order, runs independent work in parallel, retries the flaky parts, skips work
|
|
49
|
+
that hasn't changed, and tells you exactly why — without a server, a daemon,
|
|
50
|
+
or a single byte leaving your machine.
|
|
51
|
+
|
|
52
|
+
[](https://github.com/Mullassery/PyTaskWireCheck/actions/workflows/ci.yml)
|
|
53
|
+
[](https://pypi.org/project/PyTaskWireCheck/)
|
|
54
|
+
[](https://pypi.org/project/PyTaskWireCheck/)
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
[](src/pytaskwirecheck/py.typed)
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Why PyTaskWireCheck
|
|
63
|
+
|
|
64
|
+
Most build tools want to own your project. PyTaskWireCheck does one job: it orchestrates
|
|
65
|
+
*other* programs. You already have the scripts, the compilers, the test runners,
|
|
66
|
+
the data jobs — PyTaskWireCheck just knows which ones can run at once, which must wait,
|
|
67
|
+
and which can be skipped because nothing they depend on has changed.
|
|
68
|
+
|
|
69
|
+
- **Local-first.** No server, no account, no telemetry. State lives in
|
|
70
|
+
`.pytaskwirecheck/` inside your project.
|
|
71
|
+
- **Dependency-aware.** Tasks declare `depends_on`; PyTaskWireCheck topologically sorts
|
|
72
|
+
them and runs every independent branch concurrently.
|
|
73
|
+
- **Content-addressed cache.** Tasks declare `inputs` and `outputs`. If the
|
|
74
|
+
fingerprint is unchanged, the task is skipped and its outputs restored.
|
|
75
|
+
- **Deterministic failure handling.** `fail_fast` or continue-and-block, with a
|
|
76
|
+
documented state machine — never a half-observed status.
|
|
77
|
+
- **Scriptable and embeddable.** A first-class CLI *and* a small Python API.
|
|
78
|
+
- **Zero runtime dependencies** on Python 3.11+, one tiny backport otherwise.
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install PyTaskWireCheck
|
|
84
|
+
# or, for the bleeding edge:
|
|
85
|
+
pip install "PyTaskWireCheck @ git+https://github.com/Mullassery/PyTaskWireCheck"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
PyTaskWireCheck targets Python **3.9 – 3.13**. The import name and the command are both
|
|
89
|
+
`pytaskwirecheck`.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pytaskwirecheck --version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pytaskwirecheck init # write a starter pytaskwirecheck.toml
|
|
99
|
+
pytaskwirecheck validate # check the config and the graph
|
|
100
|
+
pytaskwirecheck graph # print the DAG (text, dot, or json)
|
|
101
|
+
pytaskwirecheck plan # show levels, order, and max parallelism
|
|
102
|
+
pytaskwirecheck run # run everything, in dependency order
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A minimal `pytaskwirecheck.toml`:
|
|
106
|
+
|
|
107
|
+
```toml
|
|
108
|
+
[project]
|
|
109
|
+
name = "example-workflow"
|
|
110
|
+
version = 1
|
|
111
|
+
|
|
112
|
+
[execution]
|
|
113
|
+
max_workers = 4
|
|
114
|
+
fail_fast = false
|
|
115
|
+
|
|
116
|
+
[tasks.fetch]
|
|
117
|
+
command = ["python", "scripts/fetch.py"]
|
|
118
|
+
outputs = ["data/raw.json"]
|
|
119
|
+
|
|
120
|
+
[tasks.transform]
|
|
121
|
+
command = ["python", "scripts/transform.py"]
|
|
122
|
+
depends_on = ["fetch"]
|
|
123
|
+
inputs = ["data/raw.json"]
|
|
124
|
+
outputs = ["data/clean.parquet"]
|
|
125
|
+
|
|
126
|
+
[tasks.test]
|
|
127
|
+
command = ["pytest", "-q"]
|
|
128
|
+
depends_on = ["transform"]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Run it:
|
|
132
|
+
|
|
133
|
+
```console
|
|
134
|
+
$ pytaskwirecheck run
|
|
135
|
+
run 3f9c1a2b4d5e
|
|
136
|
+
fetch running
|
|
137
|
+
transform pending
|
|
138
|
+
test pending
|
|
139
|
+
fetch succeeded 0.84s
|
|
140
|
+
transform running
|
|
141
|
+
transform succeeded 1.20s
|
|
142
|
+
test running
|
|
143
|
+
test succeeded 3.05s
|
|
144
|
+
ok: 3 succeeded in 5.11s
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Features at a glance
|
|
148
|
+
|
|
149
|
+
| Area | What you get |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| **Graph** | `depends_on`, cycle detection, missing-dependency detection, topological levels |
|
|
152
|
+
| **Scheduling** | bounded concurrency, deterministic ordering, `fail_fast` or continue-and-block |
|
|
153
|
+
| **Processes** | streaming output, process groups, timeouts, cooperative cancellation, output budget |
|
|
154
|
+
| **Retries** | bounded attempts, fixed or exponential backoff, retry-on-exit-code |
|
|
155
|
+
| **Cache** | content-addressed, sha256-verified, atomic commits, corrupt-entry recovery |
|
|
156
|
+
| **CLI** | `init` `validate` `graph` `plan` `run` `cache` `explain` `history`, each with `--json` |
|
|
157
|
+
| **TUI** | optional live dashboard when attached to a TTY (`run --tui`) |
|
|
158
|
+
| **Persistence** | local run history (SQLite, WAL) and optional webhook notifications |
|
|
159
|
+
| **Library** | `Workflow.from_file(...).run()` and functional helpers |
|
|
160
|
+
|
|
161
|
+
## Command-line reference
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
pytaskwirecheck [-C ROOT] [--config PATH] [--no-color] COMMAND ...
|
|
165
|
+
|
|
166
|
+
init [--path DIR] [--force] create a starter configuration
|
|
167
|
+
validate [--json] validate configuration and graph
|
|
168
|
+
graph [--format text|dot|json] display the task DAG
|
|
169
|
+
plan [TASKS...] [--json] show the execution plan
|
|
170
|
+
run [TASKS...] [--no-cache] [--max-workers N] [--fail-fast|--no-fail-fast]
|
|
171
|
+
[--json] [-q] [--no-output] [--tui]
|
|
172
|
+
cache status [--json] show cache statistics
|
|
173
|
+
cache clean [--force] [--json] remove cached data
|
|
174
|
+
explain TASK [--json] explain scheduling and cache status for a task
|
|
175
|
+
history [--limit N] [--json] show recent execution summaries
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`run` accepts task names and runs only those tasks plus their dependencies. Add
|
|
179
|
+
`--json` for a machine-readable summary, and `-q` to silence progress.
|
|
180
|
+
|
|
181
|
+
### Exit codes
|
|
182
|
+
|
|
183
|
+
| Code | Meaning |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| `0` | success |
|
|
186
|
+
| `1` | one or more tasks failed |
|
|
187
|
+
| `2` | usage or configuration error |
|
|
188
|
+
| `3` | graph error (cycle, missing dependency, unknown target) |
|
|
189
|
+
| `130` | interrupted (`Ctrl-C`) |
|
|
190
|
+
|
|
191
|
+
## Python API
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from pytaskwirecheck import Workflow
|
|
195
|
+
|
|
196
|
+
workflow = Workflow.from_file("pytaskwirecheck.toml")
|
|
197
|
+
|
|
198
|
+
# Inspect before running.
|
|
199
|
+
plan = workflow.plan(["test"])
|
|
200
|
+
print(plan.order, plan.max_parallelism)
|
|
201
|
+
|
|
202
|
+
# Run and react to events.
|
|
203
|
+
def on_event(event):
|
|
204
|
+
print(type(event).__name__, event)
|
|
205
|
+
|
|
206
|
+
result = workflow.run(["test"], on_event=on_event)
|
|
207
|
+
if not result.succeeded:
|
|
208
|
+
for task_id, record in result.records.items():
|
|
209
|
+
if not record.status.is_success:
|
|
210
|
+
print(task_id, record.status.value, record.exit_code)
|
|
211
|
+
|
|
212
|
+
raise SystemExit(0 if result.succeeded else 1)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Functional helpers are available for one-liners:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import pytaskwirecheck
|
|
219
|
+
|
|
220
|
+
plan = pytaskwirecheck.validate(root=".") # -> ExecutionPlan
|
|
221
|
+
result = pytaskwirecheck.run(["build"], path="ci.toml")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`Workflow.run` supports `use_cache`, `max_workers`, `fail_fast`, `on_event`,
|
|
225
|
+
`cancel_token`, `record_history`, and `notify`. See
|
|
226
|
+
[docs/python-api.md](docs/python-api.md).
|
|
227
|
+
|
|
228
|
+
## Caching model
|
|
229
|
+
|
|
230
|
+
The cache is **content-addressed** and conservative by design:
|
|
231
|
+
|
|
232
|
+
- A task is cache-eligible only if it declares `inputs` **or** `outputs`
|
|
233
|
+
(otherwise its side effects are unknown, so PyTaskWireCheck never guesses).
|
|
234
|
+
- The fingerprint covers the command, resolved inputs (hashing file contents),
|
|
235
|
+
declared outputs' expectations, relevant environment variables, the config
|
|
236
|
+
schema version, and the cache keys of upstream dependencies.
|
|
237
|
+
- A hit restores declared outputs, but only after verifying every stored file's
|
|
238
|
+
sha256 and size; anything corrupt is discarded rather than trusted.
|
|
239
|
+
- Commits are atomic (staged, then `os.replace`), and protected by a
|
|
240
|
+
cross-process lock. Restores are confined to the workspace root.
|
|
241
|
+
- Environment values are **hashed, never stored in plaintext**.
|
|
242
|
+
|
|
243
|
+
Disable it globally (`[cache] enabled = false`), per task (`cache = false`), or
|
|
244
|
+
per run (`pytaskwirecheck run --no-cache`). Inspect decisions with
|
|
245
|
+
`pytaskwirecheck explain TASK`.
|
|
246
|
+
|
|
247
|
+
## Execution semantics
|
|
248
|
+
|
|
249
|
+
Every task moves through an explicit state machine:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
PENDING → READY → RUNNING → SUCCEEDED | FAILED | TIMED_OUT | CACHED
|
|
253
|
+
↘ CANCELLED
|
|
254
|
+
dependents of a failure → BLOCKED
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- **`fail_fast = true`** stops launching new tasks and cancels in-flight work as
|
|
258
|
+
soon as a task fails.
|
|
259
|
+
- **`fail_fast = false`** lets unrelated branches finish; only the transitive
|
|
260
|
+
dependents of the failed task become `BLOCKED`.
|
|
261
|
+
- A cancelled, timed-out, or failed task is **never cached**.
|
|
262
|
+
- Persistence (history, notifications) is best-effort and **never** changes a
|
|
263
|
+
run's outcome or exit code.
|
|
264
|
+
|
|
265
|
+
## Architecture
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
pytaskwirecheck.toml
|
|
269
|
+
│
|
|
270
|
+
▼
|
|
271
|
+
config ──▶ graph ──▶ planner ──▶ scheduler ──▶ executor ──▶ processes
|
|
272
|
+
(schema, (DAG, (levels, (bounded (retries, (streaming,
|
|
273
|
+
validation) builder) order) concurrency) cache hooks) process groups)
|
|
274
|
+
│
|
|
275
|
+
▼
|
|
276
|
+
events ──▶ CLI / TUI / logs / history
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Each layer depends only on the one below it. The scheduler owns state
|
|
280
|
+
transitions; the executor owns retries and cache interaction; the process runner
|
|
281
|
+
owns the subprocess. Rendering and persistence are observers, never participants.
|
|
282
|
+
Read the full design in [docs/architecture.md](docs/architecture.md).
|
|
283
|
+
|
|
284
|
+
## Documentation
|
|
285
|
+
|
|
286
|
+
| Document | Contents |
|
|
287
|
+
| --- | --- |
|
|
288
|
+
| [docs/architecture.md](docs/architecture.md) | Layers, data flow, state machine, design rules |
|
|
289
|
+
| [docs/configuration.md](docs/configuration.md) | Every `pytaskwirecheck.toml` field |
|
|
290
|
+
| [docs/execution.md](docs/execution.md) | Scheduling, retries, timeouts, cancellation |
|
|
291
|
+
| [docs/caching.md](docs/caching.md) | Fingerprints, storage, integrity, eviction |
|
|
292
|
+
| [docs/cli.md](docs/cli.md) | Commands, flags, exit codes |
|
|
293
|
+
| [docs/python-api.md](docs/python-api.md) | Embedding PyTaskWireCheck as a library |
|
|
294
|
+
| [docs/security.md](docs/security.md) | Trust model and hardening |
|
|
295
|
+
| [docs/development.md](docs/development.md) | Contributing, tests, tooling |
|
|
296
|
+
| [docs/roadmap.md](docs/roadmap.md) | Where PyTaskWireCheck is going |
|
|
297
|
+
| [docs/implementation-status.md](docs/implementation-status.md) | What is done and verified |
|
|
298
|
+
|
|
299
|
+
## Status
|
|
300
|
+
|
|
301
|
+
PyTaskWireCheck is **0.1.0** — a complete, tested horizontal slice: config → graph →
|
|
302
|
+
scheduler → runner → cache → CLI → TUI → history → notifications. See
|
|
303
|
+
[docs/implementation-status.md](docs/implementation-status.md) for the verified
|
|
304
|
+
breakdown. It is pre-1.0, so the configuration schema is versioned (`version = 1`)
|
|
305
|
+
and unsupported versions are rejected rather than guessed at.
|
|
306
|
+
|
|
307
|
+
## Contributing
|
|
308
|
+
|
|
309
|
+
Issues and pull requests are welcome. Start with
|
|
310
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) and [docs/development.md](docs/development.md).
|
|
311
|
+
Please follow the [Code of Conduct](CODE_OF_CONDUCT.md) and report
|
|
312
|
+
vulnerabilities via [SECURITY.md](SECURITY.md).
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
pytaskwirecheck/__init__.py,sha256=9icSd3bik9pcpTr9OAkfMYBeuX4hr8h5rqb6Td93H0Q,804
|
|
2
|
+
pytaskwirecheck/api.py,sha256=HTITnLIH2aJJKITPfwoA7JWj-hfULTo3eRXMb7dJvZ8,11073
|
|
3
|
+
pytaskwirecheck/cli.py,sha256=3b6NCrrjgkj1Wjyu8JE5j_OP5oiv5V0gviRuzvLyego,22618
|
|
4
|
+
pytaskwirecheck/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
pytaskwirecheck/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pytaskwirecheck/cache/fingerprint.py,sha256=57-6HLsbtbfi4CG16X_E8OO9W9_pK8F0fICtwwREtyM,5588
|
|
7
|
+
pytaskwirecheck/cache/policy.py,sha256=cj4t3A-uONAEmTreZF1L77REtrJu0WXDRqJUJwpKZOY,1527
|
|
8
|
+
pytaskwirecheck/cache/store.py,sha256=VT_N_V6Xp-wHkfK4HBpmOK8N865ieghCFCmZJAXR6NA,12581
|
|
9
|
+
pytaskwirecheck/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pytaskwirecheck/config/loader.py,sha256=cFn05YrmVkcSKUVrj0vBVR-zAK6WFiLpAA4UMKtP7Bs,2569
|
|
11
|
+
pytaskwirecheck/config/schema.py,sha256=POJHgX-_1gt_q5ZaEiE7mYye7F2Y3HQNGB7UhApF2RU,4806
|
|
12
|
+
pytaskwirecheck/config/validation.py,sha256=PG_NX5OjJQVcyL18Sh-I8tsfp3S40qo7DBHI-Hvh7QU,18630
|
|
13
|
+
pytaskwirecheck/diagnostics/__init__.py,sha256=eOYVOYF_FiCi1Au3rclpU0xlImzxv1x4j52kmkxzDLA,643
|
|
14
|
+
pytaskwirecheck/diagnostics/errors.py,sha256=THR3OJmXmuiP4hkFGDIh1vH09KIrCi-haVCWVnKSBew,2519
|
|
15
|
+
pytaskwirecheck/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
pytaskwirecheck/engine/cancellation.py,sha256=1tXIWN31p_mNUbn65_RW2iNVoz07o04ijseTlLXuUec,1046
|
|
17
|
+
pytaskwirecheck/engine/events.py,sha256=5-LD2uFEebvF5qhK7Gqx1wsX974P9Ay6OX7Wa8c_eT8,2014
|
|
18
|
+
pytaskwirecheck/engine/executor.py,sha256=fBv89FUUCQg-yvjxOm_4XKhGUS-5RU3q6zobUmhbndM,10590
|
|
19
|
+
pytaskwirecheck/engine/scheduler.py,sha256=T_PkmwbuNtL74E-dQjPcJkPkVr23TImS3NRILhOmouU,11109
|
|
20
|
+
pytaskwirecheck/engine/state.py,sha256=TdKCpegMIjNuFwa4P2YxJjdZejyMClI2258Q4zAsXZw,3226
|
|
21
|
+
pytaskwirecheck/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
pytaskwirecheck/graph/builder.py,sha256=GhlrhjUpyS03B2CuEkK9qBM1OBwKP8yl1lIZ8DEN0rg,1178
|
|
23
|
+
pytaskwirecheck/graph/model.py,sha256=-FXJZclPFUw2XNrJ43g1iMPRKI8II0aCfp2DodumEoI,3334
|
|
24
|
+
pytaskwirecheck/graph/planner.py,sha256=aUbUE72Y2qzKoyszqiWTjptZnlAXBFk8b88N01QUECk,5068
|
|
25
|
+
pytaskwirecheck/graph/validator.py,sha256=PbEfPr3QEboKfgyEpixOfRHrGKla2VZz-jqIDfCDtG8,3054
|
|
26
|
+
pytaskwirecheck/history/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
pytaskwirecheck/history/store.py,sha256=6heaoKi7E8BulUsUVUejeeT_31bxREptmeGwtrzPhRI,5530
|
|
28
|
+
pytaskwirecheck/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
pytaskwirecheck/logging/artifacts.py,sha256=onpRAxO2DAYzhK6s7DepzJ_klbSZxqLKuDdGAou5uS0,2935
|
|
30
|
+
pytaskwirecheck/logging/structured.py,sha256=y20JoU6EVoMbMFSmq-ykyYpCr9F6DxHaD4Lc5ZiWa0Q,2510
|
|
31
|
+
pytaskwirecheck/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
pytaskwirecheck/notifications/base.py,sha256=0I-fMlKBcKYRoFuSAk3MjUkx1Foo93xT9DvcpFLiqKI,1262
|
|
33
|
+
pytaskwirecheck/notifications/webhook.py,sha256=dfkIa0H0OSSqLV4Cp4J7ohwaLEP-3lXSny3gHJDtq5o,2544
|
|
34
|
+
pytaskwirecheck/processes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
pytaskwirecheck/processes/runner.py,sha256=HuhABwnIujW1eB9eDv4fjmCopSCSJSTd6D5QAnY2E4Y,8149
|
|
36
|
+
pytaskwirecheck/processes/signals.py,sha256=o6IeQnxqx2ZEBeZdLYCfxGFmnZTbJNocbb_2eZHnSok,1923
|
|
37
|
+
pytaskwirecheck/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
pytaskwirecheck/tui/app.py,sha256=_EBYVEwh5JiR_xC_AYXCukcglu85tSH01yd9ZSQJbUg,6780
|
|
39
|
+
pytaskwirecheck/tui/renderer.py,sha256=_eSpNJYCePqbadCozlqQjJ5_541ttJDHSpHTvRyH3jw,4652
|
|
40
|
+
pytaskwirecheck-0.1.0.dist-info/licenses/LICENSE,sha256=7AYaQADXpsZdOBzCUVw82nbd6I89xeX0EEp9TrpfZLk,1085
|
|
41
|
+
pytaskwirecheck-0.1.0.dist-info/METADATA,sha256=47U65AHJ3pjspucfuhBUq-yityS0X_ehJ28C3zNQQPA,12237
|
|
42
|
+
pytaskwirecheck-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
43
|
+
pytaskwirecheck-0.1.0.dist-info/entry_points.txt,sha256=z3oOm1Vi2rpmbKL9OHBwdsTzte7k18Gtxleuz-PiaLU,61
|
|
44
|
+
pytaskwirecheck-0.1.0.dist-info/top_level.txt,sha256=gR0zDJHu-PA-6EkkhyxtWPHsAZcXs7pDohOkOaX6JAI,16
|
|
45
|
+
pytaskwirecheck-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PyTaskWireCheck Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytaskwirecheck
|