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.
@@ -1,510 +0,0 @@
1
- import ast
2
- import logging
3
- import os
4
- import shutil
5
- import socket
6
- import subprocess
7
- import tempfile
8
- from _ast import AST
9
- from functools import cached_property
10
- from textwrap import indent, dedent
11
-
12
- from IPython.core.magic import line_magic, Magics, magics_class
13
- from IPython.terminal.embed import InteractiveShellEmbed
14
- from colorama import Fore, Style
15
- from pygments import highlight
16
- from pygments.formatters.terminal import TerminalFormatter
17
- from pygments.lexers.python import PythonLexer
18
- from traitlets.config import Config
19
- from typing_extensions import List, Optional, Tuple, Dict, Type
20
-
21
- from .datastructures.callable_expression import CallableExpression, parse_string_to_expression
22
- from .datastructures.case import Case
23
- from .datastructures.dataclasses import CaseQuery
24
- from .datastructures.enums import PromptFor, Editor
25
- from .utils import extract_dependencies, contains_return_statement, get_imports_from_scope, make_list, \
26
- get_imports_from_types, extract_function_source, encapsulate_user_input, str_to_snake_case, typing_hint_to_str
27
-
28
-
29
- def detect_available_editor() -> Optional[Editor]:
30
- """
31
- Detect the available editor on the system.
32
-
33
- :return: The first found editor that is available on the system.
34
- """
35
- editor_env = os.environ.get("RDR_EDITOR")
36
- if editor_env:
37
- return Editor.from_str(editor_env)
38
- for editor in [Editor.Pycharm, Editor.Code, Editor.CodeServer]:
39
- if shutil.which(editor.value):
40
- return editor
41
- return None
42
-
43
-
44
- def is_port_in_use(port: int = 8080) -> bool:
45
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
46
- return s.connect_ex(("localhost", port)) == 0
47
-
48
-
49
- def start_code_server(workspace):
50
- """
51
- Start the code-server in the given workspace.
52
- """
53
- filename = os.path.join(os.path.dirname(__file__), "start-code-server.sh")
54
- os.system(f"chmod +x {filename}")
55
- print(f"Starting code-server at {filename}")
56
- return subprocess.Popen(["/bin/bash", filename, workspace], stdout=subprocess.PIPE,
57
- stderr=subprocess.PIPE, text=True)
58
-
59
-
60
- @magics_class
61
- class MyMagics(Magics):
62
- temp_file_path: Optional[str] = None
63
- """
64
- The path to the temporary file that is created for the user to edit.
65
- """
66
- port: int = int(os.environ.get("RDR_EDITOR_PORT", 8080))
67
- """
68
- The port to use for the code-server.
69
- """
70
- process: Optional[subprocess.Popen] = None
71
- """
72
- The process of the code-server.
73
- """
74
-
75
- def __init__(self, shell, scope,
76
- code_to_modify: Optional[str] = None,
77
- prompt_for: Optional[PromptFor] = None,
78
- case_query: Optional[CaseQuery] = None):
79
- super().__init__(shell)
80
- self.scope = scope
81
- self.code_to_modify = code_to_modify
82
- self.prompt_for = prompt_for
83
- self.case_query = case_query
84
- self.output_type = self.get_output_type()
85
- self.user_edit_line = 0
86
- self.func_name: str = self.get_func_name()
87
- self.func_doc: str = self.get_func_doc()
88
- self.function_signature: str = self.get_function_signature()
89
- self.editor: Optional[Editor] = detect_available_editor()
90
- self.workspace: str = os.environ.get("RDR_EDITOR_WORKSPACE", os.path.dirname(self.scope['__file__']))
91
- self.temp_file_path: str = os.path.join(self.workspace, "edit_code_here.py")
92
-
93
- def get_output_type(self) -> List[Type]:
94
- """
95
- :return: The output type of the function as a list of types.
96
- """
97
- if self.prompt_for == PromptFor.Conditions:
98
- output_type = bool
99
- else:
100
- output_type = self.case_query.attribute_type
101
- return make_list(output_type) if output_type is not None else None
102
-
103
- @line_magic
104
- def edit(self, line):
105
- if self.editor is None:
106
- print(f"{Fore.RED}ERROR:: No editor found. Please install PyCharm, VSCode or code-server.{Style.RESET_ALL}")
107
- return
108
-
109
- boilerplate_code = self.build_boilerplate_code()
110
- self.write_to_file(boilerplate_code)
111
-
112
- self.open_file_in_editor()
113
-
114
- def open_file_in_editor(self):
115
- """
116
- Open the file in the available editor.
117
- """
118
- if self.editor == Editor.Pycharm:
119
- subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path],
120
- stdout=subprocess.DEVNULL,
121
- stderr=subprocess.DEVNULL)
122
- elif self.editor == Editor.Code:
123
- subprocess.Popen(["code", self.temp_file_path])
124
- elif self.editor == Editor.CodeServer:
125
- try:
126
- subprocess.check_output(["pgrep", "-f", "code-server"])
127
- # check if same port is in use
128
- if is_port_in_use(self.port):
129
- print(f"Code-server is already running on port {self.port}.")
130
- else:
131
- raise ValueError("Port is not in use.")
132
- except (subprocess.CalledProcessError, ValueError) as e:
133
- self.process = start_code_server(self.workspace)
134
- print(f"Open code-server in your browser at http://localhost:{self.port}?folder={self.workspace}")
135
- print(f"Edit the file: {Fore.BLUE}{self.temp_file_path}")
136
-
137
- def build_boilerplate_code(self):
138
- imports = self.get_imports()
139
- if self.function_signature is None:
140
- self.function_signature = self.get_function_signature()
141
- if self.func_doc is None:
142
- self.func_doc = self.get_func_doc()
143
- if self.code_to_modify is not None:
144
- body = indent(dedent(self.code_to_modify), ' ')
145
- else:
146
- body = " # Write your code here\n pass"
147
- boilerplate = f"""{imports}\n\n{self.function_signature}\n \"\"\"{self.func_doc}\"\"\"\n{body}"""
148
- self.user_edit_line = imports.count('\n') + 6
149
- return boilerplate
150
-
151
- def get_function_signature(self) -> str:
152
- if self.func_name is None:
153
- self.func_name = self.get_func_name()
154
- output_type_hint = self.get_output_type_hint()
155
- func_args = self.get_func_args()
156
- return f"def {self.func_name}({func_args}){output_type_hint}:"
157
-
158
- def get_output_type_hint(self) -> str:
159
- """
160
- :return: A string containing the output type hint for the function.
161
- """
162
- output_type_hint = ""
163
- if self.prompt_for == PromptFor.Conditions:
164
- output_type_hint = " -> bool"
165
- elif self.prompt_for == PromptFor.Conclusion:
166
- output_type_hint = f" -> {self.case_query.attribute_type_hint}"
167
- return output_type_hint
168
-
169
- def get_func_args(self) -> str:
170
- """
171
- :return: A string containing the function arguments.
172
- """
173
- if self.case_query.is_function:
174
- func_args = {}
175
- for k, v in self.case_query.case.items():
176
- if (self.case_query.function_args_type_hints is not None
177
- and k in self.case_query.function_args_type_hints):
178
- func_args[k] = typing_hint_to_str(self.case_query.function_args_type_hints[k])[0]
179
- else:
180
- func_args[k] = type(v).__name__ if not isinstance(v, type) else f"Type[{v.__name__}]"
181
- func_args = ', '.join([f"{k}: {v}" if str(v) not in ["NoneType", "None"] else str(k)
182
- for k, v in func_args.items()])
183
- else:
184
- func_args = f"case: {self.case_type.__name__}"
185
- return func_args
186
-
187
- def write_to_file(self, code: str):
188
- if self.temp_file_path is None:
189
- tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".py",
190
- dir=self.workspace)
191
- tmp.write(code)
192
- tmp.flush()
193
- self.temp_file_path = tmp.name
194
- tmp.close()
195
- else:
196
- with open(self.temp_file_path, 'w+') as f:
197
- f.write(code)
198
-
199
- def get_imports(self):
200
- """
201
- :return: A string containing the imports for the function.
202
- """
203
- case_type_imports = []
204
- if self.case_query.is_function:
205
- for k, v in self.case_query.case.items():
206
- if (self.case_query.function_args_type_hints is not None
207
- and k in self.case_query.function_args_type_hints):
208
- hint_list = typing_hint_to_str(self.case_query.function_args_type_hints[k])[1]
209
- for hint in hint_list:
210
- hint_split = hint.split('.')
211
- if len(hint_split) > 1:
212
- case_type_imports.append(f"from {'.'.join(hint_split[:-1])} import {hint_split[-1]}")
213
- else:
214
- if isinstance(v, type):
215
- case_type_imports.append(f"from {v.__module__} import {v.__name__}")
216
- elif hasattr(v, "__module__") and not v.__module__.startswith("__"):
217
- case_type_imports.append(f"\nfrom {type(v).__module__} import {type(v).__name__}")
218
- else:
219
- case_type_imports.append(f"from {self.case_type.__module__} import {self.case_type.__name__}")
220
- if self.output_type is None:
221
- output_type_imports = [f"from typing_extensions import Any"]
222
- else:
223
- output_type_imports = get_imports_from_types(self.output_type)
224
- if len(self.output_type) > 1:
225
- output_type_imports.append("from typing_extensions import Union")
226
- if list in self.output_type:
227
- output_type_imports.append("from typing_extensions import List")
228
- imports = get_imports_from_scope(self.scope)
229
- imports = [i for i in imports if ("get_ipython" not in i)]
230
- imports.extend(case_type_imports)
231
- imports.extend([oti for oti in output_type_imports if oti not in imports])
232
- imports = set(imports)
233
- return '\n'.join(imports)
234
-
235
- def get_func_doc(self) -> Optional[str]:
236
- """
237
- :return: A string containing the function docstring.
238
- """
239
- if self.prompt_for == PromptFor.Conditions:
240
- return (f"Get conditions on whether it's possible to conclude a value"
241
- f" for {self.case_query.name}")
242
- else:
243
- return f"Get possible value(s) for {self.case_query.name}"
244
-
245
- def get_func_name(self) -> Optional[str]:
246
- func_name = ""
247
- if self.prompt_for == PromptFor.Conditions:
248
- func_name = f"{self.prompt_for.value.lower()}_for_"
249
- case_name = self.case_query.name.replace(".", "_")
250
- if self.case_query.is_function:
251
- # convert any CamelCase word into snake_case by adding _ before each capital letter
252
- case_name = case_name.replace(f"_{self.case_query.attribute_name}", "")
253
- func_name += case_name
254
- return str_to_snake_case(func_name)
255
-
256
- @cached_property
257
- def case_type(self) -> Type:
258
- """
259
- Get the type of the case object in the current scope.
260
-
261
- :return: The type of the case object.
262
- """
263
- case = self.scope['case']
264
- return case._obj_type if isinstance(case, Case) else type(case)
265
-
266
- @line_magic
267
- def load(self, line):
268
- if not self.temp_file_path:
269
- print(f"{Fore.RED}ERROR:: No file to load. Run %edit first.{Style.RESET_ALL}")
270
- return
271
-
272
- with open(self.temp_file_path, 'r') as f:
273
- source = f.read()
274
-
275
- tree = ast.parse(source)
276
- for node in tree.body:
277
- if isinstance(node, ast.FunctionDef) and node.name == self.func_name:
278
- exec_globals = {}
279
- exec(source, self.scope, exec_globals)
280
- user_function = exec_globals[self.func_name]
281
- self.shell.user_ns[self.func_name] = user_function
282
- print(f"{Fore.BLUE}Loaded `{self.func_name}` function into user namespace.{Style.RESET_ALL}")
283
- return
284
-
285
- print(f"{Fore.RED}ERROR:: Function `{self.func_name}` not found.{Style.RESET_ALL}")
286
-
287
- @line_magic
288
- def help(self, line):
289
- """
290
- Display help information for the Ipython shell.
291
- """
292
- help_text = f"""
293
- Directly write python code in the shell, and then `{Fore.GREEN}return {Fore.RESET}output`. Or use
294
- the magic commands to write the code in a temporary file and edit it in PyCharm:
295
- {Fore.MAGENTA}Usage: %edit{Style.RESET_ALL}
296
- Opens a temporary file in PyCharm for editing a function (conclusion or conditions for case)
297
- that will be executed on the case object.
298
- {Fore.MAGENTA}Usage: %load{Style.RESET_ALL}
299
- Loads the function defined in the temporary file into the user namespace, that can then be used inside the
300
- Ipython shell. You can then do `{Fore.GREEN}return {Fore.RESET}function_name(case)`.
301
- """
302
- print(help_text)
303
-
304
- def __del__(self):
305
- if hasattr(self, 'process') and self.process is not None and self.process.poll() is None:
306
- self.process.terminate() # Graceful shutdown
307
- self.process.wait() # Ensure cleanup
308
-
309
-
310
- class CustomInteractiveShell(InteractiveShellEmbed):
311
- def __init__(self, code_to_modify: Optional[str] = None,
312
- prompt_for: Optional[PromptFor] = None,
313
- case_query: Optional[CaseQuery] = None,
314
- **kwargs):
315
- super().__init__(**kwargs)
316
- self.my_magics = MyMagics(self, self.user_ns, code_to_modify=code_to_modify,
317
- prompt_for=prompt_for, case_query=case_query)
318
- self.register_magics(self.my_magics)
319
- self.all_lines = []
320
-
321
- def run_cell(self, raw_cell: str, **kwargs):
322
- """
323
- Override the run_cell method to capture return statements.
324
- """
325
- if contains_return_statement(raw_cell) and 'def ' not in raw_cell:
326
- if self.my_magics.func_name in raw_cell:
327
- self.all_lines = extract_function_source(self.my_magics.temp_file_path,
328
- self.my_magics.func_name,
329
- join_lines=False)[self.my_magics.func_name]
330
- self.all_lines.append(raw_cell)
331
- self.history_manager.store_inputs(line_num=self.execution_count, source=raw_cell)
332
- self.ask_exit()
333
- return None
334
- result = super().run_cell(raw_cell, **kwargs)
335
- if result.error_in_exec is None and result.error_before_exec is None:
336
- self.all_lines.append(raw_cell)
337
- return result
338
-
339
-
340
- class IPythonShell:
341
- """
342
- Create an embedded Ipython shell that can be used to prompt the user for input.
343
- """
344
-
345
- def __init__(self, scope: Optional[Dict] = None, header: Optional[str] = None,
346
- prompt_for: Optional[PromptFor] = None, case_query: Optional[CaseQuery] = None,
347
- code_to_modify: Optional[str] = None):
348
- """
349
- Initialize the Ipython shell with the given scope and header.
350
-
351
- :param scope: The scope to use for the shell.
352
- :param header: The header to display when the shell is started.
353
- :param prompt_for: The type of information to ask the user about.
354
- :param case_query: The case query which contains the case and the attribute to ask about.
355
- :param code_to_modify: The code to modify. If given, will be used as a start for user to modify.
356
- """
357
- self.scope: Dict = scope or {}
358
- self.header: str = header or ">>> Embedded Ipython Shell"
359
- self.case_query: Optional[CaseQuery] = case_query
360
- self.prompt_for: Optional[PromptFor] = prompt_for
361
- self.code_to_modify: Optional[str] = code_to_modify
362
- self.user_input: Optional[str] = None
363
- self.shell: CustomInteractiveShell = self._init_shell()
364
- self.all_code_lines: List[str] = []
365
-
366
- def _init_shell(self):
367
- """
368
- Initialize the Ipython shell with a custom configuration.
369
- """
370
- cfg = Config()
371
- shell = CustomInteractiveShell(config=cfg, user_ns=self.scope, banner1=self.header,
372
- code_to_modify=self.code_to_modify,
373
- prompt_for=self.prompt_for,
374
- case_query=self.case_query,
375
- )
376
- return shell
377
-
378
- def run(self):
379
- """
380
- Run the embedded shell.
381
- """
382
- while True:
383
- try:
384
- self.shell()
385
- self.update_user_input_from_code_lines()
386
- break
387
- except Exception as e:
388
- logging.error(e)
389
- print(f"{Fore.RED}ERROR::{e}{Style.RESET_ALL}")
390
-
391
- def update_user_input_from_code_lines(self):
392
- """
393
- Update the user input from the code lines captured in the shell.
394
- """
395
- if len(self.shell.all_lines) == 1 and self.shell.all_lines[0].replace('return', '').strip() == '':
396
- self.user_input = None
397
- else:
398
- self.all_code_lines = extract_dependencies(self.shell.all_lines)
399
- if len(self.all_code_lines) == 1 and self.all_code_lines[0].strip() == '':
400
- self.user_input = None
401
- else:
402
- self.user_input = '\n'.join(self.all_code_lines)
403
- self.user_input = encapsulate_user_input(self.user_input, self.shell.my_magics.function_signature,
404
- self.shell.my_magics.func_doc)
405
- if self.case_query.is_function:
406
- args = "**case"
407
- else:
408
- args = "case"
409
- if f"return {self.shell.my_magics.func_name}({args})" not in self.user_input:
410
- self.user_input = self.user_input.strip() + f"\nreturn {self.shell.my_magics.func_name}({args})"
411
-
412
-
413
- def prompt_user_for_expression(case_query: CaseQuery, prompt_for: PromptFor, prompt_str: Optional[str] = None) \
414
- -> Tuple[Optional[str], Optional[CallableExpression]]:
415
- """
416
- Prompt the user for an executable python expression to the given case query.
417
-
418
- :param case_query: The case query to prompt the user for.
419
- :param prompt_for: The type of information ask user about.
420
- :param prompt_str: The prompt string to display to the user.
421
- :return: A callable expression that takes a case and executes user expression on it.
422
- """
423
- prev_user_input: Optional[str] = None
424
- callable_expression: Optional[CallableExpression] = None
425
- while True:
426
- user_input, expression_tree = prompt_user_about_case(case_query, prompt_for, prompt_str,
427
- code_to_modify=prev_user_input)
428
- if user_input is None:
429
- if prompt_for == PromptFor.Conclusion:
430
- print(f"{Fore.YELLOW}No conclusion provided. Exiting.{Style.RESET_ALL}")
431
- return None, None
432
- else:
433
- print(f"{Fore.RED}Conditions must be provided. Please try again.{Style.RESET_ALL}")
434
- continue
435
- prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
436
- conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
437
- callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
438
- scope=case_query.scope,
439
- mutually_exclusive=case_query.mutually_exclusive)
440
- try:
441
- result = callable_expression(case_query.case)
442
- if len(make_list(result)) == 0:
443
- print(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
444
- f" Please modify!{Style.RESET_ALL}")
445
- continue
446
- break
447
- except Exception as e:
448
- logging.error(e)
449
- print(f"{Fore.RED}{e}{Style.RESET_ALL}")
450
- return user_input, callable_expression
451
-
452
-
453
- def prompt_user_about_case(case_query: CaseQuery, prompt_for: PromptFor,
454
- prompt_str: Optional[str] = None,
455
- code_to_modify: Optional[str] = None) -> Tuple[Optional[str], Optional[AST]]:
456
- """
457
- Prompt the user for input.
458
-
459
- :param case_query: The case query to prompt the user for.
460
- :param prompt_for: The type of information the user should provide for the given case.
461
- :param prompt_str: The prompt string to display to the user.
462
- :param code_to_modify: The code to modify. If given will be used as a start for user to modify.
463
- :return: The user input, and the executable expression that was parsed from the user input.
464
- """
465
- if prompt_str is None:
466
- if prompt_for == PromptFor.Conclusion:
467
- prompt_str = f"Give possible value(s) for:\n"
468
- else:
469
- prompt_str = f"Give conditions on when can the rule be evaluated for:\n"
470
- prompt_str += (f"{Fore.CYAN}{case_query.name}{Fore.MAGENTA} of type(s) "
471
- f"{Fore.CYAN}({', '.join(map(lambda x: x.__name__, case_query.core_attribute_type))}){Fore.MAGENTA}")
472
- if prompt_for == PromptFor.Conditions:
473
- prompt_str += (f"\ne.g. `{Fore.GREEN}return {Fore.BLUE}len{Fore.RESET}(case.attribute) > {Fore.BLUE}0` "
474
- f"{Fore.MAGENTA}\nOR `{Fore.GREEN}return {Fore.YELLOW}True`{Fore.MAGENTA} (If you want the"
475
- f" rule to be always evaluated) \n"
476
- f"You can also do {Fore.YELLOW}%edit{Fore.MAGENTA} for more complex conditions.")
477
- prompt_str = f"{Fore.MAGENTA}{prompt_str}{Fore.YELLOW}\n(Write %help for guide){Fore.RESET}"
478
- scope = {'case': case_query.case, **case_query.scope}
479
- shell = IPythonShell(scope=scope, header=prompt_str, prompt_for=prompt_for, case_query=case_query,
480
- code_to_modify=code_to_modify)
481
- return prompt_user_input_and_parse_to_expression(shell=shell)
482
-
483
-
484
- def prompt_user_input_and_parse_to_expression(shell: Optional[IPythonShell] = None,
485
- user_input: Optional[str] = None) \
486
- -> Tuple[Optional[str], Optional[ast.AST]]:
487
- """
488
- Prompt the user for input.
489
-
490
- :param shell: The Ipython shell to use for prompting the user.
491
- :param user_input: The user input to use. If given, the user input will be used instead of prompting the user.
492
- :return: The user input and the AST tree.
493
- """
494
- while True:
495
- if user_input is None:
496
- shell = IPythonShell() if shell is None else shell
497
- shell.run()
498
- user_input = shell.user_input
499
- if user_input is None:
500
- return None, None
501
- print(f"{Fore.BLUE}Captured User input: {Style.RESET_ALL}")
502
- highlighted_code = highlight(user_input, PythonLexer(), TerminalFormatter())
503
- print(highlighted_code)
504
- try:
505
- return user_input, parse_string_to_expression(user_input)
506
- except Exception as e:
507
- msg = f"Error parsing expression: {e}"
508
- logging.error(msg)
509
- print(f"{Fore.RED}{msg}{Style.RESET_ALL}")
510
- 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=zi5LVNA3oYUYL26HVUgOV8lKQ-wSmaJeOKHptdJ8A3E,6667
3
- ripple_down_rules/experts.py,sha256=8wTInaKum38mH5r5esw7tXq_iL15-JP2tf5VIHHBmpw,6229
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=KmlMRZRKqoyYIinp9l09LWL7e41DsqsGNcVbHRjebhg,23122
7
- ripple_down_rules/rdr.py,sha256=7Ir3jYqAf2qZwi0Quz-ZMc3jreiSADo9xfAfLj2gbLM,43491
8
- ripple_down_rules/rdr_decorators.py,sha256=VdmE0JrE8j89b6Af1R1tLZiKfy3h1VCvhAUefN_FLLQ,6753
9
- ripple_down_rules/rules.py,sha256=QQy7NBG6mKiowxVG_LjQJBxLTDW2hMyx5zAgwUxdCMM,17183
10
- ripple_down_rules/utils.py,sha256=SXNkIA9k75yKbfCAWQzq4XVxh3CpcxqSistnVFY3gBQ,48917
11
- ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
12
- ripple_down_rules/datastructures/callable_expression.py,sha256=jA7424_mWPbOoPICW3eLMX0-ypxnsW6gOqxrJ7JpDbE,11610
13
- ripple_down_rules/datastructures/case.py,sha256=oC8OSdhXvHE-Zx1IIQlad-fsKzQQqr6MZBW24c-dbeU,15191
14
- ripple_down_rules/datastructures/dataclasses.py,sha256=GWnUF4h4zfNHSsyBIz3L9y8sLkrXRv0FK_OxzzLc8L8,8183
15
- ripple_down_rules/datastructures/enums.py,sha256=sOEKvsmcFUnztScFOGmrxgrBNeIYHfV_CeQ5_Sj62so,5368
16
- ripple_down_rules-0.3.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
17
- ripple_down_rules-0.3.0.dist-info/METADATA,sha256=f-xZg62otuqgnzlf4Bwzt0ns25WQLWl2RgIARt9v-tk,42576
18
- ripple_down_rules-0.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
19
- ripple_down_rules-0.3.0.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
20
- ripple_down_rules-0.3.0.dist-info/RECORD,,