ripple-down-rules 0.4.86__tar.gz → 0.5.0__tar.gz

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.
Files changed (126) hide show
  1. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/PKG-INFO +4 -12
  2. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/pyproject.toml +3 -20
  3. ripple_down_rules-0.5.0/pytest.ini +3 -0
  4. ripple_down_rules-0.5.0/src/ripple_down_rules/__init__.py +5 -0
  5. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datastructures/callable_expression.py +20 -1
  6. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datastructures/case.py +7 -5
  7. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datastructures/dataclasses.py +9 -1
  8. ripple_down_rules-0.5.0/src/ripple_down_rules/experts.py +317 -0
  9. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/rdr.py +166 -80
  10. ripple_down_rules-0.5.0/src/ripple_down_rules/rdr_decorators.py +160 -0
  11. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/rules.py +5 -4
  12. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/template_file_creator.py +6 -6
  13. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/utils.py +17 -15
  14. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules.egg-info/PKG-INFO +4 -12
  15. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules.egg-info/SOURCES.txt +11 -2
  16. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules.egg-info/requires.txt +2 -11
  17. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_json_serialization.py +6 -6
  18. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_on_mutagenic.py +5 -5
  19. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_rdr.py +98 -49
  20. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_rdr_alchemy.py +3 -3
  21. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_rdr_world.py +11 -9
  22. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_relational_rdr.py +2 -2
  23. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_relational_rdr_alchemy.py +2 -2
  24. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_is_a_robot/__init__.py +1 -0
  25. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr.py +16 -0
  26. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr_defs.py +19 -0
  27. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_rdr.py +15 -0
  28. {ripple_down_rules-0.4.86/test/test_results → ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_is_a_robot/rdr_metadata}/datasets_physical_object_is_a_robot.json +18 -13
  29. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/__init__.py +1 -0
  30. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_output__mcrdr.py +20 -0
  31. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_output__mcrdr_defs.py +23 -0
  32. ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_rdr.py +15 -0
  33. {ripple_down_rules-0.4.86/test/test_results → ripple_down_rules-0.5.0/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/rdr_metadata}/datasets_physical_object_select_objects_that_are_parts_of_robot.json +20 -14
  34. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_sql_model.py +4 -4
  35. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_template_file_creator.py +29 -0
  36. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/test_qt_gui_inline.py +2 -1
  37. ripple_down_rules-0.4.86/src/ripple_down_rules/__init__.py +0 -1
  38. ripple_down_rules-0.4.86/src/ripple_down_rules/experts.py +0 -160
  39. ripple_down_rules-0.4.86/src/ripple_down_rules/rdr_decorators.py +0 -139
  40. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/.github/workflows/publish-to-test-pypi.yml +0 -0
  41. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]/shelved.patch +0 -0
  42. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]1/shelved.patch +0 -0
  43. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/LICENSE +0 -0
  44. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/README.md +0 -0
  45. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/examples/readme_example.py +0 -0
  46. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/images/thinking_pr2.jpg +0 -0
  47. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/requirements-dev.txt +0 -0
  48. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/requirements-gui.txt +0 -0
  49. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/requirements-viz.txt +0 -0
  50. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/requirements.txt +0 -0
  51. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_extra.dot +0 -0
  52. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_extra.png +0 -0
  53. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_stop_only.dot +0 -0
  54. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_stop_only.png +0 -0
  55. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_stop_plus_rule.dot +0 -0
  56. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_mcrdr_stop_plus_rule.png +0 -0
  57. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr.dot +0 -0
  58. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr.png +0 -0
  59. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr_2.dot +0 -0
  60. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr_2.png +0 -0
  61. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr_3.dot +0 -0
  62. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/complete_scrdr_3.png +0 -0
  63. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/grdr_Habitat.dot +0 -0
  64. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/grdr_Habitat.png +0 -0
  65. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/grdr_Species.dot +0 -0
  66. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/grdr_Species.png +0 -0
  67. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_extra.dot +0 -0
  68. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_extra.png +0 -0
  69. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_extra_classify.dot +0 -0
  70. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_extra_classify.png +0 -0
  71. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_stop_plus_rule_combined.dot +0 -0
  72. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/mcrdr_stop_plus_rule_combined.png +0 -0
  73. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/partial_mcrdr_extra.dot +0 -0
  74. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/partial_mcrdr_extra.png +0 -0
  75. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/relational_scrdr_classify.dot +0 -0
  76. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/results/relational_scrdr_classify.png +0 -0
  77. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/setup.cfg +0 -0
  78. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datasets.py +0 -0
  79. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  80. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/datastructures/enums.py +0 -0
  81. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/failures.py +0 -0
  82. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/helpers.py +0 -0
  83. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/start-code-server.sh +0 -0
  84. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/__init__.py +0 -0
  85. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/gui.py +0 -0
  86. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/ipython_custom_shell.py +0 -0
  87. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/object_diagram.py +0 -0
  88. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules/user_interface/prompt.py +0 -0
  89. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  90. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  91. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/correct_drawer_rdr_expert_answers_fit.json +0 -0
  92. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/grdr_expert_answers_classify.json +0 -0
  93. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/grdr_expert_answers_fit.json +0 -0
  94. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/grdr_expert_answers_fit_extra.json +0 -0
  95. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/grdr_expert_answers_fit_no_targets.json +0 -0
  96. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_expert_answers_classify.json +0 -0
  97. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_expert_answers_fit_no_targets.json +0 -0
  98. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_expert_answers_stop_only_fit.json +0 -0
  99. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_extra_expert_answers_classify.json +0 -0
  100. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_extra_expert_answers_fit.json +0 -0
  101. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_multi_line_expert_answers_fit.json +0 -0
  102. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_stop_only_answers_fit.json +0 -0
  103. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_stop_plus_rule_answers_fit.json +0 -0
  104. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_stop_plus_rule_combined_expert_answers_fit.json +0 -0
  105. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mcrdr_stop_plus_rule_expert_answers_fit.json +0 -0
  106. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/mutagenic_expert_answers.json +0 -0
  107. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/relational_scrdr_expert_answers_classify.json +0 -0
  108. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/scrdr_expert_answers_classify.json +0 -0
  109. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/scrdr_expert_answers_fit.json +0 -0
  110. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/scrdr_expert_answers_fit_no_targets.json +0 -0
  111. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/scrdr_multi_line_expert_answers_fit.json +0 -0
  112. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/scrdr_world_expert_answers_fit.json +0 -0
  113. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_expert_answers/view_rdr_expert_answers_fit.json +0 -0
  114. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_generated_rdrs/__init__.py +0 -0
  115. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_helpers/__init__.py +0 -0
  116. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_helpers/helpers.py +0 -0
  117. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_helpers/object_diagram_case_query.png +0 -0
  118. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_helpers/object_diagram_person.png +0 -0
  119. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_object_diagram.py +0 -0
  120. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_rdr_decorators.py +0 -0
  121. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/__init__.py +0 -0
  122. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/test_ipython.py +0 -0
  123. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/test_ipython_copilot.py +0 -0
  124. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/test_pdb.py +0 -0
  125. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_user_interface/test_qt_gui.py +0 -0
  126. {ripple_down_rules-0.4.86 → ripple_down_rules-0.5.0}/test/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.4.86
