ripple-down-rules 0.2.4__tar.gz → 0.3.0__tar.gz

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.
Files changed (34) hide show
  1. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/PKG-INFO +1 -1
  2. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/pyproject.toml +1 -1
  3. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datasets.py +66 -6
  4. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/callable_expression.py +13 -5
  5. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/case.py +33 -5
  6. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/dataclasses.py +30 -8
  7. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/enums.py +30 -1
  8. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/experts.py +2 -1
  9. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/prompt.py +215 -109
  10. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/rdr.py +7 -5
  11. ripple_down_rules-0.3.0/src/ripple_down_rules/rdr_decorators.py +139 -0
  12. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/utils.py +162 -18
  13. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/PKG-INFO +1 -1
  14. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/SOURCES.txt +1 -0
  15. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_rdr_alchemy.py +6 -6
  16. ripple_down_rules-0.3.0/test/test_rdr_decorators.py +27 -0
  17. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_relational_rdr.py +6 -39
  18. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_relational_rdr_alchemy.py +15 -16
  19. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_sql_model.py +4 -4
  20. ripple_down_rules-0.2.4/src/ripple_down_rules/rdr_decorators.py +0 -55
  21. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/LICENSE +0 -0
  22. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/README.md +0 -0
  23. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/setup.cfg +0 -0
  24. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/__init__.py +0 -0
  25. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  26. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/failures.py +0 -0
  27. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/helpers.py +0 -0
  28. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules/rules.py +0 -0
  29. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  30. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  31. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_json_serialization.py +0 -0
  32. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_on_mutagenic.py +0 -0
  33. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_rdr.py +0 -0
  34. {ripple_down_rules-0.2.4 → ripple_down_rules-0.3.0}/test/test_rdr_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning.
5
5
  Author-email: Abdelrhman Bassiouny <abassiou@uni-bremen.de>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "ripple_down_rules"
9
- version = "0.2.4"
9
+ version = "0.3.0"
10
10
  description = "Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning."
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Abdelrhman Bassiouny", email = "abassiou@uni-bremen.de" }]
@@ -2,15 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import pickle
5
+ from dataclasses import dataclass, field
5
6
 
6
7
  import sqlalchemy
7
8
  from sqlalchemy import ForeignKey
8
- from sqlalchemy.orm import MappedAsDataclass, Mapped, mapped_column, relationship
9
- from typing_extensions import Tuple, List, Set, Optional
9
+ from sqlalchemy.orm import MappedAsDataclass, Mapped, mapped_column, relationship, MappedColumn
10
+ from typing_extensions import Tuple, List, Set, Optional, Self
10
11
  from ucimlrepo import fetch_ucirepo
11
12
 
12
13
  from .datastructures.case import Case, create_cases_from_dataframe
13
14
  from .datastructures.enums import Category
15
+ from .rdr_decorators import RDRDecorator
14
16
 
15
17
 
16
18
  def load_cached_dataset(cache_file):
@@ -106,8 +108,65 @@ class Habitat(Category):
106
108
  air = "air"
107
109
 
108
110
 
