ethyca-fides 2.67.1b1__py2.py3-none-any.whl → 2.67.1rc0__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 (105) hide show
  1. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/RECORD +103 -103
  3. fides/_version.py +3 -3
  4. fides/api/models/manual_task/manual_task.py +6 -3
  5. fides/api/models/privacy_request/privacy_request.py +0 -78
  6. fides/api/service/connectors/sql_connector.py +19 -3
  7. fides/api/service/privacy_request/dsr_package/dsr_report_builder.py +19 -3
  8. fides/api/task/create_request_tasks.py +3 -5
  9. fides/api/task/filter_results.py +1 -1
  10. fides/api/task/manual/manual_task_address.py +2 -4
  11. fides/api/task/manual/manual_task_graph_task.py +83 -87
  12. fides/api/task/manual/manual_task_utils.py +140 -84
  13. fides/api/util/storage_util.py +0 -19
  14. fides/ui-build/static/admin/404.html +1 -1
  15. fides/ui-build/static/admin/_next/static/chunks/{6662-a9e54ead3dc53644.js → 6662-ef0edd3598b1d431.js} +1 -1
  16. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-3e5725cd06d7fe6c.js +1 -0
  17. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-e0a755c69081fffa.js +1 -0
  18. fides/ui-build/static/admin/_next/static/{sFDtXdzSc8wR5M8bdSVpg → g1jUdXFOv2xVqdiNu2h1M}/_buildManifest.js +1 -1
  19. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  20. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  21. fides/ui-build/static/admin/add-systems.html +1 -1
  22. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  23. fides/ui-build/static/admin/consent/configure.html +1 -1
  24. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  25. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  26. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  27. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  28. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  29. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  30. fides/ui-build/static/admin/consent/properties.html +1 -1
  31. fides/ui-build/static/admin/consent/reporting.html +1 -1
  32. fides/ui-build/static/admin/consent.html +1 -1
  33. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  34. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  35. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  36. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  37. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  38. fides/ui-build/static/admin/data-catalog.html +1 -1
  39. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  40. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  41. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  42. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  43. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  44. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  45. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  46. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  47. fides/ui-build/static/admin/datamap.html +1 -1
  48. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  49. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  50. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  51. fides/ui-build/static/admin/dataset/new.html +1 -1
  52. fides/ui-build/static/admin/dataset.html +1 -1
  53. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  54. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  55. fides/ui-build/static/admin/datastore-connection.html +1 -1
  56. fides/ui-build/static/admin/index.html +1 -1
  57. fides/ui-build/static/admin/integrations/[id].html +1 -1
  58. fides/ui-build/static/admin/integrations.html +1 -1
  59. fides/ui-build/static/admin/login/[provider].html +1 -1
  60. fides/ui-build/static/admin/login.html +1 -1
  61. fides/ui-build/static/admin/messaging/[id].html +1 -1
  62. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  63. fides/ui-build/static/admin/messaging.html +1 -1
  64. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  65. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  66. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  67. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  68. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  69. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  70. fides/ui-build/static/admin/poc/forms.html +1 -1
  71. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  72. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  73. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  74. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  75. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  76. fides/ui-build/static/admin/privacy-requests.html +1 -1
  77. fides/ui-build/static/admin/properties/[id].html +1 -1
  78. fides/ui-build/static/admin/properties/add-property.html +1 -1
  79. fides/ui-build/static/admin/properties.html +1 -1
  80. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  81. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  82. fides/ui-build/static/admin/settings/about.html +1 -1
  83. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  84. fides/ui-build/static/admin/settings/consent.html +1 -1
  85. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  86. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  87. fides/ui-build/static/admin/settings/domains.html +1 -1
  88. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  89. fides/ui-build/static/admin/settings/locations.html +1 -1
  90. fides/ui-build/static/admin/settings/organization.html +1 -1
  91. fides/ui-build/static/admin/settings/regulations.html +1 -1
  92. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  93. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  94. fides/ui-build/static/admin/systems.html +1 -1
  95. fides/ui-build/static/admin/taxonomy.html +1 -1
  96. fides/ui-build/static/admin/user-management/new.html +1 -1
  97. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  98. fides/ui-build/static/admin/user-management.html +1 -1
  99. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-a286affa43687eb5.js +0 -1
  100. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-8e346fb36e8034d2.js +0 -1
  101. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/WHEEL +0 -0
  102. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/entry_points.txt +0 -0
  103. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/licenses/LICENSE +0 -0
  104. {ethyca_fides-2.67.1b1.dist-info → ethyca_fides-2.67.1rc0.dist-info}/top_level.txt +0 -0
  105. /fides/ui-build/static/admin/_next/static/{sFDtXdzSc8wR5M8bdSVpg → g1jUdXFOv2xVqdiNu2h1M}/_ssgManifest.js +0 -0
