ripple-down-rules 0.1.21__py3-none-any.whl → 0.1.61__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,14 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from abc import ABC, abstractmethod
4
5
  from enum import Enum
5
6
 
6
7
  from anytree import NodeMixin
7
8
  from typing_extensions import List, Optional, Self, Union, Dict, Any
8
9
 
9
- from .datastructures import CallableExpression, Case, SQLTable
10
+ from .datastructures.callable_expression import CallableExpression
11
+ from .datastructures.case import Case
12
+ from sqlalchemy.orm import DeclarativeBase as SQLTable
10
13
  from .datastructures.enums import RDREdge, Stop
11
- from .utils import SubclassJSONSerializer, is_iterable, get_full_class_name
14
+ from .utils import SubclassJSONSerializer, is_iterable, get_full_class_name, conclusion_to_json, \
15
+ get_rule_conclusion_as_source_code
12
16
 
13
17
 
14
18
  class Rule(NodeMixin, SubclassJSONSerializer, ABC):
@@ -41,6 +45,7 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
41
45
  self.conditions = conditions if conditions else None
42
46
  self.conclusion_name: Optional[str] = conclusion_name
43
47
  self.json_serialization: Optional[Dict[str, Any]] = None
48
+ self._name: Optional[str] = None
44
49
 
45
50
  def _post_detach(self, parent):
46
51
  """
@@ -79,43 +84,48 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
79
84
 
80
85
  :param parent_indent: The indentation of the parent rule.
81
86
  """
82
- if isinstance(self.conclusion, CallableExpression):
83
- conclusion = self.conclusion.parsed_user_input
84
- elif isinstance(self.conclusion, Enum):
85
- conclusion = str(self.conclusion)
86
- else:
87
- conclusion = self.conclusion
88
- return self._conclusion_source_code_clause(conclusion, parent_indent=parent_indent)
87
+ conclusion = self.conclusion
88
+ if isinstance(conclusion, CallableExpression):
89
+ if self.conclusion.user_input is not None:
90
+ conclusion = self.conclusion.user_input
91
+ else:
92
+ conclusion = self.conclusion.conclusion
93
+ if isinstance(conclusion, Enum):
94
+ conclusion = str(conclusion)
95
+ return self._conclusion_source_code(conclusion, parent_indent=parent_indent)
89
96
 
90
97
  @abstractmethod
91
- def _conclusion_source_code_clause(self, conclusion: Any, parent_indent: str = "") -> str:
98
+ def _conclusion_source_code(self, conclusion: Any, parent_indent: str = "") -> str:
92
99
  pass
93
100
 
94
- def write_condition_as_source_code(self, parent_indent: str = "") -> str:
101
+ def write_condition_as_source_code(self, parent_indent: str = "", defs_file: Optional[str] = None) -> str:
95
102
  """
96
103
  Get the source code representation of the conditions of the rule.
97
104
 
98
105
  :param parent_indent: The indentation of the parent rule.
106
+ :param defs_file: The file to write the conditions to if they are a definition.
99
107
  """
100
108
  if_clause = self._if_statement_source_code_clause()
101
- return f"{parent_indent}{if_clause} {self.conditions.parsed_user_input}:\n"
109
+ if '\n' not in self.conditions.user_input:
110
+ return f"{parent_indent}{if_clause} {self.conditions.user_input}:\n"
111
+ elif "def " in self.conditions.user_input:
112
+ if defs_file is None:
113
+ raise ValueError("Cannot write conditions to source code as definitions python file was not given.")
114
+ # This means the conditions are a definition that should be written and then called
115
+ conditions_lines = self.conditions.user_input.split('\n')
116
+ # use regex to replace the function name
117
+ new_function_name = f"def conditions_{id(self)}"
118
+ conditions_lines[0] = re.sub(r"def (\w+)", new_function_name, conditions_lines[0])
119
+ def_code = "\n".join(conditions_lines)
120
+ with open(defs_file, 'a') as f:
121
+ f.write(def_code + "\n")
122
+ return f"\n{parent_indent}{if_clause} {new_function_name.replace('def ', '')}(case):\n"
102
123
 
