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
@@ -0,0 +1,130 @@
1
+ """
2
+ Pure utility functions for async DSR operations.
3
+
4
+ This module contains utility functions with no business logic dependencies.
5
+ These are helper functions that can be used across the async DSR system.
6
+ """
7
+
8
+ from enum import Enum
9
+
10
+ # Type checking imports
11
+ from typing import TYPE_CHECKING, List, cast
12
+
13
+ from loguru import logger
14
+ from sqlalchemy.orm import Session
15
+
16
+ from fides.api.common_exceptions import PrivacyRequestError
17
+ from fides.api.models.connectionconfig import ConnectionConfig
18
+ from fides.api.models.datasetconfig import DatasetConfig
19
+ from fides.api.models.privacy_request.request_task import AsyncTaskType, RequestTask
20
+ from fides.api.schemas.saas.saas_config import SaaSRequest
21
+
22
+ if TYPE_CHECKING:
23
+ from fides.api.service.connectors.query_configs.saas_query_config import (
24
+ SaaSQueryConfig,
25
+ )
26
+
27
+
28
+ def has_async_requests(query_config: "SaaSQueryConfig") -> bool:
29
+ """Check if any read/update/delete requests have async configuration"""
30
+ all_requests: List[SaaSRequest] = cast(
31
+ List[SaaSRequest], query_config.get_read_requests_by_identity()
32
+ )
33
+ masking_request = query_config.get_masking_request()
34
+ if masking_request:
35
+ all_requests.append(masking_request)
36
+ return any(request.async_config is not None for request in all_requests)
37
+
38
+
39
+ def is_polling_continuation(request_task: RequestTask) -> bool:
40
+ """Check if this is a polling continuation (not initial request)"""
41
+ async_type_check = request_task.async_type == AsyncTaskType.polling
42
+ sub_requests_count = len(request_task.sub_requests)
43
+
44
+ logger.warning(
45
+ f"is_polling_continuation for task {request_task.id}: "
46
+ f"async_type={request_task.async_type} (polling={async_type_check}), "
47
+ f"sub_requests_count={sub_requests_count}"
48
+ )
49
+
50
+ return async_type_check and sub_requests_count > 0
51
+
52
+
53
+ def is_callback_completion(request_task: RequestTask) -> bool:
54
+ """Check if this is a completed callback request"""
55
+ return bool(request_task.callback_succeeded)
56
+
57
+
58
+ def is_async_request(
59
+ request_task: RequestTask, query_config: "SaaSQueryConfig"
60
+ ) -> bool:
61
+ """
62
+ Check if this is an async request (callback, continuation, or initial async).
63
+
64
+ This is the main entry point for async detection.
65
+ """
66
+ return (
67
+ is_callback_completion(request_task)
68
+ or is_polling_continuation(request_task)
69
+ or has_async_requests(query_config)
70
+ )
71
+
72
+
73
+ class AsyncPhase(Enum):
74
+ """Enum representing different phases of async DSR processing."""
75
+
76
+ callback_completion = "callback_completion"
77
+ polling_continuation = "polling_continuation"
78
+ initial_async = "initial_async"
79
+ sync = "sync"
80
+
81
+
82
+ def get_async_phase(
83
+ request_task: RequestTask, query_config: "SaaSQueryConfig"
84
+ ) -> AsyncPhase:
85
+ """
86
+ Classify the phase of async request for routing purposes.
87
+
88
+ Returns:
89
+ AsyncPhase enum value representing the current phase:
90
+ - callback_completion: Callback has completed
91
+ - polling_continuation: Continuing an existing polling process
92
+ - initial_async: Initial async request setup
93
+ - sync: Not an async request
94
+ """
95
+ if is_callback_completion(request_task):
96
+ return AsyncPhase.callback_completion
97
+ if is_polling_continuation(request_task):
98
+ return AsyncPhase.polling_continuation
99
+ if has_async_requests(query_config):
100
+ return AsyncPhase.initial_async
101
+ return AsyncPhase.sync
102
+
103
+
104
+ def get_connection_config_from_task(
105
+ db: Session, request_task: RequestTask
106
+ ) -> ConnectionConfig:
107
+ """
108
+ Get ConnectionConfig from a RequestTask.
109
+
110
+ This utility function retrieves the connection configuration
111
+ associated with a request task by looking up the dataset configuration.
112
+ """
113
+ dataset_config = DatasetConfig.filter(
114
+ db=db,
115
+ conditions=(DatasetConfig.fides_key == request_task.dataset_name),
116
+ ).first()
117
+ if not dataset_config:
118
+ raise PrivacyRequestError(
119
+ f"DatasetConfig with fides_key {request_task.dataset_name} not found."
120
+ )
121
+
122
+ connection_config = ConnectionConfig.get(
123
+ db=db, object_id=dataset_config.connection_config_id
124
+ )
125
+ if not connection_config:
126
+ raise PrivacyRequestError(
127
+ f"ConnectionConfig with id {dataset_config.connection_config_id} not found."
128
+ )
129
+
130
+ return connection_config
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from asyncio import sleep
4
+ from datetime import datetime
3
5
  from typing import Any, Dict, List, Optional
4
6
 
5
7
  import httpx
6
8
  from httpx import AsyncClient, Client, HTTPStatusError, Request, RequestError, Timeout
7
9
  from loguru import logger
8
10
 
11
+ from fides.api.common_exceptions import PrivacyRequestNotFound
9
12
  from fides.api.schemas.privacy_request import (
10
13
  PrivacyRequestCreate,
11
14
  PrivacyRequestResponse,
@@ -13,11 +16,11 @@ from fides.api.schemas.privacy_request import (
13
16
  )
14
17
  from fides.api.schemas.redis_cache import Identity
15
18
  from fides.api.schemas.user import UserLogin
16
- from fides.api.service.privacy_request.request_service import poll_server_for_completion
17
19
  from fides.api.util.collection_util import Row
18
20
  from fides.api.util.errors import FidesError
19
21
  from fides.api.util.wrappers import sync
20
22
  from fides.common.api.v1 import urn_registry as urls
23
+ from fides.common.api.v1.urn_registry import PRIVACY_REQUESTS
21
24
 
22
25
  COMPLETION_STATUSES = [
23
26
  PrivacyRequestStatus.complete,
@@ -27,6 +30,65 @@ COMPLETION_STATUSES = [
27
30
  ]
28
31
 
29
32
 
33
+ def get_async_client() -> AsyncClient:
34
+ """Return an async client used to make API requests"""
35
+ return AsyncClient()
36
+
37
+
38
+ async def poll_server_for_completion(
39
+ privacy_request_id: str,
40
+ server_url: str,
41
+ token: str,
42
+ *,
43
+ poll_interval_seconds: int = 30,
44
+ timeout_seconds: int = 1800, # 30 minutes
45
+ client: AsyncClient | None = None,
46
+ ) -> PrivacyRequestResponse:
47
+ """Poll a server for privacy request completion.
48
+
49
+ Requests will report complete with if they have a status of canceled, complete,
50
+ denied, or error. By default the polling will time out if not completed in 30
51
+ minutes, time can be overridden by setting the timeout_seconds.
52
+ """
53
+ url = f"{server_url}{urls.V1_URL_PREFIX}{PRIVACY_REQUESTS}?request_id={privacy_request_id}"
54
+ start_time = datetime.now()
55
+ elapsed_time = 0.0
56
+ while elapsed_time < timeout_seconds:
57
+ if client:
58
+ response = await client.get(
59
+ url, headers={"Authorization": f"Bearer {token}"}
60
+ )
61
+ else:
62
+ async_client = get_async_client()
63
+ response = await async_client.get(
64
+ url, headers={"Authorization": f"Bearer {token}"}
65
+ )
66
+ response.raise_for_status()
67
+
68
+ # Privacy requests are returned paginated. Since this is searching for a specific
69
+ # privacy request there should only be one value present in items.
70
+ items = response.json()["items"]
71
+ if not items:
72
+ raise PrivacyRequestNotFound(
73
+ f"No privacy request found with id '{privacy_request_id}'"
74
+ )
75
+ status = PrivacyRequestResponse(**items[0])
76
+ if status.status and status.status in (
77
+ PrivacyRequestStatus.complete,
78
+ PrivacyRequestStatus.canceled,
79
+ PrivacyRequestStatus.error,
80
+ PrivacyRequestStatus.denied,
81
+ ):
82
+ return status
83
+
84
+ await sleep(poll_interval_seconds)
85
+ time_delta = datetime.now() - start_time
86
+ elapsed_time = time_delta.seconds
87
+ raise TimeoutError(
88
+ f"Timeout of {timeout_seconds} seconds has been exceeded while waiting for privacy request {privacy_request_id}"
89
+ )
90
+
91
+
30
92
  class FidesClient:
31
93
  """
32
94
  A helper client to broker communications between Fides servers.
@@ -10,7 +10,6 @@ from uuid import uuid4
10
10
  import pydash
11
11
  from fideslang.models import FidesDatasetReference
12
12
  from loguru import logger
13
- from sqlalchemy.orm import Session
14
13
 
15
14
  from fides.api.common_exceptions import FidesopsException
16
15
  from fides.api.graph.execution import ExecutionNode
@@ -140,7 +139,7 @@ class SaaSQueryConfig(QueryConfig[SaaSRequestParams]):
140
139
  )
141
140
  return request
142
141
 
143
- def get_masking_request(self, db: Session) -> Optional[SaaSRequest]:
142
+ def get_masking_request(self) -> Optional[SaaSRequest]:
144
143
  """
145
144
  Returns a tuple of the preferred action and SaaSRequest to use for masking.
146
145
  An update request is preferred, but we can use a gdpr delete endpoint or
@@ -353,7 +352,8 @@ class SaaSQueryConfig(QueryConfig[SaaSRequestParams]):
353
352
  ]
354
353
 
355
354
  param_values[UUID] = str(uuid4())
356
- param_values[ISO_8601_DATETIME] = datetime.now().date().isoformat()
355
+ # Use full ISO-8601 timestamp including time component (UTC, seconds precision)
356
+ param_values[ISO_8601_DATETIME] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
357
357
  param_values[FIELD_LIST] = ",".join(
358
358
  [
359
359
  field.name
@@ -384,8 +384,7 @@ class SaaSQueryConfig(QueryConfig[SaaSRequestParams]):
384
384
  The fields in the row are masked according to the policy and added to the request body
385
385
  if specified by the body field of the masking request.
386
386
  """
387
- session = Session.object_session(request)
388
- current_request: SaaSRequest = self.get_masking_request(session) # type: ignore
387
+ current_request: SaaSRequest = self.get_masking_request() # type: ignore
389
388
  param_values: Dict[str, Any] = self.generate_update_param_values(
390
389
  row, policy, request, current_request
391
390
  )
@@ -10,18 +10,16 @@ from requests import Response
10
10
  from sqlalchemy.orm import Session
11
11
  from starlette.status import HTTP_204_NO_CONTENT
12
12
 
13
+ from fides.api.api.deps import get_autoclose_db_session as get_db
13
14
  from fides.api.common_exceptions import (
14
- AwaitingAsyncTask,
15
15
  FidesopsException,
16
16
  PostProcessingException,
17
- PrivacyRequestError,
18
17
  SkippingConsentPropagation,
19
18
  )
20
19
  from fides.api.graph.execution import ExecutionNode
21
20
  from fides.api.models.connectionconfig import ConnectionConfig, ConnectionTestStatus
22
21
  from fides.api.models.policy import Policy
23
22
  from fides.api.models.privacy_request import PrivacyRequest, RequestTask
24
- from fides.api.models.privacy_request.request_task import AsyncTaskType
25
23
  from fides.api.schemas.consentable_item import (
26
24
  ConsentableItem,
27
25
  build_consent_item_hierarchy,
@@ -39,6 +37,10 @@ from fides.api.schemas.saas.shared_schemas import (
39
37
  ConsentPropagationStatus,
40
38
  SaaSRequestParams,
41
39
  )
40
+ from fides.api.service.async_dsr.strategies.async_dsr_strategy import AsyncDSRStrategy
41
+ from fides.api.service.async_dsr.strategies.async_dsr_strategy_factory import (
42
+ get_strategy,
43
+ )
42
44
  from fides.api.service.connectors.base_connector import BaseConnector
43
45
  from fides.api.service.connectors.query_configs.saas_query_config import SaaSQueryConfig
44
46
  from fides.api.service.connectors.saas.authenticated_client import AuthenticatedClient
@@ -216,19 +218,29 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
216
218
  request_task: RequestTask,
217
219
  input_data: Dict[str, List[Any]],
218
220
  ) -> List[Row]:
219
- """Retrieve data from SaaS APIs"""
221
+ """
222
+ Retrieve data from SaaS APIs.
223
+
224
+ Handles sync requests directly and delegates async requests to external handlers.
225
+ """
220
226
 
221
227
  # pylint: disable=too-many-branches
222
228
  self.set_privacy_request_state(privacy_request, node, request_task)
223
- if request_task.callback_succeeded:
224
- # If this is True, we assume we've received results from a third party
225
- # asynchronously and we can proceed to the next node.
226
- logger.info(
227
- "Access callback succeeded for request task '{}'", request_task.id
228
- )
229
- return request_task.get_access_data()
229
+
230
230
  query_config: SaaSQueryConfig = self.query_config(node)
231
231
 
232
+ # Delegate async requests
233
+ with get_db() as db:
234
+ if async_dsr_strategy := _get_async_dsr_strategy(
235
+ db, request_task, query_config, ActionType.access
236
+ ):
237
+ return async_dsr_strategy.async_retrieve_data(
238
+ client=self.create_client(),
239
+ request_task_id=request_task.id,
240
+ query_config=query_config,
241
+ input_data=input_data,
242
+ )
243
+
232
244
  # generate initial set of requests if read request is defined, otherwise raise an exception
233
245
  # An endpoint can be defined with multiple 'read' requests if the data for a single
234
246
  # collection can be accessed in multiple ways for example:
@@ -265,30 +277,8 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
265
277
  input_data[CUSTOM_PRIVACY_REQUEST_FIELDS] = [custom_privacy_request_fields]
266
278
 
267
279
  rows: List[Row] = []
268
- awaiting_async_processing: bool = False
269
-
270
280
  for read_request in read_requests:
271
281
  self.set_saas_request_state(read_request)
272
- if (
273
- read_request.async_config is not None
274
- and request_task.id # Only supported in DSR 3.0
275
- ):
276
- # Asynchronous read request detected. We will exit below and put the
277
- # Request Task in an "awaiting_processing" status.
278
- awaiting_async_processing = True
279
-
280
- # Validate async strategy with proper enum value checking
281
- strategy_value = read_request.async_config.strategy
282
- valid_strategies = [task_type.value for task_type in AsyncTaskType]
283
-
284
- if strategy_value not in valid_strategies:
285
- raise PrivacyRequestError(
286
- f"Invalid async type '{strategy_value}' for request task {request_task.id}. "
287
- f"Valid types are: {valid_strategies}"
288
- )
289
-
290
- request_task.async_type = AsyncTaskType(strategy_value)
291
-
292
282
  # check all the values specified by param_values are provided in input_data
293
283
  if self._missing_dataset_reference_values(
294
284
  input_data, read_request.param_values
@@ -334,7 +324,7 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
334
324
  # This allows us to build an output object even if we didn't generate and execute
335
325
  # any HTTP requests. This is useful if we just want to select specific input_data
336
326
  # values to provide as row data to the mask_data function
337
- elif read_request.output:
327
+ elif not read_request.path and read_request.output:
338
328
  rows.extend(
339
329
  self._apply_output_template(
340
330
  query_config.generate_param_value_maps(
@@ -345,17 +335,6 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
345
335
  )
346
336
 
347
337
  self.unset_connector_state()
348
- if awaiting_async_processing:
349
- # If a read request was marked to expect async results, original response data here is ignored.
350
- # We'll instead use the data received in the callback URL later.
351
- # However for polling async request we want to save the request data for ids that we will use on the pollings status
352
- if request_task.async_type == AsyncTaskType.polling:
353
- # Saving the request task access data to use it on the polling status request
354
- # TODO: Consider if we want to clean up the rows. Currently this is the concern of the GraphTask.
355
- request_task.access_data = rows
356
- # Raising an AwaitingAsyncTask to put this task in an awaiting_processing state
357
- raise AwaitingAsyncTask()
358
-
359
338
  return rows
360
339
 
361
340
  def _apply_output_template(
@@ -536,22 +515,28 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
536
515
  rows: List[Row],
537
516
  input_data: Optional[Dict[str, List[Any]]] = None,
538
517
  ) -> int:
539
- """Execute a masking request. Return the number of rows that have been updated."""
540
- self.set_privacy_request_state(privacy_request, node, request_task)
541
- if request_task.callback_succeeded:
542
- # If this is True, we assume the data was masked
543
- # asynchronously and we can proceed to the next node.
544
- logger.info(
545
- "Masking callback succeeded for request task '{}'", request_task.id
546
- )
547
- # If we've received the callback for this node, return rows_masked directly
548
- return request_task.rows_masked or 0
518
+ """
519
+ Execute masking requests.
549
520
 
521
+ Handles synchronous requests directly and delegates async requests to external handlers.
522
+ """
550
523
  self.set_privacy_request_state(privacy_request, node, request_task)
524
+
551
525
  query_config = self.query_config(node)
552
526
 
553
- session = Session.object_session(privacy_request)
554
- masking_request = query_config.get_masking_request(session)
527
+ # Delegate async requests
528
+ with get_db() as db:
529
+ if async_dsr_strategy := _get_async_dsr_strategy(
530
+ db, request_task, query_config, ActionType.erasure
531
+ ):
532
+ return async_dsr_strategy.async_mask_data(
533
+ client=self.create_client(),
534
+ request_task_id=request_task.id,
535
+ query_config=query_config,
536
+ rows=rows,
537
+ )
538
+
539
+ masking_request = query_config.get_masking_request()
555
540
  rows_updated = 0
556
541
 
557
542
  if not masking_request:
@@ -648,18 +633,6 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
648
633
  )
649
634
 
650
635
  self.unset_connector_state()
651
-
652
- awaiting_async_callback: bool = bool(
653
- masking_request.async_config
654
- and masking_request.async_config.strategy == AsyncTaskType.callback.value
655
- ) and bool(
656
- request_task.id
657
- ) # Only supported in DSR 3.0
658
- if awaiting_async_callback:
659
- # Asynchronous masking request detected in saas config.
660
- # If the masking request was marked to expect async results, original responses are ignored
661
- # and we raise an AwaitingAsyncTask to put this task in an awaiting_processing state.
662
- raise AwaitingAsyncTask()
663
636
  return rows_updated
664
637
 
665
638
  @staticmethod
@@ -1112,3 +1085,38 @@ class SaaSConnector(BaseConnector[AuthenticatedClient], Contextualizable):
1112
1085
  return []
1113
1086
 
1114
1087
  return consent_requests.opt_in if opt_in else consent_requests.opt_out # type: ignore
1088
+
1089
+
1090
+ def _get_async_dsr_strategy(
1091
+ session: Session,
1092
+ request_task: RequestTask,
1093
+ query_config: SaaSQueryConfig,
1094
+ action_type: ActionType,
1095
+ ) -> Optional[AsyncDSRStrategy]:
1096
+ """
1097
+ Returns the async DSR strategy if any of the read or masking requests have an async_config.
1098
+ """
1099
+
1100
+ # Async processing is only supported for DSR 3.0.
1101
+ # A request task with an ID of None is an indicator that the request is not DSR 3.0.
1102
+ if request_task.id is None:
1103
+ return None
1104
+
1105
+ if action_type == ActionType.access:
1106
+ read_requests = query_config.get_read_requests_by_identity()
1107
+ for request in read_requests:
1108
+ if request.async_config is not None:
1109
+ return get_strategy(
1110
+ request.async_config.strategy,
1111
+ session,
1112
+ request.async_config.configuration,
1113
+ )
1114
+ elif action_type == ActionType.erasure:
1115
+ masking_request = query_config.get_masking_request()
1116
+ if masking_request is not None and masking_request.async_config is not None:
1117
+ return get_strategy(
1118
+ masking_request.async_config.strategy,
1119
+ session,
1120
+ masking_request.async_config.configuration,
1121
+ )
1122
+ return None
@@ -5,7 +5,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple
5
5
  from loguru import logger
6
6
 
7
7
  from fides.api.models.attachment import Attachment, AttachmentType
8
- from fides.api.schemas.storage.storage import StorageDetails
8
+ from fides.api.schemas.storage.storage import StorageDetails, StorageType
9
9
 
10
10
 
11
11
  @dataclass
@@ -84,12 +84,19 @@ def get_attachments_content(
84
84
  continue
85
85
 
86
86
  processed_count += 1
87
+
88
+ # Handle bucket_name differently for different storage types
89
+ bucket_name = None
90
+ if attachment.config.type in [StorageType.s3, StorageType.gcs]:
91
+ bucket_name = attachment.config.details[StorageDetails.BUCKET.value]
92
+ # For local storage, bucket_name is not needed
93
+
87
94
  yield AttachmentData(
88
95
  file_name=attachment.file_name,
89
96
  file_size=size,
90
97
  download_url=str(url) if url else None,
91
98
  content_type=attachment.content_type,
92
- bucket_name=attachment.config.details[StorageDetails.BUCKET.value],
99
+ bucket_name=bucket_name or "", # Empty string for local storage
93
100
  file_key=attachment.file_key,
94
101
  storage_key=attachment.storage_key,
95
102
  )
@@ -2,9 +2,8 @@
2
2
  import time
3
3
  from copy import deepcopy
4
4
  from datetime import datetime, timedelta
5
- from typing import Any, Optional, Tuple
5
+ from typing import Any, Optional
6
6
 
7
- import requests
8
7
  from loguru import logger
9
8
  from pydantic import ValidationError as PydanticValidationError
10
9
  from sqlalchemy.orm import Query, Session
@@ -22,7 +21,6 @@ from fides.api.common_exceptions import (
22
21
  ValidationError,
23
22
  )
24
23
  from fides.api.db.session import get_db_session
25
- from fides.api.graph.config import CollectionAddress
26
24
  from fides.api.graph.graph import DatasetGraph
27
25
  from fides.api.models.attachment import Attachment, AttachmentReferenceType
28
26
  from fides.api.models.audit_log import AuditLog, AuditLogAction
@@ -53,7 +51,7 @@ from fides.api.schemas.policy import ActionType, CurrentStep
53
51
  from fides.api.schemas.privacy_request import PrivacyRequestStatus
54
52
  from fides.api.schemas.redis_cache import Identity
55
53
  from fides.api.schemas.storage.storage import StorageType
56
- from fides.api.service.connectors import FidesConnector, get_connector
54
+ from fides.api.service.connectors import get_connector
57
55
  from fides.api.service.connectors.consent_email_connector import (
58
56
  CONSENT_EMAIL_CONNECTOR_TYPES,
59
57
  )
@@ -85,11 +83,7 @@ from fides.api.util.collection_util import Row
85
83
  from fides.api.util.logger import Pii, _log_exception, _log_warning
86
84
  from fides.api.util.logger_context_utils import LoggerContextKeys, log_context
87
85
  from fides.api.util.memory_watchdog import memory_limiter
88
- from fides.common.api.v1.urn_registry import (
89
- PRIVACY_CENTER_DSR_PACKAGE,
90
- PRIVACY_REQUEST_TRANSFER_TO_PARENT,
91
- V1_URL_PREFIX,
92
- )
86
+ from fides.common.api.v1.urn_registry import PRIVACY_CENTER_DSR_PACKAGE
93
87
  from fides.config import CONFIG
94
88
  from fides.config.config_proxy import ConfigProxy
95
89
 
@@ -307,8 +301,11 @@ def upload_and_save_access_results( # pylint: disable=R0912
307
301
  ) -> list[str]:
308
302
  """Process the data uploads after the access portion of the privacy request has completed"""
309
303
  download_urls: list[str] = []
310
- # Remove manual webhook attachments from the list of attachments
311
- # This is done because the manual webhook attachments are already included in the manual_data
304
+ # Remove manual webhook attachments and request task attachments from the list of attachments
305
+ # This is done because:
306
+ # - manual webhook attachments are already included in the manual_data
307
+ # - manual task submission attachments are already included in the manual_data
308
+ # - request task attachments (from async polling) are already embedded in the dataset results
312
309
  loaded_attachments = [
313
310
  attachment
314
311
  for attachment in privacy_request.attachments
@@ -317,6 +314,7 @@ def upload_and_save_access_results( # pylint: disable=R0912
317
314
  in [
318
315
  AttachmentReferenceType.access_manual_webhook,
319
316
  AttachmentReferenceType.manual_task_submission,
317
+ AttachmentReferenceType.request_task,
320
318
  ]
321
319
  for ref in attachment.references
322
320
  )
@@ -922,78 +920,6 @@ def mark_paused_privacy_request_as_expired(privacy_request_id: str) -> None:
922
920
  db.close()
923
921
 
924
922
 
925
- def _retrieve_child_results( # pylint: disable=R0911
926
- fides_connector: Tuple[str, ConnectionConfig],
927
- rule_key: str,
928
- access_result: dict[str, list[Row]],
929
- ) -> Optional[list[dict[str, Optional[list[Row]]]]]:
930
- """Get child access request results to add to upload."""
931
- try:
932
- connector = FidesConnector(fides_connector[1])
933
- except Exception as e:
934
- logger.error(
935
- "Error create client for child server {}: {}", fides_connector[0], e
936
- )
937
- return None
938
-
939
- results = []
940
-
941
- for key, rows in access_result.items():
942
- address = CollectionAddress.from_string(key)
943
- privacy_request_id = None
944
- if address.dataset == fides_connector[0]:
945
- if not rows:
946
- logger.info("No rows found for result entry {}", key)
947
- continue
948
- privacy_request_id = rows[0]["id"]
949
-
950
- if not privacy_request_id:
951
- logger.error(
952
- "No privacy request found for connector key {}", fides_connector[0]
953
- )
954
- continue
955
-
956
- try:
957
- client = connector.create_client()
958
- except requests.exceptions.HTTPError as e:
959
- logger.error(
960
- "Error logger into to child server for privacy request {}: {}",
961
- privacy_request_id,
962
- e,
963
- )
964
- continue
965
-
966
- try:
967
- request = client.authenticated_request(
968
- method="get",
969
- path=f"{V1_URL_PREFIX}{PRIVACY_REQUEST_TRANSFER_TO_PARENT.format(privacy_request_id=privacy_request_id, rule_key=rule_key)}",
970
- headers={"Authorization": f"Bearer {client.token}"},
971
- )
972
- response = client.session.send(request)
973
- except requests.exceptions.HTTPError as e:
974
- logger.error(
975
- "Error retrieving data from child server for privacy request {}: {}",
976
- privacy_request_id,
977
- e,
978
- )
979
- continue
980
-
981
- if response.status_code != 200:
982
- logger.error(
983
- "Error retrieving data from child server for privacy request {}: {}",
984
- privacy_request_id,
985
- response.json(),
986
- )
987
- continue
988
-
989
- results.append(response.json())
990
-
991
- if not results:
992
- return None
993
-
994
- return results
995
-
996
-
997
923
  def get_consent_email_connection_configs(db: Session) -> Query:
998
924
  """Return enabled consent email connection configs."""
999
925
  return db.query(ConnectionConfig).filter(