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.

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