ripple-down-rules 0.5.64__tar.gz → 0.5.75__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 (152) hide show
  1. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/PKG-INFO +2 -1
  2. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/requirements.txt +2 -1
  3. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/__init__.py +1 -1
  4. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/datastructures/dataclasses.py +61 -2
  5. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/helpers.py +8 -4
  6. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/rdr.py +110 -59
  7. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/rdr_decorators.py +7 -6
  8. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/rules.py +53 -9
  9. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/utils.py +71 -4
  10. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules.egg-info/PKG-INFO +2 -1
  11. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules.egg-info/SOURCES.txt +11 -2
  12. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules.egg-info/requires.txt +1 -0
  13. ripple_down_rules-0.5.75/test/conf/world/base_config.py +48 -0
  14. ripple_down_rules-0.5.75/test/conf/world/handles_and_containers.py +50 -0
  15. ripple_down_rules-0.5.75/test/conftest.py +112 -0
  16. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/datasets.py +82 -0
  17. ripple_down_rules-0.5.75/test/factories/world/__init__.py +0 -0
  18. ripple_down_rules-0.5.75/test/factories/world/handles_and_containers.py +22 -0
  19. ripple_down_rules-0.5.75/test/test_generated_rdrs/__init__.py +1 -0
  20. ripple_down_rules-0.5.75/test/test_helpers/__init__.py +0 -0
  21. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_helpers/helpers.py +21 -11
  22. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_on_mutagenic.py +12 -8
  23. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_rdr.py +47 -33
  24. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_rdr_helpers_rdrs.py +2 -8
  25. ripple_down_rules-0.5.75/test/test_rdr_world/__init__.py +0 -0
  26. ripple_down_rules-0.5.75/test/test_rdr_world/conftest.py +147 -0
  27. ripple_down_rules-0.5.75/test/test_rdr_world/test_rdr_world.py +52 -0
  28. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr.py +1 -1
  29. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_rdr.py +2 -2
  30. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_is_a_robot/rdr_metadata/datasets_physical_object_is_a_robot.json +4 -4
  31. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/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 +2 -2
  32. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/physical_object_select_objects_that_are_parts_of_robot_rdr.py +2 -2
  33. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/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 +6 -6
  34. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_template_file_creator.py +2 -2
  35. ripple_down_rules-0.5.75/test/test_user_interface/__init__.py +0 -0
  36. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_user_interface/test_prompt.py +1 -1
  37. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_user_interface/test_qt_gui_inline.py +3 -0
  38. ripple_down_rules-0.5.64/test/conftest.py +0 -23
  39. ripple_down_rules-0.5.64/test/test_rdr_world.py +0 -191
  40. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/.github/workflows/build_and_deploy_doc.yml +0 -0
  41. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/.github/workflows/ci.yml +0 -0
  42. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/.github/workflows/publish-to-test-pypi.yml +0 -0
  43. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]/shelved.patch +0 -0
  44. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]1/shelved.patch +0 -0
  45. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/LICENSE +0 -0
  46. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/README.md +0 -0
  47. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/_config.yml +0 -0
  48. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/_toc.yml +0 -0
  49. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/bibliography.md +0 -0
  50. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/intro.md +0 -0
  51. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/references.bib +0 -0
  52. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/doc/requirements.txt +0 -0
  53. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/__init__.py +0 -0
  54. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/animal_species.py +0 -0
  55. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/part_containment_rdr/__init__.py +0 -0
  56. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/part_containment_rdr/rdr_metadata/part_containment_rdr.json +0 -0
  57. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/part_containment_rdr/robot_contained_objects_mcrdr.py +0 -0
  58. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/part_containment_rdr/robot_contained_objects_mcrdr_defs.py +0 -0
  59. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/part_containment_rdr/robot_rdr.py +0 -0
  60. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/examples/relational_example.py +0 -0
  61. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/images/scrdr.dot +0 -0
  62. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/images/scrdr.png +0 -0
  63. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/images/thinking_pr2.jpg +0 -0
  64. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/pyproject.toml +0 -0
  65. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/pytest.ini +0 -0
  66. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/requirements-dev-ci.txt +0 -0
  67. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/requirements-dev.txt +0 -0
  68. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/requirements-gui.txt +0 -0
  69. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/requirements-viz.txt +0 -0
  70. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_extra.dot +0 -0
  71. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_extra.png +0 -0
  72. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_stop_only.dot +0 -0
  73. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_stop_only.png +0 -0
  74. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_stop_plus_rule.dot +0 -0
  75. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_mcrdr_stop_plus_rule.png +0 -0
  76. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr.dot +0 -0
  77. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr.png +0 -0
  78. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr_2.dot +0 -0
  79. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr_2.png +0 -0
  80. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr_3.dot +0 -0
  81. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/complete_scrdr_3.png +0 -0
  82. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/grdr_Habitat.dot +0 -0
  83. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/grdr_Habitat.png +0 -0
  84. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/grdr_Species.dot +0 -0
  85. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/grdr_Species.png +0 -0
  86. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_extra.dot +0 -0
  87. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_extra.png +0 -0
  88. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_extra_classify.dot +0 -0
  89. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_extra_classify.png +0 -0
  90. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_stop_plus_rule_combined.dot +0 -0
  91. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/mcrdr_stop_plus_rule_combined.png +0 -0
  92. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/partial_mcrdr_extra.dot +0 -0
  93. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/partial_mcrdr_extra.png +0 -0
  94. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/relational_scrdr_classify.dot +0 -0
  95. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/results/relational_scrdr_classify.png +0 -0
  96. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/setup.cfg +0 -0
  97. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  98. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/datastructures/callable_expression.py +0 -0
  99. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/datastructures/case.py +0 -0
  100. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/datastructures/enums.py +0 -0
  101. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/experts.py +0 -0
  102. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/start-code-server.sh +0 -0
  103. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/__init__.py +0 -0
  104. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/gui.py +0 -0
  105. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/ipython_custom_shell.py +0 -0
  106. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/object_diagram.py +0 -0
  107. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/prompt.py +0 -0
  108. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules/user_interface/template_file_creator.py +0 -0
  109. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  110. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  111. {ripple_down_rules-0.5.64/test/test_generated_rdrs → ripple_down_rules-0.5.75/test/conf}/__init__.py +0 -0
  112. {ripple_down_rules-0.5.64/test/test_helpers → ripple_down_rules-0.5.75/test/conf/world}/__init__.py +0 -0
  113. {ripple_down_rules-0.5.64/test/test_user_interface → ripple_down_rules-0.5.75/test/factories}/__init__.py +0 -0
  114. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/correct_drawer_rdr_expert_answers_fit.json +0 -0
  115. ripple_down_rules-0.5.64/test/test_expert_answers/view_rdr_expert_answers_fit.json → ripple_down_rules-0.5.75/test/test_expert_answers/drawer_cabinet_expert_answers_fit.json +0 -0
  116. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/grdr_expert_answers_classify.json +0 -0
  117. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/grdr_expert_answers_fit.json +0 -0
  118. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/grdr_expert_answers_fit_extra.json +0 -0
  119. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/grdr_expert_answers_fit_no_targets.json +0 -0
  120. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_expert_answers_classify.json +0 -0
  121. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_expert_answers_fit_no_targets.json +0 -0
  122. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_expert_answers_stop_only_fit.json +0 -0
  123. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_extra_expert_answers_classify.json +0 -0
  124. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_extra_expert_answers_fit.json +0 -0
  125. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_multi_line_expert_answers_fit.json +0 -0
  126. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_stop_only_answers_fit.json +0 -0
  127. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_stop_plus_rule_answers_fit.json +0 -0
  128. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_stop_plus_rule_combined_expert_answers_fit.json +0 -0
  129. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mcrdr_stop_plus_rule_expert_answers_fit.json +0 -0
  130. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/mutagenic_expert_answers.json +0 -0
  131. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/relational_scrdr_expert_answers_classify.json +0 -0
  132. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/scrdr_expert_answers_classify.json +0 -0
  133. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/scrdr_expert_answers_fit.json +0 -0
  134. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/scrdr_expert_answers_fit_no_targets.json +0 -0
  135. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/scrdr_multi_line_expert_answers_fit.json +0 -0
  136. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_expert_answers/scrdr_world_expert_answers_fit.json +0 -0
  137. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_helpers/object_diagram_case_query.png +0 -0
  138. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_helpers/object_diagram_person.png +0 -0
  139. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_json_serialization.py +0 -0
  140. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_object_diagram.py +0 -0
  141. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_rdr_alchemy.py +0 -0
  142. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_rdr_decorators.py +0 -0
  143. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_relational_rdr.py +0 -0
  144. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_relational_rdr_alchemy.py +0 -0
  145. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_is_a_robot/__init__.py +0 -0
  146. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr_defs.py +1 -1
  147. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/__init__.py +0 -0
  148. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/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 +3 -3
  149. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_sql_model.py +0 -0
  150. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_user_interface/test_ipython.py +0 -0
  151. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/test/test_user_interface/test_ipython_copilot.py +0 -0
  152. {ripple_down_rules-0.5.64 → ripple_down_rules-0.5.75}/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.5.64
