ripple-down-rules 0.3.0__py3-none-any.whl → 0.4.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/datasets.py +5 -0
- ripple_down_rules/datastructures/enums.py +14 -0
- ripple_down_rules/experts.py +14 -7
- ripple_down_rules/rdr.py +6 -2
- ripple_down_rules/user_interface/__init__.py +0 -0
- ripple_down_rules/user_interface/gui.py +630 -0
- ripple_down_rules/user_interface/ipython_custom_shell.py +146 -0
- ripple_down_rules/user_interface/object_diagram.py +109 -0
- ripple_down_rules/user_interface/prompt.py +159 -0
- ripple_down_rules/user_interface/template_file_creator.py +293 -0
- ripple_down_rules/utils.py +2 -2
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/METADATA +8 -1
- ripple_down_rules-0.4.0.dist-info/RECORD +25 -0
- ripple_down_rules/prompt.py +0 -510
- ripple_down_rules-0.3.0.dist-info/RECORD +0 -20
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.3.0.dist-info → ripple_down_rules-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,293 @@
|
|
1
|
+
import ast
|
2
|
+
import os
|
3
|
+
import shutil
|
4
|
+
import socket
|
5
|
+
import subprocess
|
6
|
+
import tempfile
|
7
|
+
from functools import cached_property
|
8
|
+
from textwrap import indent, dedent
|
9
|
+
|
10
|
+
from colorama import Fore, Style
|
11
|
+
from ipykernel.inprocess.ipkernel import InProcessInteractiveShell
|
12
|
+
from typing_extensions import Optional, Type, List, Callable
|
13
|
+
|
14
|
+
from ..datastructures.case import Case
|
15
|
+
from ..datastructures.dataclasses import CaseQuery
|
16
|
+
from ..datastructures.enums import Editor, PromptFor
|
17
|
+
from ..utils import str_to_snake_case, get_imports_from_scope, make_list, typing_hint_to_str, \
|
18
|
+
get_imports_from_types, extract_function_source
|
19
|
+
|
20
|
+
|
21
|
+
def detect_available_editor() -> Optional[Editor]:
|
22
|
+
"""
|
23
|
+
Detect the available editor on the system.
|
24
|
+
|
25
|
+
:return: The first found editor that is available on the system.
|
26
|
+
"""
|
27
|
+
editor_env = os.environ.get("RDR_EDITOR")
|
28
|
+
if editor_env:
|
29
|
+
return Editor.from_str(editor_env)
|
30
|
+
for editor in [Editor.Pycharm, Editor.Code, Editor.CodeServer]:
|
31
|
+
if shutil.which(editor.value):
|
32
|
+
return editor
|
33
|
+
return None
|
34
|
+
|
35
|
+
|
36
|
+
def is_port_in_use(port: int = 8080) -> bool:
|
37
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
38
|
+
return s.connect_ex(("localhost", port)) == 0
|
39
|
+
|
40
|
+
|
41
|
+
def start_code_server(workspace):
|
42
|
+
"""
|
43
|
+
Start the code-server in the given workspace.
|
44
|
+
"""
|
45
|
+
filename = os.path.join(os.path.dirname(__file__), "start-code-server.sh")
|
46
|
+
os.system(f"chmod +x {filename}")
|
47
|
+
print(f"Starting code-server at {filename}")
|
48
|
+
return subprocess.Popen(["/bin/bash", filename, workspace], stdout=subprocess.PIPE,
|
49
|
+
stderr=subprocess.PIPE, text=True)
|
50
|
+
|
51
|
+
|
52
|
+
class TemplateFileCreator:
|
53
|
+
"""
|
54
|
+
A class to create a rule template file for a given case and prompt for the user to edit it.
|
55
|
+
"""
|
56
|
+
temp_file_path: Optional[str] = None
|
57
|
+
"""
|
58
|
+
The path to the temporary file that is created for the user to edit.
|
59
|
+
"""
|
60
|
+
port: int = int(os.environ.get("RDR_EDITOR_PORT", 8080))
|
61
|
+
"""
|
62
|
+
The port to use for the code-server.
|
63
|
+
"""
|
64
|
+
process: Optional[subprocess.Popen] = None
|
65
|
+
"""
|
66
|
+
The process of the code-server.
|
67
|
+
"""
|
68
|
+
all_code_lines: Optional[List[str]] = None
|
69
|
+
"""
|
70
|
+
The list of all code lines in the function in the temporary file.
|
71
|
+
"""
|
72
|
+
|
73
|
+
def __init__(self, shell: InProcessInteractiveShell, case_query: CaseQuery, prompt_for: PromptFor,
|
74
|
+
code_to_modify: Optional[str] = None, print_func: Optional[Callable[[str], None]] = None):
|
75
|
+
self.print_func = print_func if print_func else print
|
76
|
+
self.shell = shell
|
77
|
+
self.code_to_modify = code_to_modify
|
78
|
+
self.prompt_for = prompt_for
|
79
|
+
self.case_query = case_query
|
80
|
+
self.output_type = self.get_output_type()
|
81
|
+
self.user_edit_line = 0
|
82
|
+
self.func_name: str = self.get_func_name()
|
83
|
+
self.func_doc: str = self.get_func_doc()
|
84
|
+
self.function_signature: str = self.get_function_signature()
|
85
|
+
self.editor: Optional[Editor] = detect_available_editor()
|
86
|
+
self.workspace: str = os.environ.get("RDR_EDITOR_WORKSPACE", os.path.dirname(self.case_query.scope['__file__']))
|
87
|
+
self.temp_file_path: str = os.path.join(self.workspace, "edit_code_here.py")
|
88
|
+
|
89
|
+
def get_output_type(self) -> List[Type]:
|
90
|
+
"""
|
91
|
+
:return: The output type of the function as a list of types.
|
92
|
+
"""
|
93
|
+
if self.prompt_for == PromptFor.Conditions:
|
94
|
+
output_type = bool
|
95
|
+
else:
|
96
|
+
output_type = self.case_query.attribute_type
|
97
|
+
return make_list(output_type) if output_type is not None else None
|
98
|
+
|
99
|
+
def edit(self):
|
100
|
+
if self.editor is None:
|
101
|
+
self.print_func(
|
102
|
+
f"{Fore.RED}ERROR:: No editor found. Please install PyCharm, VSCode or code-server.{Style.RESET_ALL}")
|
103
|
+
return
|
104
|
+
|
105
|
+
boilerplate_code = self.build_boilerplate_code()
|
106
|
+
self.write_to_file(boilerplate_code)
|
107
|
+
|
108
|
+
self.open_file_in_editor()
|
109
|
+
|
110
|
+
def open_file_in_editor(self):
|
111
|
+
"""
|
112
|
+
Open the file in the available editor.
|
113
|
+
"""
|
114
|
+
if self.editor == Editor.Pycharm:
|
115
|
+
subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path],
|
116
|
+
stdout=subprocess.DEVNULL,
|
117
|
+
stderr=subprocess.DEVNULL)
|
118
|
+
elif self.editor == Editor.Code:
|
119
|
+
subprocess.Popen(["code", self.temp_file_path])
|
120
|
+
elif self.editor == Editor.CodeServer:
|
121
|
+
try:
|
122
|
+
subprocess.check_output(["pgrep", "-f", "code-server"])
|
123
|
+
# check if same port is in use
|
124
|
+
if is_port_in_use(self.port):
|
125
|
+
self.print_func(f"Code-server is already running on port {self.port}.")
|
126
|
+
else:
|
127
|
+
raise ValueError("Port is not in use.")
|
128
|
+
except (subprocess.CalledProcessError, ValueError) as e:
|
129
|
+
self.process = start_code_server(self.workspace)
|
130
|
+
self.print_func(f"Open code-server in your browser at http://localhost:{self.port}?folder={self.workspace}")
|
131
|
+
self.print_func(f"Edit the file: {Fore.MAGENTA}{self.temp_file_path}")
|
132
|
+
|
133
|
+
def build_boilerplate_code(self):
|
134
|
+
imports = self.get_imports()
|
135
|
+
if self.function_signature is None:
|
136
|
+
self.function_signature = self.get_function_signature()
|
137
|
+
if self.func_doc is None:
|
138
|
+
self.func_doc = self.get_func_doc()
|
139
|
+
if self.code_to_modify is not None:
|
140
|
+
body = indent(dedent(self.code_to_modify), ' ')
|
141
|
+
else:
|
142
|
+
body = " # Write your code here\n pass"
|
143
|
+
boilerplate = f"""{imports}\n\n{self.function_signature}\n \"\"\"{self.func_doc}\"\"\"\n{body}"""
|
144
|
+
self.user_edit_line = imports.count('\n') + 6
|
145
|
+
return boilerplate
|
146
|
+
|
147
|
+
def get_function_signature(self) -> str:
|
148
|
+
if self.func_name is None:
|
149
|
+
self.func_name = self.get_func_name()
|
150
|
+
output_type_hint = self.get_output_type_hint()
|
151
|
+
func_args = self.get_func_args()
|
152
|
+
return f"def {self.func_name}({func_args}){output_type_hint}:"
|
153
|
+
|
154
|
+
def get_output_type_hint(self) -> str:
|
155
|
+
"""
|
156
|
+
:return: A string containing the output type hint for the function.
|
157
|
+
"""
|
158
|
+
output_type_hint = ""
|
159
|
+
if self.prompt_for == PromptFor.Conditions:
|
160
|
+
output_type_hint = " -> bool"
|
161
|
+
elif self.prompt_for == PromptFor.Conclusion:
|
162
|
+
output_type_hint = f" -> {self.case_query.attribute_type_hint}"
|
163
|
+
return output_type_hint
|
164
|
+
|
165
|
+
def get_func_args(self) -> str:
|
166
|
+
"""
|
167
|
+
:return: A string containing the function arguments.
|
168
|
+
"""
|
169
|
+
if self.case_query.is_function:
|
170
|
+
func_args = {}
|
171
|
+
for k, v in self.case_query.case.items():
|
172
|
+
if (self.case_query.function_args_type_hints is not None
|
173
|
+
and k in self.case_query.function_args_type_hints):
|
174
|
+
func_args[k] = typing_hint_to_str(self.case_query.function_args_type_hints[k])[0]
|
175
|
+
else:
|
176
|
+
func_args[k] = type(v).__name__ if not isinstance(v, type) else f"Type[{v.__name__}]"
|
177
|
+
func_args = ', '.join([f"{k}: {v}" if str(v) not in ["NoneType", "None"] else str(k)
|
178
|
+
for k, v in func_args.items()])
|
179
|
+
else:
|
180
|
+
func_args = f"case: {self.case_type.__name__}"
|
181
|
+
return func_args
|
182
|
+
|
183
|
+
def write_to_file(self, code: str):
|
184
|
+
if self.temp_file_path is None:
|
185
|
+
tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".py",
|
186
|
+
dir=self.workspace)
|
187
|
+
tmp.write(code)
|
188
|
+
tmp.flush()
|
189
|
+
self.temp_file_path = tmp.name
|
190
|
+
tmp.close()
|
191
|
+
else:
|
192
|
+
with open(self.temp_file_path, 'w+') as f:
|
193
|
+
f.write(code)
|
194
|
+
|
195
|
+
def get_imports(self):
|
196
|
+
"""
|
197
|
+
:return: A string containing the imports for the function.
|
198
|
+
"""
|
199
|
+
case_type_imports = []
|
200
|
+
if self.case_query.is_function:
|
201
|
+
for k, v in self.case_query.case.items():
|
202
|
+
if (self.case_query.function_args_type_hints is not None
|
203
|
+
and k in self.case_query.function_args_type_hints):
|
204
|
+
hint_list = typing_hint_to_str(self.case_query.function_args_type_hints[k])[1]
|
205
|
+
for hint in hint_list:
|
206
|
+
hint_split = hint.split('.')
|
207
|
+
if len(hint_split) > 1:
|
208
|
+
case_type_imports.append(f"from {'.'.join(hint_split[:-1])} import {hint_split[-1]}")
|
209
|
+
else:
|
210
|
+
if isinstance(v, type):
|
211
|
+
case_type_imports.append(f"from {v.__module__} import {v.__name__}")
|
212
|
+
elif hasattr(v, "__module__") and not v.__module__.startswith("__"):
|
213
|
+
case_type_imports.append(f"\nfrom {type(v).__module__} import {type(v).__name__}")
|
214
|
+
else:
|
215
|
+
case_type_imports.append(f"from {self.case_type.__module__} import {self.case_type.__name__}")
|
216
|
+
if self.output_type is None:
|
217
|
+
output_type_imports = [f"from typing_extensions import Any"]
|
218
|
+
else:
|
219
|
+
output_type_imports = get_imports_from_types(self.output_type)
|
220
|
+
if len(self.output_type) > 1:
|
221
|
+
output_type_imports.append("from typing_extensions import Union")
|
222
|
+
if list in self.output_type:
|
223
|
+
output_type_imports.append("from typing_extensions import List")
|
224
|
+
imports = get_imports_from_scope(self.case_query.scope)
|
225
|
+
imports = [i for i in imports if ("get_ipython" not in i)]
|
226
|
+
imports.extend(case_type_imports)
|
227
|
+
imports.extend([oti for oti in output_type_imports if oti not in imports])
|
228
|
+
imports = set(imports)
|
229
|
+
return '\n'.join(imports)
|
230
|
+
|
231
|
+
def get_func_doc(self) -> Optional[str]:
|
232
|
+
"""
|
233
|
+
:return: A string containing the function docstring.
|
234
|
+
"""
|
235
|
+
if self.prompt_for == PromptFor.Conditions:
|
236
|
+
return (f"Get conditions on whether it's possible to conclude a value"
|
237
|
+
f" for {self.case_query.name}")
|
238
|
+
else:
|
239
|
+
return f"Get possible value(s) for {self.case_query.name}"
|
240
|
+
|
241
|
+
def get_func_name(self) -> Optional[str]:
|
242
|
+
func_name = ""
|
243
|
+
if self.prompt_for == PromptFor.Conditions:
|
244
|
+
func_name = f"{self.prompt_for.value.lower()}_for_"
|
245
|
+
case_name = self.case_query.name.replace(".", "_")
|
246
|
+
if self.case_query.is_function:
|
247
|
+
# convert any CamelCase word into snake_case by adding _ before each capital letter
|
248
|
+
case_name = case_name.replace(f"_{self.case_query.attribute_name}", "")
|
249
|
+
func_name += case_name
|
250
|
+
return str_to_snake_case(func_name)
|
251
|
+
|
252
|
+
@cached_property
|
253
|
+
def case_type(self) -> Type:
|
254
|
+
"""
|
255
|
+
Get the type of the case object in the current scope.
|
256
|
+
|
257
|
+
:return: The type of the case object.
|
258
|
+
"""
|
259
|
+
case = self.case_query.scope['case']
|
260
|
+
return case._obj_type if isinstance(case, Case) else type(case)
|
261
|
+
|
262
|
+
def load(self) -> Optional[List[str]]:
|
263
|
+
if not self.temp_file_path:
|
264
|
+
self.print_func(f"{Fore.RED}ERROR:: No file to load. Run %edit first.{Style.RESET_ALL}")
|
265
|
+
return None
|
266
|
+
|
267
|
+
with open(self.temp_file_path, 'r') as f:
|
268
|
+
source = f.read()
|
269
|
+
|
270
|
+
tree = ast.parse(source)
|
271
|
+
updates = {}
|
272
|
+
for node in tree.body:
|
273
|
+
if isinstance(node, ast.FunctionDef) and node.name == self.func_name:
|
274
|
+
exec_globals = {}
|
275
|
+
exec(source, self.case_query.scope, exec_globals)
|
276
|
+
user_function = exec_globals[self.func_name]
|
277
|
+
updates[self.func_name] = user_function
|
278
|
+
self.print_func(f"{Fore.BLUE}Loaded `{self.func_name}` function into user namespace.{Style.RESET_ALL}")
|
279
|
+
break
|
280
|
+
if updates:
|
281
|
+
self.shell.user_ns.update(updates)
|
282
|
+
self.all_code_lines = extract_function_source(self.temp_file_path,
|
283
|
+
[self.func_name],
|
284
|
+
join_lines=False)[self.func_name]
|
285
|
+
return self.all_code_lines
|
286
|
+
else:
|
287
|
+
self.print_func(f"{Fore.RED}ERROR:: Function `{self.func_name}` not found.{Style.RESET_ALL}")
|
288
|
+
return None
|
289
|
+
|
290
|
+
def __del__(self):
|
291
|
+
if hasattr(self, 'process') and self.process is not None and self.process.poll() is None:
|
292
|
+
self.process.terminate() # Graceful shutdown
|
293
|
+
self.process.wait() # Ensure cleanup
|
ripple_down_rules/utils.py
CHANGED
@@ -793,7 +793,7 @@ def is_iterable(obj: Any) -> bool:
|
|
793
793
|
|
794
794
|
:param obj: The object to check.
|
795
795
|
"""
|
796
|
-
return hasattr(obj, "__iter__") and not isinstance(obj, (str, type))
|
796
|
+
return hasattr(obj, "__iter__") and not isinstance(obj, (str, type, bytes, bytearray))
|
797
797
|
|
798
798
|
|
799
799
|
def get_type_from_string(type_path: str):
|
@@ -1347,7 +1347,7 @@ def draw_tree(root: Node, fig: plt.Figure):
|
|
1347
1347
|
Draw the tree using matplotlib and networkx.
|
1348
1348
|
"""
|
1349
1349
|
if matplotlib.get_backend().lower() not in ['qt5agg', 'qt4agg', 'qt6agg']:
|
1350
|
-
matplotlib.use("
|
1350
|
+
matplotlib.use("Qt6Agg") # or "Qt6Agg", depending on availability
|
1351
1351
|
|
1352
1352
|
if root is None:
|
1353
1353
|
return
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ripple_down_rules
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.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
|
@@ -702,6 +702,13 @@ SCRDR, MCRDR, and GRDR implementation were inspired from the book:
|
|
702
702
|
sudo apt-get install graphviz graphviz-dev
|
703
703
|
pip install ripple_down_rules
|
704
704
|
```
|
705
|
+
For GUI support, also install:
|
706
|
+
|
707
|
+
```bash
|
708
|
+
sudo apt-get install libxcb-cursor-dev
|
709
|
+
```
|
710
|
+
|
711
|
+
```bash
|
705
712
|
|
706
713
|
## Example Usage
|
707
714
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
ripple_down_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
ripple_down_rules/datasets.py,sha256=fJbZ7V-UUYTu5XVVpFinTbuzN3YePCnUB01L3AyZVM8,6837
|
3
|
+
ripple_down_rules/experts.py,sha256=S-d1EZP0SK0Ab_pMzgkOcLvZFLMdSm8_C0u3AgfouUc,6468
|
4
|
+
ripple_down_rules/failures.py,sha256=E6ajDUsw3Blom8eVLbA7d_Qnov2conhtZ0UmpQ9ZtSE,302
|
5
|
+
ripple_down_rules/helpers.py,sha256=TvTJU0BA3dPcAyzvZFvAu7jZqsp8Lu0HAAwvuizlGjg,2018
|
6
|
+
ripple_down_rules/rdr.py,sha256=UhFKsb7bCKLfplZhCSY-FAulvMfsBqVk_-K7o5WMP4o,43709
|
7
|
+
ripple_down_rules/rdr_decorators.py,sha256=VdmE0JrE8j89b6Af1R1tLZiKfy3h1VCvhAUefN_FLLQ,6753
|
8
|
+
ripple_down_rules/rules.py,sha256=QQy7NBG6mKiowxVG_LjQJBxLTDW2hMyx5zAgwUxdCMM,17183
|
9
|
+
ripple_down_rules/utils.py,sha256=tmYlbL1q8Au7iXlWNUW2j80pKqSaz6tvgwzL3fL5Cg8,48935
|
10
|
+
ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
|
11
|
+
ripple_down_rules/datastructures/callable_expression.py,sha256=jA7424_mWPbOoPICW3eLMX0-ypxnsW6gOqxrJ7JpDbE,11610
|
12
|
+
ripple_down_rules/datastructures/case.py,sha256=oC8OSdhXvHE-Zx1IIQlad-fsKzQQqr6MZBW24c-dbeU,15191
|
13
|
+
ripple_down_rules/datastructures/dataclasses.py,sha256=GWnUF4h4zfNHSsyBIz3L9y8sLkrXRv0FK_OxzzLc8L8,8183
|
14
|
+
ripple_down_rules/datastructures/enums.py,sha256=ce7tqS0otfSTNAOwsnXlhsvIn4iW_Y_N3TNebF3YoZs,5700
|
15
|
+
ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
ripple_down_rules/user_interface/gui.py,sha256=xjuJCEhZ4Oy59AZQDjftcgOce76T6PWMhX5eq4sFLWU,25703
|
17
|
+
ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=tc7ms80iPNElm9AYC6i1FlfMKkqHLT8wmOEXg_k9yAU,6275
|
18
|
+
ripple_down_rules/user_interface/object_diagram.py,sha256=TaAsjCbsCBAsO1Ffp6l8otbK-mculUW2jnVrDRFq1hU,4249
|
19
|
+
ripple_down_rules/user_interface/prompt.py,sha256=tQyIrRno1YuS3K0c4p48FVTdDJGG0HCE63WHVKpSJ1I,7976
|
20
|
+
ripple_down_rules/user_interface/template_file_creator.py,sha256=PegwW_g8UScjEF2GGe1eV0VoWUzxPhA27L9ExbL7Y_E,12610
|
21
|
+
ripple_down_rules-0.4.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
22
|
+
ripple_down_rules-0.4.0.dist-info/METADATA,sha256=HAmijc0VYsXxNqGWwnGems2bOKDhJczdDdqbTgdzyNs,42668
|
23
|
+
ripple_down_rules-0.4.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
24
|
+
ripple_down_rules-0.4.0.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
|
25
|
+
ripple_down_rules-0.4.0.dist-info/RECORD,,
|