3
+ Version: 0.5.0
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
@@ -693,20 +693,12 @@ Requires-Dist: colorama
693
693
  Requires-Dist: pygments
694
694
  Requires-Dist: sqlalchemy
695
695
  Requires-Dist: pandas
696
+ Provides-Extra: viz
697
+ Requires-Dist: networkx>=3.1; extra == "viz"
698
+ Requires-Dist: matplotlib>=3.7.5; extra == "viz"
696
699
  Provides-Extra: gui
697
700
  Requires-Dist: pyqt6; extra == "gui"
698
701
  Requires-Dist: qtconsole; extra == "gui"
699
- Provides-Extra: viz
700
- Requires-Dist: matplotlib; extra == "viz"
701
- Requires-Dist: networkx; extra == "viz"
702
- Provides-Extra: dev
703
- Requires-Dist: pyqt6; extra == "dev"
704
- Requires-Dist: qtconsole; extra == "dev"
705
- Requires-Dist: matplotlib; extra == "dev"
706
- Requires-Dist: networkx; extra == "dev"
707
- Requires-Dist: pytest; extra == "dev"
708
- Requires-Dist: ucimlrepo>=0.0.7; extra == "dev"
709
- Requires-Dist: pdbpp; extra == "dev"
710
702
  Dynamic: license-file
711
703
 