109
- # SpeciesCol = Column.create_from_enum(Species, mutually_exclusive=True)
110
- # HabitatCol = Column.create_from_enum(Habitat, mutually_exclusive=False)
111
+ class PhysicalObject:
112
+ """
113
+ A physical object is an object that can be contained in a container.
114
+ """
115
+ _rdr_json_dir: str = os.path.join(os.path.dirname(__file__), "../../test/test_results")
116
+ """
117
+ The directory where the RDR serialized JSON files are stored.
118
+ """
119
+ _rdr_python_dir: str = os.path.join(os.path.dirname(__file__), "../../test/test_generated_rdrs")
120
+ """
121
+ The directory where the RDR generated Python files are stored.
122
+ """
123
+ _is_a_robot_rdr: RDRDecorator = RDRDecorator(_rdr_json_dir, (bool,), True,
124
+ python_dir=_rdr_python_dir)
125
+ """
126
+ The RDR decorator that is used to determine if the object is a robot or not.
127
+ """
128
+ _select_parts_rdr: RDRDecorator = RDRDecorator(_rdr_json_dir, (Self,), False,
129
+ python_dir=_rdr_python_dir)
130
+ """
131
+ The RDR decorator that is used to determine if the object is a robot or not.
132
+ """
133
+
134
+ def __init__(self, name: str, contained_objects: Optional[List[PhysicalObject]] = None):
135
+ self.name: str = name
136
+ self._contained_objects: List[PhysicalObject] = contained_objects or []
137
+
138
+ @property
139
+ def contained_objects(self) -> List[PhysicalObject]:
140
+ return self._contained_objects
141
+
142
+ @contained_objects.setter
143
+ def contained_objects(self, value: List[PhysicalObject]):
144
+ self._contained_objects = value
145
+
146
+ @_is_a_robot_rdr.decorator
147
+ def is_a_robot(self) -> bool:
148
+ pass
149
+
150
+ @_select_parts_rdr.decorator
151
+ def select_objects_that_are_parts_of_robot(self, objects: List[PhysicalObject], robot: Robot) -> List[PhysicalObject]:
152
+ pass
153
+
154
+ def __str__(self):
155
+ return self.name
156
+
157
+ def __repr__(self):
158
+ return self.name
159
+
160
+
161
+ class Part(PhysicalObject):
162
+ ...
163
+
164
+
165
+ class Robot(PhysicalObject):
166
+
167
+ def __init__(self, name: str, parts: Optional[List[Part]] = None):
168
+ super().__init__(name)
169
+ self.parts: List[Part] = parts if parts else []
111
170
 
112
171
 
113
172
  class Base(sqlalchemy.orm.DeclarativeBase):
@@ -119,7 +178,7 @@ class HabitatTable(MappedAsDataclass, Base):
119
178
 
120
179
  id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
121
180
  habitat: Mapped[Habitat]
122
- animal_id = mapped_column(ForeignKey("Animal.id"), init=False)
181
+ animal_id: MappedColumn = mapped_column(ForeignKey("Animal.id"), init=False)
123
182
 
124
183
  def __hash__(self):
125
184
  return hash(self.habitat)
@@ -131,7 +190,7 @@ class HabitatTable(MappedAsDataclass, Base):
131
190
  return self.__str__()
132
191
 
133
192
 
134
- class Animal(MappedAsDataclass, Base):
193
+ class MappedAnimal(MappedAsDataclass, Base):
135
194
  __tablename__ = "Animal"
136
195
 
137
196
  id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
@@ -155,3 +214,4 @@ class Animal(MappedAsDataclass, Base):
155
214
  species: Mapped[Species] = mapped_column(nullable=True)
156
215
 
157
216
  habitats: Mapped[Set[HabitatTable]] = relationship(default_factory=set)
217
+
@@ -93,9 +93,12 @@ class CallableExpression(SubclassJSONSerializer):
93
93
  """
94
94
  encapsulating_function: str = "def _get_value(case):"
95
95
 
96
- def __init__(self, user_input: Optional[str] = None, conclusion_type: Optional[Tuple[Type]] = None,
96
+ def __init__(self, user_input: Optional[str] = None,
97
+ conclusion_type: Optional[Tuple[Type]] = None,
97
98
  expression_tree: Optional[AST] = None,
98
- scope: Optional[Dict[str, Any]] = None, conclusion: Optional[Any] = None):
99
+ scope: Optional[Dict[str, Any]] = None,
100
+ conclusion: Optional[Any] = None,
101
+ mutually_exclusive: bool = True):
99
102
  """
100
103
  Create a callable expression.
101
104
 
@@ -104,6 +107,8 @@ class CallableExpression(SubclassJSONSerializer):
104
107
  :param expression_tree: The AST tree parsed from the user input.
105
108
  :param scope: The scope to use for the callable expression.
106
109
  :param conclusion: The conclusion to use for the callable expression.
