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.
- seamless_workflow-0.2.0/PKG-INFO +157 -0
- seamless_workflow-0.2.0/README.md +129 -0
- seamless_workflow-0.2.0/pyproject.toml +52 -0
- seamless_workflow-0.2.0/seamless_workflow/__init__.py +40 -0
- seamless_workflow-0.2.0/seamless_workflow/adapters.py +50 -0
- seamless_workflow-0.2.0/seamless_workflow/assignment.py +8 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/__init__.py +3 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/api.py +217 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/fs/__init__.py +1 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/fs/native.py +63 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/fs/service.py +601 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/manual.py +65 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/output_widget.py +62 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/policy.py +41 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/runtime.py +594 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/session.py +93 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/__init__.py +2 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/api.py +300 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/archive.py +60 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/driver.py +110 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/mime.py +180 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/openapi.py +358 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/server.py +1660 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/spec.py +139 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/share/static/seamless-client.js +538 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/spec.py +42 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/traitlet.py +344 -0
- seamless_workflow-0.2.0/seamless_workflow/attachments/widget.py +229 -0
- seamless_workflow-0.2.0/seamless_workflow/builder_state.py +1107 -0
- seamless_workflow-0.2.0/seamless_workflow/configuration.py +103 -0
- seamless_workflow-0.2.0/seamless_workflow/context.py +3287 -0
- seamless_workflow-0.2.0/seamless_workflow/controller.py +160 -0
- seamless_workflow-0.2.0/seamless_workflow/diagnostics.py +69 -0
- seamless_workflow-0.2.0/seamless_workflow/endpoints.py +33 -0
- seamless_workflow-0.2.0/seamless_workflow/errors.py +69 -0
- seamless_workflow-0.2.0/seamless_workflow/graph.py +636 -0
- seamless_workflow-0.2.0/seamless_workflow/ingress.py +411 -0
- seamless_workflow-0.2.0/seamless_workflow/jupyter.py +49 -0
- seamless_workflow-0.2.0/seamless_workflow/lifecycle.py +26 -0
- seamless_workflow-0.2.0/seamless_workflow/reactive.py +386 -0
- seamless_workflow-0.2.0/seamless_workflow/runtime_api.py +227 -0
- seamless_workflow-0.2.0/seamless_workflow/scheduler.py +106 -0
- seamless_workflow-0.2.0/seamless_workflow/serialization.py +317 -0
- seamless_workflow-0.2.0/seamless_workflow/serve_graph.py +55 -0
- seamless_workflow-0.2.0/seamless_workflow/shareserver.py +36 -0
- seamless_workflow-0.2.0/seamless_workflow/sidework.py +102 -0
- seamless_workflow-0.2.0/seamless_workflow/views.py +62 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/PKG-INFO +157 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/SOURCES.txt +142 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/dependency_links.txt +1 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/entry_points.txt +2 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/requires.txt +8 -0
- seamless_workflow-0.2.0/seamless_workflow.egg-info/top_level.txt +1 -0
- seamless_workflow-0.2.0/setup.cfg +4 -0
- seamless_workflow-0.2.0/tests/test_assignment_semantics.py +277 -0
- seamless_workflow-0.2.0/tests/test_attachment_slots.py +552 -0
- seamless_workflow-0.2.0/tests/test_callable_reassignment.py +168 -0
- seamless_workflow-0.2.0/tests/test_canonical_handles.py +317 -0
- seamless_workflow-0.2.0/tests/test_cell_expression_builder.py +121 -0
- seamless_workflow-0.2.0/tests/test_cell_inspection_contracts.py +57 -0
- seamless_workflow-0.2.0/tests/test_cell_joins.py +271 -0
- seamless_workflow-0.2.0/tests/test_cell_scratch.py +36 -0
- seamless_workflow-0.2.0/tests/test_cell_semantics.py +349 -0
- seamless_workflow-0.2.0/tests/test_celljoin_fingertip.py +52 -0
- seamless_workflow-0.2.0/tests/test_celljoin_remote_integration.py +180 -0
- seamless_workflow-0.2.0/tests/test_cells_contract_alignment.py +725 -0
- seamless_workflow-0.2.0/tests/test_cells_projection_writes.py +134 -0
- seamless_workflow-0.2.0/tests/test_cells_transformer_boundary.py +57 -0
- seamless_workflow-0.2.0/tests/test_cells_wiring_contract.py +368 -0
- seamless_workflow-0.2.0/tests/test_celltype_changes.py +107 -0
- seamless_workflow-0.2.0/tests/test_compiled_celltype_workflow.py +142 -0
- seamless_workflow-0.2.0/tests/test_completion.py +86 -0
- seamless_workflow-0.2.0/tests/test_context_basics.py +101 -0
- seamless_workflow-0.2.0/tests/test_contract_attachments.py +1091 -0
- seamless_workflow-0.2.0/tests/test_contract_bash_environment.py +84 -0
- seamless_workflow-0.2.0/tests/test_contract_bound_fusion.py +142 -0
- seamless_workflow-0.2.0/tests/test_contract_cancellation_policy.py +121 -0
- seamless_workflow-0.2.0/tests/test_contract_celljoin_bound.py +241 -0
- seamless_workflow-0.2.0/tests/test_contract_cells_bound.py +618 -0
- seamless_workflow-0.2.0/tests/test_contract_cells_handles.py +845 -0
- seamless_workflow-0.2.0/tests/test_contract_compiled_pins_workflow.py +397 -0
- seamless_workflow-0.2.0/tests/test_contract_deep_celltypes_bound.py +162 -0
- seamless_workflow-0.2.0/tests/test_contract_deep_handles_bound.py +549 -0
- seamless_workflow-0.2.0/tests/test_contract_deep_wiring_bound.py +234 -0
- seamless_workflow-0.2.0/tests/test_contract_direct_expression_binding.py +110 -0
- seamless_workflow-0.2.0/tests/test_contract_handle_celltypes.py +450 -0
- seamless_workflow-0.2.0/tests/test_contract_handle_reads.py +336 -0
- seamless_workflow-0.2.0/tests/test_contract_input_side_reverse_publish.py +81 -0
- seamless_workflow-0.2.0/tests/test_contract_linger_vs_revert_hold.py +34 -0
- seamless_workflow-0.2.0/tests/test_contract_mounts.py +1294 -0
- seamless_workflow-0.2.0/tests/test_contract_node_state_lifecycle.py +997 -0
- seamless_workflow-0.2.0/tests/test_contract_pins_bound.py +555 -0
- seamless_workflow-0.2.0/tests/test_contract_process_lock.py +196 -0
- seamless_workflow-0.2.0/tests/test_contract_reference_lifecycle_anonymous.py +217 -0
- seamless_workflow-0.2.0/tests/test_contract_reference_lifecycle_scratch.py +196 -0
- seamless_workflow-0.2.0/tests/test_contract_scratch_transformer_remote.py +65 -0
- seamless_workflow-0.2.0/tests/test_contract_shares.py +1637 -0
- seamless_workflow-0.2.0/tests/test_contract_transformer_bound.py +221 -0
- seamless_workflow-0.2.0/tests/test_contract_upstream_hold.py +204 -0
- seamless_workflow-0.2.0/tests/test_contract_workflow_context.py +889 -0
- seamless_workflow-0.2.0/tests/test_controller.py +189 -0
- seamless_workflow-0.2.0/tests/test_controller_lifecycle.py +180 -0
- seamless_workflow-0.2.0/tests/test_dependencies_and_pins.py +80 -0
- seamless_workflow-0.2.0/tests/test_direct_print_removal.py +41 -0
- seamless_workflow-0.2.0/tests/test_execution_error_types.py +134 -0
- seamless_workflow-0.2.0/tests/test_expression_execution.py +189 -0
- seamless_workflow-0.2.0/tests/test_followup_audit.py +24 -0
- seamless_workflow-0.2.0/tests/test_followup_contract.py +13 -0
- seamless_workflow-0.2.0/tests/test_garbage_collection.py +130 -0
- seamless_workflow-0.2.0/tests/test_language_handlers_bound.py +92 -0
- seamless_workflow-0.2.0/tests/test_late_expression_materialization.py +64 -0
- seamless_workflow-0.2.0/tests/test_literal_retention.py +65 -0
- seamless_workflow-0.2.0/tests/test_materialisation.py +22 -0
- seamless_workflow-0.2.0/tests/test_mount_buffer_source_integration.py +182 -0
- seamless_workflow-0.2.0/tests/test_mount_null.py +173 -0
- seamless_workflow-0.2.0/tests/test_mount_transport.py +113 -0
- seamless_workflow-0.2.0/tests/test_mounts.py +613 -0
- seamless_workflow-0.2.0/tests/test_optional_pin_contract.py +111 -0
- seamless_workflow-0.2.0/tests/test_pin_celltype_changes.py +76 -0
- seamless_workflow-0.2.0/tests/test_pin_handles.py +235 -0
- seamless_workflow-0.2.0/tests/test_pin_scratch_contract.py +257 -0
- seamless_workflow-0.2.0/tests/test_public_workflow_api.py +22 -0
- seamless_workflow-0.2.0/tests/test_python_backend_bound.py +93 -0
- seamless_workflow-0.2.0/tests/test_reactive_engine.py +116 -0
- seamless_workflow-0.2.0/tests/test_reference_lifecycle_binding.py +300 -0
- seamless_workflow-0.2.0/tests/test_reference_lifecycle_forced_expiry.py +287 -0
- seamless_workflow-0.2.0/tests/test_review_expression_chains.py +168 -0
- seamless_workflow-0.2.0/tests/test_runtime_and_prune.py +71 -0
- seamless_workflow-0.2.0/tests/test_serve_graph.py +151 -0
- seamless_workflow-0.2.0/tests/test_share_browser.py +294 -0
- seamless_workflow-0.2.0/tests/test_share_client_js.py +577 -0
- seamless_workflow-0.2.0/tests/test_share_client_restart.py +230 -0
- seamless_workflow-0.2.0/tests/test_share_close_hold.py +234 -0
- seamless_workflow-0.2.0/tests/test_share_downloads.py +404 -0
- seamless_workflow-0.2.0/tests/test_share_state_graph.py +243 -0
- seamless_workflow-0.2.0/tests/test_share_transport.py +704 -0
- seamless_workflow-0.2.0/tests/test_spawned_traceback.py +42 -0
- seamless_workflow-0.2.0/tests/test_state_machine.py +124 -0
- seamless_workflow-0.2.0/tests/test_transformer_binding.py +80 -0
- seamless_workflow-0.2.0/tests/test_transformer_block_reason.py +153 -0
- seamless_workflow-0.2.0/tests/test_transformer_logs_bound.py +85 -0
- seamless_workflow-0.2.0/tests/test_transformer_streaming_bound.py +37 -0
- seamless_workflow-0.2.0/tests/test_undeserializable_expression_result.py +42 -0
- 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,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."""
|