ripple-down-rules 0.2.2__py3-none-any.whl → 0.2.4__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.
@@ -134,6 +134,8 @@ class CallableExpression(SubclassJSONSerializer):
134
134
  if output is None:
135
135
  output = scope['_get_value'](case)
136
136
  if self.conclusion_type is not None:
137
+ if not any([issubclass(ct, (list, set)) for ct in self.conclusion_type]) and is_iterable(output):
138
+ raise ValueError(f"Expected output to be {self.conclusion_type}, but got {type(output)}")
137
139
  output_types = {type(o) for o in make_list(output)}
138
140
  output_types.add(type(output))
139
141
  if not are_results_subclass_of_types(output_types, self.conclusion_type):
@@ -3,8 +3,9 @@ from __future__ import annotations
3
3
  import inspect
4
4
  from dataclasses import dataclass, field
5
5
 
6
+ import typing_extensions
6
7
  from sqlalchemy.orm import DeclarativeBase as SQLTable
7
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union
8
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List
8
9
 
9
10
  from .callable_expression import CallableExpression
10
11
  from .case import create_case, Case
@@ -61,6 +62,13 @@ class CaseQuery:
61
62
  The conditions that must be satisfied for the target value to be valid.
62
63
  """
63
64
 
65
+ @property
66
+ def case_type(self) -> Type:
67
+ """
68
+ :return: The type of the case that the attribute belongs to.
69
+ """
70
+ return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
71
+
64
72
  @property
65
73
  def case(self) -> Any:
66
74
  """
@@ -83,19 +91,33 @@ class CaseQuery:
83
91
  raise ValueError("The case must be a Case or SQLTable object.")
84
92
  self._case = value
85
93
 
94
+ @property
95
+ def attribute_type_hint(self) -> str:
96
+ """
97
+ :return: The type hint of the attribute as a typing object.
98
+ """
99
+ if len(self.core_attribute_type) > 1:
100
+ attribute_types_str = f"Union[{', '.join([t.__name__ for t in self.core_attribute_type])}]"
101
+ else:
102
+ attribute_types_str = self.core_attribute_type[0].__name__
103
+ if list in self.attribute_type:
104
+ return f"List[{attribute_types_str}]"
105
+ else:
106
+ return attribute_types_str
107
+
86
108
  @property
87
109
  def core_attribute_type(self) -> Tuple[Type]:
88
110
  """
89
111
  :return: The core type of the attribute.
90
112
  """
91
- return (t for t in self.attribute_type if t not in (set, list))
113
+ return tuple(t for t in self.attribute_type if t not in (set, list))
92
114
 
93
115
  @property
94
116
  def attribute_type(self) -> Tuple[Type]:
95
117
  """
96
118
  :return: The type of the attribute.
97
119
  """
98
- if not self.mutually_exclusive and (set not in make_list(self._attribute_types)):
120
+ if not self.mutually_exclusive and (list not in make_list(self._attribute_types)):
99
121
  self._attribute_types = tuple(set(make_list(self._attribute_types) + [set, list]))
100
122
  elif not isinstance(self._attribute_types, tuple):
101
123
  self._attribute_types = tuple(make_list(self._attribute_types))
@@ -9,6 +9,9 @@ from textwrap import indent, dedent
9
9
 
10
10
  from IPython.core.magic import register_line_magic, line_magic, Magics, magics_class
11
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
12
15
  from traitlets.config import Config
13
16
  from typing_extensions import List, Optional, Tuple, Dict, Type, Union, Any
14
17
 
@@ -19,33 +22,40 @@ from .datastructures.dataclasses import CaseQuery
19
22
  from .utils import extract_dependencies, contains_return_statement, make_set, get_imports_from_scope, make_list, \
20
23
  get_import_from_type, get_imports_from_types, is_iterable, extract_function_source, encapsulate_user_input, \
21
24
  are_results_subclass_of_types
25
+ from colorama import Fore, Style, init
22
26
 
23
27
 
24
28
  @magics_class
25
29
  class MyMagics(Magics):
