ethyca-fides 2.68.1b0__py2.py3-none-any.whl → 2.68.1b1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ethyca-fides might be problematic. Click here for more details.

Files changed (106) hide show
  1. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/RECORD +106 -104
  3. fides/_version.py +3 -3
  4. fides/api/api/deps.py +2 -0
  5. fides/api/db/ctl_session.py +3 -0
  6. fides/api/db/session.py +2 -1
  7. fides/api/models/privacy_request/privacy_request.py +15 -0
  8. fides/api/schemas/application_config.py +2 -1
  9. fides/api/schemas/privacy_center_config.py +15 -0
  10. fides/api/task/conditional_dependencies/evaluator.py +192 -45
  11. fides/api/task/conditional_dependencies/logging_utils.py +196 -0
  12. fides/api/task/conditional_dependencies/operators.py +8 -2
  13. fides/api/task/conditional_dependencies/schemas.py +25 -1
  14. fides/api/task/graph_task.py +9 -2
  15. fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
  16. fides/api/task/manual/manual_task_graph_task.py +213 -119
  17. fides/api/task/manual/manual_task_utils.py +0 -4
  18. fides/api/tasks/__init__.py +1 -0
  19. fides/config/database_settings.py +10 -1
  20. fides/ui-build/static/admin/404.html +1 -1
  21. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  22. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  23. fides/ui-build/static/admin/add-systems.html +1 -1
  24. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  25. fides/ui-build/static/admin/consent/configure.html +1 -1
  26. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  27. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  28. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  29. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  30. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  31. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  32. fides/ui-build/static/admin/consent/properties.html +1 -1
  33. fides/ui-build/static/admin/consent/reporting.html +1 -1
  34. fides/ui-build/static/admin/consent.html +1 -1
  35. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  36. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  37. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  38. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  39. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  40. fides/ui-build/static/admin/data-catalog.html +1 -1
  41. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  42. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  43. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  44. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  45. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  46. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  47. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  48. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  49. fides/ui-build/static/admin/datamap.html +1 -1
  50. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  51. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  52. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  53. fides/ui-build/static/admin/dataset/new.html +1 -1
  54. fides/ui-build/static/admin/dataset.html +1 -1
  55. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  56. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  57. fides/ui-build/static/admin/datastore-connection.html +1 -1
  58. fides/ui-build/static/admin/index.html +1 -1
  59. fides/ui-build/static/admin/integrations/[id].html +1 -1
  60. fides/ui-build/static/admin/integrations.html +1 -1
  61. fides/ui-build/static/admin/login/[provider].html +1 -1
  62. fides/ui-build/static/admin/login.html +1 -1
  63. fides/ui-build/static/admin/messaging/[id].html +1 -1
  64. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  65. fides/ui-build/static/admin/messaging.html +1 -1
  66. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  67. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  68. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  69. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  70. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  71. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  72. fides/ui-build/static/admin/poc/forms.html +1 -1
  73. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  74. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  75. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  76. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  77. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  78. fides/ui-build/static/admin/privacy-requests.html +1 -1
  79. fides/ui-build/static/admin/properties/[id].html +1 -1
  80. fides/ui-build/static/admin/properties/add-property.html +1 -1
  81. fides/ui-build/static/admin/properties.html +1 -1
  82. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  83. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  84. fides/ui-build/static/admin/settings/about.html +1 -1
  85. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  86. fides/ui-build/static/admin/settings/consent.html +1 -1
  87. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  88. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  89. fides/ui-build/static/admin/settings/domains.html +1 -1
  90. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  91. fides/ui-build/static/admin/settings/locations.html +1 -1
  92. fides/ui-build/static/admin/settings/organization.html +1 -1
  93. fides/ui-build/static/admin/settings/regulations.html +1 -1
  94. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  95. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  96. fides/ui-build/static/admin/systems.html +1 -1
  97. fides/ui-build/static/admin/taxonomy.html +1 -1
  98. fides/ui-build/static/admin/user-management/new.html +1 -1
  99. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  100. fides/ui-build/static/admin/user-management.html +1 -1
  101. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/WHEEL +0 -0
  102. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/entry_points.txt +0 -0
  103. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/licenses/LICENSE +0 -0
  104. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b1.dist-info}/top_level.txt +0 -0
  105. /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → tzF4yti8NslASlGnxnZ8m}/_buildManifest.js +0 -0
  106. /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → tzF4yti8NslASlGnxnZ8m}/_ssgManifest.js +0 -0
