ripple-down-rules 0.4.8__py3-none-any.whl → 0.4.9__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.
- ripple_down_rules/__init__.py +5 -0
- ripple_down_rules/datastructures/callable_expression.py +20 -1
- ripple_down_rules/datastructures/case.py +8 -6
- ripple_down_rules/datastructures/dataclasses.py +9 -1
- ripple_down_rules/experts.py +194 -33
- ripple_down_rules/rdr.py +196 -114
- ripple_down_rules/rdr_decorators.py +73 -52
- ripple_down_rules/rules.py +5 -4
- ripple_down_rules/start-code-server.sh +27 -0
- ripple_down_rules/user_interface/gui.py +17 -34
- ripple_down_rules/user_interface/ipython_custom_shell.py +2 -3
- ripple_down_rules/user_interface/object_diagram.py +7 -1
- ripple_down_rules/user_interface/prompt.py +9 -4
- ripple_down_rules/user_interface/template_file_creator.py +7 -8
- ripple_down_rules/utils.py +59 -21
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/METADATA +10 -8
- ripple_down_rules-0.4.9.dist-info/RECORD +26 -0
- ripple_down_rules-0.4.8.dist-info/RECORD +0 -25
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/top_level.txt +0 -0
ripple_down_rules/__init__.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import ast
|
4
4
|
import logging
|
5
|
+
import os
|
5
6
|
from _ast import AST
|
6
7
|
from enum import Enum
|
7
8
|
|
@@ -10,7 +11,7 @@ from typing_extensions import Type, Optional, Any, List, Union, Tuple, Dict, Set
|
|
10
11
|
from .case import create_case, Case
|
11
12
|
from ..utils import SubclassJSONSerializer, get_full_class_name, get_type_from_string, conclusion_to_json, is_iterable, \
|
12
13
|
build_user_input_from_conclusion, encapsulate_user_input, extract_function_source, are_results_subclass_of_types, \
|
13
|
-
make_list
|
14
|
+
make_list, get_imports_from_scope
|
14
15
|
|
15
16
|
|
16
17
|
class VariableVisitor(ast.NodeVisitor):
|
@@ -175,6 +176,24 @@ class CallableExpression(SubclassJSONSerializer):
|
|
175
176
|
return
|
176
177
|
self.user_input = self.encapsulating_function + '\n' + new_function_body
|
177
178
|
|
179
|
+
def write_to_python_file(self, file_path: str, append: bool = False):
|
180
|
+
"""
|
181
|
+
Write the callable expression to a python file.
|
182
|
+
|
183
|
+
:param file_path: The path to the file where the callable expression will be written.
|
184
|
+
:param append: If True, the callable expression will be appended to the file. If False,
|
185
|
+
the file will be overwritten.
|
186
|
+
"""
|
187
|
+
imports = '\n'.join(get_imports_from_scope(self.scope))
|
188
|
+
if append and os.path.exists(file_path):
|
189
|
+
with open(file_path, 'a') as f:
|
190
|
+
f.write('\n\n\n' + imports + '\n\n\n')
|
191
|
+
f.write(self.user_input)
|
192
|
+
else:
|
193
|
+
with open(file_path, 'w') as f:
|
194
|
+
f.write(imports + '\n\n\n')
|
195
|
+
f.write(self.user_input)
|
196
|
+
|
178
197
|
@property
|
179
198
|
def user_input(self):
|
180
199
|
"""
|
@@ -7,7 +7,7 @@ from enum import Enum
|
|
7
7
|
|
8
8
|
from pandas import DataFrame
|
9
9
|
from sqlalchemy import MetaData
|
10
|
-
from sqlalchemy.orm import DeclarativeBase as SQLTable,
|
10
|
+
from sqlalchemy.orm import DeclarativeBase as SQLTable, registry
|
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, \
|
@@ -354,11 +354,13 @@ def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] =
|
|
354
354
|
if last_evaluated_rule and last_evaluated_rule.fired:
|
355
355
|
corner_row_dict = copy_case(corner_case)
|
356
356
|
|
357
|
+
case_dict.update(targets)
|
358
|
+
case_dict.update(current_conclusions)
|
359
|
+
all_table_rows = [case_dict]
|
357
360
|
if corner_row_dict:
|
358
361
|
corner_conclusion = last_evaluated_rule.conclusion(case)
|
359
362
|
corner_row_dict.update({corner_conclusion.__class__.__name__: corner_conclusion})
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
print(table_rows_as_str(case_dict))
|
363
|
+
all_table_rows.append(corner_row_dict)
|
364
|
+
# print(table_rows_as_str(corner_row_dict))
|
365
|
+
print("\n" + "=" * 50)
|
366
|
+
print(table_rows_as_str(all_table_rows))
|
@@ -78,7 +78,15 @@ class CaseQuery:
|
|
78
78
|
"""
|
79
79
|
:return: The type of the case that the attribute belongs to.
|
80
80
|
"""
|
81
|
-
|
81
|
+
if self.is_function:
|
82
|
+
if self.function_args_type_hints is not None:
|
83
|
+
func_args = [arg for name, arg in self.function_args_type_hints.items() if name != 'return']
|
84
|
+
case_type_args = Union[tuple(func_args)]
|
85
|
+
else:
|
86
|
+
case_type_args = Any
|
87
|
+
return Dict[str, case_type_args]
|
88
|
+
else:
|
89
|
+
return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
|
82
90
|
|
83
91
|
@property
|
84
92
|
def case(self) -> Any:
|
ripple_down_rules/experts.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import ast
|
3
4
|
import json
|
5
|
+
import logging
|
6
|
+
import os
|
4
7
|
from abc import ABC, abstractmethod
|
5
8
|
|
6
9
|
from typing_extensions import Optional, TYPE_CHECKING, List
|
@@ -9,7 +12,12 @@ from .datastructures.callable_expression import CallableExpression
|
|
9
12
|
from .datastructures.enums import PromptFor
|
10
13
|
from .datastructures.dataclasses import CaseQuery
|
11
14
|
from .datastructures.case import show_current_and_corner_cases
|
12
|
-
from .
|
15
|
+
from .utils import extract_imports, extract_function_source, get_imports_from_scope, encapsulate_user_input
|
16
|
+
|
17
|
+
try:
|
18
|
+
from .user_interface.gui import RDRCaseViewer
|
19
|
+
except ImportError as e:
|
20
|
+
RDRCaseViewer = None
|
13
21
|
from .user_interface.prompt import UserPrompt
|
14
22
|
|
15
23
|
if TYPE_CHECKING:
|
@@ -32,10 +40,19 @@ class Expert(ABC):
|
|
32
40
|
A flag to indicate if the expert should use loaded answers or not.
|
33
41
|
"""
|
34
42
|
|
35
|
-
def __init__(self, use_loaded_answers: bool =
|
43
|
+
def __init__(self, use_loaded_answers: bool = True,
|
44
|
+
append: bool = False,
|
45
|
+
answers_save_path: Optional[str] = None):
|
36
46
|
self.all_expert_answers = []
|
37
47
|
self.use_loaded_answers = use_loaded_answers
|
38
48
|
self.append = append
|
49
|
+
self.answers_save_path = answers_save_path
|
50
|
+
if answers_save_path is not None:
|
51
|
+
if use_loaded_answers:
|
52
|
+
self.load_answers(answers_save_path)
|
53
|
+
else:
|
54
|
+
os.remove(answers_save_path + '.py')
|
55
|
+
self.append = True
|
39
56
|
|
40
57
|
@abstractmethod
|
41
58
|
def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
|
@@ -59,46 +76,138 @@ class Expert(ABC):
|
|
59
76
|
:return: A callable expression that can be called with a new case as an argument.
|
60
77
|
"""
|
61
78
|
|
79
|
+
def clear_answers(self, path: Optional[str] = None):
|
80
|
+
"""
|
81
|
+
Clear the expert answers.
|
62
82
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
83
|
+
:param path: The path to clear the answers from. If None, the answers will be cleared from the
|
84
|
+
answers_save_path attribute.
|
85
|
+
"""
|
86
|
+
if path is None and self.answers_save_path is None:
|
87
|
+
raise ValueError("No path provided to clear expert answers, either provide a path or set the "
|
88
|
+
"answers_save_path attribute.")
|
89
|
+
if path is None:
|
90
|
+
path = self.answers_save_path
|
91
|
+
if os.path.exists(path + '.json'):
|
92
|
+
os.remove(path + '.json')
|
93
|
+
if os.path.exists(path + '.py'):
|
94
|
+
os.remove(path + '.py')
|
95
|
+
self.all_expert_answers = []
|
67
96
|
|
68
|
-
def
|
97
|
+
def save_answers(self, path: Optional[str] = None):
|
69
98
|
"""
|
70
|
-
|
99
|
+
Save the expert answers to a file.
|
71
100
|
|
72
|
-
:param
|
101
|
+
:param path: The path to save the answers to.
|
73
102
|
"""
|
74
|
-
|
75
|
-
|
103
|
+
if path is None and self.answers_save_path is None:
|
104
|
+
raise ValueError("No path provided to save expert answers, either provide a path or set the "
|
105
|
+
"answers_save_path attribute.")
|
106
|
+
if path is None:
|
107
|
+
path = self.answers_save_path
|
108
|
+
is_json = os.path.exists(path + '.json')
|
109
|
+
if is_json:
|
110
|
+
self._save_to_json(path)
|
111
|
+
else:
|
112
|
+
self._save_to_python(path)
|
76
113
|
|
77
|
-
def
|
114
|
+
def _save_to_json(self, path: str):
|
78
115
|
"""
|
79
|
-
Save the expert answers to a file.
|
116
|
+
Save the expert answers to a JSON file.
|
80
117
|
|
81
118
|
:param path: The path to save the answers to.
|
82
119
|
"""
|
83
|
-
|
120
|
+
all_answers = self.all_expert_answers
|
121
|
+
if self.append and os.path.exists(path + '.json'):
|
84
122
|
# read the file and append the new answers
|
85
123
|
with open(path + '.json', "r") as f:
|
86
|
-
|
87
|
-
all_answers
|
88
|
-
|
89
|
-
|
90
|
-
else:
|
91
|
-
with open(path + '.json', "w") as f:
|
92
|
-
json.dump(self.all_expert_answers, f)
|
124
|
+
old_answers = json.load(f)
|
125
|
+
all_answers = old_answers + all_answers
|
126
|
+
with open(path + '.json', "w") as f:
|
127
|
+
json.dump(all_answers, f)
|
93
128
|
|
94
|
-
def
|
129
|
+
def _save_to_python(self, path: str):
|
130
|
+
"""
|
131
|
+
Save the expert answers to a Python file.
|
132
|
+
|
133
|
+
:param path: The path to save the answers to.
|
134
|
+
"""
|
135
|
+
dir_name = os.path.dirname(path)
|
136
|
+
if not os.path.exists(dir_name + '/__init__.py'):
|
137
|
+
os.makedirs(dir_name, exist_ok=True)
|
138
|
+
with open(dir_name + '/__init__.py', 'w') as f:
|
139
|
+
f.write('# This is an empty init file to make the directory a package.\n')
|
140
|
+
action = 'w' if not self.append else 'a'
|
141
|
+
with open(path + '.py', action) as f:
|
142
|
+
for scope, func_source in self.all_expert_answers:
|
143
|
+
if len(scope) > 0:
|
144
|
+
imports = '\n'.join(get_imports_from_scope(scope)) + '\n\n\n'
|
145
|
+
else:
|
146
|
+
imports = ''
|
147
|
+
if func_source is not None:
|
148
|
+
func_source = encapsulate_user_input(func_source, CallableExpression.encapsulating_function)
|
149
|
+
else:
|
150
|
+
func_source = 'pass # No user input provided for this case.\n'
|
151
|
+
f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
|
152
|
+
|
153
|
+
def load_answers(self, path: Optional[str] = None):
|
95
154
|
"""
|
96
155
|
Load the expert answers from a file.
|
97
156
|
|
157
|
+
:param path: The path to load the answers from.
|
158
|
+
"""
|
159
|
+
if path is None and self.answers_save_path is None:
|
160
|
+
raise ValueError("No path provided to load expert answers from, either provide a path or set the "
|
161
|
+
"answers_save_path attribute.")
|
162
|
+
if path is None:
|
163
|
+
path = self.answers_save_path
|
164
|
+
is_json = os.path.exists(path + '.json')
|
165
|
+
if is_json:
|
166
|
+
self._load_answers_from_json(path)
|
167
|
+
elif os.path.exists(path + '.py'):
|
168
|
+
self._load_answers_from_python(path)
|
169
|
+
|
170
|
+
def _load_answers_from_json(self, path: str):
|
171
|
+
"""
|
172
|
+
Load the expert answers from a JSON file.
|
173
|
+
|
98
174
|
:param path: The path to load the answers from.
|
99
175
|
"""
|
100
176
|
with open(path + '.json', "r") as f:
|
101
|
-
|
177
|
+
all_answers = json.load(f)
|
178
|
+
self.all_expert_answers = [({}, answer) for answer in all_answers]
|
179
|
+
|
180
|
+
def _load_answers_from_python(self, path: str):
|
181
|
+
"""
|
182
|
+
Load the expert answers from a Python file.
|
183
|
+
|
184
|
+
:param path: The path to load the answers from.
|
185
|
+
"""
|
186
|
+
file_path = path + '.py'
|
187
|
+
with open(file_path, "r") as f:
|
188
|
+
all_answers = f.read().split('\n\n\n\'===New Answer===\'\n\n\n')
|
189
|
+
for answer in all_answers:
|
190
|
+
answer = answer.strip('\n').strip()
|
191
|
+
if 'def ' not in answer and 'pass' in answer:
|
192
|
+
self.all_expert_answers.append(({}, None))
|
193
|
+
scope = extract_imports(tree=ast.parse(answer))
|
194
|
+
func_source = list(extract_function_source(file_path, []).values())[0]
|
195
|
+
self.all_expert_answers.append((scope, func_source))
|
196
|
+
|
197
|
+
|
198
|
+
class Human(Expert):
|
199
|
+
"""
|
200
|
+
The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
|
201
|
+
"""
|
202
|
+
|
203
|
+
def __init__(self, viewer: Optional[RDRCaseViewer] = None, **kwargs):
|
204
|
+
"""
|
205
|
+
Initialize the Human expert.
|
206
|
+
|
207
|
+
:param viewer: The RDRCaseViewer instance to use for prompting the user.
|
208
|
+
"""
|
209
|
+
super().__init__(**kwargs)
|
210
|
+
self.user_prompt = UserPrompt(viewer)
|
102
211
|
|
103
212
|
def ask_for_conditions(self, case_query: CaseQuery,
|
104
213
|
last_evaluated_rule: Optional[Rule] = None) \
|
@@ -121,13 +230,18 @@ class Human(Expert):
|
|
121
230
|
if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
|
122
231
|
self.use_loaded_answers = False
|
123
232
|
if self.use_loaded_answers:
|
124
|
-
|
125
|
-
|
233
|
+
try:
|
234
|
+
loaded_scope, user_input = self.all_expert_answers.pop(0)
|
235
|
+
except IndexError:
|
236
|
+
self.use_loaded_answers = False
|
237
|
+
if user_input is not None:
|
126
238
|
condition = CallableExpression(user_input, bool, scope=case_query.scope)
|
127
239
|
else:
|
128
240
|
user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions)
|
129
241
|
if not self.use_loaded_answers:
|
130
|
-
self.all_expert_answers.append(user_input)
|
242
|
+
self.all_expert_answers.append((condition.scope, user_input))
|
243
|
+
if self.answers_save_path is not None:
|
244
|
+
self.save_answers()
|
131
245
|
case_query.conditions = condition
|
132
246
|
return condition
|
133
247
|
|
@@ -139,18 +253,65 @@ class Human(Expert):
|
|
139
253
|
:return: The conclusion for the case as a callable expression.
|
140
254
|
"""
|
141
255
|
expression: Optional[CallableExpression] = None
|
256
|
+
expert_input: Optional[str] = None
|
142
257
|
if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
|
143
258
|
self.use_loaded_answers = False
|
144
259
|
if self.use_loaded_answers:
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
260
|
+
try:
|
261
|
+
loaded_scope, expert_input = self.all_expert_answers.pop(0)
|
262
|
+
if expert_input is not None:
|
263
|
+
expression = CallableExpression(expert_input, case_query.attribute_type,
|
264
|
+
scope=case_query.scope,
|
265
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
266
|
+
except IndexError:
|
267
|
+
self.use_loaded_answers = False
|
268
|
+
if not self.use_loaded_answers:
|
151
269
|
if self.user_prompt.viewer is None:
|
152
270
|
show_current_and_corner_cases(case_query.case)
|
153
271
|
expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion)
|
154
|
-
|
272
|
+
if expression is None:
|
273
|
+
self.all_expert_answers.append(({}, None))
|
274
|
+
else:
|
275
|
+
self.all_expert_answers.append((expression.scope, expert_input))
|
276
|
+
if self.answers_save_path is not None:
|
277
|
+
self.save_answers()
|
155
278
|
case_query.target = expression
|
156
279
|
return expression
|
280
|
+
|
281
|
+
|
282
|
+
class File(Expert):
|
283
|
+
"""
|
284
|
+
The File Expert class, an expert that reads the answers from a file.
|
285
|
+
This is used for testing purposes.
|
286
|
+
"""
|
287
|
+
|
288
|
+
def __init__(self, filename: str, **kwargs):
|
289
|
+
"""
|
290
|
+
Initialize the File expert.
|
291
|
+
|
292
|
+
:param filename: The path to the file containing the expert answers.
|
293
|
+
"""
|
294
|
+
super().__init__(**kwargs)
|
295
|
+
self.filename = filename
|
296
|
+
self.load_answers(filename)
|
297
|
+
|
298
|
+
def ask_for_conditions(self, case_query: CaseQuery,
|
299
|
+
last_evaluated_rule: Optional[Rule] = None) -> CallableExpression:
|
300
|
+
loaded_scope, user_input = self.all_expert_answers.pop(0)
|
301
|
+
if user_input:
|
302
|
+
condition = CallableExpression(user_input, bool, scope=case_query.scope)
|
303
|
+
else:
|
304
|
+
raise ValueError("No user input found in the expert answers file.")
|
305
|
+
case_query.conditions = condition
|
306
|
+
return condition
|
307
|
+
|
308
|
+
def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
|
309
|
+
loaded_scope, expert_input = self.all_expert_answers.pop(0)
|
310
|
+
if expert_input is not None:
|
311
|
+
expression = CallableExpression(expert_input, case_query.attribute_type,
|
312
|
+
scope=case_query.scope,
|
313
|
+
mutually_exclusive=case_query.mutually_exclusive)
|
314
|
+
else:
|
315
|
+
raise ValueError("No expert input found in the expert answers file.")
|
316
|
+
case_query.target = expression
|
317
|
+
return expression
|