ethyca-fides 2.68.0rc3__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.
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/METADATA +1 -1
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/RECORD +100 -98
- fides/_version.py +3 -3
- fides/api/schemas/application_config.py +2 -1
- fides/api/schemas/privacy_center_config.py +15 -0
- fides/api/task/conditional_dependencies/evaluator.py +192 -45
- fides/api/task/conditional_dependencies/logging_utils.py +196 -0
- fides/api/task/conditional_dependencies/operators.py +8 -2
- fides/api/task/conditional_dependencies/schemas.py +25 -1
- fides/api/task/graph_task.py +9 -2
- fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
- fides/api/task/manual/manual_task_graph_task.py +213 -119
- fides/api/task/manual/manual_task_utils.py +0 -4
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.68.0rc3.dist-info → ethyca_fides-2.68.1b1.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{5XFHjjKVYqngLW-SDXDX4 → tzF4yti8NslASlGnxnZ8m}/_buildManifest.js +0 -0
- /fides/ui-build/static/admin/_next/static/{5XFHjjKVYqngLW-SDXDX4 → 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
|
|
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
|
-
|
|
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
|
|
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(
|
|
28
|
-
|
|
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
|
-
|
|
51
|
+
leaf_result = self._evaluate_leaf_condition(rule, data)
|
|
52
|
+
return leaf_result
|
|
31
53
|
# ConditionGroup
|
|
32
|
-
|
|
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
|
-
) ->
|
|
37
|
-
"""Evaluate a leaf condition against input data
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
102
|
-
|
|
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
|
-
|
|
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()
|
fides/api/task/graph_task.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
453
|
+
message, [], action_type, ExecutionLogStatus.awaiting_processing
|
|
447
454
|
)
|
|
448
455
|
|
|
449
456
|
def log_skipped(self, action_type: ActionType, ex: str) -> None:
|