26
30
  def __init__(self, shell, scope, output_type: Optional[Type] = None, func_name: str = "user_case",
27
31
  func_doc: str = "User defined function to be executed on the case.",
28
- code_to_modify: Optional[str] = None):
32
+ code_to_modify: Optional[str] = None,
33
+ attribute_type_hint: Optional[str] = None,
34
+ prompt_for: Optional[PromptFor] = None):
29
35
  super().__init__(shell)
30
36
  self.scope = scope
31
37
  self.temp_file_path = None
32
38
  self.func_name = func_name
33
39
  self.func_doc = func_doc
34
40
  self.code_to_modify = code_to_modify
41
+ self.attribute_type_hint = attribute_type_hint
42
+ self.prompt_for = prompt_for
35
43
  self.output_type = make_list(output_type) if output_type is not None else None
36
44
  self.user_edit_line = 0
37
45
  self.function_signature: Optional[str] = None
38
46
  self.build_function_signature()
39
47
 
40
48
  @line_magic
41
- def edit_case(self, line):
49
+ def edit(self, line):
42
50
 
43
51
  boilerplate_code = self.build_boilerplate_code()
44
52
 
45
53
  self.write_to_file(boilerplate_code)
46
54
 
47
55
  print(f"Opening {self.temp_file_path} in PyCharm...")
48
- subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path])
56
+ subprocess.Popen(["pycharm", "--line", str(self.user_edit_line), self.temp_file_path],
57
+ stdout=subprocess.DEVNULL,
58
+ stderr=subprocess.DEVNULL)
49
59
 
50
60
  def build_boilerplate_code(self):
51
61
  imports = self.get_imports()
@@ -59,12 +69,11 @@ class MyMagics(Magics):
59
69
  return boilerplate
60
70
 
61
71
  def build_function_signature(self):
62
- if self.output_type is None:
63
- output_type_hint = ""
64
- elif len(self.output_type) == 1:
65
- output_type_hint = f" -> {self.output_type[0].__name__}"
66
- else:
67
- output_type_hint = f" -> Union[{', '.join([t.__name__ for t in self.output_type])}]"
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}"
68
77
  self.function_signature = f"def {self.func_name}(case: {self.case_type.__name__}){output_type_hint}:"
69
78
 
70
79
  def write_to_file(self, code: str):
@@ -83,7 +92,8 @@ class MyMagics(Magics):
83
92
  output_type_imports = get_imports_from_types(self.output_type)
84
93
  if len(self.output_type) > 1:
85
94
  output_type_imports.append("from typing_extensions import Union")
86
- print(output_type_imports)
95
+ if list in self.output_type:
96
+ output_type_imports.append("from typing_extensions import List")
87
97
  imports = get_imports_from_scope(self.scope)
88
98
  imports = [i for i in imports if ("get_ipython" not in i)]
89
99
  if case_type_import not in imports:
@@ -103,9 +113,9 @@ class MyMagics(Magics):
103
113
  return case._obj_type if isinstance(case, Case) else type(case)
104
114
 
105
115
  @line_magic
106
- def load_case(self, line):
116
+ def load(self, line):
107
117
  if not self.temp_file_path:
108
- print("No file to load. Run %edit_case first.")
118
+ print(f"{Fore.RED}ERROR:: No file to load. Run %edit first.{Style.RESET_ALL}")
109
119
  return
110
120
 
111
121
  with open(self.temp_file_path, 'r') as f:
@@ -118,18 +128,36 @@ class MyMagics(Magics):
118
128
  exec(source, self.scope, exec_globals)
119
129
  user_function = exec_globals[self.func_name]
120
130
  self.shell.user_ns[self.func_name] = user_function
121
- print(f"Loaded `{self.func_name}` function into user namespace.")
131
+ print(f"{Fore.BLUE}Loaded `{self.func_name}` function into user namespace.{Style.RESET_ALL}")
122
132
  return
123
133
 
124
- print(f"Function `{self.func_name}` not found.")
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)
125
152
 
126
153
 
127
154
  class CustomInteractiveShell(InteractiveShellEmbed):
128
155
  def __init__(self, output_type: Union[Type, Tuple[Type], None] = None, func_name: Optional[str] = None,
129
- func_doc: Optional[str] = None, code_to_modify: Optional[str] = None, **kwargs):
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):
130
158
  super().__init__(**kwargs)
