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.
Files changed (93) hide show
  1. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/PKG-INFO +41 -21
  2. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/README.md +40 -20
  3. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/architecture.md +53 -0
  4. marsh_lib-0.3.6/docs/releases/v0.3.6.md +27 -0
  5. marsh_lib-0.3.6/docs/v0.3.6-workflow-ir.md +59 -0
  6. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/pyproject.toml +1 -1
  7. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/__init__.py +12 -1
  8. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/configuration.py +93 -17
  9. marsh_lib-0.3.6/src/marsh/core/ir/__init__.py +33 -0
  10. marsh_lib-0.3.6/src/marsh/core/ir/codecs.py +189 -0
  11. marsh_lib-0.3.6/src/marsh/core/ir/errors.py +13 -0
  12. marsh_lib-0.3.6/src/marsh/core/ir/identifiers.py +19 -0
  13. marsh_lib-0.3.6/src/marsh/core/ir/models.py +50 -0
  14. marsh_lib-0.3.6/src/marsh/core/ir/operations.py +43 -0
  15. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/runtime.py +121 -29
  16. marsh_lib-0.3.6/src/marsh/core/scheduler.py +359 -0
  17. marsh_lib-0.3.6/src/marsh/core/serialization.py +75 -0
  18. marsh_lib-0.3.4/src/marsh/core/serialization.py +0 -63
  19. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.circleci/config.yml +0 -0
  20. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  21. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/ISSUE_TEMPLATE/other.md +0 -0
  23. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/pull_request_template.md +0 -0
  24. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/docker-integration.yml +0 -0
  25. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/release-rehearsal.yml +0 -0
  26. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/release.yml +0 -0
  27. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/ssh-integration.yml +0 -0
  28. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.github/workflows/test.yml +0 -0
  29. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/.gitignore +0 -0
  30. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/LICENSE +0 -0
  31. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/Taskfile.yml +0 -0
  32. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/P0_REPOSITORY_AUDIT.md +0 -0
  33. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/P6_UX_COMPOSITION_EVALUATION.md +0 -0
  34. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/api-migration.md +0 -0
  35. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/releases/v0.3.3.md +0 -0
  36. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/v0.3.2-test-portability.md +0 -0
  37. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/docs/workflow-guide.md +0 -0
  38. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_act.sh +0 -0
  39. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_pytest.sh +0 -0
  40. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/run_tests_with_sysbox.sh +0 -0
  41. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/__init__.py +0 -0
  42. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/__init__.py +0 -0
  43. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_factory.py +0 -0
  44. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_grammar.py +0 -0
  45. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_runner_decorators.py +0 -0
  46. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/bash/bash_script.py +0 -0
  47. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/constants.py +0 -0
  48. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/authenticator.py +0 -0
  49. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cache.py +0 -0
  50. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cmd_run_decorator.py +0 -0
  51. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/cmd_runner_spec.py +0 -0
  52. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/command_grammar.py +0 -0
  53. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/connector.py +0 -0
  54. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/conveyor.py +0 -0
  55. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/dag_bridge.py +0 -0
  56. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/diagnostics.py +0 -0
  57. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/domain.py +0 -0
  58. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/executor.py +0 -0
  59. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/expression.py +0 -0
  60. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/observability.py +0 -0
  61. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/policies.py +0 -0
  62. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/providers.py +0 -0
  63. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/script.py +0 -0
  64. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/core/validation.py +0 -0
  65. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/__init__.py +0 -0
  66. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/dag.py +0 -0
  67. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/node.py +0 -0
  68. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/dag/startable.py +0 -0
  69. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/__init__.py +0 -0
  70. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/docker_command_grammar.py +0 -0
  71. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/docker/docker_executor.py +0 -0
  72. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/exceptions.py +0 -0
  73. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/logger.py +0 -0
  74. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/__init__.py +0 -0
  75. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/case_conversion.py +0 -0
  76. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/modifier_functions/readers.py +0 -0
  77. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/powershell/__init__.py +0 -0
  78. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/__init__.py +0 -0
  79. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/printers.py +0 -0
  80. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/raisers.py +0 -0
  81. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/processor_functions/redirections.py +0 -0
  82. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/providers/__init__.py +0 -0
  83. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/providers/docker_provider.py +0 -0
  84. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/runtime/__init__.py +0 -0
  85. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/signals.py +0 -0
  86. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/__init__.py +0 -0
  87. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_command_grammar.py +0 -0
  88. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_connector.py +0 -0
  89. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/ssh/ssh_factory.py +0 -0
  90. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/utils/__init__.py +0 -0
  91. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/utils/output_streams.py +0 -0
  92. {marsh_lib-0.3.4 → marsh_lib-0.3.6}/src/marsh/workflow/__init__.py +0 -0
  93. {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.4
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 current canonical runtime uses a **sequential local execution model**.
239
+ The canonical runtime supports deterministic sequential execution and bounded concurrent execution.
240
240
 
241
241
  Conceptually:
242
242
 
243
- ```mermaid
244
- flowchart TD
245
- A[Workflow] --> B[Validation]
246
- B --> C[Execution Plan]
247
- C --> D[Sequential Scheduler]
248
- D --> E[Local Process Execution]
249
- E --> F[Structured Results]
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
- ```mermaid
286
- flowchart LR
287
- build --> test
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
- ```mermaid
402
- flowchart TD
403
- A[Pre-modifiers] --> B[Pre-processors]
404
- B --> C[Command runner]
405
- C --> D[Post-modifiers]
406
- D --> E[Post-processors]
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 current canonical runtime uses a **sequential local execution model**.
210
+ The canonical runtime supports deterministic sequential execution and bounded concurrent execution.
211
211
 
212
212
  Conceptually:
213
213
 
214
- ```mermaid
215
- flowchart TD
216
- A[Workflow] --> B[Validation]
217
- B --> C[Execution Plan]
218
- C --> D[Sequential Scheduler]
219
- D --> E[Local Process Execution]
220
- E --> F[Structured Results]
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
- ```mermaid
257
- flowchart LR
258
- build --> test
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
- ```mermaid
373
- flowchart TD
374
- A[Pre-modifiers] --> B[Pre-processors]
375
- B --> C[Command runner]
376
- C --> D[Post-modifiers]
377
- D --> E[Post-processors]
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.4"
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
- Configuration objects validate and normalize authoring data before runtime
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
- allowed = {"id", "tasks", "inputs", "outputs", "metadata"}
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
- raw_tasks = _sequence(value.get("tasks"), "workflow tasks")
122
- tasks = tuple(TaskConfig.from_mapping(task) for task in raw_tasks)
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 canonical Workflow IR."""
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
+ ]