712
704
  # Ripple Down Rules (RDR)
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "ripple_down_rules"
9
- dynamic = ["version", "dependencies"]
9
+ dynamic = ["version", "dependencies", "optional-dependencies"]
10
10
  description = "Implements the various versions of Ripple Down Rules (RDR) for knowledge representation and reasoning."
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Abdelrhman Bassiouny", email = "abassiou@uni-bremen.de" }]
@@ -22,22 +22,5 @@ include = ["ripple_down_rules*"]
22
22
 
23
23
  [tool.setuptools.dynamic]
24
24
  version = {attr = "ripple_down_rules.__version__"}
25
- dependencies = {file = "requirements.txt"}
26
-
27
- [project.optional-dependencies]
28
- gui = [
29
- "pyqt6",
30
- "qtconsole",
31
- ]
32
- viz = [
33
- "matplotlib",
34
- "networkx",
35
- ]
36
- dev = [
37
- "pyqt6",
38
- "qtconsole",
39
- "matplotlib",
40
- "networkx",
41
- "pytest",
42
- "ucimlrepo>=0.0.7",
43
- "pdbpp"]
25
+ dependencies = { file = ["requirements.txt"] }
26
+ optional-dependencies = { viz = {file = ["requirements-viz.txt"]}, gui = {file = ["requirements-gui.txt"]}}
@@ -0,0 +1,3 @@
1
+ [pytest]
2
+ addopts =
3
+ -s
@@ -0,0 +1,5 @@
1
+ __version__ = "0.5.00"
2
+
3
+ import logging
4
+ logger = logging.Logger("rdr")
5
+ logger.setLevel(logging.INFO)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  import logging
5
+ import os
5
6
  from _ast import AST
6
7
  from enum import Enum
7
8
 
@@ -10,7 +11,7 @@ from typing_extensions import Type, Optional, Any, List, Union, Tuple, Dict, Set
10
11
  from .case import create_case, Case
11
12
  from ..utils import SubclassJSONSerializer, get_full_class_name, get_type_from_string, conclusion_to_json, is_iterable, \
12
13
  build_user_input_from_conclusion, encapsulate_user_input, extract_function_source, are_results_subclass_of_types, \
13
- make_list
14
+ make_list, get_imports_from_scope
14
15
 
15
16
 
16
17
  class VariableVisitor(ast.NodeVisitor):
@@ -175,6 +176,24 @@ class CallableExpression(SubclassJSONSerializer):
175
176
  return
176
177
  self.user_input = self.encapsulating_function + '\n' + new_function_body
177
178
 
179
+ def write_to_python_file(self, file_path: str, append: bool = False):
180
+ """
181
+ Write the callable expression to a python file.
182
+
183
+ :param file_path: The path to the file where the callable expression will be written.
184
+ :param append: If True, the callable expression will be appended to the file. If False,
185
+ the file will be overwritten.
186
+ """
187
+ imports = '\n'.join(get_imports_from_scope(self.scope))
188
+ if append and os.path.exists(file_path):
189
+ with open(file_path, 'a') as f:
190
+ f.write('\n\n\n' + imports + '\n\n\n')
191
+ f.write(self.user_input)
192
+ else:
193
+ with open(file_path, 'w') as f:
194
+ f.write(imports + '\n\n\n')
195
+ f.write(self.user_input)
196
+
178
197
  @property
179
198
  def user_input(self):
180
199
  """
@@ -354,11 +354,13 @@ def show_current_and_corner_cases(case: Any, targets: Optional[Dict[str, Any]] =
354
354
  if last_evaluated_rule and last_evaluated_rule.fired:
355
355
  corner_row_dict = copy_case(corner_case)
356
356
 
357
+ case_dict.update(targets)
358
+ case_dict.update(current_conclusions)
359
+ all_table_rows = [case_dict]
357
360
  if corner_row_dict:
358
361
  corner_conclusion = last_evaluated_rule.conclusion(case)
359
362
  corner_row_dict.update({corner_conclusion.__class__.__name__: corner_conclusion})
360
- print(table_rows_as_str(corner_row_dict))
361
- print("=" * 50)
362
- case_dict.update(targets)
363
- case_dict.update(current_conclusions)
364
- print(table_rows_as_str(case_dict))
363
+ all_table_rows.append(corner_row_dict)
364
+ # print(table_rows_as_str(corner_row_dict))
365
+ print("\n" + "=" * 50)
366
+ print(table_rows_as_str(all_table_rows))
@@ -78,7 +78,15 @@ class CaseQuery:
78
78
  """
79
79
  :return: The type of the case that the attribute belongs to.
