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.
@@ -0,0 +1,146 @@
1
+ import logging
2
+ from typing import Optional, List
3
+
4
+ from IPython.core.magic import magics_class, Magics, line_magic
5
+ from IPython.terminal.embed import InteractiveShellEmbed
6
+ from colorama import Fore, Style
7
+ from traitlets.config import Config
8
+
9
+ from ..datastructures.dataclasses import CaseQuery
10
+ from ..datastructures.enums import PromptFor
11
+ from .gui import encapsulate_code_lines_into_a_function
12
+ from .template_file_creator import TemplateFileCreator
13
+ from ..utils import contains_return_statement, extract_dependencies
14
+
15
+
16
+ @magics_class
17
+ class MyMagics(Magics):
18
+
19
+ def __init__(self, shell,
20
+ code_to_modify: Optional[str] = None,
21
+ prompt_for: Optional[PromptFor] = None,
22
+ case_query: Optional[CaseQuery] = None):
23
+ super().__init__(shell)
24
+ self.rule_editor = TemplateFileCreator(shell, case_query, prompt_for=prompt_for, code_to_modify=code_to_modify)
25
+ self.all_code_lines: Optional[List[str]] = None
26
+
27
+ @line_magic
28
+ def edit(self, line):
29
+ self.rule_editor.edit()
30
+
31
+ @line_magic
32
+ def load(self, line):
33
+ self.all_code_lines = self.rule_editor.load()
34
+
35
+ @line_magic
36
+ def help(self, line):
37
+ """
38
+ Display help information for the Ipython shell.
39
+ """
40
+ help_text = f"""
41
+ Directly write python code in the shell, and then `{Fore.GREEN}return {Fore.RESET}output`. Or use
42
+ the magic commands to write the code in a temporary file and edit it in PyCharm:
43
+ {Fore.MAGENTA}Usage: %edit{Style.RESET_ALL}
44
+ Opens a temporary file in PyCharm for editing a function (conclusion or conditions for case)
45
+ that will be executed on the case object.
46
+ {Fore.MAGENTA}Usage: %load{Style.RESET_ALL}
47
+ Loads the function defined in the temporary file into the user namespace, that can then be used inside the
48
+ Ipython shell. You can then do `{Fore.GREEN}return {Fore.RESET}function_name(case)`.
49
+ """
50
+ print(help_text)
51
+
52
+
53
+ class CustomInteractiveShell(InteractiveShellEmbed):
54
+ def __init__(self, code_to_modify: Optional[str] = None,
55
+ prompt_for: Optional[PromptFor] = None,
56
+ case_query: Optional[CaseQuery] = None,
57
+ **kwargs):
58
+ kwargs.update({'user_ns': case_query.scope})
59
+ super().__init__(**kwargs)
60
+ self.my_magics = MyMagics(self, code_to_modify=code_to_modify,
61
+ prompt_for=prompt_for, case_query=case_query)
62
+ self.register_magics(self.my_magics)
63
+ self.all_lines = []
64
+
65
+ def run_cell(self, raw_cell: str, **kwargs):
66
+ """
67
+ Override the run_cell method to capture return statements.
68
+ """
69
+ if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
70
+ if self.my_magics.rule_editor.func_name in raw_cell:
71
+ self.all_lines = self.my_magics.all_code_lines
72
+ self.all_lines.append(raw_cell)
73
+ self.history_manager.store_inputs(line_num=self.execution_count, source=raw_cell)
74
+ self.ask_exit()
75
+ return None
76
+ result = super().run_cell(raw_cell, **kwargs)
77
+ if result.error_in_exec is None and result.error_before_exec is None:
78
+ self.all_lines.append(raw_cell)
79
+ return result
80
+
81
+
82
+ class IPythonShell:
83
+ """
84
+ Create an embedded Ipython shell that can be used to prompt the user for input.
85
+ """
86
+
87
+ def __init__(self, header: Optional[str] = None,
88
+ prompt_for: Optional[PromptFor] = None, case_query: Optional[CaseQuery] = None,
89
+ code_to_modify: Optional[str] = None):
90
+ """
91
+ Initialize the Ipython shell with the given scope and header.
92
+
93
+ :param header: The header to display when the shell is started.
94
+ :param prompt_for: The type of information to ask the user about.
95
+ :param case_query: The case query which contains the case and the attribute to ask about.
96
+ :param code_to_modify: The code to modify. If given, will be used as a start for user to modify.
97
+ """
98
+ self.header: str = header or ">>> Embedded Ipython Shell"
99
+ self.case_query: Optional[CaseQuery] = case_query
100
+ self.prompt_for: Optional[PromptFor] = prompt_for
101
+ self.code_to_modify: Optional[str] = code_to_modify
102
+ self.user_input: Optional[str] = None
103
+ self.shell: CustomInteractiveShell = self._init_shell()
104
+ self.all_code_lines: List[str] = []
105
+
106
+ def _init_shell(self):
107
+ """
108
+ Initialize the Ipython shell with a custom configuration.
109
+ """
110
+ cfg = Config()
111
+ shell = CustomInteractiveShell(config=cfg, banner1=self.header,
112
+ code_to_modify=self.code_to_modify,
113
+ prompt_for=self.prompt_for,
114
+ case_query=self.case_query,
115
+ )
116
+ return shell
117
+
118
+ def run(self):
119
+ """
120
+ Run the embedded shell.
121
+ """
122
+ while True:
123
+ try:
124
+ self.shell()
125
+ self.update_user_input_from_code_lines()
126
+ break
127
+ except Exception as e:
128
+ logging.error(e)
129
+ print(f"{Fore.RED}ERROR::{e}{Style.RESET_ALL}")
130
+
131
+ def update_user_input_from_code_lines(self):
132
+ """
133
+ Update the user input from the code lines captured in the shell.
134
+ """
135
+ if len(self.shell.all_lines) == 1 and self.shell.all_lines[0].replace('return', '').strip() == '':
136
+ self.user_input = None
137
+ else:
138
+ self.all_code_lines = extract_dependencies(self.shell.all_lines)
139
+ if len(self.all_code_lines) == 1 and self.all_code_lines[0].strip() == '':
140
+ self.user_input = None
141
+ else:
142
+ self.user_input = encapsulate_code_lines_into_a_function(self.all_code_lines,
143
+ function_name=self.shell.my_magics.rule_editor.func_name,
144
+ function_signature=self.shell.my_magics.rule_editor.function_signature,
145
+ func_doc=self.shell.my_magics.rule_editor.func_doc,
146
+ case_query=self.case_query)
@@ -0,0 +1,109 @@
1
+ import graphviz
2
+
3
+
4
+ def is_simple(obj):
5
+ return isinstance(obj, (int, float, str, bool, type(None)))
6
+
7
+
8
+ def get_colored_value(value):
9
+ if isinstance(value, str):
10
+ color = '#A31515' # red for strings
11
+ val = repr(value)
12
+ elif isinstance(value, (int, float)):
13
+ color = '#098658' # green for numbers
14
+ val = str(value)
15
+ elif isinstance(value, bool):
16
+ color = '#0000FF' # blue for booleans
17
+ val = str(value)
18
+ elif value is None:
19
+ color = '#808080' # gray for None
20
+ val = 'None'
21
+ else:
22
+ color = '#000000' # fallback
23
+ val = str(value)
24
+ val = (val.replace("class ", "").replace("'", "").replace('<', "")
25
+ .replace('>', "").replace('?', '').replace('\n', ' '))
26
+ val = val[:50] + '...' if len(val) > 50 else val
27
+ return f'<FONT COLOR="{color}">{val}</FONT>'
28
+
29
+
30
+ def generate_object_graph(obj, name='root', seen=None, graph=None, current_depth=0, max_depth=3, chain_name=None,
31
+ included_attrs=None):
32
+ if seen is None:
33
+ seen = set()
34
+ if graph is None:
35
+ graph = graphviz.Digraph(format='svg', graph_attr={'dpi': '300'})
36
+ graph.attr('node', shape='plaintext')
37
+
38
+ obj_id = id(obj)
39
+ if obj_id in seen or current_depth > max_depth:
40
+ return graph
41
+ seen.add(obj_id)
42
+
43
+ # Build HTML table label for this object node
44
+ type_name = type(obj).__name__ if not isinstance(obj, type) else obj.__name__
45
+ rows = [f'<TR><TD><B>{name}</B></TD><TD><I>{type_name}</I></TD></TR>']
46
+
47
+ # We'll keep track of non-simple attrs to add edges later
48
+ non_simple_attrs = []
49
+
50
+ if isinstance(obj, (list, tuple, set, dict)):
51
+ items = obj.items() if isinstance(obj, dict) else enumerate(obj)
52
+ for idx, item in items:
53
+ if idx == "scope":
54
+ continue
55
+ # Represent items as attr = index + type (for the label)
56
+ if is_simple(item):
57
+ val_colored = get_colored_value(item)
58
+ rows.append(f'<TR><TD ALIGN="LEFT" PORT="{idx}">[{idx}]</TD><TD ALIGN="LEFT">{val_colored}</TD></TR>')
59
+ else:
60
+ type_name = type(item).__name__ if not isinstance(item, type) else item.__name__
61
+ rows.append(
62
+ f'<TR><TD ALIGN="LEFT" PORT="{idx}">[{idx}]</TD><TD ALIGN="LEFT"><I>{type_name}</I></TD></TR>')
63
+ non_simple_attrs.append((str(idx), item))
64
+
65
+ else:
66
+ for attr in dir(obj):
67
+ if attr.startswith('_'):
68
+ continue
69
+ if attr == 'scope':
70
+ continue
71
+ value = getattr(obj, attr)
72
+ if callable(value):
73
+ continue
74
+ if is_simple(value):
75
+ val_colored = get_colored_value(value)
76
+ rows.append(f'<TR><TD ALIGN="LEFT" PORT="{attr}">{attr}</TD><TD ALIGN="LEFT">{val_colored}</TD></TR>')
77
+ else:
78
+ type_name = type(value).__name__ if not isinstance(value, type) else value.__name__
79
+ # Show just name and type inside the node
80
+ rows.append(
81
+ f'<TR><TD ALIGN="LEFT" PORT="{attr}">{attr}</TD><TD ALIGN="LEFT"><I>{type_name}</I></TD></TR>')
82
+ non_simple_attrs.append((attr, value))
83
+
84
+ label = f"""<
85
+ <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
86
+ {''.join(rows)}
87
+ </TABLE>
88
+ >"""
89
+
90
+ graph.node(str(obj_id), label)
91
+ chain_name = chain_name if chain_name is not None else name
92
+
93
+ # Add edges from this node to non-simple attribute nodes
94
+ for attr, value in non_simple_attrs:
95
+ if attr.startswith('_'):
96
+ continue
97
+ if attr == 'scope':
98
+ continue
99
+ if callable(value):
100
+ continue
101
+ attr_chain_name = f"{chain_name}.{attr}"
102
+ if included_attrs is not None and attr_chain_name not in included_attrs:
103
+ continue
104
+ generate_object_graph(value, attr, seen, graph, current_depth + 1, max_depth, chain_name=f"{chain_name}.{attr}",
105
+ included_attrs=included_attrs)
106
+ # Edge from this object's attribute port to nested object's node
107
+ graph.edge(f"{obj_id}:{attr}", str(id(value)), label=attr)
108
+
109
+ return graph
@@ -0,0 +1,159 @@
1
+ import ast
2
+ import logging
3
+ from _ast import AST
4
+
5
+ from PyQt6.QtWidgets import QApplication
6
+ from colorama import Fore, Style
7
+ from pygments import highlight
8
+ from pygments.formatters.terminal import TerminalFormatter
9
+ from pygments.lexers.python import PythonLexer
10
+ from typing_extensions import Optional, Tuple
11
+
12
+ from ..datastructures.callable_expression import CallableExpression, parse_string_to_expression
13
+ from ..datastructures.dataclasses import CaseQuery
14
+ from ..datastructures.enums import PromptFor, InteractionMode
15
+ from .gui import RDRCaseViewer
16
+ from .ipython_custom_shell import IPythonShell
17
+ from ..utils import make_list
18
+
19
+
20
+ class UserPrompt:
21
+ """
22
+ A class to handle user prompts for the RDR.
23
+ """
24
+ def __init__(self, viewer: Optional[RDRCaseViewer] = None):
25
+ """
26
+ Initialize the UserPrompt class.
27
+
28
+ :param viewer: The RDRCaseViewer instance to use for prompting the user.
29
+ """
30
+ self.viewer = viewer
31
+ self.print_func = print if viewer is None else viewer.print
32
+
33
+
34
+ def prompt_user_for_expression(self, case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None) \
35
+ -> Tuple[Optional[str], Optional[CallableExpression]]:
36
+ """
37
+ Prompt the user for an executable python expression to the given case query.
38
+
39
+ :param case_query: The case query to prompt the user for.
40
+ :param prompt_for: The type of information ask user about.
41
+ :param prompt_str: The prompt string to display to the user.
42
+ :return: A callable expression that takes a case and executes user expression on it.
43
+ """
44
+ prev_user_input: Optional[str] = None
45
+ callable_expression: Optional[CallableExpression] = None
46
+ while True:
47
+ user_input, expression_tree = self.prompt_user_about_case(case_query, prompt_for, prompt_str,
48
+ code_to_modify=prev_user_input)
49
+ if user_input is None:
50
+ if prompt_for == PromptFor.Conclusion:
51
+ self.print_func(f"{Fore.YELLOW}No conclusion provided. Exiting.{Style.RESET_ALL}")
52
+ return None, None
53
+ else:
54
+ self.print_func(f"{Fore.RED}Conditions must be provided. Please try again.{Style.RESET_ALL}")
55
+ continue
56
+ prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
57
+ conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
58
+ callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
59
+ scope=case_query.scope,
60
+ mutually_exclusive=case_query.mutually_exclusive)
61
+ try:
62
+ result = callable_expression(case_query.case)
63
+ if len(make_list(result)) == 0:
64
+ self.print_func(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
65
+ f" Please modify!{Style.RESET_ALL}")
66
+ continue
67
+ break
68
+ except Exception as e:
69
+ logging.error(e)
70
+ self.print_func(f"{Fore.RED}{e}{Style.RESET_ALL}")
71
+ return user_input, callable_expression
72
+
73
+
74
+ def prompt_user_about_case(self, case_query: CaseQuery, prompt_for: PromptFor,
75
+ prompt_str: Optional[str] = None,
76
+ code_to_modify: Optional[str] = None) -> Tuple[Optional[str], Optional[AST]]:
77
+ """
78
+ Prompt the user for input.
79
+
80
+ :param case_query: The case query to prompt the user for.
81
+ :param prompt_for: The type of information the user should provide for the given case.
82
+ :param prompt_str: The prompt string to display to the user.
83
+ :param code_to_modify: The code to modify. If given will be used as a start for user to modify.
84
+ :return: The user input, and the executable expression that was parsed from the user input.
85
+ """
86
+ if prompt_str is None:
87
+ if prompt_for == PromptFor.Conclusion:
88
+ prompt_str = f"Give possible value(s) for:"
89
+ else:
90
+ prompt_str = f"Give conditions on when can the rule be evaluated for:"
91
+ case_query.scope.update({'case': case_query.case})
92
+ shell = None
93
+ if QApplication.instance() is None:
94
+ prompt_str = self.construct_prompt_str_for_shell(case_query, prompt_for, prompt_str)
95
+ shell = IPythonShell(header=prompt_str, prompt_for=prompt_for, case_query=case_query,
96
+ code_to_modify=code_to_modify)
97
+ else:
98
+
99
+ self.viewer.update_for_case_query(case_query, prompt_str,
100
+ prompt_for=prompt_for, code_to_modify=code_to_modify)
101
+ return self.prompt_user_input_and_parse_to_expression(shell=shell)
102
+
103
+
104
+ def construct_prompt_str_for_shell(self, case_query: CaseQuery, prompt_for: PromptFor,
105
+ prompt_str: Optional[str] = None) -> str:
106
+ """
107
+ Construct the prompt string for the shell.
108
+
109
+ :param case_query: The case query to prompt the user for.
110
+ :param prompt_for: The type of information the user should provide for the given case.
111
+ :param prompt_str: The prompt string to display to the user.
112
+ """
113
+ prompt_str += (f"\n{Fore.CYAN}{case_query.name}{Fore.MAGENTA} of type(s) "
114
+ f"{Fore.CYAN}({', '.join(map(lambda x: x.__name__, case_query.core_attribute_type))}){Fore.MAGENTA}")
115
+ if prompt_for == PromptFor.Conditions:
116
+ prompt_str += (f"\ne.g. `{Fore.GREEN}return {Fore.BLUE}len{Fore.RESET}(case.attribute) > {Fore.BLUE}0` "
117
+ f"{Fore.MAGENTA}\nOR `{Fore.GREEN}return {Fore.YELLOW}True`{Fore.MAGENTA} (If you want the"
118
+ f" rule to be always evaluated) \n"
119
+ f"You can also do {Fore.YELLOW}%edit{Fore.MAGENTA} for more complex conditions.")
120
+
121
+ prompt_str = f"{Fore.MAGENTA}{prompt_str}{Fore.YELLOW}\n(Write %help for guide){Fore.RESET}"
122
+ return prompt_str
123
+
124
+
125
+ def prompt_user_input_and_parse_to_expression(self, shell: Optional[IPythonShell] = None,
126
+ user_input: Optional[str] = None) \
127
+ -> Tuple[Optional[str], Optional[ast.AST]]:
128
+ """
129
+ Prompt the user for input.
130
+
131
+ :param shell: The Ipython shell to use for prompting the user.
132
+ :param user_input: The user input to use. If given, the user input will be used instead of prompting the user.
133
+ :return: The user input and the AST tree.
134
+ """
135
+ while True:
136
+ if user_input is None:
137
+ if QApplication.instance() is None:
138
+ shell = IPythonShell() if shell is None else shell
139
+ shell.run()
140
+ user_input = shell.user_input
141
+ else:
142
+ app = QApplication.instance()
143
+ if app is None:
144
+ app = QApplication([])
145
+ self.viewer.show()
146
+ app.exec()
147
+ user_input = self.viewer.user_input
148
+ if user_input is None:
149
+ return None, None
150
+ self.print_func(f"{Fore.GREEN}Captured User input: {Style.RESET_ALL}")
151
+ highlighted_code = highlight(user_input, PythonLexer(), TerminalFormatter())
152
+ self.print_func(highlighted_code)
153
+ try:
154
+ return user_input, parse_string_to_expression(user_input)
155
+ except Exception as e:
156
+ msg = f"Error parsing expression: {e}"
157
+ logging.error(msg)
158
+ self.print_func(f"{Fore.RED}{msg}{Style.RESET_ALL}")
159
+ user_input = None