131
- keys = ['output_type', 'func_name', 'func_doc', 'code_to_modify']
132
- values = [output_type, func_name, func_doc, code_to_modify]
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]
133
161
  magics_kwargs = {key: value for key, value in zip(keys, values) if value is not None}
134
162
  self.my_magics = MyMagics(self, self.user_ns, **magics_kwargs)
135
163
  self.register_magics(self.my_magics)
@@ -145,7 +173,6 @@ class CustomInteractiveShell(InteractiveShellEmbed):
145
173
  self.my_magics.func_name,
146
174
  join_lines=False)[self.my_magics.func_name]
147
175
  self.all_lines.append(raw_cell)
148
- print("Exiting shell on `return` statement.")
149
176
  self.history_manager.store_inputs(line_num=self.execution_count, source=raw_cell)
150
177
  self.ask_exit()
151
178
  return None
@@ -161,26 +188,28 @@ class IPythonShell:
161
188
  """
162
189
 
163
190
  def __init__(self, scope: Optional[Dict] = None, header: Optional[str] = None,
164
- output_type: Optional[Type] = None, prompt_for: Optional[PromptFor] = None,
165
- attribute_name: Optional[str] = None, attribute_type: Optional[Type] = None,
191
+ prompt_for: Optional[PromptFor] = None, case_query: Optional[CaseQuery] = None,
166
192
  code_to_modify: Optional[str] = None):
167
193
  """
168
194
  Initialize the Ipython shell with the given scope and header.
169
195
 
170
196
  :param scope: The scope to use for the shell.
171
197
  :param header: The header to display when the shell is started.
172
- :param output_type: The type of the output from user input.
173
198
  :param prompt_for: The type of information to ask the user about.
174
- :param attribute_name: The name of the attribute of the case.
175
- :param attribute_type: The type of the attribute of the case.
199
+ :param case_query: The case query which contains the case and the attribute to ask about.
176
200
  :param code_to_modify: The code to modify. If given, will be used as a start for user to modify.
177
201
  """
178
202
  self.scope: Dict = scope or {}
179
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
180
211
  self.output_type: Optional[Type] = output_type
181
212
  self.prompt_for: Optional[PromptFor] = prompt_for
182
- self.attribute_name: Optional[str] = attribute_name
183
- self.attribute_type: Optional[Type] = attribute_type
184
213
  self.code_to_modify: Optional[str] = code_to_modify
185
214
  self.user_input: Optional[str] = None
186
215
  self.func_name: str = ""
@@ -196,7 +225,9 @@ class IPythonShell:
196
225
  self.build_func_name_and_doc()
197
226
  shell = CustomInteractiveShell(config=cfg, user_ns=self.scope, banner1=self.header,
198
227
  output_type=self.output_type, func_name=self.func_name, func_doc=self.func_doc,
199
- code_to_modify=self.code_to_modify)
228
+ code_to_modify=self.code_to_modify,
229
+ attribute_type_hint=self.case_query.attribute_type_hint,
230
+ prompt_for=self.prompt_for)
200
231
  return shell
201
232
 
202
233
  def build_func_name_and_doc(self) -> Tuple[str, str]:
@@ -210,31 +241,38 @@ class IPythonShell:
210
241
  self.func_name = self.build_func_name(case_type)
211
242
  self.func_doc = self.build_func_doc(case_type)
212
243
 
213
- def build_func_doc(self, case_type: Type):
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
+
214
248
  if self.prompt_for == PromptFor.Conditions:
215
249
  func_doc = (f"Get conditions on whether it's possible to conclude a value"
216
- f" for {case_type.__name__}.{self.attribute_name}")
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}"
217
253
  else:
218
- func_doc = f"Get possible value(s) for {case_type.__name__}.{self.attribute_name}"
219
- if is_iterable(self.attribute_type):
220
- possible_types = [t.__name__ for t in self.attribute_type if t not in [list, set]]
221
- func_doc += f" of types list/set of {' and/or '.join(possible_types)}"
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)}"
222
259
  else:
223
- func_doc += f" of type {self.attribute_type.__name__}"
260
+ func_doc += f" of type(s) {', '.join(possible_types)}"
261
+
224
262
  return func_doc
225
263
 
226
- def build_func_name(self, case_type: Type):
227
- func_name = f"get_{self.prompt_for.value.lower()}_for"
228
- func_name += f"_{case_type.__name__}"
229
- if self.attribute_name is not None:
230
- func_name += f"_{self.attribute_name}"
231
- if is_iterable(self.attribute_type):
232
- output_names = [f"{t.__name__}" for t in self.attribute_type if t not in [list, set]]
233
- else:
234
- output_names = [self.attribute_type.__name__] if self.attribute_type is not None else None
235
- if output_names is not None:
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]]
236
273
  func_name += '_of_type_' + '_'.join(output_names)
237
- return func_name.lower()
274
+
275
+ return func_name.lower() if func_name is not None else None
238
276
 
239
277
  def run(self):
240
278
  """