@@ -1,15 +1,20 @@
1
- from typing import Any, Union
1
+ from typing import Any, Optional, Union
2
2
 
3
3
  from loguru import logger
4
4
  from sqlalchemy.orm import Session
5
5
 
6
6
  from fides.api.graph.config import FieldPath
7
- from fides.api.task.conditional_dependencies.operators import operator_methods
7
+ from fides.api.task.conditional_dependencies.operators import (
8
+ LOGICAL_OPERATORS,
9
+ OPERATOR_METHODS,
10
+ )
8
11
  from fides.api.task.conditional_dependencies.schemas import (
9
12
  Condition,
13
+ ConditionEvaluationResult,
10
14
  ConditionGroup,
11
15
  ConditionLeaf,
12
- GroupOperator,
16
+ EvaluationResult,
17
+ GroupEvaluationResult,
13
18
  Operator,
14
19
  )
15
20
 
@@ -19,84 +24,226 @@ class ConditionEvaluationError(Exception):
19
24
 
20
25
 
21
26
  class ConditionEvaluator:
22
- """Evaluates nested conditions for manual task creation"""
27
+ """Evaluates nested conditions and returns a boolean result and a detailed evaluation report"""
23
28
 
24
29
  def __init__(self, db: Session):
25
30
  self.db = db
26
31
 
27
- def evaluate_rule(self, rule: Condition, data: Union[dict, Any]) -> bool:
28
- """Evaluate a nested condition rule against input data"""
32
+ def evaluate_rule(
33
+ self, rule: Condition, data: Union[dict, Any]
34
+ ) -> EvaluationResult:
35
+ """Evaluate a nested condition rule against input data and return detailed results
36
+
37
+ Args:
38
+ rule: The condition rule to evaluate
39
+ data: The data to evaluate the condition against
40
+
41
+ Returns:
42
+ evaluation report: A detailed report of the evaluation
43
+ - The field address of the condition
44
+ - The operator used in the condition
45
+ - The expected value of the condition
46
+ - The actual value of the condition
47
+ - The result of the condition evaluation
48
+ - A message describing the condition evaluation
49
+ """
29
50
  if isinstance(rule, ConditionLeaf):
30
- return self._evaluate_leaf_condition(rule, data)
51
+ leaf_result = self._evaluate_leaf_condition(rule, data)
52
+ return leaf_result
31
53
  # ConditionGroup
32
- return self._evaluate_group_condition(rule, data)
54
+ group_result = self._evaluate_group_condition(rule, data)
55
+ return group_result
33
56
 