3
+ Version: 0.5.75
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
@@ -694,6 +694,7 @@ Requires-Dist: pygments
694
694
  Requires-Dist: sqlalchemy
695
695
  Requires-Dist: pandas
696
696
  Requires-Dist: pyparsing>=3.2.3
697
+ Requires-Dist: omegaconf
697
698
  Provides-Extra: viz
698
699
  Requires-Dist: networkx>=3.1; extra == "viz"
699
700
  Requires-Dist: matplotlib>=3.7.5; extra == "viz"
@@ -10,4 +10,5 @@ colorama
10
10
  pygments
11
11
  sqlalchemy
12
12
  pandas
13
- pyparsing>=3.2.3
13
+ pyparsing>=3.2.3
14
+ omegaconf
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.64"
1
+ __version__ = "0.5.75"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -5,9 +5,11 @@ import typing
5
5
  from dataclasses import dataclass, field
6
6
 
7
7
  import typing_extensions
8
+ from omegaconf import MISSING
8
9
  from sqlalchemy.orm import DeclarativeBase as SQLTable
9
- from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set
10
+ from typing_extensions import Any, Optional, Dict, Type, Tuple, Union, List, get_origin, Set, Callable
10
11
 
12
+ from ..utils import get_method_name, get_function_import_data, get_function_representation
11
13
  from .callable_expression import CallableExpression
12
14
  from .case import create_case, Case
13
15
  from ..utils import copy_case, make_list, make_set, get_origin_and_args_from_type_hint, get_value_type_from_type_hint, \
@@ -37,6 +39,24 @@ class CaseQuery:
37
39
  """
38
40
  Whether the attribute can only take one value (i.e. True) or multiple values (i.e. False).
39
41
  """
