seamless-workflow 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. seamless_workflow-0.2.0/PKG-INFO +157 -0
  2. seamless_workflow-0.2.0/README.md +129 -0
  3. seamless_workflow-0.2.0/pyproject.toml +52 -0
  4. seamless_workflow-0.2.0/seamless_workflow/__init__.py +40 -0
  5. seamless_workflow-0.2.0/seamless_workflow/adapters.py +50 -0
  6. seamless_workflow-0.2.0/seamless_workflow/assignment.py +8 -0
  7. seamless_workflow-0.2.0/seamless_workflow/attachments/__init__.py +3 -0
  8. seamless_workflow-0.2.0/seamless_workflow/attachments/api.py +217 -0
  9. seamless_workflow-0.2.0/seamless_workflow/attachments/fs/__init__.py +1 -0
  10. seamless_workflow-0.2.0/seamless_workflow/attachments/fs/native.py +63 -0
  11. seamless_workflow-0.2.0/seamless_workflow/attachments/fs/service.py +601 -0
  12. seamless_workflow-0.2.0/seamless_workflow/attachments/manual.py +65 -0
  13. seamless_workflow-0.2.0/seamless_workflow/attachments/output_widget.py +62 -0
  14. seamless_workflow-0.2.0/seamless_workflow/attachments/policy.py +41 -0
  15. seamless_workflow-0.2.0/seamless_workflow/attachments/runtime.py +594 -0
  16. seamless_workflow-0.2.0/seamless_workflow/attachments/session.py +93 -0
  17. seamless_workflow-0.2.0/seamless_workflow/attachments/share/__init__.py +2 -0
  18. seamless_workflow-0.2.0/seamless_workflow/attachments/share/api.py +300 -0
  19. seamless_workflow-0.2.0/seamless_workflow/attachments/share/archive.py +60 -0
  20. seamless_workflow-0.2.0/seamless_workflow/attachments/share/driver.py +110 -0
  21. seamless_workflow-0.2.0/seamless_workflow/attachments/share/mime.py +180 -0
  22. seamless_workflow-0.2.0/seamless_workflow/attachments/share/openapi.py +358 -0
  23. seamless_workflow-0.2.0/seamless_workflow/attachments/share/server.py +1660 -0
  24. seamless_workflow-0.2.0/seamless_workflow/attachments/share/spec.py +139 -0
  25. seamless_workflow-0.2.0/seamless_workflow/attachments/share/static/seamless-client.js +538 -0
  26. seamless_workflow-0.2.0/seamless_workflow/attachments/spec.py +42 -0
  27. seamless_workflow-0.2.0/seamless_workflow/attachments/traitlet.py +344 -0
  28. seamless_workflow-0.2.0/seamless_workflow/attachments/widget.py +229 -0
  29. seamless_workflow-0.2.0/seamless_workflow/builder_state.py +1107 -0
  30. seamless_workflow-0.2.0/seamless_workflow/configuration.py +103 -0
  31. seamless_workflow-0.2.0/seamless_workflow/context.py +3287 -0
  32. seamless_workflow-0.2.0/seamless_workflow/controller.py +160 -0
  33. seamless_workflow-0.2.0/seamless_workflow/diagnostics.py +69 -0
  34. seamless_workflow-0.2.0/seamless_workflow/endpoints.py +33 -0
  35. seamless_workflow-0.2.0/seamless_workflow/errors.py +69 -0
  36. seamless_workflow-0.2.0/seamless_workflow/graph.py +636 -0
  37. seamless_workflow-0.2.0/seamless_workflow/ingress.py +411 -0
  38. seamless_workflow-0.2.0/seamless_workflow/jupyter.py +49 -0
  39. seamless_workflow-0.2.0/seamless_workflow/lifecycle.py +26 -0
  40. seamless_workflow-0.2.0/seamless_workflow/reactive.py +386 -0
  41. seamless_workflow-0.2.0/seamless_workflow/runtime_api.py +227 -0
  42. seamless_workflow-0.2.0/seamless_workflow/scheduler.py +106 -0
  43. seamless_workflow-0.2.0/seamless_workflow/serialization.py +317 -0
  44. seamless_workflow-0.2.0/seamless_workflow/serve_graph.py +55 -0
  45. seamless_workflow-0.2.0/seamless_workflow/shareserver.py +36 -0
  46. seamless_workflow-0.2.0/seamless_workflow/sidework.py +102 -0
  47. seamless_workflow-0.2.0/seamless_workflow/views.py +62 -0
  48. seamless_workflow-0.2.0/seamless_workflow.egg-info/PKG-INFO +157 -0
  49. seamless_workflow-0.2.0/seamless_workflow.egg-info/SOURCES.txt +142 -0
  50. seamless_workflow-0.2.0/seamless_workflow.egg-info/dependency_links.txt +1 -0
  51. seamless_workflow-0.2.0/seamless_workflow.egg-info/entry_points.txt +2 -0
  52. seamless_workflow-0.2.0/seamless_workflow.egg-info/requires.txt +8 -0
  53. seamless_workflow-0.2.0/seamless_workflow.egg-info/top_level.txt +1 -0
  54. seamless_workflow-0.2.0/setup.cfg +4 -0
  55. seamless_workflow-0.2.0/tests/test_assignment_semantics.py +277 -0
  56. seamless_workflow-0.2.0/tests/test_attachment_slots.py +552 -0
  57. seamless_workflow-0.2.0/tests/test_callable_reassignment.py +168 -0
  58. seamless_workflow-0.2.0/tests/test_canonical_handles.py +317 -0
  59. seamless_workflow-0.2.0/tests/test_cell_expression_builder.py +121 -0
  60. seamless_workflow-0.2.0/tests/test_cell_inspection_contracts.py +57 -0
  61. seamless_workflow-0.2.0/tests/test_cell_joins.py +271 -0
  62. seamless_workflow-0.2.0/tests/test_cell_scratch.py +36 -0
  63. seamless_workflow-0.2.0/tests/test_cell_semantics.py +349 -0
  64. seamless_workflow-0.2.0/tests/test_celljoin_fingertip.py +52 -0
  65. seamless_workflow-0.2.0/tests/test_celljoin_remote_integration.py +180 -0
  66. seamless_workflow-0.2.0/tests/test_cells_contract_alignment.py +725 -0
  67. seamless_workflow-0.2.0/tests/test_cells_projection_writes.py +134 -0
  68. seamless_workflow-0.2.0/tests/test_cells_transformer_boundary.py +57 -0
  69. seamless_workflow-0.2.0/tests/test_cells_wiring_contract.py +368 -0
  70. seamless_workflow-0.2.0/tests/test_celltype_changes.py +107 -0
  71. seamless_workflow-0.2.0/tests/test_compiled_celltype_workflow.py +142 -0
  72. seamless_workflow-0.2.0/tests/test_completion.py +86 -0
  73. seamless_workflow-0.2.0/tests/test_context_basics.py +101 -0
  74. seamless_workflow-0.2.0/tests/test_contract_attachments.py +1091 -0
  75. seamless_workflow-0.2.0/tests/test_contract_bash_environment.py +84 -0
  76. seamless_workflow-0.2.0/tests/test_contract_bound_fusion.py +142 -0
  77. seamless_workflow-0.2.0/tests/test_contract_cancellation_policy.py +121 -0
  78. seamless_workflow-0.2.0/tests/test_contract_celljoin_bound.py +241 -0
  79. seamless_workflow-0.2.0/tests/test_contract_cells_bound.py +618 -0
  80. seamless_workflow-0.2.0/tests/test_contract_cells_handles.py +845 -0
  81. seamless_workflow-0.2.0/tests/test_contract_compiled_pins_workflow.py +397 -0
  82. seamless_workflow-0.2.0/tests/test_contract_deep_celltypes_bound.py +162 -0
  83. seamless_workflow-0.2.0/tests/test_contract_deep_handles_bound.py +549 -0
  84. seamless_workflow-0.2.0/tests/test_contract_deep_wiring_bound.py +234 -0
  85. seamless_workflow-0.2.0/tests/test_contract_direct_expression_binding.py +110 -0
  86. seamless_workflow-0.2.0/tests/test_contract_handle_celltypes.py +450 -0
  87. seamless_workflow-0.2.0/tests/test_contract_handle_reads.py +336 -0
  88. seamless_workflow-0.2.0/tests/test_contract_input_side_reverse_publish.py +81 -0
  89. seamless_workflow-0.2.0/tests/test_contract_linger_vs_revert_hold.py +34 -0
  90. seamless_workflow-0.2.0/tests/test_contract_mounts.py +1294 -0
  91. seamless_workflow-0.2.0/tests/test_contract_node_state_lifecycle.py +997 -0
  92. seamless_workflow-0.2.0/tests/test_contract_pins_bound.py +555 -0
  93. seamless_workflow-0.2.0/tests/test_contract_process_lock.py +196 -0
  94. seamless_workflow-0.2.0/tests/test_contract_reference_lifecycle_anonymous.py +217 -0
  95. seamless_workflow-0.2.0/tests/test_contract_reference_lifecycle_scratch.py +196 -0
  96. seamless_workflow-0.2.0/tests/test_contract_scratch_transformer_remote.py +65 -0
  97. seamless_workflow-0.2.0/tests/test_contract_shares.py +1637 -0
  98. seamless_workflow-0.2.0/tests/test_contract_transformer_bound.py +221 -0
  99. seamless_workflow-0.2.0/tests/test_contract_upstream_hold.py +204 -0
  100. seamless_workflow-0.2.0/tests/test_contract_workflow_context.py +889 -0
  101. seamless_workflow-0.2.0/tests/test_controller.py +189 -0
  102. seamless_workflow-0.2.0/tests/test_controller_lifecycle.py +180 -0
  103. seamless_workflow-0.2.0/tests/test_dependencies_and_pins.py +80 -0
  104. seamless_workflow-0.2.0/tests/test_direct_print_removal.py +41 -0
  105. seamless_workflow-0.2.0/tests/test_execution_error_types.py +134 -0
  106. seamless_workflow-0.2.0/tests/test_expression_execution.py +189 -0
  107. seamless_workflow-0.2.0/tests/test_followup_audit.py +24 -0
  108. seamless_workflow-0.2.0/tests/test_followup_contract.py +13 -0
  109. seamless_workflow-0.2.0/tests/test_garbage_collection.py +130 -0
  110. seamless_workflow-0.2.0/tests/test_language_handlers_bound.py +92 -0
  111. seamless_workflow-0.2.0/tests/test_late_expression_materialization.py +64 -0
  112. seamless_workflow-0.2.0/tests/test_literal_retention.py +65 -0
  113. seamless_workflow-0.2.0/tests/test_materialisation.py +22 -0
  114. seamless_workflow-0.2.0/tests/test_mount_buffer_source_integration.py +182 -0
  115. seamless_workflow-0.2.0/tests/test_mount_null.py +173 -0
  116. seamless_workflow-0.2.0/tests/test_mount_transport.py +113 -0
  117. seamless_workflow-0.2.0/tests/test_mounts.py +613 -0
  118. seamless_workflow-0.2.0/tests/test_optional_pin_contract.py +111 -0
  119. seamless_workflow-0.2.0/tests/test_pin_celltype_changes.py +76 -0
  120. seamless_workflow-0.2.0/tests/test_pin_handles.py +235 -0
  121. seamless_workflow-0.2.0/tests/test_pin_scratch_contract.py +257 -0
  122. seamless_workflow-0.2.0/tests/test_public_workflow_api.py +22 -0
  123. seamless_workflow-0.2.0/tests/test_python_backend_bound.py +93 -0
  124. seamless_workflow-0.2.0/tests/test_reactive_engine.py +116 -0
  125. seamless_workflow-0.2.0/tests/test_reference_lifecycle_binding.py +300 -0
  126. seamless_workflow-0.2.0/tests/test_reference_lifecycle_forced_expiry.py +287 -0
  127. seamless_workflow-0.2.0/tests/test_review_expression_chains.py +168 -0
  128. seamless_workflow-0.2.0/tests/test_runtime_and_prune.py +71 -0
  129. seamless_workflow-0.2.0/tests/test_serve_graph.py +151 -0
  130. seamless_workflow-0.2.0/tests/test_share_browser.py +294 -0
  131. seamless_workflow-0.2.0/tests/test_share_client_js.py +577 -0
  132. seamless_workflow-0.2.0/tests/test_share_client_restart.py +230 -0
  133. seamless_workflow-0.2.0/tests/test_share_close_hold.py +234 -0
  134. seamless_workflow-0.2.0/tests/test_share_downloads.py +404 -0
  135. seamless_workflow-0.2.0/tests/test_share_state_graph.py +243 -0
  136. seamless_workflow-0.2.0/tests/test_share_transport.py +704 -0
  137. seamless_workflow-0.2.0/tests/test_spawned_traceback.py +42 -0
  138. seamless_workflow-0.2.0/tests/test_state_machine.py +124 -0
  139. seamless_workflow-0.2.0/tests/test_transformer_binding.py +80 -0
  140. seamless_workflow-0.2.0/tests/test_transformer_block_reason.py +153 -0
  141. seamless_workflow-0.2.0/tests/test_transformer_logs_bound.py +85 -0
  142. seamless_workflow-0.2.0/tests/test_transformer_streaming_bound.py +37 -0
  143. seamless_workflow-0.2.0/tests/test_undeserializable_expression_result.py +42 -0
  144. seamless_workflow-0.2.0/tests/test_widgets.py +578 -0
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: seamless-workflow
3
+ Version: 0.2.0
4
+ Summary: Reactive workflow Context layer for Seamless
5
+ Author: Sjoerd de Vries
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sjdv1982/seamless
8
+ Project-URL: Repository, https://github.com/sjdv1982/seamless-workflow
9
+ Project-URL: Issues, https://github.com/sjdv1982/seamless/issues
10
+ Keywords: seamless,workflow,reactive,checksum,reproducible
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: seamless-core>=0.2
22
+ Requires-Dist: seamless-transformer>=0.8.0
23
+ Requires-Dist: traitlets
24
+ Requires-Dist: ipywidgets
25
+ Requires-Dist: aiohttp>=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+
29
+ # seamless-workflow
30
+
31
+ Reactive workflow `Context` layer for Seamless.
32
+
33
+ Context nodes are exposed through the canonical `seamless.Cell` and
34
+ `seamless_transformer.Transformer` handles. The experimental wrapper/view surface
35
+ described by the earlier context handoff documents is superseded by
36
+ [`context-internals-followup-plan.md`](../seamless/context-internals-followup-plan.md).
37
+
38
+ Construct cells with `Cell("int")` or `Cell(celltype="int")`; the default type
39
+ is `"mixed"` without a typed source. Initial references are keyword-only, for example
40
+ `Cell("int", checksum=checksum)`. Assign values with `.set(value)`.
41
+
42
+ `celltype` is the output type; `input_celltype` is read-only and follows the
43
+ source or the constant's declared serialization type. Creating `ctx.a = ctx.b`
44
+ copies `b.celltype` once. Rewiring an existing cell retains its type. Retyping
45
+ converts the original input; `.build().run()` agrees with `.value`. A cell's own
46
+ conversion failure reports `failed` with `.exception`.
47
+
48
+ `cell.source` reports the configured source. On a projection it reports the edge
49
+ at that path, else the nearest enclosing source, else None. `.checksum` reads
50
+ the produced checksum. Public `input_ref` and `target_celltype` are retired.
51
+
52
+ `ctx.tf.pins.x` returns a fresh Pin, also when unwired. Read `.value`, `.checksum`,
53
+ `.buffer`, `.source`, `.input_celltype`, `.state`, or `.exception` explicitly.
54
+ `pin.celltype` and `ctx.tf.celltypes.x` are linked. Pins convert their original
55
+ inputs before transformation construction; a failed conversion leaves the Pin
56
+ failed and the Transformer blocked on its name. A Pin is not a source; connect
57
+ `pin.source`. There are no sub-pin targets, validators, or mounts.
58
+
59
+ For Cells and Pins, `.value`, `.buffer`, and `.checksum` assignments declare a
60
+ new input and detach connections; `set`, `set_buffer`, and `set_checksum` check
61
+ ownership. `set_checksum(cs, input_celltype=...)` declares the input encoding.
62
+ `.value = None` stores canonical null; `.buffer = None` and `.checksum = None`
63
+ clear the input. All Cell types allow null. Required pins allow null only for
64
+ plain/mixed/bytes; optional pins of any type drop null before conversion.
65
+ A connected optional pin whose upstream has no checksum blocks.
66
+
67
+ `del ctx.a` deletes a node. `del ctx.tf.pins.x` deletes a declaration only for
68
+ signatureless code; use `.checksum = None` to clear a fixed-signature pin.
69
+
70
+ Whole Context cells can be mounted to files:
71
+
72
+ ```python
73
+ from seamless.workflow import Cell, Context, Transformer
74
+
75
+ with Context() as ctx:
76
+ ctx.config = Cell("plain")
77
+ ctx.config.mount("config.json")
78
+ ctx.output = Cell("text")
79
+ ctx.output.set("ready")
80
+ ctx.output.mount("output.txt", mode="w")
81
+ report = ctx.mounts.sync(timeout=10)
82
+ ```
83
+
84
+ `mount(path, mode="rw", authority="file", persistent=True)` blocks through the
85
+ initial read and first write attempt. Modes are `r`, `w`, and `rw`. Authority
86
+ chooses the initial winner; subsequent file edits are authoritative inputs in
87
+ sensing modes. A missing path supplies no value, while zero-byte and `null\n`
88
+ files supply null. At mount time, missing, zero-byte, and an empty directory do
89
+ not override an existing cell value. A sensing mount on a cell without a value
90
+ installs null; `file-strict` instead fails when the path is missing. Invalid or
91
+ unreadable input fails the cell, preserves its stored last good value, and
92
+ continues monitoring. Delivery failures appear on `ctx.output.mount.error` and
93
+ retry with backoff; they do not invalidate the cell's value.
94
+
95
+ `ctx.compute()` waits only for graph work. Call `ctx.mounts.sync()` before
96
+ reading mounted outputs externally, or `await ctx.mounts.synchronization()` in
97
+ async code. The returned mapping contains status and errors per node path.
98
+ Synchronization establishes a finite filesystem cut; continuously changing
99
+ external files can prevent completion, so interactive clients can set a timeout.
100
+
101
+ Use `del ctx.config.mount` to unmount. `persistent=False` conditionally deletes
102
+ unchanged files at unmount or close. Explicit Context close flushes already
103
+ requested persistent deliveries, bounded by `close(timeout=60)`. A foreign
104
+ modification is never deleted based solely on its path. Two incompatible mounts
105
+ of overlapping paths within one process are refused. Write-only mounts pause
106
+ after three foreign-write reassertions in 20 seconds; inspect `.mount.error` and
107
+ call `.mount.clear_error()` to resume.
108
+
109
+ Null writes truncate an existing file to zero bytes, including `.gz` and `.zst`
110
+ paths, but do not create a missing file. Deleting a sensed file preserves the
111
+ stored cell value; under `file-strict` it raises a recoverable sense error. An
112
+ initially empty directory supplies no value, while a directory emptied after
113
+ mounting reads as `{}`. Directory null delivery leaves the tree in place and the
114
+ mount out of sync. Clearing a mounted cell is refused; unmount first. These rules
115
+ are separate from explicit nonpersistent unmount cleanup.
116
+
117
+ Text/code, JSON scalar/plain, bytes, binary, and mixed celltypes are supported.
118
+ `.gz` and `.zst` paths compress canonical bytes. `folder` and `deepfolder` use
119
+ directories, with per-leaf atomic replacement. `folder` mounts in `r`, `w` or
120
+ `rw`; `deepfolder` is read-only (sensing) and may only be mounted with
121
+ `mode="r"`. Directory trees are not replaced
122
+ atomically. Mounts follow a target symlink and preserve it; atomic file writes
123
+ break hardlink sharing. Cross-process exclusion and filesystem aliases such as
124
+ hardlinks are not covered by the registry.
125
+
126
+ Graphs retain mount specifications (format `0.4`). Legacy 0.2/0.3 graphs load
127
+ only when `target_celltype` is absent or equals `celltype`; conflicting graphs
128
+ raise `PathError`. Constant producers retain their input encoding as `value.celltype`. **Loading a graph can write
129
+ files:** use `ctx.set_graph(graph, mounts=False)` for graphs of unknown origin.
130
+ Sensing cells cannot receive incoming edges; mounted celltypes cannot change.
131
+ Unmount first. Standalone cells, sub-path projections, transformer pins and code
132
+ handles cannot be mounted directly: mount a whole cell and connect it instead.
133
+
134
+ The transport currently uses polling and a bounded daemon I/O pool. Diagnostics
135
+ include `seamless_workflow.diagnostics.record_attachments(ctx)` and the
136
+ controllable `attachments.manual.ManualDriver` for ordering tests.
137
+
138
+ Set `SEAMLESS_MOUNT_NATIVE=1` before the first mount to enable Linux inotify hints
139
+ alongside polling. Unsupported platforms and failed watches retain polling.
140
+
141
+ For notebooks, install `seamless-workflow[jupyter]` and use the public widget
142
+ helpers:
143
+
144
+ ```python
145
+ from IPython.display import display
146
+ from seamless_workflow.jupyter import traitlet, output
147
+
148
+ display(output(ctx.c))
149
+ traitlet(ctx.a).link(slider)
150
+ ```
151
+
152
+ The widget hub has its own runtime attachment slot and can coexist with
153
+ `ctx.a.mount("a.txt")` on the same cell. `ctx.mounts.sync()` reports both
154
+ sessions; `del ctx.a.mount` detaches only the file slot.
155
+
156
+ Widget attachments are runtime state and are never serialized. The underlying
157
+ `attachments.widget.WidgetDriver` remains an internal transport.
@@ -0,0 +1,129 @@
1
+ # seamless-workflow
2
+
3
+ Reactive workflow `Context` layer for Seamless.
4
+
5
+ Context nodes are exposed through the canonical `seamless.Cell` and
6
+ `seamless_transformer.Transformer` handles. The experimental wrapper/view surface
7
+ described by the earlier context handoff documents is superseded by
8
+ [`context-internals-followup-plan.md`](../seamless/context-internals-followup-plan.md).
9
+
10
+ Construct cells with `Cell("int")` or `Cell(celltype="int")`; the default type
11
+ is `"mixed"` without a typed source. Initial references are keyword-only, for example
12
+ `Cell("int", checksum=checksum)`. Assign values with `.set(value)`.
13
+
14
+ `celltype` is the output type; `input_celltype` is read-only and follows the
15
+ source or the constant's declared serialization type. Creating `ctx.a = ctx.b`
16
+ copies `b.celltype` once. Rewiring an existing cell retains its type. Retyping
17
+ converts the original input; `.build().run()` agrees with `.value`. A cell's own
18
+ conversion failure reports `failed` with `.exception`.
19
+
20
+ `cell.source` reports the configured source. On a projection it reports the edge
21
+ at that path, else the nearest enclosing source, else None. `.checksum` reads
22
+ the produced checksum. Public `input_ref` and `target_celltype` are retired.
23
+
24
+ `ctx.tf.pins.x` returns a fresh Pin, also when unwired. Read `.value`, `.checksum`,
25
+ `.buffer`, `.source`, `.input_celltype`, `.state`, or `.exception` explicitly.
26
+ `pin.celltype` and `ctx.tf.celltypes.x` are linked. Pins convert their original
27
+ inputs before transformation construction; a failed conversion leaves the Pin
28
+ failed and the Transformer blocked on its name. A Pin is not a source; connect
29
+ `pin.source`. There are no sub-pin targets, validators, or mounts.
30
+
31
+ For Cells and Pins, `.value`, `.buffer`, and `.checksum` assignments declare a
32
+ new input and detach connections; `set`, `set_buffer`, and `set_checksum` check
33
+ ownership. `set_checksum(cs, input_celltype=...)` declares the input encoding.
34
+ `.value = None` stores canonical null; `.buffer = None` and `.checksum = None`
35
+ clear the input. All Cell types allow null. Required pins allow null only for
36
+ plain/mixed/bytes; optional pins of any type drop null before conversion.
37
+ A connected optional pin whose upstream has no checksum blocks.
38
+
39
+ `del ctx.a` deletes a node. `del ctx.tf.pins.x` deletes a declaration only for
40
+ signatureless code; use `.checksum = None` to clear a fixed-signature pin.
41
+
42
+ Whole Context cells can be mounted to files:
43
+
44
+ ```python
45
+ from seamless.workflow import Cell, Context, Transformer
46
+
47
+ with Context() as ctx:
48
+ ctx.config = Cell("plain")
49
+ ctx.config.mount("config.json")
50
+ ctx.output = Cell("text")
51
+ ctx.output.set("ready")
52
+ ctx.output.mount("output.txt", mode="w")
53
+ report = ctx.mounts.sync(timeout=10)
54
+ ```
55
+
56
+ `mount(path, mode="rw", authority="file", persistent=True)` blocks through the
57
+ initial read and first write attempt. Modes are `r`, `w`, and `rw`. Authority
58
+ chooses the initial winner; subsequent file edits are authoritative inputs in
59
+ sensing modes. A missing path supplies no value, while zero-byte and `null\n`
60
+ files supply null. At mount time, missing, zero-byte, and an empty directory do
61
+ not override an existing cell value. A sensing mount on a cell without a value
62
+ installs null; `file-strict` instead fails when the path is missing. Invalid or
63
+ unreadable input fails the cell, preserves its stored last good value, and
64
+ continues monitoring. Delivery failures appear on `ctx.output.mount.error` and
65
+ retry with backoff; they do not invalidate the cell's value.
66
+
67
+ `ctx.compute()` waits only for graph work. Call `ctx.mounts.sync()` before
68
+ reading mounted outputs externally, or `await ctx.mounts.synchronization()` in
69
+ async code. The returned mapping contains status and errors per node path.
70
+ Synchronization establishes a finite filesystem cut; continuously changing
71
+ external files can prevent completion, so interactive clients can set a timeout.
72
+
73
+ Use `del ctx.config.mount` to unmount. `persistent=False` conditionally deletes
74
+ unchanged files at unmount or close. Explicit Context close flushes already
75
+ requested persistent deliveries, bounded by `close(timeout=60)`. A foreign
76
+ modification is never deleted based solely on its path. Two incompatible mounts
77
+ of overlapping paths within one process are refused. Write-only mounts pause
78
+ after three foreign-write reassertions in 20 seconds; inspect `.mount.error` and
79
+ call `.mount.clear_error()` to resume.
80
+
81
+ Null writes truncate an existing file to zero bytes, including `.gz` and `.zst`
82
+ paths, but do not create a missing file. Deleting a sensed file preserves the
83
+ stored cell value; under `file-strict` it raises a recoverable sense error. An
84
+ initially empty directory supplies no value, while a directory emptied after
85
+ mounting reads as `{}`. Directory null delivery leaves the tree in place and the
86
+ mount out of sync. Clearing a mounted cell is refused; unmount first. These rules
87
+ are separate from explicit nonpersistent unmount cleanup.
88
+
89
+ Text/code, JSON scalar/plain, bytes, binary, and mixed celltypes are supported.
90
+ `.gz` and `.zst` paths compress canonical bytes. `folder` and `deepfolder` use
91
+ directories, with per-leaf atomic replacement. `folder` mounts in `r`, `w` or
92
+ `rw`; `deepfolder` is read-only (sensing) and may only be mounted with
93
+ `mode="r"`. Directory trees are not replaced
94
+ atomically. Mounts follow a target symlink and preserve it; atomic file writes
95
+ break hardlink sharing. Cross-process exclusion and filesystem aliases such as
96
+ hardlinks are not covered by the registry.
97
+
98
+ Graphs retain mount specifications (format `0.4`). Legacy 0.2/0.3 graphs load
99
+ only when `target_celltype` is absent or equals `celltype`; conflicting graphs
100
+ raise `PathError`. Constant producers retain their input encoding as `value.celltype`. **Loading a graph can write
101
+ files:** use `ctx.set_graph(graph, mounts=False)` for graphs of unknown origin.
102
+ Sensing cells cannot receive incoming edges; mounted celltypes cannot change.
103
+ Unmount first. Standalone cells, sub-path projections, transformer pins and code
104
+ handles cannot be mounted directly: mount a whole cell and connect it instead.
105
+
106
+ The transport currently uses polling and a bounded daemon I/O pool. Diagnostics
107
+ include `seamless_workflow.diagnostics.record_attachments(ctx)` and the
108
+ controllable `attachments.manual.ManualDriver` for ordering tests.
109
+
110
+ Set `SEAMLESS_MOUNT_NATIVE=1` before the first mount to enable Linux inotify hints
111
+ alongside polling. Unsupported platforms and failed watches retain polling.
112
+
113
+ For notebooks, install `seamless-workflow[jupyter]` and use the public widget
114
+ helpers:
115
+
116
+ ```python
117
+ from IPython.display import display
118
+ from seamless_workflow.jupyter import traitlet, output
119
+
120
+ display(output(ctx.c))
121
+ traitlet(ctx.a).link(slider)
122
+ ```
123
+
124
+ The widget hub has its own runtime attachment slot and can coexist with
125
+ `ctx.a.mount("a.txt")` on the same cell. `ctx.mounts.sync()` reports both
126
+ sessions; `del ctx.a.mount` detaches only the file slot.
127
+
128
+ Widget attachments are runtime state and are never serialized. The underlying
129
+ `attachments.widget.WidgetDriver` remains an internal transport.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "seamless-workflow"
7
+ version = "0.2.0"
8
+ description = "Reactive workflow Context layer for Seamless"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{name = "Sjoerd de Vries"}]
13
+ dependencies = [
14
+ "seamless-core>=0.2",
15
+ "seamless-transformer>=0.8.0",
16
+ "traitlets",
17
+ "ipywidgets",
18
+ "aiohttp>=3.9"
19
+ ]
20
+ keywords = ["seamless", "workflow", "reactive", "checksum", "reproducible"]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Science/Research",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Operating System :: OS Independent",
28
+ "Topic :: Scientific/Engineering",
29
+ "Topic :: Software Development :: Libraries",
30
+ ]
31
+
32
+ [project.scripts]
33
+ seamless-serve-graph = "seamless_workflow.serve_graph:main"
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/sjdv1982/seamless"
40
+ Repository = "https://github.com/sjdv1982/seamless-workflow"
41
+ Issues = "https://github.com/sjdv1982/seamless/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ include = ["seamless_workflow*"]
46
+ exclude = ["tests*"]
47
+
48
+ [tool.setuptools.package-data]
49
+ seamless_workflow = ["attachments/share/static/*.js"]
50
+
51
+ [tool.pytest.ini_options]
52
+ pythonpath = ["."]
@@ -0,0 +1,40 @@
1
+ """Reactive workflow layer for Seamless."""
2
+
3
+ __version__ = "0.1.0"
4
+ from .context import Context
5
+ from seamless import Cell
6
+ from seamless_transformer import Transformer
7
+ from .endpoints import BoundEndpoint
8
+ from . import shareserver
9
+ from .errors import (
10
+ AuthorityError,
11
+ DependencyError,
12
+ PathError,
13
+ ReadOnlyEndpointError,
14
+ StaleWorkflowHandleError,
15
+ ValueUnavailableError,
16
+ ClosedContextError,
17
+ ConcurrentUpdateError,
18
+ ControllerFailedError,
19
+ ReentrantContextError,
20
+ WorkflowExecutionError,
21
+ )
22
+
23
+ __all__ = [
24
+ "AuthorityError",
25
+ "BoundEndpoint",
26
+ "Context",
27
+ "Cell",
28
+ "Transformer",
29
+ "DependencyError",
30
+ "PathError",
31
+ "ReadOnlyEndpointError",
32
+ "StaleWorkflowHandleError",
33
+ "ValueUnavailableError",
34
+ "ClosedContextError",
35
+ "ConcurrentUpdateError",
36
+ "ControllerFailedError",
37
+ "ReentrantContextError",
38
+ "WorkflowExecutionError",
39
+ "shareserver",
40
+ ]
@@ -0,0 +1,50 @@
1
+ """Narrow substrate adapters used by the workflow layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from seamless import Buffer, Checksum
8
+
9
+
10
+ def checksum_for_value(
11
+ value: Any, celltype: str = "mixed", *, checksum_is_value: bool = False
12
+ ) -> Checksum:
13
+ """Serialize a Python value and return its checksum.
14
+
15
+ An explicit Checksum is already serialized state, and is returned as it is.
16
+ The exception is a written value (``checksum_is_value``) of celltype
17
+ ``checksum``: there a Checksum is the value itself.
18
+ """
19
+
20
+ # Ordinary 64-character strings are never reinterpreted as checksums.
21
+ if isinstance(value, Checksum) and not (checksum_is_value and celltype == "checksum"):
22
+ return value
23
+ buffer = Buffer(value, celltype)
24
+ checksum = buffer.get_checksum()
25
+ buffer.tempref()
26
+ return checksum
27
+
28
+
29
+ def buffer_for_checksum(checksum: Checksum | str | bytes | None):
30
+ if checksum is None:
31
+ return None
32
+ return Checksum(checksum).resolve()
33
+
34
+
35
+ def value_for_checksum(checksum: Checksum | str | bytes | None, celltype: str = "mixed"):
36
+ if checksum is None:
37
+ return None
38
+ return Checksum(checksum).resolve(celltype)
39
+
40
+
41
+ def normalize_checksum(checksum: Checksum | str | bytes) -> Checksum:
42
+ return Checksum(checksum)
43
+
44
+
45
+ __all__ = [
46
+ "checksum_for_value",
47
+ "buffer_for_checksum",
48
+ "value_for_checksum",
49
+ "normalize_checksum",
50
+ ]
@@ -0,0 +1,8 @@
1
+ """Assignment helper namespace.
2
+
3
+ The implementation currently keeps assignment methods on :mod:`context` because they
4
+ need direct access to graph mutation internals. This module is intentionally present
5
+ as the public scaffold extension point named by the implementation handoff.
6
+ """
7
+
8
+ __all__: list[str] = []
@@ -0,0 +1,3 @@
1
+ """Context attachments and file mounts."""
2
+ from .spec import AttachmentSpec
3
+ from .session import MountError, ConflictError, SyncReport
@@ -0,0 +1,217 @@
1
+ """Caller-side mount preparation and public handles."""
2
+ import asyncio
3
+ import weakref
4
+ from concurrent.futures import Future
5
+ from uuid import uuid4
6
+ from .spec import AttachmentSpec
7
+ from .fs.service import get_service
8
+ from ..errors import ClosedContextError, ControllerFailedError
9
+
10
+
11
+ def make_sink(controller):
12
+ reference = weakref.ref(controller)
13
+ def sink(operation, payload):
14
+ def fail(error):
15
+ if hasattr(payload, 'release'):
16
+ payload.release()
17
+ reply = Future()
18
+ reply.set_exception(error)
19
+ return reply
20
+ owner = reference()
21
+ if owner is None:
22
+ return fail(ClosedContextError("Context is closed"))
23
+ try:
24
+ reply = owner.enqueue(operation, (payload,), klass=3 if operation == '_mount_observed' else 5,
25
+ internal=operation != '_mount_observed')
26
+ except (ClosedContextError, ControllerFailedError) as exc:
27
+ return fail(exc)
28
+ except BaseException as exc:
29
+ return fail(exc)
30
+ else:
31
+ if hasattr(payload, 'release'):
32
+ # Also covers accepted messages discarded by stop or poison.
33
+ reply.add_done_callback(lambda future: payload.release())
34
+ return reply
35
+ return sink
36
+
37
+
38
+ class MountHandle:
39
+ def __init__(self, backend):
40
+ self.backend = backend
41
+
42
+ def _context(self):
43
+ b = self.backend
44
+ b.context._check_public_caller()
45
+ if getattr(b, "local_path", ()) or getattr(b, "readonly", False):
46
+ raise AttributeError('Only whole Context cell nodes can be mounted')
47
+ from ..errors import NodeError, StaleWorkflowHandleError
48
+
49
+ try:
50
+ b._node()
51
+ except StaleWorkflowHandleError as exc:
52
+ raise NodeError('Mounts require an existing whole cell node') from exc
53
+ return b.context, b.node_path
54
+
55
+ def __call__(self, path, mode='rw', authority='file', *, persistent=True):
56
+ ctx, node_path = self._context()
57
+ spec = AttachmentSpec(path, mode, authority, persistent)
58
+ celltype = ctx._controller.call('_mount_validate', node_path, spec, klass=4)
59
+ service = get_service()
60
+ registration = service.reserve(spec, celltype, uuid4().hex, make_sink(ctx._controller))
61
+ try:
62
+ observation = service.initial_read(registration).result()
63
+ try:
64
+ initial = ctx._controller.call('_mount_attach', node_path, spec, registration, observation, klass=2)
65
+ finally: observation.release()
66
+ initial.result()
67
+ except Exception:
68
+ service.unregister(registration).result()
69
+ raise
70
+
71
+ def unmount(self):
72
+ ctx, path = self._context()
73
+ future = ctx._controller.call('_mount_detach', path, klass=2)
74
+ if future is not None: future.result(get_service().delivery_timeout)
75
+
76
+ @property
77
+ def spec(self):
78
+ ctx, path = self._context()
79
+ return ctx._node_snapshot(path).mount
80
+
81
+ @property
82
+ def status(self):
83
+ ctx, path = self._context()
84
+ return ctx._controller.call('_mount_status', path, klass=4)
85
+
86
+ @property
87
+ def error(self):
88
+ status = self.status
89
+ return None if status is None else status['error']
90
+
91
+ def clear_error(self):
92
+ ctx, path = self._context()
93
+ ctx._controller.call('_mount_clear_error', path, klass=2)
94
+
95
+
96
+ class ContextMounts:
97
+ def __init__(self, context): self.context = context
98
+
99
+ def sync(self, timeout=None):
100
+ ctx = self.context
101
+ ctx._check_public_caller()
102
+ future = ctx._controller.call('_mount_sync', klass=1)
103
+ try: return future.result(timeout)
104
+ finally:
105
+ if ctx._controller.accepting: ctx._controller.call('_withdraw_wait', future, klass=1)
106
+
107
+ async def synchronization(self, timeout=None):
108
+ ctx = self.context
109
+ ctx._check_public_caller()
110
+ installed = ctx._controller.submit('_mount_sync', klass=1)
111
+ try: future = await asyncio.shield(asyncio.wrap_future(installed))
112
+ except asyncio.CancelledError:
113
+ def withdraw(reply):
114
+ if not reply.cancelled() and reply.exception() is None:
115
+ ctx._controller.notify('_withdraw_wait', (reply.result(),), klass=1)
116
+ installed.add_done_callback(withdraw)
117
+ raise
118
+ try:
119
+ return await asyncio.wait_for(asyncio.shield(asyncio.wrap_future(future)), timeout)
120
+ finally:
121
+ if ctx._controller.accepting:
122
+ await asyncio.wrap_future(ctx._controller.submit('_withdraw_wait', (future,), klass=1))
123
+
124
+ @property
125
+ def errors(self):
126
+ report = self.context._controller.call('_mount_report', klass=4)
127
+ return {p: s['error'] or s['sense_error'] for p, s in report.items() if s['error'] or s['sense_error']}
128
+
129
+
130
+ def load_graph(ctx, data, *, mounts=True, shares=True):
131
+ context_shares = ctx.shares
132
+ with context_shares._lock:
133
+ return _load_graph(
134
+ ctx, data, mounts=mounts, shares=shares,
135
+ context_shares=context_shares,
136
+ )
137
+
138
+
139
+ def _load_graph(ctx, data, *, mounts=True, shares=True, context_shares):
140
+ """Load a graph, optionally restoring its file mounts, HTTP shares and downloads."""
141
+ import copy
142
+ from .spec import validate_celltype
143
+ from ..serialization import prepare_graph
144
+ graph = prepare_graph(copy.deepcopy(data))
145
+ old = ctx._controller.call('_mount_registrations', klass=4)
146
+ old_shares = tuple(
147
+ registration for registration in old
148
+ if getattr(getattr(registration, 'spec', None), 'driver', None) == 'share'
149
+ )
150
+ old_downloads = tuple(context_shares._downloads.values())
151
+ download_specs = graph.downloads if shares else {}
152
+ graph.downloads = dict(download_specs)
153
+ prepared, reservations = [], []
154
+ service = get_service() if mounts and any(n.mount for n in graph.nodes.values()) else None
155
+ share_prepared = []
156
+ download_registrations = []
157
+ share_driver = (
158
+ context_shares._driver
159
+ if shares and (download_specs or any('share' in n.attachments for n in graph.nodes.values()))
160
+ else None
161
+ )
162
+ try:
163
+ reads = []
164
+ for path, node in graph.nodes.items():
165
+ spec, node.mount = node.mount, None
166
+ share_spec = node.attachments.pop('share', None)
167
+ if spec is not None and mounts:
168
+ celltype = node.cell_config.celltype
169
+ validate_celltype(celltype, spec.mode)
170
+ reg = service.reserve(spec, celltype, uuid4().hex, make_sink(ctx._controller), replaces=old)
171
+ reservations.append(reg)
172
+ reads.append(('mount', path, spec, reg, service.initial_read(reg)))
173
+ if share_spec is not None and shares:
174
+ celltype = node.cell_config.celltype
175
+ reg = share_driver.reserve(
176
+ share_spec, celltype, uuid4().hex, make_sink(ctx._controller),
177
+ replaces=old_shares + old_downloads, staged=True, node_path=path,
178
+ )
179
+ reservations.append(reg)
180
+ reads.append(('share', path, share_spec, reg, share_driver.initial_read(reg)))
181
+ for download_spec in download_specs.values():
182
+ reg = share_driver.reserve_download(
183
+ download_spec, context_shares._download_provider(download_spec),
184
+ replaces=old_shares + old_downloads, staged=True,
185
+ )
186
+ reservations.append(reg)
187
+ download_registrations.append(reg)
188
+ if share_driver is not None:
189
+ share_driver.start()
190
+ for kind, path, spec, reg, future in reads:
191
+ prepared_item = (path, spec, reg, future.result())
192
+ (prepared if kind == 'mount' else share_prepared).append(prepared_item)
193
+ initial = ctx._controller.call(
194
+ 'set_graph', graph, mount_prepared=tuple(prepared),
195
+ share_prepared=tuple(share_prepared),
196
+ download_registrations=tuple(download_registrations), klass=2,
197
+ )
198
+ for future in initial: future.result()
199
+ for reg in download_registrations:
200
+ share_driver.activate(reg).result(share_driver.delivery_timeout)
201
+ except BaseException:
202
+ for reg in reversed(reservations):
203
+ try:
204
+ future = reg.service.unregister(reg)
205
+ if future is not None:
206
+ future.result(getattr(reg.service, 'delivery_timeout', 60))
207
+ except BaseException:
208
+ pass
209
+ raise
210
+ finally:
211
+ for _, _, _, observation in (*prepared, *share_prepared): observation.release()
212
+ # The old graph's downloads go with it, except those whose key a new
213
+ # share or download took over (already removed by the commit).
214
+ for reg in old_downloads:
215
+ if not reg.closed:
216
+ context_shares._driver.unregister(reg).result(context_shares._driver.delivery_timeout)
217
+ context_shares._downloads = {reg.spec.path: reg for reg in download_registrations}
@@ -0,0 +1 @@
1
+ """Process-global filesystem transport."""