ripple-down-rules 0.2.4__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.
@@ -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
@@ -2,14 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import builtins
5
+ import copyreg
5
6
  import importlib
6
7
  import json
7
8
  import logging
8
9
  import os
9
10
  import re
11
+ import threading
10
12
  import uuid
11
13
  from collections import UserDict
12
- from copy import deepcopy
14
+ from copy import deepcopy, copy
13
15
  from dataclasses import is_dataclass, fields
14
16
  from enum import Enum
15
17
  from types import NoneType
@@ -26,14 +28,28 @@ from tabulate import tabulate
26
28
  from typing_extensions import Callable, Set, Any, Type, Dict, TYPE_CHECKING, get_type_hints, \
27
29
  get_origin, get_args, Tuple, Optional, List, Union, Self
28
30
 
31
+
29
32
  if TYPE_CHECKING:
30
33
  from .datastructures.case import Case
31
34
  from .datastructures.dataclasses import CaseQuery
32
- from .rules import Rule
33
35
 
34
36
  import ast
35
37
 
36
- matplotlib.use("Qt5Agg") # or "Qt5Agg", depending on availability
38
+
39
+ def str_to_snake_case(snake_str: str) -> str:
40
+ """
41
+ Convert a string to snake case.
42
+
43
+ :param snake_str: The string to convert.
44
+ :return: The converted string.
45
+ """
46
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', snake_str)
47
+ s1 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
48
+ # remove redundant underscores
49
+ s1 = re.sub(r'_{2,}', '_', s1)
50
+ # remove leading and trailing underscores
51
+ s1 = re.sub(r'^_|_$', '', s1)
52
+ return s1
37
53
 
38
54
 
39
55
  def are_results_subclass_of_types(result_types: List[Any], types_: List[Type]) -> bool:
@@ -613,13 +629,76 @@ def get_func_rdr_model_path(func: Callable, model_dir: str) -> str:
613
629
  :param model_dir: The directory to save the model to.
614
630
  :return: The path to the model file.
615
631
  """
632
+ return os.path.join(model_dir, f"{get_func_rdr_model_name(func)}.json")
633
+
634
+
635
+ def get_func_rdr_model_name(func: Callable, include_file_name: bool = False) -> str:
636
+ """
637
+ :param func: The function to get the model name for.
638
+ :return: The name of the model.
639
+ """
616
640
  func_name = get_method_name(func)
617
641
  func_class_name = get_method_class_name_if_exists(func)
618
- func_file_name = get_method_file_name(func)
619
- model_name = func_file_name
620
- model_name += f"_{func_class_name}" if func_class_name else ""
621
- model_name += f"_{func_name}"
622
- return os.path.join(model_dir, f"{model_name}.json")
642
+ if include_file_name:
643
+ func_file_name = get_method_file_name(func).split(os.sep)[-1].split('.')[0]
644
+ model_name = func_file_name + '_'
645
+ else:
646
+ model_name = ''
647
+ model_name += f"{func_class_name}_" if func_class_name else ""
648
+ model_name += f"{func_name}"
649
+ return model_name
650
+
651
+
652
+ def extract_bracket_arguments(val: str) -> List[str]:
653
+ """
654
+ Extract arguments inside brackets into a list.
655
+
656
+ :param val: The string containing brackets.
657
+ :return: List of arguments inside brackets.
658
+ """
659
+ if '[' not in val:
660
+ return [val]
661
+ args_start = val.find('[')
662
+ args_end = val.rfind(']')
663
+ if args_end == -1:
664
+ return [val]
665
+ base_type = val[:args_start]
666
+ args = val[args_start + 1:args_end].split(',')
667
+ args = [arg.strip() for arg in args]
668
+ return [base_type] + args
669
+
670
+
671
+ def typing_hint_to_str(type_hint: Any) -> Tuple[str, List[str]]:
672
+ """
673
+ Convert a typing hint to a string.
674
+
675
+ :param type_hint: The typing hint to convert.
676
+ :return: The string representation of the typing hint.
677
+ """
678
+ val = (str(type_hint).strip("<>")
679
+ .replace("class ", "")
680
+ # .replace("typing.", "")
681
+ .replace("'", ""))
682
+ all_args = []
683
+ if '[' in val:
684
+ args = extract_bracket_arguments(val)
685
+ args_with_brackets = [arg for arg in args if '[' in arg]
686
+ all_args.extend([arg for arg in args if '[' not in arg])
687
+ while args_with_brackets:
688
+ for arg in args:
689
+ if '[' in arg:
690
+ sub_args = extract_bracket_arguments(arg)
691
+ args_with_brackets.remove(arg)
692
+ all_args.extend([sarg for sarg in sub_args if '[' not in sarg])
693
+ args_with_brackets.extend([sarg for sarg in sub_args if '[' in sarg])
694
+ elif arg not in all_args:
695
+ all_args.append(arg)
696
+ args = args_with_brackets
697
+ for arg in all_args:
698
+ val = val.replace(arg, arg.split('.')[-1])
699
+ else:
700
+ val = val.split('.')[-1]
701
+ return val, all_args
623
702
 
624
703
 
625
704
  def get_method_args_as_dict(method: Callable, *args, **kwargs) -> Dict[str, Any]:
@@ -632,6 +711,8 @@ def get_method_args_as_dict(method: Callable, *args, **kwargs) -> Dict[str, Any]
632
711
  :return: A dictionary of the arguments.
633
712
  """