42
+ case_factory: Optional[Callable[[], Any]] = None
43
+ """
44
+ The factory method that can be used to recreate the original case.
45
+ """
46
+ case_factory_idx: Optional[int] = None
47
+ """
48
+ This is used when the case factory is a list of cases, this index is used to select the case from the list.
49
+ """
50
+ case_conf: Optional[CaseConf] = None
51
+ """
52
+ The case configuration that is used to (re)create the original case, recommended to be used when you want to
53
+ the case to persist in the rule base, this would allow it to be used for merging with other similar conclusion RDRs.
54
+ """
55
+ scenario: Optional[Callable] = None
56
+ """
57
+ The executable scenario is the root callable that recreates the situation that the case is
58
+ created in, for example, when the case is created from a test function, this would be the test function itself.
59
+ """
40
60
  _target: Optional[CallableExpression] = None
41
61
  """
42
62
  The target expression of the attribute.
@@ -225,4 +245,43 @@ class CaseQuery:
225
245
  self.mutually_exclusive, _target=self.target, default_value=self.default_value,
226
246
  scope=self.scope, _case=copy_case(self.case), _target_value=self.target_value,
227
247
  conditions=self.conditions, is_function=self.is_function,
228
- function_args_type_hints=self.function_args_type_hints)
248
+ function_args_type_hints=self.function_args_type_hints,
249
+ case_factory=self.case_factory, case_factory_idx=self.case_factory_idx,
250
+ case_conf=self.case_conf, scenario=self.scenario)
251
+
252
+
253
+ @dataclass
254
+ class CaseConf:
255
+ factory_method: Callable[[Any], Any] = MISSING
256
+
257
+ def create(self) -> Any:
258
+ return self.factory_method()
259
+
260
+
261
+ @dataclass
262
+ class CaseFactoryMetaData:
263
+ factory_method: Optional[Callable[[Optional[CaseConf]], Any]] = None
264
+ factory_idx: Optional[int] = None
265
+ case_conf: Optional[CaseConf] = None
266
+ scenario: Optional[Callable] = None
267
+
268
+ @classmethod
269
+ def from_case_query(cls, case_query: CaseQuery) -> CaseFactoryMetaData:
270
+ return cls(factory_method=case_query.case_factory, factory_idx=case_query.case_factory_idx,
271
+ case_conf=case_query.case_conf, scenario=case_query.scenario)
272
+
273
+ def __repr__(self):
274
+ factory_method_repr = None
275
+ scenario_repr = None
276
+ if self.factory_method is not None:
277
+ factory_method_repr = get_function_representation(self.factory_method)
278
+ if self.scenario is not None:
279
+ scenario_repr = get_function_representation(self.scenario)
280
+ return (f"CaseFactoryMetaData("
281
+ f"factory_method={factory_method_repr}, "
282
+ f"factory_idx={self.factory_idx}, "
283
+ f"case_conf={self.case_conf},"
284
+ f" scenario={scenario_repr})")
285
+
286
+ def __str__(self):
287
+ return self.__repr__()
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import os
4
4
  from types import ModuleType
5
5
 
6
+ from ripple_down_rules.datastructures.dataclasses import CaseFactoryMetaData
7
+
6
8
  from .datastructures.case import create_case
7
9
  from .datastructures.dataclasses import CaseQuery
8
10
  from typing_extensions import Type, Optional, Callable, Any, Dict, TYPE_CHECKING, Union
@@ -15,7 +17,8 @@ if TYPE_CHECKING:
15
17
 
16
18
 
17
19
  def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDownRules]],
18
- case: Any, modify_original_case: bool = False) -> Dict[str, Any]:
20
+ case: Any, modify_original_case: bool = False,
21
+ case_query: Optional[CaseQuery] = None) -> Dict[str, Any]:
19
22
  """
20
23
  Classify a case by going through all classifiers and adding the categories that are classified,
21
24
  and then restarting the classification until no more categories can be added.
@@ -23,6 +26,7 @@ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDow
23
26
  :param classifiers_dict: A dictionary mapping conclusion types to the classifiers that produce them.
24
27
  :param case: The case to classify.
25
28
  :param modify_original_case: Whether to modify the original case or create a copy and modify it.
29
+ :param case_query: The case query to extract metadata from if needed.
26
30
  :return: The categories that the case belongs to.
