PyDecisionGraph 0.1.0__py3-none-any.whl → 0.1.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.
Potentially problematic release.
This version of PyDecisionGraph might be problematic. Click here for more details.
- decision_graph/__init__.py +16 -0
- decision_graph/decision_tree/__init__.py +44 -0
- {decision_tree → decision_graph/decision_tree}/abc.py +66 -18
- {decision_tree → decision_graph/decision_tree}/collection.py +7 -13
- {decision_tree → decision_graph/decision_tree}/node.py +25 -11
- decision_graph/logic_group/__init__.py +22 -0
- decision_graph/logic_group/base.py +72 -0
- decision_graph/logic_group/pending_request.py +253 -0
- pydecisiongraph-0.1.2.dist-info/METADATA +149 -0
- pydecisiongraph-0.1.2.dist-info/RECORD +15 -0
- pydecisiongraph-0.1.2.dist-info/top_level.txt +1 -0
- decision_tree/__init__.py +0 -36
- decision_tree/logic_group.py +0 -307
- pydecisiongraph-0.1.0.dist-info/METADATA +0 -21
- pydecisiongraph-0.1.0.dist-info/RECORD +0 -12
- pydecisiongraph-0.1.0.dist-info/top_level.txt +0 -1
- {decision_tree → decision_graph/decision_tree}/exc.py +0 -0
- {decision_tree → decision_graph/decision_tree}/expression.py +0 -0
- {pydecisiongraph-0.1.0.dist-info → pydecisiongraph-0.1.2.dist-info}/LICENSE +0 -0
- {pydecisiongraph-0.1.0.dist-info → pydecisiongraph-0.1.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__version__ = "0.1.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,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild("DecisionTree")
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'LOGGER', 'set_logger', 'activate_expression_model', 'activate_node_model',
|
|
9
|
+
'NodeError', 'TooManyChildren', 'TooFewChildren', 'NodeNotFountError', 'NodeValueError', 'EdgeValueError', 'ResolutionError', 'ExpressFalse', 'ContextsNotFound',
|
|
10
|
+
'LGM', 'LogicGroup', 'SkipContextsBlock', 'LogicExpression', 'ExpressionCollection', 'LogicNode', 'ActionNode', 'ELSE_CONDITION',
|
|
11
|
+
'NoAction', 'LongAction', 'ShortAction', 'RootLogicNode', 'ContextLogicExpression', 'AttrExpression', 'MathExpression', 'ComparisonExpression', 'LogicalExpression',
|
|
12
|
+
'LogicMapping', 'LogicGenerator'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
from .exc import *
|
|
16
|
+
from .abc import *
|
|
17
|
+
from .node import *
|
|
18
|
+
from .collection import *
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_logger(logger: logging.Logger):
|
|
22
|
+
global LOGGER
|
|
23
|
+
LOGGER = logger
|
|
24
|
+
|
|
25
|
+
abc.LOGGER = logger.getChild('abc')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def activate_expression_model():
|
|
29
|
+
import importlib
|
|
30
|
+
importlib.import_module('decision_graph.decision_tree.expression')
|
|
31
|
+
importlib.reload(collection)
|
|
32
|
+
collection.LogicMapping.AttrExpression = AttrExpression
|
|
33
|
+
collection.LogicGenerator.AttrExpression = AttrExpression
|
|
34
|
+
# importlib.reload(logic_group)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def activate_node_model():
|
|
38
|
+
import importlib
|
|
39
|
+
|
|
40
|
+
importlib.import_module('decision_graph.decision_tree.node')
|
|
41
|
+
importlib.reload(collection)
|
|
42
|
+
collection.LogicMapping.AttrExpression = AttrExpression
|
|
43
|
+
collection.LogicGenerator.AttrExpression = AttrExpression
|
|
44
|
+
# importlib.reload(logic_group)
|
|
@@ -11,6 +11,8 @@ from typing import Any, Self, final
|
|
|
11
11
|
from . import LOGGER
|
|
12
12
|
from .exc import TooFewChildren, TooManyChildren, EdgeValueError, NodeValueError, NodeNotFountError
|
|
13
13
|
|
|
14
|
+
LOGGER = LOGGER.getChild('abc')
|
|
15
|
+
|
|
14
16
|
__all__ = ['LGM', 'LogicGroup', 'SkipContextsBlock', 'LogicExpression', 'ExpressionCollection', 'LogicNode', 'ActionNode', 'ELSE_CONDITION']
|
|
15
17
|
|
|
16
18
|
|
|
@@ -46,9 +48,11 @@ class LogicGroupManager(metaclass=Singleton):
|
|
|
46
48
|
# Cursor to track the currently active LogicGroups
|
|
47
49
|
self._active_groups: list[LogicGroup] = []
|
|
48
50
|
self._active_nodes: list[LogicNode] = []
|
|
49
|
-
self.
|
|
50
|
-
self._pending_connection_nodes: list[ActionNode] = [] # for those
|
|
51
|
+
self._breakpoint_nodes: list[ActionNode] = [] # action nodes, usually NoAction() nodes, marked as an early-exit (breakpoint) of a logic group
|
|
52
|
+
self._pending_connection_nodes: list[ActionNode] = [] # for those breakpoint-nodes, they will be activated when the corresponding logic group is finalized.
|
|
53
|
+
self._shelved_state = [] # shelve state to support temporally initialize a separate node-graph
|
|
51
54
|
self.inspection_mode = False
|
|
55
|
+
self.vigilant_mode = False
|
|
52
56
|
|
|
53
57
|
def __call__(self, name: str, cls: type[LogicGroup], **kwargs) -> LogicGroup:
|
|
54
58
|
"""
|
|
@@ -95,7 +99,7 @@ class LogicGroupManager(metaclass=Singleton):
|
|
|
95
99
|
|
|
96
100
|
self._active_groups.pop(-1)
|
|
97
101
|
|
|
98
|
-
for node in self.
|
|
102
|
+
for node in self._breakpoint_nodes:
|
|
99
103
|
if getattr(node, 'break_from') is logic_group:
|
|
100
104
|
self._pending_connection_nodes.append(node)
|
|
101
105
|
|
|
@@ -129,6 +133,38 @@ class LogicGroupManager(metaclass=Singleton):
|
|
|
129
133
|
|
|
130
134
|
self._active_nodes.pop(-1)
|
|
131
135
|
|
|
136
|
+
def shelve(self):
|
|
137
|
+
shelved_state = dict(
|
|
138
|
+
active_nodes=self._active_nodes.copy(),
|
|
139
|
+
breakpoint_nodes=self._breakpoint_nodes.copy(),
|
|
140
|
+
pending_connection_nodes=self._pending_connection_nodes.copy()
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self._active_nodes.clear()
|
|
144
|
+
self._breakpoint_nodes.clear()
|
|
145
|
+
self._pending_connection_nodes.clear()
|
|
146
|
+
|
|
147
|
+
self._shelved_state.append(shelved_state)
|
|
148
|
+
return shelved_state
|
|
149
|
+
|
|
150
|
+
def unshelve(self, reset_active: bool = True, reset_breakpoints: bool = True, reset_pending: bool = True):
|
|
151
|
+
shelved_state = self._shelved_state.pop(-1)
|
|
152
|
+
|
|
153
|
+
if reset_active:
|
|
154
|
+
self._active_nodes.clear()
|
|
155
|
+
|
|
156
|
+
if reset_breakpoints:
|
|
157
|
+
self._breakpoint_nodes.clear()
|
|
158
|
+
|
|
159
|
+
if reset_pending:
|
|
160
|
+
self._pending_connection_nodes.clear()
|
|
161
|
+
|
|
162
|
+
self._active_nodes[:0] = shelved_state['active_nodes']
|
|
163
|
+
self._breakpoint_nodes[:0] = shelved_state['breakpoint_nodes']
|
|
164
|
+
self._pending_connection_nodes[:0] = shelved_state['pending_connection_nodes']
|
|
165
|
+
|
|
166
|
+
return shelved_state
|
|
167
|
+
|
|
132
168
|
def clear(self):
|
|
133
169
|
"""
|
|
134
170
|
Clear the cache of LogicGroup instances and reset active groups.
|
|
@@ -242,12 +278,17 @@ class LogicGroup(object, metaclass=LogicGroupMeta):
|
|
|
242
278
|
if active_node is not None:
|
|
243
279
|
active_node: LogicNode
|
|
244
280
|
if not active_node.nodes:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
281
|
+
if LGM.vigilant_mode:
|
|
282
|
+
raise TooFewChildren()
|
|
283
|
+
else:
|
|
284
|
+
LOGGER.warning('Must have at least one action node before breaking from logic group. A NoAction node will be automatically assigned.')
|
|
285
|
+
from .node import NoAction
|
|
286
|
+
NoAction()
|
|
287
|
+
|
|
288
|
+
last_node = active_node.last_leaf
|
|
289
|
+
assert isinstance(last_node, ActionNode), NodeValueError('An ActionNode is required before breaking a LogicGroup.')
|
|
290
|
+
last_node.break_from = scope
|
|
291
|
+
LGM._breakpoint_nodes.append(last_node)
|
|
251
292
|
return
|
|
252
293
|
|
|
253
294
|
raise scope.Break()
|
|
@@ -542,14 +583,14 @@ class LogicExpression(SkipContextsBlock):
|
|
|
542
583
|
|
|
543
584
|
|
|
544
585
|
class ExpressionCollection(LogicGroup):
|
|
545
|
-
def __init__(self, data: Any, name: str,
|
|
586
|
+
def __init__(self, data: Any, name: str, **kwargs):
|
|
546
587
|
if 'logic_group' not in kwargs:
|
|
547
588
|
logic_group = kwargs.get("logic_group")
|
|
548
589
|
else:
|
|
549
590
|
logic_group = LGM.active_logic_group
|
|
550
591
|
|
|
551
592
|
super().__init__(
|
|
552
|
-
name=
|
|
593
|
+
name=name if name is not None else f'{logic_group.name}.{self.__class__.__name__}',
|
|
553
594
|
parent=logic_group
|
|
554
595
|
)
|
|
555
596
|
|
|
@@ -842,7 +883,7 @@ class LogicNode(LogicExpression):
|
|
|
842
883
|
|
|
843
884
|
return root
|
|
844
885
|
|
|
845
|
-
def to_html(self, with_group=True, dry_run=True, filename="
|
|
886
|
+
def to_html(self, with_group=True, dry_run=True, filename="decision_graph.html", **kwargs):
|
|
846
887
|
"""
|
|
847
888
|
Visualizes the decision tree using PyVis.
|
|
848
889
|
If dry_run=True, shows structure without highlighting active path.
|
|
@@ -1098,8 +1139,7 @@ class LogicNode(LogicExpression):
|
|
|
1098
1139
|
class ActionNode(LogicNode):
|
|
1099
1140
|
def __init__(
|
|
1100
1141
|
self,
|
|
1101
|
-
action:
|
|
1102
|
-
dtype: type = None,
|
|
1142
|
+
action: Callable[[], Any] | None = None,
|
|
1103
1143
|
repr: str = None,
|
|
1104
1144
|
auto_connect: bool = True
|
|
1105
1145
|
):
|
|
@@ -1108,10 +1148,10 @@ class ActionNode(LogicNode):
|
|
|
1108
1148
|
|
|
1109
1149
|
Args:
|
|
1110
1150
|
action (Union[Any, Callable[[], Any]]): The action to execute.
|
|
1111
|
-
dtype (type, optional): The expected type of the evaluated value (float, int, or bool).
|
|
1112
1151
|
repr (str, optional): A string representation of the expression.
|
|
1152
|
+
auto_connect: auto-connect to the current active decision graph.
|
|
1113
1153
|
"""
|
|
1114
|
-
super().__init__(expression=True,
|
|
1154
|
+
super().__init__(expression=True, repr=repr)
|
|
1115
1155
|
self.action = action
|
|
1116
1156
|
|
|
1117
1157
|
if auto_connect:
|
|
@@ -1123,6 +1163,14 @@ class ActionNode(LogicNode):
|
|
|
1123
1163
|
def _on_exit(self):
|
|
1124
1164
|
pass
|
|
1125
1165
|
|
|
1166
|
+
def _post_eval(self):
|
|
1167
|
+
"""
|
|
1168
|
+
override this method to perform clean up functions.
|
|
1169
|
+
"""
|
|
1170
|
+
|
|
1171
|
+
if self.action is not None:
|
|
1172
|
+
self.action()
|
|
1173
|
+
|
|
1126
1174
|
def eval_recursively(self, path=None):
|
|
1127
1175
|
"""
|
|
1128
1176
|
Evaluates the decision tree from this node based on the given state.
|
|
@@ -1134,10 +1182,10 @@ class ActionNode(LogicNode):
|
|
|
1134
1182
|
|
|
1135
1183
|
value = self.eval()
|
|
1136
1184
|
|
|
1137
|
-
|
|
1138
|
-
self.action()
|
|
1185
|
+
self._post_eval()
|
|
1139
1186
|
|
|
1140
1187
|
for condition, child in self.nodes.items():
|
|
1188
|
+
LOGGER.warning(f'{self.__class__.__name__} should not have any sub-nodes.')
|
|
1141
1189
|
if condition == value or condition is NO_CONDITION:
|
|
1142
1190
|
return child.eval_recursively(path=path)
|
|
1143
1191
|
|
|
@@ -3,16 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Mapping, Sequence
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from . import AttrExpression
|
|
6
|
+
from . import AttrExpression
|
|
7
7
|
from .abc import LogicGroup, ExpressionCollection
|
|
8
8
|
|
|
9
9
|
__all__ = ['LogicMapping', 'LogicGenerator']
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class LogicMapping(ExpressionCollection):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def __init__(self, data: dict, name: str, repr: str = None, logic_group: LogicGroup = None):
|
|
13
|
+
def __init__(self, data: dict, name: str, logic_group: LogicGroup = None):
|
|
16
14
|
if data is None:
|
|
17
15
|
data = {}
|
|
18
16
|
|
|
@@ -22,7 +20,6 @@ class LogicMapping(ExpressionCollection):
|
|
|
22
20
|
super().__init__(
|
|
23
21
|
data=data,
|
|
24
22
|
name=name,
|
|
25
|
-
repr=repr,
|
|
26
23
|
logic_group=logic_group
|
|
27
24
|
)
|
|
28
25
|
|
|
@@ -33,10 +30,10 @@ class LogicMapping(ExpressionCollection):
|
|
|
33
30
|
return self.data.__len__()
|
|
34
31
|
|
|
35
32
|
def __getitem__(self, key: str):
|
|
36
|
-
return
|
|
33
|
+
return AttrExpression(attr=key, logic_group=self)
|
|
37
34
|
|
|
38
35
|
def __getattr__(self, key: str):
|
|
39
|
-
return
|
|
36
|
+
return AttrExpression(attr=key, logic_group=self)
|
|
40
37
|
|
|
41
38
|
def reset(self):
|
|
42
39
|
pass
|
|
@@ -52,9 +49,7 @@ class LogicMapping(ExpressionCollection):
|
|
|
52
49
|
|
|
53
50
|
|
|
54
51
|
class LogicGenerator(ExpressionCollection):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def __init__(self, data: list[Any], name: str, repr: str = None, logic_group: LogicGroup = None):
|
|
52
|
+
def __init__(self, data: list[Any], name: str, logic_group: LogicGroup = None):
|
|
58
53
|
if data is None:
|
|
59
54
|
data = []
|
|
60
55
|
|
|
@@ -64,7 +59,6 @@ class LogicGenerator(ExpressionCollection):
|
|
|
64
59
|
super().__init__(
|
|
65
60
|
data=data,
|
|
66
61
|
name=name,
|
|
67
|
-
repr=repr,
|
|
68
62
|
logic_group=logic_group
|
|
69
63
|
)
|
|
70
64
|
|
|
@@ -75,13 +69,13 @@ class LogicGenerator(ExpressionCollection):
|
|
|
75
69
|
# if isinstance(value, ContextLogicExpression):
|
|
76
70
|
# yield value
|
|
77
71
|
|
|
78
|
-
yield
|
|
72
|
+
yield AttrExpression(attr=index, logic_group=self)
|
|
79
73
|
|
|
80
74
|
def __len__(self) -> int:
|
|
81
75
|
return len(self.data)
|
|
82
76
|
|
|
83
77
|
def __getitem__(self, index: int):
|
|
84
|
-
return
|
|
78
|
+
return AttrExpression(attr=index, logic_group=self)
|
|
85
79
|
|
|
86
80
|
def append(self, value):
|
|
87
81
|
self.data.append(value)
|
|
@@ -14,11 +14,12 @@ __all__ = ['NoAction', 'LongAction', 'ShortAction', 'RootLogicNode', 'ContextLog
|
|
|
14
14
|
class NoAction(ActionNode):
|
|
15
15
|
def __init__(self, auto_connect: bool = True):
|
|
16
16
|
super().__init__(
|
|
17
|
-
action=None,
|
|
18
17
|
repr='<NoAction>',
|
|
19
18
|
auto_connect=auto_connect
|
|
20
19
|
)
|
|
21
20
|
|
|
21
|
+
self.sig = 0
|
|
22
|
+
|
|
22
23
|
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
23
24
|
return self
|
|
24
25
|
|
|
@@ -26,11 +27,12 @@ class NoAction(ActionNode):
|
|
|
26
27
|
class LongAction(ActionNode):
|
|
27
28
|
def __init__(self, sig: int = 1, auto_connect: bool = True):
|
|
28
29
|
super().__init__(
|
|
29
|
-
action=sig,
|
|
30
30
|
repr=f'<LongAction>(sig = {sig})',
|
|
31
31
|
auto_connect=auto_connect
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
self.sig = sig
|
|
35
|
+
|
|
34
36
|
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
35
37
|
return self
|
|
36
38
|
|
|
@@ -38,11 +40,12 @@ class LongAction(ActionNode):
|
|
|
38
40
|
class ShortAction(ActionNode):
|
|
39
41
|
def __init__(self, sig: int = -1, auto_connect: bool = True):
|
|
40
42
|
super().__init__(
|
|
41
|
-
action=sig,
|
|
42
43
|
repr=f'<ShortAction>(sig = {sig})',
|
|
43
44
|
auto_connect=auto_connect
|
|
44
45
|
)
|
|
45
46
|
|
|
47
|
+
self.sig = sig
|
|
48
|
+
|
|
46
49
|
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
47
50
|
return self
|
|
48
51
|
|
|
@@ -58,17 +61,28 @@ class RootLogicNode(LogicNode):
|
|
|
58
61
|
return True
|
|
59
62
|
|
|
60
63
|
def _on_enter(self):
|
|
61
|
-
|
|
62
|
-
LGM.inspection_mode = True
|
|
63
|
-
LGM._active_nodes.clear()
|
|
64
|
+
# pre-shelve call
|
|
64
65
|
LGM.enter_expression(node=self)
|
|
65
66
|
|
|
67
|
+
state = LGM.shelve()
|
|
68
|
+
|
|
69
|
+
state['inspection_mode'] = LGM.inspection_mode
|
|
70
|
+
|
|
71
|
+
LGM.inspection_mode = True
|
|
72
|
+
|
|
73
|
+
# post-shelve call
|
|
74
|
+
LGM._active_nodes.append(self)
|
|
75
|
+
|
|
66
76
|
def _on_exit(self):
|
|
77
|
+
# pre-unshelve call
|
|
78
|
+
# LGM.exit_expression(node=self)
|
|
79
|
+
|
|
80
|
+
state = LGM.unshelve()
|
|
81
|
+
|
|
82
|
+
# post-unshelve call
|
|
67
83
|
LGM.exit_expression(node=self)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
else:
|
|
71
|
-
LGM.inspection_mode = False
|
|
84
|
+
|
|
85
|
+
LGM.inspection_mode = state['inspection_mode']
|
|
72
86
|
|
|
73
87
|
def append(self, expression: Self, edge_condition: Any = None):
|
|
74
88
|
if self.nodes:
|
|
@@ -78,7 +92,7 @@ class RootLogicNode(LogicNode):
|
|
|
78
92
|
def eval_recursively(self, **kwargs):
|
|
79
93
|
return self.child.eval_recursively(**kwargs)
|
|
80
94
|
|
|
81
|
-
def to_html(self, with_group=True, dry_run=True, filename="
|
|
95
|
+
def to_html(self, with_group=True, dry_run=True, filename="decision_graph.html", **kwargs):
|
|
82
96
|
return self.child.to_html(with_group=with_group, dry_run=dry_run, filename=filename, **kwargs)
|
|
83
97
|
|
|
84
98
|
@property
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild("LogicGroup")
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'SignalLogicGroup', 'InstantConfirmationLogicGroup',
|
|
9
|
+
'StateMapping', 'RequestAction', 'PendingRequest', 'RequestConfirmed', 'RequestDenied', 'RequestRegistered', 'DelayedConfirmationLogicGroup',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_logger(logger: logging.Logger):
|
|
14
|
+
global LOGGER
|
|
15
|
+
LOGGER = logger
|
|
16
|
+
|
|
17
|
+
base.LOGGER = logger.getChild('base')
|
|
18
|
+
pending_request.LOGGER = logger.getChild('delayed')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from .base import *
|
|
22
|
+
from .pending_request import *
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Any, Self, overload
|
|
4
|
+
|
|
5
|
+
from . import LOGGER
|
|
6
|
+
from ..decision_tree import AttrExpression, LogicGroup, ActionNode, LGM, LongAction, ShortAction, NoAction
|
|
7
|
+
|
|
8
|
+
LOGGER = LOGGER.getChild('base')
|
|
9
|
+
|
|
10
|
+
__all__ = ['SignalLogicGroup', 'InstantConfirmationLogicGroup']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SignalLogicGroup(LogicGroup):
|
|
14
|
+
def __init__(self, name: str, parent: Self = None, contexts: dict[str, Any] = None):
|
|
15
|
+
super().__init__(name=name, parent=parent, contexts=contexts)
|
|
16
|
+
|
|
17
|
+
def get(self, attr: str, dtype: type = None, repr: str = None):
|
|
18
|
+
"""
|
|
19
|
+
Retrieve an attribute as a LogicExpression.
|
|
20
|
+
"""
|
|
21
|
+
return AttrExpression(attr=attr, logic_group=self, dtype=dtype, repr=repr)
|
|
22
|
+
|
|
23
|
+
def reset(self):
|
|
24
|
+
self.signal = 0
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def signal(self):
|
|
28
|
+
return self.contexts.get('signal', 0)
|
|
29
|
+
|
|
30
|
+
@signal.setter
|
|
31
|
+
def signal(self, value: int):
|
|
32
|
+
self.contexts['signal'] = value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InstantConfirmationLogicGroup(SignalLogicGroup):
|
|
36
|
+
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
37
|
+
super().__init__(
|
|
38
|
+
name=f'{parent.name}.Instant' if name is None else name,
|
|
39
|
+
parent=parent
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def reset(self):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def confirm(self, sig: Literal[1]) -> LongAction:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def confirm(self, sig: Literal[-1]) -> ShortAction:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def confirm(self, sig: Literal[-1, 1]) -> ActionNode:
|
|
54
|
+
self.signal = sig
|
|
55
|
+
|
|
56
|
+
if sig > 0:
|
|
57
|
+
return LongAction(sig=sig)
|
|
58
|
+
elif sig < 0:
|
|
59
|
+
return ShortAction(sig=sig)
|
|
60
|
+
|
|
61
|
+
if not LGM.inspection_mode:
|
|
62
|
+
LOGGER.warning(f'{self} received a confirmation of {sig=}! Which is not expected.')
|
|
63
|
+
|
|
64
|
+
return NoAction()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def signal(self):
|
|
68
|
+
return self.parent.signal
|
|
69
|
+
|
|
70
|
+
@signal.setter
|
|
71
|
+
def signal(self, value: int):
|
|
72
|
+
self.parent.signal = value
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Literal, Self, TypedDict
|
|
8
|
+
|
|
9
|
+
from . import LOGGER
|
|
10
|
+
from .base import SignalLogicGroup
|
|
11
|
+
from ..decision_tree import ActionNode, LogicMapping, LogicGroup, NodeValueError, LongAction, ShortAction, LGM, NoAction
|
|
12
|
+
|
|
13
|
+
LOGGER = LOGGER.getChild('request')
|
|
14
|
+
|
|
15
|
+
__all__ = ['RequestAction', 'PendingRequest', 'RequestConfirmed', 'RequestDenied', 'RequestRegistered', 'DelayedConfirmationLogicGroup']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StateMapping(TypedDict):
|
|
19
|
+
timestamp: float
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequestAction(enum.StrEnum):
|
|
23
|
+
open = enum.auto()
|
|
24
|
+
unwind = enum.auto()
|
|
25
|
+
idle = enum.auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PendingRequest(dict):
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
timestamp: float,
|
|
34
|
+
sig: Literal[-1, 1] | int,
|
|
35
|
+
action: str,
|
|
36
|
+
timeout: float,
|
|
37
|
+
logic_group: LogicGroup = None,
|
|
38
|
+
uid: uuid.UUID = None,
|
|
39
|
+
**kwargs
|
|
40
|
+
):
|
|
41
|
+
super().__init__(
|
|
42
|
+
name=name,
|
|
43
|
+
timestamp=timestamp,
|
|
44
|
+
sig=sig,
|
|
45
|
+
timeout=timeout,
|
|
46
|
+
action=RequestAction(action),
|
|
47
|
+
uid=uuid.uuid4() if uid is None else uid,
|
|
48
|
+
**kwargs
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.logic_group = logic_group
|
|
52
|
+
|
|
53
|
+
def reset(self) -> PendingRequest:
|
|
54
|
+
self.update(
|
|
55
|
+
name='DummyRequest',
|
|
56
|
+
timestamp=0,
|
|
57
|
+
sig=0,
|
|
58
|
+
action=RequestAction.idle,
|
|
59
|
+
timeout=0,
|
|
60
|
+
uid=uuid.UUID(int=0)
|
|
61
|
+
)
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def empty(cls) -> PendingRequest:
|
|
66
|
+
return PendingRequest(
|
|
67
|
+
name='DummyRequest',
|
|
68
|
+
timestamp=0,
|
|
69
|
+
sig=0,
|
|
70
|
+
action=RequestAction.idle,
|
|
71
|
+
timeout=0,
|
|
72
|
+
uid=uuid.UUID(int=0)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def __bool__(self):
|
|
76
|
+
if not self.sig:
|
|
77
|
+
return False
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def name(self) -> str:
|
|
82
|
+
return self['name']
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def timestamp(self) -> float:
|
|
86
|
+
return self['timestamp']
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def sig(self) -> int:
|
|
90
|
+
return self['sig']
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def timeout(self) -> float:
|
|
94
|
+
return self['timeout']
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def action(self) -> RequestAction:
|
|
98
|
+
return self['action']
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def uid(self) -> uuid.UUID:
|
|
102
|
+
return self['uid']
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RequestConfirmed(ActionNode):
|
|
106
|
+
def __init__(self, sig: Literal[-1, 1], req: PendingRequest, auto_connect: bool = True):
|
|
107
|
+
super().__init__(
|
|
108
|
+
repr=f'<Pending Request Confirmed {sig=}>',
|
|
109
|
+
auto_connect=auto_connect
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.sig = sig
|
|
113
|
+
self.req = req
|
|
114
|
+
|
|
115
|
+
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
116
|
+
sig = self.sig
|
|
117
|
+
|
|
118
|
+
if sig > 0:
|
|
119
|
+
return LongAction(sig=sig, auto_connect=False)
|
|
120
|
+
elif sig < 0:
|
|
121
|
+
return ShortAction(sig=sig, auto_connect=False)
|
|
122
|
+
|
|
123
|
+
if not LGM.inspection_mode:
|
|
124
|
+
LOGGER.warning(f'{self} received a confirmation of {sig=}! Which is not expected.')
|
|
125
|
+
|
|
126
|
+
return NoAction(auto_connect=False)
|
|
127
|
+
|
|
128
|
+
def _post_eval(self) -> Self:
|
|
129
|
+
self.req.reset()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RequestDenied(ActionNode):
|
|
133
|
+
def __init__(self, req: PendingRequest, auto_connect: bool = True):
|
|
134
|
+
super().__init__(
|
|
135
|
+
repr='<Pending Request Denied>',
|
|
136
|
+
auto_connect=auto_connect
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self.req = req
|
|
140
|
+
|
|
141
|
+
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
142
|
+
return NoAction(auto_connect=False)
|
|
143
|
+
|
|
144
|
+
def _post_eval(self) -> Self:
|
|
145
|
+
self.req.reset()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RequestRegistered(ActionNode):
|
|
149
|
+
def __init__(self, sig: Literal[-1, 1], req: PendingRequest, state: StateMapping | Mapping | LogicMapping = None, action: RequestAction = RequestAction.open, timeout: float = float('inf'), auto_connect: bool = True):
|
|
150
|
+
super().__init__(
|
|
151
|
+
action=self._registered,
|
|
152
|
+
repr=f'<Pending Request Registered {sig=}>',
|
|
153
|
+
auto_connect=auto_connect
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.state = state
|
|
157
|
+
self.req = req
|
|
158
|
+
|
|
159
|
+
self.sig = sig
|
|
160
|
+
self.action = action
|
|
161
|
+
self.timeout = timeout
|
|
162
|
+
|
|
163
|
+
def eval(self, enforce_dtype: bool = False) -> ActionNode:
|
|
164
|
+
return NoAction(auto_connect=False)
|
|
165
|
+
|
|
166
|
+
def _registered(self) -> Self:
|
|
167
|
+
sig = self.sig
|
|
168
|
+
timestamp = time.time() if self.state is None else self.state['timestamp']
|
|
169
|
+
action = self.action
|
|
170
|
+
timeout = self.timeout
|
|
171
|
+
uid = uuid.uuid4()
|
|
172
|
+
|
|
173
|
+
if self.sig > 0:
|
|
174
|
+
name = 'state.long'
|
|
175
|
+
elif self.sig < 0:
|
|
176
|
+
name = 'state.short'
|
|
177
|
+
else:
|
|
178
|
+
raise NodeValueError('Signal Must not be zero.')
|
|
179
|
+
|
|
180
|
+
self.req.update(
|
|
181
|
+
name=name,
|
|
182
|
+
timestamp=timestamp,
|
|
183
|
+
sig=sig,
|
|
184
|
+
timeout=timeout,
|
|
185
|
+
action=action,
|
|
186
|
+
uid=uid
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class DelayedConfirmationLogicGroup(SignalLogicGroup):
|
|
193
|
+
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
194
|
+
super().__init__(
|
|
195
|
+
name=f'{parent.name}.Delayed' if name is None else name,
|
|
196
|
+
parent=parent
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self.req = self.contexts['pending_request'] = PendingRequest.empty()
|
|
200
|
+
|
|
201
|
+
def register(self, sig: Literal[1, -1], state: StateMapping | Mapping | LogicMapping = None, timeout: float = float('inf'), action: RequestAction | str = RequestAction.open) -> RequestRegistered:
|
|
202
|
+
action_register = RequestRegistered(
|
|
203
|
+
sig=sig,
|
|
204
|
+
req=self.req,
|
|
205
|
+
state=state,
|
|
206
|
+
action=RequestAction(action),
|
|
207
|
+
timeout=timeout
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return action_register
|
|
211
|
+
|
|
212
|
+
def confirm(self, sig: Literal[1, -1]) -> RequestConfirmed:
|
|
213
|
+
action_confirm = RequestConfirmed(req=self.req, sig=sig)
|
|
214
|
+
return action_confirm
|
|
215
|
+
|
|
216
|
+
def deny(self) -> RequestDenied:
|
|
217
|
+
action_deny = RequestDenied(req=self.req)
|
|
218
|
+
return action_deny
|
|
219
|
+
|
|
220
|
+
def reset(self):
|
|
221
|
+
self.pending_request.reset()
|
|
222
|
+
super().reset()
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def action(self) -> RequestAction:
|
|
226
|
+
return self.pending_request.action
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def pending_request(self) -> LogicMapping:
|
|
230
|
+
req = self.req
|
|
231
|
+
|
|
232
|
+
m = LogicMapping(
|
|
233
|
+
data=req,
|
|
234
|
+
name=f'{self.name}.PendingRequest',
|
|
235
|
+
logic_group=self
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return m
|
|
239
|
+
|
|
240
|
+
@pending_request.setter
|
|
241
|
+
def pending_request(self, value: PendingRequest):
|
|
242
|
+
LOGGER.warning('Assigning pending request will break the reference of previous generated decision graph. Use with caution.')
|
|
243
|
+
assert isinstance(value, PendingRequest)
|
|
244
|
+
self.req = self.contexts['pending_request'] = value
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def signal(self):
|
|
248
|
+
return self.parent.signal
|
|
249
|
+
|
|
250
|
+
@signal.setter
|
|
251
|
+
def signal(self, value: Literal[-1, 0, 1]):
|
|
252
|
+
assert isinstance(value, (int, float))
|
|
253
|
+
self.parent.signal = value
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: PyDecisionGraph
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A rule-based decision tree implementation for Python
|
|
5
|
+
Home-page: https://github.com/BolunHan/PyDecisionGraph
|
|
6
|
+
Author: Han Bolun
|
|
7
|
+
Author-email: Han Bolun <Bolun.Han@outlook.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/BolunHan/PyDecisionGraph
|
|
10
|
+
Project-URL: Repository, https://github.com/BolunHan/PyDecisionGraph
|
|
11
|
+
Project-URL: Issues, https://github.com/BolunHan/PyDecisionGraph/issues
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: docs
|
|
20
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
22
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "docs"
|
|
23
|
+
Provides-Extra: visualization
|
|
24
|
+
Requires-Dist: pyvis; extra == "visualization"
|
|
25
|
+
Requires-Dist: networkx; extra == "visualization"
|
|
26
|
+
|
|
27
|
+
# PyDecisionGraph
|
|
28
|
+
|
|
29
|
+
`PyDecisionGraph` is an easy-to-use library to create custom decision trees, primarily designed for trading and financial decision-making. This package helps you build rule-based decision processes and visualize them effectively.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
To install the package, run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install PyDecisionGraph
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
# Requirements
|
|
40
|
+
|
|
41
|
+
- Python 3.12 or higher
|
|
42
|
+
|
|
43
|
+
# Documentation
|
|
44
|
+
|
|
45
|
+
For detailed documentation, visit https://pydecisiongraph.readthedocs.io/.
|
|
46
|
+
|
|
47
|
+
# Quick Start
|
|
48
|
+
|
|
49
|
+
Here is a quick demo on how to use `PyDecisionGraph` for building a decision tree based on various conditions:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from decision_graph.decision_tree import LogicNode, LOGGER, AttrExpression, LongAction, ShortAction, NoAction, RootLogicNode, LogicMapping
|
|
53
|
+
|
|
54
|
+
# Mapping of attribute names to their values
|
|
55
|
+
LogicMapping.AttrExpression = AttrExpression
|
|
56
|
+
|
|
57
|
+
state = {
|
|
58
|
+
"exposure": 0, # Current exposure
|
|
59
|
+
"working_order": 0, # Current working order
|
|
60
|
+
"up_prob": 0.8, # Probability of price going up
|
|
61
|
+
"down_prob": 0.2, # Probability of price going down
|
|
62
|
+
"volatility": 0.24, # Current market volatility
|
|
63
|
+
"ttl": 15.3 # Time to live (TTL) of the decision tree
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Root of the logic tree
|
|
67
|
+
with RootLogicNode() as root:
|
|
68
|
+
# Define root logic mapping with state data
|
|
69
|
+
with LogicMapping(name='Root', data=state) as lg_root:
|
|
70
|
+
lg_root: LogicMapping
|
|
71
|
+
|
|
72
|
+
# Condition for zero exposure
|
|
73
|
+
with lg_root.exposure == 0:
|
|
74
|
+
root: LogicNode
|
|
75
|
+
with LogicMapping(name='check_open', data=state) as lg:
|
|
76
|
+
with lg.working_order != 0:
|
|
77
|
+
break_point = NoAction() # No action if there's a working order
|
|
78
|
+
lg.break_(scope=lg) # Exit the current scope
|
|
79
|
+
|
|
80
|
+
with lg.volatility > 0.25: # Check if volatility is high
|
|
81
|
+
with lg.down_prob > 0.1: # Action for down probability
|
|
82
|
+
LongAction()
|
|
83
|
+
|
|
84
|
+
with lg.up_prob < -0.1: # Action for up probability
|
|
85
|
+
ShortAction()
|
|
86
|
+
|
|
87
|
+
# Condition when TTL is greater than 30
|
|
88
|
+
with lg_root.ttl > 30:
|
|
89
|
+
with lg_root.working_order > 0:
|
|
90
|
+
ShortAction() # Action to short if working order exists
|
|
91
|
+
LongAction() # Always take long action
|
|
92
|
+
lg_root.break_(scope=lg_root) # Exit scope
|
|
93
|
+
|
|
94
|
+
# Closing logic based on exposure and probabilities
|
|
95
|
+
with LogicMapping(name='check_close', data=state) as lg:
|
|
96
|
+
with (lg.exposure > 0) & (lg.down_prob > 0.):
|
|
97
|
+
ShortAction() # Short action for positive exposure and down probability
|
|
98
|
+
|
|
99
|
+
with (lg.exposure < 0) & (lg.up_prob > 0.):
|
|
100
|
+
LongAction() # Long action for negative exposure and up probability
|
|
101
|
+
|
|
102
|
+
# Visualize the decision tree
|
|
103
|
+
root.to_html()
|
|
104
|
+
|
|
105
|
+
# Log the evaluation result
|
|
106
|
+
LOGGER.info(root())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Explanation of the Script:
|
|
110
|
+
|
|
111
|
+
- LogicNode & LogicMapping:
|
|
112
|
+
- LogicNode: Represents a node in the decision tree where conditions are evaluated.
|
|
113
|
+
- LogicMapping: Associates logical conditions with the state (data) used in decision-making.
|
|
114
|
+
|
|
115
|
+
- State:
|
|
116
|
+
- A dictionary containing the variables used for decision-making, such as exposure, working_order, up_prob, etc.
|
|
117
|
+
|
|
118
|
+
- RootLogicNode:
|
|
119
|
+
- The entry point for the decision tree where all logical decisions are linked.
|
|
120
|
+
|
|
121
|
+
- Decision Conditions:
|
|
122
|
+
- Inside each with block, logical conditions are evaluated (e.g., lg.volatility > 0.25, lg.up_prob < -0.1) to determine which action to take.
|
|
123
|
+
- Actions like LongAction() or ShortAction() are taken based on the conditions.
|
|
124
|
+
|
|
125
|
+
- Action Handling:
|
|
126
|
+
- LongAction(), ShortAction(), and NoAction() represent different actions you can trigger in the decision tree based on the conditions.
|
|
127
|
+
|
|
128
|
+
- Logging:
|
|
129
|
+
- The result of the tree evaluation is logged using the LOGGER object, which outputs to the console.
|
|
130
|
+
|
|
131
|
+
- Visualization:
|
|
132
|
+
- root.to_html() generates an HTML representation of the decision tree for visualization.
|
|
133
|
+
|
|
134
|
+
# Features
|
|
135
|
+
|
|
136
|
+
- Easily define custom decision rules.
|
|
137
|
+
- Actionable outcomes like LongAction, ShortAction, and NoAction.
|
|
138
|
+
- Log outputs for debugging and tracking.
|
|
139
|
+
- Visualize decision paths through HTML export.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
# Contributing
|
|
144
|
+
|
|
145
|
+
Feel free to fork, submit issues, and create pull requests. Contributions are always welcome!
|
|
146
|
+
|
|
147
|
+
# License
|
|
148
|
+
|
|
149
|
+
This project is licensed under the Mozilla Public License 2.0 - see the LICENSE file for details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
decision_graph/__init__.py,sha256=4QpoJ4PAJKXqLFITUOq55uOILaN60NWWz6q-FIBhYH4,386
|
|
2
|
+
decision_graph/decision_tree/__init__.py,sha256=S1UVgQ_wj1QQ9SkVkVvVOLKA_kjulvuo56EcBIt6spM,1512
|
|
3
|
+
decision_graph/decision_tree/abc.py,sha256=28tGBgXicJ802oLCPQm0Gvcm54mB1pMpbHimnt2L1MU,42114
|
|
4
|
+
decision_graph/decision_tree/collection.py,sha256=PLb58A_SS7vLRJ0l-T1OaZFHYhAHjdwij68uP_HpL70,2242
|
|
5
|
+
decision_graph/decision_tree/exc.py,sha256=THgIIP8S2PyJ9HIyUbNRhKT1umHfmf3Nh4rYrV8ywXU,646
|
|
6
|
+
decision_graph/decision_tree/expression.py,sha256=_bd1U2tjHnCydoC1Ya6RwTasRbKweMzUsghXsDe4ZpA,16777
|
|
7
|
+
decision_graph/decision_tree/node.py,sha256=x69Fa3izFQZRU_4urwYGorTR_eNAOr2IbKU7P5eeCnI,8721
|
|
8
|
+
decision_graph/logic_group/__init__.py,sha256=7lnQfCbVdgP3DtsCwFJ3Ht1Ig9LKONgdX2zvHT3gT1M,528
|
|
9
|
+
decision_graph/logic_group/base.py,sha256=F3eUO5nE4mB11f_KiSMpiYGFUAf947-2ESxbzskHT2U,1942
|
|
10
|
+
decision_graph/logic_group/pending_request.py,sha256=0Caip4FGFqdqiw1VaRlASDJv57ExQnAvlq3PQB1NCr8,6888
|
|
11
|
+
pydecisiongraph-0.1.2.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
12
|
+
pydecisiongraph-0.1.2.dist-info/METADATA,sha256=pjalVP0bT4Z1Rkx9Qki7f-SED8NxedVl27Qfhz1hWNU,5456
|
|
13
|
+
pydecisiongraph-0.1.2.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
14
|
+
pydecisiongraph-0.1.2.dist-info/top_level.txt,sha256=3_p81U9qj9sTh1K6a1ZE-JNv7LKHaCeavuzci28VjFk,15
|
|
15
|
+
pydecisiongraph-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
decision_graph
|
decision_tree/__init__.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
LOGGER = logging.getLogger("DecisionTree")
|
|
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
|
-
|
|
17
|
-
def set_logger(logger: logging.Logger):
|
|
18
|
-
global LOGGER
|
|
19
|
-
LOGGER = logger
|
|
20
|
-
|
|
21
|
-
exc.LOGGER = logger.getChild('TradeUtils')
|
|
22
|
-
abc.LOGGER = logger.getChild('TA')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
NODE_MODEL = True
|
|
26
|
-
|
|
27
|
-
from .exc import *
|
|
28
|
-
from .abc import *
|
|
29
|
-
|
|
30
|
-
if NODE_MODEL:
|
|
31
|
-
from .node import *
|
|
32
|
-
else:
|
|
33
|
-
from .expression import *
|
|
34
|
-
|
|
35
|
-
from .collection import *
|
|
36
|
-
from .logic_group import *
|
decision_tree/logic_group.py
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import enum
|
|
4
|
-
import uuid
|
|
5
|
-
from typing import Literal, Any, Self
|
|
6
|
-
|
|
7
|
-
from . import AttrExpression, LogicMapping
|
|
8
|
-
from .abc import LogicGroup, SkipContextsBlock
|
|
9
|
-
|
|
10
|
-
__all__ = ['SignalLogicGroup', 'InstantConfirmationLogicGroup', 'RequestAction', 'PendingRequest', 'DelayedConfirmationLogicGroup', 'RacingConfirmationLogicGroup', 'BarrierConfirmationLogicGroup']
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SignalLogicGroup(LogicGroup):
|
|
14
|
-
def __init__(self, name: str, parent: Self = None, contexts: dict[str, Any] = None):
|
|
15
|
-
super().__init__(name=name, parent=parent, contexts=contexts)
|
|
16
|
-
|
|
17
|
-
def get(self, attr: str, dtype: type = None, repr: str = None):
|
|
18
|
-
"""
|
|
19
|
-
Retrieve an attribute as a LogicExpression.
|
|
20
|
-
"""
|
|
21
|
-
return AttrExpression(attr=attr, logic_group=self, dtype=dtype, repr=repr)
|
|
22
|
-
|
|
23
|
-
def reset(self):
|
|
24
|
-
self.signal = 0
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def signal(self):
|
|
28
|
-
return self.contexts.get('signal', 0)
|
|
29
|
-
|
|
30
|
-
@signal.setter
|
|
31
|
-
def signal(self, value: int):
|
|
32
|
-
self.contexts['signal'] = value
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class InstantConfirmationLogicGroup(SignalLogicGroup):
|
|
36
|
-
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
37
|
-
super().__init__(
|
|
38
|
-
name=f'{parent.name}.Instant' if name is None else name,
|
|
39
|
-
parent=parent
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
def reset(self):
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
def confirm(self, sig: Literal[-1, 1]):
|
|
46
|
-
self.signal = sig
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def signal(self):
|
|
51
|
-
return self.parent.signal
|
|
52
|
-
|
|
53
|
-
@signal.setter
|
|
54
|
-
def signal(self, value: int):
|
|
55
|
-
self.parent.signal = value
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class RequestAction(enum.StrEnum):
|
|
59
|
-
open = enum.auto()
|
|
60
|
-
unwind = enum.auto()
|
|
61
|
-
idle = enum.auto()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class PendingRequest(dict):
|
|
65
|
-
class Skip(Exception):
|
|
66
|
-
pass
|
|
67
|
-
|
|
68
|
-
def __init__(
|
|
69
|
-
self,
|
|
70
|
-
name: str | RequestAction,
|
|
71
|
-
timestamp: float,
|
|
72
|
-
sig: Literal[-1, 1] | int,
|
|
73
|
-
action: str,
|
|
74
|
-
timeout: float,
|
|
75
|
-
logic_group: LogicGroup = None,
|
|
76
|
-
uid: uuid.UUID = None,
|
|
77
|
-
**kwargs
|
|
78
|
-
):
|
|
79
|
-
super().__init__(
|
|
80
|
-
name=name,
|
|
81
|
-
timestamp=timestamp,
|
|
82
|
-
sig=sig,
|
|
83
|
-
timeout=timeout,
|
|
84
|
-
action=RequestAction(action),
|
|
85
|
-
uid=uuid.uuid4() if uid is None else uid,
|
|
86
|
-
**kwargs
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
self.logic_group = logic_group
|
|
90
|
-
|
|
91
|
-
@classmethod
|
|
92
|
-
def empty(cls) -> PendingRequest:
|
|
93
|
-
return PendingRequest(
|
|
94
|
-
name='DummyRequest',
|
|
95
|
-
timestamp=0,
|
|
96
|
-
sig=0,
|
|
97
|
-
action=RequestAction.idle,
|
|
98
|
-
timeout=0,
|
|
99
|
-
uid=uuid.UUID(int=0)
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
def __bool__(self):
|
|
103
|
-
if not self.sig:
|
|
104
|
-
return False
|
|
105
|
-
return True
|
|
106
|
-
|
|
107
|
-
@property
|
|
108
|
-
def name(self) -> str:
|
|
109
|
-
return self['name']
|
|
110
|
-
|
|
111
|
-
@property
|
|
112
|
-
def timestamp(self) -> float:
|
|
113
|
-
return self['timestamp']
|
|
114
|
-
|
|
115
|
-
@property
|
|
116
|
-
def sig(self) -> int:
|
|
117
|
-
return self['sig']
|
|
118
|
-
|
|
119
|
-
@property
|
|
120
|
-
def timeout(self) -> float:
|
|
121
|
-
return self['timeout']
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def action(self) -> RequestAction:
|
|
125
|
-
return self['action']
|
|
126
|
-
|
|
127
|
-
@property
|
|
128
|
-
def uid(self) -> uuid.UUID:
|
|
129
|
-
return self['uid']
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class DelayedConfirmationLogicGroup(SignalLogicGroup):
|
|
133
|
-
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
134
|
-
super().__init__(
|
|
135
|
-
name=f'{parent.name}.Delayed' if name is None else name,
|
|
136
|
-
parent=parent
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
def register(self, name: str, timestamp: float, sig: Literal[1, -1], timeout: float | None, action: Literal['open', 'unwind'] = 'open', uid: uuid.UUID = None, **kwargs):
|
|
140
|
-
req = self.pending_request = PendingRequest(
|
|
141
|
-
name=name,
|
|
142
|
-
logic_group=self,
|
|
143
|
-
timestamp=timestamp,
|
|
144
|
-
sig=sig,
|
|
145
|
-
timeout=timeout,
|
|
146
|
-
action=action,
|
|
147
|
-
uid=uid,
|
|
148
|
-
**kwargs
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
return req
|
|
152
|
-
|
|
153
|
-
def confirm(self):
|
|
154
|
-
req = self.contexts.get('pending_request')
|
|
155
|
-
sig = 0 if req is None else req.sig
|
|
156
|
-
self.reset()
|
|
157
|
-
self.signal = sig
|
|
158
|
-
return sig
|
|
159
|
-
|
|
160
|
-
def deny(self):
|
|
161
|
-
# denying all the pending request
|
|
162
|
-
self.reset()
|
|
163
|
-
return 0
|
|
164
|
-
|
|
165
|
-
def reset(self):
|
|
166
|
-
# self.pending_request = PendingRequest.empty()
|
|
167
|
-
self.contexts.pop('pending_request', None)
|
|
168
|
-
super().reset()
|
|
169
|
-
|
|
170
|
-
@property
|
|
171
|
-
def action(self) -> RequestAction:
|
|
172
|
-
if 'pending_request' in self.contexts:
|
|
173
|
-
return self.pending_request.action
|
|
174
|
-
|
|
175
|
-
return RequestAction.idle
|
|
176
|
-
|
|
177
|
-
@property
|
|
178
|
-
def pending_request(self) -> LogicMapping | SkipContextsBlock:
|
|
179
|
-
|
|
180
|
-
if (req := self.contexts.get('pending_request')) is None:
|
|
181
|
-
return SkipContextsBlock(True)
|
|
182
|
-
|
|
183
|
-
m = LogicMapping(
|
|
184
|
-
data=req,
|
|
185
|
-
name=f'{self.name}.PendingRequest.{req.uid.hex}',
|
|
186
|
-
logic_group=self
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
return m
|
|
190
|
-
|
|
191
|
-
@pending_request.setter
|
|
192
|
-
def pending_request(self, value: PendingRequest):
|
|
193
|
-
assert isinstance(value, PendingRequest)
|
|
194
|
-
self.contexts['pending_request'] = value
|
|
195
|
-
|
|
196
|
-
@property
|
|
197
|
-
def signal(self):
|
|
198
|
-
return self.parent.signal
|
|
199
|
-
|
|
200
|
-
@signal.setter
|
|
201
|
-
def signal(self, value: Literal[-1, 0, 1]):
|
|
202
|
-
assert isinstance(value, (int, float))
|
|
203
|
-
self.parent.signal = value
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
class RacingConfirmationLogicGroup(DelayedConfirmationLogicGroup):
|
|
207
|
-
|
|
208
|
-
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
209
|
-
super().__init__(
|
|
210
|
-
name=f'{parent.name}.Racing' if name is None else name,
|
|
211
|
-
parent=parent
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
def __getitem__(self, uid: uuid.UUID | str | bytes | int) -> PendingRequest:
|
|
215
|
-
if not (request_pool := self.pending_request):
|
|
216
|
-
raise KeyError(f'uid {uid} not found!')
|
|
217
|
-
|
|
218
|
-
match uid:
|
|
219
|
-
case uuid.UUID():
|
|
220
|
-
for _pending_request in request_pool:
|
|
221
|
-
if _pending_request.uid == uid:
|
|
222
|
-
return _pending_request
|
|
223
|
-
raise KeyError(f'uid {uid} not found!')
|
|
224
|
-
case str():
|
|
225
|
-
for _pending_request in request_pool:
|
|
226
|
-
if _pending_request.uid.hex == uid:
|
|
227
|
-
return _pending_request
|
|
228
|
-
raise KeyError(f'uid {uid} not found!')
|
|
229
|
-
case bytes():
|
|
230
|
-
for _pending_request in request_pool:
|
|
231
|
-
if _pending_request.uid.bytes == uid:
|
|
232
|
-
return _pending_request
|
|
233
|
-
raise KeyError(f'uid {uid} not found!')
|
|
234
|
-
case int():
|
|
235
|
-
return request_pool[uid]
|
|
236
|
-
case _:
|
|
237
|
-
raise TypeError(f'Invalid uid {uid}! Expected UUID or bytes or str!')
|
|
238
|
-
|
|
239
|
-
def register(self, name: str, timestamp: float, sig: Literal[1, -1], timeout: float, action: Literal['open', 'unwind'] = 'open', uid: uuid.UUID = None, **kwargs):
|
|
240
|
-
self.pending_request.append(
|
|
241
|
-
PendingRequest(
|
|
242
|
-
name=name,
|
|
243
|
-
timestamp=timestamp,
|
|
244
|
-
sig=sig,
|
|
245
|
-
timeout=timeout,
|
|
246
|
-
action=action,
|
|
247
|
-
uid=uid,
|
|
248
|
-
**kwargs
|
|
249
|
-
)
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
def confirm(self, pending_request: PendingRequest = None, uid: uuid.UUID = None):
|
|
253
|
-
if pending_request is None and uid is None:
|
|
254
|
-
assert len(self.pending_request) == 1, ValueError('Multiple pending requests found! Must assign uid or pending_request instance!')
|
|
255
|
-
pending_request = self.pending_request[0]
|
|
256
|
-
elif pending_request is None:
|
|
257
|
-
pending_request = self.__getitem__(uid=uid)
|
|
258
|
-
|
|
259
|
-
sig = pending_request.sig
|
|
260
|
-
self.reset()
|
|
261
|
-
self.signal = sig
|
|
262
|
-
return sig
|
|
263
|
-
|
|
264
|
-
def deny(self, pending_request: PendingRequest = None, uid: uuid.UUID = None):
|
|
265
|
-
# denying all the pending request
|
|
266
|
-
if pending_request is None and uid is None:
|
|
267
|
-
self.pending_request.clear()
|
|
268
|
-
self.signal = 0
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
if pending_request is not None:
|
|
272
|
-
self.pending_request.remove(pending_request)
|
|
273
|
-
self.signal = 0
|
|
274
|
-
|
|
275
|
-
if uid is not None:
|
|
276
|
-
pending_request = self.__getitem__(uid=uid)
|
|
277
|
-
self.pending_request.remove(pending_request)
|
|
278
|
-
self.signal = 0
|
|
279
|
-
|
|
280
|
-
@property
|
|
281
|
-
def pending_request(self) -> list[PendingRequest]:
|
|
282
|
-
return self.contexts.setdefault('pending_request', [])
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
class BarrierConfirmationLogicGroup(RacingConfirmationLogicGroup):
|
|
286
|
-
|
|
287
|
-
def __init__(self, parent: SignalLogicGroup, name: str = None):
|
|
288
|
-
super().__init__(
|
|
289
|
-
name=f'{parent.name}.Barrier' if name is None else name,
|
|
290
|
-
parent=parent
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
def confirm(self, pending_request: PendingRequest = None, uid: uuid.UUID = None):
|
|
294
|
-
if pending_request is None and uid is None:
|
|
295
|
-
assert len(self.pending_request) == 1, ValueError('Multiple pending requests found! Must assign uid or pending_request instance!')
|
|
296
|
-
pending_request = self.pending_request[0]
|
|
297
|
-
elif pending_request is None:
|
|
298
|
-
pending_request = self.__getitem__(uid=uid)
|
|
299
|
-
|
|
300
|
-
self.pending_request.remove(pending_request)
|
|
301
|
-
|
|
302
|
-
if self.pending_request:
|
|
303
|
-
return 0
|
|
304
|
-
|
|
305
|
-
sig = pending_request.sig
|
|
306
|
-
self.signal = sig
|
|
307
|
-
return sig
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: PyDecisionGraph
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A rule-based decision tree implementation for Python
|
|
5
|
-
Home-page: https://github.com/BolunHan/PyDecisionTree
|
|
6
|
-
Author: Han Bolun
|
|
7
|
-
Author-email: Han Bolun <Bolun.Han@outlook.com>
|
|
8
|
-
License: MIT
|
|
9
|
-
Project-URL: Homepage, https://github.com/BolunHan/PyDecisionTree
|
|
10
|
-
Project-URL: Repository, https://github.com/BolunHan/PyDecisionTree
|
|
11
|
-
Project-URL: Issues, https://github.com/BolunHan/PyDecisionTree/issues
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Requires-Python: >=3.12
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
|
|
20
|
-
# PyDecisionTree
|
|
21
|
-
A easy way to create custom decision tree
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
decision_tree/__init__.py,sha256=AFHLZX4P9b6woOM78Uw5HIo83iqk9IP87quzxgsCVUM,739
|
|
2
|
-
decision_tree/abc.py,sha256=yV9EraSbsDAAT01_TQVf0iuCbcE9hFCQKvQzui5CuKs,40439
|
|
3
|
-
decision_tree/collection.py,sha256=K9fob0e7493BSXik0zfPzVSFupQezz1zvABYvMAo27k,2403
|
|
4
|
-
decision_tree/exc.py,sha256=THgIIP8S2PyJ9HIyUbNRhKT1umHfmf3Nh4rYrV8ywXU,646
|
|
5
|
-
decision_tree/expression.py,sha256=_bd1U2tjHnCydoC1Ya6RwTasRbKweMzUsghXsDe4ZpA,16777
|
|
6
|
-
decision_tree/logic_group.py,sha256=-PkZJ9VzJf3WXRNmlepGeSRXBdf0K0o9yBr6xxeqvzI,9219
|
|
7
|
-
decision_tree/node.py,sha256=eGxI3l7_zQMhbTtXgNnrFhKMDVvkKtJOR0_d9vfzyyY,8598
|
|
8
|
-
pydecisiongraph-0.1.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
9
|
-
pydecisiongraph-0.1.0.dist-info/METADATA,sha256=LDfkD50rVrkYoSbnxwVuaiLbFjY3G2hOkhiMfp7VQQE,827
|
|
10
|
-
pydecisiongraph-0.1.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
11
|
-
pydecisiongraph-0.1.0.dist-info/top_level.txt,sha256=bpPqmvSipqlIOvoSo-_Vc2muvqyhrfKbE6Zp-Km0J7o,14
|
|
12
|
-
pydecisiongraph-0.1.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
decision_tree
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|