ripple-down-rules 0.0.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.
- ripple_down_rules/__init__.py +0 -0
- ripple_down_rules/datasets.py +148 -0
- ripple_down_rules/datastructures/__init__.py +4 -0
- ripple_down_rules/datastructures/callable_expression.py +237 -0
- ripple_down_rules/datastructures/dataclasses.py +76 -0
- ripple_down_rules/datastructures/enums.py +173 -0
- ripple_down_rules/datastructures/generated/__init__.py +0 -0
- ripple_down_rules/datastructures/generated/column/__init__.py +0 -0
- ripple_down_rules/datastructures/generated/row/__init__.py +0 -0
- ripple_down_rules/datastructures/table.py +544 -0
- ripple_down_rules/experts.py +281 -0
- ripple_down_rules/failures.py +10 -0
- ripple_down_rules/prompt.py +101 -0
- ripple_down_rules/rdr.py +687 -0
- ripple_down_rules/rules.py +260 -0
- ripple_down_rules/utils.py +463 -0
- ripple_down_rules-0.0.0.dist-info/METADATA +54 -0
- ripple_down_rules-0.0.0.dist-info/RECORD +20 -0
- ripple_down_rules-0.0.0.dist-info/WHEEL +5 -0
- ripple_down_rules-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
|
6
|
+
from sqlalchemy.orm import DeclarativeBase as SQLTable, MappedColumn as SQLColumn, Session
|
7
|
+
from typing_extensions import Optional, Dict, TYPE_CHECKING, List, Tuple, Type, Union, Any
|
8
|
+
|
9
|
+
from .datastructures import (Case, PromptFor, CallableExpression, Column, CaseQuery)
|
10
|
+
from .datastructures.table import show_current_and_corner_cases
|
11
|
+
from .prompt import prompt_user_for_expression, prompt_user_about_case
|
12
|
+
from .utils import get_all_subclasses
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from .rdr import Rule
|
16
|
+
|
17
|
+
|
18
|
+
class Expert(ABC):
|
19
|
+
"""
|
20
|
+
The Abstract Expert class, all experts should inherit from this class.
|
21
|
+
An expert is a class that can provide differentiating features and conclusions for a case when asked.
|
22
|
+
The expert can compare a case with a corner case and provide the differentiating features and can also
|
23
|
+
provide one or multiple conclusions for a case.
|
24
|
+
"""
|
25
|
+
all_expert_answers: Optional[List] = None
|
26
|
+
"""
|
27
|
+
A list of all expert answers, used for testing purposes.
|
28
|
+
"""
|
29
|
+
use_loaded_answers: bool = False
|
30
|
+
"""
|
31
|
+
A flag to indicate if the expert should use loaded answers or not.
|
32
|
+
"""
|
33
|
+
known_categories: Optional[Dict[str, Type[Column]]] = None
|
34
|
+
"""
|
35
|
+
The known categories (i.e. Column types) to use.
|
36
|
+
"""
|
37
|
+
|
38
|
+
@abstractmethod
|
39
|
+
def ask_for_conditions(self, x: Case, targets: List[Column], last_evaluated_rule: Optional[Rule] = None) \
|
40
|
+
-> CallableExpression:
|
41
|
+
"""
|
42
|
+
Ask the expert to provide the differentiating features between two cases or unique features for a case
|
43
|
+
that doesn't have a corner case to compare to.
|
44
|
+
|
45
|
+
:param x: The case to classify.
|
46
|
+
:param targets: The target categories to compare the case with.
|
47
|
+
:param last_evaluated_rule: The last evaluated rule.
|
48
|
+
:return: The differentiating features as new rule conditions.
|
49
|
+
"""
|
50
|
+
pass
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def ask_for_extra_conclusions(self, x: Case, current_conclusions: List[Column]) \
|
54
|
+
-> Dict[Column, CallableExpression]:
|
55
|
+
"""
|
56
|
+
Ask the expert to provide extra conclusions for a case by providing a pair of category and conditions for
|
57
|
+
that category.
|
58
|
+
|
59
|
+
:param x: The case to classify.
|
60
|
+
:param current_conclusions: The current conclusions for the case.
|
61
|
+
:return: The extra conclusions for the case.
|
62
|
+
"""
|
63
|
+
pass
|
64
|
+
|
65
|
+
@abstractmethod
|
66
|
+
def ask_if_conclusion_is_correct(self, x: Case, conclusion: Column,
|
67
|
+
targets: Optional[List[Column]] = None,
|
68
|
+
current_conclusions: Optional[List[Column]] = None) -> bool:
|
69
|
+
"""
|
70
|
+
Ask the expert if the conclusion is correct.
|
71
|
+
|
72
|
+
:param x: The case to classify.
|
73
|
+
:param conclusion: The conclusion to check.
|
74
|
+
:param targets: The target categories to compare the case with.
|
75
|
+
:param current_conclusions: The current conclusions for the case.
|
76
|
+
"""
|
77
|
+
pass
|
78
|
+
|
79
|
+
def ask_for_conclusion(self, case_query: CaseQuery,
|
80
|
+
session: Optional[Session] = None) -> Optional[CallableExpression]:
|
81
|
+
"""
|
82
|
+
Ask the expert to provide a relational conclusion for the case.
|
83
|
+
|
84
|
+
:param case_query: The case query containing the case to find a conclusion for.
|
85
|
+
:param session: The sqlalchemy orm session to use if the case is a Table.
|
86
|
+
:return: A callable expression that can be called with a new case as an argument.
|
87
|
+
"""
|
88
|
+
|
89
|
+
|
90
|
+
class Human(Expert):
|
91
|
+
"""
|
92
|
+
The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
|
93
|
+
"""
|
94
|
+
|
95
|
+
def __init__(self, use_loaded_answers: bool = False, session: Optional[Session] = None):
|
96
|
+
self.all_expert_answers = []
|
97
|
+
self.use_loaded_answers = use_loaded_answers
|
98
|
+
self.session = session
|
99
|
+
|
100
|
+
def save_answers(self, path: str):
|
101
|
+
"""
|
102
|
+
Save the expert answers to a file.
|
103
|
+
|
104
|
+
:param path: The path to save the answers to.
|
105
|
+
"""
|
106
|
+
with open(path + '.json', "w") as f:
|
107
|
+
json.dump(self.all_expert_answers, f)
|
108
|
+
|
109
|
+
def load_answers(self, path: str):
|
110
|
+
"""
|
111
|
+
Load the expert answers from a file.
|
112
|
+
|
113
|
+
:param path: The path to load the answers from.
|
114
|
+
"""
|
115
|
+
with open(path + '.json', "r") as f:
|
116
|
+
self.all_expert_answers = json.load(f)
|
117
|
+
|
118
|
+
def ask_for_conditions(self, case: Case,
|
119
|
+
targets: Union[List[Column], List[Column]],
|
120
|
+
last_evaluated_rule: Optional[Rule] = None) \
|
121
|
+
-> CallableExpression:
|
122
|
+
if not self.use_loaded_answers:
|
123
|
+
show_current_and_corner_cases(case, targets, last_evaluated_rule=last_evaluated_rule)
|
124
|
+
return self._get_conditions(case, targets)
|
125
|
+
|
126
|
+
def _get_conditions(self, case: Case, targets: List[Column]) \
|
127
|
+
-> CallableExpression:
|
128
|
+
"""
|
129
|
+
Ask the expert to provide the differentiating features between two cases or unique features for a case
|
130
|
+
that doesn't have a corner case to compare to.
|
131
|
+
|
132
|
+
:param case: The case to classify.
|
133
|
+
:param targets: The target categories to compare the case with.
|
134
|
+
:return: The differentiating features as new rule conditions.
|
135
|
+
"""
|
136
|
+
targets = targets if isinstance(targets, list) else [targets]
|
137
|
+
condition = None
|
138
|
+
for target in targets:
|
139
|
+
target_name = target.__class__.__name__
|
140
|
+
user_input = None
|
141
|
+
if self.use_loaded_answers:
|
142
|
+
user_input = self.all_expert_answers.pop(0)
|
143
|
+
if user_input:
|
144
|
+
condition = CallableExpression(user_input, bool, session=self.session)
|
145
|
+
else:
|
146
|
+
user_input, condition = prompt_user_for_expression(case, PromptFor.Conditions, target_name, bool)
|
147
|
+
if not self.use_loaded_answers:
|
148
|
+
self.all_expert_answers.append(user_input)
|
149
|
+
return condition
|
150
|
+
|
151
|
+
def ask_for_extra_conclusions(self, case: Case, current_conclusions: List[Column]) \
|
152
|
+
-> Dict[Column, CallableExpression]:
|
153
|
+
"""
|
154
|
+
Ask the expert to provide extra conclusions for a case by providing a pair of category and conditions for
|
155
|
+
that category.
|
156
|
+
|
157
|
+
:param case: The case to classify.
|
158
|
+
:param current_conclusions: The current conclusions for the case.
|
159
|
+
:return: The extra conclusions for the case.
|
160
|
+
"""
|
161
|
+
extra_conclusions = {}
|
162
|
+
while True:
|
163
|
+
category = self.ask_for_conclusion(CaseQuery(case), current_conclusions)
|
164
|
+
if not category:
|
165
|
+
break
|
166
|
+
extra_conclusions[category] = self._get_conditions(case, category)
|
167
|
+
return extra_conclusions
|
168
|
+
|
169
|
+
def ask_for_conclusion(self, case_query: CaseQuery,
|
170
|
+
current_conclusions: Optional[List[Any]] = None)\
|
171
|
+
-> Optional[CallableExpression]:
|
172
|
+
"""
|
173
|
+
Ask the expert to provide a conclusion for the case.
|
174
|
+
|
175
|
+
:param case_query: The case query containing the case to find a conclusion for.
|
176
|
+
:param current_conclusions: The current conclusions for the case if any.
|
177
|
+
:return: The conclusion for the case.
|
178
|
+
"""
|
179
|
+
case = case_query.case
|
180
|
+
attribute_name = case_query.attribute_name
|
181
|
+
attribute_type = case_query.attribute_type
|
182
|
+
if self.use_loaded_answers:
|
183
|
+
expert_input = self.all_expert_answers.pop(0)
|
184
|
+
expression = CallableExpression(expert_input, conclusion_type=attribute_type, session=self.session)
|
185
|
+
else:
|
186
|
+
show_current_and_corner_cases(case, current_conclusions=current_conclusions)
|
187
|
+
expert_input, expression = prompt_user_for_expression(case, PromptFor.Conclusion, attribute_name,
|
188
|
+
attribute_type)
|
189
|
+
self.all_expert_answers.append(expert_input)
|
190
|
+
return expression
|
191
|
+
|
192
|
+
def create_category_instance(self, cat_name: str, cat_value: Union[str, int, float, set]) -> Column:
|
193
|
+
"""
|
194
|
+
Create a new category instance.
|
195
|
+
|
196
|
+
:param cat_name: The name of the category.
|
197
|
+
:param cat_value: The value of the category.
|
198
|
+
:return: A new instance of the category.
|
199
|
+
"""
|
200
|
+
category_type = self.get_category_type(cat_name)
|
201
|
+
if not category_type:
|
202
|
+
category_type = self.create_new_category_type(cat_name)
|
203
|
+
return category_type(cat_value)
|
204
|
+
|
205
|
+
def get_category_type(self, cat_name: str) -> Optional[Type[Column]]:
|
206
|
+
"""
|
207
|
+
Get the category type from the known categories.
|
208
|
+
|
209
|
+
:param cat_name: The name of the category.
|
210
|
+
:return: The category type.
|
211
|
+
"""
|
212
|
+
cat_name = cat_name.lower()
|
213
|
+
self.known_categories = get_all_subclasses(Column) if not self.known_categories else self.known_categories
|
214
|
+
self.known_categories.update(Column.registry)
|
215
|
+
category_type = None
|
216
|
+
if cat_name in self.known_categories:
|
217
|
+
category_type = self.known_categories[cat_name]
|
218
|
+
return category_type
|
219
|
+
|
220
|
+
def create_new_category_type(self, cat_name: str) -> Type[Column]:
|
221
|
+
"""
|
222
|
+
Create a new category type.
|
223
|
+
|
224
|
+
:param cat_name: The name of the category.
|
225
|
+
:return: A new category type.
|
226
|
+
"""
|
227
|
+
if self.ask_if_category_is_mutually_exclusive(cat_name):
|
228
|
+
category_type: Type[Column] = Column.create(cat_name, set(), mutually_exclusive=True)
|
229
|
+
else:
|
230
|
+
category_type: Type[Column] = Column.create(cat_name, set())
|
231
|
+
return category_type
|
232
|
+
|
233
|
+
def ask_if_category_is_mutually_exclusive(self, category_name: str) -> bool:
|
234
|
+
"""
|
235
|
+
Ask the expert if the new category can have multiple values.
|
236
|
+
|
237
|
+
:param category_name: The name of the category to ask about.
|
238
|
+
"""
|
239
|
+
question = f"Can a case have multiple values of the new category {category_name}? (y/n):"
|
240
|
+
return not self.ask_yes_no_question(question)
|
241
|
+
|
242
|
+
def ask_if_conclusion_is_correct(self, x: Case, conclusion: Column,
|
243
|
+
targets: Optional[List[Column]] = None,
|
244
|
+
current_conclusions: Optional[List[Column]] = None) -> bool:
|
245
|
+
"""
|
246
|
+
Ask the expert if the conclusion is correct.
|
247
|
+
|
248
|
+
:param x: The case to classify.
|
249
|
+
:param conclusion: The conclusion to check.
|
250
|
+
:param targets: The target categories to compare the case with.
|
251
|
+
:param current_conclusions: The current conclusions for the case.
|
252
|
+
"""
|
253
|
+
question = ""
|
254
|
+
if not self.use_loaded_answers:
|
255
|
+
targets = targets or []
|
256
|
+
targets = targets if isinstance(targets, list) else [targets]
|
257
|
+
x.conclusions = current_conclusions
|
258
|
+
x.targets = targets
|
259
|
+
question = f"Is the conclusion {conclusion} correct for the case (y/n):" \
|
260
|
+
f"\n{str(x)}"
|
261
|
+
return self.ask_yes_no_question(question)
|
262
|
+
|
263
|
+
def ask_yes_no_question(self, question: str) -> bool:
|
264
|
+
"""
|
265
|
+
Ask the expert a yes or no question.
|
266
|
+
|
267
|
+
:param question: The question to ask.
|
268
|
+
:return: The answer to the question.
|
269
|
+
"""
|
270
|
+
if not self.use_loaded_answers:
|
271
|
+
print(question)
|
272
|
+
while True:
|
273
|
+
if self.use_loaded_answers:
|
274
|
+
answer = self.all_expert_answers.pop(0)
|
275
|
+
else:
|
276
|
+
answer = input()
|
277
|
+
self.all_expert_answers.append(answer)
|
278
|
+
if answer.lower() == "y":
|
279
|
+
return True
|
280
|
+
elif answer.lower() == "n":
|
281
|
+
return False
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class InvalidOperator(Exception):
|
4
|
+
def __init__(self, rule_str: str, valid_operators: list):
|
5
|
+
self.rule_str = rule_str
|
6
|
+
self.valid_operators = valid_operators
|
7
|
+
|
8
|
+
def __str__(self):
|
9
|
+
return f"Invalid operator in {self.rule_str}, valid operators are: {self.valid_operators}"
|
10
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import ast
|
2
|
+
import logging
|
3
|
+
from _ast import AST
|
4
|
+
|
5
|
+
from prompt_toolkit import PromptSession
|
6
|
+
from prompt_toolkit.completion import WordCompleter
|
7
|
+
from sqlalchemy.orm import DeclarativeBase as SQLTable, Session
|
8
|
+
from typing_extensions import Any, List, Optional, Tuple, Dict, Union, Type
|
9
|
+
|
10
|
+
from .datastructures import Case, PromptFor, CallableExpression, create_row, parse_string_to_expression
|
11
|
+
|
12
|
+
|
13
|
+
def prompt_user_for_expression(case: Union[Case, SQLTable], prompt_for: PromptFor, target_name: str,
|
14
|
+
output_type: Type, session: Optional[Session] = None) -> Tuple[str, CallableExpression]:
|
15
|
+
"""
|
16
|
+
Prompt the user for an executable python expression.
|
17
|
+
|
18
|
+
:param case: The case to classify.
|
19
|
+
:param prompt_for: The type of information ask user about.
|
20
|
+
:param target_name: The name of the target attribute to compare the case with.
|
21
|
+
:param output_type: The type of the output of the given statement from the user.
|
22
|
+
:param session: The sqlalchemy orm session.
|
23
|
+
:return: A callable expression that takes a case and executes user expression on it.
|
24
|
+
"""
|
25
|
+
while True:
|
26
|
+
user_input, expression_tree = prompt_user_about_case(case, prompt_for, target_name)
|
27
|
+
callable_expression = CallableExpression(user_input, output_type, expression_tree=expression_tree, session=session)
|
28
|
+
try:
|
29
|
+
callable_expression(case)
|
30
|
+
break
|
31
|
+
except Exception as e:
|
32
|
+
logging.error(e)
|
33
|
+
print(e)
|
34
|
+
return user_input, callable_expression
|
35
|
+
|
36
|
+
|
37
|
+
def prompt_user_about_case(case: Union[Case, SQLTable], prompt_for: PromptFor, target_name: str) \
|
38
|
+
-> Tuple[str, AST]:
|
39
|
+
"""
|
40
|
+
Prompt the user for input.
|
41
|
+
|
42
|
+
:param case: The case to prompt the user on.
|
43
|
+
:param prompt_for: The type of information the user should provide for the given case.
|
44
|
+
:param target_name: The name of the target property of the case that is queried.
|
45
|
+
:return: The user input, and the executable expression that was parsed from the user input.
|
46
|
+
"""
|
47
|
+
prompt_str = f"Give {prompt_for} for {case.__class__.__name__}.{target_name}"
|
48
|
+
session = get_prompt_session_for_obj(case)
|
49
|
+
user_input, expression_tree = prompt_user_input_and_parse_to_expression(prompt_str, session)
|
50
|
+
return user_input, expression_tree
|
51
|
+
|
52
|
+
|
53
|
+
def get_completions(obj: Any) -> List[str]:
|
54
|
+
"""
|
55
|
+
Get all completions for the object. This is used in the python prompt shell to provide completions for the user.
|
56
|
+
|
57
|
+
:param obj: The object to get completions for.
|
58
|
+
:return: A list of completions.
|
59
|
+
"""
|
60
|
+
# Define completer with all object attributes and comparison operators
|
61
|
+
completions = ['==', '!=', '>', '<', '>=', '<=', 'in', 'not', 'and', 'or', 'is']
|
62
|
+
completions += ["isinstance(", "issubclass(", "type(", "len(", "hasattr(", "getattr(", "setattr(", "delattr("]
|
63
|
+
completions += list(create_row(obj).keys())
|
64
|
+
return completions
|
65
|
+
|
66
|
+
|
67
|
+
def prompt_user_input_and_parse_to_expression(prompt: Optional[str] = None, session: Optional[PromptSession] = None,
|
68
|
+
user_input: Optional[str] = None) -> Tuple[str, ast.AST]:
|
69
|
+
"""
|
70
|
+
Prompt the user for input.
|
71
|
+
|
72
|
+
:param prompt: The prompt to display to the user.
|
73
|
+
:param session: The prompt session to use.
|
74
|
+
:param user_input: The user input to use. If given, the user input will be used instead of prompting the user.
|
75
|
+
:return: The user input and the AST tree.
|
76
|
+
"""
|
77
|
+
while True:
|
78
|
+
if not user_input:
|
79
|
+
user_input = session.prompt(f"\n{prompt} >>> ")
|
80
|
+
if user_input.lower() in ['exit', 'quit', '']:
|
81
|
+
break
|
82
|
+
try:
|
83
|
+
return user_input, parse_string_to_expression(user_input)
|
84
|
+
except Exception as e:
|
85
|
+
msg = f"Error parsing expression: {e}"
|
86
|
+
logging.error(msg)
|
87
|
+
print(msg)
|
88
|
+
user_input = None
|
89
|
+
|
90
|
+
|
91
|
+
def get_prompt_session_for_obj(obj: Any) -> PromptSession:
|
92
|
+
"""
|
93
|
+
Get a prompt session for an object.
|
94
|
+
|
95
|
+
:param obj: The object to get the prompt session for.
|
96
|
+
:return: The prompt session.
|
97
|
+
"""
|
98
|
+
completions = get_completions(obj)
|
99
|
+
completer = WordCompleter(completions)
|
100
|
+
session = PromptSession(completer=completer)
|
101
|
+
return session
|