@@ -247,7 +285,7 @@ class IPythonShell:
247
285
  break
248
286
  except Exception as e:
249
287
  logging.error(e)
250
- print(e)
288
+ print(f"{Fore.RED}ERROR::{e}{Style.RESET_ALL}")
251
289
 
252
290
  def update_user_input_from_code_lines(self):
253
291
  """
@@ -285,10 +323,10 @@ def prompt_user_for_expression(case_query: CaseQuery, prompt_for: PromptFor, pro
285
323
  prev_user_input = '\n'.join(user_input.split('\n')[2:-1])
286
324
  if user_input is None:
287
325
  if prompt_for == PromptFor.Conclusion:
288
- print("No conclusion provided. Exiting.")
326
+ print(f"{Fore.YELLOW}No conclusion provided. Exiting.{Style.RESET_ALL}")
289
327
  return None, None
290
328
  else:
291
- print("Conditions must be provided. Please try again.")
329
+ print(f"{Fore.RED}Conditions must be provided. Please try again.{Style.RESET_ALL}")
292
330
  continue
293
331
  conclusion_type = bool if prompt_for == PromptFor.Conditions else case_query.attribute_type
294
332
  callable_expression = CallableExpression(user_input, conclusion_type, expression_tree=expression_tree,
@@ -296,12 +334,13 @@ def prompt_user_for_expression(case_query: CaseQuery, prompt_for: PromptFor, pro
296
334
  try:
297
335
  result = callable_expression(case_query.case)
298
336
  if len(make_list(result)) == 0:
299
- print(f"The given expression gave an empty result for case {case_query.name}. Please modify!")
337
+ print(f"{Fore.YELLOW}The given expression gave an empty result for case {case_query.name}."
338
+ f" Please modify!{Style.RESET_ALL}")
300
339
  continue
301
340
  break
302
341
  except Exception as e:
303
342
  logging.error(e)
304
- print(e)
343
+ print(f"{Fore.RED}{e}{Style.RESET_ALL}")
305
344
  return user_input, callable_expression
306
345
 
307
346
 
@@ -318,11 +357,20 @@ def prompt_user_about_case(case_query: CaseQuery, prompt_for: PromptFor,
318
357
  :return: The user input, and the executable expression that was parsed from the user input.
319
358
  """
320
359
  if prompt_str is None:
321
- prompt_str = f"Give {prompt_for} for {case_query.name}"
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}"
322
372
  scope = {'case': case_query.case, **case_query.scope}
