ethyca-fides 2.67.2rc0__py2.py3-none-any.whl → 2.67.3b0__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 (112) hide show
  1. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/METADATA +2 -2
  2. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/RECORD +112 -110
  3. fides/_version.py +3 -3
  4. fides/api/db/crud.py +24 -41
  5. fides/api/graph/traversal.py +1 -1
  6. fides/api/main.py +2 -1
  7. fides/api/models/detection_discovery/core.py +33 -20
  8. fides/api/models/manual_task/manual_task.py +3 -6
  9. fides/api/models/privacy_request/privacy_request.py +78 -0
  10. fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +3 -19
  11. fides/api/task/conditional_dependencies/evaluator.py +12 -19
  12. fides/api/task/conditional_dependencies/operators.py +150 -0
  13. fides/api/task/conditional_dependencies/schemas.py +51 -0
  14. fides/api/task/create_request_tasks.py +5 -3
  15. fides/api/task/filter_results.py +5 -1
  16. fides/api/task/manual/manual_task_address.py +4 -2
  17. fides/api/task/manual/manual_task_graph_task.py +87 -83
  18. fides/api/task/manual/manual_task_utils.py +84 -201
  19. fides/api/util/storage_util.py +19 -0
  20. fides/config/__init__.py +4 -0
  21. fides/config/config_proxy.py +7 -0
  22. fides/config/privacy_center_settings.py +17 -0
  23. fides/config/utils.py +1 -0
  24. fides/ui-build/static/admin/404.html +1 -1
  25. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-e0a755c69081fffa.js → [id]-330475705adbd36f.js} +1 -1
  26. fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → zmR4ZN0rEFYsCKjAR-Jzz}/_buildManifest.js +1 -1
  27. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  28. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  29. fides/ui-build/static/admin/add-systems.html +1 -1
  30. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  31. fides/ui-build/static/admin/consent/configure.html +1 -1
  32. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  33. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  34. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  35. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  36. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  37. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  38. fides/ui-build/static/admin/consent/properties.html +1 -1
  39. fides/ui-build/static/admin/consent/reporting.html +1 -1
  40. fides/ui-build/static/admin/consent.html +1 -1
  41. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  42. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  43. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  44. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  45. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  46. fides/ui-build/static/admin/data-catalog.html +1 -1
  47. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  48. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  49. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  50. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  51. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  52. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  53. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  54. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  55. fides/ui-build/static/admin/datamap.html +1 -1
  56. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  57. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  58. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  59. fides/ui-build/static/admin/dataset/new.html +1 -1
  60. fides/ui-build/static/admin/dataset.html +1 -1
  61. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  62. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  63. fides/ui-build/static/admin/datastore-connection.html +1 -1
  64. fides/ui-build/static/admin/index.html +1 -1
  65. fides/ui-build/static/admin/integrations/[id].html +1 -1
  66. fides/ui-build/static/admin/integrations.html +1 -1
  67. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  68. fides/ui-build/static/admin/login/[provider].html +1 -1
  69. fides/ui-build/static/admin/login.html +1 -1
  70. fides/ui-build/static/admin/messaging/[id].html +1 -1
  71. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  72. fides/ui-build/static/admin/messaging.html +1 -1
  73. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  74. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  75. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  76. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  77. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  78. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  79. fides/ui-build/static/admin/poc/forms.html +1 -1
  80. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  81. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  82. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  83. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  84. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  85. fides/ui-build/static/admin/privacy-requests.html +1 -1
  86. fides/ui-build/static/admin/properties/[id].html +1 -1
  87. fides/ui-build/static/admin/properties/add-property.html +1 -1
  88. fides/ui-build/static/admin/properties.html +1 -1
  89. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  90. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  91. fides/ui-build/static/admin/settings/about.html +1 -1
  92. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  93. fides/ui-build/static/admin/settings/consent.html +1 -1
  94. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  95. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  96. fides/ui-build/static/admin/settings/domains.html +1 -1
  97. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  98. fides/ui-build/static/admin/settings/locations.html +1 -1
  99. fides/ui-build/static/admin/settings/organization.html +1 -1
  100. fides/ui-build/static/admin/settings/regulations.html +1 -1
  101. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  102. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  103. fides/ui-build/static/admin/systems.html +1 -1
  104. fides/ui-build/static/admin/taxonomy.html +1 -1
  105. fides/ui-build/static/admin/user-management/new.html +1 -1
  106. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  107. fides/ui-build/static/admin/user-management.html +1 -1
  108. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/WHEEL +0 -0
  109. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/entry_points.txt +0 -0
  110. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/licenses/LICENSE +0 -0
  111. {ethyca_fides-2.67.2rc0.dist-info → ethyca_fides-2.67.3b0.dist-info}/top_level.txt +0 -0
  112. /fides/ui-build/static/admin/_next/static/{Y7C7V1jdXK7rWXDIdtD13 → zmR4ZN0rEFYsCKjAR-Jzz}/_ssgManifest.js +0 -0