80
80
  """
81
- return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
81
+ if self.is_function:
82
+ if self.function_args_type_hints is not None:
83
+ func_args = [arg for name, arg in self.function_args_type_hints.items() if name != 'return']
84
+ case_type_args = Union[tuple(func_args)]
85
+ else:
86
+ case_type_args = Any
87
+ return Dict[str, case_type_args]
88
+ else:
89
+ return self.original_case._obj_type if isinstance(self.original_case, Case) else type(self.original_case)
82
90
 
83
91
  @property
84
92
  def case(self) -> Any:
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import logging
6
+ import os
7
+ from abc import ABC, abstractmethod
8
+
9
+ from typing_extensions import Optional, TYPE_CHECKING, List
10
+
11
+ from .datastructures.callable_expression import CallableExpression
12
+ from .datastructures.enums import PromptFor
13
+ from .datastructures.dataclasses import CaseQuery
14
+ from .datastructures.case import show_current_and_corner_cases
15
+ from .utils import extract_imports, extract_function_source, get_imports_from_scope, encapsulate_user_input
16
+
17
+ try:
18
+ from .user_interface.gui import RDRCaseViewer
19
+ except ImportError as e:
20
+ RDRCaseViewer = None
21
+ from .user_interface.prompt import UserPrompt
22
+
23
+ if TYPE_CHECKING:
24
+ from .rdr import Rule
25
+
26
+
27
+ class Expert(ABC):
28
+ """
29
+ The Abstract Expert class, all experts should inherit from this class.
30
+ An expert is a class that can provide differentiating features and conclusions for a case when asked.
31
+ The expert can compare a case with a corner case and provide the differentiating features and can also
32
+ provide one or multiple conclusions for a case.
33
+ """
34
+ all_expert_answers: Optional[List] = None
35
+ """
36
+ A list of all expert answers, used for testing purposes.
37
+ """
38
+ use_loaded_answers: bool = False
39
+ """
40
+ A flag to indicate if the expert should use loaded answers or not.
41
+ """
42
+
43
+ def __init__(self, use_loaded_answers: bool = True,
44
+ append: bool = False,
45
+ answers_save_path: Optional[str] = None):
46
+ self.all_expert_answers = []
47
+ self.use_loaded_answers = use_loaded_answers
48
+ self.append = append
49
+ self.answers_save_path = answers_save_path
50
+ if answers_save_path is not None:
51
+ if use_loaded_answers:
52
+ self.load_answers(answers_save_path)
53
+ else:
54
+ os.remove(answers_save_path + '.py')
55
+ self.append = True
56
+
57
+ @abstractmethod
58
+ def ask_for_conditions(self, case_query: CaseQuery, last_evaluated_rule: Optional[Rule] = None) \
59
+ -> CallableExpression:
60
+ """
61
+ Ask the expert to provide the differentiating features between two cases or unique features for a case
62
+ that doesn't have a corner case to compare to.
63
+
64
+ :param case_query: The case query containing the case to classify and the required target.
65
+ :param last_evaluated_rule: The last evaluated rule.
66
+ :return: The differentiating features as new rule conditions.
67
+ """
68
+ pass
69
+
70
+ @abstractmethod
71
+ def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
72
+ """
73
+ Ask the expert to provide a relational conclusion for the case.
74
+
75
+ :param case_query: The case query containing the case to find a conclusion for.
76
+ :return: A callable expression that can be called with a new case as an argument.
77
+ """
78
+
79
+ def clear_answers(self, path: Optional[str] = None):
80
+ """
81
+ Clear the expert answers.
82
+
83
+ :param path: The path to clear the answers from. If None, the answers will be cleared from the
84
+ answers_save_path attribute.
85
+ """
86
+ if path is None and self.answers_save_path is None:
87
+ raise ValueError("No path provided to clear expert answers, either provide a path or set the "
88
+ "answers_save_path attribute.")
89
+ if path is None:
90
+ path = self.answers_save_path
91
+ if os.path.exists(path + '.json'):
92
+ os.remove(path + '.json')
93
+ if os.path.exists(path + '.py'):
94
+ os.remove(path + '.py')
95
+ self.all_expert_answers = []
96
+
97
+ def save_answers(self, path: Optional[str] = None):
98
+ """
99
+ Save the expert answers to a file.
100
+
101
+ :param path: The path to save the answers to.
102
+ """
103
+ if path is None and self.answers_save_path is None:
104
+ raise ValueError("No path provided to save expert answers, either provide a path or set the "
105
+ "answers_save_path attribute.")
106
+ if path is None:
107
+ path = self.answers_save_path
108
+ is_json = os.path.exists(path + '.json')
109
+ if is_json:
110
+ self._save_to_json(path)
111
+ else:
112
+ self._save_to_python(path)
113
+
114
+ def _save_to_json(self, path: str):
115
+ """
116
+ Save the expert answers to a JSON file.
117
+
118
+ :param path: The path to save the answers to.
119
+ """
120
+ all_answers = self.all_expert_answers
121
+ if self.append and os.path.exists(path + '.json'):
122
+ # read the file and append the new answers
123
+ with open(path + '.json', "r") as f:
124
+ old_answers = json.load(f)
125
+ all_answers = old_answers + all_answers
126
+ with open(path + '.json', "w") as f:
127
+ json.dump(all_answers, f)
128
+
129
+ def _save_to_python(self, path: str):
130
+ """
131
+ Save the expert answers to a Python file.
132
+
133
+ :param path: The path to save the answers to.
134
+ """
135
+ dir_name = os.path.dirname(path)
136
+ if not os.path.exists(dir_name + '/__init__.py'):
137
+ os.makedirs(dir_name, exist_ok=True)
138
+ with open(dir_name + '/__init__.py', 'w') as f:
139
+ f.write('# This is an empty init file to make the directory a package.\n')
140
+ action = 'w' if not self.append else 'a'
141
+ with open(path + '.py', action) as f:
142
+ for scope, func_source in self.all_expert_answers:
143
+ if len(scope) > 0:
144
+ imports = '\n'.join(get_imports_from_scope(scope)) + '\n\n\n'
145
+ else:
146
+ imports = ''
147
+ if func_source is not None:
148
+ func_source = encapsulate_user_input(func_source, CallableExpression.encapsulating_function)
149
+ else:
150
+ func_source = 'pass # No user input provided for this case.\n'
151
+ f.write(imports + func_source + '\n' + '\n\n\n\'===New Answer===\'\n\n\n')
152
+
153
+ def load_answers(self, path: Optional[str] = None):
154
+ """
155
+ Load the expert answers from a file.
156
+
157
+ :param path: The path to load the answers from.
158
+ """
159
+ if path is None and self.answers_save_path is None:
160
+ raise ValueError("No path provided to load expert answers from, either provide a path or set the "
161
+ "answers_save_path attribute.")
162
+ if path is None:
163
+ path = self.answers_save_path
164
+ is_json = os.path.exists(path + '.json')
165
+ if is_json:
166
+ self._load_answers_from_json(path)
167
+ elif os.path.exists(path + '.py'):
168
+ self._load_answers_from_python(path)
169
+
170
+ def _load_answers_from_json(self, path: str):
171
+ """
172
+ Load the expert answers from a JSON file.
173
+
174
+ :param path: The path to load the answers from.
175
+ """
176
+ with open(path + '.json', "r") as f:
177
+ all_answers = json.load(f)
178
+ self.all_expert_answers = [({}, answer) for answer in all_answers]
179
+
180
+ def _load_answers_from_python(self, path: str):
181
+ """
182
+ Load the expert answers from a Python file.
183
+
184
+ :param path: The path to load the answers from.
185
+ """
186
+ file_path = path + '.py'
187
+ with open(file_path, "r") as f:
188
+ all_answers = f.read().split('\n\n\n\'===New Answer===\'\n\n\n')
189
+ for answer in all_answers:
190
+ answer = answer.strip('\n').strip()
191
+ if 'def ' not in answer and 'pass' in answer:
192
+ self.all_expert_answers.append(({}, None))
193
+ scope = extract_imports(tree=ast.parse(answer))
194
+ func_source = list(extract_function_source(file_path, []).values())[0]
195
+ self.all_expert_answers.append((scope, func_source))
196
+
197
+
198
+ class Human(Expert):
199
+ """
200
+ The Human Expert class, an expert that asks the human to provide differentiating features and conclusions.
201
+ """
202
+
203
+ def __init__(self, viewer: Optional[RDRCaseViewer] = None, **kwargs):
204
+ """
205
+ Initialize the Human expert.
206
+
207
+ :param viewer: The RDRCaseViewer instance to use for prompting the user.
208
+ """
209
+ super().__init__(**kwargs)
210
+ self.user_prompt = UserPrompt(viewer)
211
+
212
+ def ask_for_conditions(self, case_query: CaseQuery,
213
+ last_evaluated_rule: Optional[Rule] = None) \
214
+ -> CallableExpression:
215
+ if not self.use_loaded_answers and self.user_prompt.viewer is None:
216
+ show_current_and_corner_cases(case_query.case, {case_query.attribute_name: case_query.target_value},
217
+ last_evaluated_rule=last_evaluated_rule)
218
+ return self._get_conditions(case_query)
219
+
220
+ def _get_conditions(self, case_query: CaseQuery) \
221
+ -> CallableExpression:
222
+ """
223
+ Ask the expert to provide the differentiating features between two cases or unique features for a case
224
+ that doesn't have a corner case to compare to.
225
+
226
+ :param case_query: The case query containing the case to classify.
227
+ :return: The differentiating features as new rule conditions.
228
+ """
229
+ user_input = None
230
+ if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
231
+ self.use_loaded_answers = False
232
+ if self.use_loaded_answers:
233
+ try:
234
+ loaded_scope, user_input = self.all_expert_answers.pop(0)
235
+ except IndexError:
236
+ self.use_loaded_answers = False
237
+ if user_input is not None:
238
+ condition = CallableExpression(user_input, bool, scope=case_query.scope)
239
+ else:
240
+ user_input, condition = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conditions)
241
+ if not self.use_loaded_answers:
242
+ self.all_expert_answers.append((condition.scope, user_input))
243
+ if self.answers_save_path is not None:
244
+ self.save_answers()
245
+ case_query.conditions = condition
246
+ return condition
247
+
248
+ def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
249
+ """
250
+ Ask the expert to provide a conclusion for the case.
251
+
252
+ :param case_query: The case query containing the case to find a conclusion for.
253
+ :return: The conclusion for the case as a callable expression.
254
+ """
255
+ expression: Optional[CallableExpression] = None
256
+ expert_input: Optional[str] = None
257
+ if self.use_loaded_answers and len(self.all_expert_answers) == 0 and self.append:
258
+ self.use_loaded_answers = False
259
+ if self.use_loaded_answers:
260
+ try:
261
+ loaded_scope, expert_input = self.all_expert_answers.pop(0)
262
+ if expert_input is not None:
263
+ expression = CallableExpression(expert_input, case_query.attribute_type,
264
+ scope=case_query.scope,
265
+ mutually_exclusive=case_query.mutually_exclusive)
266
+ except IndexError:
267
+ self.use_loaded_answers = False
268
+ if not self.use_loaded_answers:
269
+ if self.user_prompt.viewer is None:
270
+ show_current_and_corner_cases(case_query.case)
271
+ expert_input, expression = self.user_prompt.prompt_user_for_expression(case_query, PromptFor.Conclusion)
272
+ if expression is None:
273
+ self.all_expert_answers.append(({}, None))
274
+ else:
275
+ self.all_expert_answers.append((expression.scope, expert_input))
276
+ if self.answers_save_path is not None:
277
+ self.save_answers()
278
+ case_query.target = expression
279
+ return expression
280
+
281
+
282
+ class File(Expert):
283
+ """
284
+ The File Expert class, an expert that reads the answers from a file.
285
+ This is used for testing purposes.
286
+ """
287
+
288
+ def __init__(self, filename: str, **kwargs):
289
+ """
290
+ Initialize the File expert.
291
+
292
+ :param filename: The path to the file containing the expert answers.
293
+ """
294
+ super().__init__(**kwargs)
295
+ self.filename = filename
296
+ self.load_answers(filename)
297
+
298
+ def ask_for_conditions(self, case_query: CaseQuery,
299
+ last_evaluated_rule: Optional[Rule] = None) -> CallableExpression:
300
+ loaded_scope, user_input = self.all_expert_answers.pop(0)
301
+ if user_input:
302
+ condition = CallableExpression(user_input, bool, scope=case_query.scope)
303
+ else:
304
+ raise ValueError("No user input found in the expert answers file.")
305
+ case_query.conditions = condition
306
+ return condition
307
+
308
+ def ask_for_conclusion(self, case_query: CaseQuery) -> Optional[CallableExpression]:
309
+ loaded_scope, expert_input = self.all_expert_answers.pop(0)
310
+ if expert_input is not None:
311
+ expression = CallableExpression(expert_input, case_query.attribute_type,
312
+ scope=case_query.scope,
313
+ mutually_exclusive=case_query.mutually_exclusive)
314
+ else:
315
+ raise ValueError("No expert input found in the expert answers file.")
316
+ case_query.target = expression
317
+ return expression