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.
@@ -5,15 +5,17 @@ of the RDRs.
5
5
  """
6
6
  import os.path
7
7
  from functools import wraps
8
+
9
+ from pyparsing.tools.cvt_pyparsing_pep8_names import camel_to_snake
8
10
  from typing_extensions import Callable, Optional, Type, Tuple, Dict, Any, Self, get_type_hints, List, Union
9
11
 
10
- from ripple_down_rules.datastructures.case import create_case
12
+ from ripple_down_rules.datastructures.case import create_case, Case
11
13
  from ripple_down_rules.datastructures.dataclasses import CaseQuery
12
14
  from ripple_down_rules.datastructures.enums import Category
13
15
  from ripple_down_rules.experts import Expert, Human
14
16
  from ripple_down_rules.rdr import GeneralRDR, RippleDownRules
15
17
  from ripple_down_rules.utils import get_method_args_as_dict, get_func_rdr_model_name, make_set, \
16
- get_method_class_if_exists
18
+ get_method_class_if_exists, get_method_name, str_to_snake_case
17
19
 
18
20
 
19
21
  class RDRDecorator:
@@ -41,99 +43,118 @@ class RDRDecorator:
41
43
  :return: A decorator to use a GeneralRDR as a classifier that monitors and modifies the function's output.
42
44
  """
43
45
  self.rdr_models_dir = models_dir
46
+ self.model_name: Optional[str] = None
44
47
  self.output_type = output_type
45
48
  self.parsed_output_type: List[Type] = []
46
49
  self.mutual_exclusive = mutual_exclusive
47
50
  self.rdr_python_path: Optional[str] = python_dir
48
51
  self.output_name = output_name
49
52
  self.fit: bool = fit
50
- self.expert = expert if expert else Human()
51
- self.rdr_model_path: Optional[str] = None
53
+ self.expert: Optional[Expert] = expert
52
54
  self.load()
53
55
 
54
56
  def decorator(self, func: Callable) -> Callable:
55
57
 
56
58
  @wraps(func)
57
59
  def wrapper(*args, **kwargs) -> Optional[Any]:
60
+
58
61
  if len(self.parsed_output_type) == 0:
59
- self.parse_output_type(func, *args)
60
- if self.rdr_model_path is None:
61
- self.initialize_rdr_model_path_and_load(func)
62
- case_dict = get_method_args_as_dict(func, *args, **kwargs)
63
- func_output = func(*args, **kwargs)
64
- case_dict.update({self.output_name: func_output})
65
- case = create_case(case_dict, obj_name=get_func_rdr_model_name(func), max_recursion_idx=3)
62
+ self.parsed_output_type = self.parse_output_type(func, self.output_type, *args)
63
+ if self.model_name is None:
64
+ self.initialize_rdr_model_name_and_load(func)
65
+
66
66
  if self.fit:
67
- scope = func.__globals__
68
- scope.update(case_dict)
69
- func_args_type_hints = get_type_hints(func)
70
- func_args_type_hints.update({self.output_name: Union[tuple(self.parsed_output_type)]})
71
- case_query = CaseQuery(case, self.output_name, Union[tuple(self.parsed_output_type)],
72
- self.mutual_exclusive,
73
- scope=scope, is_function=True, function_args_type_hints=func_args_type_hints)
67
+ expert_answers_path = os.path.join(self.rdr_models_dir, self.model_name, "expert_answers")
68
+ self.expert = self.expert or Human(answers_save_path=expert_answers_path)
69
+ case_query = self.create_case_query_from_method(func, self.parsed_output_type,
70
+ self.mutual_exclusive, self.output_name,
71
+ *args, **kwargs)
74
72
  output = self.rdr.fit_case(case_query, expert=self.expert)
75
73
  return output[self.output_name]
76
74
  else:
75
+ case, case_dict = self.create_case_from_method(func, self.output_name, *args, **kwargs)
77
76
  return self.rdr.classify(case)[self.output_name]
78
77
 
79
78
  return wrapper
80
79
 