@@ -1,5 +1,3 @@
1
- from typing import Union
2
-
3
1
  from fides.api.graph.config import CollectionAddress
4
2
 
5
3
 
@@ -22,7 +20,7 @@ class ManualTaskAddress:
22
20
  return collection_name == ManualTaskAddress.MANUAL_DATA_COLLECTION
23
21
 
24
22
  @staticmethod
25
- def is_manual_task_address(address: Union[str, CollectionAddress]) -> bool:
23
+ def is_manual_task_address(address: CollectionAddress) -> bool:
26
24
  """Check if address represents manual task data"""
27
25
  if isinstance(address, str):
28
26
  # Handle string format "connection_key:collection_name"
@@ -35,7 +33,7 @@ class ManualTaskAddress:
35
33
  return ManualTaskAddress._is_manual_data_collection(address.collection)
36
34
 
37
35
  @staticmethod
38
- def get_connection_key(address: Union[str, CollectionAddress]) -> str:
36
+ def get_connection_key(address: CollectionAddress) -> str:
39
37
  """Extract connection config key from manual task address"""
40
38
  if not ManualTaskAddress.is_manual_task_address(address):
41
39
  raise ValueError(f"Not a manual task address: {address}")
@@ -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,
10
11
  ManualTaskConfigurationType,
11
12
  ManualTaskEntityType,
12
13
  ManualTaskFieldType,
13
14
  ManualTaskInstance,
14
- ManualTaskSubmission,
15
15
  StatusType,
16
16
  )
17
17
  from fides.api.models.privacy_request import PrivacyRequest
@@ -23,7 +23,6 @@ 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
27
26
 
28
27
 
29
28
  class ManualTaskGraphTask(GraphTask):
@@ -123,36 +122,29 @@ class ManualTaskGraphTask(GraphTask):
123
122
  # request has started, while allowing different config types (access vs erasure)
124
123
  # to have separate instances.
125
124
  # ------------------------------------------------------------------
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,
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()
134
136
  )
135
137
  if existing_task_instance:
136
138
  # An instance already exists for this privacy request and config type – no need
137
139
  # to create another one tied to a newer config version.
138
140
  return
139
141
 
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
- )
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
154
147
 
155
- if config:
156
148
  ManualTaskInstance.create(
157
149
  db=db,
158
150
  data={
@@ -164,6 +156,7 @@ class ManualTaskGraphTask(GraphTask):
164
156
  },
165
157
  )
166
158
 