323
- output_type = case_query.attribute_type if prompt_for == PromptFor.Conclusion else bool
324
- shell = IPythonShell(scope=scope, header=prompt_str, output_type=output_type, prompt_for=prompt_for,
325
- attribute_name=case_query.attribute_name, attribute_type=case_query.attribute_type,
373
+ shell = IPythonShell(scope=scope, header=prompt_str, prompt_for=prompt_for, case_query=case_query,
326
374
  code_to_modify=code_to_modify)
327
375
  return prompt_user_input_and_parse_to_expression(shell=shell)
328
376
 
@@ -344,11 +392,13 @@ def prompt_user_input_and_parse_to_expression(shell: Optional[IPythonShell] = No
344
392
  user_input = shell.user_input
345
393
  if user_input is None:
346
394
  return None, None
347
- print(user_input)
395
+ print(f"{Fore.BLUE}Captured User input: {Style.RESET_ALL}")
396
+ highlighted_code = highlight(user_input, PythonLexer(), TerminalFormatter())
397
+ print(highlighted_code)
348
398
  try:
349
399
  return user_input, parse_string_to_expression(user_input)
350
400
  except Exception as e:
351
401
  msg = f"Error parsing expression: {e}"
352
402
  logging.error(msg)
353
- print(msg)
403
+ print(f"{Fore.RED}{msg}{Style.RESET_ALL}")
354
404
  user_input = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.2.2
3
+ Version: 0.2.4
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
@@ -680,7 +680,7 @@ License: GNU GENERAL PUBLIC LICENSE
680
680
  Project-URL: Homepage, https://github.com/AbdelrhmanBassiouny/ripple_down_rules
681
681
  Keywords: robotics,knowledge,reasoning,representation
682
682
  Classifier: Programming Language :: Python :: 3
683
- Requires-Python: >=3.8
683
+ Requires-Python: >=3.10
684
684
  Description-Content-Type: text/markdown
685
685
  License-File: LICENSE
686
686
  Dynamic: license-file
@@ -3,18 +3,18 @@ ripple_down_rules/datasets.py,sha256=mjJh1GLD_5qMgHaukdDWSGphXS9k_BPEF001ZXPchr8
3
3
  ripple_down_rules/experts.py,sha256=JGVvSNiWhm4FpRpg76f98tl8Ii_C7x_aWD9FxD-JDLQ,6130
4
4
  ripple_down_rules/failures.py,sha256=E6ajDUsw3Blom8eVLbA7d_Qnov2conhtZ0UmpQ9ZtSE,302
5
5
  ripple_down_rules/helpers.py,sha256=TvTJU0BA3dPcAyzvZFvAu7jZqsp8Lu0HAAwvuizlGjg,2018
6
- ripple_down_rules/prompt.py,sha256=ReXnZ6OraFPqK5kDfAqH8d4SRPYiQ5d4ESk0js_MM9c,16150
6
+ ripple_down_rules/prompt.py,sha256=8RJB8pRaK-MKdDeIbUwylrjaCOk3cIFraTxgtGW8pxU,19070
7
7
  ripple_down_rules/rdr.py,sha256=vxNZckp6sLAUD92JQgfCzhBhg9CXfMZ_7W4VgrIUFjU,43366
8
8
  ripple_down_rules/rdr_decorators.py,sha256=8SclpceI3EtrsbuukWJu8HGLh7Q1ZCgYGLX-RPlG-w0,2018
9
9
  ripple_down_rules/rules.py,sha256=QQy7NBG6mKiowxVG_LjQJBxLTDW2hMyx5zAgwUxdCMM,17183
10
10
  ripple_down_rules/utils.py,sha256=EdVdIf93TAqbxRTzbf_1422FjenRSI4MI_Ecp3e10z8,44007
11
11
  ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
12
- ripple_down_rules/datastructures/callable_expression.py,sha256=qVo8baEI_seeg6V23wmsctYdWj_tJJEOTkHeUc04Wvw,10912
12
+ ripple_down_rules/datastructures/callable_expression.py,sha256=PDOTdfrLOMg0XPIEiKXvwEvJpkXfcKm8cOGtfX-HHRw,11144
13
13
  ripple_down_rules/datastructures/case.py,sha256=nJDKOjyhGLx4gt0sHxJNxBLdy9X2SLcDn89_SmKzwoc,14035
14
- ripple_down_rules/datastructures/dataclasses.py,sha256=BUr0T0CCh98sdsW2CVAXGk2oWqfemM7w1t91QKvg_KU,6171
14
+ ripple_down_rules/datastructures/dataclasses.py,sha256=CLOTr0MzMdrFHQcz4ny_Q43b-BPNWMGY_s6e4HrQ8As,6990
15
15
  ripple_down_rules/datastructures/enums.py,sha256=hlE6LAa1jUafnG_6UazvaPDfhC1ClI7hKvD89zOyAO8,4661
16
- ripple_down_rules-0.2.2.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
17
- ripple_down_rules-0.2.2.dist-info/METADATA,sha256=cwyxHt0ta-asbPBoOblSxWbw7-ASen5e4ZJ--SAmGM0,42575
18
- ripple_down_rules-0.2.2.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
19
- ripple_down_rules-0.2.2.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
20
- ripple_down_rules-0.2.2.dist-info/RECORD,,
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,,