110
+ :param mutually_exclusive: If True, the conclusion is mutually exclusive, i.e. the callable expression can only
111
+ return one conclusion. If False, the callable expression can return multiple conclusions.
107
112
  """
108
113
  if user_input is None and conclusion is None:
109
114
  raise ValueError("Either user_input or conclusion must be provided.")
@@ -123,6 +128,7 @@ class CallableExpression(SubclassJSONSerializer):
123
128
  self.code = compile_expression_to_code(self.expression_tree)
124
129
  self.visitor = VariableVisitor()
125
130
  self.visitor.visit(self.expression_tree)
131
+ self.mutually_exclusive: bool = mutually_exclusive
126
132
 
127
133
  def __call__(self, case: Any, **kwargs) -> Any:
128
134
  try:
@@ -134,8 +140,8 @@ class CallableExpression(SubclassJSONSerializer):
134
140
  if output is None:
135
141
  output = scope['_get_value'](case)
136
142
  if self.conclusion_type is not None:
137
- if not any([issubclass(ct, (list, set)) for ct in self.conclusion_type]) and is_iterable(output):
138
- raise ValueError(f"Expected output to be {self.conclusion_type}, but got {type(output)}")
143
+ if self.mutually_exclusive and issubclass(type(output), (list, set)):
144
+ raise ValueError(f"Mutually exclusive types cannot be lists or sets, got {type(output)}")
139
145
  output_types = {type(o) for o in make_list(output)}
140
146
  output_types.add(type(output))
141
147
  if not are_results_subclass_of_types(output_types, self.conclusion_type):
@@ -229,6 +235,7 @@ class CallableExpression(SubclassJSONSerializer):
229
235
  "scope": {k: get_full_class_name(v) for k, v in self.scope.items()
230
236
  if hasattr(v, '__module__') and hasattr(v, '__name__')},
231
237
  "conclusion": conclusion_to_json(self.conclusion),
238
+ "mutually_exclusive": self.mutually_exclusive,
232
239
  }
233
240
 
234
241
  @classmethod
@@ -237,7 +244,8 @@ class CallableExpression(SubclassJSONSerializer):
237
244
  conclusion_type=tuple(get_type_from_string(t) for t in data["conclusion_type"])
238
245
  if data["conclusion_type"] else None,
239
246
  scope={k: get_type_from_string(v) for k, v in data["scope"].items()},
240
- conclusion=SubclassJSONSerializer.from_json(data["conclusion"]))
247
+ conclusion=SubclassJSONSerializer.from_json(data["conclusion"]),
248
+ mutually_exclusive=data["mutually_exclusive"])
241
249
 
242
250
 
243
251
  def compile_expression_to_code(expression_tree: AST) -> Any:
@@ -11,7 +11,7 @@ from sqlalchemy.orm import DeclarativeBase as SQLTable, MappedColumn as SQLColum
11
11
  from typing_extensions import Any, Optional, Dict, Type, Set, Hashable, Union, List, TYPE_CHECKING
12
12
 
13
13
  from ..utils import make_set, row_to_dict, table_rows_as_str, get_value_type_from_type_hint, SubclassJSONSerializer, \
14
- get_full_class_name, get_type_from_string, make_list, is_iterable, serialize_dataclass, dataclass_to_dict
14
+ get_full_class_name, get_type_from_string, make_list, is_iterable, serialize_dataclass, dataclass_to_dict, copy_case
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from ripple_down_rules.rules import Rule
@@ -65,7 +65,7 @@ class Case(UserDict, SubclassJSONSerializer):
65
65
  new_list.extend(make_list(value))
66
66
  super().__setitem__(name, new_list)
67
67
  else:
68
- super().__setitem__(name, self[name])
68
+ super().__setitem__(name, value)
69
69
  else:
70
70
  super().__setitem__(name, value)
71
71
  setattr(self, name, self[name])
@@ -102,6 +102,29 @@ class Case(UserDict, SubclassJSONSerializer):
102
102
  data[k] = SubclassJSONSerializer.from_json(v)
103
103
  return cls(_obj_type=obj_type, _id=id_, _name=name, **data)
104
104
 
105
+ def __deepcopy__(self, memo: Dict[Hashable, Any]) -> Case:
106
+ """
107
+ Create a deep copy of the case.
108
+
109
+ :param memo: A dictionary to keep track of objects that have already been copied.
110
+ :return: A deep copy of the case.
111
+ """
112
+ new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
113
+ for k, v in self.items():
114
+ new_case[k] = deepcopy(v)
115
+ return new_case
116
+
117
+ def __copy__(self) -> Case:
118
+ """
119
+ Create a shallow copy of the case.
120
+
121
+ :return: A shallow copy of the case.
122
+ """
123
+ new_case = Case(self._obj_type, _id=self._id, _name=self._name, original_object=self._original_object)
124
+ for k, v in self.items():
125
+ new_case[k] = copy(v)
126
+ return new_case
127
+
105
128
 
106
129
  @dataclass
107
130
  class CaseAttributeValue(SubclassJSONSerializer):
@@ -220,11 +243,16 @@ def create_case(obj: Any, recursion_idx: int = 0, max_recursion_idx: int = 0,
220
243
  return create_cases_from_dataframe(obj, obj_name)
221
244
  if isinstance(obj, Case) or (is_dataclass(obj) and not isinstance(obj, SQLTable)):
222
245
  return obj
223
- if ((recursion_idx > max_recursion_idx) or (obj.__class__.__module__ == "builtins")
246
+ if ((recursion_idx > max_recursion_idx)
247
+ or (obj.__class__.__module__ == "builtins" and not isinstance(obj, (list, set, dict)))
224
248
  or (obj.__class__ in [MetaData, registry])):
225
249
  return Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj,
226
250
  **{obj_name or obj.__class__.__name__: make_list(obj) if parent_is_iterable else obj})
227
251
  case = Case(type(obj), _id=id(obj), _name=obj_name, original_object=obj)
252
+ if isinstance(obj, dict):
253
+ for k, v in obj.items():
254
+ case = create_or_update_case_from_attribute(v, k, obj, obj_name, recursion_idx,
255
+ max_recursion_idx, parent_is_iterable, case)
228
256
  for attr in dir(obj):
229
257
  if attr.startswith("_") or callable(getattr(obj, attr)):
230
258
  continue
@@ -322,9 +350,9 @@ def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] =
322
350
  if last_evaluated_rule and last_evaluated_rule.fired:
323
351
  corner_row_dict = dataclass_to_dict(last_evaluated_rule.corner_case)
324
352
  else:
325
- case_dict = case
353
+ case_dict = copy_case(case)
326
354
  if last_evaluated_rule and last_evaluated_rule.fired:
327
- corner_row_dict = corner_case
355
+ corner_row_dict = copy_case(corner_case)
328
356
 
329
357
  if corner_row_dict:
330
358
  corner_conclusion = last_evaluated_rule.conclusion(case)
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import typing
4
5
  from dataclasses import dataclass, field
5
6
 
6
7
  import typing_extensions
7
8
  from sqlalchemy.orm import DeclarativeBase as SQLTable
8
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List
9
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set
9
10
 
10
11
  from .callable_expression import CallableExpression
11
12
  from .case import create_case, Case
12
- from ..utils import copy_case, make_list, make_set
13
+ from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, get_value_type_from_type_hint, \
14
+ typing_to_python_type
13
15
 
14
16
 
15
17
  @dataclass
@@ -61,6 +63,15 @@ class CaseQuery:
61
63
  """
