taskmaestro 0.2.0__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.
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/.github/workflows/ci.yml +15 -3
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/.github/workflows/publish.yml +11 -1
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/PKG-INFO +311 -7
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/README.md +309 -6
- taskmaestro-0.3.0/docs/agent-quickstart.md +102 -0
- taskmaestro-0.3.0/examples/agent_quickstart/input.yaml +2 -0
- taskmaestro-0.3.0/examples/agent_quickstart/tasks.py +19 -0
- taskmaestro-0.3.0/examples/agent_quickstart/workflow.yaml +9 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/image_processing/input.yaml +3 -1
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/image_processing/pipeline.py +20 -9
- taskmaestro-0.3.0/examples/release_pipeline/input.yaml +27 -0
- taskmaestro-0.3.0/examples/release_pipeline/pipeline.py +379 -0
- taskmaestro-0.3.0/examples/release_pipeline/workflow.yaml +44 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/text_analysis/pipeline.py +8 -1
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/text_analysis/workflow.yaml +0 -1
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/pyproject.toml +6 -2
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/__init__.py +14 -2
- taskmaestro-0.3.0/taskmaestro/__main__.py +8 -0
- taskmaestro-0.3.0/taskmaestro/cli.py +484 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/context.py +20 -0
- taskmaestro-0.3.0/taskmaestro/dependencies.py +131 -0
- taskmaestro-0.3.0/taskmaestro/exceptions.py +82 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/hooks/base.py +23 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/hooks/logging.py +13 -0
- taskmaestro-0.3.0/taskmaestro/hooks/persistence.py +44 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/hooks/timing.py +20 -0
- taskmaestro-0.3.0/taskmaestro/job.py +185 -0
- taskmaestro-0.3.0/taskmaestro/mapping.py +37 -0
- taskmaestro-0.3.0/taskmaestro/runner.py +550 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/visualization.py +84 -23
- taskmaestro-0.3.0/taskmaestro/workflow.py +948 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/workflow_task.py +11 -15
- taskmaestro-0.3.0/taskmaestro/yaml_config.py +626 -0
- taskmaestro-0.3.0/tests/test_agent_quickstart.py +39 -0
- taskmaestro-0.3.0/tests/test_cli.py +837 -0
- taskmaestro-0.3.0/tests/test_collections.py +593 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_discovery.py +1 -1
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_exceptions.py +12 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_hooks.py +74 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_job.py +47 -0
- taskmaestro-0.3.0/tests/test_mapping.py +914 -0
- taskmaestro-0.3.0/tests/test_runner.py +963 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_visualization.py +52 -3
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_workflow.py +417 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_workflow_task.py +51 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_yaml_config.py +735 -48
- taskmaestro-0.2.0/taskmaestro/exceptions.py +0 -41
- taskmaestro-0.2.0/taskmaestro/hooks/persistence.py +0 -24
- taskmaestro-0.2.0/taskmaestro/job.py +0 -116
- taskmaestro-0.2.0/taskmaestro/runner.py +0 -201
- taskmaestro-0.2.0/taskmaestro/workflow.py +0 -500
- taskmaestro-0.2.0/taskmaestro/yaml_config.py +0 -463
- taskmaestro-0.2.0/tests/test_runner.py +0 -432
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/.gitignore +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/LICENSE +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/image_processing/inner_workflow.yaml +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/image_processing/workflow.yaml +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/resinsight/input.yaml +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/resinsight/pipeline.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/resinsight/workflow.yaml +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/examples/text_analysis/input.yaml +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/discovery.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/hooks/__init__.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/object_model.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/py.typed +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro/task.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/taskmaestro.png +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/__init__.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/conftest.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_context.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_object_model.py +0 -0
- {taskmaestro-0.2.0 → taskmaestro-0.3.0}/tests/test_task.py +0 -0
|
@@ -5,9 +5,19 @@ on:
|
|
|
5
5
|
branches: [main]
|
|
6
6
|
pull_request:
|
|
7
7
|
|
|
8
|
+
# Least privilege: the CI job only reads the repository.
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
# Cancel superseded runs for the same ref (e.g. force-pushes to a PR).
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
8
17
|
jobs:
|
|
9
18
|
ci:
|
|
10
19
|
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 15
|
|
11
21
|
strategy:
|
|
12
22
|
fail-fast: false
|
|
13
23
|
matrix:
|
|
@@ -20,18 +30,20 @@ jobs:
|
|
|
20
30
|
uses: actions/setup-python@v5
|
|
21
31
|
with:
|
|
22
32
|
python-version: ${{ matrix.python-version }}
|
|
33
|
+
cache: pip
|
|
34
|
+
cache-dependency-path: pyproject.toml
|
|
23
35
|
|
|
24
36
|
- name: Install dependencies
|
|
25
37
|
run: pip install -e ".[dev]"
|
|
26
38
|
|
|
27
39
|
- name: Ruff check
|
|
28
|
-
run: ruff check taskmaestro/ tests/
|
|
40
|
+
run: ruff check taskmaestro/ tests/ examples/
|
|
29
41
|
|
|
30
42
|
- name: Ruff format check
|
|
31
|
-
run: ruff format --check taskmaestro/ tests/
|
|
43
|
+
run: ruff format --check taskmaestro/ tests/ examples/
|
|
32
44
|
|
|
33
45
|
- name: Mypy type check
|
|
34
46
|
run: mypy taskmaestro
|
|
35
47
|
|
|
36
48
|
- name: Run tests with coverage
|
|
37
|
-
run: pytest --cov=taskmaestro --cov-report=term-missing
|
|
49
|
+
run: pytest --cov=taskmaestro --cov-report=term-missing --cov-fail-under=100
|
|
@@ -4,10 +4,15 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
tags: ["v*"]
|
|
6
6
|
|
|
7
|
+
# Default to read-only; the publish job opts into id-token below.
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
7
11
|
jobs:
|
|
8
12
|
build:
|
|
9
13
|
name: Build distributions
|
|
10
14
|
runs-on: ubuntu-latest
|
|
15
|
+
timeout-minutes: 10
|
|
11
16
|
steps:
|
|
12
17
|
- uses: actions/checkout@v4
|
|
13
18
|
|
|
@@ -15,6 +20,8 @@ jobs:
|
|
|
15
20
|
uses: actions/setup-python@v5
|
|
16
21
|
with:
|
|
17
22
|
python-version: "3.12"
|
|
23
|
+
cache: pip
|
|
24
|
+
cache-dependency-path: pyproject.toml
|
|
18
25
|
|
|
19
26
|
- name: Install build
|
|
20
27
|
run: pip install build
|
|
@@ -32,6 +39,7 @@ jobs:
|
|
|
32
39
|
name: Publish to PyPI
|
|
33
40
|
needs: build
|
|
34
41
|
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 10
|
|
35
43
|
environment: pypi
|
|
36
44
|
permissions:
|
|
37
45
|
id-token: write
|
|
@@ -43,4 +51,6 @@ jobs:
|
|
|
43
51
|
path: dist/
|
|
44
52
|
|
|
45
53
|
- name: Publish via trusted publishing
|
|
46
|
-
|
|
54
|
+
# Third-party action pinned to a full commit SHA (tag v1.14.2); the
|
|
55
|
+
# `release/v1` branch is mutable and could be repointed.
|
|
56
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskmaestro
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A Python 3.12+ library for defining and executing typed DAG task workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/OPM/taskmaestro
|
|
6
6
|
Project-URL: Repository, https://github.com/OPM/taskmaestro
|
|
@@ -15,6 +15,7 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
20
|
Classifier: Typing :: Typed
|
|
20
21
|
Requires-Python: >=3.12
|
|
@@ -36,6 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
36
37
|
|
|
37
38
|
A Python 3.12+ library for defining and executing typed DAG task workflows with Pydantic models, lifecycle hooks, and fail-fast semantics.
|
|
38
39
|
|
|
40
|
+
For an agent-friendly, runnable YAML example with validation and troubleshooting steps, see the [agent quickstart](docs/agent-quickstart.md).
|
|
41
|
+
|
|
39
42
|
## Installation
|
|
40
43
|
|
|
41
44
|
```bash
|
|
@@ -140,16 +143,55 @@ You define **Tasks** (typed units of work), compose them into a **Workflow** (li
|
|
|
140
143
|
| Concept | Description |
|
|
141
144
|
|---|---|
|
|
142
145
|
| **Task** | Subclass `Task[I, O]` with Pydantic models for input and output, then implement `run(input, ctx)`. Each task can declare an optional `timeout_seconds`. For tasks with multiple named outputs, use inline `Inputs`/`Outputs` classes inside the task body. |
|
|
143
|
-
| **Workflow** | Build a linear pipeline with `Workflow(tasks=[...])` or a DAG with `Workflow.builder()`.
|
|
146
|
+
| **Workflow** | Build a linear pipeline with `Workflow(tasks=[...])` or a DAG with `Workflow.builder()`. Prefer `builder.task()` and task handles for unambiguous dependencies; the fluent `add_task()` API remains supported. The builder accepts `collect()` for gathering outputs into collection fields and `mapped_over=TaskMap(...)` for sequential expansion over configured mappings. Use `config_fields` to declare which input fields come from `JobConfiguration`. Workflows are validated at build time for cycles, type compatibility, and input completeness. |
|
|
144
147
|
| **Job** | Binds a Workflow to a typed config (the root task's input). Tracks `status` (`pending` → `running` → `completed`/`failed`), the final `result`, any `error`, and per-task `task_results`. Optionally accepts a `JobConfiguration` for per-task static config values. A job can only be run once. |
|
|
145
148
|
| **Runner** | Executes tasks in topological order, stopping on the first failure (fail-fast). Supports per-task and per-job timeouts via `signal.alarm` (Unix only). Dispatches lifecycle events to registered hooks. |
|
|
146
149
|
| **ExecutionContext** | Passed to every `run()` call. Provides a `logger`, an auto-generated `correlation_id` (UUID), a `scratch_dir` (temporary directory), and a service registry (`register()`/`resolve()`) for injecting shared resources like DB connections. |
|
|
147
150
|
| **Hooks** | Subclass `BaseHook` and override methods like `on_job_start`, `on_task_complete`, etc. Hook errors are swallowed and reported via `warnings.warn()`, so they never crash the job. Built-ins: `LoggingHook`, `TimingHook`, `ResultPersistenceHook`. |
|
|
148
151
|
| **ObjectModel** | Generic `ObjectModel[T]` base model for wrapping arbitrary (non-Pydantic) objects. Enables `arbitrary_types_allowed` so fields can hold native library objects like database connections or API clients. |
|
|
149
152
|
|
|
153
|
+
For common cases, run a workflow directly without constructing `Job` and `Runner`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
result = workflow.run(
|
|
157
|
+
Input(value=5),
|
|
158
|
+
task_config={"configured_task": {"option": "value"}},
|
|
159
|
+
hooks=[LoggingHook()],
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The explicit `Job` and `Runner` API remains available for advanced lifecycle control.
|
|
164
|
+
|
|
165
|
+
## Task Handles
|
|
166
|
+
|
|
167
|
+
`builder.task()` adds a task and returns a handle to that specific instance. Handles avoid ambiguous class and string references, especially when the same task class is registered more than once:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
builder = Workflow.builder(name="parallel_wells")
|
|
171
|
+
model = builder.task(LoadModel)
|
|
172
|
+
well_1 = builder.task(LoadWellPath, name="well_1", depends_on=model)
|
|
173
|
+
well_2 = builder.task(LoadWellPath, name="well_2", depends_on=model)
|
|
174
|
+
builder.task(Process, name="proc_1", depends_on=well_1)
|
|
175
|
+
builder.task(Process, name="proc_2", depends_on=well_2)
|
|
176
|
+
workflow = builder.build()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Use `handle.field("field_name")` to route one output field. Named input dependencies can be passed directly to `task()`, while keyword arguments to `collect()` provide a concise keyed collection:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
merged = builder.task(
|
|
183
|
+
MergeResults,
|
|
184
|
+
primary=producer.field("result"),
|
|
185
|
+
checks=collect(tests=tests, lint=lint, types=types),
|
|
186
|
+
)
|
|
187
|
+
builder.set_result_task(merged)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Handles are accepted anywhere dependency references are accepted. A handle from a different builder is rejected. Use the `depends_on` dictionary form when an input field conflicts with a reserved builder argument such as `name` or `config_fields`. The existing fluent `add_task()` API remains fully supported for backward compatibility.
|
|
191
|
+
|
|
150
192
|
## Named Task Instances
|
|
151
193
|
|
|
152
|
-
The same Task class can appear multiple times in a workflow with different names.
|
|
194
|
+
The same Task class can appear multiple times in a workflow with different names. With the fluent API, use the `name=` parameter in `add_task()`:
|
|
153
195
|
|
|
154
196
|
```python
|
|
155
197
|
workflow = (
|
|
@@ -190,6 +232,8 @@ job = Job(workflow=workflow, config=EmptyConfig(), job_configuration=job_config)
|
|
|
190
232
|
result = Runner().run(job)
|
|
191
233
|
```
|
|
192
234
|
|
|
235
|
+
Every declared config field, on root, dependent and mapped tasks alike, must have a value in the `JobConfiguration`; otherwise `Job(...)` raises `WorkflowDefinitionError`.
|
|
236
|
+
|
|
193
237
|
## Nested Workflows
|
|
194
238
|
|
|
195
239
|
A workflow can be wrapped as a typed task and used inside a larger workflow. Its input
|
|
@@ -261,6 +305,155 @@ workflow = (
|
|
|
261
305
|
)
|
|
262
306
|
```
|
|
263
307
|
|
|
308
|
+
## Collecting Multiple Outputs
|
|
309
|
+
|
|
310
|
+
Use `collect()` when several task outputs should populate one `list[T]` or
|
|
311
|
+
`dict[str, T]` field. Positional members preserve declaration order:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from taskmaestro import collect
|
|
315
|
+
|
|
316
|
+
class GridInput(BaseModel):
|
|
317
|
+
surfaces: list[Surface]
|
|
318
|
+
|
|
319
|
+
workflow = (
|
|
320
|
+
Workflow.builder("create_grid")
|
|
321
|
+
.add_task(LoadSurface, name="top")
|
|
322
|
+
.add_task(GenerateSurface, name="middle")
|
|
323
|
+
.add_task(LoadSurface, name="base")
|
|
324
|
+
.add_task(
|
|
325
|
+
CreateGrid,
|
|
326
|
+
depends_on={"surfaces": collect("top", "middle", "base")},
|
|
327
|
+
)
|
|
328
|
+
.build()
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Use a mapping to preserve aliases in a `dict[str, T]`, and use `(task, "field")`
|
|
333
|
+
to collect a specific output field:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
depends_on={
|
|
337
|
+
"surfaces": collect({
|
|
338
|
+
"top": ("top_loader", "surface"),
|
|
339
|
+
"base": ("base_loader", "surface"),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The equivalent YAML forms are:
|
|
345
|
+
|
|
346
|
+
```yaml
|
|
347
|
+
depends_on:
|
|
348
|
+
surfaces:
|
|
349
|
+
collect:
|
|
350
|
+
- top
|
|
351
|
+
- [middle, generated_surface]
|
|
352
|
+
- base
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```yaml
|
|
356
|
+
depends_on:
|
|
357
|
+
surfaces:
|
|
358
|
+
collect:
|
|
359
|
+
top: [top_loader, surface]
|
|
360
|
+
base: [base_loader, surface]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Every member is checked against the field's element type when the workflow is
|
|
364
|
+
built. Subtypes are accepted. `collect()` and `collect({})` explicitly create
|
|
365
|
+
empty list and dictionary inputs, respectively.
|
|
366
|
+
|
|
367
|
+
## Mapped Tasks
|
|
368
|
+
|
|
369
|
+
A mapped task invokes one task declaration for every entry in a configured
|
|
370
|
+
mapping. Mapped items execute sequentially in mapping declaration order.
|
|
371
|
+
Each item gets a fresh task instance and child `ExecutionContext`.
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
builder = Workflow.builder("create_grid")
|
|
375
|
+
connection = builder.task(ConnectToResInsight)
|
|
376
|
+
surfaces = builder.map_task(
|
|
377
|
+
LoadRegularSurface,
|
|
378
|
+
name="load_surfaces",
|
|
379
|
+
depends_on={"resinsight": connection},
|
|
380
|
+
config_fields=["unit"],
|
|
381
|
+
over="surfaces",
|
|
382
|
+
key_as="surface_name",
|
|
383
|
+
value_as="path",
|
|
384
|
+
error_mode="fail_fast",
|
|
385
|
+
)
|
|
386
|
+
builder.task(CreateGrid, depends_on={"surfaces": surfaces})
|
|
387
|
+
workflow = builder.build()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The mapped task's input model contains the injected key and value fields, not
|
|
391
|
+
the source mapping:
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
class LoadSurfaceInput(BaseModel):
|
|
395
|
+
resinsight: RipsInstance
|
|
396
|
+
unit: str
|
|
397
|
+
surface_name: str # key_as
|
|
398
|
+
path: str # value_as
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Configure the source through `JobConfiguration`:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
job_configuration = JobConfiguration({
|
|
405
|
+
"load_surfaces": {
|
|
406
|
+
"unit": "meters",
|
|
407
|
+
"surfaces": {
|
|
408
|
+
"top": "/data/top.irap",
|
|
409
|
+
"base": "/data/base.irap",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The logical output is a `MappedOutput[O]` Pydantic root model containing an
|
|
416
|
+
insertion-ordered `dict[str, O]`, where `O` is the task's declared output type.
|
|
417
|
+
When a mapped task is connected to a named `dict[str, O]` input field, its
|
|
418
|
+
`root` value is unwrapped automatically:
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
class CreateGridInput(BaseModel):
|
|
422
|
+
surfaces: dict[str, RegularSurface]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The equivalent YAML task declaration is:
|
|
426
|
+
|
|
427
|
+
```yaml
|
|
428
|
+
- task: resinsight.load_regular_surface
|
|
429
|
+
name: load_surfaces
|
|
430
|
+
map:
|
|
431
|
+
over: surfaces
|
|
432
|
+
key_as: surface_name
|
|
433
|
+
value_as: path
|
|
434
|
+
error_mode: fail_fast
|
|
435
|
+
depends_on:
|
|
436
|
+
resinsight: resinsight.connect
|
|
437
|
+
config_fields: [unit]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Input YAML:
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
load_surfaces:
|
|
444
|
+
unit: meters
|
|
445
|
+
surfaces:
|
|
446
|
+
top: /data/top.irap
|
|
447
|
+
base: /data/base.irap
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`fail_fast` stops at the first failed item. `collect_all` attempts every item
|
|
451
|
+
and reports an aggregate `MappedTaskExecutionError`. An empty mapping succeeds
|
|
452
|
+
with `MappedOutput(root={})`. Per-item records are available in
|
|
453
|
+
`job.mapped_item_results`, and
|
|
454
|
+
built-in logging, timing, and persistence hooks observe individual items.
|
|
455
|
+
Concurrent mapped execution is intentionally deferred.
|
|
456
|
+
|
|
264
457
|
## ObjectModel
|
|
265
458
|
|
|
266
459
|
`ObjectModel[T]` wraps arbitrary (non-Pydantic) objects so they can flow through workflows. Use it as a type alias for simple wrappers, or subclass it to add extra fields:
|
|
@@ -312,6 +505,25 @@ without importing plugin modules, or `get_registered_task(name)` and
|
|
|
312
505
|
`get_registered_workflow(name)` to load one plugin. Duplicate names and invalid plugin
|
|
313
506
|
types raise `PluginLoadError`.
|
|
314
507
|
|
|
508
|
+
Agents can inspect installed task plugins from the CLI without loading every plugin:
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
taskmaestro tasks list --json
|
|
512
|
+
taskmaestro tasks describe acme.prepare --json
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`list --json` emits `{"tasks": ["acme.prepare", ...]}` in sorted order; an empty list
|
|
516
|
+
means no task plugins are installed. `describe --json` loads only the named plugin
|
|
517
|
+
and emits its registered `identifier`, task `name`, `timeout_seconds`, and Pydantic
|
|
518
|
+
`input_schema` / `output_schema` (JSON Schema objects). These commands inspect
|
|
519
|
+
**installed entry points**, not task classes local to a workflow YAML file. Without
|
|
520
|
+
`--json`, `list` prints one identifier per line and `describe` prints indented JSON.
|
|
521
|
+
For runtime-only Python objects (such as `ObjectModel[rips.EclipseCase]`), schema
|
|
522
|
+
fields include `"not": {}`, `"x-taskmaestro-opaque": true`, and
|
|
523
|
+
`"x-taskmaestro-python-type"`. They cannot be supplied as JSON; wire them from
|
|
524
|
+
upstream tasks or a Python context instead. Other unsupported schema constructs,
|
|
525
|
+
unknown identifiers, and invalid plugins report an error on stderr and exit with status 2.
|
|
526
|
+
|
|
315
527
|
## YAML Configuration
|
|
316
528
|
|
|
317
529
|
Workflows can be defined entirely in YAML instead of Python. A `task:` value may be
|
|
@@ -354,8 +566,9 @@ context:
|
|
|
354
566
|
|
|
355
567
|
```yaml
|
|
356
568
|
# input.yaml
|
|
357
|
-
|
|
358
|
-
|
|
569
|
+
prepare_text:
|
|
570
|
+
text: "Python is a high-level programming language..."
|
|
571
|
+
title: "Python Overview"
|
|
359
572
|
```
|
|
360
573
|
|
|
361
574
|
Load and run:
|
|
@@ -371,7 +584,18 @@ result = loaded.run()
|
|
|
371
584
|
result = run_workflow_from_yaml("workflow.yaml", "input.yaml")
|
|
372
585
|
```
|
|
373
586
|
|
|
374
|
-
YAML
|
|
587
|
+
YAML input always uses per-task configuration: every top-level key in `input.yaml` must be a registered task instance name, and its value must be a mapping or `null`. Unknown task names and scalar task values are rejected. Fields are validated against the task's input model and can configure root tasks, downstream tasks, and mapped tasks.
|
|
588
|
+
|
|
589
|
+
YAML also supports named task instances (`name:`), fan-in dictionaries, and output field routing via `[task, field]` lists. Named instances use their instance name as the input key:
|
|
590
|
+
|
|
591
|
+
```yaml
|
|
592
|
+
load_well_path_1:
|
|
593
|
+
path: first.dev
|
|
594
|
+
load_well_path_2:
|
|
595
|
+
path: second.dev
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
When the same task class (or the same inner YAML file) appears more than once under different `name:`s, `depends_on` and `result_task` must use the instance name — referencing the class path is rejected as ambiguous.
|
|
375
599
|
|
|
376
600
|
Use `workflow:` instead of `task:` to compose another YAML workflow. Paths are resolved
|
|
377
601
|
relative to the containing workflow file, and `workflow_input:` optionally supplies the
|
|
@@ -435,19 +659,99 @@ WorkflowRunnerError (base)
|
|
|
435
659
|
├── JobStateError # e.g., re-running a completed job
|
|
436
660
|
├── ConfigLoadError # YAML config loading failure
|
|
437
661
|
└── TaskExecutionError # Runtime task failure
|
|
662
|
+
├── MappedTaskExecutionError # One or more mapped items failed
|
|
438
663
|
├── TaskOutputTypeError # Output type mismatch
|
|
439
664
|
└── TaskTimeoutError # Task exceeded timeout
|
|
440
665
|
```
|
|
441
666
|
|
|
667
|
+
## Command-Line Interface
|
|
668
|
+
|
|
669
|
+
Installed packages provide a `taskmaestro` command for YAML workflows:
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
taskmaestro validate workflow.yaml --input input.yaml
|
|
673
|
+
taskmaestro graph workflow.yaml --input input.yaml
|
|
674
|
+
taskmaestro run workflow.yaml --input input.yaml --log-level INFO
|
|
675
|
+
taskmaestro workflow describe workflow.yaml --json
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
By default, `run` prints the final output as JSON and reports errors on stderr;
|
|
679
|
+
`validate` prints a human-readable confirmation. `graph` prints Mermaid markup.
|
|
680
|
+
|
|
681
|
+
For automation, use `--json` with `validate` or `run`:
|
|
682
|
+
|
|
683
|
+
```bash
|
|
684
|
+
taskmaestro validate workflow.yaml --input input.yaml --json
|
|
685
|
+
taskmaestro run workflow.yaml --input input.yaml --json
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Successful validation emits `{"status":"valid","workflow":"..."}`. A successful
|
|
689
|
+
run emits `{"status":"completed","workflow":"...","result":{...}}`. A failure
|
|
690
|
+
emits one JSON object on stdout, for example:
|
|
691
|
+
|
|
692
|
+
```json
|
|
693
|
+
{"status":"failed","workflow":"example","failed_task":"prepare","error":{"code":"task_failed","type":"ValueError","message":"Task failed","task":"prepare","field":null,"issues":[]}}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Errors contain `code`, exception `type`, a safe `message`, nullable `task` and
|
|
697
|
+
`field`, and `issues` (field paths and error codes, without input values).
|
|
698
|
+
Loading failures have status `invalid` and `code: "configuration_error"`; task
|
|
699
|
+
failures have status `failed` and `code: "task_failed"`. If a completed result
|
|
700
|
+
cannot be encoded as JSON (e.g. a Python-only object), `run --json` returns
|
|
701
|
+
`code: "serialization_error"`. Missing configuration fields produce `issues`
|
|
702
|
+
with code `missing`; when no structured field metadata is available, `field`
|
|
703
|
+
is `null`. Use text mode when you need the original exception message.
|
|
704
|
+
|
|
705
|
+
Exit codes: `0` success, `1` task or result-serialization failure, `2` workflow
|
|
706
|
+
configuration failure. In JSON mode logs and ordinary Python `print()` output
|
|
707
|
+
from imports/tasks go to stderr, reserving stdout for the result document.
|
|
708
|
+
Application code can still write directly to file descriptor 1; JSON mode is
|
|
709
|
+
not a sandbox. Error objects intentionally omit raw exception messages, but
|
|
710
|
+
application-generated stderr may contain sensitive data.
|
|
711
|
+
|
|
712
|
+
Inspect a workflow **before its input file is complete** with
|
|
713
|
+
`taskmaestro workflow describe workflow.yaml --json`. The JSON contains the
|
|
714
|
+
workflow name and result task plus topologically ordered task instances. Each
|
|
715
|
+
instance includes its `name`, `python_type`, `depends_on` references (`task` /
|
|
716
|
+
`field`), `config_fields`, `required_input_fields`, optional `map` configuration,
|
|
717
|
+
and Pydantic input/output schemas. Collection dependencies include their kind
|
|
718
|
+
(`positional` or `keyed`) and member references. `python_type` identifies the
|
|
719
|
+
loaded class; it may differ from the plugin entry-point identifier used in YAML.
|
|
720
|
+
|
|
721
|
+
Pass `--input input.yaml` to check the input against the workflow. The input
|
|
722
|
+
may be incomplete (for example a template whose runtime-picker fields a UI
|
|
723
|
+
fills in): `provided_config_fields` lists the field names set for each task,
|
|
724
|
+
`missing_config_fields` lists declared `config_fields` the input does not set,
|
|
725
|
+
and `config_values` maps exactly the provided names to their values; without
|
|
726
|
+
`--input`, all three are `null`. Other problems, such as unknown task keys or a
|
|
727
|
+
missing or invalid map source, still fail. `validate` and `run` reject missing
|
|
728
|
+
configuration fields. Values are what `input.yaml` contains for
|
|
729
|
+
the task, **before** Pydantic validation, so opaque markers (for example
|
|
730
|
+
`{"__resinsight_ref__": "EclipseCase", "case_id": 0}`) come through unchanged.
|
|
731
|
+
Dates and datetimes become ISO 8601 strings, paths become strings, sets become
|
|
732
|
+
sorted lists and non-string mapping keys become JSON key strings; binary values,
|
|
733
|
+
NaN/infinity and keys that collide after conversion are rejected as invalid.
|
|
734
|
+
Fields without a value fall back to `input_schema.properties.<field>.default`.
|
|
735
|
+
**The output contains the input values, including any secrets.** This check
|
|
736
|
+
does not run tasks or hooks and does not validate every configured value's
|
|
737
|
+
runtime type. Loading YAML still imports
|
|
738
|
+
Python modules (and nested workflows); **do not inspect untrusted YAML or plugins**
|
|
739
|
+
under a privileged account. In JSON mode inspection failures have status
|
|
740
|
+
`invalid`, an `error` object, and exit code 2. Without `--json`, inspection
|
|
741
|
+
prints indented JSON.
|
|
742
|
+
|
|
743
|
+
`python -m taskmaestro ...` is equivalent, which is useful when the scripts directory is not on `PATH`.
|
|
744
|
+
|
|
442
745
|
## Examples
|
|
443
746
|
|
|
444
|
-
|
|
747
|
+
Four full example pipelines are included in the `examples/` directory:
|
|
445
748
|
|
|
446
749
|
| Example | Features |
|
|
447
750
|
|---|---|
|
|
448
751
|
| `examples/text_analysis/` | DAG with fan-out/fan-in, output field routing, inline `Inputs`/`Outputs` classes, YAML config, Mermaid visualization |
|
|
449
752
|
| `examples/resinsight/` | `ObjectModel[T]` for gRPC objects, `JobConfiguration` with per-task config, named task instances, `config_fields`, YAML config |
|
|
450
753
|
| `examples/image_processing/` | Nested workflows through `Workflow.as_task()` and YAML `workflow:`, typed boundaries, expanded Mermaid subgraph |
|
|
754
|
+
| `examples/release_pipeline/` | Keyed `collect()` dependencies, mapped tasks, mapped output routing, per-task YAML config |
|
|
451
755
|
|
|
452
756
|
Run an example:
|
|
453
757
|
|