ethyca-fides 2.68.1b0__py2.py3-none-any.whl → 2.68.1b2__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.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/METADATA +1 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/RECORD +128 -118
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/3baf42d251a6_add_generic_taxonomy_models.py +239 -0
- fides/api/api/deps.py +2 -0
- fides/api/api/v1/endpoints/generic_overrides.py +64 -167
- fides/api/db/base.py +6 -0
- fides/api/db/ctl_session.py +3 -0
- fides/api/db/session.py +2 -1
- fides/api/models/privacy_request/privacy_request.py +15 -0
- fides/api/models/taxonomy.py +275 -0
- fides/api/schemas/application_config.py +2 -1
- fides/api/schemas/privacy_center_config.py +15 -0
- fides/api/service/deps.py +5 -0
- fides/api/service/privacy_request/request_service.py +6 -1
- 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 +224 -119
- fides/api/task/manual/manual_task_utils.py +0 -4
- fides/api/tasks/__init__.py +1 -0
- fides/api/util/connection_type.py +68 -33
- fides/config/database_settings.py +10 -1
- fides/data/sample_project/docker-compose.yml +3 -3
- fides/service/taxonomy/__init__.py +0 -0
- fides/service/taxonomy/handlers/__init__.py +11 -0
- fides/service/taxonomy/handlers/base.py +42 -0
- fides/service/taxonomy/handlers/legacy_handler.py +95 -0
- fides/service/taxonomy/taxonomy_service.py +261 -0
- fides/service/taxonomy/utils.py +160 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-65723cd4b8fc36ac.js → _app-2c10f6b217b7978b.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-58827eb86516931f.js +1 -0
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-766e57bcf38b5b1e.js → [id]-4e286a1e501a0c73.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-709bcb0bc6a5382d.js +1 -0
- fides/ui-build/static/admin/_next/static/css/a72179b1754aadd3.css +1 -0
- fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_buildManifest.js +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/lib/fides-preview.js +1 -1
- fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
- fides/ui-build/static/admin/lib/fides.js +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
- fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-53a763e49ce34a74.js +0 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-f43a988542813110.js +0 -1
- fides/ui-build/static/admin/_next/static/css/e1628f15dd5f019b.css +0 -1
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_ssgManifest.js +0 -0
|
@@ -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:
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic.v1.utils import deep_update
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from fides.api.graph.config import CollectionAddress, FieldAddress
|
|
7
|
+
from fides.api.models.manual_task import ManualTask
|
|
8
|
+
from fides.api.models.manual_task.conditional_dependency import (
|
|
9
|
+
ManualTaskConditionalDependency,
|
|
10
|
+
)
|
|
11
|
+
from fides.api.task.conditional_dependencies.evaluator import ConditionEvaluator
|
|
12
|
+
from fides.api.task.conditional_dependencies.schemas import EvaluationResult
|
|
13
|
+
from fides.api.util.collection_util import Row
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_conditional_dependency_data_from_inputs(
|
|
17
|
+
*inputs: list[Row], manual_task: ManualTask, input_keys: list[CollectionAddress]
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Extract data for conditional dependency field addresses from input data.
|
|
21
|
+
|
|
22
|
+
This method processes data from upstream regular tasks that provide fields
|
|
23
|
+
referenced in manual task conditional dependencies. It extracts the relevant
|
|
24
|
+
field values and makes them available for conditional dependency evaluation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
*inputs: Input data from upstream nodes (regular tasks)
|
|
28
|
+
manual_task: Manual task to extract conditional dependencies from
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary mapping field addresses to their values from input data
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
conditional_data: dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
# Get all conditional dependencies field addresses
|
|
37
|
+
field_addresses = [
|
|
38
|
+
dependency.field_address
|
|
39
|
+
for dependency in manual_task.conditional_dependencies
|
|
40
|
+
if dependency.field_address
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# if no field addresses, return empty conditional data
|
|
44
|
+
# This will allow the manual task to be executed if there are no conditional dependencies
|
|
45
|
+
if not field_addresses:
|
|
46
|
+
return conditional_data
|
|
47
|
+
|
|
48
|
+
# For manual tasks, we need to preserve the original field names from conditional dependencies
|
|
49
|
+
# Instead of using pre_process_input_data which consolidates fields, we'll extract directly
|
|
50
|
+
# from the raw input data based on the execution node's input_keys
|
|
51
|
+
|
|
52
|
+
# Create a mapping between collections and their input data
|
|
53
|
+
# Convert CollectionAddress objects to strings for consistent key types
|
|
54
|
+
collection_data_map = {
|
|
55
|
+
str(collection_key): input_data
|
|
56
|
+
for collection_key, input_data in zip(input_keys, inputs)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Extract data for each conditional dependency field address
|
|
60
|
+
for field_address in field_addresses:
|
|
61
|
+
source_collection_key, field_path = parse_field_address(field_address)
|
|
62
|
+
|
|
63
|
+
# Find the input data for this collection
|
|
64
|
+
field_value = None
|
|
65
|
+
input_data = collection_data_map.get(source_collection_key)
|
|
66
|
+
if input_data:
|
|
67
|
+
|
|
68
|
+
# Look for the field in the input data
|
|
69
|
+
for row in input_data:
|
|
70
|
+
|
|
71
|
+
# Traverse the nested field path to get the actual value
|
|
72
|
+
field_value = extract_nested_field_value(row, field_path)
|
|
73
|
+
|
|
74
|
+
if field_value is not None:
|
|
75
|
+
break
|
|
76
|
+
# Found the field value, break out of the inner loop (over rows)
|
|
77
|
+
# but continue with the outer loop to process this field
|
|
78
|
+
|
|
79
|
+
# Always include the field in conditional_data, even if value is None
|
|
80
|
+
# This allows conditional dependencies to evaluate existence, non-existence, and falsy values
|
|
81
|
+
nested_data = set_nested_value(field_address, field_value)
|
|
82
|
+
conditional_data = deep_update(conditional_data, nested_data)
|
|
83
|
+
|
|
84
|
+
return conditional_data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_field_address(field_address: str) -> tuple[str, list[str]]:
|
|
88
|
+
"""
|
|
89
|
+
Parse a field address into dataset, collection, and field path.
|
|
90
|
+
|
|
91
|
+
For complex addresses with > 2 colons, uses manual string parsing.
|
|
92
|
+
For simple addresses with ≤ 2 colons, uses FieldAddress.from_string().
|
|
93
|
+
"""
|
|
94
|
+
if field_address.count(":") > 2:
|
|
95
|
+
# Parse manually: dataset:collection:field:subfield -> dataset, collection, [field, subfield]
|
|
96
|
+
dataset, collection, *field_path = field_address.split(":")
|
|
97
|
+
source_collection_key = f"{dataset}:{collection}"
|
|
98
|
+
else:
|
|
99
|
+
# Use standard FieldAddress parsing for simple cases
|
|
100
|
+
field_address_obj = FieldAddress.from_string(field_address)
|
|
101
|
+
source_collection_key = str(field_address_obj.collection_address())
|
|
102
|
+
field_path = list(field_address_obj.field_path.levels)
|
|
103
|
+
return source_collection_key, field_path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def extract_nested_field_value(data: Any, field_path: list[str]) -> Any:
|
|
107
|
+
"""
|
|
108
|
+
Extract a nested field value by traversing the field path.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
data: The data to extract from (usually a dict)
|
|
112
|
+
field_path: List of field names to traverse (e.g., ["profile", "preferences", "theme"])
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The value at the end of the field path, or None if not found
|
|
116
|
+
"""
|
|
117
|
+
if not field_path:
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
current = data
|
|
121
|
+
for field_name in field_path:
|
|
122
|
+
if isinstance(current, dict) and field_name in current:
|
|
123
|
+
current = current[field_name]
|
|
124
|
+
else:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
return current
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_nested_value(field_address: str, value: Any) -> dict[str, Any]:
|
|
131
|
+
"""
|
|
132
|
+
Set a field value in the conditional data structure.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
field_address: Colon-separated field address (e.g., "dataset:collection:field" or "dataset:collection:nested:field")
|
|
136
|
+
value: The value to set
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dictionary with the field value set at the specified path
|
|
140
|
+
"""
|
|
141
|
+
# For conditional dependencies, we want to set the field value directly
|
|
142
|
+
# The field_address format is "dataset:collection:field" or "dataset:collection:nested:field"
|
|
143
|
+
# We want to create: {dataset: {collection: {field: value}}} or {dataset: {collection: {nested: {field: value}}}}
|
|
144
|
+
parts = field_address.split(":")
|
|
145
|
+
|
|
146
|
+
if len(parts) >= 3:
|
|
147
|
+
dataset, collection = parts[0], parts[1]
|
|
148
|
+
# Handle nested field paths beyond the first 3 parts
|
|
149
|
+
if len(parts) == 3:
|
|
150
|
+
# Simple case: dataset:collection:field
|
|
151
|
+
field = parts[2]
|
|
152
|
+
return {dataset: {collection: {field: value}}}
|
|
153
|
+
|
|
154
|
+
# Nested case: dataset:collection:nested:field
|
|
155
|
+
# Build the nested structure from the remaining parts
|
|
156
|
+
nested_structure = value
|
|
157
|
+
for part in reversed(parts[2:]):
|
|
158
|
+
nested_structure = {part: nested_structure}
|
|
159
|
+
return {dataset: {collection: nested_structure}}
|
|
160
|
+
|
|
161
|
+
# Fallback for unexpected formats
|
|
162
|
+
return {field_address: value}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def evaluate_conditional_dependencies(
|
|
166
|
+
db: Session, manual_task: ManualTask, conditional_data: dict[str, Any]
|
|
167
|
+
) -> Optional[EvaluationResult]:
|
|
168
|
+
"""
|
|
169
|
+
Evaluate conditional dependencies for a manual task using data from regular tasks.
|
|
170
|
+
|
|
171
|
+
This method evaluates whether a manual task should be executed based on its
|
|
172
|
+
conditional dependencies and the data received from upstream regular tasks.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
manual_task: The manual task to evaluate
|
|
176
|
+
conditional_data: Data from regular tasks for conditional dependency fields
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
EvaluationResult object containing detailed information about which conditions
|
|
180
|
+
were met or not met, or None if no conditional dependencies exist
|
|
181
|
+
"""
|
|
182
|
+
# Get the root condition for this manual task
|
|
183
|
+
root_condition = ManualTaskConditionalDependency.get_root_condition(
|
|
184
|
+
db, manual_task.id
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not root_condition:
|
|
188
|
+
# No conditional dependencies - always execute
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Evaluate the condition using the data from regular tasks
|
|
192
|
+
evaluator = ConditionEvaluator(db)
|
|
193
|
+
return evaluator.evaluate_rule(root_condition, conditional_data)
|