27
31
  """
28
32
  conclusions = {}
@@ -31,7 +35,7 @@ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDow
31
35
  while True:
32
36
  new_conclusions = {}
33
37
  for attribute_name, rdr in classifiers_dict.items():
34
- pred_atts = rdr.classify(case_cp)
38
+ pred_atts = rdr.classify(case_cp, case_query=case_query)
35
39
  if pred_atts is None:
36
40
  continue
37
41
  if rdr.mutually_exclusive:
@@ -49,8 +53,8 @@ def general_rdr_classify(classifiers_dict: Dict[str, Union[ModuleType, RippleDow
49
53
  conclusions[attribute_name] = set()
50
54
  conclusions[attribute_name].update(pred_atts)
51
55
  if attribute_name in new_conclusions:
52
- case_query = CaseQuery(case_cp, attribute_name, rdr.conclusion_type, rdr.mutually_exclusive)
53
- update_case(case_query, new_conclusions)
56
+ temp_case_query = CaseQuery(case_cp, attribute_name, rdr.conclusion_type, rdr.mutually_exclusive)
57
+ update_case(temp_case_query, new_conclusions)
54
58
  if len(new_conclusions) == 0:
55
59
  break
56
60
  return conclusions
@@ -4,6 +4,8 @@ import copyreg
4
4
  import importlib
5
5
  import os
6
6
 
7
+ from ripple_down_rules.datastructures.dataclasses import CaseFactoryMetaData
8
+
7
9
  from . import logger
8
10
  import sys
9
11
  from abc import ABC, abstractmethod
@@ -37,7 +39,7 @@ except ImportError as e:
37
39
  from .utils import draw_tree, make_set, copy_case, \
38
40
  SubclassJSONSerializer, make_list, get_type_from_string, \
39
41
  is_conflicting, get_imports_from_scope, extract_function_source, extract_imports, get_full_class_name, \
40
- is_iterable, str_to_snake_case
42
+ is_iterable, str_to_snake_case, get_import_path_from_path, get_imports_from_types
41
43
 
42
44
 
43
45
  class RippleDownRules(SubclassJSONSerializer, ABC):
@@ -112,7 +114,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
112
114
  if not os.path.exists(save_dir + '/__init__.py'):
113
115
  os.makedirs(save_dir, exist_ok=True)
114
116
  with open(save_dir + '/__init__.py', 'w') as f:
115
- f.write("# This is an empty __init__.py file to make the directory a package.\n")
117
+ f.write("from . import *\n")
116
118
  if model_name is not None:
117
119
  self.model_name = model_name
118
120
  elif self.model_name is None:
@@ -136,7 +138,11 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
136
138
  model_dir = os.path.join(load_dir, model_name)
137
139
  json_file = os.path.join(model_dir, cls.metadata_folder, model_name)
138
140
  rdr = cls.from_json_file(json_file)
139
- rdr.update_from_python(model_dir)
141
+ try:
142
+ rdr.update_from_python(model_dir)
143
+ except (FileNotFoundError, ValueError) as e:
144
+ logger.warning(f"Could not load the python file for the model {model_name} from {model_dir}. "
145
+ f"Make sure the file exists and is valid.")
140
146
  rdr.save_dir = load_dir
141
147
  rdr.model_name = model_name
142
148
  return rdr
@@ -215,13 +221,15 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
215
221
  return self.classify(case)
216
222
 
217
223
  @abstractmethod
218
- def classify(self, case: Union[Case, SQLTable], modify_case: bool = False) \
224
+ def classify(self, case: Union[Case, SQLTable], modify_case: bool = False,
225
+ case_query: Optional[CaseQuery] = None) \
219
226
  -> Optional[Union[CallableExpression, Dict[str, CallableExpression]]]:
220
227
  """
221
228
  Classify a case.
222
229
 
223
230
  :param case: The case to classify.
224
231
  :param modify_case: Whether to modify the original case attributes with the conclusion or not.
232
+ :param case_query: The case query containing the case to classify and the target category to compare the case with.
225
233
  :return: The category that the case belongs to.
226
234
  """
227
235
  pass
@@ -229,6 +237,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
229
237
  def fit_case(self, case_query: CaseQuery,
230
238
  expert: Optional[Expert] = None,
231
239
  update_existing_rules: bool = True,
240
+ scenario: Optional[Callable] = None,
232
241
  **kwargs) \
233
242
  -> Union[CallableExpression, Dict[str, CallableExpression]]:
234
243
  """
@@ -239,6 +248,7 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
239
248
  :param expert: The expert to ask for differentiating features as new rule conditions.
240
249
  :param update_existing_rules: Whether to update the existing same conclusion type rules that already gave
241
250
  some conclusions with the type required by the case query.
251
+ :param scenario: The scenario at which the case was created, this is used to recreate the case if needed.
242
252
  :return: The category that the case belongs to.
243
253
  """
244
254
  if case_query is None:
@@ -247,14 +257,14 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
247
257
  self.name = case_query.attribute_name if self.name is None else self.name
248
258
  self.case_type = case_query.case_type if self.case_type is None else self.case_type
249
259
  self.case_name = case_query.case_name if self.case_name is None else self.case_name
260
+ case_query.scenario = scenario if case_query.scenario is None else case_query.scenario
250
261
 
251
262
  expert = expert or Human(viewer=self.viewer,
252
263
  answers_save_path=self.save_dir + '/expert_answers'
253
264
  if self.save_dir else None)
254
-
255
265
  if case_query.target is None:
256
266
  case_query_cp = copy(case_query)
257
- conclusions = self.classify(case_query_cp.case, modify_case=True)
267
+ conclusions = self.classify(case_query_cp.case, modify_case=True, case_query=case_query_cp)
258
268
  if self.should_i_ask_the_expert_for_a_target(conclusions, case_query_cp, update_existing_rules):
259
269
  expert.ask_for_conclusion(case_query_cp)
260
270
  case_query.target = case_query_cp.target
@@ -389,13 +399,11 @@ class RippleDownRules(SubclassJSONSerializer, ABC):
389
399
  :return: The module that contains the rdr classifier function.
390
400
  """
391
401
  # remove from imports if exists first
392
- name = f"{package_name.strip('./').replace('/', '.')}.{self.generated_python_file_name}"
393
- try:
394
- module = importlib.import_module(name)
395
- del sys.modules[name]
396
- except ModuleNotFoundError:
397
- pass
398
- return importlib.import_module(name).classify
402
+ package_name = get_import_path_from_path(package_name)
403
+ name = f"{package_name}.{self.generated_python_file_name}" if package_name else self.generated_python_file_name
404
+ module = importlib.import_module(name)
405
+ importlib.reload(module)
406
+ return module.classify
399
407
 
400
408
 
401
409
  class RDRWithCodeWriter(RippleDownRules, ABC):
@@ -411,6 +419,10 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
411
419
  conclusion_func_names = [f'conclusion_{rid}' for rid in rules_dict.keys() if not isinstance(rules_dict[rid], MultiClassStopRule)]
