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
graphrefly/__init__.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""graphrefly — Reactive graph protocol for human and LLM co-operation."""
|
|
2
|
+
|
|
3
|
+
from graphrefly import compat, integrations, patterns
|
|
4
|
+
from graphrefly.core import (
|
|
5
|
+
V0,
|
|
6
|
+
V1,
|
|
7
|
+
Actor,
|
|
8
|
+
DeferWhen,
|
|
9
|
+
DynamicNodeImpl,
|
|
10
|
+
EmitStrategy,
|
|
11
|
+
GuardAction,
|
|
12
|
+
GuardDenied,
|
|
13
|
+
GuardFn,
|
|
14
|
+
HashFn,
|
|
15
|
+
Message,
|
|
16
|
+
Messages,
|
|
17
|
+
MessageType,
|
|
18
|
+
Node,
|
|
19
|
+
NodeActions,
|
|
20
|
+
NodeFn,
|
|
21
|
+
NodeImpl,
|
|
22
|
+
NodeStatus,
|
|
23
|
+
NodeVersionInfo,
|
|
24
|
+
PipeOperator,
|
|
25
|
+
Runner,
|
|
26
|
+
SubscribeHints,
|
|
27
|
+
VersioningLevel,
|
|
28
|
+
access_hint_for_guard,
|
|
29
|
+
acquire_subgraph_write_lock,
|
|
30
|
+
acquire_subgraph_write_lock_with_defer,
|
|
31
|
+
advance_version,
|
|
32
|
+
batch,
|
|
33
|
+
compose_guards,
|
|
34
|
+
create_versioning,
|
|
35
|
+
default_hash,
|
|
36
|
+
defer_down,
|
|
37
|
+
defer_set,
|
|
38
|
+
derived,
|
|
39
|
+
describe_node,
|
|
40
|
+
dispatch_messages,
|
|
41
|
+
dynamic_node,
|
|
42
|
+
effect,
|
|
43
|
+
emit_with_batch,
|
|
44
|
+
ensure_registered,
|
|
45
|
+
get_default_runner,
|
|
46
|
+
is_batching,
|
|
47
|
+
is_phase2_message,
|
|
48
|
+
is_v1,
|
|
49
|
+
meta_snapshot,
|
|
50
|
+
monotonic_ns,
|
|
51
|
+
node,
|
|
52
|
+
normalize_actor,
|
|
53
|
+
partition_for_batch,
|
|
54
|
+
pipe,
|
|
55
|
+
policy,
|
|
56
|
+
policy_from_rules,
|
|
57
|
+
producer,
|
|
58
|
+
record_mutation,
|
|
59
|
+
resolve_runner,
|
|
60
|
+
set_default_runner,
|
|
61
|
+
state,
|
|
62
|
+
system_actor,
|
|
63
|
+
union_nodes,
|
|
64
|
+
wall_clock_ns,
|
|
65
|
+
)
|
|
66
|
+
from graphrefly.graph import (
|
|
67
|
+
GRAPH_META_SEGMENT,
|
|
68
|
+
GRAPH_SNAPSHOT_VERSION,
|
|
69
|
+
META_PATH_SEG,
|
|
70
|
+
PATH_SEP,
|
|
71
|
+
Graph,
|
|
72
|
+
GraphAutoCheckpointHandle,
|
|
73
|
+
GraphDiffResult,
|
|
74
|
+
GraphObserveSource,
|
|
75
|
+
ObserveResult,
|
|
76
|
+
SpyHandle,
|
|
77
|
+
TraceEntry,
|
|
78
|
+
reachable,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__version__ = "0.1.0"
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"GRAPH_META_SEGMENT",
|
|
85
|
+
"GRAPH_SNAPSHOT_VERSION",
|
|
86
|
+
"GraphAutoCheckpointHandle",
|
|
87
|
+
"Graph",
|
|
88
|
+
"GraphDiffResult",
|
|
89
|
+
"GraphObserveSource",
|
|
90
|
+
"HashFn",
|
|
91
|
+
"META_PATH_SEG",
|
|
92
|
+
"NodeVersionInfo",
|
|
93
|
+
"ObserveResult",
|
|
94
|
+
"PATH_SEP",
|
|
95
|
+
"SpyHandle",
|
|
96
|
+
"TraceEntry",
|
|
97
|
+
"V0",
|
|
98
|
+
"V1",
|
|
99
|
+
"VersioningLevel",
|
|
100
|
+
"reachable",
|
|
101
|
+
"compat",
|
|
102
|
+
"integrations",
|
|
103
|
+
"patterns",
|
|
104
|
+
"Runner",
|
|
105
|
+
"get_default_runner",
|
|
106
|
+
"resolve_runner",
|
|
107
|
+
"set_default_runner",
|
|
108
|
+
"Actor",
|
|
109
|
+
"DeferWhen",
|
|
110
|
+
"DynamicNodeImpl",
|
|
111
|
+
"EmitStrategy",
|
|
112
|
+
"GuardAction",
|
|
113
|
+
"GuardDenied",
|
|
114
|
+
"GuardFn",
|
|
115
|
+
"Message",
|
|
116
|
+
"MessageType",
|
|
117
|
+
"Messages",
|
|
118
|
+
"Node",
|
|
119
|
+
"PipeOperator",
|
|
120
|
+
"NodeActions",
|
|
121
|
+
"NodeFn",
|
|
122
|
+
"NodeImpl",
|
|
123
|
+
"NodeStatus",
|
|
124
|
+
"SubscribeHints",
|
|
125
|
+
"__version__",
|
|
126
|
+
"advance_version",
|
|
127
|
+
"create_versioning",
|
|
128
|
+
"default_hash",
|
|
129
|
+
"is_v1",
|
|
130
|
+
"monotonic_ns",
|
|
131
|
+
"wall_clock_ns",
|
|
132
|
+
"access_hint_for_guard",
|
|
133
|
+
"acquire_subgraph_write_lock",
|
|
134
|
+
"acquire_subgraph_write_lock_with_defer",
|
|
135
|
+
"batch",
|
|
136
|
+
"compose_guards",
|
|
137
|
+
"defer_down",
|
|
138
|
+
"defer_set",
|
|
139
|
+
"describe_node",
|
|
140
|
+
"dispatch_messages",
|
|
141
|
+
"emit_with_batch",
|
|
142
|
+
"ensure_registered",
|
|
143
|
+
"is_batching",
|
|
144
|
+
"is_phase2_message",
|
|
145
|
+
"meta_snapshot",
|
|
146
|
+
"node",
|
|
147
|
+
"normalize_actor",
|
|
148
|
+
"partition_for_batch",
|
|
149
|
+
"policy",
|
|
150
|
+
"policy_from_rules",
|
|
151
|
+
"record_mutation",
|
|
152
|
+
"system_actor",
|
|
153
|
+
"union_nodes",
|
|
154
|
+
"dynamic_node",
|
|
155
|
+
"derived",
|
|
156
|
+
"effect",
|
|
157
|
+
"pipe",
|
|
158
|
+
"producer",
|
|
159
|
+
"state",
|
|
160
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Framework compatibility layer — asyncio/trio runners and async utilities (roadmap §5.1)."""
|
|
2
|
+
|
|
3
|
+
from graphrefly.compat.async_utils import (
|
|
4
|
+
first_value_from_async,
|
|
5
|
+
settled,
|
|
6
|
+
to_async_iter,
|
|
7
|
+
)
|
|
8
|
+
from graphrefly.compat.asyncio_runner import AsyncioRunner
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AsyncioRunner",
|
|
12
|
+
"first_value_from_async",
|
|
13
|
+
"settled",
|
|
14
|
+
"to_async_iter",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# TrioRunner is available via direct import (optional trio dependency):
|
|
18
|
+
# from graphrefly.compat.trio_runner import TrioRunner
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Async utility functions for consuming GraphReFly nodes from async code (roadmap §5.1).
|
|
2
|
+
|
|
3
|
+
All utilities are **reactive** — they subscribe to nodes via
|
|
4
|
+
:meth:`~graphrefly.core.node.NodeImpl.subscribe` and bridge to the async world
|
|
5
|
+
via :class:`asyncio.Event` / :class:`asyncio.Queue`. No polling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from graphrefly.core.protocol import MessageType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
from graphrefly.core.node import Node
|
|
20
|
+
from graphrefly.core.protocol import Messages
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def to_async_iter(source: Node[Any]) -> AsyncIterator[Any]:
|
|
24
|
+
"""Yield values from *source* as an async iterator.
|
|
25
|
+
|
|
26
|
+
Subscribes reactively — each ``DATA`` message payload is yielded, and
|
|
27
|
+
each ``RESOLVED`` message yields the current value via ``source.get()``.
|
|
28
|
+
``COMPLETE`` / ``ERROR`` / ``TEARDOWN`` end the iteration.
|
|
29
|
+
Unsubscribes on ``break`` / ``aclose()``.
|
|
30
|
+
|
|
31
|
+
Must be called from a running asyncio event loop.
|
|
32
|
+
|
|
33
|
+
Note:
|
|
34
|
+
``state()`` nodes do not emit ``DATA`` on subscribe — they only emit
|
|
35
|
+
when :meth:`~graphrefly.core.node.NodeImpl.set` is called. For such
|
|
36
|
+
nodes, the iterator will block until the first update arrives.
|
|
37
|
+
Derived nodes emit ``DATA`` on subscribe when they recompute.
|
|
38
|
+
|
|
39
|
+
Example::
|
|
40
|
+
|
|
41
|
+
from graphrefly.extra import of
|
|
42
|
+
from graphrefly.compat import to_async_iter
|
|
43
|
+
|
|
44
|
+
async for value in to_async_iter(of(1, 2, 3)):
|
|
45
|
+
print(value)
|
|
46
|
+
|
|
47
|
+
Yields:
|
|
48
|
+
Each ``DATA`` payload or current value on ``RESOLVED`` from the source.
|
|
49
|
+
"""
|
|
50
|
+
loop = asyncio.get_running_loop()
|
|
51
|
+
queue: asyncio.Queue[tuple[str, Any]] = asyncio.Queue()
|
|
52
|
+
|
|
53
|
+
def _enqueue(tag: str, payload: Any) -> None:
|
|
54
|
+
with contextlib.suppress(RuntimeError):
|
|
55
|
+
loop.call_soon_threadsafe(queue.put_nowait, (tag, payload))
|
|
56
|
+
|
|
57
|
+
def sink(messages: Messages) -> None:
|
|
58
|
+
for msg in messages:
|
|
59
|
+
t = msg[0]
|
|
60
|
+
if t is MessageType.DATA:
|
|
61
|
+
_enqueue("DATA", msg[1] if len(msg) > 1 else None)
|
|
62
|
+
elif t is MessageType.RESOLVED:
|
|
63
|
+
_enqueue("DATA", source.get())
|
|
64
|
+
elif t is MessageType.ERROR:
|
|
65
|
+
err = msg[1] if len(msg) > 1 else RuntimeError("node error")
|
|
66
|
+
_enqueue("ERROR", err)
|
|
67
|
+
elif t is MessageType.COMPLETE or t is MessageType.TEARDOWN:
|
|
68
|
+
_enqueue("DONE", None)
|
|
69
|
+
|
|
70
|
+
unsub = source.subscribe(sink)
|
|
71
|
+
try:
|
|
72
|
+
while True:
|
|
73
|
+
tag, payload = await queue.get()
|
|
74
|
+
if tag == "DATA":
|
|
75
|
+
yield payload
|
|
76
|
+
elif tag == "ERROR":
|
|
77
|
+
raise payload
|
|
78
|
+
else: # DONE
|
|
79
|
+
return
|
|
80
|
+
finally:
|
|
81
|
+
unsub()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def first_value_from_async(source: Node[Any]) -> Any:
|
|
85
|
+
"""Await the first ``DATA`` value from *source*.
|
|
86
|
+
|
|
87
|
+
If the node already has a settled cached value, returns it immediately
|
|
88
|
+
without subscribing. Otherwise subscribes reactively and waits.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
RuntimeError: If the source completes without emitting ``DATA``.
|
|
92
|
+
Exception: If the source emits ``ERROR``.
|
|
93
|
+
|
|
94
|
+
Example::
|
|
95
|
+
|
|
96
|
+
from graphrefly import state
|
|
97
|
+
from graphrefly.compat import first_value_from_async
|
|
98
|
+
|
|
99
|
+
s = state(42)
|
|
100
|
+
value = await first_value_from_async(s)
|
|
101
|
+
assert value == 42
|
|
102
|
+
"""
|
|
103
|
+
# Fast path: already settled with a cached value.
|
|
104
|
+
# ``source.get()`` returns ``None`` when no value is cached, so
|
|
105
|
+
# ``is not None`` doubles as the "has value" sentinel while still
|
|
106
|
+
# returning falsy values like ``0``, ``False``, ``""``.
|
|
107
|
+
status = source.status
|
|
108
|
+
if status in ("settled", "resolved"):
|
|
109
|
+
v = source.get()
|
|
110
|
+
if v is not None:
|
|
111
|
+
return v
|
|
112
|
+
|
|
113
|
+
loop = asyncio.get_running_loop()
|
|
114
|
+
future: asyncio.Future[Any] = loop.create_future()
|
|
115
|
+
|
|
116
|
+
def _set_result(value: Any) -> None:
|
|
117
|
+
with contextlib.suppress(RuntimeError):
|
|
118
|
+
loop.call_soon_threadsafe(_resolve, future, value)
|
|
119
|
+
|
|
120
|
+
def _set_error(err: BaseException) -> None:
|
|
121
|
+
with contextlib.suppress(RuntimeError):
|
|
122
|
+
loop.call_soon_threadsafe(_reject, future, err)
|
|
123
|
+
|
|
124
|
+
def sink(messages: Messages) -> None:
|
|
125
|
+
for msg in messages:
|
|
126
|
+
t = msg[0]
|
|
127
|
+
if t is MessageType.DATA:
|
|
128
|
+
_set_result(msg[1] if len(msg) > 1 else None)
|
|
129
|
+
return
|
|
130
|
+
if t is MessageType.ERROR:
|
|
131
|
+
err = msg[1] if len(msg) > 1 else RuntimeError("node error")
|
|
132
|
+
_set_error(err)
|
|
133
|
+
return
|
|
134
|
+
if t is MessageType.COMPLETE or t is MessageType.TEARDOWN:
|
|
135
|
+
_set_error(RuntimeError("source completed without DATA"))
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
unsub = source.subscribe(sink)
|
|
139
|
+
try:
|
|
140
|
+
return await future
|
|
141
|
+
finally:
|
|
142
|
+
unsub()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def settled(source: Node[Any]) -> Any:
|
|
146
|
+
"""Await until *source* has a settled (non-dirty) value.
|
|
147
|
+
|
|
148
|
+
If the node already holds a cached value and is in a settled/resolved
|
|
149
|
+
status, returns it without waiting. Otherwise subscribes and waits for
|
|
150
|
+
the first ``DATA`` or ``RESOLVED`` message.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
RuntimeError: If the source completes or tears down without settling.
|
|
154
|
+
Exception: If the source emits ``ERROR``.
|
|
155
|
+
|
|
156
|
+
Example::
|
|
157
|
+
|
|
158
|
+
from graphrefly import derived, state
|
|
159
|
+
from graphrefly.compat import settled
|
|
160
|
+
|
|
161
|
+
a = state(10)
|
|
162
|
+
b = derived([a], lambda deps, _: deps[0] * 2)
|
|
163
|
+
value = await settled(b)
|
|
164
|
+
assert value == 20
|
|
165
|
+
"""
|
|
166
|
+
# Fast path: already settled (see ``first_value_from_async`` for the
|
|
167
|
+
# ``is not None`` rationale).
|
|
168
|
+
status = source.status
|
|
169
|
+
if status in ("settled", "resolved"):
|
|
170
|
+
v = source.get()
|
|
171
|
+
if v is not None:
|
|
172
|
+
return v
|
|
173
|
+
|
|
174
|
+
loop = asyncio.get_running_loop()
|
|
175
|
+
future: asyncio.Future[Any] = loop.create_future()
|
|
176
|
+
|
|
177
|
+
def _set_result(value: Any) -> None:
|
|
178
|
+
with contextlib.suppress(RuntimeError):
|
|
179
|
+
loop.call_soon_threadsafe(_resolve, future, value)
|
|
180
|
+
|
|
181
|
+
def _set_error(err: BaseException) -> None:
|
|
182
|
+
with contextlib.suppress(RuntimeError):
|
|
183
|
+
loop.call_soon_threadsafe(_reject, future, err)
|
|
184
|
+
|
|
185
|
+
def sink(messages: Messages) -> None:
|
|
186
|
+
for msg in messages:
|
|
187
|
+
t = msg[0]
|
|
188
|
+
if t is MessageType.DATA:
|
|
189
|
+
_set_result(msg[1] if len(msg) > 1 else None)
|
|
190
|
+
return
|
|
191
|
+
if t is MessageType.RESOLVED:
|
|
192
|
+
_set_result(source.get())
|
|
193
|
+
return
|
|
194
|
+
if t is MessageType.ERROR:
|
|
195
|
+
err = msg[1] if len(msg) > 1 else RuntimeError("node error")
|
|
196
|
+
_set_error(err)
|
|
197
|
+
return
|
|
198
|
+
if t is MessageType.COMPLETE or t is MessageType.TEARDOWN:
|
|
199
|
+
_set_error(RuntimeError("source completed without settling"))
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
unsub = source.subscribe(sink)
|
|
203
|
+
try:
|
|
204
|
+
return await future
|
|
205
|
+
finally:
|
|
206
|
+
unsub()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Helpers
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _resolve(future: asyncio.Future[Any], value: Any) -> None:
|
|
215
|
+
if not future.done():
|
|
216
|
+
future.set_result(value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _reject(future: asyncio.Future[Any], err: BaseException) -> None:
|
|
220
|
+
if not future.done():
|
|
221
|
+
future.set_exception(err)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = [
|
|
225
|
+
"first_value_from_async",
|
|
226
|
+
"settled",
|
|
227
|
+
"to_async_iter",
|
|
228
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""AsyncioRunner — schedule coroutines on an asyncio event loop (roadmap §5.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Callable, Coroutine
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncioRunner:
|
|
13
|
+
"""Runner backed by a running :mod:`asyncio` event loop.
|
|
14
|
+
|
|
15
|
+
Schedule coroutines via ``loop.create_task`` with thread-safe dispatch.
|
|
16
|
+
Use inside an ``async def`` context (e.g. FastAPI lifespan, async test).
|
|
17
|
+
|
|
18
|
+
Example::
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from graphrefly.compat import AsyncioRunner
|
|
22
|
+
from graphrefly.core.runner import set_default_runner
|
|
23
|
+
|
|
24
|
+
async def main():
|
|
25
|
+
runner = AsyncioRunner.from_running()
|
|
26
|
+
set_default_runner(runner)
|
|
27
|
+
# ... build graph, use from_awaitable, etc.
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__slots__ = ("_loop",)
|
|
33
|
+
|
|
34
|
+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
35
|
+
self._loop = loop
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_running(cls) -> AsyncioRunner:
|
|
39
|
+
"""Create from the currently running asyncio event loop.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
RuntimeError: If no event loop is running.
|
|
43
|
+
"""
|
|
44
|
+
return cls(asyncio.get_running_loop())
|
|
45
|
+
|
|
46
|
+
def schedule(
|
|
47
|
+
self,
|
|
48
|
+
coro: Coroutine[Any, Any, Any],
|
|
49
|
+
on_result: Callable[[Any], None],
|
|
50
|
+
on_error: Callable[[BaseException], None],
|
|
51
|
+
) -> Callable[[], None]:
|
|
52
|
+
task: asyncio.Task[Any] | None = None
|
|
53
|
+
cancelled = False
|
|
54
|
+
|
|
55
|
+
def _create_task() -> None:
|
|
56
|
+
nonlocal task
|
|
57
|
+
if cancelled:
|
|
58
|
+
coro.close()
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
async def _wrapper() -> None:
|
|
62
|
+
try:
|
|
63
|
+
result = await coro
|
|
64
|
+
except asyncio.CancelledError:
|
|
65
|
+
raise
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
raise
|
|
68
|
+
except SystemExit:
|
|
69
|
+
raise
|
|
70
|
+
except BaseException as err:
|
|
71
|
+
on_error(err)
|
|
72
|
+
else:
|
|
73
|
+
on_result(result)
|
|
74
|
+
|
|
75
|
+
task = self._loop.create_task(_wrapper())
|
|
76
|
+
|
|
77
|
+
# Thread-safe: schedule task creation on the event loop.
|
|
78
|
+
self._loop.call_soon_threadsafe(_create_task)
|
|
79
|
+
|
|
80
|
+
def cancel() -> None:
|
|
81
|
+
nonlocal cancelled
|
|
82
|
+
cancelled = True
|
|
83
|
+
if task is not None:
|
|
84
|
+
task.cancel()
|
|
85
|
+
|
|
86
|
+
return cancel
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["AsyncioRunner"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""TrioRunner — schedule coroutines on a trio nursery (roadmap §5.1).
|
|
2
|
+
|
|
3
|
+
Requires the ``trio`` package (optional dependency).
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
import trio
|
|
8
|
+
from graphrefly.compat.trio_runner import TrioRunner
|
|
9
|
+
from graphrefly.core.runner import set_default_runner
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
async with trio.open_nursery() as nursery:
|
|
13
|
+
runner = TrioRunner(nursery)
|
|
14
|
+
set_default_runner(runner)
|
|
15
|
+
# ... build graph, use from_awaitable, etc.
|
|
16
|
+
|
|
17
|
+
trio.run(main)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable, Coroutine
|
|
26
|
+
|
|
27
|
+
import trio # type: ignore[import-not-found]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TrioRunner:
|
|
31
|
+
"""Runner backed by a :mod:`trio` nursery.
|
|
32
|
+
|
|
33
|
+
Each scheduled coroutine runs as a trio task in the nursery.
|
|
34
|
+
Cancel scopes provide best-effort cancellation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("_nursery",)
|
|
38
|
+
|
|
39
|
+
def __init__(self, nursery: trio.Nursery) -> None:
|
|
40
|
+
self._nursery = nursery
|
|
41
|
+
|
|
42
|
+
def schedule(
|
|
43
|
+
self,
|
|
44
|
+
coro: Coroutine[Any, Any, Any],
|
|
45
|
+
on_result: Callable[[Any], None],
|
|
46
|
+
on_error: Callable[[BaseException], None],
|
|
47
|
+
) -> Callable[[], None]:
|
|
48
|
+
import trio as _trio
|
|
49
|
+
|
|
50
|
+
cancel_scope = _trio.CancelScope()
|
|
51
|
+
cancelled = False
|
|
52
|
+
|
|
53
|
+
async def _wrapper() -> None:
|
|
54
|
+
with cancel_scope:
|
|
55
|
+
if cancelled:
|
|
56
|
+
coro.close()
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
result = await coro
|
|
60
|
+
except _trio.Cancelled:
|
|
61
|
+
raise
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
raise
|
|
64
|
+
except SystemExit:
|
|
65
|
+
raise
|
|
66
|
+
except BaseException as err:
|
|
67
|
+
on_error(err)
|
|
68
|
+
else:
|
|
69
|
+
on_result(result)
|
|
70
|
+
|
|
71
|
+
self._nursery.start_soon(_wrapper)
|
|
72
|
+
|
|
73
|
+
def cancel() -> None:
|
|
74
|
+
nonlocal cancelled
|
|
75
|
+
cancelled = True
|
|
76
|
+
cancel_scope.cancel()
|
|
77
|
+
|
|
78
|
+
return cancel
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["TrioRunner"]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Core node primitives and protocol types for graphrefly."""
|
|
2
|
+
|
|
3
|
+
from graphrefly.core.clock import monotonic_ns, wall_clock_ns
|
|
4
|
+
from graphrefly.core.dynamic_node import DynamicNodeImpl, dynamic_node
|
|
5
|
+
from graphrefly.core.guard import (
|
|
6
|
+
Actor,
|
|
7
|
+
GuardAction,
|
|
8
|
+
GuardDenied,
|
|
9
|
+
GuardFn,
|
|
10
|
+
access_hint_for_guard,
|
|
11
|
+
compose_guards,
|
|
12
|
+
normalize_actor,
|
|
13
|
+
policy,
|
|
14
|
+
policy_from_rules,
|
|
15
|
+
record_mutation,
|
|
16
|
+
system_actor,
|
|
17
|
+
)
|
|
18
|
+
from graphrefly.core.meta import describe_node, meta_snapshot
|
|
19
|
+
from graphrefly.core.node import (
|
|
20
|
+
Node,
|
|
21
|
+
NodeActions,
|
|
22
|
+
NodeFn,
|
|
23
|
+
NodeImpl,
|
|
24
|
+
NodeStatus,
|
|
25
|
+
SubscribeHints,
|
|
26
|
+
node,
|
|
27
|
+
)
|
|
28
|
+
from graphrefly.core.protocol import (
|
|
29
|
+
DeferWhen,
|
|
30
|
+
EmitStrategy,
|
|
31
|
+
Message,
|
|
32
|
+
Messages,
|
|
33
|
+
MessageType,
|
|
34
|
+
batch,
|
|
35
|
+
dispatch_messages,
|
|
36
|
+
emit_with_batch,
|
|
37
|
+
is_batching,
|
|
38
|
+
is_phase2_message,
|
|
39
|
+
is_terminal_message,
|
|
40
|
+
message_tier,
|
|
41
|
+
partition_for_batch,
|
|
42
|
+
propagates_to_meta,
|
|
43
|
+
)
|
|
44
|
+
from graphrefly.core.runner import (
|
|
45
|
+
Runner,
|
|
46
|
+
get_default_runner,
|
|
47
|
+
resolve_runner,
|
|
48
|
+
set_default_runner,
|
|
49
|
+
)
|
|
50
|
+
from graphrefly.core.subgraph_locks import (
|
|
51
|
+
acquire_subgraph_write_lock,
|
|
52
|
+
acquire_subgraph_write_lock_with_defer,
|
|
53
|
+
defer_down,
|
|
54
|
+
defer_set,
|
|
55
|
+
ensure_registered,
|
|
56
|
+
union_nodes,
|
|
57
|
+
)
|
|
58
|
+
from graphrefly.core.sugar import (
|
|
59
|
+
PipeOperator,
|
|
60
|
+
derived,
|
|
61
|
+
effect,
|
|
62
|
+
pipe,
|
|
63
|
+
producer,
|
|
64
|
+
state,
|
|
65
|
+
)
|
|
66
|
+
from graphrefly.core.versioning import (
|
|
67
|
+
V0,
|
|
68
|
+
V1,
|
|
69
|
+
HashFn,
|
|
70
|
+
NodeVersionInfo,
|
|
71
|
+
VersioningLevel,
|
|
72
|
+
advance_version,
|
|
73
|
+
create_versioning,
|
|
74
|
+
default_hash,
|
|
75
|
+
is_v1,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"Actor",
|
|
80
|
+
"DynamicNodeImpl",
|
|
81
|
+
"DeferWhen",
|
|
82
|
+
"EmitStrategy",
|
|
83
|
+
"GuardAction",
|
|
84
|
+
"GuardDenied",
|
|
85
|
+
"GuardFn",
|
|
86
|
+
"Message",
|
|
87
|
+
"MessageType",
|
|
88
|
+
"Messages",
|
|
89
|
+
"Node",
|
|
90
|
+
"PipeOperator",
|
|
91
|
+
"NodeActions",
|
|
92
|
+
"NodeFn",
|
|
93
|
+
"NodeImpl",
|
|
94
|
+
"NodeStatus",
|
|
95
|
+
"SubscribeHints",
|
|
96
|
+
"monotonic_ns",
|
|
97
|
+
"wall_clock_ns",
|
|
98
|
+
"access_hint_for_guard",
|
|
99
|
+
"acquire_subgraph_write_lock",
|
|
100
|
+
"acquire_subgraph_write_lock_with_defer",
|
|
101
|
+
"batch",
|
|
102
|
+
"compose_guards",
|
|
103
|
+
"defer_down",
|
|
104
|
+
"defer_set",
|
|
105
|
+
"describe_node",
|
|
106
|
+
"dispatch_messages",
|
|
107
|
+
"emit_with_batch",
|
|
108
|
+
"ensure_registered",
|
|
109
|
+
"is_batching",
|
|
110
|
+
"is_phase2_message",
|
|
111
|
+
"is_terminal_message",
|
|
112
|
+
"message_tier",
|
|
113
|
+
"propagates_to_meta",
|
|
114
|
+
"meta_snapshot",
|
|
115
|
+
"node",
|
|
116
|
+
"normalize_actor",
|
|
117
|
+
"partition_for_batch",
|
|
118
|
+
"policy",
|
|
119
|
+
"policy_from_rules",
|
|
120
|
+
"record_mutation",
|
|
121
|
+
"system_actor",
|
|
122
|
+
"union_nodes",
|
|
123
|
+
"dynamic_node",
|
|
124
|
+
"Runner",
|
|
125
|
+
"get_default_runner",
|
|
126
|
+
"resolve_runner",
|
|
127
|
+
"set_default_runner",
|
|
128
|
+
"derived",
|
|
129
|
+
"effect",
|
|
130
|
+
"pipe",
|
|
131
|
+
"producer",
|
|
132
|
+
"state",
|
|
133
|
+
"V0",
|
|
134
|
+
"V1",
|
|
135
|
+
"NodeVersionInfo",
|
|
136
|
+
"VersioningLevel",
|
|
137
|
+
"HashFn",
|
|
138
|
+
"advance_version",
|
|
139
|
+
"create_versioning",
|
|
140
|
+
"default_hash",
|
|
141
|
+
"is_v1",
|
|
142
|
+
]
|