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,678 @@
1
+ from typing import TYPE_CHECKING, Any, Dict, List, Tuple, cast
2
+ from uuid import uuid4
3
+
4
+ import pydash
5
+ from loguru import logger
6
+ from sqlalchemy.orm import Session
7
+
8
+ from fides.api.common_exceptions import (
9
+ AwaitingAsyncProcessing,
10
+ FidesopsException,
11
+ PrivacyRequestError,
12
+ )
13
+ from fides.api.models.policy import Policy
14
+ from fides.api.models.privacy_request.request_task import (
15
+ AsyncTaskType,
16
+ RequestTask,
17
+ RequestTaskSubRequest,
18
+ )
19
+ from fides.api.models.worker_task import ExecutionLogStatus
20
+ from fides.api.schemas.policy import ActionType
21
+ from fides.api.schemas.saas.async_polling_configuration import (
22
+ AsyncPollingConfiguration,
23
+ PollingResult,
24
+ PollingResultType,
25
+ )
26
+ from fides.api.schemas.saas.saas_config import ReadSaaSRequest, SaaSRequest
27
+ from fides.api.schemas.saas.shared_schemas import SaaSRequestParams
28
+ from fides.api.service.async_dsr.handlers.polling_attachment_handler import (
29
+ PollingAttachmentHandler,
30
+ )
31
+ from fides.api.service.async_dsr.handlers.polling_request_handler import (
32
+ PollingRequestHandler,
33
+ )
34
+ from fides.api.service.async_dsr.handlers.polling_response_handler import (
35
+ PollingResponseProcessor,
36
+ )
37
+ from fides.api.service.async_dsr.handlers.polling_sub_request_handler import (
38
+ PollingSubRequestHandler,
39
+ )
40
+ from fides.api.service.async_dsr.strategies.async_dsr_strategy import AsyncDSRStrategy
41
+ from fides.api.service.async_dsr.utils import AsyncPhase, get_async_phase
42
+ from fides.api.service.connectors.saas.authenticated_client import AuthenticatedClient
43
+ from fides.api.service.saas_request.saas_request_override_factory import (
44
+ SaaSRequestOverrideFactory,
45
+ SaaSRequestType,
46
+ )
47
+ from fides.api.util.collection_util import Row
48
+ from fides.api.util.saas_util import map_param_values
49
+ from fides.config import CONFIG
50
+
51
+ if TYPE_CHECKING:
52
+ from fides.api.models.privacy_request.privacy_request import PrivacyRequest
53
+ from fides.api.service.connectors.query_configs.saas_query_config import (
54
+ SaaSQueryConfig,
55
+ )
56
+
57
+
58
+ class AsyncPollingStrategy(AsyncDSRStrategy):
59
+ """
60
+ Enhanced strategy for polling async DSR requests.
61
+ Works for both access and erasure operations with internal phase-based organization.
62
+ """
63
+
64
+ type = AsyncTaskType.polling
65
+ configuration_model = AsyncPollingConfiguration
66
+
67
+ def __init__(self, session: Session, configuration: AsyncPollingConfiguration):
68
+ self.session = session
69
+ self.status_request = configuration.status_request
70
+ self.result_request = configuration.result_request
71
+
72
+ def async_retrieve_data(
73
+ self,
74
+ client: AuthenticatedClient,
75
+ request_task_id: str,
76
+ query_config: "SaaSQueryConfig",
77
+ input_data: Dict[str, List[Any]],
78
+ ) -> List[Row]:
79
+ """
80
+ Execute async retrieve data with internal phase routing.
81
+ """
82
+ request_task = self._get_request_task(request_task_id)
83
+ async_phase = get_async_phase(request_task, query_config)
84
+
85
+ if async_phase == AsyncPhase.initial_async:
86
+ return self._initial_request_access(
87
+ client, request_task, query_config, input_data
88
+ )
89
+
90
+ if async_phase == AsyncPhase.polling_continuation:
91
+ return self._polling_continuation_access(client, request_task, query_config)
92
+
93
+ logger.warning(
94
+ f"Unexpected async phase '{async_phase}' for polling access task {request_task.id}"
95
+ )
96
+ return []
97
+
98
+ def async_mask_data(
99
+ self,
100
+ client: AuthenticatedClient,
101
+ request_task_id: str,
102
+ query_config: "SaaSQueryConfig",
103
+ rows: List[Row],
104
+ ) -> int:
105
+ """
106
+ Execute async mask data with internal phase routing.
107
+ """
108
+ request_task = self._get_request_task(request_task_id)
109
+ async_phase = get_async_phase(request_task, query_config)
110
+
111
+ if async_phase == AsyncPhase.initial_async:
112
+ return self._initial_request_erasure(
113
+ client, request_task, query_config, rows
114
+ )
115
+
116
+ if async_phase == AsyncPhase.polling_continuation:
117
+ return self._polling_continuation_erasure(
118
+ client, request_task, query_config
119
+ )
120
+
121
+ logger.warning(
122
+ f"Unexpected async phase '{async_phase}' for polling erasure task {request_task.id}"
123
+ )
124
+ return 0
125
+
126
+ # Private helper methods
127
+
128
+ def _initial_request_access(
129
+ self,
130
+ client: AuthenticatedClient,
131
+ request_task: RequestTask,
132
+ query_config: "SaaSQueryConfig",
133
+ input_data: Dict[str, List[Any]],
134
+ ) -> List[Row]:
135
+ """Handle initial setup for access polling requests."""
136
+ logger.info(f"Initial polling request for access task {request_task.id}")
137
+
138
+ policy = request_task.privacy_request.policy
139
+
140
+ async_requests_to_process = [
141
+ req
142
+ for req in query_config.get_read_requests_by_identity()
143
+ if req.async_config
144
+ ]
145
+
146
+ if not async_requests_to_process:
147
+ logger.warning(
148
+ f"No async-configured read requests found for task {request_task.id}"
149
+ )
150
+ return []
151
+
152
+ request_task.async_type = AsyncTaskType.polling
153
+ self.session.add(request_task)
154
+ self.session.commit()
155
+
156
+ for read_request in async_requests_to_process:
157
+ logger.info(
158
+ f"Creating initial polling sub-requests for task {request_task.id}"
159
+ )
160
+ self._handle_polling_initial_request(
161
+ request_task,
162
+ query_config,
163
+ read_request,
164
+ input_data,
165
+ policy,
166
+ client,
167
+ )
168
+
169
+ self.session.refresh(request_task)
170
+ raise AwaitingAsyncProcessing(
171
+ f"Waiting for next scheduled check of {request_task.dataset_name} access results."
172
+ )
173
+
174
+ def _initial_request_erasure(
175
+ self,
176
+ client: AuthenticatedClient,
177
+ request_task: RequestTask,
178
+ query_config: "SaaSQueryConfig",
179
+ rows: List[Row],
180
+ ) -> int:
181
+ """Handle initial setup for erasure polling requests."""
182
+ logger.info(f"Initial polling request for erasure task {request_task.id}")
183
+
184
+ privacy_request = request_task.privacy_request
185
+ policy = privacy_request.policy
186
+
187
+ all_requests = []
188
+ masking_request = query_config.get_masking_request()
189
+ if masking_request:
190
+ all_requests.append(masking_request)
191
+
192
+ # Set async type once for the task
193
+ request_task.async_type = AsyncTaskType.polling
194
+ self.session.add(request_task)
195
+ self.session.commit()
196
+
197
+ for request in all_requests:
198
+ if not (request.async_config and request_task.id):
199
+ continue
200
+
201
+ if request.path:
202
+ logger.info(
203
+ f"Executing initial masking request for polling task {request_task.id}"
204
+ )
205
+ self._handle_polling_initial_erasure_request(
206
+ request_task,
207
+ query_config,
208
+ request,
209
+ rows,
210
+ policy,
211
+ privacy_request,
212
+ client,
213
+ )
214
+
215
+ # After processing all requests, raise AwaitingAsyncProcessing (like access flow)
216
+ # But only if we actually created any sub-requests
217
+ self.session.refresh(request_task)
218
+ if request_task.sub_requests:
219
+ raise AwaitingAsyncProcessing(
220
+ f"Waiting for next scheduled check of {request_task.dataset_name} erasure results."
221
+ )
222
+ # If no sub-requests were created (no rows to process), the task is already complete
223
+ return 0
224
+
225
+ def _polling_continuation_access(
226
+ self,
227
+ client: AuthenticatedClient,
228
+ request_task: RequestTask,
229
+ query_config: "SaaSQueryConfig",
230
+ ) -> List[Row]:
231
+ """Handle polling continuation for access requests."""
232
+ logger.info(f"Continuing polling for access task {request_task.id}")
233
+
234
+ polling_complete = self._execute_polling_requests(
235
+ client, request_task, query_config
236
+ )
237
+
238
+ if not polling_complete:
239
+ raise AwaitingAsyncProcessing(
240
+ f"Waiting for next scheduled check of {request_task.dataset_name} access results."
241
+ )
242
+
243
+ # Aggregate all sub-request results, merging attachments
244
+ aggregated_results: List[Row] = []
245
+ merged_attachments = []
246
+
247
+ for sub_request in request_task.sub_requests:
248
+ if sub_request.access_data:
249
+ for row in sub_request.access_data:
250
+ # Check if this row has retrieved_attachments
251
+ if "retrieved_attachments" in row:
252
+ # Collect attachments for merging
253
+ merged_attachments.extend(row["retrieved_attachments"])
254
+ # Remove retrieved_attachments from the row to avoid duplication
255
+ row_without_attachments = {
256
+ k: v for k, v in row.items() if k != "retrieved_attachments"
257
+ }
258
+ # Only add the row if it's not empty or if it's the first row
259
+ if row_without_attachments or not aggregated_results:
260
+ aggregated_results.append(row_without_attachments)
261
+ else:
262
+ # Regular row without attachments
263
+ aggregated_results.append(row)
264
+
265
+ # If we have merged attachments, add them to the first aggregated result
266
+ if merged_attachments and aggregated_results:
267
+ aggregated_results[0]["retrieved_attachments"] = merged_attachments
268
+
269
+ return aggregated_results
270
+
271
+ def _polling_continuation_erasure(
272
+ self,
273
+ client: AuthenticatedClient,
274
+ request_task: RequestTask,
275
+ query_config: "SaaSQueryConfig",
276
+ ) -> int:
277
+ """Handle polling continuation for erasure requests."""
278
+ logger.info(f"Continuing polling for erasure task {request_task.id}")
279
+
280
+ polling_complete = self._execute_polling_requests(
281
+ client, request_task, query_config
282
+ )
283
+
284
+ if not polling_complete:
285
+ raise AwaitingAsyncProcessing(
286
+ f"Waiting for next scheduled check of {request_task.dataset_name} erasure results."
287
+ )
288
+
289
+ # Aggregate rows_masked from all sub-requests
290
+ total_rows_masked = sum(
291
+ sub_request.rows_masked or 0 for sub_request in request_task.sub_requests
292
+ )
293
+ return total_rows_masked
294
+
295
+ def _handle_polling_initial_request(
296
+ self,
297
+ request_task: RequestTask,
298
+ query_config: "SaaSQueryConfig",
299
+ read_request: ReadSaaSRequest,
300
+ input_data: Dict[str, Any],
301
+ policy: Policy,
302
+ client: "AuthenticatedClient",
303
+ ) -> None:
304
+ """Handles the setup for asynchronous initial requests."""
305
+ query_config.action = "Polling - start"
306
+ prepared_requests: List[Tuple[SaaSRequestParams, Dict[str, Any]]] = (
307
+ query_config.generate_requests(input_data, policy, read_request)
308
+ )
309
+ logger.info(f"Prepared requests: {len(prepared_requests)}")
310
+
311
+ for next_request, param_value_map in prepared_requests:
312
+ response = client.send(next_request)
313
+
314
+ if not response.ok:
315
+ raise FidesopsException(
316
+ f"Initial async request failed with status code {response.status_code}: {response.text}"
317
+ )
318
+
319
+ try:
320
+ response_data = response.json()
321
+ correlation_id = pydash.get(
322
+ response_data, read_request.correlation_id_path
323
+ )
324
+ if not correlation_id:
325
+ raise FidesopsException(
326
+ f"Could not extract correlation ID from response using path: {read_request.correlation_id_path}"
327
+ )
328
+ except ValueError as exc:
329
+ raise FidesopsException(
330
+ f"Invalid JSON response from initial request: {exc}"
331
+ )
332
+
333
+ param_value_map["correlation_id"] = str(correlation_id)
334
+ PollingSubRequestHandler.create_sub_request(
335
+ self.session, request_task, param_value_map
336
+ )
337
+
338
+ def _handle_polling_initial_erasure_request(
339
+ self,
340
+ request_task: RequestTask,
341
+ query_config: "SaaSQueryConfig",
342
+ request: SaaSRequest,
343
+ rows: List[Row],
344
+ policy: Policy,
345
+ privacy_request: "PrivacyRequest",
346
+ client: "AuthenticatedClient",
347
+ ) -> None:
348
+ """Handles the setup for asynchronous initial erasure requests."""
349
+ logger.info(
350
+ f"Processing {len(rows)} rows for erasure request in task {request_task.id}"
351
+ )
352
+
353
+ # Create sub-requests for erasure operations (similar to access operations)
354
+ for row in rows or [{}]:
355
+ try:
356
+ # Generate parameter values first (like access requests)
357
+ param_value_map = query_config.generate_update_param_values(
358
+ row, policy, privacy_request, request
359
+ )
360
+
361
+ # Generate the update request using the param_values
362
+ prepared_request = query_config.generate_update_stmt(
363
+ row, policy, privacy_request
364
+ )
365
+ response = client.send(prepared_request, request.ignore_errors)
366
+
367
+ # Extract correlation ID from response (required, like access requests)
368
+ try:
369
+ response_data = response.json()
370
+ correlation_id = pydash.get(
371
+ response_data, request.correlation_id_path
372
+ )
373
+ if not correlation_id:
374
+ raise FidesopsException(
375
+ f"Could not extract correlation ID from response using path: {request.correlation_id_path}"
376
+ )
377
+ except ValueError as exc:
378
+ raise FidesopsException(
379
+ f"Invalid JSON response from initial erasure request: {exc}"
380
+ )
381
+
382
+ # Add correlation_id to the existing param_value_map (like access requests)
383
+ param_value_map["correlation_id"] = str(correlation_id)
384
+ PollingSubRequestHandler.create_sub_request(
385
+ self.session, request_task, param_value_map
386
+ )
387
+
388
+ except ValueError as exc:
389
+ if request.skip_missing_param_values:
390
+ logger.debug("Skipping optional masking request: {}", exc)
391
+ continue
392
+ raise exc
393
+
394
+ def _get_requests_for_action(
395
+ self, polling_task: RequestTask, query_config: "SaaSQueryConfig"
396
+ ) -> List[ReadSaaSRequest]:
397
+ """
398
+ Get the appropriate requests based on the action type.
399
+
400
+ Args:
401
+ polling_task: The polling task to get requests for
402
+ query_config: The SaaS query configuration
403
+
404
+ Returns:
405
+ List of ReadSaaSRequest objects for the given action type
406
+
407
+ Raises:
408
+ PrivacyRequestError: If action type is unsupported or masking request not found
409
+ """
410
+ # Validate result_request is provided for access operations
411
+ if polling_task.action_type == ActionType.access and not self.result_request:
412
+ raise PrivacyRequestError(
413
+ f"result_request is required for access operations but was not provided in polling configuration for task {polling_task.id}"
414
+ )
415
+
416
+ if polling_task.action_type == ActionType.access:
417
+ return list(query_config.get_read_requests_by_identity())
418
+ if polling_task.action_type == ActionType.erasure:
419
+ masking_request = query_config.get_masking_request()
420
+ if not masking_request:
421
+ raise PrivacyRequestError(
422
+ f"No masking request found for erasure task {polling_task.id}"
423
+ )
424
+ return [cast(ReadSaaSRequest, masking_request)]
425
+ raise PrivacyRequestError(
426
+ f"Unsupported action type: {polling_task.action_type}"
427
+ )
428
+
429
+ def _check_sub_request_status(
430
+ self, client: AuthenticatedClient, param_values: Dict[str, Any]
431
+ ) -> bool:
432
+ """
433
+ Check the status of a sub-request using either override function or HTTP request.
434
+
435
+ Args:
436
+ client: The authenticated client
437
+ param_values: The parameter values for the request
438
+
439
+ Returns:
440
+ bool: True if the request is complete, False if still in progress
441
+
442
+ Raises:
443
+ PrivacyRequestError: If status_path is required but not provided
444
+ """
445
+ # Check for status override vs standard HTTP request
446
+ if self.status_request.request_override:
447
+ # Handle status override function directly
448
+ override_function = SaaSRequestOverrideFactory.get_override(
449
+ self.status_request.request_override,
450
+ SaaSRequestType.POLLING_STATUS,
451
+ )
452
+
453
+ # Override functions return boolean status directly
454
+ return cast(
455
+ bool,
456
+ override_function(
457
+ client=client,
458
+ param_values=param_values,
459
+ request_config=self.status_request,
460
+ secrets=client.configuration.secrets,
461
+ ),
462
+ )
463
+
464
+ # Standard HTTP status request - create handler only when needed
465
+ polling_handler = PollingRequestHandler(
466
+ self.status_request, self.result_request
467
+ )
468
+ response = polling_handler.get_status_response(client, param_values)
469
+
470
+ # Process status response
471
+ status_path = self.status_request.status_path
472
+
473
+ if status_path is None:
474
+ raise PrivacyRequestError(
475
+ "status_path is required when request_override is not provided"
476
+ )
477
+
478
+ return PollingResponseProcessor.evaluate_status_response(
479
+ response,
480
+ status_path,
481
+ self.status_request.status_completed_value,
482
+ )
483
+
484
+ def _process_completed_sub_request(
485
+ self,
486
+ client: AuthenticatedClient,
487
+ param_values: Dict[str, Any],
488
+ sub_request: RequestTaskSubRequest,
489
+ polling_task: RequestTask,
490
+ ) -> None:
491
+ """
492
+ Process a completed sub-request by getting results and handling them appropriately.
493
+
494
+ Args:
495
+ client: The authenticated client
496
+ param_values: The parameter values for the request
497
+ sub_request: The completed sub-request
498
+ polling_task: The parent polling task
499
+
500
+ Raises:
501
+ PrivacyRequestError: If polling result is not the expected type
502
+ """
503
+ # Handle erasure operations differently - they don't need result_request
504
+ if polling_task.action_type == ActionType.erasure:
505
+ # For erasure operations, just mark as complete and increment counter
506
+ sub_request.rows_masked = 1
507
+ sub_request.update_status(self.session, ExecutionLogStatus.complete.value)
508
+ logger.info(
509
+ f"Sub-request {sub_request.id} for {polling_task.action_type} task {polling_task.id} completed"
510
+ )
511
+ return
512
+
513
+ # For access operations, we need result_request to get the data
514
+ if not self.result_request:
515
+ raise PrivacyRequestError(
516
+ f"result_request is required for processing completed sub-request {sub_request.id} but was not provided"
517
+ )
518
+
519
+ if self.result_request.request_override:
520
+ # Handle override function directly
521
+ override_function = SaaSRequestOverrideFactory.get_override(
522
+ self.result_request.request_override,
523
+ SaaSRequestType.POLLING_RESULT,
524
+ )
525
+
526
+ polling_result = override_function(
527
+ client=client,
528
+ param_values=param_values,
529
+ request_config=self.result_request,
530
+ secrets=client.configuration.secrets,
531
+ )
532
+ else:
533
+ # Standard HTTP request processing
534
+ polling_handler = PollingRequestHandler(
535
+ self.status_request, self.result_request
536
+ )
537
+ response = polling_handler.get_result_response(client, param_values)
538
+
539
+ # We need to reconstruct the request path for processing
540
+ prepared_result_request = map_param_values(
541
+ action="result",
542
+ context="polling request",
543
+ current_request=self.result_request,
544
+ param_values=param_values,
545
+ )
546
+
547
+ polling_result = PollingResponseProcessor.process_result_response(
548
+ prepared_result_request.path,
549
+ response,
550
+ self.result_request.result_path,
551
+ )
552
+
553
+ # Ensure we have the expected polling result type
554
+ if not isinstance(polling_result, PollingResult):
555
+ raise PrivacyRequestError("Polling result must be PollingResult instance")
556
+
557
+ # Store results on the sub-request
558
+ self._store_sub_request_result(polling_result, sub_request, polling_task)
559
+
560
+ # Mark as complete using existing method
561
+ sub_request.update_status(self.session, ExecutionLogStatus.complete.value)
562
+
563
+ logger.info(
564
+ f"Sub-request {sub_request.id} for task {polling_task.id} completed successfully"
565
+ )
566
+
567
+ def _process_sub_requests_for_request(
568
+ self,
569
+ client: AuthenticatedClient,
570
+ request: ReadSaaSRequest,
571
+ polling_task: RequestTask,
572
+ ) -> None:
573
+ """
574
+ Process all sub-requests for a given request.
575
+
576
+ Args:
577
+ client: The authenticated client
578
+ request: The SaaS request being processed
579
+ polling_task: The parent polling task
580
+ """
581
+ sub_requests: List[RequestTaskSubRequest] = polling_task.sub_requests
582
+
583
+ for sub_request in sub_requests:
584
+ # Skip already completed sub-requests
585
+ if sub_request.status == ExecutionLogStatus.complete.value:
586
+ continue
587
+
588
+ param_values = sub_request.param_values
589
+
590
+ try:
591
+ # Check status of the sub-request
592
+ status = self._check_sub_request_status(client, param_values)
593
+
594
+ if status:
595
+ self._process_completed_sub_request(
596
+ client, param_values, sub_request, polling_task
597
+ )
598
+ else:
599
+ logger.debug(
600
+ f"Sub-request {sub_request.id} for task {polling_task.id} still not ready"
601
+ )
602
+
603
+ except Exception as exc:
604
+ logger.error(
605
+ f"Error processing sub-request {sub_request.id} for task {polling_task.id}: {exc}"
606
+ )
607
+ sub_request.update_status(self.session, ExecutionLogStatus.error.value)
608
+ raise exc
609
+
610
+ def _execute_polling_requests(
611
+ self,
612
+ client: AuthenticatedClient,
613
+ polling_task: RequestTask,
614
+ query_config: "SaaSQueryConfig",
615
+ ) -> bool:
616
+ """
617
+ Internal polling execution orchestrator with proper error handling and timeout management.
618
+
619
+ Stores results on individual sub-requests and only aggregates when ALL are successful.
620
+ Implements timeout checking and raises exceptions for errors and timeouts.
621
+
622
+ Returns:
623
+ bool: True if all polling is complete (success or failure), False if still in progress
624
+ """
625
+ # Check for timeout before processing requests
626
+ timeout_days = CONFIG.execution.async_polling_request_timeout_days
627
+ PollingSubRequestHandler.check_timeout(polling_task, timeout_days)
628
+
629
+ # Get appropriate requests based on action type
630
+ requests = self._get_requests_for_action(polling_task, query_config)
631
+
632
+ # Process each request and its sub-requests
633
+ for request in requests:
634
+ if request.async_config:
635
+ self._process_sub_requests_for_request(client, request, polling_task)
636
+
637
+ # Check final status and return completion status
638
+ return PollingSubRequestHandler.check_completion(polling_task)
639
+
640
+ def _store_sub_request_result(
641
+ self,
642
+ polling_result: PollingResult,
643
+ sub_request: RequestTaskSubRequest,
644
+ polling_task: RequestTask,
645
+ ) -> None:
646
+ """Store result data on the individual sub-request."""
647
+ if polling_result.result_type == PollingResultType.rows:
648
+ # Store rows directly on the sub-request (data is always a list for rows)
649
+ sub_request.access_data = polling_result.data
650
+ sub_request.save(self.session)
651
+
652
+ elif polling_result.result_type == PollingResultType.attachment:
653
+ try:
654
+ attachment_bytes = PollingAttachmentHandler.ensure_attachment_bytes(
655
+ polling_result.data
656
+ )
657
+ attachment_id = PollingAttachmentHandler.store_attachment(
658
+ self.session,
659
+ polling_task,
660
+ attachment_data=attachment_bytes,
661
+ filename=polling_result.metadata.get(
662
+ "filename", f"attachment_{str(uuid4())[:8]}"
663
+ ),
664
+ )
665
+
666
+ # Store attachment metadata on sub-request
667
+ attachment_metadata: List[Row] = [{"retrieved_attachments": []}]
668
+ PollingAttachmentHandler.add_metadata_to_rows(
669
+ self.session, attachment_id, attachment_metadata
670
+ )
671
+ sub_request.access_data = attachment_metadata
672
+ sub_request.save(self.session)
673
+ except Exception as exc:
674
+ raise PrivacyRequestError(f"Attachment storage failed: {exc}")
675
+ else:
676
+ raise PrivacyRequestError(
677
+ f"Unsupported result type: {polling_result.result_type}"
678
+ )