ripple-down-rules 0.4.8__py3-none-any.whl → 0.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ripple_down_rules/__init__.py +5 -0
- ripple_down_rules/datastructures/callable_expression.py +20 -1
- ripple_down_rules/datastructures/case.py +8 -6
- ripple_down_rules/datastructures/dataclasses.py +9 -1
- ripple_down_rules/experts.py +194 -33
- ripple_down_rules/rdr.py +196 -114
- ripple_down_rules/rdr_decorators.py +73 -52
- ripple_down_rules/rules.py +5 -4
- ripple_down_rules/start-code-server.sh +27 -0
- ripple_down_rules/user_interface/gui.py +17 -34
- ripple_down_rules/user_interface/ipython_custom_shell.py +2 -3
- ripple_down_rules/user_interface/object_diagram.py +7 -1
- ripple_down_rules/user_interface/prompt.py +9 -4
- ripple_down_rules/user_interface/template_file_creator.py +7 -8
- ripple_down_rules/utils.py +59 -21
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/METADATA +10 -8
- ripple_down_rules-0.4.9.dist-info/RECORD +26 -0
- ripple_down_rules-0.4.8.dist-info/RECORD +0 -25
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/WHEEL +0 -0
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/licenses/LICENSE +0 -0
- {ripple_down_rules-0.4.8.dist-info → ripple_down_rules-0.4.9.dist-info}/top_level.txt +0 -0
@@ -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
|
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.
|
61
|
-
self.
|
62
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
121
|
-
self.rdr = GeneralRDR.load(self.
|
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
|
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.
|
160
|
+
self.rdr.update_from_python(self.rdr_models_dir, self.model_name)
|
ripple_down_rules/rules.py
CHANGED
@@ -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
|
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
|
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
|
-
|
4
|
+
import logging
|
5
5
|
from types import MethodType
|
6
6
|
|
7
|
-
|
8
|
-
from PyQt6.
|
9
|
-
from PyQt6.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
from qtconsole.
|
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,
|
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.
|
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(
|
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
|
@@ -2,7 +2,13 @@ import ast
|
|
2
2
|
import logging
|
3
3
|
from _ast import AST
|
4
4
|
|
5
|
-
|
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
|
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
|
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,
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
ripple_down_rules/utils.py
CHANGED
@@ -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)
|
189
|
+
if len(functions_source) >= len(function_names):
|
176
190
|
break
|
177
|
-
if len(functions_source)
|
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
|
-
|
895
|
-
deserialized_obj = cls.from_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(
|
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
|
1102
|
+
:param row_dicts: The rows to print.
|
1092
1103
|
:param columns_per_row: The maximum number of columns per row.
|
1093
1104
|
"""
|
1094
|
-
|
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
|
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
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
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:
|
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.
|
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,,
|