ripple-down-rules 0.4.88__tar.gz → 0.5.1__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 (127) hide show
  1. ripple_down_rules-0.5.1/.github/workflows/ci.yml +41 -0
  2. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/PKG-INFO +3 -2
  3. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/README.md +1 -1
  4. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/examples/readme_example.py +1 -1
  5. ripple_down_rules-0.5.1/requirements-dev-ci.txt +4 -0
  6. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/requirements.txt +2 -1
  7. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/__init__.py +1 -1
  8. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/datastructures/callable_expression.py +20 -1
  9. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/datastructures/dataclasses.py +9 -1
  10. ripple_down_rules-0.5.1/src/ripple_down_rules/experts.py +317 -0
  11. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/rdr.py +162 -76
  12. ripple_down_rules-0.5.1/src/ripple_down_rules/rdr_decorators.py +160 -0
  13. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/rules.py +5 -4
  14. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/template_file_creator.py +6 -6
  15. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/utils.py +3 -6
  16. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules.egg-info/PKG-INFO +3 -2
  17. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules.egg-info/SOURCES.txt +13 -5
  18. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules.egg-info/requires.txt +1 -0
  19. {ripple_down_rules-0.4.88/src/ripple_down_rules → ripple_down_rules-0.5.1/test}/datasets.py +6 -12
  20. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_helpers/helpers.py +2 -2
  21. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_json_serialization.py +8 -10
  22. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_object_diagram.py +1 -1
  23. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_on_mutagenic.py +5 -5
  24. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_rdr.py +84 -34
  25. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_rdr_alchemy.py +2 -2
  26. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_rdr_decorators.py +1 -1
  27. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_rdr_world.py +20 -12
  28. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_relational_rdr.py +2 -3
  29. ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_is_a_robot/__init__.py +1 -0
  30. ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr.py +16 -0
  31. ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_output__scrdr_defs.py +19 -0
  32. ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_is_a_robot/physical_object_is_a_robot_rdr.py +15 -0
  33. {ripple_down_rules-0.4.88/test/test_results → ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_is_a_robot/rdr_metadata}/datasets_physical_object_is_a_robot.json +18 -13
  34. ripple_down_rules-0.5.1/test/test_results/datasets_physical_object_select_objects_that_are_parts_of_robot/__init__.py +1 -0
  35. ripple_down_rules-0.5.1/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
  36. ripple_down_rules-0.5.1/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
  37. ripple_down_rules-0.5.1/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
  38. {ripple_down_rules-0.4.88/test/test_results → ripple_down_rules-0.5.1/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 +23 -17
  39. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_sql_model.py +1 -1
  40. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_template_file_creator.py +29 -0
  41. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_user_interface/test_qt_gui_inline.py +13 -7
  42. ripple_down_rules-0.4.88/src/ripple_down_rules/experts.py +0 -160
  43. ripple_down_rules-0.4.88/src/ripple_down_rules/rdr_decorators.py +0 -139
  44. ripple_down_rules-0.4.88/test/test_user_interface/test_pdb.py +0 -37
  45. ripple_down_rules-0.4.88/test/test_user_interface/test_qt_gui.py +0 -333
  46. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/.github/workflows/publish-to-test-pypi.yml +0 -0
  47. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]/shelved.patch +0 -0
  48. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/.idea/shelf/Uncommitted_changes_before_Checkout_at_2_4_25,_6_32_PM_[Changes]1/shelved.patch +0 -0
  49. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/LICENSE +0 -0
  50. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/images/thinking_pr2.jpg +0 -0
  51. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/pyproject.toml +0 -0
  52. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/pytest.ini +0 -0
  53. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/requirements-dev.txt +0 -0
  54. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/requirements-gui.txt +0 -0
  55. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/requirements-viz.txt +0 -0
  56. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_extra.dot +0 -0
  57. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_extra.png +0 -0
  58. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_stop_only.dot +0 -0
  59. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_stop_only.png +0 -0
  60. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_stop_plus_rule.dot +0 -0
  61. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_mcrdr_stop_plus_rule.png +0 -0
  62. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr.dot +0 -0
  63. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr.png +0 -0
  64. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr_2.dot +0 -0
  65. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr_2.png +0 -0
  66. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr_3.dot +0 -0
  67. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/complete_scrdr_3.png +0 -0
  68. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/grdr_Habitat.dot +0 -0
  69. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/grdr_Habitat.png +0 -0
  70. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/grdr_Species.dot +0 -0
  71. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/grdr_Species.png +0 -0
  72. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_extra.dot +0 -0
  73. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_extra.png +0 -0
  74. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_extra_classify.dot +0 -0
  75. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_extra_classify.png +0 -0
  76. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_stop_plus_rule_combined.dot +0 -0
  77. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/mcrdr_stop_plus_rule_combined.png +0 -0
  78. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/partial_mcrdr_extra.dot +0 -0
  79. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/partial_mcrdr_extra.png +0 -0
  80. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/relational_scrdr_classify.dot +0 -0
  81. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/results/relational_scrdr_classify.png +0 -0
  82. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/setup.cfg +0 -0
  83. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/datastructures/__init__.py +0 -0
  84. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/datastructures/case.py +0 -0
  85. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/datastructures/enums.py +0 -0
  86. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/failures.py +0 -0
  87. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/helpers.py +0 -0
  88. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/start-code-server.sh +0 -0
  89. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/__init__.py +0 -0
  90. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/gui.py +0 -0
  91. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/ipython_custom_shell.py +0 -0
  92. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/object_diagram.py +0 -0
  93. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules/user_interface/prompt.py +0 -0
  94. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules.egg-info/dependency_links.txt +0 -0
  95. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/src/ripple_down_rules.egg-info/top_level.txt +0 -0
  96. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/correct_drawer_rdr_expert_answers_fit.json +0 -0
  97. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/grdr_expert_answers_classify.json +0 -0
  98. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/grdr_expert_answers_fit.json +0 -0
  99. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/grdr_expert_answers_fit_extra.json +0 -0
  100. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/grdr_expert_answers_fit_no_targets.json +0 -0
  101. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_expert_answers_classify.json +0 -0
  102. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_expert_answers_fit_no_targets.json +0 -0
  103. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_expert_answers_stop_only_fit.json +0 -0
  104. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_extra_expert_answers_classify.json +0 -0
  105. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_extra_expert_answers_fit.json +0 -0
  106. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_multi_line_expert_answers_fit.json +0 -0
  107. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_stop_only_answers_fit.json +0 -0
  108. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_stop_plus_rule_answers_fit.json +0 -0
  109. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_stop_plus_rule_combined_expert_answers_fit.json +0 -0
  110. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mcrdr_stop_plus_rule_expert_answers_fit.json +0 -0
  111. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/mutagenic_expert_answers.json +0 -0
  112. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/relational_scrdr_expert_answers_classify.json +0 -0
  113. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/scrdr_expert_answers_classify.json +0 -0
  114. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/scrdr_expert_answers_fit.json +0 -0
  115. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/scrdr_expert_answers_fit_no_targets.json +0 -0
  116. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/scrdr_multi_line_expert_answers_fit.json +0 -0
  117. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/scrdr_world_expert_answers_fit.json +0 -0
  118. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_expert_answers/view_rdr_expert_answers_fit.json +0 -0
  119. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_generated_rdrs/__init__.py +0 -0
  120. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_helpers/__init__.py +0 -0
  121. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_helpers/object_diagram_case_query.png +0 -0
  122. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_helpers/object_diagram_person.png +0 -0
  123. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_relational_rdr_alchemy.py +0 -0
  124. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_user_interface/__init__.py +0 -0
  125. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_user_interface/test_ipython.py +0 -0
  126. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_user_interface/test_ipython_copilot.py +0 -0
  127. {ripple_down_rules-0.4.88 → ripple_down_rules-0.5.1}/test/test_utils.py +0 -0
