loopgraph 0.2.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.
- loopgraph/__init__.py +38 -0
- loopgraph/_debug.py +45 -0
- loopgraph/bus/__init__.py +5 -0
- loopgraph/bus/eventbus.py +186 -0
- loopgraph/concurrency/__init__.py +5 -0
- loopgraph/concurrency/policies.py +181 -0
- loopgraph/core/__init__.py +18 -0
- loopgraph/core/graph.py +425 -0
- loopgraph/core/state.py +443 -0
- loopgraph/core/types.py +72 -0
- loopgraph/diagnostics/__init__.py +5 -0
- loopgraph/diagnostics/inspect.py +70 -0
- loopgraph/persistence/__init__.py +6 -0
- loopgraph/persistence/event_log.py +63 -0
- loopgraph/persistence/snapshot.py +52 -0
- loopgraph/py.typed +0 -0
- loopgraph/registry/__init__.py +1 -0
- loopgraph/registry/function_registry.py +117 -0
- loopgraph/scheduler/__init__.py +5 -0
- loopgraph/scheduler/scheduler.py +569 -0
- loopgraph-0.2.0.dist-info/METADATA +165 -0
- loopgraph-0.2.0.dist-info/RECORD +24 -0
- loopgraph-0.2.0.dist-info/WHEEL +5 -0
- loopgraph-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Snapshot persistence interfaces with in-memory reference implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Mapping, Protocol
|
|
7
|
+
|
|
8
|
+
from .._debug import log_branch, log_parameter, log_variable_change
|
|
9
|
+
|
|
10
|
+
SnapshotPayload = Mapping[str, object]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SnapshotStore(Protocol):
|
|
14
|
+
"""Protocol defining snapshot persistence operations.
|
|
15
|
+
|
|
16
|
+
>>> store = InMemorySnapshotStore()
|
|
17
|
+
>>> store.save("graph-1", {"state": 1})
|
|
18
|
+
>>> store.load("graph-1")["state"]
|
|
19
|
+
1
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def save(self, graph_id: str, snapshot: SnapshotPayload) -> None:
|
|
23
|
+
"""Persist a snapshot for a graph."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def load(self, graph_id: str) -> SnapshotPayload:
|
|
27
|
+
"""Load the latest snapshot for a graph."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class InMemorySnapshotStore(SnapshotStore):
|
|
33
|
+
"""Keep snapshots in memory for testing and examples."""
|
|
34
|
+
|
|
35
|
+
_snapshots: Dict[str, SnapshotPayload] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
def save(self, graph_id: str, snapshot: SnapshotPayload) -> None:
|
|
38
|
+
func_name = "InMemorySnapshotStore.save"
|
|
39
|
+
log_parameter(func_name, graph_id=graph_id, snapshot=snapshot)
|
|
40
|
+
self._snapshots[graph_id] = snapshot
|
|
41
|
+
log_variable_change(func_name, f"self._snapshots[{graph_id!r}]", snapshot)
|
|
42
|
+
|
|
43
|
+
def load(self, graph_id: str) -> SnapshotPayload:
|
|
44
|
+
func_name = "InMemorySnapshotStore.load"
|
|
45
|
+
log_parameter(func_name, graph_id=graph_id)
|
|
46
|
+
if graph_id not in self._snapshots:
|
|
47
|
+
log_branch(func_name, "missing_snapshot")
|
|
48
|
+
raise KeyError(f"Snapshot for graph '{graph_id}' not found")
|
|
49
|
+
log_branch(func_name, "snapshot_found")
|
|
50
|
+
snapshot = self._snapshots[graph_id]
|
|
51
|
+
log_variable_change(func_name, "snapshot", snapshot)
|
|
52
|
+
return snapshot
|
loopgraph/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Function registry primitives."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Function registry for node handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Any, Awaitable, Callable, Dict, TypeGuard
|
|
7
|
+
|
|
8
|
+
from .._debug import log_branch, log_parameter, log_variable_change
|
|
9
|
+
|
|
10
|
+
Handler = Callable[..., Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_awaitable(value: object) -> TypeGuard[Awaitable[Any]]:
|
|
14
|
+
"""Type guard helping to determine whether a value can be awaited.
|
|
15
|
+
|
|
16
|
+
>>> _is_awaitable(1)
|
|
17
|
+
False
|
|
18
|
+
>>> class DummyAwaitable:
|
|
19
|
+
... def __await__(self):
|
|
20
|
+
... yield
|
|
21
|
+
... return None
|
|
22
|
+
>>> _is_awaitable(DummyAwaitable())
|
|
23
|
+
True
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
func_name = "_is_awaitable"
|
|
27
|
+
log_parameter(func_name, value=value)
|
|
28
|
+
result = inspect.isawaitable(value)
|
|
29
|
+
log_variable_change(func_name, "result", result)
|
|
30
|
+
return bool(result)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _resolve_result(value: object) -> Any:
|
|
34
|
+
"""Normalize handler results into awaited values.
|
|
35
|
+
|
|
36
|
+
>>> import asyncio
|
|
37
|
+
>>> asyncio.run(_resolve_result(3))
|
|
38
|
+
3
|
|
39
|
+
>>> async def sample() -> str:
|
|
40
|
+
... return "ok"
|
|
41
|
+
>>> asyncio.run(_resolve_result(sample()))
|
|
42
|
+
'ok'
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
func_name = "_resolve_result"
|
|
46
|
+
log_parameter(func_name, value=value)
|
|
47
|
+
if _is_awaitable(value):
|
|
48
|
+
log_branch(func_name, "awaitable")
|
|
49
|
+
awaited = await value
|
|
50
|
+
log_variable_change(func_name, "awaited", awaited)
|
|
51
|
+
return awaited
|
|
52
|
+
log_branch(func_name, "immediate")
|
|
53
|
+
log_variable_change(func_name, "value", value)
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FunctionRegistry:
|
|
58
|
+
"""Manage handler functions keyed by name.
|
|
59
|
+
|
|
60
|
+
>>> import asyncio
|
|
61
|
+
>>> registry = FunctionRegistry()
|
|
62
|
+
>>> async def handler(value: int) -> int:
|
|
63
|
+
... return value + 1
|
|
64
|
+
>>> registry.register("inc", handler)
|
|
65
|
+
>>> asyncio.run(registry.execute("inc", 1))
|
|
66
|
+
2
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
func_name = "FunctionRegistry.__init__"
|
|
71
|
+
log_parameter(func_name)
|
|
72
|
+
self._handlers: Dict[str, Handler] = {}
|
|
73
|
+
log_variable_change(func_name, "self._handlers", self._handlers)
|
|
74
|
+
|
|
75
|
+
def register(self, name: str, handler: Handler) -> None:
|
|
76
|
+
"""Register a handler by name.
|
|
77
|
+
|
|
78
|
+
>>> registry = FunctionRegistry()
|
|
79
|
+
>>> registry.register("noop", lambda: None)
|
|
80
|
+
"""
|
|
81
|
+
func_name = "FunctionRegistry.register"
|
|
82
|
+
log_parameter(func_name, name=name, handler=handler)
|
|
83
|
+
self._handlers[name] = handler
|
|
84
|
+
log_variable_change(
|
|
85
|
+
func_name, f"self._handlers[{name!r}]", self._handlers[name]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get(self, name: str) -> Handler:
|
|
89
|
+
"""Retrieve a registered handler."""
|
|
90
|
+
func_name = "FunctionRegistry.get"
|
|
91
|
+
log_parameter(func_name, name=name)
|
|
92
|
+
if name not in self._handlers:
|
|
93
|
+
log_branch(func_name, "missing_handler")
|
|
94
|
+
raise KeyError(f"Handler '{name}' is not registered")
|
|
95
|
+
log_branch(func_name, "handler_found")
|
|
96
|
+
handler = self._handlers[name]
|
|
97
|
+
log_variable_change(func_name, "handler", handler)
|
|
98
|
+
return handler
|
|
99
|
+
|
|
100
|
+
async def execute(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
101
|
+
"""Execute a registered handler, awaiting async functions.
|
|
102
|
+
|
|
103
|
+
>>> import asyncio
|
|
104
|
+
>>> registry = FunctionRegistry()
|
|
105
|
+
>>> registry.register("add", lambda a, b: a + b)
|
|
106
|
+
>>> asyncio.run(registry.execute("add", 1, 2))
|
|
107
|
+
3
|
|
108
|
+
"""
|
|
109
|
+
func_name = "FunctionRegistry.execute"
|
|
110
|
+
log_parameter(func_name, name=name, args=args, kwargs=kwargs)
|
|
111
|
+
handler = self.get(name)
|
|
112
|
+
log_variable_change(func_name, "handler", handler)
|
|
113
|
+
result = handler(*args, **kwargs)
|
|
114
|
+
log_variable_change(func_name, "result", result)
|
|
115
|
+
resolved = await _resolve_result(result)
|
|
116
|
+
log_variable_change(func_name, "resolved", resolved)
|
|
117
|
+
return resolved
|