34
57
  def _evaluate_leaf_condition(
35
58
  self, condition: ConditionLeaf, data: Union[dict, Any]
36
- ) -> bool:
37
- """Evaluate a leaf condition against input data"""
38
- data_value = self._get_nested_value(data, condition.field_address.split("."))
39
- # Apply operator and return result
40
- return self._apply_operator(data_value, condition.operator, condition.value)
59
+ ) -> ConditionEvaluationResult:
60
+ """Evaluate a leaf condition against input data
61
+
62
+ Args:
63
+ condition: The leaf condition to evaluate
64
+ data: The data to evaluate the condition against
65
+
66
+ Returns:
67
+ A detailed evaluation report for the leaf condition
68
+
69
+ Raises:
70
+ ConditionEvaluationError: If there is an issue applying the operator or if an unexpected error occurs.
71
+ """
72
+ # Handle both colon-separated and dot-separated field addresses
73
+ if ":" in condition.field_address:
74
+ # Full field address like "dataset:collection:field" - split on colons
75
+ keys = condition.field_address.split(":")
76
+ else:
77
+ # Relative field path like "field.subfield" - split on dots
78
+ keys = condition.field_address.split(".")
79
+
80
+ data_value = self._get_nested_value(data, keys)
81
+
82
+ # Apply operator and get result
83
+ try:
84
+ result = self._apply_operator(
85
+ data_value, condition.operator, condition.value
86
+ )
87
+ message = f"Condition '{condition.field_address} {condition.operator} {condition.value}' evaluated to {result}"
88
+ except ConditionEvaluationError as e:
89
+ logger.error(
90
+ f"Unexpected error evaluating condition '{condition.field_address} {condition.operator} {condition.value}': {str(e)}"
91
+ )
92
+ raise
93
+
94
+ return ConditionEvaluationResult(
95
+ field_address=condition.field_address,
96
+ operator=condition.operator,
97
+ expected_value=condition.value,
98
+ actual_value=data_value,
99
+ result=result,
100
+ message=message,
101
+ )
41
102
 
42
103
  def _evaluate_group_condition(
43
104
  self, group: ConditionGroup, data: Union[dict, Any]
44
- ) -> bool:
45
- """Evaluate a group condition against input data"""
105
+ ) -> GroupEvaluationResult:
106
+ """Evaluate a group condition against input data
107
+
108
+ Args:
109
+ group: The group condition to evaluate
110
+ data: The data to evaluate the condition against
111
+
112
+ Returns:
113
+ A detailed evaluation report for the group condition
114
+
115
+ Raises:
116
+ ConditionEvaluationError: If there is an issue evaluating the group condition (e.g., from evaluate_rule calls)
117
+ """
118
+ try:
119
+ operator_func = LOGICAL_OPERATORS[group.logical_operator]
120
+ except KeyError as e:
121
+ raise ConditionEvaluationError(
122
+ f"Unknown logical operator: {group.logical_operator}"
123
+ ) from e
124
+
46
125
  results = [
47
126
  self.evaluate_rule(condition, data) for condition in group.conditions
48
127
  ]
128
+ group_result = operator_func([r.result for r in results])
129
+
130
+ return GroupEvaluationResult(
131
+ logical_operator=group.logical_operator,
132
+ condition_results=results,
133
+ result=group_result,
134
+ )
49
135
 
50
- logical_operators = {GroupOperator.and_: all, GroupOperator.or_: any}
51
- operator_func = logical_operators.get(group.logical_operator)
136
+ def _get_nested_value_from_fides_reference_structure(
137
+ self, data: Any, keys: list[str]
138
+ ) -> Optional[Any]:
139
+ """Get nested value from Fides reference structure
52
140
 
53
- if operator_func is None:
54
- logger.warning(f"Unknown logical operator: {group.logical_operator}")
55
- return False
141
+ Args:
142
+ data: The Fides reference structure to get the nested value from
143
+ keys: The keys to for the specific nested value in the data
56
144
 
57
- return operator_func(results)
145
+ Returns:
146
+ The nested value from the data or None if not a Fides reference structure
147
+
148
+ Raises:
149
+ AttributeError: If the data does not have a get_field_value method
150
+ ValueError: If the keys are not valid for the Fides reference structure
151
+ """
152
+ if hasattr(data, "get_field_value"):
153
+ try:
154
+ field_path = FieldPath(*keys) if len(keys) > 1 else FieldPath(keys[0])
155
+ return data.get_field_value(field_path)
156
+ except (AttributeError, ValueError):
157
+ logger.debug(
158
+ f"Fides reference structure does not have a get_field_value method: {data}"
159
+ )
160
+ raise
161
+ raise ConditionEvaluationError(
162
+ f"Data does not have a get_field_value method: {data}"
163
+ )
164
+
165
+ def _get_nested_value_from_dict(self, data: dict, keys: list[str]) -> Optional[Any]:
166
+ """Get nested value from dictionary. This is the fallback and will return None if the key is not found.
167
+ When the data is missing the None value will work with exists/not_exists operations and correctly evaluate to False
168
+ for other operations like eq, not_eq, etc.
169
+
170
+ Args:
171
+ data: The dictionary to get the nested value from
172
+ keys: The keys to for the specific nested value in the data
173
+
174
+ Returns:
175
+ The nested value from the data
176
+
177
+ Raises:
178
+ KeyError: If the keys are not valid for the dictionary
179
+ """
180
+ current: Any = data
181
+ for key in keys:
182
+ if not isinstance(current, dict):
183
+ return None
184
+ current = current.get(key)
185
+ if current is None:
186
+ return None
187
+ return current
58
188
 