81
- def initialize_rdr_model_path_and_load(self, func: Callable) -> None:
80
+ @staticmethod
81
+ def create_case_query_from_method(func: Callable, output_type, mutual_exclusive: bool,
82
+ output_name: str = 'output_', *args, **kwargs) -> CaseQuery:
83
+ """
84
+ Create a CaseQuery from the function and its arguments.
85
+
86
+ :param func: The function to create a case from.
87
+ :param output_type: The type of the output.
88
+ :param mutual_exclusive: If True, the output types are mutually exclusive.
89
+ :param output_name: The name of the output in the case. Defaults to 'output_'.
90
+ :param args: The positional arguments of the function.
91
+ :param kwargs: The keyword arguments of the function.
92
+ :return: A CaseQuery object representing the case.
93
+ """
94
+ output_type = make_set(output_type)
95
+ case, case_dict = RDRDecorator.create_case_from_method(func, output_name, *args, **kwargs)
96
+ scope = func.__globals__
97
+ scope.update(case_dict)
98
+ func_args_type_hints = get_type_hints(func)
99
+ func_args_type_hints.update({output_name: Union[tuple(output_type)]})
100
+ return CaseQuery(case, output_name, Union[tuple(output_type)],
101
+ mutual_exclusive, scope=scope,
102
+ is_function=True, function_args_type_hints=func_args_type_hints)
103
+
104
+ @staticmethod
105
+ def create_case_from_method(func: Callable, output_name: str = "output_", *args, **kwargs) -> Tuple[Case, Dict[str, Any]]:
106
+ """
107
+ Create a Case from the function and its arguments.
108
+
109
+ :param func: The function to create a case from.
110
+ :param output_name: The name of the output in the case. Defaults to 'output_'.
111
+ :param args: The positional arguments of the function.
112
+ :param kwargs: The keyword arguments of the function.
113
+ :return: A Case object representing the case.
114
+ """
115
+ case_dict = get_method_args_as_dict(func, *args, **kwargs)
116
+ func_output = func(*args, **kwargs)
117
+ case_dict.update({output_name: func_output})
118
+ case_name = get_func_rdr_model_name(func)
119
+ return create_case(case_dict, obj_name=case_name, max_recursion_idx=3), case_dict
120
+
121
+ def initialize_rdr_model_name_and_load(self, func: Callable) -> None:
82
122
  model_file_name = get_func_rdr_model_name(func, include_file_name=True)
83
- model_file_name = (''.join(['_' + c.lower() if c.isupper() else c for c in model_file_name]).lstrip('_')
84
- .replace('__', '_') + ".json")
85
- self.rdr_model_path = os.path.join(self.rdr_models_dir, model_file_name)
123
+ self.model_name = str_to_snake_case(model_file_name)
86
124
  self.load()
87
125
 
88
- def parse_output_type(self, func: Callable, *args) -> None:
89
- for ot in make_set(self.output_type):
126
+ @staticmethod
127
+ def parse_output_type(func: Callable, output_type: Any, *args) -> List[Type]:
128
+ parsed_output_type = []
129
+ for ot in make_set(output_type):
90
130
  if ot is Self:
91
131
  func_class = get_method_class_if_exists(func, *args)
92
132
  if func_class is not None:
93
- self.parsed_output_type.append(func_class)
133
+ parsed_output_type.append(func_class)
94
134
  else:
95
135
  raise ValueError(f"The function {func} is not a method of a class,"
96
136
  f" and the output type is {Self}.")
97
137
  else:
98
- self.parsed_output_type.append(ot)
138
+ parsed_output_type.append(ot)
139
+ return parsed_output_type
99
140
 
100
141
  def save(self):
101
142
  """
102
143
  Save the RDR model to the specified directory.
103
144
  """
104
- self.rdr.save(self.rdr_model_path)
105
-
106
- if self.rdr_python_path is not None:
107
- if not os.path.exists(self.rdr_python_path):
108
- os.makedirs(self.rdr_python_path)
109
- if not os.path.exists(os.path.join(self.rdr_python_path, "__init__.py")):
110
- # add __init__.py file to the directory
111
- with open(os.path.join(self.rdr_python_path, "__init__.py"), "w") as f:
112
- f.write("# This is an empty __init__.py file to make the directory a package.")
113
- # write the RDR model to a python file
114
- self.rdr.write_to_python_file(self.rdr_python_path)
145
+ self.rdr.save(self.rdr_models_dir)
115
146
 
116
147
  def load(self):
117
148
  """
118
149
  Load the RDR model from the specified directory.
119
150
  """
120
- if self.rdr_model_path is not None and os.path.exists(self.rdr_model_path):
121
- self.rdr = GeneralRDR.load(self.rdr_model_path)
151
+ if self.model_name is not None and os.path.exists(os.path.join(self.rdr_models_dir, self.model_name)):
152
+ self.rdr = GeneralRDR.load(self.rdr_models_dir, self.model_name)
122
153
  else:
123
- self.rdr = GeneralRDR()
124
-
125
- def write_to_python_file(self, package_dir: str, file_name_postfix: str = ""):
126
- """
127
- Write the RDR model to a python file.
128
-
129
- :param package_dir: The path to the directory to write the python file.
130
- """
131
- self.rdr.write_to_python_file(package_dir, postfix=file_name_postfix)
154
+ self.rdr = GeneralRDR(save_dir=self.rdr_models_dir, model_name=self.model_name)
132
155
 
133
- def update_from_python_file(self, package_dir: str):
156
+ def update_from_python(self):
134
157
  """
135
158
  Update the RDR model from a python file.
136
-
137
- :param package_dir: The directory of the package that contains the generated python file.
138
159
  """
139
- self.rdr.update_from_python_file(package_dir)
160
+ self.rdr.update_from_python(self.rdr_models_dir, self.model_name)
@@ -118,7 +118,7 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
118
118
  func_call = f"{parent_indent} return {new_function_name.replace('def ', '')}(case)\n"
119
119
  return "\n".join(conclusion_lines).strip(' '), func_call
120
120
  else:
121
- raise ValueError(f"Conclusion is format is not valid, it should be contain a function definition."
121
+ raise ValueError(f"Conclusion format is not valid, it should contain a function definition."
122
122
  f" Instead got:\n{conclusion}\n")
123
123
 
124
124
  def write_condition_as_source_code(self, parent_indent: str = "", defs_file: Optional[str] = None) -> str:
@@ -129,9 +129,7 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
129
129
  :param defs_file: The file to write the conditions to if they are a definition.
130
130
  """
131
131
  if_clause = self._if_statement_source_code_clause()
132
- if '\n' not in self.conditions.user_input:
133
- return f"{parent_indent}{if_clause} {self.conditions.user_input}:\n"
134
- elif "def " in self.conditions.user_input:
132
+ if "def " in self.conditions.user_input:
135
133
  if defs_file is None:
136
134
  raise ValueError("Cannot write conditions to source code as definitions python file was not given.")
137
135
  # This means the conditions are a definition that should be written and then called
@@ -143,6 +141,9 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
143
141
  with open(defs_file, 'a') as f:
144
142
  f.write(def_code.strip() + "\n\n\n")
145
143
  return f"\n{parent_indent}{if_clause} {new_function_name.replace('def ', '')}(case):\n"
144
+ else:
145
+ raise ValueError(f"Conditions format is not valid, it should contain a function definition"
146
+ f" Instead got:\n{self.conditions.user_input}\n")
146
147
 
147
148
  @abstractmethod
148
149
  def _if_statement_source_code_clause(self) -> str:
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ set -e
3
+ if [ -z "$RDR_EDITOR_PORT" ]; then
4
+ echo "RDR_EDITOR_PORT is not set. Using default port 8080."
5
+ RDR_EDITOR_PORT=8080
6
+ fi
7
+ ADDR="0.0.0.0:$RDR_EDITOR_PORT"
8
+ # DATA_DIR="/root/.local/share/code-server"
9
+ echo "🚀 Starting code-server on $ADDR"
10
+ # Activate your Python virtual environment if exists else ignore
11
+ if [ -z "$RDR_VENV_PATH" ]; then
12
+ echo "No virtual environment found. Skipping activation."
13
+ else
14
+ source "$RDR_VENV_PATH/bin/activate"
15
+ # Set the default Python interpreter for VS Code
16
+ export DEFAULT_PYTHON_PATH=$(which python)
17
+ fi
18
+
19
+ # Start code-server.
20
+ echo "🚀 Starting code-server on $ADDR"
21
+ if [ -z "$CODE_SERVER_USER_DATA_DIR" ]; then
22
+ echo "No user data directory found. Using default"
23
+ code-server --bind-addr $ADDR --auth none "$@"
24
+ else
25
+ echo "Using user data directory: $CODE_SERVER_USER_DATA_DIR"
26
+ code-server --bind-addr $ADDR --user-data-dir $CODE_SERVER_USER_DATA_DIR --auth none "$@"
27
+ fi
@@ -1,23 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
- from copy import copy
4
+ import logging
5
5
  from types import MethodType
6
6
 
7
- from PyQt6.QtCore import Qt
8
- from PyQt6.QtGui import QPixmap, QPainter, QPalette
9
- from PyQt6.QtWidgets import (
10
- QWidget, QVBoxLayout, QLabel, QScrollArea,
11
- QSizePolicy, QToolButton, QHBoxLayout, QPushButton, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
12
- )
13
- from qtconsole.inprocess import QtInProcessKernelManager
14
- from qtconsole.rich_jupyter_widget import RichJupyterWidget
7
+ try:
8
+ from PyQt6.QtCore import Qt
9
+ from PyQt6.QtGui import QPixmap, QPainter, QPalette
10
+ from PyQt6.QtWidgets import (
11
+ QWidget, QVBoxLayout, QLabel, QScrollArea,
12
+ QSizePolicy, QToolButton, QHBoxLayout, QPushButton, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
13
+ )
14
+ from qtconsole.inprocess import QtInProcessKernelManager
15
+ from qtconsole.rich_jupyter_widget import RichJupyterWidget
16
+ except ImportError as e:
17
+ logging.debug("RDRCaseViewer is not available. GUI features will not work. "
18
+ "Make sure you have PyQt6 installed if you want to use the GUI features.")
19
+ raise ImportError("PyQt6 is required for the GUI features. Please install it using 'pip install PyQt6'") from e
20
+
15
21
  from typing_extensions import Optional, Any, List, Dict, Callable
16
22
 
17
23
  from ..datastructures.dataclasses import CaseQuery
18
24
  from ..datastructures.enums import PromptFor
19
25
  from .template_file_creator import TemplateFileCreator
20
- from ..utils import is_iterable, contains_return_statement, encapsulate_user_input
26
+ from ..utils import is_iterable, contains_return_statement, encapsulate_code_lines_into_a_function
21
27
  from .object_diagram import generate_object_graph
22
28
 
23
29
 
@@ -467,8 +473,7 @@ class RDRCaseViewer(QMainWindow):
467
473
  self.close()
468
474
 
469
475
  def _edit(self):
470
- self.template_file_creator = TemplateFileCreator(self.ipython_console.kernel.shell,
471
- self.case_query, self.prompt_for, self.code_to_modify,
476
+ self.template_file_creator = TemplateFileCreator(self.case_query, self.prompt_for, self.code_to_modify,
472
477
  self.print)
473
478
  self.template_file_creator.edit()
474
479
 
@@ -561,28 +566,6 @@ class RDRCaseViewer(QMainWindow):
561
566
  layout.addWidget(item_label)
562
567
 
563
568
 
564
- def encapsulate_code_lines_into_a_function(code_lines: List[str], function_name: str, function_signature: str,
565
- func_doc: str, case_query: CaseQuery) -> str:
566
- """
567
- Encapsulate the given code lines into a function with the specified name, signature, and docstring.
568
-
569
- :param code_lines: The lines of code to include in the user input.
570
- :param function_name: The name of the function to include in the user input.
571
- :param function_signature: The function signature to include in the user input.
572
- :param func_doc: The function docstring to include in the user input.
573
- :param case_query: The case query object.
574
- """
575
- code = '\n'.join(code_lines)
576
- code = encapsulate_user_input(code, function_signature, func_doc)
577
- if case_query.is_function:
578
- args = "**case"
579
- else:
580
- args = "case"
581
- if f"return {function_name}({args})" not in code:
582
- code = code.strip() + f"\nreturn {function_name}({args})"
583
- return code
584
-
585
-
586
569
  class IPythonConsole(RichJupyterWidget):
587
570
  def __init__(self, namespace=None, parent=None):
588
571
  super(IPythonConsole, self).__init__(parent)
@@ -8,9 +8,8 @@ from traitlets.config import Config
8
8
 
9
9
  from ..datastructures.dataclasses import CaseQuery
10
10
  from ..datastructures.enums import PromptFor
11
- from .gui import encapsulate_code_lines_into_a_function
12
11
  from .template_file_creator import TemplateFileCreator
13
- from ..utils import contains_return_statement, extract_dependencies
12
+ from ..utils import contains_return_statement, extract_dependencies, encapsulate_code_lines_into_a_function
14
13
 
15
14
 
16
15
  @magics_class
@@ -21,7 +20,7 @@ class MyMagics(Magics):
21
20
  prompt_for: Optional[PromptFor] = None,
22
21
  case_query: Optional[CaseQuery] = None):
23
22
  super().__init__(shell)
24
- self.rule_editor = TemplateFileCreator(shell, case_query, prompt_for=prompt_for, code_to_modify=code_to_modify)
23
+ self.rule_editor = TemplateFileCreator(case_query, prompt_for=prompt_for, code_to_modify=code_to_modify)
25
24
  self.all_code_lines: Optional[List[str]] = None
26
25
 
27
26
  @line_magic
@@ -1,4 +1,10 @@
1
- import graphviz
1
+ import logging
2
+
3
+ try:
4
+ import graphviz
5
+ except ImportError:
6
+ graphviz = None
7
+ logging.debug("Graphviz is not installed")
2
8
 
3
9
 
4
10
  def is_simple(obj):
@@ -2,7 +2,13 @@ import ast
2
2
  import logging
3
3
  from _ast import AST
4
4
 
5
- from PyQt6.QtWidgets import QApplication
5
+ try:
6
+ from PyQt6.QtWidgets import QApplication
7
+ from .gui import RDRCaseViewer
8
+ except ImportError:
9
+ QApplication = None
10
+ RDRCaseViewer = None
11
+
6
12
  from colorama import Fore, Style
7
13
  from pygments import highlight
8
14
  from pygments.formatters.terminal import TerminalFormatter
@@ -11,8 +17,7 @@ from typing_extensions import Optional, Tuple
11
17
 
12
18
  from ..datastructures.callable_expression import CallableExpression, parse_string_to_expression
13
19
  from ..datastructures.dataclasses import CaseQuery
14
- from ..datastructures.enums import PromptFor, InteractionMode
15
- from .gui import RDRCaseViewer
20
+ from ..datastructures.enums import PromptFor
16
21
  from .ipython_custom_shell import IPythonShell
17
22
  from ..utils import make_list
18
23
 
@@ -90,7 +95,7 @@ class UserPrompt:
90
95
  prompt_str = f"Give conditions on when can the rule be evaluated for:"
91
96
  case_query.scope.update({'case': case_query.case})
92
97
  shell = None
93
- if QApplication.instance() is None:
98
+ if self.viewer is None:
94
99
  prompt_str = self.construct_prompt_str_for_shell(case_query, prompt_for, prompt_str)
95
100
  shell = IPythonShell(header=prompt_str, prompt_for=prompt_for, case_query=case_query,
96
101
  code_to_modify=code_to_modify)
@@ -8,7 +8,6 @@ from functools import cached_property
8
8
  from textwrap import indent, dedent
9
9
 
10
10
  from colorama import Fore, Style
11
- from ipykernel.inprocess.ipkernel import InProcessInteractiveShell
12
11
  from typing_extensions import Optional, Type, List, Callable, Tuple, Dict
13
12
 
14
13
  from ..datastructures.case import Case
@@ -73,10 +72,9 @@ class TemplateFileCreator:
73
72
  The list of all code lines in the function in the temporary file.
74
73
  """
75
74
 
76
- def __init__(self, shell: InProcessInteractiveShell, case_query: CaseQuery, prompt_for: PromptFor,
75
+ def __init__(self, case_query: CaseQuery, prompt_for: PromptFor,
77
76
  code_to_modify: Optional[str] = None, print_func: Callable[[str], None] = print):
78
77
  self.print_func = print_func
79
- self.shell = shell
80
78
  self.code_to_modify = code_to_modify
81
79
  self.prompt_for = prompt_for
82
80
  self.case_query = case_query
@@ -260,11 +258,12 @@ class TemplateFileCreator:
260
258
  func_name = f"{prompt_for.value.lower()}_for_"
261
259
  case_name = case_query.name.replace(".", "_")
262
260
  if case_query.is_function:
263
- # convert any CamelCase word into snake_case by adding _ before each capital letter
264
- case_name = case_name.replace(f"_{case_query.attribute_name}", "")
265
- func_name += case_name
266
- func_name += f"_of_type_{'_or_'.join(map(lambda c: c.__name__,
267
- TemplateFileCreator.get_core_attribute_types(case_query)))}"
261
+ func_name += case_name.replace(f"_{case_query.attribute_name}", "")
262
+ else:
263
+ func_name += case_name
264
+ attribute_types = TemplateFileCreator.get_core_attribute_types(case_query)
265
+ attribute_type_names = [t.__name__ for t in attribute_types]
266
+ func_name += f"_of_type_{'_or_'.join(attribute_type_names)}"
268
267
  return str_to_snake_case(func_name)