@@ -0,0 +1,41 @@
1
+ # .github/workflows/ci.yml
2
+ name: CI
3
+
4
+ on:
5
+ push:
6
+ branches: [ main ]
7
+ pull_request:
8
+ branches: [ main ]
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ['3.10']
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v4
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+
25
+ - name: Install dependencies
26
+ run: |
27
+ sudo apt-get update
28
+ sudo apt install graphviz graphviz-dev
29
+ python -m pip install --upgrade pip
30
+ pip install virtualenv
31
+ python -m virtualenv venv
32
+ source venv/bin/activate
33
+ pip install -r requirements-dev-ci.txt
34
+ pip install pytest mypy flake8 black isort
35
+ pip install .
36
+
37
+ - name: Run tests
38
+ run: |
39
+ source venv/bin/activate
40
+ cd test
41
+ pytest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripple_down_rules
3
- Version: 0.4.88
3
+ Version: 0.5.1
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,6 +693,7 @@ Requires-Dist: colorama
693
693
  Requires-Dist: pygments
694
694
  Requires-Dist: sqlalchemy
695
695
  Requires-Dist: pandas
696
+ Requires-Dist: pyparsing
696
697
  Provides-Extra: viz
697
698
  Requires-Dist: networkx>=3.1; extra == "viz"
698
699
  Requires-Dist: matplotlib>=3.7.5; extra == "viz"
@@ -735,7 +736,7 @@ and render the tree to a file:
735
736
  ```python
736
737
  from ripple_down_rules.datastructures.dataclasses import CaseQuery
737
738
  from ripple_down_rules.rdr import SingleClassRDR
738
- from ripple_down_rules.datasets import load_zoo_dataset
739
+ from datasets import load_zoo_dataset
739
740
  from ripple_down_rules.utils import render_tree
740
741
 
741
742
  all_cases, targets = load_zoo_dataset()
@@ -32,7 +32,7 @@ and render the tree to a file:
32
32
  ```python
33
33
  from ripple_down_rules.datastructures.dataclasses import CaseQuery
34
34
  from ripple_down_rules.rdr import SingleClassRDR
35
- from ripple_down_rules.datasets import load_zoo_dataset
35
+ from datasets import load_zoo_dataset
36
36
  from ripple_down_rules.utils import render_tree
37
37
 
38
38
  all_cases, targets = load_zoo_dataset()
@@ -1,6 +1,6 @@
1
1
  from ripple_down_rules.datastructures.dataclasses import CaseQuery
2
2
  from ripple_down_rules.rdr import SingleClassRDR
3
- from ripple_down_rules.datasets import load_zoo_dataset
3
+ from datasets import load_zoo_dataset
4
4
  from ripple_down_rules.utils import render_tree
5
5
 
6
6
  all_cases, targets = load_zoo_dataset()
@@ -0,0 +1,4 @@
1
+ -r requirements.txt
2
+ ucimlrepo>=0.0.7
3
+ pdbpp
4
+ pytest
@@ -9,4 +9,5 @@ requests
9
9
  colorama
10
10
  pygments
11
11
  sqlalchemy
12
- pandas
12
+ pandas
13
+ pyparsing
@@ -1,4 +1,4 @@
1
- __version__ = "0.4.88"
1
+ __version__ = "0.5.1"
2
2
 
3
3
  import logging
4
4
  logger = logging.Logger("rdr")
@@ -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
  """
@@ -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