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
@@ -1,7 +1,7 @@
1
1
  from typing import Any, Optional
2
2
 
3
3
  from loguru import logger
4
- from sqlalchemy.orm import Session
4
+ from pydantic.v1.utils import deep_update
5
5
 
6
6
  from fides.api.common_exceptions import AwaitingAsyncTaskCallback
7
7
  from fides.api.models.attachment import AttachmentType
@@ -15,13 +15,23 @@ from fides.api.models.manual_task import (
15
15
  StatusType,
16
16
  )
17
17
  from fides.api.models.privacy_request import PrivacyRequest
18
+ from fides.api.models.worker_task import ExecutionLogStatus
18
19
  from fides.api.schemas.policy import ActionType
19
20
  from fides.api.schemas.privacy_request import PrivacyRequestStatus
21
+ from fides.api.task.conditional_dependencies.logging_utils import (
22
+ format_evaluation_failure_message,
23
+ format_evaluation_success_message,
24
+ )
20
25
  from fides.api.task.graph_task import GraphTask, retry
21
26
  from fides.api.task.manual.manual_task_address import ManualTaskAddress
27
+ from fides.api.task.manual.manual_task_conditional_evaluation import (
28
+ evaluate_conditional_dependencies,
29
+ extract_conditional_dependency_data_from_inputs,
30
+ )
22
31
  from fides.api.task.manual.manual_task_utils import (
23
32
  get_manual_task_for_connection_config,
24
33
  )
34
+ from fides.api.task.task_resources import TaskResources
25
35
  from fides.api.util.collection_util import Row
26
36
  from fides.api.util.storage_util import format_size
27
37
 
@@ -29,88 +39,236 @@ from fides.api.util.storage_util import format_size
29
39
  class ManualTaskGraphTask(GraphTask):
30
40
  """GraphTask implementation for ManualTask execution"""
31
41
 
42
+ # class level constants
43
+ DRY_RUN_PLACEHOLDER_VALUE = 1
44
+
45
+ def __init__(self, resources: TaskResources) -> None:
46
+ super().__init__(resources)
47
+ self.connection_key = ManualTaskAddress.get_connection_key(
48
+ self.execution_node.address
49
+ )
50
+
51
+ # ------------------------------------------------------------------------------------------------
52
+ # Public methods
53
+ # ------------------------------------------------------------------------------------------------
54
+
55
+ def dry_run_task(self) -> int:
56
+ """Return estimated row count for dry run - manual tasks don't have predictable counts"""
57
+ return self.DRY_RUN_PLACEHOLDER_VALUE
58
+
32
59
  @retry(action_type=ActionType.access, default_return=[])
33
60
  def access_request(self, *inputs: list[Row]) -> list[Row]:
61
+ """
62
+ Execute manual task logic following the standard GraphTask pattern.
63
+ Calls _run_request with ACCESS configs.
64
+ Returns data if submitted, raise AwaitingAsyncTaskCallback if not
65
+ """
66
+ if self.resources.request.policy.get_action_type() == ActionType.erasure:
67
+ # We're in an erasure privacy request's access phase - complete access task immediately
68
+ # since access is just for data collection to support erasure, not for user data access
69
+ self.update_status(
70
+ "Access task completed immediately for erasure privacy request (data collection only)",
71
+ [],
72
+ ActionType.access,
73
+ ExecutionLogStatus.complete,
74
+ )
75
+ return []
76
+
77
+ result = self._run_request(
78
+ ManualTaskConfigurationType.access_privacy_request,
79
+ ActionType.access,
80
+ *inputs,
81
+ )
82
+ if result is None:
83
+ # Conditional skip or not applicable already logged upstream; do not mark complete here
84
+ return []
85
+
86
+ # We are picking up after awaiting input and have provided data – mark complete with record count
87
+ self.log_end(ActionType.access, record_count=len(result))
88
+ return result
89
+
90
+ # Provide erasure support for manual tasks
91
+ @retry(action_type=ActionType.erasure, default_return=0)
92
+ def erasure_request(
93
+ self,
94
+ retrieved_data: list[Row], # This is not used for manual tasks.
95
+ *erasure_prereqs: int, # noqa: D401, pylint: disable=unused-argument # TODO Remove when we stop support for DSR 2.0
96
+ inputs: Optional[list[list[Row]]] = None,
97
+ ) -> int:
98
+ """Execute manual-task-driven erasure logic.
99
+ Calls _run_request with ERASURE configs.
100
+
101
+ Mirrors access_request behaviour but returns the number of rows masked (always 0)
102
+ once all required manual task submissions are present. If submissions are
103
+ incomplete the privacy request is paused awaiting user input.
104
+ Returns the number of rows masked (always 0)
105
+ Raises AwaitingAsyncTaskCallback if data is not submitted
106
+ """
107
+ if not inputs:
108
+ inputs = []
109
+ result = self._run_request(
110
+ ManualTaskConfigurationType.erasure_privacy_request,
111
+ ActionType.erasure,
112
+ *inputs,
113
+ )
114
+ if result is None:
115
+ # Conditional skip or not applicable already logged upstream; do not mark complete here
116
+ return 0
117
+
118
+ # Mark rows_masked = 0 (manual tasks do not mask data directly)
119
+ if self.request_task.id:
120
+ # Storing result for DSR 3.0; SQLAlchemy column typing triggers mypy warning
121
+ self.request_task.rows_masked = 0 # type: ignore[assignment]
122
+
123
+ # Picking up after awaiting input, mark erasure node complete with rows masked count (always 0)
124
+ self.log_end(ActionType.erasure, record_count=0)
125
+ return 0
126
+
127
+ # ------------------------------------------------------------------------------------------------
128
+ # Private methods
129
+ # ------------------------------------------------------------------------------------------------
130
+
131
+ def _run_request(
132
+ self,
133
+ config_type: ManualTaskConfigurationType,
134
+ action_type: ActionType,
135
+ *inputs: list[Row],
136
+ ) -> Optional[list[Row]]:
34
137
  """
35
138
  Execute manual task logic following the standard GraphTask pattern:
36
139
  1. Create ManualTaskInstances if they don't exist
37
- 2. Check for submissions
140
+ 2. Check if all required submissions are present
38
141
  3. Return data if submitted, raise AwaitingAsyncTaskCallback if not
39
142
  """
40
- db = self.resources.session
41
- collection_address = self.execution_node.address
143
+ manual_task = self._get_manual_task_or_none()
144
+ if manual_task is None:
145
+ return None
42
146
 
43
- # Verify this is a manual task address
44
- if not ManualTaskAddress.is_manual_task_address(collection_address):
45
- raise ValueError(f"Invalid manual task address: {collection_address}")
147
+ # Complete a series of checks to determine if the manual task should be executed
148
+ # If any of these checks fail, complete immediately or mark as skipped
46
149
 
47
- connection_key = ManualTaskAddress.get_connection_key(collection_address)
150
+ # Check if any eligible manual tasks have applicable configs
151
+ if not self._check_manual_task_configs(manual_task, config_type, action_type):
152
+ return None
48
153
 
49
- # Get manual tasks for this connection
50
- manual_task = get_manual_task_for_connection_config(db, connection_key)
154
+ # Check if there are any rules for this action type
155
+ if not self.resources.request.policy.get_rules_for_action(
156
+ action_type=action_type
157
+ ):
158
+ return None
51
159
 
52
- if not manual_task:
53
- return []
160
+ # Extract conditional dependency data from inputs
161
+ conditional_data = extract_conditional_dependency_data_from_inputs(
162
+ *inputs, manual_task=manual_task, input_keys=self.execution_node.input_keys
163
+ )
164
+ # Evaluate conditional dependencies
165
+ evaluation_result = evaluate_conditional_dependencies(
166
+ self.resources.session, manual_task, conditional_data=conditional_data
167
+ )
168
+ detailed_message: Optional[str] = None
169
+ # if there were conditional dependencies and they were not met,
170
+ # clean up any existing ManualTaskInstances and return None to cause a skip
171
+ if evaluation_result is not None and not evaluation_result.result:
172
+ self._cleanup_manual_task_instances(manual_task, self.resources.request)
173
+ detailed_message = format_evaluation_failure_message(evaluation_result)
174
+ self.update_status(
175
+ f"Manual task conditional dependencies not met. {detailed_message}",
176
+ [],
177
+ ActionType(self.resources.privacy_request_task.action_type),
178
+ ExecutionLogStatus.skipped,
179
+ )
180
+ return None
54
181
 