269
268
 
270
269
  @cached_property
@@ -16,13 +16,27 @@ from dataclasses import is_dataclass, fields
16
16
  from enum import Enum
17
17
  from textwrap import dedent
18
18
  from types import NoneType
19
+ from typing import List
20
+
21
+ try:
22
+ import matplotlib
23
+ from matplotlib import pyplot as plt
24
+ Figure = plt.Figure
25
+ except ImportError as e:
26
+ matplotlib = None
27
+ plt = None
28
+ Figure = None
29
+ logging.debug(f"{e}: matplotlib is not installed")
30
+
31
+ try:
32
+ import networkx as nx
33
+ except ImportError as e:
34
+ nx = None
35
+ logging.debug(f"{e}: networkx is not installed")
19
36
 
20
- import matplotlib
21
- import networkx as nx
22
37
  import requests
23
38
  from anytree import Node, RenderTree
24
39
  from anytree.exporter import DotExporter
25
- from matplotlib import pyplot as plt
26
40
  from sqlalchemy import MetaData, inspect
27
41
  from sqlalchemy.orm import Mapped, registry, class_mapper, DeclarativeBase as SQLTable, Session
28
42
  from tabulate import tabulate
@@ -164,7 +178,7 @@ def extract_function_source(file_path: str,
164
178
  functions_source: Dict[str, Union[str, List[str]]] = {}
165
179
  line_numbers = []
166
180
  for node in tree.body:
167
- if isinstance(node, ast.FunctionDef) and node.name in function_names:
181
+ if isinstance(node, ast.FunctionDef) and (node.name in function_names or len(function_names) == 0):
168
182
  # Get the line numbers of the function
169
183
  lines = source.splitlines()
170
184
  func_lines = lines[node.lineno - 1:node.end_lineno]
@@ -172,9 +186,9 @@ def extract_function_source(file_path: str,
172
186
  func_lines = func_lines[1:]
173
187
  line_numbers.append((node.lineno, node.end_lineno))
174
188
  functions_source[node.name] = dedent("\n".join(func_lines)) if join_lines else func_lines
175
- if len(functions_source) == len(function_names):
189
+ if len(functions_source) >= len(function_names):
176
190
  break
177
- if len(functions_source) != len(function_names):
191
+ if len(functions_source) < len(function_names):
178
192
  raise ValueError(f"Could not find all functions in {file_path}: {function_names} not found,"
179
193
  f"functions not found: {set(function_names) - set(functions_source.keys())}")
180
194
  if return_line_numbers:
@@ -891,8 +905,8 @@ class SubclassJSONSerializer:
891
905
  if not filename.endswith(".json"):
892
906
  filename += ".json"
893
907
  with open(filename, "r") as f:
894
- scrdr_json = json.load(f)
895
- deserialized_obj = cls.from_json(scrdr_json)
908
+ rdr_json = json.load(f)
909
+ deserialized_obj = cls.from_json(rdr_json)
896
910
  cls.data_class_refs.clear()
897
911
  return deserialized_obj
898
912
 
@@ -939,9 +953,6 @@ class SubclassJSONSerializer:
939
953
 
940
954
  raise ValueError("Unknown type {}".format(data["_type"]))
941
955
 
942
- save = to_json_file
943
- load = from_json_file
944
-
945
956
 
946
957
  def _pickle_thread(thread_obj) -> Any:
947
958
  """Return a plain object with user-defined attributes but no thread behavior."""
@@ -1084,24 +1095,29 @@ def get_origin_and_args_from_type_hint(type_hint: Type) -> Tuple[Optional[Type],
1084
1095
  return origin, args
1085
1096
 
1086
1097
 
1087
- def table_rows_as_str(row_dict: Dict[str, Any], columns_per_row: int = 9):
1098
+ def table_rows_as_str(row_dicts: List[Dict[str, Any]], columns_per_row: int = 20):
1088
1099
  """
1089
1100
  Print a table row.
1090
1101
 
1091
- :param row_dict: The row to print.
1102
+ :param row_dicts: The rows to print.
1092
1103
  :param columns_per_row: The maximum number of columns per row.
1093
1104
  """
1094
- all_items = list(row_dict.items())
1105
+ all_row_dicts_items = [list(row_dict.items()) for row_dict in row_dicts]
1095
1106
  # make items a list of n rows such that each row has a max size of 4
1096
- all_items = [all_items[i:i + columns_per_row] for i in range(0, len(all_items), columns_per_row)]
1107
+ all_items = [all_items[i:i + columns_per_row] for all_items in all_row_dicts_items
1108
+ for i in range(0, len(all_items), columns_per_row)]
1097
1109
  keys = [list(map(lambda i: i[0], row)) for row in all_items]
1098
1110
  values = [list(map(lambda i: i[1], row)) for row in all_items]
1111
+ zipped_keys = list(zip(*keys))
1112
+ zipped_values = list(zip(*values))
1113
+ keys_values = [list(zip(zipped_keys[i], zipped_values[i])) for i in range(len(zipped_keys))]
1114
+ keys_values = [list(r[0]) + list(r[1]) if len(r) > 1 else r[0] for r in keys_values]
1099
1115
  all_table_rows = []
1100
- for row_keys, row_values in zip(keys, values):
1101
- row_values = [str(v) if v is not None else "" for v in row_values]
1102
- row_values = [v.lower() if v in ["True", "False"] else v for v in row_values]
1103
- table = tabulate([row_values], headers=row_keys, tablefmt='plain', maxcolwidths=[20] * len(row_keys))
1104
- all_table_rows.append(table)
1116
+ row_values = [list(map(lambda v: str(v) if v is not None else "", row)) for row in keys_values]
1117
+ row_values = [list(map(lambda v: v[:150] + '...' if len(v) > 150 else v, row)) for row in row_values]
1118
+ row_values = [list(map(lambda v: v.lower() if v in ["True", "False"] else v, row)) for row in row_values]
1119
+ table = tabulate(row_values, tablefmt='simple_grid', maxcolwidths=[150] * 2)
1120
+ all_table_rows.append(table)
1105
1121
  return "\n".join(all_table_rows)
1106
1122
 
1107
1123
 
@@ -1350,7 +1366,7 @@ def render_tree(root: Node, use_dot_exporter: bool = False,
1350
1366
  de.to_picture(f"{filename}{'.png'}")
1351
1367
 
1352
1368
 
1353
- def draw_tree(root: Node, fig: plt.Figure):
1369
+ def draw_tree(root: Node, fig: Figure):
1354
1370
  """
1355
1371
  Draw the tree using matplotlib and networkx.
1356
1372
  """
@@ -1374,3 +1390,25 @@ def draw_tree(root: Node, fig: plt.Figure):
1374
1390
  nx.draw_networkx_edge_labels(graph, pos, edge_labels=nx.get_edge_attributes(graph, 'weight'),
1375
1391
  ax=fig.gca(), rotate=False, clip_on=False)
1376
1392
  plt.pause(0.1)
1393
+
1394
+
1395
+ def encapsulate_code_lines_into_a_function(code_lines: List[str], function_name: str, function_signature: str,
1396
+ func_doc: str, case_query: CaseQuery) -> str:
1397
+ """
1398
+ Encapsulate the given code lines into a function with the specified name, signature, and docstring.
1399
+
1400
+ :param code_lines: The lines of code to include in the user input.
1401
+ :param function_name: The name of the function to include in the user input.
1402
+ :param function_signature: The function signature to include in the user input.
1403
+ :param func_doc: The function docstring to include in the user input.
1404
+ :param case_query: The case query object.
1405
+ """
1406
+ code = '\n'.join(code_lines)
1407
+ code = encapsulate_user_input(code, function_signature, func_doc)
1408
+ if case_query.is_function:
1409
+ args = "**case"
1410
+ else:
1411
+ args = "case"
1412
+ if f"return {function_name}({args})" not in code:
1413
+ code = code.strip() + f"\nreturn {function_name}({args})"
1414
+ return code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.4.8
3
+ Version: 0.4.9
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
@@ -682,21 +682,23 @@ Classifier: Programming Language :: Python :: 3
682
682
  Description-Content-Type: text/markdown
683
683
  License-File: LICENSE
684
684
  Requires-Dist: anytree>=2.8.0
685
- Requires-Dist: pandas>=2.0.3
686
- Requires-Dist: networkx>=3.1
687
685
  Requires-Dist: ordered_set>=4
688
686
  Requires-Dist: pygraphviz>=1.7
689
- Requires-Dist: ucimlrepo>=0.0.7
690
687
  Requires-Dist: typing-extensions>=4.12.2
691
- Requires-Dist: matplotlib>=3.7.5
692
- Requires-Dist: sqlalchemy
693
- Requires-Dist: pyqt6
694
- Requires-Dist: qtconsole
695
688
  Requires-Dist: graphviz
696
689
  Requires-Dist: tabulate
697
690
  Requires-Dist: ipython
698
691
  Requires-Dist: requests
699
692
  Requires-Dist: colorama
693
+ Requires-Dist: pygments
694
+ Requires-Dist: sqlalchemy
695
+ Requires-Dist: pandas
696
+ Provides-Extra: viz
697
+ Requires-Dist: networkx>=3.1; extra == "viz"
698
+ Requires-Dist: matplotlib>=3.7.5; extra == "viz"
699
+ Provides-Extra: gui
700
+ Requires-Dist: pyqt6; extra == "gui"
701
+ Requires-Dist: qtconsole; extra == "gui"
700
702
  Dynamic: license-file
701
703
 
702
704
  # Ripple Down Rules (RDR)
@@ -0,0 +1,26 @@
1
+ ripple_down_rules/__init__.py,sha256=PUnq-y-UONNX6u9U669iIwMK6S5qMXYECFClTLPBrbI,99
2
+ ripple_down_rules/datasets.py,sha256=fJbZ7V-UUYTu5XVVpFinTbuzN3YePCnUB01L3AyZVM8,6837
3
+ ripple_down_rules/experts.py,sha256=9Vc3vx0uhDPy3YlNjwKuWJLl_A-kubRPUU6bMvQhaAg,13237
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=E1OiiZClQyAfGjL64ID-MWYFO4-h8iUAX-Vm9qrOoeQ,48727
7
+ ripple_down_rules/rdr_decorators.py,sha256=pYCKLgMKgQ6x_252WQtF2t4ZNjWPBxnaWtJ6TpGdcc0,7820
8
+ ripple_down_rules/rules.py,sha256=TPNVMqW9T-_46BS4WemrspLg5uG8kP6tsPvWWBAzJxg,17515
9
+ ripple_down_rules/start-code-server.sh,sha256=otClk7VmDgBOX2TS_cjws6K0UwvgAUJhoA0ugkPCLqQ,949
10
+ ripple_down_rules/utils.py,sha256=uS38KcFceRMzT_470DCL1M0LzETdP5RLwE7cCmfo7eI,51086
11
+ ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
12
+ ripple_down_rules/datastructures/callable_expression.py,sha256=3EucsD3jWzekhjyzL2y0dyUsucd-aqC9glmgPL0Ubb4,12425
13
+ ripple_down_rules/datastructures/case.py,sha256=r8kjL9xP_wk84ThXusspgPMrAoed2bGQmKi54fzhmH8,15258
14
+ ripple_down_rules/datastructures/dataclasses.py,sha256=PuD-7zWqWT2p4FnGvnihHvZlZKg9A1ctnFgVYf2cs-8,8554
15
+ ripple_down_rules/datastructures/enums.py,sha256=ce7tqS0otfSTNAOwsnXlhsvIn4iW_Y_N3TNebF3YoZs,5700
16
+ ripple_down_rules/user_interface/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ ripple_down_rules/user_interface/gui.py,sha256=SB0gUhgReJ3yx-NEHRPMGVuNRLPRUwW8-qup-Kd4Cfo,27182
18
+ ripple_down_rules/user_interface/ipython_custom_shell.py,sha256=24MIFwqnAhC6ofObEO6x5xRWRnyQmPpPmTvxbCKBrzM,6514
19
+ ripple_down_rules/user_interface/object_diagram.py,sha256=tsB6iuLNEbHxp5lR2WjyejjWbnAX_nHF9xS8jNPOQVk,4548
20
+ ripple_down_rules/user_interface/prompt.py,sha256=AkkltdDIaioN43lkRKDPKSjJcmdSSGZDMYz7AL7X9lE,8082
21
+ ripple_down_rules/user_interface/template_file_creator.py,sha256=ycCbddy_BJP8d0Q2Sj21UzamhGtqGZuK_e73VTJqznY,13766
22
+ ripple_down_rules-0.4.9.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
23
+ ripple_down_rules-0.4.9.dist-info/METADATA,sha256=T-Q5WS_rd2wJsVcip0d6-so4qlvygGnUdsYBWLVBKEE,43306
24
+ ripple_down_rules-0.4.9.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
25
+ ripple_down_rules-0.4.9.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
26
+ ripple_down_rules-0.4.9.dist-info/RECORD,,