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 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
+ """
@@ -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
@@ -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