412
420
  all_func_names = condition_func_names + conclusion_func_names
413
421
  filepath = f"{model_dir}/{self.generated_python_defs_file_name}.py"
422
+ cases_path = f"{model_dir}/{self.generated_python_cases_file_name}.py"
423
+ cases_import_path = get_import_path_from_path(model_dir)
424
+ cases_import_path = f"{cases_import_path}.{self.generated_python_cases_file_name}" if cases_import_path\
425
+ else self.generated_python_cases_file_name
414
426
  functions_source = extract_function_source(filepath, all_func_names, include_signature=False)
415
427
  # get the scope from the imports in the file
416
428
  scope = extract_imports(filepath)
@@ -418,13 +430,17 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
418
430
  if rule.conditions is not None:
419
431
  rule.conditions.user_input = functions_source[f"conditions_{rule.uid}"]
420
432
  rule.conditions.scope = scope
433
+ if os.path.exists(cases_path):
434
+ module = importlib.import_module(cases_import_path)
435
+ importlib.reload(module)
436
+ rule.corner_case_metadata = module.__dict__.get(f"corner_case_{rule.uid}", None)
421
437
  if rule.conclusion is not None and not isinstance(rule, MultiClassStopRule):
422
438
  rule.conclusion.user_input = functions_source[f"conclusion_{rule.uid}"]
423
439
  rule.conclusion.scope = scope
424
440
 
425
441
  @abstractmethod
426
442
  def write_rules_as_source_code_to_file(self, rule: Rule, file, parent_indent: str = "",
427
- defs_file: Optional[str] = None):
443
+ defs_file: Optional[str] = None, cases_file: Optional[str] = None):
428
444
  """
429
445
  Write the rules as source code to a file.
430
446
 
@@ -432,6 +448,7 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
432
448
  :param file: The file to write the source code to.
433
449
  :param parent_indent: The indentation of the parent rule.
434
450
  :param defs_file: The file to write the definitions to.
451
+ :param cases_file: The file to write the cases to.
435
452
  """
436
453
  pass
437
454
 
@@ -444,14 +461,19 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
444
461
  os.makedirs(model_dir, exist_ok=True)
445
462
  if not os.path.exists(model_dir + '/__init__.py'):
446
463
  with open(model_dir + '/__init__.py', 'w') as f:
447
- f.write("# This is an empty __init__.py file to make the directory a package.\n")
448
- func_def = f"def classify(case: {self.case_type.__name__}) -> {self.conclusion_type_hint}:\n"
464
+ f.write("from . import *\n")
465
+ func_def = f"def classify(case: {self.case_type.__name__}, **kwargs) -> {self.conclusion_type_hint}:\n"
449
466
  file_name = model_dir + f"/{self.generated_python_file_name}.py"
450
467
  defs_file_name = model_dir + f"/{self.generated_python_defs_file_name}.py"
468
+ cases_file_name = model_dir + f"/{self.generated_python_cases_file_name}.py"
451
469
  imports, defs_imports = self._get_imports()
452
470
  # clear the files first
453
471
  with open(defs_file_name, "w") as f:
454
472
  f.write(defs_imports + "\n\n")
473
+ case_factory_import = get_imports_from_types([CaseFactoryMetaData])
474
+ with open(cases_file_name, "w") as cases_f:
475
+ cases_f.write("# This file contains the corner cases for the rules.\n")
476
+ cases_f.write('\n'.join(case_factory_import) + "\n\n\n")
455
477
  with open(file_name, "w") as f:
456
478
  imports += f"from .{self.generated_python_defs_file_name} import *\n"
457
479
  f.write(imports + "\n\n")
@@ -461,7 +483,8 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
461
483
  f.write(f"\n\n{func_def}")
462
484
  f.write(f"{' ' * 4}if not isinstance(case, Case):\n"
463
485
  f"{' ' * 4} case = create_case(case, max_recursion_idx=3)\n""")
464
- self.write_rules_as_source_code_to_file(self.start_rule, f, " " * 4, defs_file=defs_file_name)
486
+ self.write_rules_as_source_code_to_file(self.start_rule, file_name, " " * 4, defs_file=defs_file_name,
487
+ cases_file=cases_file_name)
465
488
 
466
489
  @property
467
490
  @abstractmethod
@@ -510,6 +533,10 @@ class RDRWithCodeWriter(RippleDownRules, ABC):
510
533
  def generated_python_defs_file_name(self) -> str:
511
534
  return f"{self.generated_python_file_name}_defs"
512
535
 
536
+ @property
537
+ def generated_python_cases_file_name(self) -> str:
538
+ return f"{self.generated_python_file_name}_cases"
539
+
513
540
 
514
541
  @property
515
542
  def conclusion_type(self) -> Tuple[Type]:
@@ -592,7 +619,7 @@ class SingleClassRDR(RDRWithCodeWriter):
592
619
  pred = self.evaluate(case_query.case)
593
620
  if pred.conclusion(case_query.case) != case_query.target_value:
594
621
  expert.ask_for_conditions(case_query, pred)
595
- pred.fit_rule(case_query.case, case_query.target, conditions=case_query.conditions)
622
+ pred.fit_rule(case_query)
596
623
 
597
624
  return self.classify(case_query.case)
598
625
 