634
713
  func_arg_names = method.__code__.co_varnames
714
+ func_arg_names = list(map(lambda arg_name: f"{arg_name}_" if arg_name in ["self", "cls"] else arg_name,
715
+ func_arg_names))
635
716
  func_arg_values = args + tuple(kwargs.values())
636
717
  return dict(zip(func_arg_names, func_arg_values))
637
718
 
@@ -653,8 +734,27 @@ def get_method_class_name_if_exists(method: Callable) -> Optional[str]:
653
734
  :param method: The method to get the class name of.
654
735
  :return: The class name of the method.
655
736
  """
656
- if hasattr(method, "__self__") and hasattr(method.__self__, "__class__"):
657
- return method.__self__.__class__.__name__
737
+ if hasattr(method, "__self__"):
738
+ if hasattr(method.__self__, "__class__"):
739
+ return method.__self__.__class__.__name__
740
+ return method.__qualname__.split('.')[0] if hasattr(method, "__qualname__") else None
741
+
742
+
743
+ def get_method_class_if_exists(method: Callable, *args) -> Optional[Type]:
744
+ """
745
+ Get the class of a method if it has one.
746
+
747
+ :param method: The method to get the class of.
748
+ :return: The class of the method, if it exists otherwise None.
749
+ """
750
+ if hasattr(method, "__self__"):
751
+ if hasattr(method.__self__, "__class__"):
752
+ return method.__self__.__class__
753
+ elif method.__code__.co_varnames:
754
+ if method.__code__.co_varnames[0] == 'self':
755
+ return args[0].__class__
756
+ elif method.__code__.co_varnames[0] == 'cls':
757
+ return args[0]
658
758
  return None
659
759
 
660
760
 
@@ -693,7 +793,7 @@ def is_iterable(obj: Any) -> bool:
693
793
 
694
794
  :param obj: The object to check.
695
795
  """
696
- return hasattr(obj, "__iter__") and not isinstance(obj, (str, type))
796
+ return hasattr(obj, "__iter__") and not isinstance(obj, (str, type, bytes, bytearray))
697
797
 
698
798
 
699
799
  def get_type_from_string(type_path: str):
@@ -839,7 +939,25 @@ class SubclassJSONSerializer:
839
939
  load = from_json_file
840
940
 
841
941
 