59
189
  def _get_nested_value(self, data: Union[dict, Any], keys: list[str]) -> Any:
60
- """Get nested value from data using dot notation
190
+ """Get nested value from data using dot notation or colon notation
61
191
 
62
192
  Supports both simple dictionary access and Fides reference structures:
63
193
  - Simple dict: data["user"]["name"]
64
194
  - Fides FieldAddress: data.get_field_value(FieldAddress("dataset", "collection", "field_address"))
65
195
  - Fides Collection: data.get_field_value(FieldPath("field_address", "subfield"))
196
+
197
+ Also supports full field addresses with dataset:collection:field format
198
+
199
+ Args:
200
+ data: The data to get the nested value from
201
+ keys: The keys to for the specific nested value in the data
202
+
203
+ Returns:
204
+ The nested value from the data
205
+
206
+ Raises:
207
+ KeyError: If the keys are not valid for the dictionary
66
208
  """
67
209
  if not keys:
68
210
  return data
69
211
 
70
- current = data
71
-
72
212
  # Try Fides reference structures first
73
- if hasattr(current, "get_field_value"):
74
- try:
75
- field_path = FieldPath(*keys) if len(keys) > 1 else FieldPath(keys[0])
76
- return current.get_field_value(field_path)
77
- except (AttributeError, ValueError):
78
- pass
79
-
80
- # Fall back to dictionary access
81
- for key in keys:
82
- if not isinstance(current, dict):
83
- current = current.get(key, {}) if hasattr(current, "get") else None
84
- else:
85
- current = current.get(key, {})
213
+ try:
214
+ return self._get_nested_value_from_fides_reference_structure(data, keys)
215
+ except (AttributeError, ValueError, ConditionEvaluationError):
216
+ pass
86
217
 
87
- return current if current != {} else None
218
+ # Fall back to dictionary access for all path types
219
+ return self._get_nested_value_from_dict(data, keys)
88
220
 
89
221
  def _apply_operator(
90
222
  self, data_value: Any, operator: Operator, user_input_value: Any
91
223
  ) -> bool:
92
- """Apply operator to actual and expected values"""
224
+ """Apply operator to actual and expected values
225
+ The operator is validated in the ConditionLeaf and ConditionGroup schemas,
226
+ so we don't need to validate it here.
93
227
 