@@ -605,18 +632,24 @@ class SingleClassRDR(RDRWithCodeWriter):
605
632
  """
606
633
  if not self.start_rule:
607
634
  expert.ask_for_conditions(case_query)
608
- self.start_rule = SingleClassRule(case_query.conditions, case_query.target, corner_case=case_query.case,
609
- conclusion_name=case_query.attribute_name)
635
+ self.start_rule = SingleClassRule.from_case_query(case_query)
610
636
 
611
- def classify(self, case: Case, modify_case: bool = False) -> Optional[Any]:
637
+ def classify(self, case: Case, modify_case: bool = False,
638
+ case_query: Optional[CaseQuery] = None) -> Optional[Any]:
612
639
  """
613
640
  Classify a case by recursively evaluating the rules until a rule fires or the last rule is reached.
614
641
 
615
642
  :param case: The case to classify.
616
643
  :param modify_case: Whether to modify the original case attributes with the conclusion or not.
644
+ :param case_query: The case query containing the case and the target category to compare the case with.
617
645
  """
618
646
  pred = self.evaluate(case)
619
- return pred.conclusion(case) if pred is not None and pred.fired else self.default_conclusion
647
+ conclusion = pred.conclusion(case) if pred is not None else None
648
+ if pred is not None and pred.fired and case_query is not None:
649
+ if pred.corner_case_metadata is None and conclusion is not None\
650
+ and type(conclusion) in case_query.core_attribute_type:
651
+ pred.corner_case_metadata = CaseFactoryMetaData.from_case_query(case_query)
652
+ return conclusion if pred is not None and pred.fired else self.default_conclusion
620
653
 
621
654
  def evaluate(self, case: Case) -> SingleClassRule:
622
655
  """
@@ -631,23 +664,27 @@ class SingleClassRDR(RDRWithCodeWriter):
631
664
  with open(model_dir + f"/{self.generated_python_file_name}.py", "a") as f:
632
665
  f.write(f"{' ' * 4}else:\n{' ' * 8}return {self.default_conclusion}\n")
633
666
 
634
- def write_rules_as_source_code_to_file(self, rule: SingleClassRule, file: TextIOWrapper, parent_indent: str = "",
635
- defs_file: Optional[str] = None):
667
+ def write_rules_as_source_code_to_file(self, rule: SingleClassRule, filename: str, parent_indent: str = "",
668
+ defs_file: Optional[str] = None, cases_file: Optional[str] = None):
636
669
  """
637
670
  Write the rules as source code to a file.
638
671
  """
639
672
  if rule.conditions:
673
+ rule.write_corner_case_as_source_code(cases_file)
640
674
  if_clause = rule.write_condition_as_source_code(parent_indent, defs_file)
641
- file.write(if_clause)
675
+ with open(filename, "a") as file:
676
+ file.write(if_clause)
642
677
  if rule.refinement:
643
- self.write_rules_as_source_code_to_file(rule.refinement, file, parent_indent + " ",
644
- defs_file=defs_file)
678
+ self.write_rules_as_source_code_to_file(rule.refinement, filename, parent_indent + " ",
679
+ defs_file=defs_file, cases_file=cases_file)
645
680
 
646
681
  conclusion_call = rule.write_conclusion_as_source_code(parent_indent, defs_file)
647
- file.write(conclusion_call)
682
+ with open(filename, "a") as file:
683
+ file.write(conclusion_call)
648
684
 
649
685
  if rule.alternative:
650
- self.write_rules_as_source_code_to_file(rule.alternative, file, parent_indent, defs_file=defs_file)
686
+ self.write_rules_as_source_code_to_file(rule.alternative, filename, parent_indent, defs_file=defs_file,
687
+ cases_file=cases_file)
651
688
 
652
689
  @property
653
690
  def conclusion_type_hint(self) -> str:
@@ -699,13 +736,19 @@ class MultiClassRDR(RDRWithCodeWriter):
699
736
  super(MultiClassRDR, self).__init__(start_rule, **kwargs)
700
737
  self.mode: MCRDRMode = mode
701
738
 
702
- def classify(self, case: Union[Case, SQLTable], modify_case: bool = False) -> Set[Any]:
739
+ def classify(self, case: Union[Case, SQLTable], modify_case: bool = False,
740
+ case_query: Optional[CaseQuery] = None) -> Set[Any]:
703
741
  evaluated_rule = self.start_rule
704
742
  self.conclusions = []
705
743
  while evaluated_rule:
706
744
  next_rule = evaluated_rule(case)
707
745
  if evaluated_rule.fired:
708
- self.add_conclusion(evaluated_rule, case)
746
+ rule_conclusion = evaluated_rule.conclusion(case)
747
+ if evaluated_rule.corner_case_metadata is None and case_query is not None:
748
+ if rule_conclusion is not None and len(make_list(rule_conclusion)) > 0\
749
+ and any(ct in case_query.core_attribute_type for ct in map(type, make_list(rule_conclusion))):
750
+ evaluated_rule.corner_case_metadata = CaseFactoryMetaData.from_case_query(case_query)
751
+ self.add_conclusion(rule_conclusion)
709
752
  evaluated_rule = next_rule
710
753
  return make_set(self.conclusions)
711
754
 
@@ -733,7 +776,7 @@ class MultiClassRDR(RDRWithCodeWriter):
733
776
  self.stop_wrong_conclusion_else_add_it(case_query, expert, evaluated_rule)
734
777
  else:
735
778
  # Rule fired and target is correct or there is no target to compare
736
- self.add_conclusion(evaluated_rule, case_query.case)
779
+ self.add_conclusion(rule_conclusion)
737
780
 
738
781
  if not next_rule:
739
782
  if not make_set(target_value).issubset(make_set(self.conclusions)):
