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.
@@ -1,404 +0,0 @@
1
- import ast
2
- import logging
3
- import os
4
- import subprocess
5
- import tempfile
6
- from _ast import AST
7
- from functools import cached_property
8
- from textwrap import indent, dedent
9
-
10
- from IPython.core.magic import register_line_magic, line_magic, Magics, magics_class
11
- from IPython.terminal.embed import InteractiveShellEmbed
12
- from pygments import highlight
13
- from pygments.formatters.terminal import TerminalFormatter
14
- from pygments.lexers.python import PythonLexer
15
- from traitlets.config import Config
16
- from typing_extensions import List, Optional, Tuple, Dict, Type, Union, Any
17
-
18
- from .datastructures.enums import PromptFor
19
- from .datastructures.case import Case
20
- from .datastructures.callable_expression import CallableExpression, parse_string_to_expression
21
- from .datastructures.dataclasses import CaseQuery
22
- from .utils import extract_dependencies, contains_return_statement, make_set, get_imports_from_scope, make_list, \
23
- get_import_from_type, get_imports_from_types, is_iterable, extract_function_source, encapsulate_user_input, \
24
- are_results_subclass_of_types
25
- from colorama import Fore, Style, init
26
-
27
-
28
- @magics_class
29
- class MyMagics(Magics):
30
- def __init__(self, shell, scope, output_type: Optional[Type] = None, func_name: str = "user_case",
31
- func_doc: str = "User defined function to be executed on the case.",
32
- code_to_modify: Optional[str] = None,
33
- attribute_type_hint: Optional[str] = None,
34
- prompt_for: Optional[PromptFor] = None):
35
- super().__init__(shell)
36
- self.scope = scope
37
- self.temp_file_path = None
38
- self.func_name = func_name
39
- self.func_doc = func_doc
40
- self.code_to_modify = code_to_modify
41
- self.attribute_type_hint = attribute_type_hint
42
- self.prompt_for = prompt_for
43
- self.output_type = make_list(output_type) if output_type is not None else None
44
- self.user_edit_line = 0
45
- self.function_signature: Optional[str] = None
46
- self.build_function_signature()
47
-
48
- @line_magic
49
- def edit(self, line):
50
-
51
- boilerplate_code = self.build_boilerplate_code()
52
-
53
- self.write_to_file(boilerplate_code)
54
-
55
- print(f"Opening {self.temp_file_path} in PyCharm...")
56
- subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path],
57
- stdout=subprocess.DEVNULL,
58
- stderr=subprocess.DEVNULL)
59
-
60
- def build_boilerplate_code(self):
61
- imports = self.get_imports()
62
- self.build_function_signature()
63
- if self.code_to_modify is not None:
64
- body = indent(dedent(self.code_to_modify), ' ')
65
- else:
66
- body = " # Write your code here\n pass"
67
- boilerplate = f"""{imports}\n\n{self.function_signature}\n \"\"\"{self.func_doc}\"\"\"\n{body}"""
68
- self.user_edit_line = imports.count('\n')+6
69
- return boilerplate
70
-
71
- def build_function_signature(self):
72
- output_type_hint = ""
73
- if self.prompt_for == PromptFor.Conditions:
74
- output_type_hint = " -> bool"
75
- elif self.prompt_for == PromptFor.Conclusion:
76
- output_type_hint = f" -> {self.attribute_type_hint}"
77
- self.function_signature = f"def {self.func_name}(case: {self.case_type.__name__}){output_type_hint}:"
78
-
79
- def write_to_file(self, code: str):
80
- tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".py",
81
- dir=os.path.dirname(self.scope['__file__']))
82
- tmp.write(code)
83
- tmp.flush()
84
- self.temp_file_path = tmp.name
85
- tmp.close()
86
-
87
- def get_imports(self):
88
- case_type_import = f"from {self.case_type.__module__} import {self.case_type.__name__}"
89
- if self.output_type is None:
90
- output_type_imports = [f"from typing_extensions import Any"]
91
- else:
92
- output_type_imports = get_imports_from_types(self.output_type)
93
- if len(self.output_type) > 1:
94
- output_type_imports.append("from typing_extensions import Union")
95
- if list in self.output_type:
96
- output_type_imports.append("from typing_extensions import List")
97
- imports = get_imports_from_scope(self.scope)
98
- imports = [i for i in imports if ("get_ipython" not in i)]
99
- if case_type_import not in imports:
100
- imports.append(case_type_import)
101
- imports.extend([oti for oti in output_type_imports if oti not in imports])
102
- imports = set(imports)
103
- return '\n'.join(imports)
104
-
105
- @cached_property
106
- def case_type(self) -> Type:
107
- """
108
- Get the type of the case object in the current scope.
109
-
110
- :return: The type of the case object.
111
- """
112
- case = self.scope['case']
113
- return case._obj_type if isinstance(case, Case) else type(case)
114
-
115
- @line_magic
116
- def load(self, line):
117
- if not self.temp_file_path:
118
- print(f"{Fore.RED}ERROR:: No file to load. Run %edit first.{Style.RESET_ALL}")
119
- return
120
-
121
- with open(self.temp_file_path, 'r') as f:
122
- source = f.read()
123
-
124
- tree = ast.parse(source)
125
- for node in tree.body:
126
- if isinstance(node, ast.FunctionDef) and node.name == self.func_name:
127
- exec_globals = {}
128
- exec(source, self.scope, exec_globals)
129
- user_function = exec_globals[self.func_name]
130
- self.shell.user_ns[self.func_name] = user_function
131
- print(f"{Fore.BLUE}Loaded `{self.func_name}` function into user namespace.{Style.RESET_ALL}")
132
- return
133
-
134
- print(f"{Fore.RED}ERROR:: Function `{self.func_name}` not found.{Style.RESET_ALL}")
135
-
136
- @line_magic
137
- def help(self, line):
138
- """
139
- Display help information for the Ipython shell.
140
- """
141
- help_text = f"""
142
- Directly write python code in the shell, and then `{Fore.GREEN}return {Fore.RESET}output`. Or use
143
- the magic commands to write the code in a temporary file and edit it in PyCharm:
144
- {Fore.MAGENTA}Usage: %edit{Style.RESET_ALL}
145
- Opens a temporary file in PyCharm for editing a function (conclusion or conditions for case)
146
- that will be executed on the case object.
147
- {Fore.MAGENTA}Usage: %load{Style.RESET_ALL}
148
- Loads the function defined in the temporary file into the user namespace, that can then be used inside the
149
- Ipython shell. You can then do `{Fore.GREEN}return {Fore.RESET}function_name(case)`.
150
- """
151
- print(help_text)
152
-
153
-
154
- class CustomInteractiveShell(InteractiveShellEmbed):
155
- def __init__(self, output_type: Union[Type, Tuple[Type], None] = None, func_name: Optional[str] = None,
156
- func_doc: Optional[str] = None, code_to_modify: Optional[str] = None,
157
- attribute_type_hint: Optional[str] = None, prompt_for: Optional[PromptFor] = None, **kwargs):
158
- super().__init__(**kwargs)
159
- keys = ['output_type', 'func_name', 'func_doc', 'code_to_modify', 'attribute_type_hint', 'prompt_for']
160
- values = [output_type, func_name, func_doc, code_to_modify, attribute_type_hint, prompt_for]
161
- magics_kwargs = {key: value for key, value in zip(keys, values) if value is not None}
162
- self.my_magics = MyMagics(self, self.user_ns, **magics_kwargs)
163
- self.register_magics(self.my_magics)
164
- self.all_lines = []
165
-
166
- def run_cell(self, raw_cell: str, **kwargs):
167
- """
168
- Override the run_cell method to capture return statements.
169
- """
170
- if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
171
- if self.my_magics.func_name in raw_cell:
172
- self.all_lines = extract_function_source(self.my_magics.temp_file_path,
173
- self.my_magics.func_name,
174
- join_lines=False)[self.my_magics.func_name]
175
- self.all_lines.append(raw_cell)
176
- self.history_manager.store_inputs(line_num=self.execution_count, source=raw_cell)
177
- self.ask_exit()
178
- return None
179
- result = super().run_cell(raw_cell, **kwargs)
180
- if result.error_in_exec is None and result.error_before_exec is None:
181
- self.all_lines.append(raw_cell)
182
- return result
183
-
184
-
185
- class IPythonShell:
186
- """
187
- Create an embedded Ipython shell that can be used to prompt the user for input.
188
- """
189
-
190
- def __init__(self, scope: Optional[Dict] = None, header: Optional[str] = None,
191
- prompt_for: Optional[PromptFor] = None, case_query: Optional[CaseQuery] = None,
192
- code_to_modify: Optional[str] = None):
193
- """
194
- Initialize the Ipython shell with the given scope and header.
195
-
196
- :param scope: The scope to use for the shell.
197
- :param header: The header to display when the shell is started.
198
- :param prompt_for: The type of information to ask the user about.
199
- :param case_query: The case query which contains the case and the attribute to ask about.
200
- :param code_to_modify: The code to modify. If given, will be used as a start for user to modify.
201
- """
202
- self.scope: Dict = scope or {}
203
- self.header: str = header or ">>> Embedded Ipython Shell"
204
- output_type = None
205
- if prompt_for is not None:
206
- if prompt_for == PromptFor.Conclusion and case_query is not None:
207
- output_type = case_query.attribute_type
208
- elif prompt_for == PromptFor.Conditions:
209
- output_type = bool
210
- self.case_query: Optional[CaseQuery] = case_query
211
- self.output_type: Optional[Type] = output_type
212
- self.prompt_for: Optional[PromptFor] = prompt_for
213
- self.code_to_modify: Optional[str] = code_to_modify
214
- self.user_input: Optional[str] = None
215
- self.func_name: str = ""
216
- self.func_doc: str = ""
217
- self.shell: CustomInteractiveShell = self._init_shell()
218
- self.all_code_lines: List[str] = []
219
-
220
- def _init_shell(self):
221
- """
222
- Initialize the Ipython shell with a custom configuration.
223
- """
224
- cfg = Config()
225
- self.build_func_name_and_doc()
226
- shell = CustomInteractiveShell(config=cfg, user_ns=self.scope, banner1=self.header,
227
- output_type=self.output_type, func_name=self.func_name, func_doc=self.func_doc,
228
- code_to_modify=self.code_to_modify,
229
- attribute_type_hint=self.case_query.attribute_type_hint,
230
- prompt_for=self.prompt_for)
231
- return shell
232
-
233
- def build_func_name_and_doc(self) -> Tuple[str, str]:
234
- """
235
- Build the function name and docstring for the user-defined function.
236
-
237
- :return: A tuple containing the function name and docstring.
238
- """
239
- case = self.scope['case']
240
- case_type = case._obj_type if isinstance(case, Case) else type(case)
241
- self.func_name = self.build_func_name(case_type)
242
- self.func_doc = self.build_func_doc(case_type)
243
-
244
- def build_func_doc(self, case_type: Type) -> Optional[str]:
245
- if self.case_query is None or self.prompt_for is None:
246
- return
247
-
248
- if self.prompt_for == PromptFor.Conditions:
249
- func_doc = (f"Get conditions on whether it's possible to conclude a value"
250
- f" for {case_type.__name__}.{self.case_query.attribute_name}")
251
- elif self.prompt_for == PromptFor.Conclusion:
252
- func_doc = f"Get possible value(s) for {case_type.__name__}.{self.case_query.attribute_name}"
253
- else:
254
- return
255
-
256
- possible_types = [t.__name__ for t in self.case_query.attribute_type if t not in [list, set]]
257
- if list in self.case_query.attribute_type:
258
- func_doc += f" of type list of {' and/or '.join(possible_types)}"
259
- else:
260
- func_doc += f" of type(s) {', '.join(possible_types)}"
261
-
262
- return func_doc
263
-
264
- def build_func_name(self, case_type: Type) -> Optional[str]:
265
- func_name = None
266
- if self.prompt_for is not None:
267
- func_name = f"get_{self.prompt_for.value.lower()}_for"
268
- func_name += f"_{case_type.__name__}"
269
-
270
- if self.case_query is not None:
271
- func_name += f"_{self.case_query.attribute_name}"
272
- output_names = [f"{t.__name__}" for t in self.case_query.attribute_type if t not in [list, set]]
273
- func_name += '_of_type_' + '_'.join(output_names)
274
-
275
- return func_name.lower() if func_name is not None else None
276
-
277
- def run(self):
278
- """
279
- Run the embedded shell.
280
- """
281
- while True:
282
- try:
283
- self.shell()
284
- self.update_user_input_from_code_lines()
285
- break
286
- except Exception as e:
287
- logging.error(e)
288
- print(f"{Fore.RED}ERROR::{e}{Style.RESET_ALL}")
289
-
290
- def update_user_input_from_code_lines(self):
291
- """
292
- Update the user input from the code lines captured in the shell.
293
- """
294
- if len(self.shell.all_lines) == 1 and self.shell.all_lines[0].replace('return', '').strip() == '':
295
- self.user_input = None
296
- else:
297
- self.all_code_lines = extract_dependencies(self.shell.all_lines)
298
- if len(self.all_code_lines) == 1 and self.all_code_lines[0].strip() == '':
299
- self.user_input = None
300
- else:
301
- self.user_input = '\n'.join(self.all_code_lines)
302
- self.user_input = encapsulate_user_input(self.user_input, self.shell.my_magics.function_signature,
303
- self.func_doc)
304
- if f"return {self.func_name}(case)" not in self.user_input:
305
- self.user_input = self.user_input.strip() + f"\nreturn {self.func_name}(case)"
306
-
307
-
308
- def prompt_user_for_expression(case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None)\
309
- -> Tuple[Optional[str], Optional[CallableExpression]]:
310
- """
311
- Prompt the user for an executable python expression to the given case query.
312
-
313
- :param case_query: The case query to prompt the user for.
314
- :param prompt_for: The type of information ask user about.
315
- :param prompt_str: The prompt string to display to the user.
316
- :return: A callable expression that takes a case and executes user expression on it.
317
- """
318
- prev_user_input: Optional[str] = None
319
- callable_expression: Optional[CallableExpression] = None
320
- while True:
321
- user_input, expression_tree = prompt_user_about_case(case_query, prompt_for, prompt_str,
322
- code_to_modify=prev_user_input)
323
- prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
324
- if user_input is None:
325
- if prompt_for == PromptFor.Conclusion:
326
- print(f"{Fore.YELLOW}No conclusion provided. Exiting.{Style.RESET_ALL}")
327
- return None, None
328
- else:
329
- print(f"{Fore.RED}Conditions must be provided. Please try again.{Style.RESET_ALL}")
330
- continue
331
- conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
332
- callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
333
- scope=case_query.scope)
334
- try:
335
- result = callable_expression(case_query.case)
336
- if len(make_list(result)) == 0:
337
- print(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
338
- f" Please modify!{Style.RESET_ALL}")
339
- continue
340
- break
341
- except Exception as e:
342
- logging.error(e)
343
- print(f"{Fore.RED}{e}{Style.RESET_ALL}")
344
- return user_input, callable_expression
345
-
346
-
347
- def prompt_user_about_case(case_query: CaseQuery, prompt_for: PromptFor,
348
- prompt_str: Optional[str] = None,
349
- code_to_modify: Optional[str] = None) -> Tuple[Optional[str], Optional[AST]]:
350
- """
351
- Prompt the user for input.
352
-
353
- :param case_query: The case query to prompt the user for.
354
- :param prompt_for: The type of information the user should provide for the given case.
355
- :param prompt_str: The prompt string to display to the user.
356
- :param code_to_modify: The code to modify. If given will be used as a start for user to modify.
357
- :return: The user input, and the executable expression that was parsed from the user input.
358
- """
359
- if prompt_str is None:
360
- if prompt_for == PromptFor.Conclusion:
361
- prompt_str = f"Give possible value(s) for:\n"
362
- else:
363
- prompt_str = f"Give conditions on when can the rule be evaluated for:\n"
364
- prompt_str += (f"{Fore.CYAN}{case_query.name}{Fore.MAGENTA} of type(s) "
365
- f"{Fore.CYAN}({', '.join(map(lambda x: x.__name__, case_query.core_attribute_type))}){Fore.MAGENTA}")
366
- if prompt_for == PromptFor.Conditions:
367
- prompt_str += (f"\ne.g. `{Fore.GREEN}return {Fore.BLUE}len{Fore.RESET}(case.attribute) > {Fore.BLUE}0` "
368
- f"{Fore.MAGENTA}\nOR `{Fore.GREEN}return {Fore.YELLOW}True`{Fore.MAGENTA} (If you want the"
369
- f" rule to be always evaluated) \n"
370
- f"You can also do {Fore.YELLOW}%edit{Fore.MAGENTA} for more complex conditions.")
371
- prompt_str = f"{Fore.MAGENTA}{prompt_str}{Fore.YELLOW}\n(Write %help for guide){Fore.RESET}"
372
- scope = {'case': case_query.case, **case_query.scope}
373
- shell = IPythonShell(scope=scope, header=prompt_str, prompt_for=prompt_for, case_query=case_query,
374
- code_to_modify=code_to_modify)
375
- return prompt_user_input_and_parse_to_expression(shell=shell)
376
-
377
-
378
- def prompt_user_input_and_parse_to_expression(shell: Optional[IPythonShell] = None,
379
- user_input: Optional[str] = None)\
380
- -> Tuple[Optional[str], Optional[ast.AST]]:
381
- """
382
- Prompt the user for input.
383
-
384
- :param shell: The Ipython shell to use for prompting the user.
385
- :param user_input: The user input to use. If given, the user input will be used instead of prompting the user.
386
- :return: The user input and the AST tree.
387
- """
388
- while True:
389
- if user_input is None:
390
- shell = IPythonShell() if shell is None else shell
391
- shell.run()
392
- user_input = shell.user_input
393
- if user_input is None:
394
- return None, None
395
- print(f"{Fore.BLUE}Captured User input: {Style.RESET_ALL}")
396
- highlighted_code = highlight(user_input, PythonLexer(), TerminalFormatter())
397
- print(highlighted_code)
398
- try:
399
- return user_input, parse_string_to_expression(user_input)
400
- except Exception as e:
401
- msg = f"Error parsing expression: {e}"
402
- logging.error(msg)
403
- print(f"{Fore.RED}{msg}{Style.RESET_ALL}")
404
- user_input = None
@@ -1,20 +0,0 @@
1
- ripple_down_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ripple_down_rules/datasets.py,sha256=mjJh1GLD_5qMgHaukdDWSGphXS9k_BPEF001ZXPchr8,4687
3
- ripple_down_rules/experts.py,sha256=JGVvSNiWhm4FpRpg76f98tl8Ii_C7x_aWD9FxD-JDLQ,6130
4
- ripple_down_rules/failures.py,sha256=E6ajDUsw3Blom8eVLbA7d_Qnov2conhtZ0UmpQ9ZtSE,302
5
- ripple_down_rules/helpers.py,sha256=TvTJU0BA3dPcAyzvZFvAu7jZqsp8Lu0HAAwvuizlGjg,2018
6
- ripple_down_rules/prompt.py,sha256=8RJB8pRaK-MKdDeIbUwylrjaCOk3cIFraTxgtGW8pxU,19070
7
- ripple_down_rules/rdr.py,sha256=vxNZckp6sLAUD92JQgfCzhBhg9CXfMZ_7W4VgrIUFjU,43366
8
- ripple_down_rules/rdr_decorators.py,sha256=8SclpceI3EtrsbuukWJu8HGLh7Q1ZCgYGLX-RPlG-w0,2018
9
- ripple_down_rules/rules.py,sha256=QQy7NBG6mKiowxVG_LjQJBxLTDW2hMyx5zAgwUxdCMM,17183
10
- ripple_down_rules/utils.py,sha256=EdVdIf93TAqbxRTzbf_1422FjenRSI4MI_Ecp3e10z8,44007
11
- ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
12
- ripple_down_rules/datastructures/callable_expression.py,sha256=PDOTdfrLOMg0XPIEiKXvwEvJpkXfcKm8cOGtfX-HHRw,11144
13
- ripple_down_rules/datastructures/case.py,sha256=nJDKOjyhGLx4gt0sHxJNxBLdy9X2SLcDn89_SmKzwoc,14035
14
- ripple_down_rules/datastructures/dataclasses.py,sha256=CLOTr0MzMdrFHQcz4ny_Q43b-BPNWMGY_s6e4HrQ8As,6990
15
- ripple_down_rules/datastructures/enums.py,sha256=hlE6LAa1jUafnG_6UazvaPDfhC1ClI7hKvD89zOyAO8,4661
16
- ripple_down_rules-0.2.4.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
17
- ripple_down_rules-0.2.4.dist-info/METADATA,sha256=7WAVU6ymoPo5fcQTRZc7ex5BngsI0NqWOBEPR_-xVro,42576
18
- ripple_down_rules-0.2.4.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
19
- ripple_down_rules-0.2.4.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
20
- ripple_down_rules-0.2.4.dist-info/RECORD,,