159
+ # pylint: disable=too-many-branches,too-many-nested-blocks
167
160
  def _get_submitted_data(
168
161
  self,
169
162
  db: Session,
@@ -175,90 +168,93 @@ class ManualTaskGraphTask(GraphTask):
175
168
  Check if all manual task instances have submissions for ALL fields and return aggregated data
176
169
  Returns None if any field submissions are missing (all fields must be completed or skipped)
177
170
  """
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
- ]
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
+ )
184
191
 
185
192
  if not candidate_instances:
186
193
  return None # No instance yet for this manual task
187
194
 
188
- # Check for incomplete fields and update status in single pass
189
195
  for inst in candidate_instances:
190
- if inst.incomplete_fields:
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):
191
204
  return None # At least one instance still incomplete
192
205
 
193
- # Update status if needed
206
+ # Ensure status set
194
207
  if inst.status != StatusType.completed:
195
208
  inst.status = StatusType.completed
196
209
  inst.save(db)
197
210
 
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
- )
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
219
215
 
220
- for submission in valid_submissions:
221
216
  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")
225
217
 
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
- )
218
+ if not isinstance(submission.data, dict):
219
+ continue
232
220
 
233
- return aggregated_data
221
+ data_dict: dict[str, Any] = submission.data
234
222
 
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]] = {}
223
+ field_type = data_dict.get("field_type")
240
224
 
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
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
256
252
 
257
253
  def dry_run_task(self) -> int:
258
254
  """Return estimated row count for dry run - manual tasks don't have predictable counts"""
259
255
  return 1 # Placeholder - manual tasks generate variable data
260
256
 
261
- # Provide erasure support for manual tasks
257
+ # NEW METHOD: Provide erasure support for manual tasks
262
258
  @retry(action_type=ActionType.erasure, default_return=0)
263
259
  def erasure_request(
264
260
  self,
@@ -1,4 +1,3 @@
1
- from loguru import logger
2
1
  from sqlalchemy.orm import Session
3
2
 
4
3
  from fides.api.graph.config import (
@@ -13,7 +12,15 @@ from fides.api.graph.traversal import TraversalNode
13
12
  from fides.api.models.connectionconfig import ConnectionConfig
14
13
 
15
14
  # Import application models
16
- from fides.api.models.manual_task import ManualTask, ManualTaskConfigurationType
15
+ from fides.api.models.manual_task import (
16
+ ManualTask,
17
+ ManualTaskConfig,
18
+ ManualTaskConfigurationType,
19
+ ManualTaskEntityType,
20
+ ManualTaskInstance,
21
+ )
22
+ from fides.api.models.privacy_request import PrivacyRequest
23
+ from fides.api.schemas.policy import ActionType
17
24
  from fides.api.task.manual.manual_task_address import ManualTaskAddress
18
25
 
19
26
 
@@ -21,18 +28,13 @@ def get_connection_configs_with_manual_tasks(db: Session) -> list[ConnectionConf
21
28
  """
22
29
  Get all connection configs that have manual tasks.
23
30
  """
24
- logger.info("Querying for connection configs with manual tasks")
25
- connection_configs = (
31
+ return (
26
32
  db.query(ConnectionConfig)
27
33
  .join(ManualTask, ConnectionConfig.id == ManualTask.parent_entity_id)
28
34
  .filter(ManualTask.parent_entity_type == "connection_config")
29
35
  .filter(ConnectionConfig.disabled.is_(False))
30
36
  .all()
31
37
  )
32
- logger.info(
33
- f"Found {len(connection_configs)} connection configs with manual tasks: {[cc.key for cc in connection_configs]}"
34
- )
35
- return connection_configs
36
38
 
37
39
 
38
40
  def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
@@ -45,19 +47,12 @@ def get_manual_task_addresses(db: Session) -> list[CollectionAddress]:
45
47
  """
46
48
  # Get all connection configs that have manual tasks (excluding disabled ones)
47
49
  connection_configs_with_manual_tasks = get_connection_configs_with_manual_tasks(db)
48
- logger.debug(
49
- f"Found {len(connection_configs_with_manual_tasks)} connection configs with manual tasks"
50
- )
51
50
 
52
51
  # Create addresses for all connections that have manual tasks
53
52
  manual_task_addresses = []
54
53
  for config in connection_configs_with_manual_tasks:
55
- logger.info(f"Creating manual task address for connection config: {config.key}")
56
54
  manual_task_addresses.append(ManualTaskAddress.create(config.key))
57
55
 
58
- logger.info(
59
- f"Created {len(manual_task_addresses)} manual task addresses: {manual_task_addresses}"
60
- )
61
56
  return manual_task_addresses
62
57
 
63
58
 
@@ -67,11 +62,7 @@ def get_manual_task_for_connection_config(
67
62
  """Get the ManualTask for a specific connection config,
68
63
  the manual task/connection config relationship is 1:1.
69
64
  """
70
- logger.info(
71
- f"Looking for manual task for connection config: {connection_config_key}"
72
- )
73
-
74
- manual_task = (
65
+ return (
75
66
  db.query(ManualTask)
76
67
  .join(ConnectionConfig, ManualTask.parent_entity_id == ConnectionConfig.id)
77
68
  .filter(
@@ -81,17 +72,6 @@ def get_manual_task_for_connection_config(
81
72
  .one_or_none()
82
73
  )
83
74
 
84
- if manual_task:
85
- logger.info(
86
- f"Found manual task {manual_task.id} for connection {connection_config_key}"
87
- )
88
- else:
89
- logger.warning(
90
- f"No manual task found for connection config: {connection_config_key}"
91
- )
92
-
93
- return manual_task
94
-
95
75
 
96
76
  def create_manual_data_traversal_node(
97
77
  db: Session, address: CollectionAddress
@@ -142,6 +122,116 @@ def create_manual_data_traversal_node(
142
122
  return traversal_node
143
123
 
144
124
 
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
+
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)
133
+
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)
140
+ )
141
+
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
+ )
151
+
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
+ )
233
+
234
+
145
235
  def create_manual_task_artificial_graphs(
146
236
  db: Session,
147
237
  ) -> list:
@@ -164,18 +254,11 @@ def create_manual_task_artificial_graphs(
164
254
  List of GraphDataset objects representing manual tasks as root nodes
165
255
  """
166
256
 
167
- logger.debug("Creating manual task artificial graphs")
168
257
  manual_task_graphs = []
169
258
  manual_addresses = get_manual_task_addresses(db)
170
- logger.debug(
171
- f"Found {len(manual_addresses)} manual task addresses: {manual_addresses}"
172
- )
173
259
 
174
260
  for address in manual_addresses:
175
261
  connection_key = address.dataset
176
- logger.debug(
177
- f"Processing manual task address: {address} for connection: {connection_key}"
178
- )
179
262
 
180
263
  # Get manual tasks for this connection to determine fields
181
264
  manual_task = get_manual_task_for_connection_config(db, connection_key)
@@ -185,47 +268,28 @@ def create_manual_task_artificial_graphs(
185
268
 
186
269
  # Manual task collections act as root nodes - they don't need identity dependencies
187
270
  # since they provide manually-entered data rather than consuming identity data.
188
- if manual_task:
189
- logger.debug(
190
- f"Processing manual task {manual_task.id} with {len(manual_task.configs)} configs"
191
- )
192
- current_configs = [
193
- config
194
- for config in manual_task.configs
195
- if config.is_current
196
- and config.config_type
197
- in [
198
- ManualTaskConfigurationType.access_privacy_request,
199
- ManualTaskConfigurationType.erasure_privacy_request,
200
- ]
201
- ]
202
- logger.debug(
203
- f"Found {len(current_configs)} current configs for manual task {manual_task.id}"
204
- )
205
-
206
- for config in current_configs:
207
- logger.debug(
208
- f"Processing config {config.id} with {len(config.field_definitions)} fields"
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,
209
289
  )
210
- for field in config.field_definitions:
211
- # Create a scalar field for each manual task field
212
- field_metadata = field.field_metadata or {}
213
- data_categories = field_metadata.get("data_categories", [])
214
-
215
- scalar_field = ScalarField(
216
- name=field.field_key,
217
- data_categories=data_categories,
218
- )
219
- fields.append(scalar_field)
220
- else:
221
- logger.warning(
222
- f"No manual task found for connection {connection_key}, skipping"
223
- )
290
+ fields.append(scalar_field)
224
291
 
225
292
  if fields: # Only create graph if there are fields
226
- logger.debug(
227
- f"Creating graph for connection {connection_key} with {len(fields)} fields"
228
- )
229
293
  # Create a synthetic Collection
230
294
  collection = Collection(
231
295
  name=ManualTaskAddress.MANUAL_DATA_COLLECTION,
@@ -243,13 +307,5 @@ def create_manual_task_artificial_graphs(
243
307
  )
244
308
 
245
309
  manual_task_graphs.append(graph_dataset)
246
- logger.debug(
247
- f"Successfully created manual task graph for connection {connection_key}"
248
- )
249
- else:
250
- logger.warning(
251
- f"No fields found for connection {connection_key}, skipping graph creation"
252
- )
253
310
 
254
- logger.debug(f"Created {len(manual_task_graphs)} manual task graphs")
255
311
  return manual_task_graphs
@@ -14,25 +14,6 @@ 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
-
36
17
  def get_schema_for_secrets(
37
18
  storage_type: Union[StorageType, str],
38
19
  secrets: possible_storage_secrets,
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d9924caa849931b3.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d9924caa849931b3.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-90e8ec1fc5c6455b.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-090643377c8254e6.js" defer=""></script><script src="/_next/static/chunks/pages/_app-750d6bd16c971bb9.js" defer=""></script><script src="/_next/static/chunks/pages/404-2d803dab6a00f353.js" defer=""></script><script src="/_next/static/sFDtXdzSc8wR5M8bdSVpg/_buildManifest.js" defer=""></script><script src="/_next/static/sFDtXdzSc8wR5M8bdSVpg/_ssgManifest.js" defer=""></script><style>.data-ant-cssinjs-cache-path{content:"";}</style></head><body><div id="__next"><div style="height:100%;display:flex"></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/404","query":{},"buildId":"sFDtXdzSc8wR5M8bdSVpg","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>
1
+ <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d9924caa849931b3.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d9924caa849931b3.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-90e8ec1fc5c6455b.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-090643377c8254e6.js" defer=""></script><script src="/_next/static/chunks/pages/_app-750d6bd16c971bb9.js" defer=""></script><script src="/_next/static/chunks/pages/404-2d803dab6a00f353.js" defer=""></script><script src="/_next/static/g1jUdXFOv2xVqdiNu2h1M/_buildManifest.js" defer=""></script><script src="/_next/static/g1jUdXFOv2xVqdiNu2h1M/_ssgManifest.js" defer=""></script><style>.data-ant-cssinjs-cache-path{content:"";}</style></head><body><div id="__next"><div style="height:100%;display:flex"></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/404","query":{},"buildId":"g1jUdXFOv2xVqdiNu2h1M","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>