842
- def copy_case(case: Union[Case, SQLTable]) -> Union[Case, SQLTable]:
942
+ def _pickle_thread(thread_obj) -> Any:
943
+ """Return a plain object with user-defined attributes but no thread behavior."""
944
+
945
+ class DummyThread:
946
+ pass
947
+
948
+ dummy = DummyThread()
949
+ # Copy only non-thread-related attributes
950
+ for attr, value in thread_obj.__dict__.items():
951
+ print(attr)
952
+ if not attr.startswith("_"): # Skip internal Thread attributes
953
+ setattr(dummy, attr, value)
954
+ return dummy
955
+
956
+
957
+ copyreg.pickle(threading.Thread, _pickle_thread)
958
+
959
+
960
+ def copy_case(case: Union[Case, SQLTable]) -> Union[Case, SQLTable, Any]:
843
961
  """
844
962
  Copy a case.
845
963
 
@@ -849,7 +967,18 @@ def copy_case(case: Union[Case, SQLTable]) -> Union[Case, SQLTable]:
849
967
  if isinstance(case, SQLTable):
850
968
  return copy_orm_instance_with_relationships(case)
851
969
  else:
852
- return deepcopy(case)
970
+ # copy the case recursively for 1 level
971
+ # try:
972
+ # case_copy = deepcopy(case)
973
+ # except Exception as e:
974
+ case_copy = copy(case)
975
+ for attr in dir(case):
976
+ if attr.startswith("_") or callable(getattr(case, attr)):
977
+ continue
978
+ attr_value = getattr(case, attr)
979
+ if is_iterable(attr_value):
980
+ setattr(case_copy, attr, copy(attr_value))
981
+ return case_copy
853
982
 
854
983
 
855
984
  def copy_orm_instance(instance: SQLTable) -> SQLTable:
@@ -908,7 +1037,7 @@ def get_value_type_from_type_hint(attr_name: str, obj: Any) -> Type:
908
1037
  return attr_value_type
909
1038
 
910
1039
 
911
- def get_hint_for_attribute(attr_name: str, obj: Any) -> Tuple[Optional[Any], Optional[Any], Tuple[Any]]:
1040
+ def get_hint_for_attribute(attr_name: str, obj: Any) -> Tuple[Optional[Type], Optional[Type], Tuple[Type]]:
912
1041
  """
913
1042
  Get the type hint for an attribute of an object.
914
1043
 
@@ -928,12 +1057,23 @@ def get_hint_for_attribute(attr_name: str, obj: Any) -> Tuple[Optional[Any], Opt
928
1057
  hint = get_type_hints(obj.__class__)[attr_name]
929
1058
  except KeyError:
930
1059
  hint = type(class_attr)
931
- origin = get_origin(hint)
932
- args = get_args(hint)
1060
+ origin, args = get_origin_and_args_from_type_hint(hint)
1061
+ return origin, origin, args
1062
+
1063
+
1064
+ def get_origin_and_args_from_type_hint(type_hint: Type) -> Tuple[Optional[Type], Tuple[Type]]:
1065
+ """
1066
+ Get the origin and arguments from a type hint.W
1067
+
1068
+ :param type_hint: The type hint to get the origin and arguments from.
1069
+ :return: The origin and arguments of the type hint.
1070
+ """
1071
+ origin = get_origin(type_hint)
1072
+ args = get_args(type_hint)
933
1073
  if origin is Mapped:
934
- return args[0], get_origin(args[0]), get_args(args[0])
1074
+ return get_origin(args[0]), get_args(args[0])
935
1075
  else:
936
- return hint, origin, args
1076
+ return origin, args
937
1077
 
938
1078
 
939
1079
  def table_rows_as_str(row_dict: Dict[str, Any], columns_per_row: int = 9):
@@ -951,6 +1091,7 @@ def table_rows_as_str(row_dict: Dict[str, Any], columns_per_row: int = 9):
951
1091
  all_table_rows = []
952
1092
  for row_keys, row_values in zip(keys, values):
953
1093
  row_values = [str(v) if v is not None else "" for v in row_values]
1094
+ row_values = [v.lower() if v in ["True", "False"] else v for v in row_values]
954
1095
  table = tabulate([row_values], headers=row_keys, tablefmt='plain', maxcolwidths=[20] * len(row_keys))
955
1096
  all_table_rows.append(table)
956
1097
  return "\n".join(all_table_rows)
@@ -1100,7 +1241,7 @@ def get_all_subclasses(cls: Type) -> Dict[str, Type]:
1100
1241
  return all_subclasses
1101
1242
 
1102
1243
 
1103
- def make_set(value: Any) -> Set[Any]:
1244
+ def make_set(value: Any) -> Set:
1104
1245
  """
1105
1246
  Make a set from a value.
1106
1247
 
@@ -1205,6 +1346,9 @@ def draw_tree(root: Node, fig: plt.Figure):
1205
1346
  """
1206
1347
  Draw the tree using matplotlib and networkx.
1207
1348
  """
1349
+ if matplotlib.get_backend().lower() not in ['qt5agg', 'qt4agg', 'qt6agg']:
1350
+ matplotlib.use("Qt6Agg") # or "Qt6Agg", depending on availability
1351
+
1208
1352
  if root is None:
1209
1353
  return
1210
1354
  fig.clf()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.2.4
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5