55
- # Check if any manual tasks have ACCESS configs
56
- # TODO: This will be changed with Manual Task Dependencies Implementation.
182
+ # Check/Create manual task instances for applicable configs only
183
+ self._ensure_manual_task_instances(
184
+ manual_task,
185
+ self.resources.request,
186
+ config_type,
187
+ )
57
188
 
189
+ # Check if all manual task instances have submissions for applicable configs only
190
+ # No separate pending log; include details in the awaiting-processing log
191
+ if evaluation_result:
192
+ detailed_message = format_evaluation_success_message(evaluation_result)
193
+ result = self._set_submitted_data_or_raise_awaiting_async_task_callback(
194
+ manual_task,
195
+ config_type,
196
+ action_type,
197
+ conditional_data=conditional_data,
198
+ awaiting_detail_message=detailed_message,
199
+ )
200
+ return result
201
+
202
+ def _check_manual_task_configs(
203
+ self,
204
+ manual_task: ManualTask,
205
+ config_type: ManualTaskConfigurationType,
206
+ action_type: ActionType,
207
+ ) -> bool:
58
208
  has_access_configs = [
59
209
  config
60
210
  for config in manual_task.configs
61
- if config.is_current
62
- and config.config_type == ManualTaskConfigurationType.access_privacy_request
211
+ if config.is_current and config.config_type == config_type
63
212
  ]
64
213
 
65
214
  if not has_access_configs:
66
215
  # No access configs - complete immediately
67
- self.log_end(ActionType.access)
68
- return []
216
+ self.log_end(action_type)
217
+ return False
69
218
 
70
- if not self.resources.request.policy.get_rules_for_action(
71
- action_type=ActionType.access
72
- ):
73
- # TODO: This will be changed with Manual Task Dependencies Implementation.
74
- self.log_end(ActionType.access)
75
- return []
219
+ return True
76
220
 