103
124
  @abstractmethod
104
125
  def _if_statement_source_code_clause(self) -> str:
105
126
  pass
106
127
 
107
128
  def _to_json(self) -> Dict[str, Any]:
108
- def conclusion_to_json(conclusion):
109
- if is_iterable(conclusion):
110
- conclusions = {'_type': get_full_class_name(type(conclusion)), 'value': []}
111
- for c in conclusion:
112
- conclusions['value'].append(conclusion_to_json(c))
113
- elif hasattr(conclusion, 'to_json'):
114
- conclusions = conclusion.to_json()
115
- else:
116
- conclusions = {'_type': get_full_class_name(type(conclusion)), 'value': conclusion}
117
- return conclusions
118
-
119
129
  json_serialization = {"conditions": self.conditions.to_json(),
120
130
  "conclusion": conclusion_to_json(self.conclusion),
121
131
  "parent": self.parent.json_serialization if self.parent else None,
@@ -137,7 +147,14 @@ class Rule(NodeMixin, SubclassJSONSerializer, ABC):
137
147
  """
138
148
  Get the name of the rule, which is the conditions and the conclusion.
139
149
  """
140
- return self.__str__()
150
+ return self._name if self._name is not None else self.__str__()
151
+
152
+ @name.setter
153
+ def name(self, new_name: str):
154
+ """
155
+ Set the name of the rule.
156
+ """
157
+ self._name = new_name
141
158
 
142
159
  def __str__(self, sep="\n"):
143
160
  """
@@ -247,8 +264,13 @@ class SingleClassRule(Rule, HasAlternativeRule, HasRefinementRule):
247
264
  loaded_rule.alternative = SingleClassRule.from_json(data["alternative"])
248
265
  return loaded_rule
249
266
 
250
- def _conclusion_source_code_clause(self, conclusion: Any, parent_indent: str = "") -> str:
251
- return f"{parent_indent}{' ' * 4}return {conclusion}\n"
267
+ def _conclusion_source_code(self, conclusion: Any, parent_indent: str = "") -> str:
268
+ conclusion = str(conclusion)
269
+ indent = parent_indent + " " * 4
270
+ if '\n' not in conclusion:
271
+ return f"{indent}return {conclusion}\n"
272
+ else:
273
+ return get_rule_conclusion_as_source_code(self, conclusion, parent_indent=parent_indent)
252
274
 
253
275
  def _if_statement_source_code_clause(self) -> str:
254
276
  return "elif" if self.weight == RDREdge.Alternative.value else "if"
@@ -293,7 +315,7 @@ class MultiClassStopRule(Rule, HasAlternativeRule):
293
315
  loaded_rule.alternative = MultiClassStopRule.from_json(data["alternative"])
294
316
  return loaded_rule
295
317
 
296
- def _conclusion_source_code_clause(self, conclusion: Any, parent_indent: str = "") -> str:
318
+ def _conclusion_source_code(self, conclusion: Any, parent_indent: str = "") -> str:
297
319
  return f"{parent_indent}{' ' * 4}pass\n"
298
320
 
299
321
  def _if_statement_source_code_clause(self) -> str:
@@ -338,12 +360,22 @@ class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
338
360
  loaded_rule.alternative = MultiClassTopRule.from_json(data["alternative"])
339
361
  return loaded_rule
340
362
 
341
- def _conclusion_source_code_clause(self, conclusion: Any, parent_indent: str = "") -> str:
342
- if is_iterable(conclusion):
343
- conclusion_str = "{" + ", ".join([str(c) for c in conclusion]) + "}"
363
+ def _conclusion_source_code(self, conclusion: Any, parent_indent: str = "") -> str:
364
+ conclusion_str = str(conclusion)
365
+ indent = parent_indent + " " * 4
366
+ statement = ""
367
+ if '\n' not in conclusion_str:
368
+ if is_iterable(conclusion):
369
+ conclusion_str = "{" + ", ".join([str(c) for c in conclusion]) + "}"
370
+ else:
371
+ conclusion_str = "{" + str(conclusion) + "}"
344
372
  else:
345
- conclusion_str = "{" + str(conclusion) + "}"
346
- statement = f"{parent_indent}{' ' * 4}conclusions.update({conclusion_str})\n"
373
+ conclusion_str = get_rule_conclusion_as_source_code(self, conclusion_str, parent_indent=parent_indent)
374
+ lines = conclusion_str.split("\n")
375
+ conclusion_str = lines[-2].replace("return ", "").strip()
376
+ statement += "\n".join(lines[:-2]) + "\n"
377
+
378
+ statement += f"{indent}conclusions.update(make_set({conclusion_str}))\n"
347
379
  if self.alternative is None:
348
380
  statement += f"{parent_indent}return conclusions\n"
349
381
  return statement
@@ -1,17 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ import builtins
4
5
  import importlib
5
6
  import json
6
7
  import logging
7
8
  import os
8
- from abc import abstractmethod
9
+ import re
9
10
  from collections import UserDict
10
11
  from copy import deepcopy
11
- from dataclasses import dataclass, is_dataclass, fields
12
+ from dataclasses import is_dataclass, fields
13
+ from types import NoneType
12
14
 
13
15
  import matplotlib
14
16
  import networkx as nx
17
+ import requests
15
18
  from anytree import Node, RenderTree
16
19
  from anytree.exporter import DotExporter
17
20
  from matplotlib import pyplot as plt
@@ -22,11 +25,203 @@ from typing_extensions import Callable, Set, Any, Type, Dict, TYPE_CHECKING, get
22
25
  get_origin, get_args, Tuple, Optional, List, Union, Self
23
26
 
24
27
  if TYPE_CHECKING:
25
- from .datastructures import Case
28
+ from .datastructures.case import Case
29
+ from .rules import Rule
30
+
31
+ import ast
26
32
 
27
33
  matplotlib.use("Qt5Agg") # or "Qt5Agg", depending on availability
28
34
 
29
35
 
36
+ def is_matching(rdr_classifier: Callable[[Any], Any], case_query: CaseQuery, pred_cat: Optional[Dict[str, Any]] = None) -> bool:
37
+ """
38
+ :param rdr_classifier: The RDR classifier to check the prediction of.
39
+ :param case_query: The case query to check.
40
+ :param pred_cat: The predicted category.
41
+ :return: Whether the classifier prediction is matching case_query target or not.
42
+ """
43
+ if case_query.target is None:
44
+ return False
45
+ if pred_cat is None:
46
+ pred_cat = rdr_classifier(case_query.case)
47
+ if not isinstance(pred_cat, dict):
48
+ pred_cat = {case_query.attribute_name: pred_cat}
49
+ target = {case_query.attribute_name: case_query.target_value}
50
+ precision, recall = calculate_precision_and_recall(pred_cat, target)
51
+ return all(recall) and all(precision)
52
+
53
+
54
+ def calculate_precision_and_recall(pred_cat: Dict[str, Any], target: Dict[str, Any]) -> Tuple[
55
+ List[bool], List[bool]]:
56
+ """
57
+ :param pred_cat: The predicted category.
58
+ :param target: The target category.
59
+ :return: The precision and recall of the classifier.
60
+ """
61
+ recall = []
62
+ precision = []
63
+ for pred_key, pred_value in pred_cat.items():
64
+ if pred_key not in target:
65
+ continue
66
+ precision.extend([v in make_set(target[pred_key]) for v in make_set(pred_value)])
67
+ for target_key, target_value in target.items():
68
+ if target_key not in pred_cat:
69
+ recall.append(False)
70
+ continue
71
+ recall.extend([v in make_set(pred_cat[target_key]) for v in make_set(target_value)])
72
+ return precision, recall
73
+
74
+
75
+ def get_rule_conclusion_as_source_code(rule: Rule, conclusion: str, parent_indent: str = "") -> str:
76
+ """
77
+ Convert the conclusion of a rule to source code.
78
+
79
+ :param rule: The rule to get the conclusion from.
80
+ :param conclusion: The conclusion to convert to source code.
81
+ :param parent_indent: The indentation to use for the source code.
82
+ :return: The source code of the conclusion.
83
+ """
84
+ indent = f"{parent_indent}{' ' * 4}"
85
+ if "def " in conclusion:
86
+ # This means the conclusion is a definition that should be written and then called
87
+ conclusion_lines = conclusion.split('\n')
88
+ # use regex to replace the function name
89
+ new_function_name = f"def conclusion_{id(rule)}"
90
+ conclusion_lines[0] = re.sub(r"def (\w+)", new_function_name, conclusion_lines[0])
91
+ conclusion_lines = [f"{indent}{line}" for line in conclusion_lines]
92
+ conclusion_lines.append(f"{indent}return {new_function_name.replace('def ', '')}(case)\n")
93
+ return "\n".join(conclusion_lines)
94
+ else:
95
+ raise ValueError(f"Conclusion is format is not valid, it should be a one line string or "
96
+ f"contain a function definition. Instead got:\n{conclusion}\n")
97
+
98
+
99
+ def ask_llm(prompt):
100
+ try:
101
+ response = requests.post("http://localhost:11434/api/generate", json={
102
+ "model": "codellama:7b-instruct", # or "phi"
103
+ "prompt": prompt,
104
+ "stream": False,
105
+ })
106
+ result = response.json()
107
+ return result.get("response", "").strip()
108
+ except Exception as e:
109
+ return f"❌ Local LLM error: {e}"
110
+
111
+
112
+ def get_case_attribute_type(original_case: Any, attribute_name: str,
113
+ known_value: Optional[Any] = None) -> Type:
114
+ """
115
+ :param original_case: The case to get the attribute from.
116
+ :param attribute_name: The name of the attribute.
117
+ :param known_value: A known value of the attribute.
118
+ :return: The type of the attribute.
119
+ """
120
+ if known_value is not None:
121
+ return type(known_value)
122
+ elif hasattr(original_case, attribute_name):
123
+ hint, origin, args = get_hint_for_attribute(attribute_name, original_case)
124
+ if origin is not None:
125
+ origin = typing_to_python_type(origin)
126
+ if origin == Union:
127
+ if len(args) == 2:
128
+ if args[1] is type(None):
129
+ return typing_to_python_type(args[0])
130
+ elif args[0] is type(None):
131
+ return typing_to_python_type(args[1])
132
+ elif len(args) == 1:
133
+ return typing_to_python_type(args[0])
134
+ else:
135
+ raise ValueError(f"Union with more than 2 types is not supported: {args}")
136
+ elif origin is not None:
137
+ return origin
138
+ if hint is not None:
139
+ return typing_to_python_type(hint)
140
+
141
+
142
+ def conclusion_to_json(conclusion):
143
+ if is_iterable(conclusion):
144
+ conclusions = {'_type': get_full_class_name(type(conclusion)), 'value': []}
145
+ for c in conclusion:
146
+ conclusions['value'].append(conclusion_to_json(c))
147
+ elif hasattr(conclusion, 'to_json'):
148
+ conclusions = conclusion.to_json()
149
+ else:
150
+ conclusions = {'_type': get_full_class_name(type(conclusion)), 'value': conclusion}
151
+ return conclusions
152
+
153
+
154
+ def contains_return_statement(source: str) -> bool:
155
+ """
156
+ :param source: The source code to check.
157
+ :return: True if the source code contains a return statement, False otherwise.
158
+ """
159
+ try:
160
+ tree = ast.parse(source)
161
+ for node in tree.body:
162
+ if isinstance(node, ast.Return):
163
+ return True
164
+ return False
165
+ except SyntaxError:
166
+ return False
167
+
168
+
169
+ def get_names_used(node):
170
+ return {n.id for n in ast.walk(node) if isinstance(n, ast.Name)}
171
+
172
+
173
+ def extract_dependencies(code_lines):
174
+ full_code = '\n'.join(code_lines)
175
+ tree = ast.parse(full_code)
176
+ final_stmt = tree.body[-1]
177
+
178
+ if not isinstance(final_stmt, ast.Return):
179
+ raise ValueError("Last line is not a return statement")
180
+
181
+ needed = get_names_used(final_stmt.value)
182
+ required_lines = []
183
+ line_map = {id(node): i for i, node in enumerate(tree.body)}
184
+
185
+ def handle_stmt(stmt, needed):
186
+ keep = False
187
+ if isinstance(stmt, ast.Assign):
188
+ targets = [t.id for t in stmt.targets if isinstance(t, ast.Name)]
189
+ if any(t in needed for t in targets):
190
+ needed.update(get_names_used(stmt.value))
191
+ keep = True
192
+ elif isinstance(stmt, ast.AugAssign):
193
+ if isinstance(stmt.target, ast.Name) and stmt.target.id in needed:
194
+ needed.update(get_names_used(stmt.value))
195
+ keep = True
196
+ elif isinstance(stmt, ast.FunctionDef):
197
+ if stmt.name in needed:
198
+ for n in ast.walk(stmt):
199
+ if isinstance(n, ast.Name):
200
+ needed.add(n.id)
201
+ keep = True
202
+ elif isinstance(stmt, (ast.For, ast.While, ast.If)):
203
+ # Check if any of the body statements interact with needed variables
204
+ for substmt in stmt.body + getattr(stmt, 'orelse', []):
205
+ if handle_stmt(substmt, needed):
206
+ keep = True
207
+ # Also check the condition (test or iter)
208
+ if isinstance(stmt, ast.For):
209
+ if isinstance(stmt.target, ast.Name) and stmt.target.id in needed:
210
+ keep = True
211
+ needed.update(get_names_used(stmt.iter))
212
+ elif isinstance(stmt, ast.If) or isinstance(stmt, ast.While):
213
+ needed.update(get_names_used(stmt.test))
214
+
215
+ return keep
216
+
217
+ for stmt in reversed(tree.body[:-1]):
218
+ if handle_stmt(stmt, needed):
219
+ required_lines.insert(0, code_lines[line_map[id(stmt)]])
220
+
221
+ required_lines.append(code_lines[-1]) # Always include return
222
+ return required_lines
223
+
224
+
30
225
  def serialize_dataclass(obj: Any) -> Union[Dict, Any]:
31
226
  """
32
227
  Recursively serialize a dataclass to a dictionary. If the dataclass contains any nested dataclasses, they will be
@@ -61,6 +256,7 @@ def deserialize_dataclass(data: dict) -> Any:
61
256
  :param data: The dictionary to deserialize.
62
257
  :return: The deserialized dataclass.
63
258
  """
259
+
64
260
  def recursive_load(obj):
65
261
  if isinstance(obj, dict) and "__dataclass__" in obj:
66
262
  module_name, class_name = obj["__dataclass__"].rsplit(".", 1)
@@ -219,6 +415,8 @@ def get_type_from_string(type_path: str):
219
415
  """
220
416
  module_path, class_name = type_path.rsplit(".", 1)
221
417
  module = importlib.import_module(module_path)
418
+ if module == builtins and class_name == 'NoneType':
419
+ return type(None)
222
420
  return getattr(module, class_name)
223
421
 
224
422
 
@@ -316,6 +514,8 @@ class SubclassJSONSerializer:
316
514
  data_type = get_type_from_string(data["_type"])
317
515
  if len(data) == 1:
318
516
  return data_type
517
+ if data_type == NoneType:
518
+ return None
319
519
  if data_type.__module__ == 'builtins':
320
520
  if is_iterable(data['value']) and not isinstance(data['value'], dict):
321
521
  return data_type([cls.from_json(d) for d in data['value']])
@@ -443,6 +643,7 @@ def table_rows_as_str(row_dict: Dict[str, Any], columns_per_row: int = 9):
443
643
  values = [list(map(lambda i: i[1], row)) for row in all_items]
444
644
  all_table_rows = []
445
645
  for row_keys, row_values in zip(keys, values):
646
+ row_values = [str(v) if v is not None else "" for v in row_values]
446
647
  table = tabulate([row_values], headers=row_keys, tablefmt='plain', maxcolwidths=[20] * len(row_keys))
447
648
  all_table_rows.append(table)
448
649
  return "\n".join(all_table_rows)
@@ -579,7 +780,7 @@ def get_all_subclasses(cls: Type) -> Dict[str, Type]:
579
780
  return all_subclasses
580
781
 
581
782
 
582
- def make_set(value: Any) -> Set:
783
+ def make_set(value: Any) -> Set[Any]:
583
784
  """
584
785
  Make a set from a value.
585
786
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.1.21
3
+ Version: 0.1.61
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
@@ -709,7 +709,7 @@ Fit the SCRDR to the data, then classify one of the data cases to check if its c
709
709
  and render the tree to a file:
710
710
 
711
711
  ```Python
712
- from ripple_down_rules.datastructures import CaseQuery
712
+ from ripple_down_rules.datastructures.dataclasses import CaseQuery
713
713
  from ripple_down_rules.rdr import SingleClassRDR
714
714
  from ripple_down_rules.datasets import load_zoo_dataset
715
715
  from ripple_down_rules.utils import render_tree
@@ -719,12 +719,13 @@ all_cases, targets = load_zoo_dataset()
719
719
  scrdr = SingleClassRDR()
720
720
 
721
721
  # Fit the SCRDR to the data
722
- case_queries = [CaseQuery(case, target=target) for case, target in zip(all_cases, targets)]
722
+ case_queries = [CaseQuery(case, 'species', type(target), True, _target=target)
723
+ for case, target in zip(all_cases[:10], targets[:10])]
723
724
  scrdr.fit(case_queries, animate_tree=True)
724
725
 
725
726
  # Render the tree to a file
726
727
  render_tree(scrdr.start_rule, use_dot_exporter=True, filename="scrdr")
727
728
 
728
- cat = scrdr.fit_case(all_cases[50], targets[50])
729
+ cat = scrdr.classify(all_cases[50])
729
730
  assert cat == targets[50]
730
731
  ```
@@ -0,0 +1,20 @@
1
+ ripple_down_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ripple_down_rules/datasets.py,sha256=rCSpeFeu1gTuKESwjHUdQkPPvomI5OMRNGpbdKmHwMg,4639
3
+ ripple_down_rules/experts.py,sha256=sA9Cmx9BlwlCFYRDDLz3VG6e5njujAFZEItSnnzrG5E,10490
4
+ ripple_down_rules/failures.py,sha256=E6ajDUsw3Blom8eVLbA7d_Qnov2conhtZ0UmpQ9ZtSE,302
5
+ ripple_down_rules/helpers.py,sha256=AhqerAQoCdSovJ7SdQrNtAI_hYagKpLsy2nJQGA0bl0,1062
6
+ ripple_down_rules/prompt.py,sha256=6g-WqMiOFp9QyAZDmiNbHbPjAeeJHb6ItLGdQAVxGKk,6063
7
+ ripple_down_rules/rdr.py,sha256=VT7AWTDlLOyk2FILa4mHixdno2kXtk82m_pSY1CoEiE,48789
8
+ ripple_down_rules/rdr_decorators.py,sha256=8SclpceI3EtrsbuukWJu8HGLh7Q1ZCgYGLX-RPlG-w0,2018
9
+ ripple_down_rules/rules.py,sha256=KTB7kPnyyU9GuZhVe9ba25-3ICdzl46r9MFduckk-_Y,16147
10
+ ripple_down_rules/utils.py,sha256=ppKTt3_O6JgmTqCdkjVBfuVaI6P7b4oRCSOmnBaqaVM,32110
11
+ ripple_down_rules/datastructures/__init__.py,sha256=V2aNgf5C96Y5-IGghra3n9uiefpoIm_QdT7cc_C8cxQ,111
12
+ ripple_down_rules/datastructures/callable_expression.py,sha256=TW_u6CJfelW2CiJj9pWFpdOBNIxeEuhhsQEz_pLpFVE,9092
13
+ ripple_down_rules/datastructures/case.py,sha256=A7qkl5W48zldTtA4m-NJRYEwlMBpo7uGugnriNwcY0E,13597
14
+ ripple_down_rules/datastructures/dataclasses.py,sha256=inhTE4tlMrwVRcYDtqAaR0JlxlyD87JIUvXIu5H9Ioo,5860
15
+ ripple_down_rules/datastructures/enums.py,sha256=l0Eu-TeJ6qB2XHoJycXmUgLw-3yUebQ8SsEbW8bBZdM,4543
16
+ ripple_down_rules-0.1.61.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
17
+ ripple_down_rules-0.1.61.dist-info/METADATA,sha256=fstYXWm2KIN0bmiUkJTD6UIDiGQGtjE-pt263bDgJqk,42576
18
+ ripple_down_rules-0.1.61.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
19
+ ripple_down_rules-0.1.61.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
20
+ ripple_down_rules-0.1.61.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,20 +0,0 @@
1
- ripple_down_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ripple_down_rules/datasets.py,sha256=AzPtqUXuR1qLQNtRsWLsJ3gX2oIf8nIkFvmsmz7fHlw,4601
3
- ripple_down_rules/experts.py,sha256=Xz1U1Tdq7jrFlcVuSusaMB241AG9TEs7q101i59Xijs,10683
4
- ripple_down_rules/failures.py,sha256=E6ajDUsw3Blom8eVLbA7d_Qnov2conhtZ0UmpQ9ZtSE,302
5
- ripple_down_rules/helpers.py,sha256=AhqerAQoCdSovJ7SdQrNtAI_hYagKpLsy2nJQGA0bl0,1062
6
- ripple_down_rules/prompt.py,sha256=z6KddZOsNiStptgCRNh2OVHHuH6Ooa2f-nsrgJH1qJ8,6311
7
- ripple_down_rules/rdr.py,sha256=NXVYIflUxcDzC5DDrK-l_ZT-sBmUV1ZgkznshSsJZYc,43508
8
- ripple_down_rules/rdr_decorators.py,sha256=8SclpceI3EtrsbuukWJu8HGLh7Q1ZCgYGLX-RPlG-w0,2018
9
- ripple_down_rules/rules.py,sha256=aM3Im4ePuFDlkuD2EKRtiVmYgoQ_sxlwcbzrDKqXAfs,14578
10
- ripple_down_rules/utils.py,sha256=9gPnRWlLye7FettI2QRWJx8oU9z3ckwdO5jopXK8b-8,24290
11
- ripple_down_rules/datastructures/__init__.py,sha256=zpmiYm4WkwNHaGdTIfacS7llN5d2xyU6U-saH_TpydI,103
12
- ripple_down_rules/datastructures/callable_expression.py,sha256=ac2TaMr0hiRX928GMcr3oTQic8KXXO4syLw4KV-Iehs,10515
13
- ripple_down_rules/datastructures/case.py,sha256=3Pl07jmYn94wdCVTaRZDmBPgyAsN1TjebvrE6-68MVU,13606
14
- ripple_down_rules/datastructures/dataclasses.py,sha256=AI-wqNy8y9QPg6lov0P-c5b8JXemuM4X62tIRhW-Gqs,4231
15
- ripple_down_rules/datastructures/enums.py,sha256=l0Eu-TeJ6qB2XHoJycXmUgLw-3yUebQ8SsEbW8bBZdM,4543
16
- ripple_down_rules-0.1.21.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
17
- ripple_down_rules-0.1.21.dist-info/METADATA,sha256=Jg_qdgg-DkAqblqWTisVcv1K3H8Thr_CjdeIX3UyLgE,42519
18
- ripple_down_rules-0.1.21.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
19
- ripple_down_rules-0.1.21.dist-info/top_level.txt,sha256=VeoLhEhyK46M1OHwoPbCQLI1EifLjChqGzhQ6WEUqeM,18
20
- ripple_down_rules-0.1.21.dist-info/RECORD,,