62
64
  The conditions that must be satisfied for the target value to be valid.
63
65
  """
66
+ is_function: bool = False
67
+ """
68
+ Whether the case is a dict representing the arguments of an actual function or not,
69
+ most likely means it came from RDRDecorator, the the rdr takes function arguments and outputs the function output.
70
+ """
71
+ function_args_type_hints: Optional[Dict[str, Type]] = None
72
+ """
73
+ The type hints of the function arguments. This is used to recreate the function signature.
74
+ """
64
75
 
65
76
  @property
66
77
  def case_type(self) -> Type:
@@ -117,10 +128,20 @@ class CaseQuery:
117
128
  """
118
129
  :return: The type of the attribute.
119
130
  """
120
- if not self.mutually_exclusive and (list not in make_list(self._attribute_types)):
121
- self._attribute_types = tuple(set(make_list(self._attribute_types) + [set, list]))
122
- elif not isinstance(self._attribute_types, tuple):
123
- self._attribute_types = tuple(make_list(self._attribute_types))
131
+ if not isinstance(self._attribute_types, tuple):
132
+ self._attribute_types = tuple(make_set(self._attribute_types))
133
+ origin, args = get_origin_and_args_from_type_hint(self._attribute_types)
134
+ if origin is not None:
135
+ att_types = make_set(origin)
136
+ if origin in (list, set, tuple, List, Set, Union, Tuple):
137
+ att_types.update(make_set(args))
138
+ elif origin in (dict, Dict):
139
+ # ignore the key type
140
+ if args and len(args) > 1:
141
+ att_types.update(make_set(args[1]))
142
+ self._attribute_types = tuple(att_types)
143
+ if not self.mutually_exclusive and (list not in self._attribute_types):
144
+ self._attribute_types = tuple(make_list(self._attribute_types) + [set, list])
124
145
  return self._attribute_types
125
146
 
126
147
  @attribute_type.setter
@@ -151,7 +172,7 @@ class CaseQuery:
151
172
  """
152
173
  if (self._target is not None) and (not isinstance(self._target, CallableExpression)):
153
174
  self._target = CallableExpression(conclusion=self._target, conclusion_type=self.attribute_type,
154
- scope=self.scope)
175
+ scope=self.scope, mutually_exclusive=self.mutually_exclusive)
155
176
  return self._target
156
177
 
157
178
  @target.setter
@@ -195,4 +216,5 @@ class CaseQuery:
195
216
  return CaseQuery(self.original_case, self.attribute_name, self.attribute_type,
196
217
  self.mutually_exclusive, _target=self.target, default_value=self.default_value,
197
218
  scope=self.scope, _case=copy_case(self.case), _target_value=self.target_value,
198
- conditions=self.conditions)
219
+ conditions=self.conditions, is_function=self.is_function,
220
+ function_args_type_hints=self.function_args_type_hints)
@@ -2,11 +2,40 @@ from __future__ import annotations
2
2
 
3
3
  from enum import auto, Enum
4
4
 
5
- from typing_extensions import List, Dict, Any
5
+ from typing_extensions import List, Dict, Any, Type
6
6
 
7
7
  from ripple_down_rules.utils import SubclassJSONSerializer
8
8
 
9
9
 
10
+ class Editor(str, Enum):
11
+ """
12
+ The editor that is used to edit the rules.
13
+ """
14
+ Pycharm = "pycharm"
15
+ """
16
+ PyCharm editor.
17
+ """
18
+ Code = "code"
19
+ """
20
+ Visual Studio Code editor.
21
+ """
22
+ CodeServer = "code-server"
23
+ """
24
+ Visual Studio Code server editor.
25
+ """
26
+ @classmethod
27
+ def from_str(cls, editor: str) -> Editor:
28
+ """
29
+ Convert a string value to an Editor enum.
30
+
31
+ :param editor: The string that represents the editor name.
32
+ :return: The Editor enum.
33
+ """
34
+ if editor not in cls._value2member_map_:
35
+ raise ValueError(f"Editor {editor} is not supported.")
36
+ return cls._value2member_map_[editor]
37
+
38
+
10
39
  class Category(str, SubclassJSONSerializer, Enum):
11
40
 
12
41
  @classmethod
@@ -138,7 +138,8 @@ class Human(Expert):
138
138
  expert_input = self.all_expert_answers.pop(0)
139
139
  if expert_input is not None:
140
140
  expression = CallableExpression(expert_input, case_query.attribute_type,
141
- scope=case_query.scope)
141
+ scope=case_query.scope,
142
+ mutually_exclusive=case_query.mutually_exclusive)
142
143
  else:
143
144
  show_current_and_corner_cases(case_query.case)
144
145
  expert_input, expression = prompt_user_for_expression(case_query, PromptFor.Conclusion)