77
- # Check/create manual task instances for ACCESS configs only
78
- self._ensure_manual_task_instances(
79
- db,
80
- manual_task,
81
- self.resources.request,
82
- ManualTaskConfigurationType.access_privacy_request,
221
+ def _get_manual_task_or_none(self) -> Optional[ManualTask]:
222
+ # Verify this is a manual task address
223
+ if not ManualTaskAddress.is_manual_task_address(self.execution_node.address):
224
+ raise ValueError(
225
+ f"Invalid manual task address: {self.execution_node.address}"
226
+ )
227
+
228
+ # Get the manual task for this connection config (1:1 relationship)
229
+ manual_task = get_manual_task_for_connection_config(
230
+ self.resources.session, self.connection_key
83
231
  )
232
+ return manual_task
84
233
 
234
+ def _set_submitted_data_or_raise_awaiting_async_task_callback(
235
+ self,
236
+ manual_task: ManualTask,
237
+ config_type: ManualTaskConfigurationType,
238
+ action_type: ActionType,
239
+ conditional_data: Optional[dict[str, Any]] = None,
240
+ awaiting_detail_message: Optional[str] = None,
241
+ ) -> Optional[list[Row]]:
242
+ """
243
+ Set submitted data for a manual task and raise AwaitingAsyncTaskCallback if all instances are not completed
244
+ """
85
245
  # Check if all manual task instances have submissions for ACCESS configs only
86
246
  submitted_data = self._get_submitted_data(
87
- db,
88
247
  manual_task,
89
248
  self.resources.request,
90
- ManualTaskConfigurationType.access_privacy_request,
249
+ config_type,
250
+ conditional_data=conditional_data,
91
251
  )
92
252
 
93
253
  if submitted_data is not None:
94
254
  result: list[Row] = [submitted_data] if submitted_data else []
95
255
  self.request_task.access_data = result
96
256
 
97
- # Mark request task as complete and write execution log
98
- self.log_end(ActionType.access)
99
257
  return result
100
258
 
101
259
  # Set privacy request status to requires_input if not already set
102
260
  if self.resources.request.status != PrivacyRequestStatus.requires_input:
103
261
  self.resources.request.status = PrivacyRequestStatus.requires_input
104
- self.resources.request.save(db)
262
+ self.resources.request.save(self.resources.session)
105
263
 
106
- # This should trigger log_awaiting_processing via the @retry decorator
107
- raise AwaitingAsyncTaskCallback(
108
- f"Manual task for {connection_key} requires user input"
109
- )
264
+ # This will trigger log_awaiting_processing via the @retry decorator; include conditional details
265
+ base_msg = f"Manual task for {self.connection_key} requires user input"
266
+ if awaiting_detail_message:
267
+ base_msg = f"{base_msg}. {awaiting_detail_message}"
268
+ raise AwaitingAsyncTaskCallback(base_msg)
110
269
 
111
270
  def _ensure_manual_task_instances(
112
271
  self,
113
- db: Session,
114
272
  manual_task: ManualTask,
115
273
  privacy_request: PrivacyRequest,
116
274
  allowed_config_type: "ManualTaskConfigurationType",
@@ -139,6 +297,7 @@ class ManualTaskGraphTask(GraphTask):
139
297
 
140
298
  # If no existing instances, create a new one for the current config
141
299
  # There will only be one config of each type per manual task
300
+ # Sort by version descending to get the latest version first
142
301
  config = next(
143
302
  (
144
303
  config
@@ -154,7 +313,7 @@ class ManualTaskGraphTask(GraphTask):
154
313
 
155
314
  if config:
156
315
  ManualTaskInstance.create(
157
- db=db,
316
+ db=self.resources.session,
158
317
  data={
159
318
  "task_id": manual_task.id,
160
319
  "config_id": config.id,
@@ -166,10 +325,10 @@ class ManualTaskGraphTask(GraphTask):
166
325
 
167
326
  def _get_submitted_data(
168
327
  self,
169
- db: Session,
170
328
  manual_task: ManualTask,
171
329
  privacy_request: PrivacyRequest,
172
330
  allowed_config_type: "ManualTaskConfigurationType",
331
+ conditional_data: Optional[dict[str, Any]] = None,
173
332
  ) -> Optional[dict[str, Any]]:
174
333
  """
175
334
  Check if all manual task instances have submissions for ALL fields and return aggregated data
@@ -193,10 +352,15 @@ class ManualTaskGraphTask(GraphTask):
193
352
  # Update status if needed
194
353
  if inst.status != StatusType.completed:
195
354
  inst.status = StatusType.completed
196
- inst.save(db)
355
+ inst.save(self.resources.session)
197
356
 
198
357
  # Aggregate submission data from all instances
199
358
  aggregated_data = self._aggregate_submission_data(candidate_instances)
359
+
360
+ # Merge conditional data with aggregated submission data
361
+ if conditional_data:
362
+ aggregated_data = deep_update(aggregated_data, conditional_data)
363
+
200
364
  return aggregated_data or None
201
365
 
202
366
  def _aggregate_submission_data(
@@ -254,84 +418,25 @@ class ManualTaskGraphTask(GraphTask):
254
418
  )
255
419
  return attachment_map or None
256
420
 
257
- def dry_run_task(self) -> int:
258
- """Return estimated row count for dry run - manual tasks don't have predictable counts"""
259
- return 1 # Placeholder - manual tasks generate variable data
260
-
261
- # Provide erasure support for manual tasks
262
- @retry(action_type=ActionType.erasure, default_return=0)
263
- def erasure_request(
264
- self,
265
- retrieved_data: list[Row],
266
- *erasure_prereqs: int, # noqa: D401, pylint: disable=unused-argument
267
- inputs: Optional[list[list[Row]]] = None,
268
- ) -> int:
269
- """Execute manual-task-driven erasure logic.
270
-
271
- Mirrors access_request behaviour but returns the number of rows masked (always 0)
272
- once all required manual task submissions are present. If submissions are
273
- incomplete the privacy request is paused awaiting user input.
421
+ def _cleanup_manual_task_instances(
422
+ self, manual_task: ManualTask, privacy_request: PrivacyRequest
423
+ ) -> None:
274
424
  """
275
- db = self.resources.session
276
- collection_address = self.execution_node.address
277
-
278
- # Validate manual task address
279
- if not ManualTaskAddress.is_manual_task_address(collection_address):
280
- raise ValueError(f"Invalid manual task address: {collection_address}")
281
-
282
- connection_key = ManualTaskAddress.get_connection_key(collection_address)
283
-
284
- # Fetch relevant manual tasks for this connection
285
- manual_task = get_manual_task_for_connection_config(db, connection_key)
286
- if not manual_task:
287
- # No manual tasks defined – nothing to erase
288
- self.log_end(ActionType.erasure)
289
- return 0
425
+ Clean up ManualTaskInstances for a manual task when conditional dependencies are not met.
290
426
 
291
- # Check if any manual tasks have ERASURE configs
292
- has_erasure_configs = [
293
- config
294
- for config in manual_task.configs
295
- if config.is_current
296
- and config.config_type
297
- == ManualTaskConfigurationType.erasure_privacy_request
427
+ This method removes any existing instances that were created before the conditional
428
+ dependency evaluation determined the task should not execute.
429
+ """
430
+ # Find all instances for this manual task and privacy request
431
+ instances_to_remove = [
432
+ instance
433
+ for instance in privacy_request.manual_task_instances
434
+ if instance.task_id == manual_task.id
298
435
  ]
299
436
 
300
- if not has_erasure_configs:
301
- # No erasure configs - complete immediately
302
- self.log_end(ActionType.erasure)
303
- return 0
437
+ if instances_to_remove:
304
438
 
305
- # Create ManualTaskInstances for ERASURE configs only
306
- self._ensure_manual_task_instances(
307
- db,
308
- manual_task,
309
- self.resources.request,
310
- ManualTaskConfigurationType.erasure_privacy_request,
311
- )
312
-
313
- # Check for full submissions – reuse helper used by access flow, filtering ERASURE configs
314
- submissions_complete = self._get_submitted_data(
315
- db,
316
- manual_task,
317
- self.resources.request,
318
- ManualTaskConfigurationType.erasure_privacy_request,
319
- )
320
-
321
- # If any field submissions are missing, pause processing
322
- if submissions_complete is None:
323
- if self.resources.request.status != PrivacyRequestStatus.requires_input:
324
- self.resources.request.status = PrivacyRequestStatus.requires_input
325
- self.resources.request.save(db)
326
- raise AwaitingAsyncTaskCallback(
327
- f"Manual erasure task for {connection_key} requires user input"
328
- )
329
-
330
- # Mark rows_masked = 0 (manual tasks do not mask data directly)
331
- if self.request_task.id:
332
- # Storing result for DSR 3.0; SQLAlchemy column typing triggers mypy warning
333
- self.request_task.rows_masked = 0 # type: ignore[assignment]
334
-
335
- # Mark successful completion
336
- self.log_end(ActionType.erasure)
337
- return 0
439
+ # Remove instances from the database
440
+ for instance in instances_to_remove:
441
+ instance.delete(self.resources.session)
442
+ self.resources.session.commit()
@@ -1,6 +1,5 @@
1
1
  from typing import Optional
2
2
 
3
- from loguru import logger
4
3
  from sqlalchemy.orm import Session
5
4
 
6
5
  from fides.api.graph.config import (
@@ -110,9 +109,6 @@ def create_conditional_dependency_scalar_fields(
110
109
  # Use the full field address as the field name to preserve collection context
111
110
  # This allows the manual task to receive data from specific collections
112
111
  # e.g., "user.name" or "customer.profile.email" instead of just "name" or "email"
113
- logger.info(
114
- f"Creating conditional dependency scalar field for field address: {field_address}"
115
- )
116
112
  field_address_obj = FieldAddress.from_string(field_address)
117
113
 
118
114
  scalar_field = ScalarField(
@@ -74,6 +74,7 @@ class DatabaseTask(Task): # pylint: disable=W0223
74
74
  keepalives_idle=CONFIG.database.task_engine_keepalives_idle,
75
75
  keepalives_interval=CONFIG.database.task_engine_keepalives_interval,
76
76
  keepalives_count=CONFIG.database.task_engine_keepalives_count,
77
+ pool_pre_ping=CONFIG.database.task_engine_pool_pre_ping,
77
78
  )
78
79
 
79
80
  # same for the sessionmaker
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict, Set
3
+ from typing import Any, Dict, Optional, Set
4
4
 
5
5
  from fides.api.common_exceptions import NoSuchConnectionTypeSecretSchemaError
6
6
  from fides.api.models.connectionconfig import ConnectionType
@@ -141,7 +141,9 @@ def get_connection_type_secret_schema(*, connection_type: str) -> dict[str, Any]
141
141
  Note that this does not return actual secrets, instead we return the *types* of
142
142
  secret fields needed to authenticate.
143
143
  """
144
- connection_system_types: list[ConnectionSystemTypeMap] = get_connection_types()
144
+ connection_system_types: list[ConnectionSystemTypeMap] = get_connection_types(
145
+ include_test_connections=True
146
+ )
145
147
  if not any(item.identifier == connection_type for item in connection_system_types):
146
148
  raise NoSuchConnectionTypeSecretSchemaError(
147
149
  f"No connection type found with name '{connection_type}'."
@@ -189,15 +191,15 @@ def get_connection_type_secret_schema(*, connection_type: str) -> dict[str, Any]
189
191
  return schema
190
192
 
191
193
 
192
- def get_connection_types(
193
- search: str | None = None,
194
- system_type: SystemType | None = None,
194
+ def _is_match(elem: str, search: Optional[str] = None) -> bool:
195
+ """If a search query param was included, is it a substring of an available connector type?"""
196
+ return search.lower() in elem.lower() if search else True
197
+
198
+
199
+ def get_saas_connection_types(
195
200
  action_types: Set[ActionType] = SUPPORTED_ACTION_TYPES,
201
+ search: Optional[str] = None,
196
202
  ) -> list[ConnectionSystemTypeMap]:
197
- def is_match(elem: str) -> bool:
198
- """If a search query param was included, is it a substring of an available connector type?"""
199
- return search.lower() in elem.lower() if search else True
200
-
201
203
  def saas_request_type_filter(connection_type: str) -> bool:
202
204
  """
203
205
  If any of the request type filters are set to true,
@@ -216,6 +218,45 @@ def get_connection_types(
216
218
  action_type in template.supported_actions for action_type in action_types
217
219
  )
218
220
 
221
+ saas_connection_types: list[ConnectionSystemTypeMap] = []
222
+ saas_types: list[str] = sorted(
223
+ [
224
+ saas_type
225
+ for saas_type in ConnectorRegistry.connector_types()
226
+ if _is_match(saas_type, search) and saas_request_type_filter(saas_type)
227
+ ]
228
+ )
229
+
230
+ for item in saas_types:
231
+ connector_template = ConnectorRegistry.get_connector_template(item)
232
+ if connector_template is not None:
233
+ saas_connection_types.append(
234
+ ConnectionSystemTypeMap(
235
+ identifier=item,
236
+ type=SystemType.saas,
237
+ human_readable=connector_template.human_readable,
238
+ encoded_icon=connector_template.icon,
239
+ authorization_required=connector_template.authorization_required,
240
+ user_guide=connector_template.user_guide,
241
+ supported_actions=connector_template.supported_actions,
242
+ )
243
+ )
244
+
245
+ return saas_connection_types
246
+
247
+
248
+ # FIXME: this function needs a refactor
249
+ def get_connection_types(
250
+ search: str | None = None,
251
+ system_type: SystemType | None = None,
252
+ action_types: Set[ActionType] = SUPPORTED_ACTION_TYPES,
253
+ include_test_connections: bool = False,
254
+ ) -> list[ConnectionSystemTypeMap]:
255
+ """
256
+ Returns a list of ConnectionSystemTypeMap objects that match the given search and system type.
257
+
258
+ If include_test_connections is True, test connections like test_website will be included in the response.
259
+ """
219
260
  connection_system_types: list[ConnectionSystemTypeMap] = []
220
261
  if (system_type == SystemType.database or system_type is None) and (
221
262
  ActionType.access in action_types or ActionType.erasure in action_types
@@ -238,7 +279,7 @@ def get_connection_types(
238
279
  ConnectionType.sovrn,
239
280
  ConnectionType.test_website,
240
281
  ]
241
- and is_match(conn_type.value)
282
+ and _is_match(conn_type.value, search)
242
283
  ],
243
284
  key=lambda x: x.value,
244
285
  )
@@ -254,28 +295,10 @@ def get_connection_types(
254
295
  ]
255
296
  )
256
297
  if system_type == SystemType.saas or system_type is None:
257
- saas_types: list[str] = sorted(
258
- [
259
- saas_type
260
- for saas_type in ConnectorRegistry.connector_types()
261
- if is_match(saas_type) and saas_request_type_filter(saas_type)
262
- ]
298
+ saas_connection_types = get_saas_connection_types(
299
+ action_types=action_types, search=search
263
300
  )
264
-
265
- for item in saas_types:
266
- connector_template = ConnectorRegistry.get_connector_template(item)
267
- if connector_template is not None:
268
- connection_system_types.append(
269
- ConnectionSystemTypeMap(
270
- identifier=item,
271
- type=SystemType.saas,
272
- human_readable=connector_template.human_readable,
273
- encoded_icon=connector_template.icon,
274
- authorization_required=connector_template.authorization_required,
275
- user_guide=connector_template.user_guide,
276
- supported_actions=connector_template.supported_actions,
277
- )
278
- )
301
+ connection_system_types.extend(saas_connection_types)
279
302
 
280
303
  if (system_type == SystemType.manual or system_type is None) and (
281
304
  ActionType.access in action_types or ActionType.erasure in action_types
@@ -285,7 +308,7 @@ def get_connection_types(
285
308
  manual_type.value
286
309
  for manual_type in ConnectionType
287
310
  if manual_type == ConnectionType.manual_webhook
288
- and is_match(manual_type.value)
311
+ and _is_match(manual_type.value, search)
289
312
  ]
290
313
  )
291
314
  connection_system_types.extend(
@@ -307,7 +330,7 @@ def get_connection_types(
307
330
  for email_type in ConnectionType
308
331
  if email_type
309
332
  in ERASURE_EMAIL_CONNECTOR_TYPES + CONSENT_EMAIL_CONNECTOR_TYPES
310
- and is_match(email_type.value)
333
+ and _is_match(email_type.value, search)
311
334
  and ( # include consent or erasure connectors if requested, respectively
312
335
  (
313
336
  ActionType.consent in action_types
@@ -339,4 +362,16 @@ def get_connection_types(
339
362
  ]
340
363
  )
341
364
 
365
+ if include_test_connections and (
366
+ system_type == SystemType.website or system_type is None
367
+ ):
368
+ connection_system_types.append(
369
+ ConnectionSystemTypeMap(
370
+ identifier=ConnectionType.test_website.value,
371
+ type=SystemType.website,
372
+ human_readable=ConnectionType.test_website.human_readable,
373
+ supported_actions=[],
374
+ )
375
+ )
376
+
342
377
  return connection_system_types
@@ -44,6 +44,11 @@ class DatabaseSettings(FidesSettings):
44
44
  default=5,
45
45
  description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
46
46
  )
47
+ api_engine_pool_pre_ping: bool = Field(
48
+ default=True,
49
+ description="If true, the engine will pre-ping connections to ensure they are still valid before using them.",
50
+ )
51
+
47
52
  # Async Engine Settings
48
53
  api_async_engine_keepalives_idle: Optional[int] = Field(
49
54
  default=None,
@@ -58,7 +63,7 @@ class DatabaseSettings(FidesSettings):
58
63
  description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
59
64
  )
60
65
  api_async_engine_pool_pre_ping: bool = Field(
61
- default=False,
66
+ default=True,
62
67
  description="If true, the async engine will pre-ping connections to ensure they are still valid before using them.",
63
68
  )
64
69
 
@@ -115,6 +120,10 @@ class DatabaseSettings(FidesSettings):
115
120
  default=5,
116
121
  description="Maximum number of TCP keepalive retries before the client considers the connection dead and closes it.",
117
122
  )
123
+ task_engine_pool_pre_ping: bool = Field(
124
+ default=True,
125
+ description="If true, the engine will pre-ping connections to ensure they are still valid before using them.",
126
+ )
118
127
  test_db: str = Field(
119
128
  default="default_test_db",
120
129
  description="Used instead of the 'db' value when the FIDES_TEST_MODE environment variable is set to True. Avoids overwriting production data.",