@@ -745,24 +788,31 @@ class MultiClassRDR(RDRWithCodeWriter):
745
788
  return self.conclusions
746
789
 
747
790
  def write_rules_as_source_code_to_file(self, rule: Union[MultiClassTopRule, MultiClassStopRule],
748
- file, parent_indent: str = "", defs_file: Optional[str] = None):
791
+ filename: str, parent_indent: str = "", defs_file: Optional[str] = None,
792
+ cases_file: Optional[str] = None):
749
793
  if rule == self.start_rule:
750
- file.write(f"{parent_indent}conclusions = set()\n")
794
+ with open(filename, "a") as file:
795
+ file.write(f"{parent_indent}conclusions = set()\n")
751
796
  if rule.conditions:
797
+ rule.write_corner_case_as_source_code(cases_file)
752
798
  if_clause = rule.write_condition_as_source_code(parent_indent, defs_file)
753
- file.write(if_clause)
799
+ with open(filename, "a") as file:
800
+ file.write(if_clause)
754
801
  conclusion_indent = parent_indent
755
802
  if hasattr(rule, "refinement") and rule.refinement:
756
- self.write_rules_as_source_code_to_file(rule.refinement, file, parent_indent + " ",
757
- defs_file=defs_file)
803
+ self.write_rules_as_source_code_to_file(rule.refinement, filename, parent_indent + " ",
804
+ defs_file=defs_file, cases_file=cases_file)
758
805
  conclusion_indent = parent_indent + " " * 4
759
- file.write(f"{conclusion_indent}else:\n")
806
+ with open(filename, "a") as file:
807
+ file.write(f"{conclusion_indent}else:\n")
760
808
 
761
809
  conclusion_call = rule.write_conclusion_as_source_code(conclusion_indent, defs_file)
762
- file.write(conclusion_call)
810
+ with open(filename, "a") as file:
811
+ file.write(conclusion_call)
763
812
 
764
813
  if rule.alternative:
765
- self.write_rules_as_source_code_to_file(rule.alternative, file, parent_indent, defs_file=defs_file)
814
+ self.write_rules_as_source_code_to_file(rule.alternative, filename, parent_indent, defs_file=defs_file,
815
+ cases_file=cases_file)
766
816
 
767
817
  @property
768
818
  def conclusion_type_hint(self) -> str:
@@ -788,8 +838,7 @@ class MultiClassRDR(RDRWithCodeWriter):
788
838
  """
789
839
  if not self.start_rule:
790
840
  conditions = expert.ask_for_conditions(case_query)
791
- self.start_rule = MultiClassTopRule(conditions, case_query.target, corner_case=case_query.case,
792
- conclusion_name=case_query.attribute_name)
841
+ self.start_rule = MultiClassTopRule.from_case_query(case_query)
793
842
 
794
843
  @property
795
844
  def last_top_rule(self) -> Optional[MultiClassTopRule]:
@@ -810,7 +859,7 @@ class MultiClassRDR(RDRWithCodeWriter):
810
859
  if is_conflicting(rule_conclusion, case_query.target_value):
811
860
  self.stop_conclusion(case_query, expert, evaluated_rule)
812
861
  else:
813
- self.add_conclusion(evaluated_rule, case_query.case)
862
+ self.add_conclusion(rule_conclusion)
814
863
 
815
864
  def stop_conclusion(self, case_query: CaseQuery,
816
865
  expert: Expert, evaluated_rule: MultiClassTopRule):
@@ -822,12 +871,13 @@ class MultiClassRDR(RDRWithCodeWriter):
822
871
  :param evaluated_rule: The evaluated rule to ask the expert about.
823
872
  """
824
873
  conditions = expert.ask_for_conditions(case_query, evaluated_rule)
825
- evaluated_rule.fit_rule(case_query.case, case_query.target, conditions=conditions)
874
+ evaluated_rule.fit_rule(case_query)
826
875
  if self.mode == MCRDRMode.StopPlusRule:
827
876
  self.stop_rule_conditions = conditions
828
877
  if self.mode == MCRDRMode.StopPlusRuleCombined:
829
878
  new_top_rule_conditions = conditions.combine_with(evaluated_rule.conditions)
830
- self.add_top_rule(new_top_rule_conditions, case_query.target, case_query.case)
879
+ case_query.conditions = new_top_rule_conditions
880
+ self.add_top_rule(case_query)
831
881
 
832
882
  def add_rule_for_case(self, case_query: CaseQuery, expert: Expert):
833
883
  """
@@ -839,19 +889,19 @@ class MultiClassRDR(RDRWithCodeWriter):
839
889
  if self.stop_rule_conditions and self.mode == MCRDRMode.StopPlusRule:
840
890
  conditions = self.stop_rule_conditions
841
891
  self.stop_rule_conditions = None
892
+ case_query.conditions = conditions
842
893
  else:
843
894
  conditions = expert.ask_for_conditions(case_query)
844
- self.add_top_rule(conditions, case_query.target, case_query.case)
895
+ self.add_top_rule(case_query)
845
896
 
846
- def add_conclusion(self, evaluated_rule: Rule, case: Case) -> None:
897
+ def add_conclusion(self, rule_conclusion: List[Any]) -> None:
847
898
  """
848
899
  Add the conclusion of the evaluated rule to the list of conclusions.
849
900
 