@@ -7,11 +7,11 @@ from fides.api.common_exceptions import AwaitingAsyncTaskCallback
7
7
  from fides.api.models.attachment import AttachmentType
8
8
  from fides.api.models.manual_task import (
9
9
  ManualTask,
10
- ManualTaskConfig,
11
10
  ManualTaskConfigurationType,
12
11
  ManualTaskEntityType,
13
12
  ManualTaskFieldType,
14
13
  ManualTaskInstance,
14
+ ManualTaskSubmission,
15
15
  StatusType,
16
16
  )
17
17
  from fides.api.models.privacy_request import PrivacyRequest
@@ -23,6 +23,7 @@ from fides.api.task.manual.manual_task_utils import (
23
23
  get_manual_task_for_connection_config,
24
24
  )
25
25
  from fides.api.util.collection_util import Row
26
+ from fides.api.util.storage_util import format_size
26
27
 
27
28
 
28
29
  class ManualTaskGraphTask(GraphTask):
@@ -122,29 +123,36 @@ class ManualTaskGraphTask(GraphTask):
122
123
  # request has started, while allowing different config types (access vs erasure)
123
124
  # to have separate instances.
124
125
  # ------------------------------------------------------------------
125
- existing_task_instance = (
126
- db.query(ManualTaskInstance)
127
- .join(ManualTaskInstance.config) # Join to access config information
128
- .filter(
129
- ManualTaskInstance.task_id == manual_task.id,
130
- ManualTaskInstance.entity_id == privacy_request.id,
131
- ManualTaskInstance.entity_type == ManualTaskEntityType.privacy_request,
132
- # Only check for instances of the same config type
133
- ManualTaskConfig.config_type == allowed_config_type,
134
- )
135
- .first()
126
+ existing_task_instance = next(
127
+ (
128
+ instance
129
+ for instance in privacy_request.manual_task_instances
130
+ if instance.task_id == manual_task.id
131
+ and instance.config.config_type == allowed_config_type
132
+ ),
133
+ None,
136
134
  )
137
135
  if existing_task_instance:
138
136
  # An instance already exists for this privacy request and config type – no need
139
137
  # to create another one tied to a newer config version.
140
138
  return
141
139
 
142
- # Check each active config for instances (now we know none exist yet for this config type)
143
- for config in manual_task.configs:
144
- if not config.is_current or config.config_type != allowed_config_type:
145
- # Skip configs that are not current or not relevant for this request type
146
- continue
140
+ # If no existing instances, create a new one for the current config
141
+ # There will only be one config of each type per manual task
142
+ config = next(
143
+ (
144
+ config
145
+ for config in sorted(
146
+ manual_task.configs,
147
+ key=lambda c: c.version if hasattr(c, "version") else 0,
148
+ reverse=True,
149
+ )
150
+ if config.is_current and config.config_type == allowed_config_type
151
+ ),
152
+ None,
153
+ )
147
154
 
155
+ if config:
148
156
  ManualTaskInstance.create(
149
157
  db=db,
150
158
  data={
@@ -156,7 +164,6 @@ class ManualTaskGraphTask(GraphTask):
156
164
  },
157
165
  )
158
166
 
159
- # pylint: disable=too-many-branches,too-many-nested-blocks
160
167
  def _get_submitted_data(
161
168
  self,
162
169
  db: Session,
@@ -168,93 +175,90 @@ class ManualTaskGraphTask(GraphTask):
168
175
  Check if all manual task instances have submissions for ALL fields and return aggregated data
169
176
  Returns None if any field submissions are missing (all fields must be completed or skipped)
170
177
  """
171
- aggregated_data: dict[str, Any] = {}
172
-
173
- def _format_size(size_bytes: int) -> str:
174
- units = ["B", "KB", "MB", "GB", "TB"]
175
- size = float(size_bytes)
176
- for unit in units:
177
- if size < 1024.0:
178
- return f"{size:.1f} {unit}"
179
- size /= 1024.0
180
- return f"{size:.1f} PB"
181
-
182
- candidate_instances: list[ManualTaskInstance] = (
183
- db.query(ManualTaskInstance)
184
- .filter(
185
- ManualTaskInstance.task_id == manual_task.id,
186
- ManualTaskInstance.entity_id == privacy_request.id,
187
- ManualTaskInstance.entity_type == ManualTaskEntityType.privacy_request,
188
- )
189
- .all()
190
- )
178
+ candidate_instances: list[ManualTaskInstance] = [
179
+ instance
180
+ for instance in privacy_request.manual_task_instances
181
+ if instance.task_id == manual_task.id
182
+ and instance.config.config_type == allowed_config_type
183
+ ]
191
184
 
192
185
  if not candidate_instances:
193
186
  return None # No instance yet for this manual task
194
187
 
188
+ # Check for incomplete fields and update status in single pass
195
189
  for inst in candidate_instances:
196
- # Skip instances tied to other request types
197
- if not inst.config or inst.config.config_type != allowed_config_type:
198
- continue
199
-
200
- all_fields = inst.config.field_definitions or []
201
-
202
- # Every field must have a submission
203
- if not all(inst.get_submission_for_field(f.id) for f in all_fields):
190
+ if inst.incomplete_fields:
204
191
  return None # At least one instance still incomplete
205
192
 
206
- # Ensure status set
193
+ # Update status if needed
207
194
  if inst.status != StatusType.completed:
208
195
  inst.status = StatusType.completed
209
196
  inst.save(db)
210
197
 
211
- # Aggregate submission data from this instance
212
- for submission in inst.submissions:
213
- if not submission.field or not submission.field.field_key:
214
- continue
198
+ # Aggregate submission data from all instances
199
+ aggregated_data = self._aggregate_submission_data(candidate_instances)
200
+ return aggregated_data or None
201
+
202
+ def _aggregate_submission_data(
203
+ self, instances: list[ManualTaskInstance]
204
+ ) -> dict[str, Any]:
205
+ """Aggregate submission data from all instances into a single dictionary."""
206
+ aggregated_data: dict[str, Any] = {}
207
+
208
+ for inst in instances:
209
+ # Filter valid submissions and process them
210
+ valid_submissions = (
211
+ submission
212
+ for submission in inst.submissions
213
+ if (
214
+ submission.field
215
+ and submission.field.field_key
216
+ and isinstance(submission.data, dict)
217
+ )
218
+ )
215
219
 
220
+ for submission in valid_submissions:
216
221
  field_key = submission.field.field_key
222
+ # We already checked isinstance(submission.data, dict) in valid_submissions
223
+ data_dict: dict[str, Any] = submission.data # type: ignore[assignment]
224
+ field_type = data_dict.get("field_type")
217
225
 
218
- if not isinstance(submission.data, dict):
219
- continue
226
+ # Process field data based on type
227
+ aggregated_data[field_key] = (
228
+ self._process_attachment_field(submission)
229
+ if field_type == ManualTaskFieldType.attachment.value
230
+ else data_dict.get("value")
231
+ )
220
232
 
221
- data_dict: dict[str, Any] = submission.data
233
+ return aggregated_data
222
234
 
223
- field_type = data_dict.get("field_type")
235
+ def _process_attachment_field(
236
+ self, submission: ManualTaskSubmission
237
+ ) -> Optional[dict[str, dict[str, Any]]]:
238
+ """Process attachment field and return attachment map or None."""
239
+ attachment_map: dict[str, dict[str, Any]] = {}
224
240
 
225
- if field_type == ManualTaskFieldType.attachment.value:
226
- attachment_map: dict[str, dict[str, Any]] = {}
227
- for attachment in submission.attachments or []:
228
- if (
229
- attachment.attachment_type
230
- == AttachmentType.include_with_access_package
231
- ):
232
- try:
233
- size, url = attachment.retrieve_attachment()
234
- attachment_map[attachment.file_name] = {
235
- "url": str(url) if url else None,
236
- "size": (_format_size(size) if size else "Unknown"),
237
- }
238
- except (
239
- Exception
240
- ) as exc: # pylint: disable=broad-exception-caught
241
- logger.warning(
242
- "Error retrieving attachment {}: {}",
243
- attachment.file_name,
244
- str(exc),
245
- )
246
-
247
- aggregated_data[field_key] = attachment_map or None
248
- else:
249
- aggregated_data[field_key] = data_dict.get("value")
250
-
251
- return aggregated_data if aggregated_data else None
241
+ for attachment in filter(
242
+ lambda a: a.attachment_type == AttachmentType.include_with_access_package,
243
+ submission.attachments,
244
+ ):
245
+ try:
246
+ size, url = attachment.retrieve_attachment()
247
+ attachment_map[attachment.file_name] = {
248
+ "url": str(url) if url else None,
249
+ "size": (format_size(size) if size else "Unknown"),
250
+ }
251
+ except Exception as exc: # pylint: disable=broad-exception-caught
252
+ logger.warning(
253
+ f"Error retrieving attachment {attachment.file_name}: {str(exc)}"
254
+ )
255
+ return attachment_map or None
252
256
 
253
257
  def dry_run_task(self) -> int:
254
258
  """Return estimated row count for dry run - manual tasks don't have predictable counts"""
255
259
  return 1 # Placeholder - manual tasks generate variable data
256
260
 
257
- # NEW METHOD: Provide erasure support for manual tasks
261
+ # Provide erasure support for manual tasks
258
262
  @retry(action_type=ActionType.erasure, default_return=0)
259
263
  def erasure_request(
260
264
  self,
@@ -1,40 +1,43 @@
1
+ from typing import Optional
2
+
3
+ from loguru import logger
1
4
  from sqlalchemy.orm import Session
2
5
 
3
6
  from fides.api.graph.config import (
4
7
  Collection,
5
8
  CollectionAddress,
6
- Field,
9
+ FieldAddress,
7
10
  GraphDataset,
8
11
  ScalarField,
9
12
  )
10
- from fides.api.graph.graph import Node
11
- from fides.api.graph.traversal import TraversalNode
12
13
  from fides.api.models.connectionconfig import ConnectionConfig
13
14
 
14
15
  # Import application models
15
16
  from fides.api.models.manual_task import (
16
17
  ManualTask,
17
- ManualTaskConfig,
18
+ ManualTaskConditionalDependencyType,
18
19
  ManualTaskConfigurationType,
19
- ManualTaskEntityType,
20
- ManualTaskInstance,
21
20
  )
22
- from fides.api.models.privacy_request import PrivacyRequest
23
- from fides.api.schemas.policy import ActionType
24
21
  from fides.api.task.manual.manual_task_address import ManualTaskAddress
25
22
 
23
+ PRIVACY_REQUEST_CONFIG_TYPES = {
24
+ ManualTaskConfigurationType.access_privacy_request,
25
+ ManualTaskConfigurationType.erasure_privacy_request,
26
+ }
27
+
26
28
 
27
29
  def get_connection_configs_with_manual_tasks(db: Session) -> list[ConnectionConfig]:
28
30
  """
29
31
  Get all connection configs that have manual tasks.
30
32
  """
31
- return (
33
+ connection_configs = (
32
34
  db.query(ConnectionConfig)
33
35
  .join(ManualTask, ConnectionConfig.id == ManualTask.parent_entity_id)
34
36
  .filter(ManualTask.parent_entity_type == "connection_config")
35
37
  .filter(ConnectionConfig.disabled.is_(False))
36
38
  .all()
37
39
  )
40
+ return connection_configs
38
41
 
39
42
 
40
43
  def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
@@ -48,12 +51,11 @@ def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
48
51
  # Get all connection configs that have manual tasks (excluding disabled ones)
49
52
  connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
50
53
 
51
- # Create addresses for all connections that have manual tasks
52
- manual_task_addresses = []
53
- for config in connection_configs_with_manual_tasks:
54
- manual_task_addresses.append(ManualTaskAddress.create(config.key))
55
-
56
- return manual_task_addresses
54
+ # Return addresses for all connections that have manual tasks
55
+ return [
56
+ ManualTaskAddress.create(config.key)
57
+ for config in connection_configs_with_manual_tasks
58
+ ]
57
59
 
58
60
 
59
61
  def get_manual_task_for_connection_config(
@@ -73,20 +75,18 @@ def get_manual_task_for_connection_config(
73
75
  )
74
76
 
75
77
 
76
- def create_manual_data_traversal_node(
77
- db: Session, address: CollectionAddress
78
- ) -> "TraversalNode":
78
+ def create_data_category_scalar_fields(manual_task: ManualTask) -> list[ScalarField]:
79
79
  """
80
- Create a TraversalNode for a manual_data collection
80
+ Create scalar fields for each field in the given manual task configs.
81
81
  """
82
- connection_key = address.dataset
83
-
84
- # Get manual tasks for this connection to determine fields
85
- manual_task = get_manual_task_for_connection_config(db, connection_key)
86
-
87
- # Create fields based on ManualTaskConfigFields
88
- fields: list[Field] = []
89
- for config in manual_task.configs:
82
+ fields = []
83
+ # Get current privacy request configs for this manual task
84
+ current_configs = [
85
+ config
86
+ for config in manual_task.configs
87
+ if config.is_current and config.config_type in PRIVACY_REQUEST_CONFIG_TYPES
88
+ ]
89
+ for config in current_configs:
90
90
  for field in config.field_definitions:
91
91
  # Create a scalar field for each manual task field
92
92
  # Extract data categories from field metadata if available
@@ -99,211 +99,94 @@ def create_manual_data_traversal_node(
99
99
  # Manual task fields don't have complex relationships
100
100
  )
101
101
  fields.append(scalar_field)
102
+ return fields
103
+
104
+
105
+ def create_conditional_dependency_scalar_fields(
106
+ field_addresses: set[str],
107
+ ) -> list[ScalarField]:
108
+ fields: list[ScalarField] = []
109
+ for field_address in field_addresses:
110
+ # Use the full field address as the field name to preserve collection context
111
+ # This allows the manual task to receive data from specific collections
112
+ # 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
+ field_address_obj = FieldAddress.from_string(field_address)
102
117
 
103
- # Create a synthetic Collection
104
- collection = Collection(
105
- name=ManualTaskAddress.MANUAL_DATA_COLLECTION,
106
- fields=fields,
107
- # Manual tasks don't have complex dependencies
108
- after=set(),
109
- )
110
-
111
- # Create a synthetic GraphDataset
112
- dataset = GraphDataset(
113
- name=connection_key,
114
- collections=[collection],
115
- connection_key=connection_key,
116
- after=set(),
117
- )
118
-
119
- node = Node(dataset, collection)
120
- traversal_node = TraversalNode(node)
121
-
122
- return traversal_node
118
+ scalar_field = ScalarField(
119
+ name=field_address_obj.value,
120
+ # Conditional dependency fields don't have predefined data categories
121
+ data_categories=[],
122
+ references=[(field_address_obj, "from")],
123
+ )
124
+ fields.append(scalar_field)
123
125
 
126
+ return fields
124
127
 
125
- def create_manual_task_instances_for_privacy_request(
126
- db: Session, privacy_request: PrivacyRequest
127
- ) -> list[ManualTaskInstance]:
128
- """Create ManualTaskInstance entries for all active manual tasks relevant to a privacy request."""
129
- instances = []
130
128
 
131
- # Get all connection configs that have manual tasks (excluding disabled ones)
132
- connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
129
+ def create_collection_for_connection_key(
130
+ db: Session, connection_key: str
131
+ ) -> Optional[Collection]:
132
+ # Get the manual task for this connection config
133
+ manual_task = get_manual_task_for_connection_config(db, connection_key)
133
134
 
134
- # Determine the privacy request type based on policy rules
135
- has_access_rules = bool(
136
- privacy_request.policy.get_rules_for_action(action_type=ActionType.access)
137
- )
138
- has_erasure_rules = bool(
139
- privacy_request.policy.get_rules_for_action(action_type=ActionType.erasure)
135
+ if not manual_task:
136
+ return None
137
+
138
+ # Get conditional dependency field addresses - raw field data
139
+ conditional_field_addresses: set[str] = {
140
+ dependency.field_address
141
+ for dependency in manual_task.conditional_dependencies
142
+ if dependency.condition_type == ManualTaskConditionalDependencyType.leaf
143
+ and dependency.field_address is not None
144
+ }
145
+
146
+ # Create scalar fields for data category fields and conditional dependency field addresses
147
+ fields: list[ScalarField] = []
148
+ fields.extend(create_data_category_scalar_fields(manual_task))
149
+ fields.extend(
150
+ create_conditional_dependency_scalar_fields(conditional_field_addresses)
140
151
  )
141
152
 
142
- for connection_config in connection_configs_with_manual_tasks:
143
- manual_tasks = (
144
- db.query(ManualTask)
145
- .filter(
146
- ManualTask.parent_entity_id == connection_config.id,
147
- ManualTask.parent_entity_type == "connection_config",
148
- )
149
- .all()
150
- )
153
+ # Only create collection if there are fields
154
+ if not fields:
155
+ return None
151
156
 
152
- for manual_task in manual_tasks:
153
- # Get the active config for this manual task, filtered by request type
154
- active_config_query = db.query(ManualTaskConfig).filter(
155
- ManualTaskConfig.task_id == manual_task.id,
156
- ManualTaskConfig.is_current.is_(True),
157
- )
158
-
159
- # Filter by configuration type based on privacy request type
160
- if has_access_rules and has_erasure_rules:
161
- # If both access and erasure rules exist, include both types
162
- active_config_query = active_config_query.filter(
163
- ManualTaskConfig.config_type.in_(
164
- [
165
- ManualTaskConfigurationType.access_privacy_request,
166
- ManualTaskConfigurationType.erasure_privacy_request,
167
- ]
168
- )
169
- )
170
- elif has_access_rules:
171
- # Only access rules - only include access configurations
172
- active_config_query = active_config_query.filter(
173
- ManualTaskConfig.config_type
174
- == ManualTaskConfigurationType.access_privacy_request
175
- )
176
- elif has_erasure_rules:
177
- # Only erasure rules - only include erasure configurations
178
- active_config_query = active_config_query.filter(
179
- ManualTaskConfig.config_type
180
- == ManualTaskConfigurationType.erasure_privacy_request
181
- )
182
- else:
183
- # No relevant rules - skip this manual task
184
- continue
185
-
186
- active_configs = active_config_query.all()
187
-
188
- if not active_configs:
189
- continue # Skip if no active configs
190
-
191
- # Create instances for each active config
192
- for active_config in active_configs:
193
- # Check if instance already exists for this config
194
- existing_instance = (
195
- db.query(ManualTaskInstance)
196
- .filter(
197
- ManualTaskInstance.entity_id == privacy_request.id,
198
- ManualTaskInstance.entity_type == "privacy_request",
199
- ManualTaskInstance.task_id == manual_task.id,
200
- ManualTaskInstance.config_id == active_config.id,
201
- )
202
- .first()
203
- )
204
-
205
- if not existing_instance:
206
- instance = ManualTaskInstance(
207
- entity_id=privacy_request.id,
208
- entity_type=ManualTaskEntityType.privacy_request,
209
- task_id=manual_task.id,
210
- config_id=active_config.id,
211
- )
212
- db.add(instance)
213
- instances.append(instance)
214
-
215
- if instances:
216
- db.commit()
217
-
218
- return instances
219
-
220
-
221
- def get_manual_task_instances_for_privacy_request(
222
- db: Session, privacy_request: PrivacyRequest
223
- ) -> list[ManualTaskInstance]:
224
- """Get all manual task instances for a privacy request."""
225
- return (
226
- db.query(ManualTaskInstance)
227
- .filter(
228
- ManualTaskInstance.entity_id == privacy_request.id,
229
- ManualTaskInstance.entity_type == "privacy_request",
230
- )
231
- .all()
232
- )
157
+ return Collection(name=ManualTaskAddress.MANUAL_DATA_COLLECTION, fields=fields)
233
158
 
234
159
 
235
- def create_manual_task_artificial_graphs(
236
- db: Session,
237
- ) -> list:
160
+ def create_manual_task_artificial_graphs(db: Session) -> list[GraphDataset]:
238
161
  """
239
162
  Create artificial GraphDataset objects for manual tasks that can be included
240
163
  in the main dataset graph during the dataset configuration phase.
241
164
 
242
- Manual tasks should be treated as data sources/datasets rather than being
243
- appended to the traversal graph later.
244
-
245
- Manual task collections are designed as root nodes that execute immediately when
246
- the privacy request starts, in parallel with identity processing. They don't depend
247
- on identity data since they provide manually-entered data rather than consuming it.
165
+ Each manual task gets its own collection with its own dependencies based on
166
+ its specific conditional dependencies. This allows individual manual tasks
167
+ to receive only the data they need from regular tasks.
248
168
 
249
169
  Args:
250
170
  db: Database session
251
- policy: The policy being executed (optional, for filtering manual task configs)
252
171
 
253
172
  Returns:
254
- List of GraphDataset objects representing manual tasks as root nodes
173
+ List of GraphDataset objects representing manual tasks as individual collections
255
174
  """
256
-
257
175
  manual_task_graphs = []
258
176
  manual_addresses = get_manual_task_addresses(db)
259
177
 
260
178
  for address in manual_addresses:
261
179
  connection_key = address.dataset
262
180
 
263
- # Get manual tasks for this connection to determine fields
264
- manual_task = get_manual_task_for_connection_config(db, connection_key)
265
-
266
- # Create fields based only on ManualTaskConfigFields
267
- fields: list = []
268
-
269
- # Manual task collections act as root nodes - they don't need identity dependencies
270
- # since they provide manually-entered data rather than consuming identity data.
271
- current_configs = [
272
- config for config in manual_task.configs if config.is_current
273
- ]
274
- for config in current_configs:
275
- if config.config_type not in [
276
- ManualTaskConfigurationType.access_privacy_request,
277
- ManualTaskConfigurationType.erasure_privacy_request,
278
- ]:
279
- continue
280
-
281
- for field in config.field_definitions:
282
- # Create a scalar field for each manual task field
283
- field_metadata = field.field_metadata or {}
284
- data_categories = field_metadata.get("data_categories", [])
285
-
286
- scalar_field = ScalarField(
287
- name=field.field_key,
288
- data_categories=data_categories,
289
- )
290
- fields.append(scalar_field)
291
-
292
- if fields: # Only create graph if there are fields
293
- # Create a synthetic Collection
294
- collection = Collection(
295
- name=ManualTaskAddress.MANUAL_DATA_COLLECTION,
296
- fields=fields,
297
- # Manual tasks have no dependencies - they're root nodes
298
- after=set(),
299
- )
181
+ # Get the collection for this connection config using the reusable function
182
+ collection = create_collection_for_connection_key(db, connection_key)
300
183
 
301
- # Create a synthetic GraphDataset
184
+ if collection: # Only create graph if there are collections
185
+ # Create a synthetic GraphDataset with all manual task collections
302
186
  graph_dataset = GraphDataset(
303
187
  name=connection_key,
304
188
  collections=[collection],
305
189
  connection_key=connection_key,
306
- after=set(),
307
190
  )
308
191
 
309
192
  manual_task_graphs.append(graph_dataset)
@@ -14,6 +14,25 @@ from fides.api.schemas.storage.storage_secrets_docs_only import possible_storage
14
14
  from fides.api.util.custom_json_encoder import CustomJSONEncoder
15
15
 
16
16
 
17
+ def format_size(size_bytes: float) -> str:
18
+ """
19
+ Format size in bytes to human readable format.
20
+
21
+ Args:
22
+ size_bytes: Size in bytes
23
+
24
+ Returns:
25
+ Formatted string with appropriate unit (B, KB, MB, GB, TB, PB)
26
+ """
27
+ units = ["B", "KB", "MB", "GB", "TB"]
28
+ size = float(size_bytes)
29
+ for unit in units:
30
+ if size < 1024.0:
31
+ return f"{size:.1f} {unit}"
32
+ size /= 1024.0
33
+ return f"{size:.1f} PB"
34
+
35
+
17
36
  def get_schema_for_secrets(
18
37
  storage_type: Union[StorageType, str],
19
38
  secrets: possible_storage_secrets,
fides/config/__init__.py CHANGED
@@ -29,6 +29,7 @@ from .fides_settings import FidesSettings
29
29
  from .helpers import handle_deprecated_env_variables, handle_deprecated_fields
30
30
  from .logging_settings import LoggingSettings
31
31
  from .notification_settings import NotificationSettings
32
+ from .privacy_center_settings import PrivacyCenterSettings
32
33
  from .redis_settings import RedisSettings
33
34
  from .security_settings import SecuritySettings
34
35
  from .user_settings import UserSettings
@@ -82,6 +83,7 @@ class FidesConfig(FidesSettings):
82
83
  logging: LoggingSettings
83
84
  notifications: NotificationSettings
84
85
  redis: RedisSettings
86
+ privacy_center: PrivacyCenterSettings
85
87
  security: SecuritySettings
86
88
  user: UserSettings
87
89
 
@@ -111,6 +113,7 @@ class FidesConfig(FidesSettings):
111
113
  self.security,
112
114
  self.execution,
113
115
  self.admin_ui,
116
+ self.privacy_center,
114
117
  ]:
115
118
  for key, value in settings.model_dump(mode="json").items(): # type: ignore
116
119
  log.debug(
@@ -161,6 +164,7 @@ def build_config(config_dict: Dict[str, Any]) -> FidesConfig:
161
164
  "execution": ExecutionSettings,
162
165
  "logging": LoggingSettings,
163
166
  "notifications": NotificationSettings,
167
+ "privacy_center": PrivacyCenterSettings,
164
168
  "redis": RedisSettings,
165
169
  "security": SecuritySettings,
166
170
  "user": UserSettings,