PyDecisionGraph 0.2.2__cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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.
- decision_graph/__init__.py +16 -0
- decision_graph/decision_tree/__init__.py +78 -0
- decision_graph/decision_tree/capi/__init__.py +57 -0
- decision_graph/decision_tree/capi/c_abc.cpython-312-x86_64-linux-gnu.so +0 -0
- decision_graph/decision_tree/capi/c_abc.pyi +539 -0
- decision_graph/decision_tree/capi/c_collection.cpython-312-x86_64-linux-gnu.so +0 -0
- decision_graph/decision_tree/capi/c_collection.pyi +245 -0
- decision_graph/decision_tree/capi/c_node.cpython-312-x86_64-linux-gnu.so +0 -0
- decision_graph/decision_tree/capi/c_node.pyi +417 -0
- decision_graph/decision_tree/exc.py +74 -0
- decision_graph/decision_tree/native/__init__.py +57 -0
- decision_graph/decision_tree/native/abc.py +1099 -0
- decision_graph/decision_tree/native/collection.py +134 -0
- decision_graph/decision_tree/native/node.py +463 -0
- decision_graph/decision_tree/webui/__init__.py +28 -0
- decision_graph/decision_tree/webui/app.py +255 -0
- decision_graph/decision_tree/webui/main.py +53 -0
- decision_graph/logic_group/__init__.py +25 -0
- decision_graph/logic_group/base.py +100 -0
- decision_graph/logic_group/pending_request.py +384 -0
- pydecisiongraph-0.2.2.dist-info/METADATA +146 -0
- pydecisiongraph-0.2.2.dist-info/RECORD +25 -0
- pydecisiongraph-0.2.2.dist-info/WHEEL +7 -0
- pydecisiongraph-0.2.2.dist-info/licenses/LICENSE +373 -0
- pydecisiongraph-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
LOGGER = logging.getLogger("DecisionGraph")
|
|
7
|
+
LOGGER.setLevel(logging.INFO)
|
|
8
|
+
|
|
9
|
+
if not LOGGER.hasHandlers():
|
|
10
|
+
ch = logging.StreamHandler(sys.stdout)
|
|
11
|
+
ch.setLevel(logging.INFO) # Set handler level
|
|
12
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
13
|
+
ch.setFormatter(formatter)
|
|
14
|
+
LOGGER.addHandler(ch)
|
|
15
|
+
|
|
16
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild("DecisionTree")
|
|
6
|
+
|
|
7
|
+
from .exc import *
|
|
8
|
+
|
|
9
|
+
USING_CAPI = False
|
|
10
|
+
try:
|
|
11
|
+
# Attempt to import the C API module
|
|
12
|
+
from . import capi
|
|
13
|
+
from .capi import *
|
|
14
|
+
from .capi import c_abc as abc
|
|
15
|
+
from .capi import c_node as node
|
|
16
|
+
from .capi import c_collection as collection
|
|
17
|
+
|
|
18
|
+
USING_CAPI = True
|
|
19
|
+
except Exception:
|
|
20
|
+
# Fallback to the python node model
|
|
21
|
+
from . import native
|
|
22
|
+
from .native import *
|
|
23
|
+
from .native import abc
|
|
24
|
+
from .native import node
|
|
25
|
+
from .native import collection
|
|
26
|
+
|
|
27
|
+
USING_CAPI = False
|
|
28
|
+
|
|
29
|
+
from .webui import DecisionTreeWebUi, show, to_html
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_logger(logger: logging.Logger):
|
|
33
|
+
global LOGGER
|
|
34
|
+
LOGGER = logger
|
|
35
|
+
|
|
36
|
+
# ensure abc module (imported above) receives logger
|
|
37
|
+
if USING_CAPI:
|
|
38
|
+
capi.set_logger(logger.getChild('CAPI'))
|
|
39
|
+
else:
|
|
40
|
+
native.set_logger(logger.getChild('Native'))
|
|
41
|
+
|
|
42
|
+
webui.set_logger(logger.getChild('WebUI'))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
'USING_CAPI',
|
|
47
|
+
|
|
48
|
+
# .exc
|
|
49
|
+
'NO_DEFAULT',
|
|
50
|
+
'EmptyBlock', 'BreakBlock',
|
|
51
|
+
'NodeError', 'TooManyChildren', 'TooFewChildren', 'NodeNotFountError', 'NodeValueError', 'NodeTypeError', 'NodeContextError',
|
|
52
|
+
'EdgeValueError',
|
|
53
|
+
'ResolutionError', 'ExpressFalse', 'ContextsNotFound',
|
|
54
|
+
|
|
55
|
+
# .capi.c_abc or .native.abc
|
|
56
|
+
'LOGGER', 'set_logger',
|
|
57
|
+
'Singleton',
|
|
58
|
+
'NodeEdgeCondition', 'ConditionElse', 'ConditionAny', 'ConditionAuto', 'BinaryCondition', 'ConditionTrue', 'ConditionFalse',
|
|
59
|
+
'NO_CONDITION', 'ELSE_CONDITION', 'AUTO_CONDITION', 'TRUE_CONDITION', 'FALSE_CONDITION',
|
|
60
|
+
'SkipContextsBlock', 'LogicExpression', 'LogicNode',
|
|
61
|
+
'LogicGroupManager', 'LGM', 'LogicGroup',
|
|
62
|
+
'ActionNode', 'BreakpointNode', 'PlaceholderNode',
|
|
63
|
+
'NoAction', 'LongAction', 'ShortAction',
|
|
64
|
+
|
|
65
|
+
# .capi.c_node or .native.node
|
|
66
|
+
'RootLogicNode', 'ContextLogicExpression',
|
|
67
|
+
'AttrExpression', 'AttrNestedExpression',
|
|
68
|
+
'GetterExpression', 'GetterNestedExpression',
|
|
69
|
+
'MathExpressionOperator', 'MathExpression',
|
|
70
|
+
'ComparisonExpressionOperator', 'ComparisonExpression',
|
|
71
|
+
'LogicalExpressionOperator', 'LogicalExpression',
|
|
72
|
+
|
|
73
|
+
# .capi.c_collection or .native.collection
|
|
74
|
+
'LogicMapping', 'LogicSequence', 'LogicGenerator',
|
|
75
|
+
|
|
76
|
+
# .webui
|
|
77
|
+
'DecisionTreeWebUi', 'show', 'to_html'
|
|
78
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild('CAPI')
|
|
6
|
+
|
|
7
|
+
from .c_abc import (
|
|
8
|
+
Singleton,
|
|
9
|
+
NodeEdgeCondition, ConditionElse, ConditionAny, ConditionAuto, BinaryCondition, ConditionTrue, ConditionFalse,
|
|
10
|
+
NO_CONDITION, ELSE_CONDITION, AUTO_CONDITION, TRUE_CONDITION, FALSE_CONDITION,
|
|
11
|
+
SkipContextsBlock, LogicExpression, LogicNode,
|
|
12
|
+
LogicGroupManager, LGM, LogicGroup,
|
|
13
|
+
ActionNode, BreakpointNode, PlaceholderNode,
|
|
14
|
+
NoAction, LongAction, ShortAction
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .c_node import (
|
|
18
|
+
RootLogicNode, ContextLogicExpression,
|
|
19
|
+
AttrExpression, AttrNestedExpression,
|
|
20
|
+
GetterExpression, GetterNestedExpression,
|
|
21
|
+
MathExpressionOperator, MathExpression,
|
|
22
|
+
ComparisonExpressionOperator, ComparisonExpression,
|
|
23
|
+
LogicalExpressionOperator, LogicalExpression,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .c_collection import (
|
|
27
|
+
LogicMapping,
|
|
28
|
+
LogicSequence,
|
|
29
|
+
LogicGenerator,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_logger(logger: logging.Logger):
|
|
34
|
+
global LOGGER
|
|
35
|
+
LOGGER = logger
|
|
36
|
+
c_abc.LOGGER = logger.getChild('abc')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
'LOGGER', 'set_logger',
|
|
41
|
+
'Singleton',
|
|
42
|
+
'NodeEdgeCondition', 'ConditionElse', 'ConditionAny', 'ConditionAuto', 'BinaryCondition', 'ConditionTrue', 'ConditionFalse',
|
|
43
|
+
'NO_CONDITION', 'ELSE_CONDITION', 'AUTO_CONDITION', 'TRUE_CONDITION', 'FALSE_CONDITION',
|
|
44
|
+
'SkipContextsBlock', 'LogicExpression', 'LogicNode',
|
|
45
|
+
'LogicGroupManager', 'LGM', 'LogicGroup',
|
|
46
|
+
'ActionNode', 'BreakpointNode', 'PlaceholderNode',
|
|
47
|
+
'NoAction', 'LongAction', 'ShortAction',
|
|
48
|
+
|
|
49
|
+
'RootLogicNode', 'ContextLogicExpression',
|
|
50
|
+
'AttrExpression', 'AttrNestedExpression',
|
|
51
|
+
'GetterExpression', 'GetterNestedExpression',
|
|
52
|
+
'MathExpressionOperator', 'MathExpression',
|
|
53
|
+
'ComparisonExpressionOperator', 'ComparisonExpression',
|
|
54
|
+
'LogicalExpressionOperator', 'LogicalExpression',
|
|
55
|
+
|
|
56
|
+
'LogicMapping', 'LogicSequence', 'LogicGenerator'
|
|
57
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterable, Callable
|
|
3
|
+
from typing import Any, Never, final
|
|
4
|
+
|
|
5
|
+
from decision_graph.decision_tree.exc import NO_DEFAULT
|
|
6
|
+
|
|
7
|
+
LOGGER: logging.Logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Singleton(object):
|
|
11
|
+
"""Lightweight base to mark extension types as singletons.
|
|
12
|
+
|
|
13
|
+
Used internally by Cython classes to ensure only one instance of certain
|
|
14
|
+
helper types is created per process. Users typically won't need this.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Edge condition types
|
|
19
|
+
class NodeEdgeCondition(Singleton):
|
|
20
|
+
"""Represents an edge condition in a decision graph.
|
|
21
|
+
|
|
22
|
+
This is the base type for all concrete condition markers. Most users
|
|
23
|
+
don't create these directly; instead, use the pre-created constants
|
|
24
|
+
like ``TRUE_CONDITION``, ``FALSE_CONDITION``, ``ELSE_CONDITION``, or
|
|
25
|
+
let conditions be inferred automatically when building graphs.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def value(self) -> Any: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConditionElse(NodeEdgeCondition):
|
|
33
|
+
"""Represents an explicit "else" branch in decision trees.
|
|
34
|
+
|
|
35
|
+
It matches when none of the other registered conditions match.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConditionAny(NodeEdgeCondition):
|
|
40
|
+
"""Represents an unconditioned branch (always eligible as a fallback)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConditionAuto(NodeEdgeCondition):
|
|
44
|
+
"""Marker used internally to request auto-inference of the edge condition."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BinaryCondition(NodeEdgeCondition):
|
|
48
|
+
"""Base type for binary True/False conditions."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ConditionTrue(BinaryCondition):
|
|
52
|
+
"""The boolean True branch condition.
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
- Truthy when converted to ``bool``.
|
|
56
|
+
- ``int(ConditionTrue)`` equals ``1``.
|
|
57
|
+
- Unary negation or bitwise invert toggles to ``FALSE_CONDITION``.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConditionFalse(BinaryCondition):
|
|
62
|
+
"""The boolean False branch condition.
|
|
63
|
+
|
|
64
|
+
Notes:
|
|
65
|
+
- Falsy when converted to ``bool``.
|
|
66
|
+
- ``int(ConditionFalse)`` equals ``0``.
|
|
67
|
+
- Unary negation or bitwise invert toggles to ``TRUE_CONDITION``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Pre-created condition singletons exposed by the module
|
|
72
|
+
NO_CONDITION: ConditionAny
|
|
73
|
+
ELSE_CONDITION: ConditionElse
|
|
74
|
+
AUTO_CONDITION: ConditionAuto
|
|
75
|
+
TRUE_CONDITION: ConditionTrue
|
|
76
|
+
FALSE_CONDITION: ConditionFalse
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SkipContextsBlock:
|
|
80
|
+
"""Context manager that may skip executing the body of a with-block.
|
|
81
|
+
|
|
82
|
+
- If the entry check passes, ``__enter__`` returns ``self`` and normal
|
|
83
|
+
execution proceeds until ``__exit__`` is called.
|
|
84
|
+
- If the entry check fails, execution of the block is prevented via
|
|
85
|
+
tracing hooks and an internal control-flow exception; ``__exit__`` then
|
|
86
|
+
suppresses that exception so the program continues after the block.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
default_entry_check (bool): If True, the block executes by default.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
default_entry_check: bool
|
|
93
|
+
|
|
94
|
+
@final
|
|
95
|
+
def __enter__(self) -> SkipContextsBlock: ...
|
|
96
|
+
|
|
97
|
+
@final
|
|
98
|
+
def __exit__(
|
|
99
|
+
self,
|
|
100
|
+
exc_type: type[BaseException] | None,
|
|
101
|
+
exc_value: BaseException | None,
|
|
102
|
+
exc_traceback,
|
|
103
|
+
) -> bool | None: ...
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LogicExpression(SkipContextsBlock):
|
|
107
|
+
"""Represents a logical or mathematical expression with deferred eval.
|
|
108
|
+
|
|
109
|
+
The expression can be a static value, an exception to raise on evaluation,
|
|
110
|
+
or a callable returning a value. An optional ``dtype`` can be provided to
|
|
111
|
+
enforce or check the evaluated type.
|
|
112
|
+
|
|
113
|
+
This class supports boolean logic (``&``, ``|``, comparisons) and basic
|
|
114
|
+
arithmetic operators that produce new ``LogicExpression`` instances.
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
expression (object): The underlying expression (value, exception, or callable).
|
|
118
|
+
dtype (type | None): Optional type to enforce on evaluation, if requested.
|
|
119
|
+
repr (str): String representation for debugging and logging.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
expression: object
|
|
123
|
+
dtype: type | None
|
|
124
|
+
repr: str
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
expression: float | int | bool | Exception | Callable[[], Any] = None,
|
|
130
|
+
dtype: type | None = ...,
|
|
131
|
+
repr: str | None = ...,
|
|
132
|
+
**kwargs,
|
|
133
|
+
) -> None: ...
|
|
134
|
+
|
|
135
|
+
def eval(self, enforce_dtype: bool = ...) -> Any:
|
|
136
|
+
"""Evaluate the expression and return the resulting value.
|
|
137
|
+
|
|
138
|
+
If ``enforce_dtype`` is True and a ``dtype`` was provided, the result
|
|
139
|
+
is cast using ``self.dtype(value)``.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def cast(
|
|
144
|
+
cls,
|
|
145
|
+
value: int | float | bool | Exception | LogicExpression | Callable[..., Any],
|
|
146
|
+
dtype: type | None = ...,
|
|
147
|
+
) -> LogicExpression:
|
|
148
|
+
"""
|
|
149
|
+
Cast a value into a LogicExpression.
|
|
150
|
+
If the value is already a LogicExpression, it is returned as-is (same instance).
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
LogicExpression: The resulting LogicExpression instance.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
TypeError: If the value type is unsupported.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __bool__(self) -> bool:
|
|
160
|
+
"""Evaluate the expression and return its boolean value."""
|
|
161
|
+
|
|
162
|
+
def __and__(self, other: LogicExpression | bool) -> LogicExpression:
|
|
163
|
+
"""Return a new LogicExpression representing logical AND with ``other``."""
|
|
164
|
+
|
|
165
|
+
def __eq__(self, other: object) -> LogicExpression:
|
|
166
|
+
"""Return a new LogicExpression representing equality comparison to ``other``."""
|
|
167
|
+
|
|
168
|
+
def __or__(self, other: LogicExpression | bool) -> LogicExpression:
|
|
169
|
+
"""Return a new LogicExpression representing logical OR with ``other``."""
|
|
170
|
+
|
|
171
|
+
def __add__(self, other: object) -> LogicExpression:
|
|
172
|
+
"""Return a new LogicExpression representing addition with ``other``."""
|
|
173
|
+
|
|
174
|
+
def __sub__(self, other: object) -> LogicExpression:
|
|
175
|
+
"""Return a new LogicExpression representing subtraction with ``other``."""
|
|
176
|
+
|
|
177
|
+
def __mul__(self, other: object) -> LogicExpression:
|
|
178
|
+
"""Return a new LogicExpression representing multiplication with ``other``."""
|
|
179
|
+
|
|
180
|
+
def __truediv__(self, other: object) -> LogicExpression:
|
|
181
|
+
"""Return a new LogicExpression representing true division with ``other``."""
|
|
182
|
+
|
|
183
|
+
def __floordiv__(self, other: object) -> LogicExpression:
|
|
184
|
+
"""Return a new LogicExpression representing floor division with ``other``."""
|
|
185
|
+
|
|
186
|
+
def __pow__(self, other: object) -> LogicExpression:
|
|
187
|
+
"""Return a new LogicExpression representing exponentiation with ``other``."""
|
|
188
|
+
|
|
189
|
+
def __lt__(self, other: object) -> LogicExpression:
|
|
190
|
+
"""Return a new LogicExpression representing less-than comparison to ``other``."""
|
|
191
|
+
|
|
192
|
+
def __le__(self, other: object) -> LogicExpression:
|
|
193
|
+
"""Return a new LogicExpression representing less-than-or-equal comparison to ``other``."""
|
|
194
|
+
|
|
195
|
+
def __gt__(self, other: object) -> LogicExpression:
|
|
196
|
+
"""Return a new LogicExpression representing greater-than comparison to ``other``."""
|
|
197
|
+
|
|
198
|
+
def __ge__(self, other: object) -> LogicExpression:
|
|
199
|
+
"""Return a new LogicExpression representing greater-than-or-equal comparison to ``other``."""
|
|
200
|
+
|
|
201
|
+
def __repr__(self) -> str:
|
|
202
|
+
"""Return the string representation of the LogicExpression."""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class LogicGroupManager(Singleton):
|
|
206
|
+
"""Singleton manager for LogicGroup instances and runtime expression context.
|
|
207
|
+
|
|
208
|
+
Handles caching and reuse of ``LogicGroup`` objects and manages runtime
|
|
209
|
+
stacks for active groups and nodes while building or evaluating decision
|
|
210
|
+
graphs.
|
|
211
|
+
|
|
212
|
+
Also supports shelving/unshelving state to create decision sub-graphs
|
|
213
|
+
(for example, across function calls) without interfering with the main
|
|
214
|
+
active state.
|
|
215
|
+
|
|
216
|
+
Attributes:
|
|
217
|
+
inspection_mode (bool): If True, generate layout without executing actions.
|
|
218
|
+
vigilant_mode (bool): If True, perform stricter validation and avoid auto-generated nodes.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
inspection_mode: bool
|
|
222
|
+
vigilant_mode: bool
|
|
223
|
+
|
|
224
|
+
def __call__(self, name: str, cls: type[LogicGroup], **kwargs) -> LogicGroup:
|
|
225
|
+
"""Get or create a cached LogicGroup instance with the given name.
|
|
226
|
+
|
|
227
|
+
Useful for closed-loop operations that need to reuse the same logic group.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
name (str): The name of the logic group.
|
|
231
|
+
cls (type[LogicGroup]): The LogicGroup subclass to instantiate if not cached.
|
|
232
|
+
**kwargs: Additional keyword arguments to pass to the LogicGroup constructor.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
LogicGroup: The cached or newly created LogicGroup instance.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __contains__(self, instance: LogicGroup) -> bool:
|
|
239
|
+
"""Return True if the given LogicGroup instance is cached by this manager."""
|
|
240
|
+
|
|
241
|
+
def clear(self) -> None:
|
|
242
|
+
"""Clear all cached LogicGroup instances and reset runtime stacks."""
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def active_group(self) -> LogicGroup | None:
|
|
246
|
+
"""The currently active LogicGroup, or None if no group context is entered."""
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def active_node(self) -> LogicNode | None:
|
|
250
|
+
"""The currently active LogicNode expression, or None if no expression context is entered."""
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Global instance of the manager
|
|
254
|
+
LGM: LogicGroupManager
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class LogicGroup:
|
|
258
|
+
"""A minimal context manager to scope logic groups and break operations.
|
|
259
|
+
|
|
260
|
+
A logic group is a lightweight context that records its name and an
|
|
261
|
+
optional parent, and it provides a ``Break`` exception type for orderly
|
|
262
|
+
early exit via ``LogicGroup.break_``.
|
|
263
|
+
|
|
264
|
+
In runtime mode, breaking from a logic group propagates through nested
|
|
265
|
+
groups and moves the execution cursor to the first line after the block;
|
|
266
|
+
during this process, on-exit hooks of nested groups and nodes run.
|
|
267
|
+
|
|
268
|
+
In inspection mode, breaking does not raise immediately. Instead, missing
|
|
269
|
+
branches are auto-filled with ``NoAction`` to allow layout evaluation to
|
|
270
|
+
continue, especially when a break occurs before the other branch is built.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
name (str): The name of the logic group.
|
|
274
|
+
parent (LogicGroup | None): The parent logic group, if any.
|
|
275
|
+
Break (type[BaseException]): The exception type used for breaks.
|
|
276
|
+
contexts (dict[str, Any]): Context-specific storage for the group.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
name: str
|
|
280
|
+
parent: LogicGroup | None
|
|
281
|
+
Break: type[BaseException]
|
|
282
|
+
contexts: dict[str, Any]
|
|
283
|
+
|
|
284
|
+
def __init__(self, *, name: str = None, parent: LogicGroup = None, contexts: dict = None, **kwargs):
|
|
285
|
+
"""Initialize a LogicGroup with the given name, parent, and contexts.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
name (str): The name of the logic group. If None, a unique name is assigned.
|
|
289
|
+
parent (LogicGroup | None): The parent logic group, if any.
|
|
290
|
+
contexts (dict[str, Any] | None): Optional context-specific storage.
|
|
291
|
+
kwargs: __cinit__ extra kwargs guardian of for subclassing support, not used is this base class.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str: ...
|
|
295
|
+
|
|
296
|
+
def __enter__(self) -> LogicGroup:
|
|
297
|
+
"""Enter the logic group context and mark it as active."""
|
|
298
|
+
...
|
|
299
|
+
|
|
300
|
+
def __exit__(
|
|
301
|
+
self,
|
|
302
|
+
exc_type: type[BaseException] | None,
|
|
303
|
+
exc_value: BaseException | None,
|
|
304
|
+
exc_traceback,
|
|
305
|
+
) -> bool | None:
|
|
306
|
+
"""Exit the logic group context, handling Break exceptions gracefully."""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def break_(cls, scope: LogicGroup | None = ...) -> None:
|
|
311
|
+
"""Break out from the given ``scope`` (or the active scope if None).
|
|
312
|
+
|
|
313
|
+
In inspection mode, the break is recorded to be connected to the next
|
|
314
|
+
entered node. In runtime mode, this propagates break through nested
|
|
315
|
+
groups until the target scope is exited.
|
|
316
|
+
"""
|
|
317
|
+
...
|
|
318
|
+
|
|
319
|
+
def break_active(self) -> None:
|
|
320
|
+
"""Break out from the currently active logic group only (top of stack)."""
|
|
321
|
+
|
|
322
|
+
def break_inspection(self) -> None:
|
|
323
|
+
"""Record a break while in inspection mode without raising immediately."""
|
|
324
|
+
|
|
325
|
+
def break_runtime(self) -> None:
|
|
326
|
+
"""Propagate a break through nested groups until the target scope is exited."""
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class LogicNode(LogicExpression):
|
|
330
|
+
"""A decision node that branches to children based on an evaluated value.
|
|
331
|
+
|
|
332
|
+
Each child is registered against an edge condition, and the node supports
|
|
333
|
+
auto-inference of binary conditions for succinct graph construction.
|
|
334
|
+
|
|
335
|
+
Attributes:
|
|
336
|
+
parent (LogicNode | None): The parent node, if any.
|
|
337
|
+
condition_to_parent (NodeEdgeCondition): The edge condition leading to this node from its parent.
|
|
338
|
+
children (dict[NodeEdgeCondition, LogicNode]): Mapping of edge conditions to child nodes.
|
|
339
|
+
labels (list[str]): LogicGroup names this node belongs to.
|
|
340
|
+
autogen (bool): Whether this node was auto-generated to fill a missing branch.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
parent: LogicNode | None
|
|
344
|
+
condition_to_parent: NodeEdgeCondition
|
|
345
|
+
children: dict[NodeEdgeCondition, LogicNode]
|
|
346
|
+
labels: list[str]
|
|
347
|
+
autogen: bool
|
|
348
|
+
|
|
349
|
+
def __init__(self, *, expression: object = None, dtype: type = None, repr: str = None, **kwargs):
|
|
350
|
+
"""
|
|
351
|
+
Initialize the LogicExpression.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
expression (Union[Any, Callable[[], Any]]): A callable or static value.
|
|
355
|
+
dtype (type, optional): The expected type of the evaluated value (float, int, or bool).
|
|
356
|
+
repr (str, optional): A string representation of the expression.
|
|
357
|
+
kwargs: __cinit__ extra kwargs guardian of for subclassing support, not used is this base class.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
def __rshift__(self, other: LogicNode) -> LogicNode:
|
|
361
|
+
"""
|
|
362
|
+
Convenience for ``append`` to support chaining, e.g.::
|
|
363
|
+
|
|
364
|
+
>>> node1 = LogicNode(expression=...)
|
|
365
|
+
>>> node2 = LogicNode(expression=...)
|
|
366
|
+
>>> node1 >> node2
|
|
367
|
+
|
|
368
|
+
Returns the ``other`` node so calls can be chained.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def __call__(self, default: Any | None = ...) -> Any:
|
|
372
|
+
"""Evaluate the tree from this node and return the final action/value.
|
|
373
|
+
|
|
374
|
+
If ``default`` is not provided, a ``NoAction`` node will be used as the
|
|
375
|
+
fallback terminal.
|
|
376
|
+
|
|
377
|
+
You can pass ``NO_DEFAULT`` to explicitly require a matching branch;
|
|
378
|
+
if no edge matches, a ``ValueError`` will be raised.
|
|
379
|
+
See ``eval_recursively`` for details.
|
|
380
|
+
"""
|
|
381
|
+
...
|
|
382
|
+
|
|
383
|
+
def append(self, child: LogicNode, condition: NodeEdgeCondition = ...) -> None:
|
|
384
|
+
"""Append a child node with the given edge condition.
|
|
385
|
+
|
|
386
|
+
If ``condition`` is ``AUTO_CONDITION``, the condition is inferred
|
|
387
|
+
automatically based on existing children (for binary branching).
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
ValueError: If an invalid condition is provided (e.g., ``None``).
|
|
391
|
+
KeyError: If the condition is already used by another child.
|
|
392
|
+
EdgeValueError: If condition inference fails due to incompatible existing children.
|
|
393
|
+
TooManyChildren: If inference fails due to too many existing children.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def overwrite(self, new_node: LogicNode, condition: NodeEdgeCondition) -> None:
|
|
397
|
+
"""Overwrite the child node for the given edge condition.
|
|
398
|
+
|
|
399
|
+
If ``condition`` is ``AUTO_CONDITION``, the condition is inferred
|
|
400
|
+
automatically based on existing children (for binary branching).
|
|
401
|
+
|
|
402
|
+
Raises:
|
|
403
|
+
ValueError: If an invalid condition is provided (e.g., ``None``).
|
|
404
|
+
KeyError: If there is no existing child for the given condition.
|
|
405
|
+
EdgeValueError: If condition inference fails due to incompatible existing children.
|
|
406
|
+
TooManyChildren: If inference fails due to too many existing children.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def replace(self, original_node: LogicNode, new_node: LogicNode) -> None:
|
|
410
|
+
"""Replace an existing child node with a new node.
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
RuntimeError: If the original node is currently active.
|
|
414
|
+
LookupError: If the stack is out of sync with the node's children.
|
|
415
|
+
NodeNotFountError: If the original node is not a child of this node.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
def eval_recursively(
|
|
419
|
+
self,
|
|
420
|
+
path: list[LogicNode] | None = ...,
|
|
421
|
+
default: Any = NO_DEFAULT,
|
|
422
|
+
) -> tuple[Any, list[LogicNode]]:
|
|
423
|
+
"""Evaluate the decision tree recursively from this node.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
path (list[LogicNode] | None): If provided, a list to record the
|
|
427
|
+
sequence of nodes traversed during evaluation.
|
|
428
|
+
default (Any): The default value or action to use if no matching
|
|
429
|
+
child is found. Use ``NO_DEFAULT`` to request an error when no
|
|
430
|
+
branch matches.
|
|
431
|
+
Returns:
|
|
432
|
+
tuple[Any, list[LogicNode]]: The resulting value/action and the
|
|
433
|
+
path list of nodes traversed during evaluation.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def list_labels(self) -> dict[str, list[LogicNode]]:
|
|
437
|
+
"""List all LogicGroup names in the subtree rooted at this node.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
dict[str, list[LogicNode]]: A mapping from label strings (logic group names)
|
|
441
|
+
to lists of nodes that have that label.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def leaves(self) -> Iterable[LogicNode]:
|
|
446
|
+
"""An iterable of all leaf nodes in the subtree rooted at this node."""
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def is_leaf(self) -> bool:
|
|
450
|
+
"""True if this node has no children; otherwise False."""
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def child_stack(self) -> Iterable[LogicNode]:
|
|
454
|
+
"""An iterable of all child nodes in the subtree rooted at this node."""
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class BreakpointNode(LogicNode):
|
|
458
|
+
"""A logic node that represents a breakpoint in the decision tree, used for breaking out of logic groups.
|
|
459
|
+
|
|
460
|
+
This node is auto-generated and can connect to at most one child node.
|
|
461
|
+
|
|
462
|
+
During evaluation, if connected, it delegates to the child's evaluation; otherwise, it returns its default expression (NoAction) in vigilant mode.
|
|
463
|
+
|
|
464
|
+
Attributes:
|
|
465
|
+
break_from (LogicGroup): The logic group from which this breakpoint breaks.
|
|
466
|
+
await_connection (bool): Whether to wait for a connection to a child node during inspection.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
break_from: LogicGroup
|
|
470
|
+
await_connection: bool
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class ActionNode(LogicNode):
|
|
474
|
+
"""A terminal node that can execute an optional ``action`` upon selection."""
|
|
475
|
+
|
|
476
|
+
action: Callable[[], Any] | None
|
|
477
|
+
|
|
478
|
+
def __init__(
|
|
479
|
+
self,
|
|
480
|
+
*,
|
|
481
|
+
action: Callable[[], Any] | None = ...,
|
|
482
|
+
expression: object | None = ...,
|
|
483
|
+
dtype: type | None = ...,
|
|
484
|
+
repr: str | None = ...,
|
|
485
|
+
auto_connect: bool = True,
|
|
486
|
+
**kwargs,
|
|
487
|
+
) -> None: ...
|
|
488
|
+
|
|
489
|
+
def __enter__(self) -> Never:
|
|
490
|
+
"""
|
|
491
|
+
ActionNode does not support the context manager protocol.
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
NodeContextError: Using ``with ActionNode()`` is invalid.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
def append(self, child: LogicNode, condition: NodeEdgeCondition = ...) -> Never:
|
|
498
|
+
"""
|
|
499
|
+
Appending children to an ActionNode is not supported.
|
|
500
|
+
|
|
501
|
+
Since ActionNodes are terminal, ``replace`` and ``overwrite`` are also
|
|
502
|
+
not applicable and will fail naturally if attempted.
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
TooManyChildren: Always raised to signal invalid operation.
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class PlaceholderNode(ActionNode):
|
|
510
|
+
"""An action node that serves as a placeholder in the decision tree.
|
|
511
|
+
|
|
512
|
+
This node is auto-generated and during evaluation returns itself in vigilant mode, otherwise returns a NoAction instance.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class NoAction(ActionNode):
|
|
517
|
+
"""An action node whose evaluation returns itself and performs no action."""
|
|
518
|
+
|
|
519
|
+
sig: int = 0
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class LongAction(ActionNode):
|
|
523
|
+
"""An action node variant carrying a positive ``sig`` marker.
|
|
524
|
+
|
|
525
|
+
Attributes:
|
|
526
|
+
sig (int): The signature marker for the long action. Defaults to ``1``.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
sig: int
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class ShortAction(ActionNode):
|
|
533
|
+
"""An action node variant carrying a negative ``sig`` marker.
|
|
534
|
+
|
|
535
|
+
Attributes:
|
|
536
|
+
sig (int): The signature marker for the long action. Defaults to ``-1``.
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
sig: int
|