graphrefly 0.1.0__py3-none-any.whl
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.
- graphrefly/__init__.py +160 -0
- graphrefly/compat/__init__.py +18 -0
- graphrefly/compat/async_utils.py +228 -0
- graphrefly/compat/asyncio_runner.py +89 -0
- graphrefly/compat/trio_runner.py +81 -0
- graphrefly/core/__init__.py +142 -0
- graphrefly/core/clock.py +20 -0
- graphrefly/core/dynamic_node.py +749 -0
- graphrefly/core/guard.py +277 -0
- graphrefly/core/meta.py +149 -0
- graphrefly/core/node.py +963 -0
- graphrefly/core/protocol.py +460 -0
- graphrefly/core/runner.py +107 -0
- graphrefly/core/subgraph_locks.py +296 -0
- graphrefly/core/sugar.py +138 -0
- graphrefly/core/versioning.py +193 -0
- graphrefly/extra/__init__.py +313 -0
- graphrefly/extra/adapters.py +2149 -0
- graphrefly/extra/backoff.py +287 -0
- graphrefly/extra/backpressure.py +113 -0
- graphrefly/extra/checkpoint.py +307 -0
- graphrefly/extra/composite.py +303 -0
- graphrefly/extra/cron.py +133 -0
- graphrefly/extra/data_structures.py +707 -0
- graphrefly/extra/resilience.py +727 -0
- graphrefly/extra/sources.py +766 -0
- graphrefly/extra/tier1.py +1067 -0
- graphrefly/extra/tier2.py +1802 -0
- graphrefly/graph/__init__.py +31 -0
- graphrefly/graph/graph.py +2249 -0
- graphrefly/integrations/__init__.py +1 -0
- graphrefly/integrations/fastapi.py +767 -0
- graphrefly/patterns/__init__.py +5 -0
- graphrefly/patterns/ai.py +2132 -0
- graphrefly/patterns/cqrs.py +515 -0
- graphrefly/patterns/memory.py +639 -0
- graphrefly/patterns/messaging.py +553 -0
- graphrefly/patterns/orchestration.py +536 -0
- graphrefly/patterns/reactive_layout/__init__.py +81 -0
- graphrefly/patterns/reactive_layout/measurement_adapters.py +276 -0
- graphrefly/patterns/reactive_layout/reactive_block_layout.py +434 -0
- graphrefly/patterns/reactive_layout/reactive_layout.py +943 -0
- graphrefly/py.typed +1 -0
- graphrefly-0.1.0.dist-info/METADATA +253 -0
- graphrefly-0.1.0.dist-info/RECORD +47 -0
- graphrefly-0.1.0.dist-info/WHEEL +4 -0
- graphrefly-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Orchestration patterns (roadmap §4.1).
|
|
2
|
+
|
|
3
|
+
Domain-layer helpers that build workflow shapes on top of core + extra primitives.
|
|
4
|
+
Export under ``graphrefly.patterns.orchestration`` to avoid collisions with Phase 2
|
|
5
|
+
operator names like ``gate`` and ``for_each``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
14
|
+
|
|
15
|
+
from graphrefly.core.node import Node, NodeActions, node
|
|
16
|
+
from graphrefly.core.protocol import MessageType
|
|
17
|
+
from graphrefly.graph.graph import GRAPH_META_SEGMENT, PATH_SEP, Graph
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
type StepRef = str | Node[Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BranchResult(TypedDict):
|
|
26
|
+
branch: str
|
|
27
|
+
value: Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class SensorControls[T]:
|
|
32
|
+
"""Result bundle for :func:`sensor`."""
|
|
33
|
+
|
|
34
|
+
node: Node[T]
|
|
35
|
+
push: Callable[[T], None]
|
|
36
|
+
error: Callable[[BaseException | Any], None]
|
|
37
|
+
complete: Callable[[], None]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve_dep(graph: Graph, dep: StepRef) -> tuple[Node[Any], str | None]:
|
|
41
|
+
if isinstance(dep, str):
|
|
42
|
+
return graph.resolve(dep), dep
|
|
43
|
+
path = _find_registered_node_path(graph, dep)
|
|
44
|
+
if path is None:
|
|
45
|
+
msg = (
|
|
46
|
+
"orchestration dep node must already be registered in the graph so "
|
|
47
|
+
"explicit edges can be recorded; pass a string path or register the node first"
|
|
48
|
+
)
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
return dep, path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_registered_node_path(graph: Graph, target: Node[Any]) -> str | None:
|
|
54
|
+
described = graph.describe()
|
|
55
|
+
meta_segment = f"{PATH_SEP}{GRAPH_META_SEGMENT}{PATH_SEP}"
|
|
56
|
+
for path in sorted(str(k) for k in described["nodes"]):
|
|
57
|
+
if meta_segment in path:
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
if graph.resolve(path) is target:
|
|
61
|
+
return path
|
|
62
|
+
except KeyError:
|
|
63
|
+
continue
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _register_step(
|
|
68
|
+
graph: Graph,
|
|
69
|
+
step_name: str,
|
|
70
|
+
step: Node[Any],
|
|
71
|
+
dep_paths: list[str],
|
|
72
|
+
) -> None:
|
|
73
|
+
graph.add(step_name, step)
|
|
74
|
+
for dep_path in dep_paths:
|
|
75
|
+
graph.connect(dep_path, step_name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _base_meta(kind: str, meta: dict[str, Any] | None) -> dict[str, Any]:
|
|
79
|
+
out: dict[str, Any] = {"orchestration": True, "orchestration_type": kind}
|
|
80
|
+
if meta:
|
|
81
|
+
out.update(meta)
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _coerce_loop_iterations(raw: Any) -> int:
|
|
86
|
+
parsed: float
|
|
87
|
+
if isinstance(raw, str):
|
|
88
|
+
trimmed = raw.strip()
|
|
89
|
+
if len(trimmed) == 0:
|
|
90
|
+
parsed = 0.0
|
|
91
|
+
else:
|
|
92
|
+
try:
|
|
93
|
+
parsed = float(trimmed)
|
|
94
|
+
except ValueError:
|
|
95
|
+
parsed = float("nan")
|
|
96
|
+
elif raw is None:
|
|
97
|
+
parsed = 0.0
|
|
98
|
+
else:
|
|
99
|
+
try:
|
|
100
|
+
parsed = float(raw)
|
|
101
|
+
except (TypeError, ValueError):
|
|
102
|
+
parsed = float("nan")
|
|
103
|
+
if not math.isfinite(parsed):
|
|
104
|
+
return 1
|
|
105
|
+
count = int(parsed)
|
|
106
|
+
if count < 0:
|
|
107
|
+
return 0
|
|
108
|
+
return count
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def pipeline(name: str, *, opts: dict[str, Any] | None = None) -> Graph:
|
|
112
|
+
"""Create an orchestration graph container."""
|
|
113
|
+
return Graph(name, opts)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def task(
|
|
117
|
+
graph: Graph,
|
|
118
|
+
name: str,
|
|
119
|
+
run: Any,
|
|
120
|
+
*,
|
|
121
|
+
deps: list[StepRef] | None = None,
|
|
122
|
+
meta: dict[str, Any] | None = None,
|
|
123
|
+
**node_opts: Any,
|
|
124
|
+
) -> Node[Any]:
|
|
125
|
+
"""Register a workflow task node."""
|
|
126
|
+
resolved = [_resolve_dep(graph, dep) for dep in (deps or [])]
|
|
127
|
+
dep_nodes = [n for n, _ in resolved]
|
|
128
|
+
dep_paths = [p for _n, p in resolved if p is not None]
|
|
129
|
+
step = node(
|
|
130
|
+
dep_nodes,
|
|
131
|
+
run,
|
|
132
|
+
name=name,
|
|
133
|
+
describe_kind="derived",
|
|
134
|
+
meta=_base_meta("task", meta),
|
|
135
|
+
**node_opts,
|
|
136
|
+
)
|
|
137
|
+
_register_step(graph, name, step, dep_paths)
|
|
138
|
+
return step
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def branch(
|
|
142
|
+
graph: Graph,
|
|
143
|
+
name: str,
|
|
144
|
+
source: StepRef,
|
|
145
|
+
predicate: Any,
|
|
146
|
+
*,
|
|
147
|
+
meta: dict[str, Any] | None = None,
|
|
148
|
+
**node_opts: Any,
|
|
149
|
+
) -> Node[BranchResult]:
|
|
150
|
+
"""Register a branch step that tags each value as ``then`` or ``else``."""
|
|
151
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
152
|
+
|
|
153
|
+
def compute(deps: list[Any], _actions: NodeActions) -> BranchResult:
|
|
154
|
+
value = deps[0]
|
|
155
|
+
return {
|
|
156
|
+
"branch": "then" if bool(predicate(value)) else "else",
|
|
157
|
+
"value": value,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
step = node(
|
|
161
|
+
[source_node],
|
|
162
|
+
compute,
|
|
163
|
+
name=name,
|
|
164
|
+
describe_kind="derived",
|
|
165
|
+
meta=_base_meta("branch", meta),
|
|
166
|
+
**node_opts,
|
|
167
|
+
)
|
|
168
|
+
_register_step(graph, name, step, [source_path] if source_path else [])
|
|
169
|
+
return step
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def gate(
|
|
173
|
+
graph: Graph,
|
|
174
|
+
name: str,
|
|
175
|
+
source: StepRef,
|
|
176
|
+
control: StepRef,
|
|
177
|
+
*,
|
|
178
|
+
meta: dict[str, Any] | None = None,
|
|
179
|
+
**node_opts: Any,
|
|
180
|
+
) -> Node[Any]:
|
|
181
|
+
"""Register a value-level gate step controlled by a boolean signal."""
|
|
182
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
183
|
+
control_node, control_path = _resolve_dep(graph, control)
|
|
184
|
+
|
|
185
|
+
def compute(_deps: list[Any], actions: NodeActions) -> Any:
|
|
186
|
+
if not bool(control_node.get()):
|
|
187
|
+
actions.down([(MessageType.RESOLVED,)])
|
|
188
|
+
return None
|
|
189
|
+
return source_node.get()
|
|
190
|
+
|
|
191
|
+
step = node(
|
|
192
|
+
[source_node, control_node],
|
|
193
|
+
compute,
|
|
194
|
+
name=name,
|
|
195
|
+
describe_kind="operator",
|
|
196
|
+
meta=_base_meta("gate", meta),
|
|
197
|
+
**node_opts,
|
|
198
|
+
)
|
|
199
|
+
paths = [p for p in (source_path, control_path) if p is not None]
|
|
200
|
+
_register_step(graph, name, step, paths)
|
|
201
|
+
return step
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def approval(
|
|
205
|
+
graph: Graph,
|
|
206
|
+
name: str,
|
|
207
|
+
source: StepRef,
|
|
208
|
+
approver: StepRef,
|
|
209
|
+
*,
|
|
210
|
+
is_approved: Any | None = None,
|
|
211
|
+
meta: dict[str, Any] | None = None,
|
|
212
|
+
**node_opts: Any,
|
|
213
|
+
) -> Node[Any]:
|
|
214
|
+
"""Register an approval gate (human/LLM/system signal controls value flow)."""
|
|
215
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
216
|
+
approver_node, approver_path = _resolve_dep(graph, approver)
|
|
217
|
+
approved_fn = is_approved if is_approved is not None else (lambda value: bool(value))
|
|
218
|
+
|
|
219
|
+
def compute(_deps: list[Any], actions: NodeActions) -> Any:
|
|
220
|
+
if not bool(approved_fn(approver_node.get())):
|
|
221
|
+
actions.down([(MessageType.RESOLVED,)])
|
|
222
|
+
return None
|
|
223
|
+
return source_node.get()
|
|
224
|
+
|
|
225
|
+
step = node(
|
|
226
|
+
[source_node, approver_node],
|
|
227
|
+
compute,
|
|
228
|
+
name=name,
|
|
229
|
+
describe_kind="operator",
|
|
230
|
+
meta=_base_meta("approval", meta),
|
|
231
|
+
**node_opts,
|
|
232
|
+
)
|
|
233
|
+
paths = [p for p in (source_path, approver_path) if p is not None]
|
|
234
|
+
_register_step(graph, name, step, paths)
|
|
235
|
+
return step
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def for_each(
|
|
239
|
+
graph: Graph,
|
|
240
|
+
name: str,
|
|
241
|
+
source: StepRef,
|
|
242
|
+
run: Callable[[Any, NodeActions], None],
|
|
243
|
+
*,
|
|
244
|
+
meta: dict[str, Any] | None = None,
|
|
245
|
+
**node_opts: Any,
|
|
246
|
+
) -> Node[Any]:
|
|
247
|
+
"""Register a workflow side-effect step that remains graph-observable."""
|
|
248
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
249
|
+
terminated = [False]
|
|
250
|
+
|
|
251
|
+
def on_message(msg: Any, index: int, actions: NodeActions) -> bool:
|
|
252
|
+
if terminated[0]:
|
|
253
|
+
return True
|
|
254
|
+
if index != 0:
|
|
255
|
+
actions.down([msg])
|
|
256
|
+
if msg[0] is MessageType.COMPLETE or msg[0] is MessageType.ERROR:
|
|
257
|
+
terminated[0] = True
|
|
258
|
+
return True
|
|
259
|
+
if msg[0] is MessageType.DATA:
|
|
260
|
+
try:
|
|
261
|
+
run(msg[1], actions)
|
|
262
|
+
actions.down([msg])
|
|
263
|
+
except BaseException as err:
|
|
264
|
+
terminated[0] = True
|
|
265
|
+
actions.down([(MessageType.ERROR, err)])
|
|
266
|
+
return True
|
|
267
|
+
actions.down([msg])
|
|
268
|
+
if msg[0] is MessageType.COMPLETE or msg[0] is MessageType.ERROR:
|
|
269
|
+
terminated[0] = True
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
step = node(
|
|
273
|
+
[source_node],
|
|
274
|
+
lambda _deps, _actions: None,
|
|
275
|
+
name=name,
|
|
276
|
+
describe_kind="effect",
|
|
277
|
+
complete_when_deps_complete=False,
|
|
278
|
+
on_message=on_message,
|
|
279
|
+
meta=_base_meta("for_each", meta),
|
|
280
|
+
**node_opts,
|
|
281
|
+
)
|
|
282
|
+
_register_step(graph, name, step, [source_path] if source_path else [])
|
|
283
|
+
return step
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def join(
|
|
287
|
+
graph: Graph,
|
|
288
|
+
name: str,
|
|
289
|
+
deps: list[StepRef],
|
|
290
|
+
*,
|
|
291
|
+
meta: dict[str, Any] | None = None,
|
|
292
|
+
**node_opts: Any,
|
|
293
|
+
) -> Node[Any]:
|
|
294
|
+
"""Register a join step that emits the latest tuple of dependency values."""
|
|
295
|
+
resolved = [_resolve_dep(graph, dep) for dep in deps]
|
|
296
|
+
dep_nodes = [n for n, _ in resolved]
|
|
297
|
+
dep_paths = [p for _n, p in resolved if p is not None]
|
|
298
|
+
step = node(
|
|
299
|
+
dep_nodes,
|
|
300
|
+
lambda values, _a: tuple(values),
|
|
301
|
+
name=name,
|
|
302
|
+
describe_kind="derived",
|
|
303
|
+
meta=_base_meta("join", meta),
|
|
304
|
+
**node_opts,
|
|
305
|
+
)
|
|
306
|
+
_register_step(graph, name, step, dep_paths)
|
|
307
|
+
return step
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def loop(
|
|
311
|
+
graph: Graph,
|
|
312
|
+
name: str,
|
|
313
|
+
source: StepRef,
|
|
314
|
+
iterate: Callable[[Any, int, NodeActions], Any],
|
|
315
|
+
*,
|
|
316
|
+
iterations: int | StepRef = 1,
|
|
317
|
+
meta: dict[str, Any] | None = None,
|
|
318
|
+
**node_opts: Any,
|
|
319
|
+
) -> Node[Any]:
|
|
320
|
+
"""Register a loop step that applies ``iterate`` N times per source value."""
|
|
321
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
322
|
+
iter_node: Node[Any] | None = None
|
|
323
|
+
iter_path: str | None = None
|
|
324
|
+
if isinstance(iterations, int):
|
|
325
|
+
static_iterations = iterations
|
|
326
|
+
else:
|
|
327
|
+
static_iterations = None
|
|
328
|
+
iter_node, iter_path = _resolve_dep(graph, iterations)
|
|
329
|
+
|
|
330
|
+
def compute(_deps: list[Any], actions: NodeActions) -> Any:
|
|
331
|
+
value = source_node.get()
|
|
332
|
+
raw: Any = (
|
|
333
|
+
static_iterations
|
|
334
|
+
if static_iterations is not None
|
|
335
|
+
else (iter_node.get() if iter_node is not None else 1)
|
|
336
|
+
)
|
|
337
|
+
count = _coerce_loop_iterations(raw)
|
|
338
|
+
for i in range(count):
|
|
339
|
+
value = iterate(value, i, actions)
|
|
340
|
+
return value
|
|
341
|
+
|
|
342
|
+
deps_list: list[Node[Any]] = [source_node] + ([iter_node] if iter_node is not None else [])
|
|
343
|
+
step = node(
|
|
344
|
+
deps_list,
|
|
345
|
+
compute,
|
|
346
|
+
name=name,
|
|
347
|
+
describe_kind="derived",
|
|
348
|
+
meta=_base_meta("loop", meta),
|
|
349
|
+
**node_opts,
|
|
350
|
+
)
|
|
351
|
+
_register_step(graph, name, step, [p for p in (source_path, iter_path) if p is not None])
|
|
352
|
+
return step
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def sub_pipeline(
|
|
356
|
+
graph: Graph,
|
|
357
|
+
name: str,
|
|
358
|
+
child_or_build: Graph | Callable[[Graph], None] | None = None,
|
|
359
|
+
*,
|
|
360
|
+
opts: dict[str, Any] | None = None,
|
|
361
|
+
) -> Graph:
|
|
362
|
+
"""Mount and return a child workflow graph."""
|
|
363
|
+
child = child_or_build if isinstance(child_or_build, Graph) else pipeline(name, opts=opts)
|
|
364
|
+
if callable(child_or_build):
|
|
365
|
+
child_or_build(child)
|
|
366
|
+
graph.mount(name, child)
|
|
367
|
+
return child
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def sensor(
|
|
371
|
+
graph: Graph,
|
|
372
|
+
name: str,
|
|
373
|
+
*,
|
|
374
|
+
initial: Any | None = None,
|
|
375
|
+
meta: dict[str, Any] | None = None,
|
|
376
|
+
**node_opts: Any,
|
|
377
|
+
) -> SensorControls[Any]:
|
|
378
|
+
"""Register a producer-like sensor source and return imperative controls."""
|
|
379
|
+
src = node(
|
|
380
|
+
[],
|
|
381
|
+
lambda _deps, _actions: None,
|
|
382
|
+
name=name,
|
|
383
|
+
initial=initial,
|
|
384
|
+
describe_kind="producer",
|
|
385
|
+
meta=_base_meta("sensor", meta),
|
|
386
|
+
**node_opts,
|
|
387
|
+
)
|
|
388
|
+
_register_step(graph, name, src, [])
|
|
389
|
+
return SensorControls(
|
|
390
|
+
node=src,
|
|
391
|
+
push=lambda value: src.down([(MessageType.DATA, value)]),
|
|
392
|
+
error=lambda err: src.down([(MessageType.ERROR, err)]),
|
|
393
|
+
complete=lambda: src.down([(MessageType.COMPLETE,)]),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def wait(
|
|
398
|
+
graph: Graph,
|
|
399
|
+
name: str,
|
|
400
|
+
source: StepRef,
|
|
401
|
+
seconds: float,
|
|
402
|
+
*,
|
|
403
|
+
meta: dict[str, Any] | None = None,
|
|
404
|
+
**node_opts: Any,
|
|
405
|
+
) -> Node[Any]:
|
|
406
|
+
"""Register a delayed-forwarding step (value-level wait)."""
|
|
407
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
408
|
+
timers: set[threading.Timer] = set()
|
|
409
|
+
lock = threading.Lock()
|
|
410
|
+
terminated = [False]
|
|
411
|
+
completed = [False]
|
|
412
|
+
|
|
413
|
+
def clear_all() -> None:
|
|
414
|
+
with lock:
|
|
415
|
+
current = list(timers)
|
|
416
|
+
timers.clear()
|
|
417
|
+
terminated[0] = True
|
|
418
|
+
for timer in current:
|
|
419
|
+
timer.cancel()
|
|
420
|
+
|
|
421
|
+
def on_message(msg: Any, index: int, actions: NodeActions) -> bool:
|
|
422
|
+
if terminated[0]:
|
|
423
|
+
return True
|
|
424
|
+
if index != 0:
|
|
425
|
+
actions.down([msg])
|
|
426
|
+
if msg[0] is MessageType.COMPLETE or msg[0] is MessageType.ERROR:
|
|
427
|
+
terminated[0] = True
|
|
428
|
+
return True
|
|
429
|
+
if msg[0] is MessageType.DATA:
|
|
430
|
+
|
|
431
|
+
def fire() -> None:
|
|
432
|
+
actions.down([msg])
|
|
433
|
+
should_complete = False
|
|
434
|
+
with lock:
|
|
435
|
+
timers.discard(timer)
|
|
436
|
+
should_complete = completed[0] and len(timers) == 0
|
|
437
|
+
if should_complete:
|
|
438
|
+
actions.down([(MessageType.COMPLETE,)])
|
|
439
|
+
|
|
440
|
+
timer = threading.Timer(seconds, fire)
|
|
441
|
+
with lock:
|
|
442
|
+
timers.add(timer)
|
|
443
|
+
timer.start()
|
|
444
|
+
return True
|
|
445
|
+
if msg[0] is MessageType.COMPLETE:
|
|
446
|
+
with lock:
|
|
447
|
+
terminated[0] = True
|
|
448
|
+
completed[0] = True
|
|
449
|
+
done_now = len(timers) == 0
|
|
450
|
+
if done_now:
|
|
451
|
+
actions.down([(MessageType.COMPLETE,)])
|
|
452
|
+
return True
|
|
453
|
+
if msg[0] is MessageType.ERROR:
|
|
454
|
+
clear_all()
|
|
455
|
+
actions.down([msg])
|
|
456
|
+
return True
|
|
457
|
+
actions.down([msg])
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
def compute(_deps: list[Any], _actions: NodeActions) -> Any:
|
|
461
|
+
return clear_all
|
|
462
|
+
|
|
463
|
+
step = node(
|
|
464
|
+
[source_node],
|
|
465
|
+
compute,
|
|
466
|
+
name=name,
|
|
467
|
+
initial=source_node.get(),
|
|
468
|
+
describe_kind="operator",
|
|
469
|
+
complete_when_deps_complete=False,
|
|
470
|
+
on_message=on_message,
|
|
471
|
+
meta=_base_meta("wait", meta),
|
|
472
|
+
**node_opts,
|
|
473
|
+
)
|
|
474
|
+
_register_step(graph, name, step, [source_path] if source_path else [])
|
|
475
|
+
return step
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def on_failure(
|
|
479
|
+
graph: Graph,
|
|
480
|
+
name: str,
|
|
481
|
+
source: StepRef,
|
|
482
|
+
recover: Callable[[BaseException | Any, NodeActions], Any],
|
|
483
|
+
*,
|
|
484
|
+
meta: dict[str, Any] | None = None,
|
|
485
|
+
**node_opts: Any,
|
|
486
|
+
) -> Node[Any]:
|
|
487
|
+
"""Register an error-recovery step for a source."""
|
|
488
|
+
source_node, source_path = _resolve_dep(graph, source)
|
|
489
|
+
terminated = [False]
|
|
490
|
+
|
|
491
|
+
def on_message(msg: Any, _index: int, actions: NodeActions) -> bool:
|
|
492
|
+
if terminated[0]:
|
|
493
|
+
return True
|
|
494
|
+
if msg[0] is MessageType.ERROR:
|
|
495
|
+
try:
|
|
496
|
+
actions.emit(recover(msg[1], actions))
|
|
497
|
+
except BaseException as err:
|
|
498
|
+
terminated[0] = True
|
|
499
|
+
actions.down([(MessageType.ERROR, err)])
|
|
500
|
+
return True
|
|
501
|
+
actions.down([msg])
|
|
502
|
+
if msg[0] is MessageType.COMPLETE:
|
|
503
|
+
terminated[0] = True
|
|
504
|
+
return True
|
|
505
|
+
|
|
506
|
+
step = node(
|
|
507
|
+
[source_node],
|
|
508
|
+
lambda _deps, _actions: None,
|
|
509
|
+
name=name,
|
|
510
|
+
describe_kind="operator",
|
|
511
|
+
complete_when_deps_complete=False,
|
|
512
|
+
on_message=on_message,
|
|
513
|
+
meta=_base_meta("on_failure", meta),
|
|
514
|
+
**node_opts,
|
|
515
|
+
)
|
|
516
|
+
_register_step(graph, name, step, [source_path] if source_path else [])
|
|
517
|
+
return step
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
__all__ = [
|
|
521
|
+
"BranchResult",
|
|
522
|
+
"SensorControls",
|
|
523
|
+
"StepRef",
|
|
524
|
+
"approval",
|
|
525
|
+
"branch",
|
|
526
|
+
"for_each",
|
|
527
|
+
"gate",
|
|
528
|
+
"join",
|
|
529
|
+
"loop",
|
|
530
|
+
"on_failure",
|
|
531
|
+
"pipeline",
|
|
532
|
+
"sensor",
|
|
533
|
+
"sub_pipeline",
|
|
534
|
+
"task",
|
|
535
|
+
"wait",
|
|
536
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Reactive layout pattern — standalone importable module.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from graphrefly.patterns.reactive_layout import reactive_layout, CliMeasureAdapter
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from graphrefly.patterns.reactive_layout.measurement_adapters import (
|
|
9
|
+
CliMeasureAdapter,
|
|
10
|
+
ImageSizeAdapter,
|
|
11
|
+
PillowMeasureAdapter,
|
|
12
|
+
PrecomputedAdapter,
|
|
13
|
+
SvgBoundsAdapter,
|
|
14
|
+
)
|
|
15
|
+
from graphrefly.patterns.reactive_layout.reactive_block_layout import (
|
|
16
|
+
BlockAdapters,
|
|
17
|
+
ContentBlock,
|
|
18
|
+
ImageBlock,
|
|
19
|
+
ImageMeasurer,
|
|
20
|
+
MeasuredBlock,
|
|
21
|
+
PositionedBlock,
|
|
22
|
+
ReactiveBlockLayoutBundle,
|
|
23
|
+
SvgBlock,
|
|
24
|
+
SvgMeasurer,
|
|
25
|
+
TextBlock,
|
|
26
|
+
compute_block_flow,
|
|
27
|
+
compute_total_height,
|
|
28
|
+
measure_block,
|
|
29
|
+
measure_blocks,
|
|
30
|
+
reactive_block_layout,
|
|
31
|
+
)
|
|
32
|
+
from graphrefly.patterns.reactive_layout.reactive_layout import (
|
|
33
|
+
CharPosition,
|
|
34
|
+
LayoutLine,
|
|
35
|
+
LineBreaksResult,
|
|
36
|
+
MeasurementAdapter,
|
|
37
|
+
PreparedSegment,
|
|
38
|
+
ReactiveLayoutBundle,
|
|
39
|
+
SegmentBreakKind,
|
|
40
|
+
analyze_and_measure,
|
|
41
|
+
compute_char_positions,
|
|
42
|
+
compute_line_breaks,
|
|
43
|
+
reactive_layout,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# reactive_layout
|
|
48
|
+
"CharPosition",
|
|
49
|
+
"LayoutLine",
|
|
50
|
+
"LineBreaksResult",
|
|
51
|
+
"MeasurementAdapter",
|
|
52
|
+
"PreparedSegment",
|
|
53
|
+
"ReactiveLayoutBundle",
|
|
54
|
+
"SegmentBreakKind",
|
|
55
|
+
"analyze_and_measure",
|
|
56
|
+
"compute_char_positions",
|
|
57
|
+
"compute_line_breaks",
|
|
58
|
+
"reactive_layout",
|
|
59
|
+
# reactive_block_layout
|
|
60
|
+
"BlockAdapters",
|
|
61
|
+
"ContentBlock",
|
|
62
|
+
"ImageBlock",
|
|
63
|
+
"ImageMeasurer",
|
|
64
|
+
"MeasuredBlock",
|
|
65
|
+
"PositionedBlock",
|
|
66
|
+
"ReactiveBlockLayoutBundle",
|
|
67
|
+
"SvgBlock",
|
|
68
|
+
"SvgMeasurer",
|
|
69
|
+
"TextBlock",
|
|
70
|
+
"compute_block_flow",
|
|
71
|
+
"compute_total_height",
|
|
72
|
+
"measure_block",
|
|
73
|
+
"measure_blocks",
|
|
74
|
+
"reactive_block_layout",
|
|
75
|
+
# measurement_adapters
|
|
76
|
+
"CliMeasureAdapter",
|
|
77
|
+
"ImageSizeAdapter",
|
|
78
|
+
"PillowMeasureAdapter",
|
|
79
|
+
"PrecomputedAdapter",
|
|
80
|
+
"SvgBoundsAdapter",
|
|
81
|
+
]
|