graphk 1.0.2__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.
- graphk/__init__.py +69 -0
- graphk/common/logger.py +82 -0
- graphk/common/matcher.py +342 -0
- graphk/common/persistence.py +83 -0
- graphk/common/store.py +659 -0
- graphk/core/context.py +54 -0
- graphk/core/emitter.py +116 -0
- graphk/core/node.py +207 -0
- graphk/core/pipeline.py +298 -0
- graphk/core/policy.py +85 -0
- graphk/core/runner.py +590 -0
- graphk/core/session.py +257 -0
- graphk/pipes/branch.py +208 -0
- graphk/pipes/demos.py +186 -0
- graphk/pipes/multi.py +105 -0
- graphk/pipes/sequence.py +35 -0
- graphk-1.0.2.dist-info/METADATA +220 -0
- graphk-1.0.2.dist-info/RECORD +20 -0
- graphk-1.0.2.dist-info/WHEEL +4 -0
- graphk-1.0.2.dist-info/licenses/LICENSE.txt +21 -0
graphk/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphK - Framework for Graph-based execution and pipeline programming.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .common.store import Store
|
|
6
|
+
from .common.matcher import Matcher
|
|
7
|
+
from .common.logger import Logger
|
|
8
|
+
from .common.persistence import Persistence
|
|
9
|
+
from .core.session import Session
|
|
10
|
+
from .core.context import Context
|
|
11
|
+
from .core.policy import Policy
|
|
12
|
+
from .core.node import Node
|
|
13
|
+
from .core.pipeline import Pipeline
|
|
14
|
+
from .core.runner import Runner
|
|
15
|
+
from .core.emitter import Emitter
|
|
16
|
+
from .pipes.branch import BranchPipe
|
|
17
|
+
from .pipes.demos import (
|
|
18
|
+
AccumulateNode,
|
|
19
|
+
ApplyNode,
|
|
20
|
+
ConcatNode,
|
|
21
|
+
EchoNode,
|
|
22
|
+
IncrementNode,
|
|
23
|
+
RouteNode,
|
|
24
|
+
TextConcatNode,
|
|
25
|
+
)
|
|
26
|
+
from .pipes.multi import MultiPipe
|
|
27
|
+
from .pipes.sequence import SequencePipe
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Store",
|
|
31
|
+
"Matcher",
|
|
32
|
+
"Logger",
|
|
33
|
+
"Session",
|
|
34
|
+
"Context",
|
|
35
|
+
"Policy",
|
|
36
|
+
"Node",
|
|
37
|
+
"Pipeline",
|
|
38
|
+
"Runner",
|
|
39
|
+
"Emitter",
|
|
40
|
+
"Persistence",
|
|
41
|
+
"EchoNode",
|
|
42
|
+
"ApplyNode",
|
|
43
|
+
"AccumulateNode",
|
|
44
|
+
"IncrementNode",
|
|
45
|
+
"ConcatNode",
|
|
46
|
+
"TextConcatNode",
|
|
47
|
+
"RouteNode",
|
|
48
|
+
"BranchPipe",
|
|
49
|
+
"MultiPipe",
|
|
50
|
+
"SequencePipe"
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
__title__ = "graphk"
|
|
54
|
+
__version__ = "1.0.2"
|
|
55
|
+
__description__ = "Framework for Graph-based execution and pipeline programming"
|
|
56
|
+
__github__ = "https://github.com/kochf1/graphk"
|
|
57
|
+
__license__ = "MIT"
|
|
58
|
+
__about__ = """
|
|
59
|
+
GraphK provides the core execution model for graph-based programming. It defines
|
|
60
|
+
pipelines as executable graphs composed of nodes, branches, and nested pipelines,
|
|
61
|
+
with explicit control flow and runtime semantics.
|
|
62
|
+
"""
|
|
63
|
+
__disclaimer__= """
|
|
64
|
+
This codebase was developed with the assistance of agentic AI tools as part of a
|
|
65
|
+
vibe coding workflow. Multiple AI models and development tools were used while
|
|
66
|
+
following established code style guidelines and testing practices. Certain components
|
|
67
|
+
required manual intervention, and human-in-the-loop review was applied throughout
|
|
68
|
+
the development process to ensure correctness, consistency, and code quality.
|
|
69
|
+
"""
|
graphk/common/logger.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
##
|
|
2
|
+
## GraphK - Framework for Graph-based execution and pipeline programming.
|
|
3
|
+
## common/logger.py — Shared logging configuration and levels.
|
|
4
|
+
##
|
|
5
|
+
# Copyright (c) 2026, Dr. Fernando Koch
|
|
6
|
+
# http://github.com/kochf1/graphk
|
|
7
|
+
#
|
|
8
|
+
# Check 'About', 'License' and 'Disclaimer' at BASE_DIR/__init__.py
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Logger:
|
|
17
|
+
"""Central logging contract for GraphK components."""
|
|
18
|
+
|
|
19
|
+
LOGGER_INSTANCE = logging.getLogger("graphk")
|
|
20
|
+
NAME = "graphk"
|
|
21
|
+
|
|
22
|
+
SILENT = logging.CRITICAL + 10
|
|
23
|
+
ERROR = logging.ERROR
|
|
24
|
+
BASIC = logging.INFO
|
|
25
|
+
DETAIL = 15
|
|
26
|
+
DEBUG = logging.DEBUG
|
|
27
|
+
|
|
28
|
+
INSTANCE = LOGGER_INSTANCE
|
|
29
|
+
LOG_SILENT = SILENT
|
|
30
|
+
LOG_ERROR = ERROR
|
|
31
|
+
LOG_BASIC = BASIC
|
|
32
|
+
LOG_DETAIL = DETAIL
|
|
33
|
+
LOG_DEBUG = DEBUG
|
|
34
|
+
|
|
35
|
+
logging.addLevelName(DETAIL, "DETAIL")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def enable(cls, level: int = BASIC) -> bool:
|
|
39
|
+
"""Enable package logging at the requested level."""
|
|
40
|
+
valid_levels = {cls.SILENT, cls.ERROR, cls.BASIC, cls.DETAIL, cls.DEBUG}
|
|
41
|
+
|
|
42
|
+
if level not in valid_levels:
|
|
43
|
+
raise ValueError(f"invalid log level: {level}")
|
|
44
|
+
|
|
45
|
+
if len(cls.LOGGER_INSTANCE.handlers) == 0:
|
|
46
|
+
handler = logging.StreamHandler(stream=sys.stdout)
|
|
47
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
48
|
+
cls.LOGGER_INSTANCE.addHandler(handler)
|
|
49
|
+
cls.LOGGER_INSTANCE.propagate = False
|
|
50
|
+
|
|
51
|
+
cls.LOGGER_INSTANCE.setLevel(level)
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def format_event(
|
|
56
|
+
cls,
|
|
57
|
+
index: int,
|
|
58
|
+
level: int,
|
|
59
|
+
component: str,
|
|
60
|
+
action: str,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Format one structured package log line."""
|
|
64
|
+
parts = [f"action={action}"]
|
|
65
|
+
for key, value in kwargs.items():
|
|
66
|
+
parts.append(f"{key}={value!r}")
|
|
67
|
+
|
|
68
|
+
fields = "; ".join(parts)
|
|
69
|
+
return f"{index:03d} [{logging.getLevelName(level)}] {component}; {fields}"
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def log(
|
|
73
|
+
cls,
|
|
74
|
+
index: int,
|
|
75
|
+
level: int,
|
|
76
|
+
component: str,
|
|
77
|
+
action: str,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Format and emit one structured package log line."""
|
|
81
|
+
cls.LOGGER_INSTANCE.log(level, cls.format_event(index, level, component, action, **kwargs))
|
|
82
|
+
return
|
graphk/common/matcher.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
##
|
|
2
|
+
## GraphK - Framework for Graph-based execution and pipeline programming.
|
|
3
|
+
## common/matcher.py — Hierarchical Store for Set of Tests.
|
|
4
|
+
##
|
|
5
|
+
# Copyright (c) 2025, Dr. Fernando Koch
|
|
6
|
+
# http://github.com/kochf1/graphk
|
|
7
|
+
#
|
|
8
|
+
# Check 'About', 'License' and 'Disclaimer' at BASE_DIR/__init__.py
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
from .store import Store
|
|
14
|
+
|
|
15
|
+
class Matcher(Store):
|
|
16
|
+
"""Matcher evaluates rules against a store."""
|
|
17
|
+
|
|
18
|
+
# Rule Matching strategy
|
|
19
|
+
MATCH_ALL: int = 1
|
|
20
|
+
MATCH_ANY: int = 2
|
|
21
|
+
|
|
22
|
+
# Policy resolver strategy
|
|
23
|
+
"""
|
|
24
|
+
RESOLVER_BOTTOMUP - Rules are resolved from the current matcher upward through the hierarchy.
|
|
25
|
+
Local policies may specialize or override higher-level policies while still inheriting them.
|
|
26
|
+
|
|
27
|
+
RESOLVER_TOPDOWN - Rules are resolved from the root matcher downward through the hierarchy.
|
|
28
|
+
Global policies take precedence and cannot be overridden by lower policy layers.
|
|
29
|
+
|
|
30
|
+
RESOLVER_ROOT - Only the root matcher rules are resolved.
|
|
31
|
+
The top-level policy acts as the supreme authority and all lower policy layers are ignored.
|
|
32
|
+
|
|
33
|
+
RESOLVER_BOTTOM - Only the current matcher rules are resolved.
|
|
34
|
+
The local context defines policy independently without inheriting higher-level policies.
|
|
35
|
+
|
|
36
|
+
RESOLVER_FLAT_BOTTOMUP - The matcher hierarchy is flattened using bottom-up precedence before resolving.
|
|
37
|
+
The effective policy is derived by allowing lower policy layers to override higher ones.
|
|
38
|
+
|
|
39
|
+
RESOLVER_FLAT_TOPDOWN - The matcher hierarchy is flattened using top-down precedence before resolving.
|
|
40
|
+
The effective policy is derived by enforcing root-level policies as authoritative baselines.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
RESOLVER_BOTTOMUP = 1
|
|
44
|
+
RESOLVER_TOPDOWN = 2
|
|
45
|
+
RESOLVER_ROOT = 3
|
|
46
|
+
RESOLVER_BOTTOM = 4
|
|
47
|
+
RESOLVER_FLAT_BOTTOMUP = 5
|
|
48
|
+
RESOLVER_FLAT_TOPDOWN = 6
|
|
49
|
+
|
|
50
|
+
# ===================================================================
|
|
51
|
+
# Initialization
|
|
52
|
+
# ===================================================================
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
eval_strategy: int = MATCH_ALL,
|
|
57
|
+
resolve_strategy: int = RESOLVER_FLAT_BOTTOMUP,
|
|
58
|
+
case_sensitive: bool = False,
|
|
59
|
+
scope: Optional[str] = None,
|
|
60
|
+
parent: Optional[Store] = None,
|
|
61
|
+
**kwargs
|
|
62
|
+
) -> None:
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
Initializes a Matcher with matching strategy and rules.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
m = Matcher(role="admin", age=">=18")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
self._eval: int = eval_strategy
|
|
72
|
+
self._resolver: int = resolve_strategy
|
|
73
|
+
self._sensitive: bool = case_sensitive
|
|
74
|
+
|
|
75
|
+
Store.__init__(self, scope=scope, parent=parent, **kwargs)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# ===================================================================
|
|
79
|
+
# Private Methods
|
|
80
|
+
# ===================================================================
|
|
81
|
+
|
|
82
|
+
OPERATORS = (">=", "<=", "==", "!=", ">", "<")
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _operator(a: float, b: float, op: str) -> bool:
|
|
86
|
+
"""Apply a numeric comparison operator to two values."""
|
|
87
|
+
"""Execute a numeric comparison operator."""
|
|
88
|
+
|
|
89
|
+
# fail-fast guards
|
|
90
|
+
|
|
91
|
+
# deliberation
|
|
92
|
+
|
|
93
|
+
res: bool = False
|
|
94
|
+
|
|
95
|
+
if op == ">":
|
|
96
|
+
res = a > b
|
|
97
|
+
elif op == "<":
|
|
98
|
+
res = a < b
|
|
99
|
+
elif op == ">=":
|
|
100
|
+
res = a >= b
|
|
101
|
+
elif op == "<=":
|
|
102
|
+
res = a <= b
|
|
103
|
+
elif op == "==":
|
|
104
|
+
res = a == b
|
|
105
|
+
elif op == "!=":
|
|
106
|
+
res = a != b
|
|
107
|
+
|
|
108
|
+
return res
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _match_exp(case: Any, expression: Any, case_sensitive: bool = False) -> bool:
|
|
112
|
+
"""Evaluate one case value against one matcher expression."""
|
|
113
|
+
"""
|
|
114
|
+
Evaluate a field value against an expression.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# fail-fast guards
|
|
118
|
+
if case is None or expression is None:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# deliberation
|
|
122
|
+
res: bool = False
|
|
123
|
+
|
|
124
|
+
if callable(expression):
|
|
125
|
+
value = expression(case)
|
|
126
|
+
if isinstance(value, bool):
|
|
127
|
+
res = value
|
|
128
|
+
|
|
129
|
+
elif isinstance(expression, (int, float)):
|
|
130
|
+
try:
|
|
131
|
+
res = float(case) == float(expression)
|
|
132
|
+
except (TypeError, ValueError):
|
|
133
|
+
res = False
|
|
134
|
+
|
|
135
|
+
elif not isinstance(expression, str):
|
|
136
|
+
res = case == expression
|
|
137
|
+
|
|
138
|
+
else:
|
|
139
|
+
case_str = str(case)
|
|
140
|
+
exp = expression
|
|
141
|
+
|
|
142
|
+
if not case_sensitive:
|
|
143
|
+
case_str = case_str.lower()
|
|
144
|
+
exp = exp.lower()
|
|
145
|
+
|
|
146
|
+
if exp.startswith("r/") and exp.endswith("/"):
|
|
147
|
+
pattern = exp[2:-1]
|
|
148
|
+
try:
|
|
149
|
+
res = re.search(pattern, case_str) is not None
|
|
150
|
+
except re.error:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
elif any(exp.startswith(op) for op in Matcher.OPERATORS):
|
|
154
|
+
for op in Matcher.OPERATORS:
|
|
155
|
+
if exp.startswith(op):
|
|
156
|
+
value = exp[len(op):]
|
|
157
|
+
try:
|
|
158
|
+
case_val = float(case)
|
|
159
|
+
exp_val = float(value)
|
|
160
|
+
res = Matcher._operator(case_val, exp_val, op)
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
res = False
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
elif "*" in exp:
|
|
166
|
+
pattern = "^" + re.escape(exp).replace(r"\*", ".*") + "$"
|
|
167
|
+
try:
|
|
168
|
+
compiled = re.compile(pattern)
|
|
169
|
+
res = compiled.match(case_str) is not None
|
|
170
|
+
except re.error:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
else:
|
|
174
|
+
res = case_str == exp
|
|
175
|
+
|
|
176
|
+
return res
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _match_lambdas(funcs: list[Any], state: Store, eval_strategy: int) -> bool:
|
|
181
|
+
"""Evaluate global predicate functions against the given state."""
|
|
182
|
+
"""Evaluate one or more global lambda predicates."""
|
|
183
|
+
|
|
184
|
+
# deliberation
|
|
185
|
+
|
|
186
|
+
res: bool = (eval_strategy == Matcher.MATCH_ALL)
|
|
187
|
+
|
|
188
|
+
for fn in funcs:
|
|
189
|
+
local = False
|
|
190
|
+
|
|
191
|
+
if callable(fn):
|
|
192
|
+
value = fn(state)
|
|
193
|
+
if isinstance(value, bool):
|
|
194
|
+
local = value
|
|
195
|
+
|
|
196
|
+
if eval_strategy == Matcher.MATCH_ALL and not local:
|
|
197
|
+
res = False
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if eval_strategy == Matcher.MATCH_ANY and local:
|
|
201
|
+
res = True
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
return res
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _resolve(matcher: "Matcher", resolve_strategy: int) -> Optional[list[Any]]:
|
|
208
|
+
"""Resolve matcher layers according to the selected strategy."""
|
|
209
|
+
"""
|
|
210
|
+
Resolves rule sources according to the matching resolve strategy.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
root = Matcher(role="admin", age=">=18")
|
|
214
|
+
pipeline = Matcher(parent=root, role="user")
|
|
215
|
+
session = Matcher(parent=pipeline, region="US")
|
|
216
|
+
|
|
217
|
+
ctx = Store(role="user", age=21, region="US")
|
|
218
|
+
|
|
219
|
+
# bottom-up layered evaluation
|
|
220
|
+
layers = Matcher._resolve(session, Matcher.RESOLVER_BOTTOMUP)
|
|
221
|
+
# -> [session, pipeline, root]
|
|
222
|
+
|
|
223
|
+
# top-down layered evaluation
|
|
224
|
+
layers = Matcher._resolve(session, Matcher.RESOLVER_TOPDOWN)
|
|
225
|
+
# -> [root, pipeline, session]
|
|
226
|
+
|
|
227
|
+
# flattened rule resolution
|
|
228
|
+
rules = Matcher._resolve(session, Matcher.RESOLVER_FLAT_BOTTOMUP)
|
|
229
|
+
# -> [{'role': 'user', 'age': '>=18', 'region': 'US'}]
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
# fail-fast guards
|
|
233
|
+
|
|
234
|
+
# deliberation
|
|
235
|
+
|
|
236
|
+
res: Optional[list[Any]] = None
|
|
237
|
+
if resolve_strategy == Matcher.RESOLVER_FLAT_BOTTOMUP:
|
|
238
|
+
res = [matcher.flatten()]
|
|
239
|
+
|
|
240
|
+
elif resolve_strategy == Matcher.RESOLVER_FLAT_TOPDOWN:
|
|
241
|
+
res = [matcher.flatten(top_down=True)]
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
chain = matcher._chain()
|
|
245
|
+
|
|
246
|
+
if resolve_strategy == Matcher.RESOLVER_BOTTOMUP:
|
|
247
|
+
res = chain
|
|
248
|
+
|
|
249
|
+
elif resolve_strategy == Matcher.RESOLVER_TOPDOWN:
|
|
250
|
+
res = list(reversed(chain))
|
|
251
|
+
|
|
252
|
+
elif resolve_strategy == Matcher.RESOLVER_ROOT:
|
|
253
|
+
res = [chain[-1]]
|
|
254
|
+
|
|
255
|
+
elif resolve_strategy == Matcher.RESOLVER_BOTTOM:
|
|
256
|
+
res = [chain[0]]
|
|
257
|
+
|
|
258
|
+
return res
|
|
259
|
+
|
|
260
|
+
# ===================================================================
|
|
261
|
+
# Public Interface
|
|
262
|
+
# ===================================================================
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def to_dict(self) -> dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Return the effective rule set.
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
matcher = Matcher(role="admin")
|
|
271
|
+
matcher.to_dict()
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
# deliberation
|
|
275
|
+
|
|
276
|
+
layers = Matcher._resolve(self, self._resolver)
|
|
277
|
+
|
|
278
|
+
if isinstance(layers[0], dict):
|
|
279
|
+
res = dict(layers[0])
|
|
280
|
+
else:
|
|
281
|
+
merged: dict[str, Any] = {}
|
|
282
|
+
|
|
283
|
+
for layer in layers:
|
|
284
|
+
merged.update(dict(layer))
|
|
285
|
+
|
|
286
|
+
res = merged
|
|
287
|
+
|
|
288
|
+
return res
|
|
289
|
+
|
|
290
|
+
def match(self, state: Store) -> bool:
|
|
291
|
+
"""
|
|
292
|
+
Match the current rules against a store.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
matcher = Matcher(role="admin")
|
|
296
|
+
state = Store(role="admin")
|
|
297
|
+
matcher.match(state)
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
# fail-fast guards
|
|
301
|
+
|
|
302
|
+
# deliberation
|
|
303
|
+
|
|
304
|
+
res: bool = (self._eval == Matcher.MATCH_ALL)
|
|
305
|
+
layers = Matcher._resolve(self, self._resolver)
|
|
306
|
+
done: bool = False
|
|
307
|
+
|
|
308
|
+
for layer in layers:
|
|
309
|
+
items = layer.items()
|
|
310
|
+
|
|
311
|
+
for key, expression in items:
|
|
312
|
+
|
|
313
|
+
if key == "_":
|
|
314
|
+
funcs = expression if isinstance(expression, list) else [expression]
|
|
315
|
+
valid = all(callable(fn) for fn in funcs)
|
|
316
|
+
local: bool = False
|
|
317
|
+
|
|
318
|
+
if valid:
|
|
319
|
+
local = Matcher._match_lambdas(funcs, state, self._eval)
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
case = state.get(key, None)
|
|
323
|
+
local = Matcher._match_exp(
|
|
324
|
+
case=case,
|
|
325
|
+
expression=expression,
|
|
326
|
+
case_sensitive=self._sensitive,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if self._eval == Matcher.MATCH_ALL and not local:
|
|
330
|
+
res = False
|
|
331
|
+
done = True
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
if self._eval == Matcher.MATCH_ANY and local:
|
|
335
|
+
res = True
|
|
336
|
+
done = True
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
if done:
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
return res
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
##
|
|
2
|
+
## GraphK - Framework for Graph-based execution and pipeline programming.
|
|
3
|
+
## common/persistence.py — File persistence helpers for Session objects.
|
|
4
|
+
##
|
|
5
|
+
# Copyright (c) 2026, Dr. Fernando Koch
|
|
6
|
+
# http://github.com/kochf1/graphk
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from ..core.session import Session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Persistence:
|
|
18
|
+
"""Save and load Session snapshots from disk."""
|
|
19
|
+
|
|
20
|
+
DEFAULT_PREFIX = "session"
|
|
21
|
+
DEFAULT_ID_KEY = "id"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _ensure_id(session: Session, id_key: str = DEFAULT_ID_KEY) -> str:
|
|
25
|
+
"""Resolve or create a stable session id for file naming."""
|
|
26
|
+
session_id = session.get(id_key)
|
|
27
|
+
|
|
28
|
+
if session_id is None:
|
|
29
|
+
session_id = uuid4().hex
|
|
30
|
+
session.set(id_key, session_id)
|
|
31
|
+
|
|
32
|
+
return str(session_id)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _payload(session: Session) -> Dict[str, Any]:
|
|
36
|
+
"""Build a JSON-safe snapshot payload for one session."""
|
|
37
|
+
return {
|
|
38
|
+
"session": session.save(),
|
|
39
|
+
"code": session.code,
|
|
40
|
+
"message": session.message,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _resolve_path(session: Session, target: str | Path) -> Path:
|
|
45
|
+
"""Resolve the final save path from a filename or folder target."""
|
|
46
|
+
path = Path(target)
|
|
47
|
+
|
|
48
|
+
if path.exists() and path.is_dir():
|
|
49
|
+
session_id = Persistence._ensure_id(session)
|
|
50
|
+
return path / f"{Persistence.DEFAULT_PREFIX}-{session_id}.json"
|
|
51
|
+
|
|
52
|
+
if path.suffix == "":
|
|
53
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
session_id = Persistence._ensure_id(session)
|
|
55
|
+
return path / f"{Persistence.DEFAULT_PREFIX}-{session_id}.json"
|
|
56
|
+
|
|
57
|
+
parent = path.parent
|
|
58
|
+
if str(parent) not in ("", "."):
|
|
59
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def save(session: Session, target: str | Path) -> str:
|
|
65
|
+
"""Save a session snapshot to a file path or folder."""
|
|
66
|
+
if not isinstance(session, Session):
|
|
67
|
+
raise TypeError("session must be a Session")
|
|
68
|
+
|
|
69
|
+
path = Persistence._resolve_path(session, target)
|
|
70
|
+
payload = Persistence._payload(session)
|
|
71
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
|
72
|
+
return str(path)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def load(source: str | Path) -> Session:
|
|
76
|
+
"""Load a session snapshot from disk."""
|
|
77
|
+
path = Path(source)
|
|
78
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
|
|
80
|
+
session = Session()
|
|
81
|
+
session.restore(payload["session"])
|
|
82
|
+
session._restore_status(payload.get("code"), payload.get("message"))
|
|
83
|
+
return session
|