228
+ Args:
229
+ data_value: The actual value to evaluate
230
+ operator: The operator to apply
231
+ user_input_value: The expected value to evaluate against
232
+
233
+ Returns:
234
+ The result of the operator applied to the actual and expected values
235
+ """
94
236
  # Get the method for the operator and execute it
95
- operator_method = operator_methods.get(operator)
96
- if operator_method is None:
97
- logger.warning(f"Unknown operator: {operator}")
98
- raise ConditionEvaluationError(f"Unknown operator: {operator}")
99
237
  try:
238
+ operator_method = OPERATOR_METHODS[operator]
100
239
  return operator_method(data_value, user_input_value)
101
- except (TypeError, ValueError) as e:
102
- raise ConditionEvaluationError(f"Error evaluating condition: {e}") from e
240
+ except KeyError as e:
241
+ # Unknown operator
242
+ logger.error(f"Unknown operator: {operator}")
243
+ raise ConditionEvaluationError(f"Unknown operator: {operator}") from e
244
+ except Exception as e:
245
+ # Log unexpected errors but still raise them
246
+ logger.error(f"Unexpected error in operator {operator}: {e}")
247
+ raise ConditionEvaluationError(
248
+ f"Unexpected error evaluating condition: {e}"
249
+ ) from e
@@ -0,0 +1,196 @@
1
+ from typing import Optional, cast
2
+
3
+ from fides.api.task.conditional_dependencies.schemas import (
4
+ ConditionEvaluationResult,
5
+ EvaluationResult,
6
+ GroupEvaluationResult,
7
+ )
8
+
9
+ MAX_DEPTH = 100
10
+
11
+
12
+ def format_evaluation_success_message(
13
+ evaluation_result: Optional[EvaluationResult],
14
+ ) -> str:
15
+ """Format a detailed message about which conditions succeeded
16
+
17
+ Args:
18
+ evaluation_result: The evaluation result to create a string from
19
+
20
+ Returns:
21
+ A string describing the evaluation result
22
+
23
+ """
24
+ if not evaluation_result:
25
+ return "No conditional dependencies to evaluate."
26
+
27
+ return _format_evaluation_message(evaluation_result, success=True, depth=0)
28
+
29
+
30
+ def format_evaluation_failure_message(
31
+ evaluation_result: Optional[EvaluationResult],
32
+ ) -> str:
33
+ """Format a detailed message about which conditions failed
34
+
35
+ Args:
36
+ evaluation_result: The evaluation result to create a string from
37
+
38
+ Returns:
39
+ A string describing the evaluation result
40
+ """
41
+ if not evaluation_result:
42
+ return "No conditional dependencies to evaluate."
43
+
44
+ return _format_evaluation_message(evaluation_result, success=False, depth=0)
45
+
46
+
47
+ def _format_leaf_condition(evaluation_result: ConditionEvaluationResult) -> str:
48
+ """Format a single leaf condition into a readable string
49
+
50
+ Args:
51
+ evaluation_result: The conditionevaluation result to create a string from
52
+
53
+ Returns:
54
+ A string describing the evaluation result
55
+ """
56
+ condition_desc = f"{evaluation_result.field_address} {evaluation_result.operator}"
57
+ if evaluation_result.expected_value is not None:
58
+ condition_desc += f" {evaluation_result.expected_value}"
59
+ return condition_desc
60
+
61
+
62
+ def _format_condition_list(
63
+ results: list[EvaluationResult], success: bool, depth: int
64
+ ) -> list[str]:
65
+ """Format a list of conditions (either leaf or group) into readable strings
66
+
67
+ Args:
68
+ results: The list of conditions to format
69
+ success: Whether the conditions were successful
70
+ depth: The depth of the conditions
71
+
72
+ Returns:
73
+ A list of strings describing the conditions
74
+ """
75
+ condition_descriptions = []
76
+ for sub_result in results:
77
+ if _is_leaf_condition(sub_result):
78
+ # Cast to the specific type after verification
79
+ leaf_result = cast(ConditionEvaluationResult, sub_result)
80
+ condition_descriptions.append(_format_leaf_condition(leaf_result))
81
+ elif _is_group_condition(sub_result):
82
+ # Cast to the specific type after verification
83
+ group_result = cast(GroupEvaluationResult, sub_result)
84
+ condition_descriptions.append(
85
+ _format_evaluation_message(group_result, success, depth + 1)
86
+ )
87
+ return condition_descriptions
88
+
89
+
90
+ def _format_evaluation_message(
91
+ evaluation_result: EvaluationResult, success: bool, depth: int = 0
92
+ ) -> str:
93
+ """Format evaluation results into a readable message
94
+
95
+ Args:
96
+ evaluation_result: The evaluation result to format
97
+ success: Whether the conditions were successful
98
+ depth: The depth of the conditions
99
+
100
+ Returns:
101
+ A string describing the evaluation result
102
+ """
103
+ # Prevent infinite recursion
104
+ if depth > MAX_DEPTH:
105
+ return "Condition evaluation too deeply nested"
106
+
107
+ # Try to format as group condition first
108
+ if _is_group_condition(evaluation_result):
109
+ # Cast to the specific type after verification
110
+ group_result = cast(GroupEvaluationResult, evaluation_result)
111
+ return _format_group_condition(group_result, success, depth)
112
+
113
+ # Try to format as leaf condition
114
+ if _is_leaf_condition(evaluation_result):
115
+ # Cast to the specific type after verification
116
+ leaf_result = cast(ConditionEvaluationResult, evaluation_result)
117
+ return _format_leaf_condition_message(leaf_result, success)
118
+
119
+ # Unknown condition type
120
+ return "Evaluation result details unavailable"
121
+
122
+
123
+ def _is_group_condition(evaluation_result: EvaluationResult) -> bool:
124
+ """Check if evaluation_result is a group condition by checking for group-specific attributes
125
+
126
+ Args:
127
+ evaluation_result: The evaluation result to check
128
+
129
+ Returns:
130
+ True if the evaluation result is a group condition, False otherwise
131
+ """
132
+ # Check for attributes that are unique to GroupEvaluationResult
133
+ return hasattr(evaluation_result, "logical_operator") and hasattr(
134
+ evaluation_result, "condition_results"
135
+ )
136
+
137
+
138
+ def _is_leaf_condition(evaluation_result: EvaluationResult) -> bool:
139
+ """Check if evaluation_result is a leaf condition by checking for leaf-specific attributes
140
+
141
+ Args:
142
+ evaluation_result: The evaluation result to check
143
+
144
+ Returns:
145
+ True if the evaluation result is a leaf condition, False otherwise
146
+ """
147
+ # Check for attributes that are unique to ConditionEvaluationResult
148
+ return hasattr(evaluation_result, "field_address") and hasattr(
149
+ evaluation_result, "operator"
150
+ )
151
+
152
+
153
+ def _format_group_condition(
154
+ evaluation_result: GroupEvaluationResult, success: bool, depth: int
155
+ ) -> str:
156
+ """Format a group condition evaluation result
157
+
158
+ Args:
159
+ evaluation_result: The group evaluation result to format
160
+ success: Whether the conditions were successful
161
+ depth: The depth of the conditions
162
+
163
+ Returns:
164
+ A string describing the evaluation result
165
+ """
166
+ logical_operator = evaluation_result.logical_operator
167
+ results = evaluation_result.condition_results
168
+ condition_descriptions = _format_condition_list(results, success, depth)
169
+
170
+ if success:
171
+ return f"All conditions in {logical_operator.upper()} group were met: {'; '.join(condition_descriptions)}"
172
+
173
+ if condition_descriptions:
174
+ return f"Failed conditions in {logical_operator.upper()} group: {'; '.join(condition_descriptions)}"
175
+
176
+ return f"Group condition with {logical_operator.upper()} operator failed"
177
+
178
+
179
+ def _format_leaf_condition_message(
180
+ evaluation_result: ConditionEvaluationResult, success: bool
181
+ ) -> str:
182
+ """Format a leaf condition evaluation result
183
+
184
+ Args:
185
+ evaluation_result: The leaf evaluation result to format
186
+ success: Whether the conditions were successful
187
+
188
+ Returns:
189
+ A string describing the evaluation result
190
+ """
191
+ condition_desc = _format_leaf_condition(evaluation_result)
192
+
193
+ if success:
194
+ return f"Condition '{condition_desc}' was met"
195
+
196
+ return f"Condition '{condition_desc}' was not met"
@@ -1,7 +1,7 @@
1
1
  import numbers
2
2
  import operator as py_operator
3
3
 
4
- from fides.api.task.conditional_dependencies.schemas import Operator
4
+ from fides.api.task.conditional_dependencies.schemas import GroupOperator, Operator
5
5
 
6
6
  # Define operator methods for validation
7
7
  #
@@ -17,7 +17,7 @@ from fides.api.task.conditional_dependencies.schemas import Operator
17
17
  # * None in [] returns False
18
18
  # * None not in [] returns True
19
19
  # * This allows None to be a valid list element for membership testing
20
- operator_methods = {
20
+ OPERATOR_METHODS = {
21
21
  # Basic operators - handle None naturally using Python's built-in behavior
22
22
  Operator.exists: lambda a, _: a is not None,
23
23
  Operator.not_exists: lambda a, _: a is None,
@@ -103,6 +103,12 @@ operator_methods = {
103
103
  ),
104
104
  }
105
105
 
106
+ # Define logical operators for group conditions
107
+ LOGICAL_OPERATORS = {
108
+ GroupOperator.and_: all,
109
+ GroupOperator.or_: any,
110
+ }
111
+
106
112
  # Common operators that work with most data types
107
113
  COMMON_OPERATORS = {
108
114
  Operator.eq,
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import List, Optional, Union
2
+ from typing import Any, List, Optional, Union
3
3
 
4
4
  from pydantic import BaseModel, Field, model_validator
5
5
 
@@ -100,6 +100,30 @@ class ConditionGroup(BaseModel):
100
100
  return self
101
101
 
102
102
 
103
+ # Evaluation result for a single condition
104
+ class ConditionEvaluationResult(BaseModel):
105
+ field_address: str
106
+ operator: Operator
107
+ expected_value: Optional[
108
+ Union[str, int, float, bool, List[Union[str, int, float, bool]]]
109
+ ]
110
+ actual_value: Any
111
+ result: bool
112
+ message: str
113
+
114
+
115
+ # Evaluation result for a group of conditions
116
+ class GroupEvaluationResult(BaseModel):
117
+ logical_operator: GroupOperator
118
+ condition_results: list["EvaluationResult"]
119
+ result: bool
120
+
121
+
122
+ # Union type for evaluation results
123
+ EvaluationResult = Union[ConditionEvaluationResult, GroupEvaluationResult]
124
+
125
+
103
126
  # Use model_rebuild to allow recursive nesting
104
127
  Condition = Union[ConditionLeaf, ConditionGroup]
105
128
  ConditionGroup.model_rebuild()
129
+ GroupEvaluationResult.model_rebuild()
@@ -437,13 +437,20 @@ class GraphTask(ABC): # pylint: disable=too-many-instance-attributes
437
437
  self.update_status("retrying", [], action_type, ExecutionLogStatus.retrying)
438
438
 
439
439
  def log_awaiting_processing(
440
- self, action_type: ActionType, ex: Optional[BaseException]
440
+ self,
441
+ action_type: ActionType,
442
+ ex: Optional[BaseException],
443
+ extra_message: Optional[str] = None,
441
444
  ) -> None:
442
445
  """On paused activities"""
443
446
  logger.info("Pausing node {}", self.key)
444
447
 
448
+ message = str(ex)
449
+ if extra_message:
450
+ message = f"{message}. {extra_message}"
451
+
445
452
  self.update_status(
446
- str(ex), [], action_type, ExecutionLogStatus.awaiting_processing
453
+ message, [], action_type, ExecutionLogStatus.awaiting_processing
447
454
  )
448
455
 
449
456
  def log_skipped(self, action_type: ActionType, ex: str) -> None: