ethyca-fides 2.71.1b1__py2.py3-none-any.whl → 2.71.1rc1__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 (194) hide show
  1. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/METADATA +2 -2
  2. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/RECORD +170 -159
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/4bfbeff34611_add_polling_status.py +68 -0
  5. fides/api/alembic/migrations/versions/7db29f9cd77b_create_new_sub_request_table.py +95 -0
  6. fides/api/alembic/migrations/versions/b97e92b038d2_add_digest_execution_model.py +117 -0
  7. fides/api/api/v1/endpoints/generic_overrides.py +3 -9
  8. fides/api/common_exceptions.py +4 -0
  9. fides/api/main.py +2 -2
  10. fides/api/models/attachment.py +1 -0
  11. fides/api/models/digest/__init__.py +2 -0
  12. fides/api/models/digest/digest_config.py +10 -1
  13. fides/api/models/digest/digest_execution.py +132 -0
  14. fides/api/models/event_audit.py +8 -0
  15. fides/api/models/privacy_notice.py +0 -8
  16. fides/api/models/privacy_request/request_task.py +98 -1
  17. fides/api/models/worker_task.py +8 -0
  18. fides/api/schemas/saas/async_polling_configuration.py +81 -0
  19. fides/api/schemas/saas/saas_config.py +10 -3
  20. fides/api/schemas/saas/strategy_configuration.py +0 -12
  21. fides/api/service/async_dsr/handlers/__init__.py +0 -0
  22. fides/api/service/async_dsr/handlers/polling_attachment_handler.py +155 -0
  23. fides/api/service/async_dsr/handlers/polling_request_handler.py +88 -0
  24. fides/api/service/async_dsr/handlers/polling_response_handler.py +261 -0
  25. fides/api/service/async_dsr/handlers/polling_sub_request_handler.py +123 -0
  26. fides/api/service/async_dsr/strategies/__init__.py +0 -0
  27. fides/api/service/async_dsr/strategies/async_dsr_strategy.py +52 -0
  28. fides/api/service/async_dsr/strategies/async_dsr_strategy_callback.py +199 -0
  29. fides/api/service/async_dsr/strategies/async_dsr_strategy_factory.py +72 -0
  30. fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +678 -0
  31. fides/api/service/async_dsr/utils.py +130 -0
  32. fides/api/service/connectors/fides/fides_client.py +63 -1
  33. fides/api/service/connectors/query_configs/saas_query_config.py +4 -5
  34. fides/api/service/connectors/saas_connector.py +77 -69
  35. fides/api/service/privacy_request/attachment_handling.py +9 -2
  36. fides/api/service/privacy_request/request_runner_service.py +9 -83
  37. fides/api/service/privacy_request/request_service.py +47 -74
  38. fides/api/service/saas_request/saas_request_override_factory.py +66 -1
  39. fides/api/task/execute_request_tasks.py +5 -2
  40. fides/api/task/filter_results.py +35 -2
  41. fides/api/task/graph_task.py +34 -2
  42. fides/config/execution_settings.py +7 -3
  43. fides/service/dataset/dataset_service.py +0 -39
  44. fides/service/privacy_request/privacy_request_service.py +48 -103
  45. fides/ui-build/static/admin/404.html +1 -1
  46. fides/ui-build/static/admin/_next/static/{_IxwgneyQjdSaZFEF3Tqu → AfNel282iPq07N-lE1Vzx}/_buildManifest.js +1 -1
  47. fides/ui-build/static/admin/_next/static/chunks/155-c1ae010c664e2245.js +1 -0
  48. fides/ui-build/static/admin/_next/static/chunks/{3585-f728d32fda6f1ac1.js → 3585-efd5d41f08e180c4.js} +1 -1
  49. fides/ui-build/static/admin/_next/static/chunks/3855-12ee1dfbbe47fd28.js +1 -0
  50. fides/ui-build/static/admin/_next/static/chunks/409-c1256ecda1b15db6.js +1 -0
  51. fides/ui-build/static/admin/_next/static/chunks/4558-de5ced790b3380dc.js +1 -0
  52. fides/ui-build/static/admin/_next/static/chunks/4608-a9941d0c236ebca1.js +1 -0
  53. fides/ui-build/static/admin/_next/static/chunks/{4718-3a412bdb90add82f.js → 4718-6585c97c26647e65.js} +1 -1
  54. fides/ui-build/static/admin/_next/static/chunks/502-d3ecae97b67befbd.js +1 -0
  55. fides/ui-build/static/admin/_next/static/chunks/{7045-14e955890f1147e4.js → 7045-f15044a4d4525946.js} +1 -1
  56. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-c1c2f757b1f3da12.js → _app-a7c02dd2ff07f9e1.js} +1 -1
  57. fides/ui-build/static/admin/_next/static/chunks/pages/add-systems-58920afe2b67f952.js +1 -0
  58. fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-ae4909cad9b67822.js +1 -0
  59. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-29c1fb777bd464e0.js +1 -0
  60. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-2635ef588bf06145.js +1 -0
  61. fides/ui-build/static/admin/_next/static/chunks/pages/datamap-15616bea02397ef4.js +1 -0
  62. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-ea198c4a7869f402.js +1 -0
  63. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-f682b1def859931e.js +1 -0
  64. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-febf156d2977f3ac.js +1 -0
  65. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-648d775d0fce49dc.js +1 -0
  66. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-5edfec10a945ca43.js +1 -0
  67. fides/ui-build/static/admin/_next/static/chunks/pages/settings/{privacy-requests-97221067330c0c27.js → privacy-requests-8cbdfd08e0fa88fb.js} +1 -1
  68. fides/ui-build/static/admin/_next/static/chunks/pages/systems-0f1d833282f09684.js +1 -0
  69. fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-a8cfa7de4948b374.js +1 -0
  70. fides/ui-build/static/admin/_next/static/css/{295d729ea1b11885.css → 64fac6fb884435c2.css} +1 -1
  71. fides/ui-build/static/admin/_next/static/css/f38242c11f7fea64.css +1 -0
  72. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  73. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  74. fides/ui-build/static/admin/add-systems.html +1 -1
  75. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  76. fides/ui-build/static/admin/consent/configure.html +1 -1
  77. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  78. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  79. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  80. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  81. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  82. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  83. fides/ui-build/static/admin/consent/properties.html +1 -1
  84. fides/ui-build/static/admin/consent/reporting.html +1 -1
  85. fides/ui-build/static/admin/consent.html +1 -1
  86. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  87. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  88. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  89. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  90. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  91. fides/ui-build/static/admin/data-catalog.html +1 -1
  92. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  93. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  94. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  95. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  96. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  97. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  98. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  99. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  100. fides/ui-build/static/admin/datamap.html +1 -1
  101. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  102. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  103. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  104. fides/ui-build/static/admin/dataset/new.html +1 -1
  105. fides/ui-build/static/admin/dataset.html +1 -1
  106. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  107. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  108. fides/ui-build/static/admin/datastore-connection.html +1 -1
  109. fides/ui-build/static/admin/index.html +1 -1
  110. fides/ui-build/static/admin/integrations/[id].html +1 -1
  111. fides/ui-build/static/admin/integrations.html +1 -1
  112. fides/ui-build/static/admin/lib/fides-preview.js +1 -1
  113. fides/ui-build/static/admin/lib/fides-tcf.js +2 -2
  114. fides/ui-build/static/admin/lib/fides.js +2 -2
  115. fides/ui-build/static/admin/login/[provider].html +1 -1
  116. fides/ui-build/static/admin/login.html +1 -1
  117. fides/ui-build/static/admin/messaging/[id].html +1 -1
  118. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  119. fides/ui-build/static/admin/messaging.html +1 -1
  120. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  121. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  122. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  123. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  124. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  125. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  126. fides/ui-build/static/admin/poc/forms.html +1 -1
  127. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  128. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  129. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  130. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  131. fides/ui-build/static/admin/privacy-requests.html +1 -1
  132. fides/ui-build/static/admin/properties/[id].html +1 -1
  133. fides/ui-build/static/admin/properties/add-property.html +1 -1
  134. fides/ui-build/static/admin/properties.html +1 -1
  135. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  136. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  137. fides/ui-build/static/admin/settings/about.html +1 -1
  138. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  139. fides/ui-build/static/admin/settings/consent.html +1 -1
  140. fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
  141. fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
  142. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  143. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  144. fides/ui-build/static/admin/settings/domains.html +1 -1
  145. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  146. fides/ui-build/static/admin/settings/locations.html +1 -1
  147. fides/ui-build/static/admin/settings/messaging-providers/[key].html +1 -1
  148. fides/ui-build/static/admin/settings/messaging-providers/new.html +1 -1
  149. fides/ui-build/static/admin/settings/messaging-providers.html +1 -1
  150. fides/ui-build/static/admin/settings/organization.html +1 -1
  151. fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
  152. fides/ui-build/static/admin/settings/regulations.html +1 -1
  153. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  154. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  155. fides/ui-build/static/admin/systems.html +1 -1
  156. fides/ui-build/static/admin/taxonomy.html +1 -1
  157. fides/ui-build/static/admin/user-management/new.html +1 -1
  158. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  159. fides/ui-build/static/admin/user-management.html +1 -1
  160. fides/api/service/async_dsr/async_dsr_service.py +0 -195
  161. fides/api/service/async_dsr/async_dsr_strategy.py +0 -5
  162. fides/api/service/async_dsr/async_dsr_strategy_callback.py +0 -16
  163. fides/api/service/async_dsr/async_dsr_strategy_factory.py +0 -63
  164. fides/api/service/async_dsr/async_dsr_strategy_polling.py +0 -94
  165. fides/ui-build/static/admin/_next/static/chunks/155-b4337d0826d5addc.js +0 -1
  166. fides/ui-build/static/admin/_next/static/chunks/3855-ed226b8a8050bd40.js +0 -1
  167. fides/ui-build/static/admin/_next/static/chunks/409-5c3d31163028339f.js +0 -1
  168. fides/ui-build/static/admin/_next/static/chunks/4558-8305aee48def1dcd.js +0 -1
  169. fides/ui-build/static/admin/_next/static/chunks/4608-0c6ef78e30a51f84.js +0 -1
  170. fides/ui-build/static/admin/_next/static/chunks/502-0d9f4ac29ef34a1c.js +0 -1
  171. fides/ui-build/static/admin/_next/static/chunks/pages/add-systems-19214babd1f219e3.js +0 -1
  172. fides/ui-build/static/admin/_next/static/chunks/pages/consent/reporting-c1a3caf3c286bf5d.js +0 -1
  173. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/[monitorId]/[systemId]-5b57f9132426fe52.js +0 -1
  174. fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/activity-a28cc0e23bbe4fc8.js +0 -1
  175. fides/ui-build/static/admin/_next/static/chunks/pages/datamap-7d22222608ec3aac.js +0 -1
  176. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-d514cd4ec62e3b03.js +0 -1
  177. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-331544e9b85c4ac2.js +0 -1
  178. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/[id]-7dac2302f573f5ee.js +0 -1
  179. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/storage-479890582973deaf.js +0 -1
  180. fides/ui-build/static/admin/_next/static/chunks/pages/settings/custom-fields-2fcd95c41e578d57.js +0 -1
  181. fides/ui-build/static/admin/_next/static/chunks/pages/systems-6c91bdea40875227.js +0 -1
  182. fides/ui-build/static/admin/_next/static/chunks/pages/taxonomy-3059aba38adefa56.js +0 -1
  183. fides/ui-build/static/admin/_next/static/css/073713cd1eddda79.css +0 -1
  184. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/WHEEL +0 -0
  185. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/entry_points.txt +0 -0
  186. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/licenses/LICENSE +0 -0
  187. {ethyca_fides-2.71.1b1.dist-info → ethyca_fides-2.71.1rc1.dist-info}/top_level.txt +0 -0
  188. /fides/ui-build/static/admin/_next/static/{_IxwgneyQjdSaZFEF3Tqu → AfNel282iPq07N-lE1Vzx}/_ssgManifest.js +0 -0
  189. /fides/ui-build/static/admin/_next/static/chunks/{6882-dbe0a25dcf1a8ee0.js → 6882-10296485ec326e6b.js} +0 -0
  190. /fides/ui-build/static/admin/_next/static/chunks/{7079-50571e9f3269d74d.js → 7079-bbc7b856802a4834.js} +0 -0
  191. /fides/ui-build/static/admin/_next/static/chunks/{9046-b6616ba7b59d947e.js → 9046-2a332fe338535c84.js} +0 -0
  192. /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-b7326c51d88cc2cc.js → data-catalog-56fd0f3e465e52b6.js} +0 -0
  193. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-0abd30eada811b5b.js → [...subfieldNames]-d4031e438c363fff.js} +0 -0
  194. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-007965429368d9a3.js → [collectionName]-9463af37079762d0.js} +0 -0
@@ -8,8 +8,9 @@ from typing import TYPE_CHECKING, List, Optional, Tuple
8
8
  from loguru import logger
9
9
  from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
10
10
  from sqlalchemy.dialects.postgresql import JSONB
11
+ from sqlalchemy.ext.declarative import declared_attr
11
12
  from sqlalchemy.ext.mutable import MutableDict, MutableList
12
- from sqlalchemy.orm import Query, Session, relationship
13
+ from sqlalchemy.orm import Query, RelationshipProperty, Session, relationship
13
14
  from sqlalchemy_utils.types.encrypted.encrypted_type import (
14
15
  AesGcmEngine,
15
16
  StringEncryptedType,
@@ -183,6 +184,14 @@ class RequestTask(WorkerTask, Base):
183
184
  uselist=False,
184
185
  )
185
186
 
187
+ # Stores the sub-requests data for async polling tasks
188
+ sub_requests: "RelationshipProperty[List[RequestTaskSubRequest]]" = relationship(
189
+ "RequestTaskSubRequest",
190
+ back_populates="request_task",
191
+ cascade="all, delete-orphan",
192
+ order_by="RequestTaskSubRequest.created_at",
193
+ )
194
+
186
195
  @property
187
196
  def request_task_address(self) -> CollectionAddress:
188
197
  """Convert the collection_address into Collection Address format"""
@@ -318,3 +327,91 @@ class RequestTask(WorkerTask, Base):
318
327
  )
319
328
 
320
329
  return task_in_flight
330
+
331
+
332
+ class RequestTaskSubRequest(Base):
333
+ """
334
+ Model for storing individual sub-request data during the execution of a request task.
335
+ Supports 1:N relationship - each RequestTask can have multiple sub-requests.
336
+ Currently used for storing request data for polling tasks.
337
+ """
338
+
339
+ @declared_attr
340
+ def __tablename__(cls) -> str:
341
+ """Overriding base class method to set the table name."""
342
+ return "request_task_sub_request"
343
+
344
+ request_task_id = Column(
345
+ String(255),
346
+ ForeignKey(
347
+ "requesttask.id",
348
+ name="request_task_sub_request_request_task_id_fkey",
349
+ ondelete="CASCADE",
350
+ ),
351
+ nullable=False,
352
+ index=True,
353
+ )
354
+
355
+ request_task = relationship(
356
+ "RequestTask",
357
+ back_populates="sub_requests",
358
+ )
359
+
360
+ # Individual sub-request data (e.g., request_id, status, result data)
361
+ # Additional fields for enhanced sub-request tracking
362
+ param_values = Column( # An encrypted JSON String - saved as a dict
363
+ StringEncryptedType(
364
+ type_in=JSONTypeOverride,
365
+ key=CONFIG.security.app_encryption_key,
366
+ engine=AesGcmEngine,
367
+ padding="pkcs5",
368
+ ),
369
+ nullable=False,
370
+ )
371
+ status = Column(String, nullable=False)
372
+
373
+ # Raw data retrieved from an access request is stored here. This contains all of the
374
+ # intermediate data we retrieved, needed for downstream tasks, but hasn't been filtered
375
+ # by data category for the end user.
376
+ _access_data = Column( # An encrypted JSON String - saved as a list of Rows
377
+ "access_data",
378
+ StringEncryptedType(
379
+ type_in=JSONTypeOverride,
380
+ key=CONFIG.security.app_encryption_key,
381
+ engine=AesGcmEngine,
382
+ padding="pkcs5",
383
+ ),
384
+ )
385
+
386
+ # Use descriptors for automatic external storage handling
387
+ access_data = EncryptedLargeDataDescriptor(
388
+ field_name="access_data", empty_default=[]
389
+ )
390
+
391
+ # Written after an erasure is completed
392
+ rows_masked = Column(Integer)
393
+
394
+ def get_correlation_id(self) -> Optional[str]:
395
+ """Helper method to extract correlation_id from param_values."""
396
+ if self.param_values and "request_id" in self.param_values:
397
+ return self.param_values["request_id"]
398
+ return None
399
+
400
+ def update_status(self, db: Session, status: str) -> None:
401
+ """Helper method to update the status of this sub-request."""
402
+ self.status = status
403
+ self.save(db)
404
+
405
+ def cleanup_external_storage(self) -> None:
406
+ """Clean up all external storage files for this sub-request"""
407
+ # Access the descriptor from the class to call cleanup
408
+ RequestTaskSubRequest.access_data.cleanup(self)
409
+
410
+ def get_access_data(self) -> List[Row]:
411
+ """Helper to retrieve access data or default to empty list"""
412
+ return self.access_data or []
413
+
414
+ def delete(self, db: Session) -> None:
415
+ """Override delete to cleanup external storage first"""
416
+ self.cleanup_external_storage()
417
+ super().delete(db)
@@ -17,6 +17,14 @@ class ExecutionLogStatus(enum.Enum):
17
17
  awaiting_processing = "paused" # "paused" in the database to avoid a migration, but use "awaiting_processing" in the app
18
18
  retrying = "retrying"
19
19
  skipped = "skipped"
20
+ polling = "polling"
21
+
22
+
23
+ # Statuses that can be resumed
24
+ RESUMABLE_EXECUTION_LOG_STATUSES = [
25
+ ExecutionLogStatus.pending,
26
+ ExecutionLogStatus.polling,
27
+ ]
20
28
 
21
29
 
22
30
  class WorkerTask:
@@ -0,0 +1,81 @@
1
+ from enum import Enum
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+ from fides.api.schemas.saas.saas_config import SaaSRequest
7
+ from fides.api.schemas.saas.strategy_configuration import StrategyConfiguration
8
+ from fides.api.util.collection_util import Row
9
+
10
+
11
+ class SupportedDataType(Enum):
12
+ """Supported data types for polling async DSR result requests."""
13
+
14
+ # Structured data types that can be parsed into rows
15
+ json = "json" # Parsed into List[Row] from JSON response
16
+ csv = "csv" # Parsed into List[Row] from CSV response
17
+ # Binary/non-parseable data stored as raw bytes
18
+ attachment = "attachment" # Binary files (.zip, .pdf, .xml, etc.) stored as bytes
19
+
20
+
21
+ class PollingResultType(Enum):
22
+ """Types of results from async polling operations."""
23
+
24
+ rows = "rows" # Structured data parsed into List[Row]
25
+ attachment = "attachment" # Binary file data stored as bytes
26
+
27
+
28
+ class PollingResult(BaseModel):
29
+ """
30
+ Flexible result container for async polling operations.
31
+ Handles both structured data and file attachments.
32
+ """
33
+
34
+ data: Union[List[Row], bytes]
35
+ result_type: PollingResultType
36
+ metadata: Dict[str, Any] = Field(default_factory=dict)
37
+
38
+
39
+ class PollingStatusRequest(SaaSRequest):
40
+ """
41
+ Extended SaaSRequest for checking async job status.
42
+ Uses request_override for custom status checking logic or standard fields for simple cases.
43
+ """
44
+
45
+ status_path: Optional[str] = None
46
+ status_completed_value: Optional[Union[str, bool, int]] = None
47
+
48
+ @model_validator(mode="after")
49
+ def validate_status_fields(self) -> "PollingStatusRequest":
50
+ """Ensure required fields are present unless using an override."""
51
+ if self.request_override:
52
+ return self
53
+
54
+ if not self.status_path:
55
+ raise ValueError("status_path is required when request_override is not set")
56
+ if self.status_completed_value is None:
57
+ raise ValueError(
58
+ "status_completed_value is required when request_override is not set"
59
+ )
60
+ return self
61
+
62
+
63
+ class PollingResultRequest(SaaSRequest):
64
+ """
65
+ Extended SaaSRequest for retrieving async job results.
66
+ Uses request_override for custom result retrieval or standard HTTP request for simple cases.
67
+ Data type is automatically inferred from response.
68
+ """
69
+
70
+ result_path: Optional[str] = None
71
+
72
+
73
+ class AsyncPollingConfiguration(StrategyConfiguration):
74
+ """
75
+ Simplified configuration for polling async DSR requests.
76
+ The main read request serves as the initial request.
77
+ """
78
+
79
+ status_request: PollingStatusRequest
80
+ # result_request is optional for delete/update operations
81
+ result_request: Optional[PollingResultRequest] = None
@@ -2,7 +2,9 @@ from typing import Any, Dict, List, Optional, Set, Union
2
2
 
3
3
  from fideslang.models import FidesCollectionKey, FidesDatasetReference
4
4
  from fideslang.validation import FidesKey
5
- from pydantic import BaseModel, ConfigDict, field_validator, model_validator
5
+ from pydantic import BaseModel, ConfigDict
6
+ from pydantic import Field as PydanticField
7
+ from pydantic import field_validator, model_validator
6
8
 
7
9
  from fides.api.common_exceptions import ValidationError
8
10
  from fides.api.graph.config import (
@@ -115,6 +117,10 @@ class SaaSRequest(BaseModel):
115
117
  skip_missing_param_values: Optional[bool] = (
116
118
  False # Skip instead of raising an exception if placeholders can't be populated in body
117
119
  )
120
+ correlation_id_path: Optional[str] = PydanticField(
121
+ default=None,
122
+ description="The path to the correlation ID in the response. For use with async polling.",
123
+ )
118
124
  model_config = ConfigDict(
119
125
  from_attributes=True, use_enum_values=True, extra="forbid"
120
126
  )
@@ -213,7 +219,7 @@ class SaaSRequest(BaseModel):
213
219
  class ReadSaaSRequest(SaaSRequest):
214
220
  """
215
221
  An extension of the base SaaSRequest that allows the inclusion of an output template
216
- that is used to format each collection result.
222
+ that is used to format each collection result, and correlation_id_path for async polling.
217
223
  """
218
224
 
219
225
  output: Optional[str] = None
@@ -230,7 +236,8 @@ class ReadSaaSRequest(SaaSRequest):
230
236
  raise ValueError(
231
237
  "A read request must specify a method if a path is provided and no request_override is specified"
232
238
  )
233
- else:
239
+
240
+ if self.request_override:
234
241
  allowed_fields = {
235
242
  "request_override",
236
243
  "param_values",
@@ -178,15 +178,3 @@ class OAuth2ClientCredentialsConfiguration(OAuth2BaseConfiguration):
178
178
  """
179
179
 
180
180
  refresh_request: Optional[SaaSRequest] = Field(exclude=True)
181
-
182
-
183
- class PollingAsyncDSRConfiguration(StrategyConfiguration):
184
- """
185
- Configuration for polling async DSR requests.
186
- """
187
-
188
- status_request: SaaSRequest
189
- status_path: str
190
- status_completed_value: Optional[str] = None
191
- result_request: SaaSRequest
192
- result_path: str
File without changes
@@ -0,0 +1,155 @@
1
+ """Handler for storing and managing attachments from async polling operations."""
2
+
3
+ from io import BytesIO
4
+ from typing import List, Union
5
+
6
+ from loguru import logger
7
+ from sqlalchemy.orm import Session
8
+
9
+ from fides.api.common_exceptions import PrivacyRequestError
10
+ from fides.api.models.attachment import (
11
+ Attachment,
12
+ AttachmentReference,
13
+ AttachmentReferenceType,
14
+ AttachmentType,
15
+ )
16
+ from fides.api.models.privacy_request.request_task import RequestTask
17
+ from fides.api.models.storage import get_active_default_storage_config
18
+ from fides.api.util.collection_util import Row
19
+
20
+
21
+ class PollingAttachmentHandler:
22
+ """Utility class for handling attachment storage and metadata in polling operations."""
23
+
24
+ @staticmethod
25
+ def store_attachment(
26
+ session: Session,
27
+ request_task: RequestTask,
28
+ attachment_data: bytes,
29
+ filename: str,
30
+ ) -> str:
31
+ """
32
+ Store polling attachment data and return attachment ID.
33
+
34
+ This utility function handles the storage of attachment data
35
+ from polling results and creates the necessary database records.
36
+
37
+ Args:
38
+ session: Database session
39
+ request_task: The request task associated with this attachment
40
+ attachment_data: The binary attachment data to store
41
+ filename: The filename for the attachment
42
+
43
+ Returns:
44
+ str: The ID of the created attachment
45
+
46
+ Raises:
47
+ PrivacyRequestError: If storage configuration is not found or storage fails
48
+ """
49
+ try:
50
+ # Get active storage config
51
+ storage_config = get_active_default_storage_config(session)
52
+ if not storage_config:
53
+ raise PrivacyRequestError("No active storage configuration found")
54
+
55
+ # Create attachment record and upload to storage
56
+ attachment = Attachment.create_and_upload(
57
+ db=session,
58
+ data={
59
+ "file_name": filename,
60
+ "attachment_type": AttachmentType.include_with_access_package,
61
+ "storage_key": storage_config.key,
62
+ },
63
+ attachment_file=BytesIO(attachment_data),
64
+ )
65
+
66
+ # Create attachment references
67
+ AttachmentReference.create(
68
+ db=session,
69
+ data={
70
+ "attachment_id": attachment.id,
71
+ "reference_id": request_task.id,
72
+ "reference_type": AttachmentReferenceType.request_task,
73
+ },
74
+ )
75
+
76
+ AttachmentReference.create(
77
+ db=session,
78
+ data={
79
+ "attachment_id": attachment.id,
80
+ "reference_id": request_task.privacy_request.id,
81
+ "reference_type": AttachmentReferenceType.privacy_request,
82
+ },
83
+ )
84
+
85
+ logger.info(
86
+ f"Successfully stored polling attachment {attachment.id} for request_task {request_task.id}"
87
+ )
88
+ return attachment.id
89
+
90
+ except Exception as e:
91
+ logger.error(f"Failed to store polling attachment: {e}")
92
+ raise PrivacyRequestError(f"Failed to store polling attachment: {e}")
93
+
94
+ @staticmethod
95
+ def ensure_attachment_bytes(data: Union[List[Row], bytes]) -> bytes:
96
+ """
97
+ Ensure attachment polling results provide bytes content.
98
+
99
+ Args:
100
+ data: The data that should be bytes
101
+
102
+ Returns:
103
+ bytes: The validated bytes data
104
+
105
+ Raises:
106
+ PrivacyRequestError: If data is not bytes
107
+ """
108
+ if isinstance(data, bytes):
109
+ return data
110
+ raise PrivacyRequestError("Expected bytes data for attachment polling result")
111
+
112
+ @staticmethod
113
+ def add_metadata_to_rows(db: Session, attachment_id: str, rows: List[Row]) -> None:
114
+ """
115
+ Add attachment metadata to rows collection (like manual tasks do).
116
+
117
+ Args:
118
+ db: Database session
119
+ attachment_id: The ID of the attachment to add metadata for
120
+ rows: The list of rows to add the attachment metadata to
121
+ """
122
+ attachment_record = (
123
+ db.query(Attachment).filter(Attachment.id == attachment_id).first()
124
+ )
125
+
126
+ if attachment_record:
127
+ try:
128
+ size, url = attachment_record.retrieve_attachment()
129
+ attachment_info = {
130
+ "file_name": attachment_record.file_name,
131
+ "download_url": str(url) if url else None,
132
+ "file_size": size,
133
+ }
134
+ except Exception as exc:
135
+ logger.warning(
136
+ f"Could not retrieve attachment content for {attachment_record.file_name}: {exc}"
137
+ )
138
+ attachment_info = {
139
+ "file_name": attachment_record.file_name,
140
+ "download_url": None,
141
+ "file_size": None,
142
+ }
143
+
144
+ # Add attachment to the polling results
145
+ attachments_item = None
146
+ for item in rows:
147
+ if isinstance(item, dict) and "retrieved_attachments" in item:
148
+ attachments_item = item
149
+ break
150
+
151
+ if attachments_item is None:
152
+ attachments_item = {"retrieved_attachments": []}
153
+ rows.append(attachments_item)
154
+
155
+ attachments_item["retrieved_attachments"].append(attachment_info)
@@ -0,0 +1,88 @@
1
+ """
2
+ Pure HTTP utility for executing polling requests.
3
+
4
+ This module contains low-level HTTP request execution for async DSR polling,
5
+ with no business logic or orchestration dependencies.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+ from requests import Response
11
+
12
+ from fides.api.common_exceptions import PrivacyRequestError
13
+ from fides.api.schemas.saas.async_polling_configuration import (
14
+ PollingResultRequest,
15
+ PollingStatusRequest,
16
+ )
17
+ from fides.api.service.connectors.saas.authenticated_client import AuthenticatedClient
18
+ from fides.api.util.saas_util import map_param_values
19
+
20
+
21
+ class PollingRequestHandler:
22
+ """
23
+ Pure HTTP utility for executing polling requests.
24
+
25
+ Handles status checks and result retrieval with no business logic dependencies.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ status_request: PollingStatusRequest,
31
+ result_request: Optional[PollingResultRequest] = None,
32
+ ):
33
+ self.status_request = status_request
34
+ self.result_request = result_request
35
+
36
+ def get_status_response(
37
+ self,
38
+ client: AuthenticatedClient,
39
+ param_values: Dict[str, Any],
40
+ ) -> Response:
41
+ """Execute HTTP status request and return raw response."""
42
+ if not self.status_request:
43
+ raise PrivacyRequestError(
44
+ "status_request is not configured in the async polling configuration"
45
+ )
46
+
47
+ prepared_status_request = map_param_values(
48
+ action="status",
49
+ context="polling request",
50
+ current_request=self.status_request,
51
+ param_values=param_values,
52
+ )
53
+
54
+ response: Response = client.send(prepared_status_request)
55
+
56
+ if not response.ok:
57
+ raise PrivacyRequestError(
58
+ f"Status request failed with status code {response.status_code}: {response.text}"
59
+ )
60
+
61
+ return response
62
+
63
+ def get_result_response(
64
+ self,
65
+ client: AuthenticatedClient,
66
+ param_values: Dict[str, Any],
67
+ ) -> Response:
68
+ """Execute HTTP result request and return raw response."""
69
+ if not self.result_request:
70
+ raise PrivacyRequestError(
71
+ "result_request is not configured in the async polling configuration"
72
+ )
73
+
74
+ prepared_result_request = map_param_values(
75
+ action="result",
76
+ context="polling request",
77
+ current_request=self.result_request,
78
+ param_values=param_values,
79
+ )
80
+
81
+ response: Response = client.send(prepared_result_request)
82
+
83
+ if not response.ok:
84
+ raise PrivacyRequestError(
85
+ f"Result request failed with status code {response.status_code}: {response.text}"
86
+ )
87
+
88
+ return response