PyDecisionGraph 0.1.0__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_tree/__init__.py +36 -0
- decision_tree/abc.py +1147 -0
- decision_tree/collection.py +93 -0
- decision_tree/exc.py +38 -0
- decision_tree/expression.py +478 -0
- decision_tree/logic_group.py +307 -0
- decision_tree/node.py +180 -0
- pydecisiongraph-0.1.0.dist-info/LICENSE +373 -0
- pydecisiongraph-0.1.0.dist-info/METADATA +21 -0
- pydecisiongraph-0.1.0.dist-info/RECORD +12 -0
- pydecisiongraph-0.1.0.dist-info/WHEEL +5 -0
- pydecisiongraph-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import AttrExpression as _AE
|
|
7
|
+
from .abc import LogicGroup, ExpressionCollection
|
|
8
|
+
|
|
9
|
+
__all__ = ['LogicMapping', 'LogicGenerator']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogicMapping(ExpressionCollection):
|
|
13
|
+
AttrExpression = _AE
|
|
14
|
+
|
|
15
|
+
def __init__(self, data: dict, name: str, repr: str = None, logic_group: LogicGroup = None):
|
|
16
|
+
if data is None:
|
|
17
|
+
data = {}
|
|
18
|
+
|
|
19
|
+
if not isinstance(data, Mapping):
|
|
20
|
+
raise TypeError("The 'data' parameter must be a mapping!.")
|
|
21
|
+
|
|
22
|
+
super().__init__(
|
|
23
|
+
data=data,
|
|
24
|
+
name=name,
|
|
25
|
+
repr=repr,
|
|
26
|
+
logic_group=logic_group
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def __bool__(self):
|
|
30
|
+
return bool(self.data)
|
|
31
|
+
|
|
32
|
+
def __len__(self):
|
|
33
|
+
return self.data.__len__()
|
|
34
|
+
|
|
35
|
+
def __getitem__(self, key: str):
|
|
36
|
+
return self.AttrExpression(attr=key, logic_group=self)
|
|
37
|
+
|
|
38
|
+
def __getattr__(self, key: str):
|
|
39
|
+
return self.AttrExpression(attr=key, logic_group=self)
|
|
40
|
+
|
|
41
|
+
def reset(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def update(self, data: dict = None, **kwargs):
|
|
45
|
+
if data is None:
|
|
46
|
+
self.data.update(**kwargs)
|
|
47
|
+
else:
|
|
48
|
+
self.data.update(data, **kwargs)
|
|
49
|
+
|
|
50
|
+
def clear(self):
|
|
51
|
+
self.data.clear()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LogicGenerator(ExpressionCollection):
|
|
55
|
+
AttrExpression = _AE
|
|
56
|
+
|
|
57
|
+
def __init__(self, data: list[Any], name: str, repr: str = None, logic_group: LogicGroup = None):
|
|
58
|
+
if data is None:
|
|
59
|
+
data = []
|
|
60
|
+
|
|
61
|
+
if not isinstance(data, Sequence):
|
|
62
|
+
raise TypeError("The 'data' parameter must be a sequence!.")
|
|
63
|
+
|
|
64
|
+
super().__init__(
|
|
65
|
+
data=data,
|
|
66
|
+
name=name,
|
|
67
|
+
repr=repr,
|
|
68
|
+
logic_group=logic_group
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.data = self.contexts.setdefault('data', data)
|
|
72
|
+
|
|
73
|
+
def __iter__(self):
|
|
74
|
+
for index, value in enumerate(self.data):
|
|
75
|
+
# if isinstance(value, ContextLogicExpression):
|
|
76
|
+
# yield value
|
|
77
|
+
|
|
78
|
+
yield self.AttrExpression(attr=index, logic_group=self)
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
return len(self.data)
|
|
82
|
+
|
|
83
|
+
def __getitem__(self, index: int):
|
|
84
|
+
return self.AttrExpression(attr=index, logic_group=self)
|
|
85
|
+
|
|
86
|
+
def append(self, value):
|
|
87
|
+
self.data.append(value)
|
|
88
|
+
|
|
89
|
+
def remove(self, value):
|
|
90
|
+
self.data.remove(value)
|
|
91
|
+
|
|
92
|
+
def clear(self):
|
|
93
|
+
return self.data.clear()
|
decision_tree/exc.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
__all__ = ['NodeError', 'TooManyChildren', 'TooFewChildren', 'NodeNotFountError', 'NodeValueError', 'EdgeValueError', 'ResolutionError', 'ExpressFalse', 'ContextsNotFound']
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NodeError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TooManyChildren(NodeError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TooFewChildren(NodeError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NodeNotFountError(NodeError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NodeValueError(NodeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EdgeValueError(NodeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ResolutionError(NodeError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExpressFalse(Exception):
|
|
33
|
+
"""Custom exception raised when a LogicExpression evaluates to False."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContextsNotFound(Exception):
|
|
38
|
+
pass
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import builtins
|
|
5
|
+
import enum
|
|
6
|
+
import importlib
|
|
7
|
+
import json
|
|
8
|
+
import operator
|
|
9
|
+
import traceback
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from typing import Any, Self
|
|
12
|
+
|
|
13
|
+
from .abc import LogicGroup, LGM, LogicExpression, ExpressionCollection
|
|
14
|
+
from .exc import ContextsNotFound
|
|
15
|
+
|
|
16
|
+
__all__ = ['ContextLogicExpression', 'AttrExpression', 'MathExpression', 'ComparisonExpression', 'LogicalExpression']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextLogicExpression(LogicExpression, metaclass=abc.ABCMeta):
|
|
20
|
+
def __init__(self, expression: float | int | bool | Exception | Callable[[], Any], dtype: type = None, repr: str = None, logic_group: LogicGroup = None):
|
|
21
|
+
if logic_group is None:
|
|
22
|
+
if not LGM.active_logic_group is None:
|
|
23
|
+
logic_group = LGM.active_logic_group
|
|
24
|
+
else:
|
|
25
|
+
raise ContextsNotFound(f'Must assign a logic group or initialize {self.__class__.__name__} with in a LogicGroup with statement!')
|
|
26
|
+
|
|
27
|
+
super().__init__(expression=expression, dtype=dtype, repr=repr)
|
|
28
|
+
|
|
29
|
+
self.logic_group = logic_group
|
|
30
|
+
|
|
31
|
+
# magic method to invoke AttrExpression
|
|
32
|
+
def __getitem__(self, key: str) -> AttrExpression:
|
|
33
|
+
return AttrExpression(attr=key, logic_group=self.logic_group)
|
|
34
|
+
|
|
35
|
+
def __getattr__(self, key: str) -> AttrExpression:
|
|
36
|
+
return AttrExpression(attr=key, logic_group=self.logic_group)
|
|
37
|
+
|
|
38
|
+
# math operation to invoke MathExpression
|
|
39
|
+
|
|
40
|
+
def __add__(self, other: int | float | bool | Self) -> Self:
|
|
41
|
+
return MathExpression(left=self, op=MathExpression.Operator.add, right=other, logic_group=self.logic_group)
|
|
42
|
+
|
|
43
|
+
def __sub__(self, other: int | float | bool | Self) -> Self:
|
|
44
|
+
return MathExpression(left=self, op=MathExpression.Operator.sub, right=other, logic_group=self.logic_group)
|
|
45
|
+
|
|
46
|
+
def __mul__(self, other: int | float | bool | Self) -> Self:
|
|
47
|
+
return MathExpression(left=self, op=MathExpression.Operator.mul, right=other, logic_group=self.logic_group)
|
|
48
|
+
|
|
49
|
+
def __truediv__(self, other: int | float | bool | Self) -> Self:
|
|
50
|
+
return MathExpression(left=self, op=MathExpression.Operator.truediv, right=other, logic_group=self.logic_group)
|
|
51
|
+
|
|
52
|
+
def __floordiv__(self, other: int | float | bool | Self) -> Self:
|
|
53
|
+
return MathExpression(left=self, op=MathExpression.Operator.floordiv, right=other, logic_group=self.logic_group)
|
|
54
|
+
|
|
55
|
+
def __pow__(self, other: int | float | bool | Self) -> Self:
|
|
56
|
+
return MathExpression(left=self, op=MathExpression.Operator.pow, right=other, logic_group=self.logic_group)
|
|
57
|
+
|
|
58
|
+
def __neg__(self):
|
|
59
|
+
return MathExpression(left=self, op=MathExpression.Operator.neg, repr=f'-{self.repr}', logic_group=self.logic_group)
|
|
60
|
+
|
|
61
|
+
# Comparison operation to invoke ComparisonExpression
|
|
62
|
+
|
|
63
|
+
def __eq__(self, other: int | float | bool | str | Self) -> Self:
|
|
64
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.eq, right=other, logic_group=self.logic_group)
|
|
65
|
+
|
|
66
|
+
def __ne__(self, other: int | float | bool | str | Self) -> Self:
|
|
67
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.ne, right=other, logic_group=self.logic_group)
|
|
68
|
+
|
|
69
|
+
def __gt__(self, other: int | float | bool | Self) -> Self:
|
|
70
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.gt, right=other, logic_group=self.logic_group)
|
|
71
|
+
|
|
72
|
+
def __ge__(self, other: int | float | bool | Self) -> Self:
|
|
73
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.ge, right=other, logic_group=self.logic_group)
|
|
74
|
+
|
|
75
|
+
def __lt__(self, other: int | float | bool | Self) -> Self:
|
|
76
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.lt, right=other, logic_group=self.logic_group)
|
|
77
|
+
|
|
78
|
+
def __le__(self, other: int | float | bool | Self) -> Self:
|
|
79
|
+
return ComparisonExpression(left=self, op=ComparisonExpression.Operator.le, right=other, logic_group=self.logic_group)
|
|
80
|
+
|
|
81
|
+
# Logical operation to invoke LogicalExpression
|
|
82
|
+
|
|
83
|
+
def __and__(self, other: int | float | bool | Self) -> Self:
|
|
84
|
+
return LogicalExpression(left=self, op=LogicalExpression.Operator.and_, right=other, logic_group=self.logic_group)
|
|
85
|
+
|
|
86
|
+
def __or__(self, other: Self | bool) -> Self:
|
|
87
|
+
return LogicalExpression(left=self, op=LogicalExpression.Operator.or_, right=other, logic_group=self.logic_group)
|
|
88
|
+
|
|
89
|
+
def __invert__(self) -> Self:
|
|
90
|
+
return LogicalExpression(left=self, op=LogicalExpression.Operator.not_, repr=f'~{self.repr}', logic_group=self.logic_group)
|
|
91
|
+
|
|
92
|
+
def __bool__(self) -> bool:
|
|
93
|
+
return bool(self.eval())
|
|
94
|
+
|
|
95
|
+
@abc.abstractmethod
|
|
96
|
+
def to_json(self, fmt='dict') -> dict | str:
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
@abc.abstractmethod
|
|
101
|
+
def from_json(cls, json_message: str | bytes | dict) -> Self:
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def dtype_to_str(cls, dtype: type) -> str:
|
|
106
|
+
if dtype is None or dtype is Any:
|
|
107
|
+
return 'null'
|
|
108
|
+
|
|
109
|
+
# Check if the type is in built-in types
|
|
110
|
+
if dtype in vars(builtins).values():
|
|
111
|
+
return dtype.__name__
|
|
112
|
+
|
|
113
|
+
# For non-built-in types, construct the fully qualified name
|
|
114
|
+
module = dtype.__module__
|
|
115
|
+
qualname = dtype.__qualname__
|
|
116
|
+
return f"{module}.{qualname}"
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def str_to_dtype(cls, type_name: str) -> type:
|
|
120
|
+
if type_name == 'null': # Handle the special case for `None` or `Any`
|
|
121
|
+
return type(None)
|
|
122
|
+
|
|
123
|
+
# Check if it's a built-in type
|
|
124
|
+
if hasattr(builtins, type_name):
|
|
125
|
+
return getattr(builtins, type_name)
|
|
126
|
+
|
|
127
|
+
# For non-built-in types, split the string into module and class name
|
|
128
|
+
parts = type_name.rsplit('.', 1)
|
|
129
|
+
if len(parts) != 2:
|
|
130
|
+
raise ValueError(f"Invalid type name: {type_name}")
|
|
131
|
+
|
|
132
|
+
module_name, class_name = parts
|
|
133
|
+
try:
|
|
134
|
+
# Import the module and get the type
|
|
135
|
+
module = importlib.import_module(module_name)
|
|
136
|
+
return getattr(module, class_name)
|
|
137
|
+
except (ImportError, AttributeError) as e:
|
|
138
|
+
raise ValueError(f"Failed to deserialize type '{type_name}': {e}")
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def safe_alias(cls, v: ContextLogicExpression | int | float | bool) -> str:
|
|
142
|
+
if isinstance(v, ContextLogicExpression):
|
|
143
|
+
return v.repr
|
|
144
|
+
|
|
145
|
+
return str(v)
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def safe_dump(cls, v: ContextLogicExpression | int | float | bool) -> dict[str, Any] | Any:
|
|
149
|
+
if isinstance(v, ContextLogicExpression):
|
|
150
|
+
return v.to_json(fmt='dict')
|
|
151
|
+
|
|
152
|
+
return v
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def safe_load(cls, d: dict[str, Any] | Any) -> ContextLogicExpression | int | float | bool:
|
|
156
|
+
if isinstance(d, dict) and 'expression_type' in d:
|
|
157
|
+
constructor = globals()[d['expression_type']]
|
|
158
|
+
return constructor.from_json(d)
|
|
159
|
+
|
|
160
|
+
return d
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def safe_eval(cls, v: ContextLogicExpression | int | float | bool) -> Any:
|
|
164
|
+
if isinstance(v, ContextLogicExpression):
|
|
165
|
+
return v.eval()
|
|
166
|
+
|
|
167
|
+
return v
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AttrExpression(ContextLogicExpression):
|
|
171
|
+
def __init__(self, attr: str | int, dtype: type = None, repr: str = None, logic_group: LogicGroup = None):
|
|
172
|
+
self.attr = attr
|
|
173
|
+
|
|
174
|
+
super().__init__(
|
|
175
|
+
expression=self._eval,
|
|
176
|
+
dtype=dtype,
|
|
177
|
+
repr=repr if repr is not None else f'{logic_group.name}.{attr}',
|
|
178
|
+
logic_group=logic_group
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _eval(self) -> Any:
|
|
182
|
+
if isinstance(self.logic_group, LogicGroup) and self.attr in self.logic_group.contexts:
|
|
183
|
+
return self.logic_group.contexts[self.attr]
|
|
184
|
+
elif isinstance(self.logic_group, ExpressionCollection): # for indexing purpose, will not check attr existence.
|
|
185
|
+
return self.logic_group.contexts['data'][self.attr]
|
|
186
|
+
elif isinstance(self.logic_group, Mapping) and self.attr in self.logic_group:
|
|
187
|
+
return self.logic_group[self.attr]
|
|
188
|
+
elif hasattr(self.logic_group, self.attr):
|
|
189
|
+
return getattr(self.logic_group, self.attr)
|
|
190
|
+
else:
|
|
191
|
+
try:
|
|
192
|
+
self.logic_group[self.attr]
|
|
193
|
+
except:
|
|
194
|
+
raise AttributeError(f'Attribute {self.attr} does not exist in {self.logic_group}!\n{traceback.format_exc()}')
|
|
195
|
+
|
|
196
|
+
def to_json(self, fmt='dict') -> dict | str:
|
|
197
|
+
json_dict = dict(
|
|
198
|
+
expression_type=self.__class__.__name__,
|
|
199
|
+
attr=self.attr,
|
|
200
|
+
repr=self.repr
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if self.dtype is not None and self.dtype is not Any:
|
|
204
|
+
json_dict['dtype'] = self.dtype_to_str(self.dtype)
|
|
205
|
+
|
|
206
|
+
if fmt == 'dict':
|
|
207
|
+
return json_dict
|
|
208
|
+
else:
|
|
209
|
+
return json.dumps(json_dict)
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def from_json(cls, json_message: str | bytes | dict) -> Self:
|
|
213
|
+
if isinstance(json_message, dict):
|
|
214
|
+
json_dict = json_message
|
|
215
|
+
else:
|
|
216
|
+
json_dict = json.loads(json_message)
|
|
217
|
+
|
|
218
|
+
assert json_dict['expression_type'] == cls.__name__
|
|
219
|
+
|
|
220
|
+
kwargs = dict(
|
|
221
|
+
attr=json_dict['attr'],
|
|
222
|
+
repr=json_dict['repr'],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if 'dtype' in json_dict:
|
|
226
|
+
kwargs['dtype'] = cls.str_to_dtype(json_dict['dtype'])
|
|
227
|
+
|
|
228
|
+
self = cls(
|
|
229
|
+
**kwargs
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class MathExpression(ContextLogicExpression):
|
|
236
|
+
class Operator(enum.StrEnum):
|
|
237
|
+
add = '+'
|
|
238
|
+
sub = '-'
|
|
239
|
+
mul = '*'
|
|
240
|
+
truediv = '/'
|
|
241
|
+
floordiv = '//'
|
|
242
|
+
pow = '**'
|
|
243
|
+
neg = '--'
|
|
244
|
+
|
|
245
|
+
def __init__(self, left: ContextLogicExpression | int | float, op: Operator, right: ContextLogicExpression | int | float = None, dtype: type = None, repr: str = None, logic_group: LogicGroup = None):
|
|
246
|
+
self.op = op
|
|
247
|
+
self.left = left
|
|
248
|
+
self.right = right
|
|
249
|
+
|
|
250
|
+
super().__init__(
|
|
251
|
+
expression=self._eval,
|
|
252
|
+
dtype=dtype,
|
|
253
|
+
repr=repr if repr is not None else
|
|
254
|
+
f'-{self.safe_alias(left)}' if self.op is self.Operator.neg else
|
|
255
|
+
f'{self.safe_alias(left)} {self.op} {self.safe_alias(right)}',
|
|
256
|
+
logic_group=logic_group
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _eval(self) -> Any:
|
|
260
|
+
op = getattr(operator, self.op.name)
|
|
261
|
+
res = op(self.safe_eval(self.left), self.safe_eval(self.right))
|
|
262
|
+
return res
|
|
263
|
+
|
|
264
|
+
def _neg(self) -> Any:
|
|
265
|
+
res = -self.safe_eval(self.left)
|
|
266
|
+
return res
|
|
267
|
+
|
|
268
|
+
def to_json(self, fmt='dict') -> dict | str:
|
|
269
|
+
json_dict = dict(
|
|
270
|
+
expression_type=self.__class__.__name__,
|
|
271
|
+
left=self.safe_dump(self.left),
|
|
272
|
+
op=self.op.name,
|
|
273
|
+
repr=self.repr
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if self.right is not None:
|
|
277
|
+
json_dict['right'] = self.safe_dump(self.right)
|
|
278
|
+
|
|
279
|
+
if self.dtype is not None and self.dtype is not Any:
|
|
280
|
+
json_dict['dtype'] = self.dtype_to_str(self.dtype)
|
|
281
|
+
|
|
282
|
+
if fmt == 'dict':
|
|
283
|
+
return json_dict
|
|
284
|
+
else:
|
|
285
|
+
return json.dumps(json_dict)
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def from_json(cls, json_message: str | bytes | dict) -> Self:
|
|
289
|
+
if isinstance(json_message, dict):
|
|
290
|
+
json_dict = json_message
|
|
291
|
+
else:
|
|
292
|
+
json_dict = json.loads(json_message)
|
|
293
|
+
|
|
294
|
+
assert json_dict['expression_type'] == cls.__name__
|
|
295
|
+
|
|
296
|
+
kwargs = dict(
|
|
297
|
+
left=cls.safe_load(json_dict['left']),
|
|
298
|
+
op=cls.Operator[json_dict['op']],
|
|
299
|
+
repr=json_dict['repr'],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if 'right' in json_dict:
|
|
303
|
+
kwargs['right'] = cls.safe_load(json_dict['right'])
|
|
304
|
+
|
|
305
|
+
if 'dtype' in json_dict:
|
|
306
|
+
kwargs['dtype'] = cls.str_to_dtype(json_dict['dtype'])
|
|
307
|
+
|
|
308
|
+
self = cls(
|
|
309
|
+
**kwargs
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return self
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ComparisonExpression(ContextLogicExpression):
|
|
316
|
+
class Operator(enum.StrEnum):
|
|
317
|
+
eq = '=='
|
|
318
|
+
ne = '!='
|
|
319
|
+
gt = '>'
|
|
320
|
+
ge = '>='
|
|
321
|
+
lt = '<'
|
|
322
|
+
le = '<='
|
|
323
|
+
|
|
324
|
+
def __init__(self, left: ContextLogicExpression | int | float, op: Operator, right: ContextLogicExpression | int | float = None, dtype: type = None, repr: str = None, logic_group: LogicGroup = None):
|
|
325
|
+
self.op = op
|
|
326
|
+
self.left = left
|
|
327
|
+
self.right = right
|
|
328
|
+
|
|
329
|
+
super().__init__(
|
|
330
|
+
expression=self._eval,
|
|
331
|
+
dtype=dtype,
|
|
332
|
+
repr=repr if repr is not None else f'{self.safe_alias(left)} {self.op} {self.safe_alias(right)}',
|
|
333
|
+
logic_group=logic_group
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _eval(self) -> Any:
|
|
337
|
+
left = self.safe_eval(self.left)
|
|
338
|
+
right = self.safe_eval(self.right)
|
|
339
|
+
|
|
340
|
+
match self.op:
|
|
341
|
+
case self.Operator.eq:
|
|
342
|
+
return left == right
|
|
343
|
+
case self.Operator.ne:
|
|
344
|
+
return left != right
|
|
345
|
+
case self.Operator.gt:
|
|
346
|
+
return left > right
|
|
347
|
+
case self.Operator.ge:
|
|
348
|
+
return left >= right
|
|
349
|
+
case self.Operator.lt:
|
|
350
|
+
return left < right
|
|
351
|
+
case self.Operator.le:
|
|
352
|
+
return left <= right
|
|
353
|
+
case _:
|
|
354
|
+
raise NotImplementedError(f'Invalid comparison operator {self.op}!')
|
|
355
|
+
|
|
356
|
+
def to_json(self, fmt='dict') -> dict | str:
|
|
357
|
+
json_dict = dict(
|
|
358
|
+
expression_type=self.__class__.__name__,
|
|
359
|
+
left=self.safe_dump(self.left),
|
|
360
|
+
right=self.safe_dump(self.right),
|
|
361
|
+
op=self.op.name,
|
|
362
|
+
repr=self.repr
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if self.dtype is not None and self.dtype is not Any:
|
|
366
|
+
json_dict['dtype'] = self.dtype_to_str(self.dtype)
|
|
367
|
+
|
|
368
|
+
if fmt == 'dict':
|
|
369
|
+
return json_dict
|
|
370
|
+
else:
|
|
371
|
+
return json.dumps(json_dict)
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def from_json(cls, json_message: str | bytes | dict) -> Self:
|
|
375
|
+
if isinstance(json_message, dict):
|
|
376
|
+
json_dict = json_message
|
|
377
|
+
else:
|
|
378
|
+
json_dict = json.loads(json_message)
|
|
379
|
+
|
|
380
|
+
assert json_dict['expression_type'] == cls.__name__
|
|
381
|
+
|
|
382
|
+
kwargs = dict(
|
|
383
|
+
left=cls.safe_load(json_dict['left']),
|
|
384
|
+
right=cls.safe_load(json_dict['right']),
|
|
385
|
+
op=cls.Operator[json_dict['op']],
|
|
386
|
+
repr=json_dict['repr'],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if 'dtype' in json_dict:
|
|
390
|
+
kwargs['dtype'] = cls.str_to_dtype(json_dict['dtype'])
|
|
391
|
+
|
|
392
|
+
self = cls(
|
|
393
|
+
**kwargs
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class LogicalExpression(ContextLogicExpression):
|
|
400
|
+
class Operator(enum.StrEnum):
|
|
401
|
+
and_ = '&'
|
|
402
|
+
or_ = '|'
|
|
403
|
+
not_ = '~'
|
|
404
|
+
|
|
405
|
+
def __init__(self, left: ContextLogicExpression | int | float, op: Operator, right: ContextLogicExpression | int | float = None, dtype: type = None, repr: str = None, logic_group: LogicGroup = None):
|
|
406
|
+
self.op = op
|
|
407
|
+
self.left = left
|
|
408
|
+
self.right = right
|
|
409
|
+
|
|
410
|
+
super().__init__(
|
|
411
|
+
expression=self._eval,
|
|
412
|
+
dtype=dtype,
|
|
413
|
+
repr=repr if repr is not None else
|
|
414
|
+
f'~{self.safe_alias(left)}' if self.op is self.Operator.not_ else
|
|
415
|
+
f'{self.safe_alias(left)} {self.op} {self.safe_alias(right)}',
|
|
416
|
+
logic_group=logic_group
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def _eval(self) -> Any:
|
|
420
|
+
match self.op:
|
|
421
|
+
case self.Operator.and_:
|
|
422
|
+
return self.safe_eval(self.left) and self.safe_eval(self.right)
|
|
423
|
+
case self.Operator.or_:
|
|
424
|
+
return self.safe_eval(self.left) or self.safe_eval(self.right)
|
|
425
|
+
case self.Operator.not_:
|
|
426
|
+
return not self.safe_eval(self.left)
|
|
427
|
+
case _:
|
|
428
|
+
raise NotImplementedError(f'Invalid comparison operator {self.op}!')
|
|
429
|
+
|
|
430
|
+
def _not(self) -> Any:
|
|
431
|
+
res = not self.safe_eval(self.left)
|
|
432
|
+
return res
|
|
433
|
+
|
|
434
|
+
def to_json(self, fmt='dict') -> dict | str:
|
|
435
|
+
json_dict = dict(
|
|
436
|
+
expression_type=self.__class__.__name__,
|
|
437
|
+
left=self.safe_dump(self.left),
|
|
438
|
+
op=self.op.name,
|
|
439
|
+
repr=self.repr
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if self.right is not None:
|
|
443
|
+
json_dict['right'] = self.safe_dump(self.right)
|
|
444
|
+
|
|
445
|
+
if self.dtype is not None and self.dtype is not Any:
|
|
446
|
+
json_dict['dtype'] = self.dtype_to_str(self.dtype)
|
|
447
|
+
|
|
448
|
+
if fmt == 'dict':
|
|
449
|
+
return json_dict
|
|
450
|
+
else:
|
|
451
|
+
return json.dumps(json_dict)
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def from_json(cls, json_message: str | bytes | dict) -> Self:
|
|
455
|
+
if isinstance(json_message, dict):
|
|
456
|
+
json_dict = json_message
|
|
457
|
+
else:
|
|
458
|
+
json_dict = json.loads(json_message)
|
|
459
|
+
|
|
460
|
+
assert json_dict['expression_type'] == cls.__name__
|
|
461
|
+
|
|
462
|
+
kwargs = dict(
|
|
463
|
+
left=cls.safe_load(json_dict['left']),
|
|
464
|
+
op=cls.Operator[json_dict['op']],
|
|
465
|
+
repr=json_dict['repr'],
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if 'right' in json_dict:
|
|
469
|
+
kwargs['right'] = cls.safe_load(json_dict['right'])
|
|
470
|
+
|
|
471
|
+
if 'dtype' in json_dict:
|
|
472
|
+
kwargs['dtype'] = cls.str_to_dtype(json_dict['dtype'])
|
|
473
|
+
|
|
474
|
+
self = cls(
|
|
475
|
+
**kwargs
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return self
|