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.
@@ -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
+ ]
@@ -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