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.
@@ -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
@@ -0,0 +1,5 @@
1
+ """Scheduling utilities."""
2
+
3
+ from .scheduler import Scheduler
4
+
5
+ __all__ = ["Scheduler"]