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.

Files changed (131) hide show
  1. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/RECORD +128 -118
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/3baf42d251a6_add_generic_taxonomy_models.py +239 -0
  5. fides/api/api/deps.py +2 -0
  6. fides/api/api/v1/endpoints/generic_overrides.py +64 -167
  7. fides/api/db/base.py +6 -0
  8. fides/api/db/ctl_session.py +3 -0
  9. fides/api/db/session.py +2 -1
  10. fides/api/models/privacy_request/privacy_request.py +15 -0
  11. fides/api/models/taxonomy.py +275 -0
  12. fides/api/schemas/application_config.py +2 -1
  13. fides/api/schemas/privacy_center_config.py +15 -0
  14. fides/api/service/deps.py +5 -0
  15. fides/api/service/privacy_request/request_service.py +6 -1
  16. fides/api/task/conditional_dependencies/evaluator.py +192 -45
  17. fides/api/task/conditional_dependencies/logging_utils.py +196 -0
  18. fides/api/task/conditional_dependencies/operators.py +8 -2
  19. fides/api/task/conditional_dependencies/schemas.py +25 -1
  20. fides/api/task/graph_task.py +9 -2
  21. fides/api/task/manual/manual_task_conditional_evaluation.py +193 -0
  22. fides/api/task/manual/manual_task_graph_task.py +224 -119
  23. fides/api/task/manual/manual_task_utils.py +0 -4
  24. fides/api/tasks/__init__.py +1 -0
  25. fides/api/util/connection_type.py +68 -33
  26. fides/config/database_settings.py +10 -1
  27. fides/data/sample_project/docker-compose.yml +3 -3
  28. fides/service/taxonomy/__init__.py +0 -0
  29. fides/service/taxonomy/handlers/__init__.py +11 -0
  30. fides/service/taxonomy/handlers/base.py +42 -0
  31. fides/service/taxonomy/handlers/legacy_handler.py +95 -0
  32. fides/service/taxonomy/taxonomy_service.py +261 -0
  33. fides/service/taxonomy/utils.py +160 -0
  34. fides/ui-build/static/admin/404.html +1 -1
  35. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-65723cd4b8fc36ac.js → _app-2c10f6b217b7978b.js} +1 -1
  36. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-58827eb86516931f.js +1 -0
  37. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-766e57bcf38b5b1e.js → [id]-4e286a1e501a0c73.js} +1 -1
  38. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-709bcb0bc6a5382d.js +1 -0
  39. fides/ui-build/static/admin/_next/static/css/a72179b1754aadd3.css +1 -0
  40. fides/ui-build/static/admin/_next/static/{JLiYN-Wiw1kNc_8IVythJ → qvk5eMANVfwYkdURE7fgG}/_buildManifest.js +1 -1
  41. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  42. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  43. fides/ui-build/static/admin/add-systems.html +1 -1
  44. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  45. fides/ui-build/static/admin/consent/configure.html +1 -1
  46. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  47. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  48. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  49. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  50. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  51. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  52. fides/ui-build/static/admin/consent/properties.html +1 -1
  53. fides/ui-build/static/admin/consent/reporting.html +1 -1
  54. fides/ui-build/static/admin/consent.html +1 -1
  55. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  56. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  57. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  58. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  59. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  60. fides/ui-build/static/admin/data-catalog.html +1 -1
  61. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  62. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  63. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  64. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  65. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  66. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  67. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  68. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  69. fides/ui-build/static/admin/datamap.html +1 -1
  70. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  71. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  72. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  73. fides/ui-build/static/admin/dataset/new.html +1 -1
  74. fides/ui-build/static/admin/dataset.html +1 -1
  75. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  76. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  77. fides/ui-build/static/admin/datastore-connection.html +1 -1
  78. fides/ui-build/static/admin/index.html +1 -1
  79. fides/ui-build/static/admin/integrations/[id].html +1 -1
  80. fides/ui-build/static/admin/integrations.html +1 -1
  81. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  82. fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
  83. fides/ui-build/static/admin/lib/fides.js +1 -1
  84. fides/ui-build/static/admin/login/[provider].html +1 -1
  85. fides/ui-build/static/admin/login.html +1 -1
  86. fides/ui-build/static/admin/messaging/[id].html +1 -1
  87. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  88. fides/ui-build/static/admin/messaging.html +1 -1
  89. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  90. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  91. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  92. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  93. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  94. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  95. fides/ui-build/static/admin/poc/forms.html +1 -1
  96. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  97. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  98. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  99. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  100. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  101. fides/ui-build/static/admin/privacy-requests.html +1 -1
  102. fides/ui-build/static/admin/properties/[id].html +1 -1
  103. fides/ui-build/static/admin/properties/add-property.html +1 -1
  104. fides/ui-build/static/admin/properties.html +1 -1
  105. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  106. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  107. fides/ui-build/static/admin/settings/about.html +1 -1
  108. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  109. fides/ui-build/static/admin/settings/consent.html +1 -1
  110. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  111. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  112. fides/ui-build/static/admin/settings/domains.html +1 -1
  113. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  114. fides/ui-build/static/admin/settings/locations.html +1 -1
  115. fides/ui-build/static/admin/settings/organization.html +1 -1
  116. fides/ui-build/static/admin/settings/regulations.html +1 -1
  117. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  118. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  119. fides/ui-build/static/admin/systems.html +1 -1
  120. fides/ui-build/static/admin/taxonomy.html +1 -1
  121. fides/ui-build/static/admin/user-management/new.html +1 -1
  122. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  123. fides/ui-build/static/admin/user-management.html +1 -1
  124. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center-53a763e49ce34a74.js +0 -1
  125. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-f43a988542813110.js +0 -1
  126. fides/ui-build/static/admin/_next/static/css/e1628f15dd5f019b.css +0 -1
  127. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/WHEEL +0 -0
  128. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/entry_points.txt +0 -0
  129. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/licenses/LICENSE +0 -0
  130. {ethyca_fides-2.68.1b0.dist-info → ethyca_fides-2.68.1b2.dist-info}/top_level.txt +0 -0
  131. /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
- 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:
@@ -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)