850
- :param evaluated_rule: The evaluated rule to add the conclusion of.
851
- :param case: The case to add the conclusion for.
901
+ :param rule_conclusion: The conclusion of the evaluated rule, which can be a single conclusion
902
+ or a set of conclusions.
852
903
  """
853
904
  conclusion_types = [type(c) for c in self.conclusions]
854
- rule_conclusion = evaluated_rule.conclusion(case)
855
905
  if type(rule_conclusion) not in conclusion_types:
856
906
  self.conclusions.extend(make_list(rule_conclusion))
857
907
  else:
@@ -864,15 +914,13 @@ class MultiClassRDR(RDRWithCodeWriter):
864
914
  self.conclusions.remove(c)
865
915
  self.conclusions.extend(make_list(combined_conclusion))
866
916
 
867
- def add_top_rule(self, conditions: CallableExpression, conclusion: Any, corner_case: Union[Case, SQLTable]):
917
+ def add_top_rule(self, case_query: CaseQuery):
868
918
  """
869
919
  Add a top rule to the classifier, which is a rule that is always checked and is part of the start_rules list.
870
920
 
871
- :param conditions: The conditions of the rule.
872
- :param conclusion: The conclusion of the rule.
873
- :param corner_case: The corner case of the rule.
921
+ :param case_query: The case query to add the top rule for.
874
922
  """
875
- self.start_rule.alternative = MultiClassTopRule(conditions, conclusion, corner_case=corner_case)
923
+ self.start_rule.alternative = MultiClassTopRule.from_case_query(case_query)
876
924
 
877
925
  @staticmethod
878
926
  def start_rule_type() -> Type[Rule]:
@@ -933,16 +981,19 @@ class GeneralRDR(RippleDownRules):
933
981
  def start_rules(self) -> List[Union[SingleClassRule, MultiClassTopRule]]:
934
982
  return [rdr.start_rule for rdr in self.start_rules_dict.values()]
935
983
 
936
- def classify(self, case: Any, modify_case: bool = False) -> Optional[Dict[str, Any]]:
984
+ def classify(self, case: Any, modify_case: bool = False,
985
+ case_query: Optional[CaseQuery] = None) -> Optional[Dict[str, Any]]:
937
986
  """
938
987
  Classify a case by going through all RDRs and adding the categories that are classified, and then restarting
939
988
  the classification until no more categories can be added.
940
989
 
941
990
  :param case: The case to classify.
942
991
  :param modify_case: Whether to modify the original case or create a copy and modify it.
992
+ :param case_query: The case query containing the case and the target category to compare the case with.
943
993
  :return: The categories that the case belongs to.
944
994
  """
945
- return general_rdr_classify(self.start_rules_dict, case, modify_original_case=modify_case)
995
+ return general_rdr_classify(self.start_rules_dict, case, modify_original_case=modify_case,
996
+ case_query=case_query)
946
997
 
947
998
  def _fit_case(self, case_query: CaseQuery, expert: Optional[Expert] = None, **kwargs) \
948
999
  -> Dict[str, Any]:
@@ -1029,7 +1080,7 @@ class GeneralRDR(RippleDownRules):
1029
1080
  """
1030
1081
  for rdr in self.start_rules_dict.values():
1031
1082
  rdr._write_to_python(model_dir)
1032
- func_def = f"def classify(case: {self.case_type.__name__}) -> {self.conclusion_type_hint}:\n"
1083
+ func_def = f"def classify(case: {self.case_type.__name__}, **kwargs) -> {self.conclusion_type_hint}:\n"
1033
1084
  with open(model_dir + f"/{self.generated_python_file_name}.py", "w") as f:
1034
1085
  f.write(self._get_imports() + "\n\n")
1035
1086
  f.write("classifiers_dict = dict()\n")
@@ -1039,7 +1090,7 @@ class GeneralRDR(RippleDownRules):
1039
1090
  f.write(func_def)
1040
1091
  f.write(f"{' ' * 4}if not isinstance(case, Case):\n"
1041
1092
  f"{' ' * 4} case = create_case(case, max_recursion_idx=3)\n""")
1042
- f.write(f"{' ' * 4}return general_rdr_classify(classifiers_dict, case)\n")
1093
+ f.write(f"{' ' * 4}return general_rdr_classify(classifiers_dict, case, **kwargs)\n")
1043
1094
 
1044
1095
  @property
1045
1096
  def _default_generated_python_file_name(self) -> Optional[str]:
@@ -6,17 +6,18 @@ of the RDRs.
6
6
  import os.path
7
7
  from functools import wraps
8
8
 
9
- from pyparsing.tools.cvt_pyparsing_pep8_names import camel_to_snake
10
9
  from typing_extensions import Callable, Optional, Type, Tuple, Dict, Any, Self, get_type_hints, List, Union, Sequence
11
10
 
12
- from ripple_down_rules.datastructures.case import create_case, Case
11
+ from ripple_down_rules.datastructures.case import Case
13
12
  from ripple_down_rules.datastructures.dataclasses import CaseQuery
14
- from ripple_down_rules.datastructures.enums import Category
15
13
  from ripple_down_rules.experts import Expert, Human
16
- from ripple_down_rules.rdr import GeneralRDR, RippleDownRules
17
- from ripple_down_rules.user_interface.gui import RDRCaseViewer
14
+ from ripple_down_rules.rdr import GeneralRDR
15
+ try:
16
+ from ripple_down_rules.user_interface.gui import RDRCaseViewer
17
+ except ImportError:
18
+ RDRCaseViewer = None
18
19
  from ripple_down_rules.utils import get_method_args_as_dict, get_func_rdr_model_name, make_set, \
19
- get_method_class_if_exists, get_method_name, str_to_snake_case
20
+ get_method_class_if_exists, str_to_snake_case
20
21
 
21
22
 
22
23
  class RDRDecorator: