ripple-down-rules 0.6.0__py3-none-any.whl → 0.6.6__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.
@@ -1,5 +1,25 @@
1
- __version__ = "0.6.0"
1
+ __version__ = "0.6.6"
2
2
 
3
3
  import logging
4
+ import sys
5
+
4
6
  logger = logging.Logger("rdr")
5
7
  logger.setLevel(logging.INFO)
8
+
9
+ try:
10
+ from PyQt6.QtWidgets import QApplication
11
+ app = QApplication(sys.argv)
12
+ except ImportError:
13
+ pass
14
+
15
+
16
+ # Trigger patch
17
+ try:
18
+ from .predicates import *
19
+ from .datastructures.tracked_object import TrackedObjectMixin
20
+ from .datastructures.dataclasses import CaseQuery
21
+ from .rdr_decorators import RDRDecorator
22
+ from .rdr import MultiClassRDR, SingleClassRDR, GeneralRDR
23
+ import ripple_down_rules_meta._apply_overrides
24
+ except ImportError:
25
+ pass
@@ -95,7 +95,7 @@ class CallableExpression(SubclassJSONSerializer):
95
95
  encapsulating_function_name: str = "_get_value"
96
96
 
97
97
  def __init__(self, user_input: Optional[str] = None,
98
- conclusion_type: Optional[Tuple[Type]] = None,
98
+ conclusion_type: Optional[Tuple[Type, ...]] = None,
99
99
  expression_tree: Optional[AST] = None,
100
100
  scope: Optional[Dict[str, Any]] = None,
101
101
  conclusion: Optional[Any] = None,
@@ -116,13 +116,24 @@ class CallableExpression(SubclassJSONSerializer):
116
116
  if user_input is None:
117
117
  user_input = build_user_input_from_conclusion(conclusion)
118
118
  self.conclusion: Optional[Any] = conclusion
119
- self._user_input: str = encapsulate_user_input(user_input, self.get_encapsulating_function())
119
+ if "def " in user_input:
120
+ self.user_defined_name = user_input.split('(')[0].replace('def ', '')
121
+ else:
122
+ self.user_defined_name = user_input
123
+ if f"def {self.encapsulating_function_name}" not in user_input:
124
+ user_input = encapsulate_user_input(user_input, self.get_encapsulating_function())
125
+ self._user_input: str = user_input
120
126
  if conclusion_type is not None:
121
127
  if is_iterable(conclusion_type):
122
128
  conclusion_type = tuple(conclusion_type)
123
129
  else:
124
130
  conclusion_type = (conclusion_type,)
125
131
  self.conclusion_type = conclusion_type
132
+ self.expected_types: Set[Type] = set(conclusion_type) if conclusion_type is not None else set()
133
+ if list in self.expected_types:
134
+ self.expected_types.remove(list)
135
+ if set in self.expected_types:
136
+ self.expected_types.add(set)
126
137
  self.scope: Optional[Dict[str, Any]] = scope if scope is not None else {}
127
138
  self.scope = get_used_scope(self.user_input, self.scope)
128
139
  self.expression_tree: AST = expression_tree if expression_tree else parse_string_to_expression(self.user_input)
@@ -140,6 +151,8 @@ class CallableExpression(SubclassJSONSerializer):
140
151
 
141
152
  def __call__(self, case: Any, **kwargs) -> Any:
142
153
  try:
154
+ # if not self.mutually_exclusive:
155
+ # self.expected_types.update({list, set})
143
156
  if self.user_input is not None:
144
157
  if not isinstance(case, Case):
145
158
  case = create_case(case, max_recursion_idx=3)
@@ -151,8 +164,7 @@ class CallableExpression(SubclassJSONSerializer):
151
164
  if self.mutually_exclusive and issubclass(type(output), (list, set)):
152
165
  raise ValueError(f"Mutually exclusive types cannot be lists or sets, got {type(output)}")
153
166
  output_types = {type(o) for o in make_list(output)}
154
- output_types.add(type(output))
155
- if not are_results_subclass_of_types(output_types, self.conclusion_type):
167
+ if not are_results_subclass_of_types(output_types, self.expected_types):
156
168
  raise ValueError(f"Not all result types {output_types} are subclasses of expected types"
157
169
  f" {self.conclusion_type}")
158
170
  return output
@@ -161,7 +173,7 @@ class CallableExpression(SubclassJSONSerializer):
161
173
  else:
162
174
  raise ValueError("Either user_input or conclusion must be provided.")
163
175
  except Exception as e:
164
- raise ValueError(f"Error during evaluation: {e}")
176
+ raise ValueError(f"Error during evaluation: {e}, user_input: {self.user_input}")
165
177
 
166
178
  def combine_with(self, other: 'CallableExpression') -> 'CallableExpression':
167
179
  """
@@ -172,7 +184,8 @@ class CallableExpression(SubclassJSONSerializer):
172
184
  new_user_input = (f"{cond1_user_input}\n"
173
185
  f"{cond2_user_input}\n"
174
186
  f"return _cond1(case) and _cond2(case)")
175
- return CallableExpression(new_user_input, conclusion_type=self.conclusion_type)
187
+ return CallableExpression(new_user_input, conclusion_type=self.conclusion_type,
188
+ mutually_exclusive=self.mutually_exclusive)
176
189
 
177
190
  def update_user_input_from_file(self, file_path: str, function_name: str):
178
191
  """
@@ -214,6 +227,10 @@ class CallableExpression(SubclassJSONSerializer):
214
227
  Set the user input.
215
228
  """
216
229
  if value is not None:
230
+ if "def " in value:
231
+ self.user_defined_name = value.split('(')[0].replace('def ', '')
232
+ else:
233
+ self.user_defined_name = value
217
234
  self._user_input = encapsulate_user_input(value, self.get_encapsulating_function())
218
235
  self.scope = get_used_scope(self.user_input, self.scope)
219
236
  self.expression_tree = parse_string_to_expression(self.user_input)
@@ -299,7 +316,7 @@ def parse_string_to_expression(expression_str: str) -> AST:
299
316
  :param expression_str: The string which will be parsed.
300
317
  :return: The parsed expression.
301
318
  """
302
- if not expression_str.startswith(CallableExpression.get_encapsulating_function()):
319
+ if not expression_str.startswith(f"def {CallableExpression.encapsulating_function_name}"):
303
320
  expression_str = encapsulate_user_input(expression_str, CallableExpression.get_encapsulating_function())
304
321
  mode = 'exec' if expression_str.startswith('def') else 'eval'
305
322
  tree = ast.parse(expression_str, mode=mode)
@@ -20,19 +20,20 @@ if TYPE_CHECKING:
20
20
 
21
21
  class Case(UserDict, SubclassJSONSerializer):
22
22
  """
23
- A collection of attributes that represents a set of constraints on a case. This is a dictionary where the keys are
24
- the names of the attributes and the values are the attributes. All are stored in lower case.
23
+ A collection of attributes that represents a set of attributes of a case. This is a dictionary where the keys are
24
+ the names of the attributes and the values are the attributes. All are stored in lower case, and can be accessed
25
+ using the dot notation as well as the dictionary access notation.
25
26
  """
26
27
 
27
28
  def __init__(self, _obj_type: Type, _id: Optional[Hashable] = None,
28
29
  _name: Optional[str] = None, original_object: Optional[Any] = None, **kwargs):
29
30
  """
30
- Create a new row.
31
+ Create a new case.
31
32
 
32
- :param _obj_type: The type of the object that the row represents.
33
- :param _id: The id of the row.
34
- :param _name: The semantic name that describes the row.
35
- :param kwargs: The attributes of the row.
33
+ :param _obj_type: The original type of the object that the case represents.
34
+ :param _id: The id of the case.
35
+ :param _name: The semantic name that describes the case.
36
+ :param kwargs: The attributes of the case.
36
37
  """
37
38
  super().__init__(kwargs)
38
39
  self._original_object = original_object
@@ -43,12 +44,12 @@ class Case(UserDict, SubclassJSONSerializer):
43
44
  @classmethod
44
45
  def from_obj(cls, obj: Any, obj_name: Optional[str] = None, max_recursion_idx: int = 3) -> Case:
45
46
  """
46
- Create a row from an object.
47
+ Create a case from an object.
47
48
 
48
- :param obj: The object to create a row from.
49
+ :param obj: The object to create a case from.
49
50
  :param max_recursion_idx: The maximum recursion index to prevent infinite recursion.
50
51
  :param obj_name: The name of the object.
51
- :return: The row of the object.
52
+ :return: The case that represents the object.
52
53
  """
53
54
  return create_case(obj, max_recursion_idx=max_recursion_idx, obj_name=obj_name)
54
55
 
@@ -129,7 +130,7 @@ class Case(UserDict, SubclassJSONSerializer):
129
130
  @dataclass
130
131
  class CaseAttributeValue(SubclassJSONSerializer):
131
132
  """
132
- A column value is a value in a column.
133
+ Encapsulates a single value of a case attribute, it adds an id to the value.
133
134
  """
134
135
  id: Hashable
135
136
  """
@@ -1,19 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
- import typing
4
+ import uuid
5
5
  from dataclasses import dataclass, field
6
6
 
7
- import typing_extensions
7
+ from colorama import Fore, Style
8
8
  from omegaconf import MISSING
9
9
  from sqlalchemy.orm import DeclarativeBase as SQLTable
10
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set, Callable
10
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, Set, Callable, TYPE_CHECKING
11
11
 
12
- from ..utils import get_method_name, get_function_import_data, get_function_representation
13
12
  from .callable_expression import CallableExpression
14
13
  from .case import create_case, Case
15
- from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, get_value_type_from_type_hint, \
16
- typing_to_python_type
14
+ from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, render_tree, \
15
+ get_function_representation, get_method_object_from_pytest_request
16
+
17
+ if TYPE_CHECKING:
18
+ from ..rdr import RippleDownRules
19
+ from ..rules import Rule
17
20
 
18
21
 
19
22
  @dataclass
@@ -31,7 +34,7 @@ class CaseQuery:
31
34
  """
32
35
  The name of the attribute.
33
36
  """
34
- _attribute_types: Tuple[Type]
37
+ _attribute_types: Tuple[Type, ...]
35
38
  """
36
39
  The type(s) of the attribute.
37
40
  """
@@ -57,9 +60,14 @@ class CaseQuery:
57
60
  The executable scenario is the root callable that recreates the situation that the case is
58
61
  created in, for example, when the case is created from a test function, this would be the test function itself.
59
62
  """
63
+ this_case_target_value: Optional[Any] = None
64
+ """
65
+ The non relational case query instance target value.
66
+ """
60
67
  _target: Optional[CallableExpression] = None
61
68
  """
62
- The target expression of the attribute.
69
+ The relational target (the evaluatable conclusion of the rule) which is a callable expression that varies with
70
+ the case.
63
71
  """
64
72
  default_value: Optional[Any] = None
65
73
  """
@@ -92,6 +100,38 @@ class CaseQuery:
92
100
  """
93
101
  The type hints of the function arguments. This is used to recreate the function signature.
94
102
  """
103
+ rdr: Optional[RippleDownRules] = None
104
+ """
105
+ The Ripple Down Rules that was used to answer the case query.
106
+ """
107
+
108
+ def render_rule_tree(self, filepath: Optional[str] = None, view: bool = False):
109
+ if self.rdr is None:
110
+ return
111
+ render_tree(self.rdr.start_rule, use_dot_exporter=True, filename=filepath, view=view)
112
+
113
+ @property
114
+ def current_value_str(self):
115
+ return (f"{Fore.MAGENTA}Current value of {Fore.CYAN}{self.name}{Fore.MAGENTA} of type(s) "
116
+ f"{Fore.CYAN}({self.core_attribute_type_str}){Fore.MAGENTA}: "
117
+ f"{Fore.WHITE}{self.current_value}{Style.RESET_ALL}")
118
+
119
+ @property
120
+ def current_value(self) -> Any:
121
+ """
122
+ :return: The current value of the attribute.
123
+ """
124
+ if not hasattr(self.case, self.attribute_name):
125
+ return None
126
+
127
+ attr_value = getattr(self.case, self.attribute_name)
128
+
129
+ if attr_value is None:
130
+ return attr_value
131
+ elif self.mutually_exclusive:
132
+ return attr_value
133
+ else:
134
+ return list({v for v in make_list(attr_value) if isinstance(v, self.core_attribute_type)})
95
135
 
96
136
  @property
97
137
  def case_type(self) -> Type:
@@ -139,13 +179,20 @@ class CaseQuery:
139
179
  attribute_types_str = f"Union[{', '.join([t.__name__ for t in self.core_attribute_type])}]"
140
180
  else:
141
181
  attribute_types_str = self.core_attribute_type[0].__name__
142
- if all(t in self.attribute_type for t in [list, set]) and len(self.core_attribute_type) > 2:
182
+ if not self.mutually_exclusive:
143
183
  return f"List[{attribute_types_str}]"
144
184
  else:
145
185
  return attribute_types_str
146
186
 
147
187
  @property
148
- def core_attribute_type(self) -> Tuple[Type]:
188
+ def core_attribute_type_str(self) -> str:
189
+ """
190
+ :return: The names of the core types of the attribute.
191
+ """
192
+ return ','.join([t.__name__ for t in self.core_attribute_type])
193
+
194
+ @property
195
+ def core_attribute_type(self) -> Tuple[Type, ...]:
149
196
  """
150
197
  :return: The core type of the attribute.
151
198
  """
@@ -247,7 +294,7 @@ class CaseQuery:
247
294
  conditions=self.conditions, is_function=self.is_function,
248
295
  function_args_type_hints=self.function_args_type_hints,
249
296
  case_factory=self.case_factory, case_factory_idx=self.case_factory_idx,
250
- case_conf=self.case_conf, scenario=self.scenario)
297
+ case_conf=self.case_conf, scenario=self.scenario, rdr=self.rdr)
251
298
 
252
299
 
253
300
  @dataclass
@@ -264,6 +311,12 @@ class CaseFactoryMetaData:
264
311
  factory_idx: Optional[int] = None
265
312
  case_conf: Optional[CaseConf] = None
266
313
  scenario: Optional[Callable] = None
314
+ pytest_request: Optional[Callable] = field(hash=False, compare=False, default=None)
315
+ this_case_target_value: Optional[Any] = None
316
+
317
+ def __post_init__(self):
318
+ if self.pytest_request is not None and self.scenario is None:
319
+ self.scenario = get_method_object_from_pytest_request(self.pytest_request)
267
320
 
268
321
  @classmethod
269
322
  def from_case_query(cls, case_query: CaseQuery) -> CaseFactoryMetaData:
@@ -280,8 +333,76 @@ class CaseFactoryMetaData:
280
333
  return (f"CaseFactoryMetaData("
281
334
  f"factory_method={factory_method_repr}, "
282
335
  f"factory_idx={self.factory_idx}, "
283
- f"case_conf={self.case_conf},"
284
- f" scenario={scenario_repr})")
336
+ f"case_conf={self.case_conf}, "
337
+ f"scenario={scenario_repr}, "
338
+ f"this_case_target_value={self.this_case_target_value})")
285
339
 
286
340
  def __str__(self):
287
- return self.__repr__()
341
+ return self.__repr__()
342
+
343
+
344
+ @dataclass
345
+ class RDRConclusion:
346
+ """
347
+ This dataclass represents a conclusion of a Ripple Down Rule.
348
+ It contains the conclusion expression, the type of the conclusion, and the scope in which it is evaluated.
349
+ """
350
+ _conclusion: Any
351
+ """
352
+ The conclusion value.
353
+ """
354
+ _frozen_case: Any
355
+ """
356
+ The frozen case that the conclusion was made for.
357
+ """
358
+ _rule: Rule
359
+ """
360
+ The rule that gave this conclusion.
361
+ """
362
+ _rdr: RippleDownRules
363
+ """
364
+ The Ripple Down Rules that classified the case and produced this conclusion.
365
+ """
366
+ _id: int = field(default_factory=lambda: uuid.uuid4().int)
367
+ """
368
+ The unique identifier of the conclusion.
369
+ """
370
+ def __getattribute__(self, name: str) -> Any:
371
+ if name.startswith('_'):
372
+ return object.__getattribute__(self, name)
373
+ else:
374
+ conclusion = object.__getattribute__(self, "_conclusion")
375
+
376
+ value = getattr(conclusion, name)
377
+
378
+ self._record_dependency(name)
379
+
380
+ return value
381
+
382
+ def __setattr__(self, name, value):
383
+ if name.startswith('_'):
384
+ object.__setattr__(self, name, value)
385
+ else:
386
+ setattr(self._wrapped, name, value)
387
+
388
+ def _record_dependency(self, attr_name):
389
+ # Inspect stack to find instance of CallableExpression
390
+ for frame_info in inspect.stack():
391
+ func_name = frame_info.function
392
+ local_self = frame_info.frame.f_locals.get("self", None)
393
+ if (
394
+ func_name == "__call__" and
395
+ local_self is not None and
396
+ type(local_self) is CallableExpression
397
+ ):
398
+ self._used_in_tracker = True
399
+ print("RDRConclusion used inside CallableExpression")
400
+ break
401
+
402
+ def __hash__(self):
403
+ return hash(self.id)
404
+
405
+ def __eq__(self, other):
406
+ if not isinstance(other, RDRConclusion):
407
+ return False
408
+ return self.id == other.id
@@ -7,6 +7,19 @@ from typing_extensions import List, Dict, Any, Type
7
7
  from ripple_down_rules.utils import SubclassJSONSerializer
8
8
 
9
9
 
10
+ class ExitStatus(Enum):
11
+ """
12
+ Describes the status at exit of the user interface.
13
+ """
14
+ CLOSE = auto()
15
+ """
16
+ The user wants to stop the program.
17
+ """
18
+ SUCCESS = auto()
19
+ """
20
+ The user completed the task successfully.
21
+ """
22
+
10
23
  class InteractionMode(Enum):
11
24
  """
12
25
  The interaction mode of the RDR.
@@ -80,20 +93,6 @@ class Stop(Category):
80
93
  stop = "stop"
81
94
 
82
95
 
83
- class ExpressionParser(Enum):
84
- """
85
- Parsers for expressions to evaluate and encapsulate the expression into a callable function.
86
- """
87
- ASTVisitor: int = auto()
88
- """
89
- Generic python Abstract Syntax Tree that detects variables, attributes, binary/boolean expressions , ...etc.
90
- """
91
- SQLAlchemy: int = auto()
92
- """
93
- Specific for SQLAlchemy expressions on ORM Tables.
94
- """
95
-
96
-
97
96
  class PromptFor(Enum):
98
97
  """
99
98
  The reason of the prompt. (e.g. get conditions, conclusions, or affirmation).
@@ -118,51 +117,6 @@ class PromptFor(Enum):
118
117
  return self.__str__()
119
118
 
120
119
 
121
- class CategoricalValue(Enum):
122
- """
123
- A categorical value is a value that is a category.
124
- """
125
-
126
- def __eq__(self, other):
127
- if isinstance(other, CategoricalValue):
128
- return self.name == other.name
129
- elif isinstance(other, str):
130
- return self.name == other
131
- return self.name == other
132
-
133
- def __hash__(self):
134
- return hash(self.name)
135
-
136
- @classmethod
137
- def to_list(cls):
138
- return list(cls._value2member_map_.keys())
139
-
140
- @classmethod
141
- def from_str(cls, category: str):
142
- return cls[category.lower()]
143
-
144
- @classmethod
145
- def from_strs(cls, categories: List[str]):
146
- return [cls.from_str(c) for c in categories]
147
-
148
- def __str__(self):
149
- return self.name
150
-
151
- def __repr__(self):
152
- return self.__str__()
153
-
154
-
155
- class RDRMode(Enum):
156
- Propositional = auto()
157
- """
158
- Propositional mode, the mode where the rules are propositional.
159
- """
160
- Relational = auto()
161
- """
162
- Relational mode, the mode where the rules are relational.
163
- """
164
-
165
-
166
120
  class MCRDRMode(Enum):
167
121
  """
168
122
  The modes of the MultiClassRDR.
@@ -196,34 +150,23 @@ class RDREdge(Enum):
196
150
  """
197
151
  Next edge, the edge that represents the next rule to be evaluated.
198
152
  """
199
-
200
-
201
- class ValueType(Enum):
202
- Unary = auto()
203
- """
204
- Unary value type (eg. null).
153
+ Filter = "filter if"
205
154
  """
206
- Binary = auto()
155
+ Filter edge, the edge that represents the filter condition.
207
156
  """
208
- Binary value type (eg. True, False).
157
+ Empty = ""
209
158
  """
210
- Discrete = auto()
211
- """
212
- Discrete value type (eg. 1, 2, 3).
213
- """
214
- Continuous = auto()
215
- """
216
- Continuous value type (eg. 1.0, 2.5, 3.4).
217
- """
218
- Nominal = auto()
219
- """
220
- Nominal value type (eg. red, blue, green), categories where the values have no natural order.
221
- """
222
- Ordinal = auto()
223
- """
224
- Ordinal value type (eg. low, medium, high), categories where the values have a natural order.
225
- """
226
- Iterable = auto()
227
- """
228
- Iterable value type (eg. [1, 2, 3]).
159
+ Empty edge, used for example for the root/input node of the tree.
229
160
  """
161
+
162
+ @classmethod
163
+ def from_value(cls, value: str) -> RDREdge:
164
+ """
165
+ Convert a string value to an RDREdge enum.
166
+
167
+ :param value: The string that represents the edge type.
168
+ :return: The RDREdge enum.
169
+ """
170
+ if value not in cls._value2member_map_:
171
+ raise ValueError(f"RDREdge {value} is not supported.")
172
+ return cls._value2member_map_[value]