marsh-lib 0.3.4__tar.gz → 0.3.6__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.
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/PKG-INFO +41 -21
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/README.md +40 -20
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/architecture.md +53 -0
- marsh_lib-0.3.6/docs/releases/v0.3.6.md +27 -0
- marsh_lib-0.3.6/docs/v0.3.6-workflow-ir.md +59 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/pyproject.toml +1 -1
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/__init__.py +12 -1
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/configuration.py +93 -17
- marsh_lib-0.3.6/src/marsh/core/ir/__init__.py +33 -0
- marsh_lib-0.3.6/src/marsh/core/ir/codecs.py +189 -0
- marsh_lib-0.3.6/src/marsh/core/ir/errors.py +13 -0
- marsh_lib-0.3.6/src/marsh/core/ir/identifiers.py +19 -0
- marsh_lib-0.3.6/src/marsh/core/ir/models.py +50 -0
- marsh_lib-0.3.6/src/marsh/core/ir/operations.py +43 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/runtime.py +121 -29
- marsh_lib-0.3.6/src/marsh/core/scheduler.py +359 -0
- marsh_lib-0.3.6/src/marsh/core/serialization.py +75 -0
- marsh_lib-0.3.4/src/marsh/core/serialization.py +0 -63
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.circleci/config.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/other.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/pull_request_template.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/docker-integration.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/release-rehearsal.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/release.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/ssh-integration.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/test.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.gitignore +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/LICENSE +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/Taskfile.yml +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/P0_REPOSITORY_AUDIT.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/P6_UX_COMPOSITION_EVALUATION.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/api-migration.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/releases/v0.3.3.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/v0.3.2-test-portability.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/workflow-guide.md +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_act.sh +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_pytest.sh +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_tests_with_sysbox.sh +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_factory.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_grammar.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_runner_decorators.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_script.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/constants.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/authenticator.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cache.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cmd_run_decorator.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cmd_runner_spec.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/command_grammar.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/connector.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/conveyor.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/dag_bridge.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/diagnostics.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/domain.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/executor.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/expression.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/observability.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/policies.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/providers.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/script.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/validation.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/dag.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/node.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/startable.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/docker_command_grammar.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/docker_executor.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/exceptions.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/logger.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/case_conversion.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/readers.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/powershell/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/printers.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/raisers.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/redirections.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/providers/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/providers/docker_provider.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/runtime/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/signals.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_command_grammar.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_connector.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_factory.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/utils/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/utils/output_streams.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/workflow/__init__.py +0 -0
- {marsh_lib-0.3.4 → marsh_lib-0.3.6}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: marsh-lib
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Lightweight, extensible Python library for building, managing, and executing command workflows.
|
|
5
5
|
Project-URL: Repository, https://github.com/CedricAnover/marsh
|
|
6
6
|
Author-email: Cedric Anover <cedric.anover@hotmail.com>
|
|
@@ -236,18 +236,32 @@ Execute a validated workflow with:
|
|
|
236
236
|
results = execute_workflow(workflow)
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
The
|
|
239
|
+
The canonical runtime supports deterministic sequential execution and bounded concurrent execution.
|
|
240
240
|
|
|
241
241
|
Conceptually:
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
Workflow
|
|
244
|
+
|
|
|
245
|
+
v
|
|
246
|
+
Validation / Planning
|
|
247
|
+
|
|
|
248
|
+
v
|
|
249
|
+
Scheduler State Machine
|
|
250
|
+
|
|
|
251
|
+
+--> Sequential Scheduler
|
|
252
|
+
+--> Async Scheduler
|
|
253
|
+
+--> Thread Scheduler
|
|
254
|
+
+--> Process Scheduler
|
|
255
|
+
|
|
|
256
|
+
v
|
|
257
|
+
Execution Mechanism
|
|
258
|
+
|
|
|
259
|
+
v
|
|
260
|
+
Structured Results
|
|
261
|
+
|
|
262
|
+
Concurrent schedulers share the same dependency semantics and bounded ready-queue state machine. The scheduler controls readiness and dispatch; execution mechanisms remain responsible for running individual tasks.
|
|
263
|
+
|
|
264
|
+
For async execution, use `execute_workflow_async()`. For bounded thread or process execution, pass `ThreadScheduler(max_concurrency=...)` or `ProcessScheduler(max_concurrency=...)` to `execute_workflow()`.
|
|
251
265
|
|
|
252
266
|
The workflow model is intentionally separated from the runtime so additional scheduling and execution mechanisms can be introduced without creating another workflow engine.
|
|
253
267
|
|
|
@@ -282,10 +296,10 @@ The dependency graph is validated before execution.
|
|
|
282
296
|
|
|
283
297
|
For the example above, the dependency relationship is:
|
|
284
298
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
299
|
+
build
|
|
300
|
+
|
|
|
301
|
+
v
|
|
302
|
+
test
|
|
289
303
|
|
|
290
304
|
The current canonical scheduler executes tasks sequentially in deterministic topological order.
|
|
291
305
|
|
|
@@ -398,13 +412,19 @@ def add_prefix(stdout, stderr):
|
|
|
398
412
|
|
|
399
413
|
The evaluation order is:
|
|
400
414
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
415
|
+
Pre-modifiers
|
|
416
|
+
|
|
|
417
|
+
v
|
|
418
|
+
Pre-processors
|
|
419
|
+
|
|
|
420
|
+
v
|
|
421
|
+
Command runner
|
|
422
|
+
|
|
|
423
|
+
v
|
|
424
|
+
Post-modifiers
|
|
425
|
+
|
|
|
426
|
+
v
|
|
427
|
+
Post-processors
|
|
408
428
|
|
|
409
429
|
This distinction is important:
|
|
410
430
|
|
|
@@ -207,18 +207,32 @@ Execute a validated workflow with:
|
|
|
207
207
|
results = execute_workflow(workflow)
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
The
|
|
210
|
+
The canonical runtime supports deterministic sequential execution and bounded concurrent execution.
|
|
211
211
|
|
|
212
212
|
Conceptually:
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
214
|
+
Workflow
|
|
215
|
+
|
|
|
216
|
+
v
|
|
217
|
+
Validation / Planning
|
|
218
|
+
|
|
|
219
|
+
v
|
|
220
|
+
Scheduler State Machine
|
|
221
|
+
|
|
|
222
|
+
+--> Sequential Scheduler
|
|
223
|
+
+--> Async Scheduler
|
|
224
|
+
+--> Thread Scheduler
|
|
225
|
+
+--> Process Scheduler
|
|
226
|
+
|
|
|
227
|
+
v
|
|
228
|
+
Execution Mechanism
|
|
229
|
+
|
|
|
230
|
+
v
|
|
231
|
+
Structured Results
|
|
232
|
+
|
|
233
|
+
Concurrent schedulers share the same dependency semantics and bounded ready-queue state machine. The scheduler controls readiness and dispatch; execution mechanisms remain responsible for running individual tasks.
|
|
234
|
+
|
|
235
|
+
For async execution, use `execute_workflow_async()`. For bounded thread or process execution, pass `ThreadScheduler(max_concurrency=...)` or `ProcessScheduler(max_concurrency=...)` to `execute_workflow()`.
|
|
222
236
|
|
|
223
237
|
The workflow model is intentionally separated from the runtime so additional scheduling and execution mechanisms can be introduced without creating another workflow engine.
|
|
224
238
|
|
|
@@ -253,10 +267,10 @@ The dependency graph is validated before execution.
|
|
|
253
267
|
|
|
254
268
|
For the example above, the dependency relationship is:
|
|
255
269
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
build
|
|
271
|
+
|
|
|
272
|
+
v
|
|
273
|
+
test
|
|
260
274
|
|
|
261
275
|
The current canonical scheduler executes tasks sequentially in deterministic topological order.
|
|
262
276
|
|
|
@@ -369,13 +383,19 @@ def add_prefix(stdout, stderr):
|
|
|
369
383
|
|
|
370
384
|
The evaluation order is:
|
|
371
385
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
Pre-modifiers
|
|
387
|
+
|
|
|
388
|
+
v
|
|
389
|
+
Pre-processors
|
|
390
|
+
|
|
|
391
|
+
v
|
|
392
|
+
Command runner
|
|
393
|
+
|
|
|
394
|
+
v
|
|
395
|
+
Post-modifiers
|
|
396
|
+
|
|
|
397
|
+
v
|
|
398
|
+
Post-processors
|
|
379
399
|
|
|
380
400
|
This distinction is important:
|
|
381
401
|
|
|
@@ -34,6 +34,59 @@ The existing command and DAG APIs remain useful low-level building blocks and co
|
|
|
34
34
|
|
|
35
35
|
This document describes the architecture implemented by the current Alpha release. Future capabilities are described only as extension boundaries and are not presented as implemented features.
|
|
36
36
|
|
|
37
|
+
### Scheduler state machine (v0.3.5)
|
|
38
|
+
|
|
39
|
+
The canonical runtime now uses one shared scheduler state model across sequential and bounded-concurrency modes:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
READY -> RUNNING -> SUCCEEDED
|
|
43
|
+
-> FAILED
|
|
44
|
+
-> CANCELLED
|
|
45
|
+
READY -> BLOCKED
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Dependency readiness is defined by `all(dependency == SUCCEEDED)`. Failed, cancelled, or blocked dependencies prevent downstream dispatch. Concurrent modes use a deterministic ready ordering and a hard `max_concurrency` bound.
|
|
49
|
+
|
|
50
|
+
The scheduler owns readiness and dispatch policy; Machine/Process/Provider components own execution. Process-mode execution crosses the process boundary only with serializable task state, dependency results, and a picklable execution callback.
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
Workflow
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
Validation / graphlib planning
|
|
57
|
+
|
|
|
58
|
+
v
|
|
59
|
+
Scheduler state machine
|
|
60
|
+
|---- READY tasks ----> bounded dispatch ----> execution
|
|
61
|
+
| |
|
|
62
|
+
+<------------- Result / state ---------------+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Canonical Workflow IR (v0.3.6)
|
|
68
|
+
|
|
69
|
+
The canonical workflow boundary is versioned independently from the package release:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Authoring
|
|
73
|
+
|
|
|
74
|
+
v
|
|
75
|
+
Configuration
|
|
76
|
+
|
|
|
77
|
+
v
|
|
78
|
+
marsh.workflow/v1
|
|
79
|
+
|
|
|
80
|
+
+--> deterministic JSON
|
|
81
|
+
|
|
|
82
|
+
v
|
|
83
|
+
Reconstruction
|
|
84
|
+
|
|
|
85
|
+
v
|
|
86
|
+
Existing Runtime / Scheduler
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The IR is provider-independent and does not introduce a second execution model. Workflow and task IDs are semantic identities; executable Python callables are represented by explicit `OperationRef` values rather than arbitrary live object state. Unsupported runtime values fail explicitly at the boundary.
|
|
37
90
|
|
|
38
91
|
## Provider boundary (v0.3.4)
|
|
39
92
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# v0.3.6 — Workflow IR & Configuration Boundary
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Established the versioned `marsh.workflow/v1` canonical Workflow IR.
|
|
6
|
+
- Added explicit `OperationRef` values for portable top-level Python callables.
|
|
7
|
+
- Added deterministic canonical JSON serialization and explicit rejection of unsupported runtime values.
|
|
8
|
+
- Added provider-neutral Machine, Provider, Scheduler, and Policy configuration boundaries.
|
|
9
|
+
- Preserved legacy authoring normalization when no IR schema envelope is present.
|
|
10
|
+
- Added cross-process reconstruction coverage and configuration boundary tests.
|
|
11
|
+
- Documented the IR contract, security boundary, compatibility behavior, and architecture.
|
|
12
|
+
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
The existing runtime and scheduler model remains unchanged. The IR is a semantic representation and serialization boundary, not a second execution engine.
|
|
16
|
+
|
|
17
|
+
Unsupported schema versions, non-portable callables, non-finite numbers, bytes, and arbitrary runtime objects fail explicitly.
|
|
18
|
+
|
|
19
|
+
## Verification
|
|
20
|
+
|
|
21
|
+
- CircleCI build: passed
|
|
22
|
+
- CircleCI lint: passed
|
|
23
|
+
- CircleCI Python 3.10 tests: passed
|
|
24
|
+
- CircleCI Python 3.11 tests: passed
|
|
25
|
+
- CircleCI Python 3.12 tests: passed
|
|
26
|
+
- CircleCI integration suite: passed
|
|
27
|
+
- Security review: no secrets, arbitrary `eval`/`exec`, pickle/dill usage, or new dependencies in the v0.3.6 change set.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# v0.3.6 Workflow IR
|
|
2
|
+
|
|
3
|
+
Marsh v0.3.6 establishes the versioned `marsh.workflow/v1` canonical Workflow IR.
|
|
4
|
+
|
|
5
|
+
## Boundary
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Authoring -> Configuration -> Canonical Workflow IR -> deterministic JSON
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
Reconstruction
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
Existing Runtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package version and IR schema version are intentionally independent. The document envelope is:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
WorkflowDocument
|
|
21
|
+
+- schema
|
|
22
|
+
| +- name = marsh.workflow
|
|
23
|
+
| +- version = 1
|
|
24
|
+
+- workflow
|
|
25
|
+
| +- id
|
|
26
|
+
| +- tasks[]
|
|
27
|
+
| +- inputs
|
|
28
|
+
| +- outputs
|
|
29
|
+
| +- metadata
|
|
30
|
+
+- extensions
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Workflow and task identifiers are semantic identities. They are never derived from object addresses, PIDs, thread IDs, memory addresses, local paths, or callable representations.
|
|
34
|
+
|
|
35
|
+
## Operations
|
|
36
|
+
|
|
37
|
+
Importable top-level Python callables are represented explicitly:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{"kind":"python_callable","module":"package.tasks","qualname":"build"}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Lambdas, local functions, and `__main__` callables are rejected. Reconstruction uses `importlib.import_module()` and qualified attribute traversal; serialized Python source is never evaluated.
|
|
44
|
+
|
|
45
|
+
Process operations use structured `ProcessSpec` data. The v1 value contract accepts null, booleans, integers, finite numbers, strings, arrays, and string-keyed objects. Bytes and arbitrary runtime objects are rejected rather than silently encoded.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Configuration remains separate from runtime domain objects. v0.3.6 adds provider-neutral Machine, Scheduler, Policy, and Provider configuration families.
|
|
50
|
+
|
|
51
|
+
Pydantic 2.x was evaluated as a boundary-validation dependency but is not required for this release: the existing dependency-free dataclass boundary is sufficiently small, explicit, and testable, while adding Pydantic would increase the minimal core dependency surface without removing a current semantic responsibility.
|
|
52
|
+
|
|
53
|
+
## Determinism and compatibility
|
|
54
|
+
|
|
55
|
+
Canonical JSON uses stable task ordering, recursively normalized mappings, string object keys, compact separators, and `allow_nan=False`.
|
|
56
|
+
|
|
57
|
+
The existing `workflow_to_*()` APIs now emit/read the canonical v1 envelope, while `workflow_from_dict()` and `workflow_from_json()` continue to accept legacy data-oriented authoring inputs when no schema envelope is present.
|
|
58
|
+
|
|
59
|
+
Unknown schema versions fail explicitly. Reconstruction does not require the original live Python object graph for supported operations.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "marsh-lib"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.6"
|
|
8
8
|
description = "Lightweight, extensible Python library for building, managing, and executing command workflows."
|
|
9
9
|
authors = [{ name = "Cedric Anover", email = "cedric.anover@hotmail.com" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -13,11 +13,21 @@ from marsh.core.serialization import (
|
|
|
13
13
|
validate_workflow,
|
|
14
14
|
workflow_from_dict,
|
|
15
15
|
workflow_from_json,
|
|
16
|
+
workflow_to_document,
|
|
16
17
|
workflow_to_dict,
|
|
17
18
|
workflow_to_json,
|
|
18
19
|
)
|
|
20
|
+
from marsh.core.ir import (
|
|
21
|
+
IR_VERSION,
|
|
22
|
+
OperationRef,
|
|
23
|
+
WorkflowDocument,
|
|
24
|
+
WorkflowIR,
|
|
25
|
+
OperationResolutionError,
|
|
26
|
+
SerializationError,
|
|
27
|
+
UnsupportedSchemaVersionError,
|
|
28
|
+
)
|
|
19
29
|
|
|
20
|
-
from marsh.core.runtime import ExecutionPlan, LocalMachine, LocalProcess, SequentialScheduler, execute_workflow, plan_workflow
|
|
30
|
+
from marsh.core.runtime import ExecutionPlan, LocalMachine, LocalProcess, SequentialScheduler, execute_workflow, execute_workflow_async, plan_workflow
|
|
21
31
|
from marsh.core.policies import ExecutionPolicy, FailurePolicy, ResourcePolicy, RetryPolicy, TimeoutPolicy
|
|
22
32
|
from marsh.core.providers import (
|
|
23
33
|
LocalProvider,
|
|
@@ -32,3 +42,4 @@ from marsh.core.providers import (
|
|
|
32
42
|
)
|
|
33
43
|
from marsh.core.cache import Cache, CachePolicy, MemoryCache, cache_key_for_task
|
|
34
44
|
from marsh.core.observability import EventType, Observer, RuntimeEvent, emit_event
|
|
45
|
+
from marsh.core.scheduler import AsyncScheduler, ConcurrentScheduler, ExecutorScheduler, ProcessScheduler, SchedulerState, TaskState, ThreadScheduler
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
"""Boundary configuration models for the canonical Marsh workflow IR.
|
|
1
|
+
"""Boundary configuration models for the canonical Marsh workflow IR."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
objects are materialized. They intentionally remain separate from runtime
|
|
5
|
-
domain objects.
|
|
6
|
-
"""
|
|
3
|
+
from __future__ import annotations
|
|
7
4
|
|
|
8
5
|
from dataclasses import dataclass, field
|
|
9
6
|
from typing import Any, Mapping
|
|
@@ -49,7 +46,6 @@ class TaskConfig:
|
|
|
49
46
|
def from_mapping(cls, value: Mapping[str, Any]) -> "TaskConfig":
|
|
50
47
|
if not isinstance(value, Mapping):
|
|
51
48
|
raise ValueError("task must be a mapping")
|
|
52
|
-
|
|
53
49
|
allowed = {
|
|
54
50
|
"id", "operation", "machine", "inputs", "outputs",
|
|
55
51
|
"dependencies", "metadata",
|
|
@@ -58,20 +54,17 @@ class TaskConfig:
|
|
|
58
54
|
if unknown:
|
|
59
55
|
names = ", ".join(sorted(unknown))
|
|
60
56
|
raise ValueError(f"task has unknown fields: {names}")
|
|
61
|
-
|
|
62
57
|
if "id" not in value:
|
|
63
58
|
raise ValueError("task id is required")
|
|
64
59
|
task_id = _identifier(value["id"], "task id")
|
|
65
60
|
if "operation" not in value:
|
|
66
61
|
raise ValueError(f"task {task_id!r} operation is required")
|
|
67
|
-
|
|
68
62
|
outputs = _sequence(value.get("outputs"), "task outputs")
|
|
69
63
|
dependencies = _sequence(value.get("dependencies"), "task dependencies")
|
|
70
64
|
if not all(isinstance(item, str) and item.strip() for item in outputs):
|
|
71
65
|
raise ValueError("task outputs must contain non-empty strings")
|
|
72
66
|
if not all(isinstance(item, str) and item.strip() for item in dependencies):
|
|
73
67
|
raise ValueError("task dependencies must contain non-empty strings")
|
|
74
|
-
|
|
75
68
|
return cls(
|
|
76
69
|
id=task_id,
|
|
77
70
|
operation=value["operation"],
|
|
@@ -94,6 +87,72 @@ class TaskConfig:
|
|
|
94
87
|
)
|
|
95
88
|
|
|
96
89
|
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class MachineConfig:
|
|
92
|
+
"""Provider-neutral machine configuration."""
|
|
93
|
+
|
|
94
|
+
name: str
|
|
95
|
+
provider: str = "local"
|
|
96
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "MachineConfig":
|
|
100
|
+
if not isinstance(value, Mapping):
|
|
101
|
+
raise ValueError("machine configuration must be a mapping")
|
|
102
|
+
return cls(
|
|
103
|
+
name=_identifier(value.get("name"), "machine name"),
|
|
104
|
+
provider=_identifier(value.get("provider", "local"), "machine provider"),
|
|
105
|
+
options=_mapping(value.get("options"), "machine options"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class SchedulerConfig:
|
|
111
|
+
"""Provider-independent scheduler selection and concurrency configuration."""
|
|
112
|
+
|
|
113
|
+
strategy: str = "sequential"
|
|
114
|
+
max_concurrency: int = 1
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_mapping(cls, value: Mapping[str, Any] | None) -> "SchedulerConfig":
|
|
118
|
+
value = value or {}
|
|
119
|
+
if not isinstance(value, Mapping):
|
|
120
|
+
raise ValueError("scheduler configuration must be a mapping")
|
|
121
|
+
strategy = _identifier(value.get("strategy", "sequential"), "scheduler strategy")
|
|
122
|
+
max_concurrency = value.get("max_concurrency", 1)
|
|
123
|
+
if not isinstance(max_concurrency, int) or isinstance(max_concurrency, bool) or max_concurrency < 1:
|
|
124
|
+
raise ValueError("scheduler max_concurrency must be a positive integer")
|
|
125
|
+
return cls(strategy=strategy, max_concurrency=max_concurrency)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class PolicyConfig:
|
|
130
|
+
"""Opaque, validated policy data kept independent of runtime policy classes."""
|
|
131
|
+
|
|
132
|
+
values: Mapping[str, Any] = field(default_factory=dict)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_mapping(cls, value: Mapping[str, Any] | None) -> "PolicyConfig":
|
|
136
|
+
return cls(values=_mapping(value, "policy configuration"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class ProviderConfig:
|
|
141
|
+
"""Provider-neutral provider selection at the configuration boundary."""
|
|
142
|
+
|
|
143
|
+
name: str
|
|
144
|
+
options: Mapping[str, Any] = field(default_factory=dict)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> "ProviderConfig":
|
|
148
|
+
if not isinstance(value, Mapping):
|
|
149
|
+
raise ValueError("provider configuration must be a mapping")
|
|
150
|
+
return cls(
|
|
151
|
+
name=_identifier(value.get("name"), "provider name"),
|
|
152
|
+
options=_mapping(value.get("options"), "provider options"),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
97
156
|
@dataclass(frozen=True)
|
|
98
157
|
class WorkflowConfig:
|
|
99
158
|
"""Validated, syntax-neutral configuration for a workflow."""
|
|
@@ -103,13 +162,19 @@ class WorkflowConfig:
|
|
|
103
162
|
inputs: Mapping[str, Any] = field(default_factory=dict)
|
|
104
163
|
outputs: Mapping[str, Any] = field(default_factory=dict)
|
|
105
164
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
165
|
+
machines: tuple[MachineConfig, ...] = ()
|
|
166
|
+
providers: tuple[ProviderConfig, ...] = ()
|
|
167
|
+
scheduler: SchedulerConfig = field(default_factory=SchedulerConfig)
|
|
168
|
+
policy: PolicyConfig = field(default_factory=PolicyConfig)
|
|
106
169
|
|
|
107
170
|
@classmethod
|
|
108
171
|
def from_mapping(cls, value: Mapping[str, Any]) -> "WorkflowConfig":
|
|
109
172
|
if not isinstance(value, Mapping):
|
|
110
173
|
raise ValueError("workflow must be a mapping")
|
|
111
|
-
|
|
112
|
-
|
|
174
|
+
allowed = {
|
|
175
|
+
"id", "tasks", "inputs", "outputs", "metadata",
|
|
176
|
+
"machines", "providers", "scheduler", "policy",
|
|
177
|
+
}
|
|
113
178
|
unknown = set(value) - allowed
|
|
114
179
|
if unknown:
|
|
115
180
|
names = ", ".join(sorted(unknown))
|
|
@@ -117,16 +182,28 @@ class WorkflowConfig:
|
|
|
117
182
|
if "id" not in value:
|
|
118
183
|
raise ValueError("workflow id is required")
|
|
119
184
|
workflow_id = _identifier(value["id"], "workflow id")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
185
|
+
tasks = tuple(
|
|
186
|
+
TaskConfig.from_mapping(task)
|
|
187
|
+
for task in _sequence(value.get("tasks"), "workflow tasks")
|
|
188
|
+
)
|
|
189
|
+
machines = tuple(
|
|
190
|
+
MachineConfig.from_mapping(item)
|
|
191
|
+
for item in _sequence(value.get("machines"), "workflow machines")
|
|
192
|
+
)
|
|
193
|
+
providers = tuple(
|
|
194
|
+
ProviderConfig.from_mapping(item)
|
|
195
|
+
for item in _sequence(value.get("providers"), "workflow providers")
|
|
196
|
+
)
|
|
124
197
|
return cls(
|
|
125
198
|
id=workflow_id,
|
|
126
199
|
tasks=tasks,
|
|
127
200
|
inputs=_mapping(value.get("inputs"), "workflow inputs"),
|
|
128
201
|
outputs=_mapping(value.get("outputs"), "workflow outputs"),
|
|
129
202
|
metadata=_mapping(value.get("metadata"), "workflow metadata"),
|
|
203
|
+
machines=machines,
|
|
204
|
+
providers=providers,
|
|
205
|
+
scheduler=SchedulerConfig.from_mapping(value.get("scheduler")),
|
|
206
|
+
policy=PolicyConfig.from_mapping(value.get("policy")),
|
|
130
207
|
)
|
|
131
208
|
|
|
132
209
|
def to_workflow(self) -> Workflow:
|
|
@@ -140,8 +217,7 @@ class WorkflowConfig:
|
|
|
140
217
|
|
|
141
218
|
|
|
142
219
|
def normalize_workflow(value: Workflow | WorkflowConfig | Mapping[str, Any]) -> Workflow:
|
|
143
|
-
"""Normalize Python or mapping authoring into the
|
|
144
|
-
|
|
220
|
+
"""Normalize Python or mapping authoring into the runtime Workflow contract."""
|
|
145
221
|
if isinstance(value, Workflow):
|
|
146
222
|
return value
|
|
147
223
|
if isinstance(value, WorkflowConfig):
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Canonical Workflow IR for Marsh."""
|
|
2
|
+
|
|
3
|
+
from marsh.core.ir.errors import (
|
|
4
|
+
OperationResolutionError,
|
|
5
|
+
SerializationError,
|
|
6
|
+
UnsupportedSchemaVersionError,
|
|
7
|
+
)
|
|
8
|
+
from marsh.core.ir.models import (
|
|
9
|
+
IR_VERSION,
|
|
10
|
+
OperationRef,
|
|
11
|
+
WorkflowDocument,
|
|
12
|
+
WorkflowIR,
|
|
13
|
+
)
|
|
14
|
+
from marsh.core.ir.operations import resolve_operation
|
|
15
|
+
from marsh.core.ir.codecs import (
|
|
16
|
+
canonical_json,
|
|
17
|
+
document_from_workflow,
|
|
18
|
+
document_to_workflow,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"IR_VERSION",
|
|
23
|
+
"OperationRef",
|
|
24
|
+
"WorkflowDocument",
|
|
25
|
+
"WorkflowIR",
|
|
26
|
+
"OperationResolutionError",
|
|
27
|
+
"SerializationError",
|
|
28
|
+
"UnsupportedSchemaVersionError",
|
|
29
|
+
"canonical_json",
|
|
30
|
+
"document_from_workflow",
|
|
31
|
+
"document_to_workflow",
|
|
32
|
+
"resolve_operation",
|
|
33
|
+
]
|