ethyca-fides 2.73.2b0__py2.py3-none-any.whl → 2.73.2b1__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 (237) hide show
  1. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/RECORD +233 -230
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/xx_2025_10_29_1659_80d28dea3b6b_added_duplicate_group_table.py +79 -0
  5. fides/api/models/privacy_request/duplicate_group.py +84 -0
  6. fides/api/models/privacy_request/privacy_request.py +12 -2
  7. fides/api/schemas/privacy_request.py +2 -1
  8. fides/api/service/async_dsr/strategies/async_dsr_strategy_polling.py +13 -6
  9. fides/api/service/privacy_request/duplication_detection.py +357 -0
  10. fides/api/service/saas_request/saas_request_override_factory.py +7 -2
  11. fides/ui-build/static/admin/404.html +1 -1
  12. fides/ui-build/static/admin/_next/static/chunks/5909-56b37040ff0e6933.js +1 -0
  13. fides/ui-build/static/admin/_next/static/chunks/{7245-c9bc628d078c2170.js → 7245-c41aa2528645a47d.js} +1 -1
  14. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-ee588a308812715d.js → _app-2986534ccbf5e4a5.js} +2 -2
  15. fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-cc9e47f347b657bf.js +1 -0
  16. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-9692057d7ba62e04.js +1 -0
  17. fides/ui-build/static/admin/_next/static/wfI6N9ecEPXinU3OlxHLy/_buildManifest.js +1 -0
  18. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  19. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  20. fides/ui-build/static/admin/add-systems.html +1 -1
  21. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  22. fides/ui-build/static/admin/consent/configure.html +1 -1
  23. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  24. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  25. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  26. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  27. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  28. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  29. fides/ui-build/static/admin/consent/properties.html +1 -1
  30. fides/ui-build/static/admin/consent/reporting.html +1 -1
  31. fides/ui-build/static/admin/consent.html +1 -1
  32. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  33. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  34. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  35. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  36. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  37. fides/ui-build/static/admin/data-catalog.html +1 -1
  38. fides/ui-build/static/admin/data-discovery/action-center/datastore/[monitorId].html +1 -1
  39. fides/ui-build/static/admin/data-discovery/action-center/datastore.html +1 -1
  40. fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId]/[systemId].html +1 -1
  41. fides/ui-build/static/admin/data-discovery/action-center/website/[monitorId].html +1 -1
  42. fides/ui-build/static/admin/data-discovery/action-center/website.html +1 -1
  43. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  44. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  45. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  46. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  47. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  48. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  49. fides/ui-build/static/admin/datamap.html +1 -1
  50. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  51. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  52. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  53. fides/ui-build/static/admin/dataset/new.html +1 -1
  54. fides/ui-build/static/admin/dataset.html +1 -1
  55. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  56. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  57. fides/ui-build/static/admin/datastore-connection.html +1 -1
  58. fides/ui-build/static/admin/index.html +1 -1
  59. fides/ui-build/static/admin/integrations/[id].html +1 -1
  60. fides/ui-build/static/admin/integrations.html +1 -1
  61. fides/ui-build/static/admin/login/[provider].html +1 -1
  62. fides/ui-build/static/admin/login.html +1 -1
  63. fides/ui-build/static/admin/new-privacy-requests.html +1 -1
  64. fides/ui-build/static/admin/notifications/digests/[id].html +1 -1
  65. fides/ui-build/static/admin/notifications/digests/new.html +1 -1
  66. fides/ui-build/static/admin/notifications/digests.html +1 -1
  67. fides/ui-build/static/admin/notifications/providers/[key].html +1 -1
  68. fides/ui-build/static/admin/notifications/providers/new.html +1 -1
  69. fides/ui-build/static/admin/notifications/providers.html +1 -1
  70. fides/ui-build/static/admin/notifications/templates/[id].html +1 -1
  71. fides/ui-build/static/admin/notifications/templates/add-template.html +1 -1
  72. fides/ui-build/static/admin/notifications/templates.html +1 -1
  73. fides/ui-build/static/admin/notifications.html +1 -1
  74. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  75. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  76. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  77. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  78. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  79. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  80. fides/ui-build/static/admin/poc/forms.html +1 -1
  81. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  82. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  83. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  84. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  85. fides/ui-build/static/admin/privacy-requests.html +1 -1
  86. fides/ui-build/static/admin/properties/[id].html +1 -1
  87. fides/ui-build/static/admin/properties/add-property.html +1 -1
  88. fides/ui-build/static/admin/properties.html +1 -1
  89. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  90. fides/ui-build/static/admin/sandbox/privacy-notices.html +1 -1
  91. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  92. fides/ui-build/static/admin/settings/about.html +1 -1
  93. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  94. fides/ui-build/static/admin/settings/consent.html +1 -1
  95. fides/ui-build/static/admin/settings/custom-fields/[id].html +1 -1
  96. fides/ui-build/static/admin/settings/custom-fields/new.html +1 -1
  97. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  98. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  99. fides/ui-build/static/admin/settings/domains.html +1 -1
  100. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  101. fides/ui-build/static/admin/settings/locations.html +1 -1
  102. fides/ui-build/static/admin/settings/organization.html +1 -1
  103. fides/ui-build/static/admin/settings/privacy-requests.html +1 -1
  104. fides/ui-build/static/admin/settings/regulations.html +1 -1
  105. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  106. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  107. fides/ui-build/static/admin/systems.html +1 -1
  108. fides/ui-build/static/admin/taxonomy.html +1 -1
  109. fides/ui-build/static/admin/user-management/new.html +1 -1
  110. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  111. fides/ui-build/static/admin/user-management.html +1 -1
  112. fides/ui-build/static/admin/_next/static/chunks/3873-d18e47b327445db5.js +0 -1
  113. fides/ui-build/static/admin/_next/static/chunks/pages/new-privacy-requests-df0c95e408c54c7e.js +0 -1
  114. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests-6e4c535b6d614596.js +0 -1
  115. fides/ui-build/static/admin/_next/static/wCNFtmYQhEDMaMPeBB4BM/_buildManifest.js +0 -1
  116. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/WHEEL +0 -0
  117. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/entry_points.txt +0 -0
  118. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/licenses/LICENSE +0 -0
  119. {ethyca_fides-2.73.2b0.dist-info → ethyca_fides-2.73.2b1.dist-info}/top_level.txt +0 -0
  120. /fides/ui-build/static/admin/_next/static/chunks/{1099-31f9d973bc287df8.js → 1099-53dc1dd16a1e615b.js} +0 -0
  121. /fides/ui-build/static/admin/_next/static/chunks/{1115-26393b586775ea29.js → 1115-5f133b52b4c719f2.js} +0 -0
  122. /fides/ui-build/static/admin/_next/static/chunks/{1437-8b1f6c8797c68bfd.js → 1437-9485f6a257000689.js} +0 -0
  123. /fides/ui-build/static/admin/_next/static/chunks/{155-f6302d32cba4cab6.js → 155-c4b86b97419eee1b.js} +0 -0
  124. /fides/ui-build/static/admin/_next/static/chunks/{1817-2d5cf537a2992c79.js → 1817-a317936d371df0d0.js} +0 -0
  125. /fides/ui-build/static/admin/_next/static/chunks/{2040-7eed8491ca7276ed.js → 2040-041247847a1d23c9.js} +0 -0
  126. /fides/ui-build/static/admin/_next/static/chunks/{2397-083fc511acb6105d.js → 2397-2426b7f2f3c61c4b.js} +0 -0
  127. /fides/ui-build/static/admin/_next/static/chunks/{2921-34a43f2f8f5e5e69.js → 2921-4ee55c84908dcb6e.js} +0 -0
  128. /fides/ui-build/static/admin/_next/static/chunks/{3377-988ac2f3a2e8d5d4.js → 3377-421918a9708a85fa.js} +0 -0
  129. /fides/ui-build/static/admin/_next/static/chunks/{3696-6f90e41a53d22920.js → 3696-a22951845ef33489.js} +0 -0
  130. /fides/ui-build/static/admin/_next/static/chunks/{3700-f695f2f6b8251971.js → 3700-5cd6f6a219952e84.js} +0 -0
  131. /fides/ui-build/static/admin/_next/static/chunks/{3772-9f1713f9d5f97a10.js → 3772-240d907365cfd536.js} +0 -0
  132. /fides/ui-build/static/admin/_next/static/chunks/{3923-f0a85dc5c3684fa0.js → 3923-c0d5a344c1bcb75f.js} +0 -0
  133. /fides/ui-build/static/admin/_next/static/chunks/{401-7c345d019bb9bcbd.js → 401-2ae64c1931df4bbe.js} +0 -0
  134. /fides/ui-build/static/admin/_next/static/chunks/{4496-4ff19366c597ec16.js → 4496-94d9ef9a7b77bcd8.js} +0 -0
  135. /fides/ui-build/static/admin/_next/static/chunks/{4817-1f3e6ea38625d8d5.js → 4817-119bf7124a66b06d.js} +0 -0
  136. /fides/ui-build/static/admin/_next/static/chunks/{5185-e7f8b81dd3dfbe0b.js → 5185-6220718ffe0578af.js} +0 -0
  137. /fides/ui-build/static/admin/_next/static/chunks/{5258-62d6bc19add60aa6.js → 5258-566264abf15c818e.js} +0 -0
  138. /fides/ui-build/static/admin/_next/static/chunks/{5279-bd6cccabdd6ca447.js → 5279-d702b465ff72c384.js} +0 -0
  139. /fides/ui-build/static/admin/_next/static/chunks/{549-28537a6de666944b.js → 549-9ea1f2819cfa2b47.js} +0 -0
  140. /fides/ui-build/static/admin/_next/static/chunks/{5643-3459282d296a3c59.js → 5643-2c3308ff2cee965d.js} +0 -0
  141. /fides/ui-build/static/admin/_next/static/chunks/{5783-21775c232dce7af7.js → 5783-50e5735cf9cb0f2e.js} +0 -0
  142. /fides/ui-build/static/admin/_next/static/chunks/{6277-3759894435cb8569.js → 6277-7c765deedc54e9a6.js} +0 -0
  143. /fides/ui-build/static/admin/_next/static/chunks/{6315-e2fb5ea77179a871.js → 6315-9f22209b868f61a5.js} +0 -0
  144. /fides/ui-build/static/admin/_next/static/chunks/{6853-de9905d28e5b19b3.js → 6853-748e92c6414f313e.js} +0 -0
  145. /fides/ui-build/static/admin/_next/static/chunks/{6954-84789a4e4fb04eb9.js → 6954-2a52d012b8c44598.js} +0 -0
  146. /fides/ui-build/static/admin/_next/static/chunks/{7476-a02d970ea4d3f7d0.js → 7476-c1e9b034b4d3b1b6.js} +0 -0
  147. /fides/ui-build/static/admin/_next/static/chunks/{7488-cf92601852e3c509.js → 7488-4145699c604c9554.js} +0 -0
  148. /fides/ui-build/static/admin/_next/static/chunks/{7630-a11610c2b31ab2ca.js → 7630-a6631a3a78029573.js} +0 -0
  149. /fides/ui-build/static/admin/_next/static/chunks/{7773-9ae233109bc64ec2.js → 7773-dd824bff406d3057.js} +0 -0
  150. /fides/ui-build/static/admin/_next/static/chunks/{796-e36d610066135f8c.js → 796-8f56cd4d8dfa663e.js} +0 -0
  151. /fides/ui-build/static/admin/_next/static/chunks/{8011-75af8b480fa114e6.js → 8011-3d6cc0af757b41de.js} +0 -0
  152. /fides/ui-build/static/admin/_next/static/chunks/{8212-393420e5a9751791.js → 8212-10bf793014dafd8b.js} +0 -0
  153. /fides/ui-build/static/admin/_next/static/chunks/{8373-22b4d20e8cc06b3a.js → 8373-9b5db3cceef9add4.js} +0 -0
  154. /fides/ui-build/static/admin/_next/static/chunks/{9046-d9c6498368b993d1.js → 9046-48bd5b591ff9cadc.js} +0 -0
  155. /fides/ui-build/static/admin/_next/static/chunks/{9330-f753636a31c4ea04.js → 9330-4a29ef6a17e61b3f.js} +0 -0
  156. /fides/ui-build/static/admin/_next/static/chunks/{9682-d1a3afa1394f8304.js → 9682-1cfc98b906cf0d97.js} +0 -0
  157. /fides/ui-build/static/admin/_next/static/chunks/{9826-a737a9956c1d0905.js → 9826-517b8ba731a6642e.js} +0 -0
  158. /fides/ui-build/static/admin/_next/static/chunks/{9899-d6437facac926264.js → 9899-5eea93b50aee65f7.js} +0 -0
  159. /fides/ui-build/static/admin/_next/static/chunks/{9965-25621dd507e0cfd6.js → 9965-e3a7ac5848e62b5f.js} +0 -0
  160. /fides/ui-build/static/admin/_next/static/chunks/pages/{404-eb019192ce498f32.js → 404-e30ec05084303d6b.js} +0 -0
  161. /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{manual-7081e0e49f67716c.js → manual-6466b8cdac3674ea.js} +0 -0
  162. /fides/ui-build/static/admin/_next/static/chunks/pages/add-systems/{multiple-a188f84239f4b2a8.js → multiple-bc06457d27216fad.js} +0 -0
  163. /fides/ui-build/static/admin/_next/static/chunks/pages/{add-systems-ee9df33ebd471099.js → add-systems-eeba4d74db895be7.js} +0 -0
  164. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/configure/{add-vendors-4f9cf087fcee87e6.js → add-vendors-ff54e031f93924ad.js} +0 -0
  165. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{configure-9af75caefc74eaca.js → configure-13651347dee871d0.js} +0 -0
  166. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-1b02a4991201b7e4.js → [id]-3645a8f2244cdcf8.js} +0 -0
  167. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-experience-b08abefec298ccf1.js → privacy-experience-c33ebeb4a7288596.js} +0 -0
  168. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-8c164c4b8310214e.js → [id]-d1868093b96d5dd0.js} +0 -0
  169. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-db789002d84c8829.js → new-a3de236a9ae52934.js} +0 -0
  170. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{privacy-notices-6528eb24165aceb6.js → privacy-notices-e530117d048ce019.js} +0 -0
  171. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{properties-5cba30eba1e97e56.js → properties-29b62430692da924.js} +0 -0
  172. /fides/ui-build/static/admin/_next/static/chunks/pages/consent/{reporting-11f1683aa15e1a62.js → reporting-4d1f50fbe0ed1562.js} +0 -0
  173. /fides/ui-build/static/admin/_next/static/chunks/pages/{consent-614af0a2c2ba966c.js → consent-5bebe58701b192c8.js} +0 -0
  174. /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/projects/{[projectUrn]-d60761c20382b259.js → [projectUrn]-cb92b6b0bda753df.js} +0 -0
  175. /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{projects-07e7d38ce34e1e6c.js → projects-52a3660671c248ed.js} +0 -0
  176. /fides/ui-build/static/admin/_next/static/chunks/pages/data-catalog/[systemId]/{resources-aed94957009eb3fd.js → resources-26155daa740bcc3a.js} +0 -0
  177. /fides/ui-build/static/admin/_next/static/chunks/pages/{data-catalog-127c114dd8f102ed.js → data-catalog-487fcb8f2caed2da.js} +0 -0
  178. /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/datastore/{[monitorId]-5aa7a9fa96160de8.js → [monitorId]-b74862be16b8e2ae.js} +0 -0
  179. /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/[monitorId]/{[systemId]-899bf30dde8b3292.js → [systemId]-3d9fe61dd9e6f664.js} +0 -0
  180. /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/action-center/website/{[monitorId]-73085f50abb775c0.js → [monitorId]-958927ffa1fd8645.js} +0 -0
  181. /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{action-center-823d0dd77e66585b.js → action-center-037f91cf033616bc.js} +0 -0
  182. /fides/ui-build/static/admin/_next/static/chunks/pages/data-discovery/{activity-24a82e07a0008516.js → activity-a1cfa65b549a8612.js} +0 -0
  183. /fides/ui-build/static/admin/_next/static/chunks/pages/{datamap-3b100c44ea9e3988.js → datamap-2f28227308bcddd3.js} +0 -0
  184. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/[collectionName]/{[...subfieldNames]-5f88280db168083e.js → [...subfieldNames]-6d342de24c1a91ad.js} +0 -0
  185. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/[datasetId]/{[collectionName]-bfbcf19c28c794ae.js → [collectionName]-8f78f42dafb99e59.js} +0 -0
  186. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{[datasetId]-6fbe2b584a509226.js → [datasetId]-2b3cdd68b333aadf.js} +0 -0
  187. /fides/ui-build/static/admin/_next/static/chunks/pages/dataset/{new-3d1e48f4b95d7f6b.js → new-1072d90df5d75ba1.js} +0 -0
  188. /fides/ui-build/static/admin/_next/static/chunks/pages/{dataset-d3c6ecf7f29bea6e.js → dataset-bf9e1cd7b8a91e81.js} +0 -0
  189. /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{[id]-0a4aa42be2da0255.js → [id]-e2a756a05906ce5b.js} +0 -0
  190. /fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-14313e441a13192c.js → new-7435d78fcf749e65.js} +0 -0
  191. /fides/ui-build/static/admin/_next/static/chunks/pages/{datastore-connection-f139d1ce26404f30.js → datastore-connection-dc50c7e9717c31ce.js} +0 -0
  192. /fides/ui-build/static/admin/_next/static/chunks/pages/{index-1343fa525a206571.js → index-91fb482245b14d86.js} +0 -0
  193. /fides/ui-build/static/admin/_next/static/chunks/pages/integrations/{[id]-b2d3d28b10a758e6.js → [id]-6166dab37d311c92.js} +0 -0
  194. /fides/ui-build/static/admin/_next/static/chunks/pages/{integrations-a733e5d7c3ce9bb8.js → integrations-82ffe15ce196c688.js} +0 -0
  195. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{[id]-63b3be660fb12c0f.js → [id]-0c7b41a17cd81544.js} +0 -0
  196. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/digests/{new-d3b577962dd33266.js → new-7e02a48cc86f7835.js} +0 -0
  197. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{digests-aed9afd988a48acf.js → digests-b0c864e6411cb8b9.js} +0 -0
  198. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{[key]-720cde29f81db47f.js → [key]-c5e272e1f0c9b097.js} +0 -0
  199. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/providers/{new-3668866076b53016.js → new-3cce4a6b31154b54.js} +0 -0
  200. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{providers-36a0ac36062abd02.js → providers-4136309c03cd69b9.js} +0 -0
  201. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{[id]-8063dceb32310c85.js → [id]-95aa9b844d3f19d0.js} +0 -0
  202. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/templates/{add-template-4931c70bee62232f.js → add-template-dbdfa8cd7fbb5e3c.js} +0 -0
  203. /fides/ui-build/static/admin/_next/static/chunks/pages/notifications/{templates-a14c876b49422597.js → templates-a4ce2e4c547ab85a.js} +0 -0
  204. /fides/ui-build/static/admin/_next/static/chunks/pages/{notifications-93af719dab3bd053.js → notifications-7d2e09a261df0655.js} +0 -0
  205. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{ant-components-9cfb469de7b4aeab.js → ant-components-8ab33b70c0087799.js} +0 -0
  206. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{AntForm-e715cc654fb6a5cd.js → AntForm-c0f0b4344f2d120a.js} +0 -0
  207. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikAntFormItem-72ae299bcb6adae6.js → FormikAntFormItem-d6567b5b37000e85.js} +0 -0
  208. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikControlled-2337f8c81a766eb0.js → FormikControlled-4f632928b6dfd0d0.js} +0 -0
  209. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/form-experiments/{FormikField-0af454f55494f6fa.js → FormikField-ec194d81e022bc47.js} +0 -0
  210. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{forms-d1b90ffa996fbd89.js → forms-dad16174a6bfd99c.js} +0 -0
  211. /fides/ui-build/static/admin/_next/static/chunks/pages/poc/{table-migration-19724b9e0581b96d.js → table-migration-55db71ad40292372.js} +0 -0
  212. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-cdd3754289a28317.js → [id]-4dd70793c7edf0a8.js} +0 -0
  213. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/configure/{storage-cc41ae605f2b55ae.js → storage-63588b8a340002fd.js} +0 -0
  214. /fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{configure-fa82cffba448ccd8.js → configure-415700a23b1d3c11.js} +0 -0
  215. /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{[id]-bc1c289647e52c48.js → [id]-61469c255ff06e69.js} +0 -0
  216. /fides/ui-build/static/admin/_next/static/chunks/pages/properties/{add-property-cbfaa23d96f5ed0b.js → add-property-4be683f2820027cd.js} +0 -0
  217. /fides/ui-build/static/admin/_next/static/chunks/pages/{properties-a15a3fd0ed88f39c.js → properties-b32d615283f0c31b.js} +0 -0
  218. /fides/ui-build/static/admin/_next/static/chunks/pages/reporting/{datamap-632b3ee563d070f2.js → datamap-1b4bdd9277062e60.js} +0 -0
  219. /fides/ui-build/static/admin/_next/static/chunks/pages/sandbox/{privacy-notices-afe921f6e2a526fb.js → privacy-notices-f28535ed0a0080ad.js} +0 -0
  220. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/consent/[configuration_id]/{[purpose_id]-19de0024418a4924.js → [purpose_id]-b1500de5cd1fe441.js} +0 -0
  221. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{consent-5b6807dced8d03c5.js → consent-f136f4204c5d6fc5.js} +0 -0
  222. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{custom-fields-7dce52bfc1b2652c.js → custom-fields-bf9b2ac61418b508.js} +0 -0
  223. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domain-records-3b87002753b23ca5.js → domain-records-318bcdad75571ca2.js} +0 -0
  224. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{domains-aacd9d0ad47082d4.js → domains-1d0b2c16a3565091.js} +0 -0
  225. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{email-templates-3cdd0b39901190ba.js → email-templates-33a5f7e22a7f8b71.js} +0 -0
  226. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{locations-61076eedbfd137b9.js → locations-e2d25e574b5b46ee.js} +0 -0
  227. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{organization-b07b11d6002f8c8c.js → organization-0055cabba6190e47.js} +0 -0
  228. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{privacy-requests-084a2b4431d35322.js → privacy-requests-f521263a3beaac9b.js} +0 -0
  229. /fides/ui-build/static/admin/_next/static/chunks/pages/settings/{regulations-b7c0d3b1b754e70f.js → regulations-8329b9cd8cd89579.js} +0 -0
  230. /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/[id]/{test-datasets-b72d36243a0a545c.js → test-datasets-81530b3fc0daa137.js} +0 -0
  231. /fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-59c89489fa32a4cb.js → [id]-4d94d20d4f7089f7.js} +0 -0
  232. /fides/ui-build/static/admin/_next/static/chunks/pages/{systems-cfaa37a0df83674b.js → systems-22e7979c69e25bda.js} +0 -0
  233. /fides/ui-build/static/admin/_next/static/chunks/pages/{taxonomy-56a5434969cbe9ba.js → taxonomy-652edb2ad165a845.js} +0 -0
  234. /fides/ui-build/static/admin/_next/static/chunks/pages/user-management/profile/{[id]-5d5a68e555d18693.js → [id]-1d7d51b194f9dcc6.js} +0 -0
  235. /fides/ui-build/static/admin/_next/static/chunks/pages/{user-management-e6a211d8a0401086.js → user-management-b23b698c019a9560.js} +0 -0
  236. /fides/ui-build/static/admin/_next/static/chunks/{webpack-c2c11809187b9f84.js → webpack-d8c0ff59e7dbc75d.js} +0 -0
  237. /fides/ui-build/static/admin/_next/static/{wCNFtmYQhEDMaMPeBB4BM → wfI6N9ecEPXinU3OlxHLy}/_ssgManifest.js +0 -0
fides/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-10-30T20:40:29+0000",
11
+ "date": "2025-10-31T14:51:49+0000",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "b17dcbb16dfac3ce994a9bd97fd39d126ec66d9f",
15
- "version": "2.73.2b0"
14
+ "full-revisionid": "3a382cb972e2ea964f078259f93ed9b6ea0b1d02",
15
+ "version": "2.73.2b1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,79 @@
1
+ """added duplicate group table
2
+
3
+ Revision ID: 80d28dea3b6b
4
+ Revises: c09e76282dd1
5
+ Create Date: 2025-10-29 16:59:09.551676
6
+
7
+ """
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "80d28dea3b6b"
15
+ down_revision = "c09e76282dd1"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade():
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table(
23
+ "duplicate_group",
24
+ sa.Column(
25
+ "created_at",
26
+ sa.DateTime(timezone=True),
27
+ server_default=sa.text("now()"),
28
+ nullable=True,
29
+ ),
30
+ sa.Column(
31
+ "updated_at",
32
+ sa.DateTime(timezone=True),
33
+ server_default=sa.text("now()"),
34
+ nullable=True,
35
+ ),
36
+ sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
37
+ sa.Column("rule_version", sa.Text(), nullable=False),
38
+ sa.Column("is_active", sa.Boolean(), server_default="t", nullable=False),
39
+ sa.Column("dedup_key", sa.Text(), nullable=False),
40
+ sa.PrimaryKeyConstraint("id"),
41
+ )
42
+ op.drop_index(
43
+ "ix_privacyrequest_duplicate_request_group_id", table_name="privacyrequest"
44
+ )
45
+ op.drop_column("privacyrequest", "duplicate_request_group_id")
46
+ op.add_column(
47
+ "privacyrequest",
48
+ sa.Column(
49
+ "duplicate_request_group_id", postgresql.UUID(as_uuid=True), nullable=True
50
+ ),
51
+ )
52
+ op.create_foreign_key(
53
+ "fk_privacyrequest_duplicate_group",
54
+ "privacyrequest",
55
+ "duplicate_group",
56
+ ["duplicate_request_group_id"],
57
+ ["id"],
58
+ )
59
+ # ### end Alembic commands ###
60
+
61
+
62
+ def downgrade():
63
+ # ### commands auto generated by Alembic - please adjust! ###
64
+ op.drop_constraint(
65
+ "fk_privacyrequest_duplicate_group", "privacyrequest", type_="foreignkey"
66
+ )
67
+ op.drop_column("privacyrequest", "duplicate_request_group_id")
68
+ op.add_column(
69
+ "privacyrequest",
70
+ sa.Column("duplicate_request_group_id", sa.String(), nullable=True),
71
+ )
72
+ op.create_index(
73
+ "ix_privacyrequest_duplicate_request_group_id",
74
+ "privacyrequest",
75
+ ["duplicate_request_group_id"],
76
+ unique=False,
77
+ )
78
+ op.drop_table("duplicate_group")
79
+ # ### end Alembic commands ###
@@ -0,0 +1,84 @@
1
+ import hashlib
2
+ import json
3
+ import uuid
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from sqlalchemy import Boolean, Column, Text
7
+ from sqlalchemy.dialects.postgresql import UUID
8
+ from sqlalchemy.ext.declarative import declared_attr
9
+ from sqlalchemy.orm import Session, relationship
10
+
11
+ from fides.api.db.base_class import Base # type: ignore[attr-defined]
12
+ from fides.config.duplicate_detection_settings import DuplicateDetectionSettings
13
+
14
+ if TYPE_CHECKING:
15
+ from fides.api.models.privacy_request.privacy_request import PrivacyRequest
16
+
17
+
18
+ def generate_deterministic_uuid(rule_version: str, dedup_key: str) -> uuid.UUID:
19
+ """Overrides the base class method to generate a deterministic group id.
20
+
21
+ The actual rule version is a hash of the duplicate detection settings config,
22
+ using simple examples here for illustration:
23
+ - under rule "email", duplicate@example.com → group A,
24
+ - under rule "email|phone", duplicate@example.com|1234567890 → group B.
25
+ - ... and so on.
26
+
27
+ No collisions, no overlap.
28
+ """
29
+ # Combine the rule version and the key into a stable hash
30
+ hash_input = f"{rule_version}:{dedup_key}".encode("utf-8")
31
+ return uuid.UUID(hashlib.md5(hash_input).hexdigest())
32
+
33
+
34
+ def generate_rule_version(config: DuplicateDetectionSettings) -> str:
35
+ """Generate a stable short hash for the dedup rule config."""
36
+ normalized = json.dumps(config.model_dump(), sort_keys=True)
37
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:8]
38
+
39
+
40
+ class DuplicateGroup(Base):
41
+ """
42
+ A table for storing duplicate request group information.
43
+ """
44
+
45
+ @declared_attr
46
+ def __tablename__(self) -> str:
47
+ return "duplicate_group"
48
+
49
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
50
+ rule_version = Column(Text, nullable=False)
51
+ # TODO: The is_active column is not used yet since we are defaulting to just email for
52
+ # duplicate detection. When we allow users to configure the duplicate detection settings,
53
+ # we will need to add a way to activate/deactivate duplicate groups.
54
+ # This should be done by getting the previous rule version and deactivating all groups with
55
+ # that rule version. New groups will be created with the new rule version.
56
+ is_active = Column(Boolean, default=True, nullable=False)
57
+ dedup_key = Column(Text, nullable=False)
58
+ privacy_requests = relationship(
59
+ "PrivacyRequest",
60
+ back_populates="duplicate_group",
61
+ lazy="dynamic",
62
+ primaryjoin="and_(DuplicateGroup.id == PrivacyRequest.duplicate_request_group_id)",
63
+ )
64
+
65
+ @classmethod
66
+ def create(
67
+ cls, db: Session, *, data: dict[str, Any], check_name: bool = False
68
+ ) -> "DuplicateGroup":
69
+ """Create a new duplicate group.
70
+ Override the base class method to generate a deterministic group id.
71
+ If the group already exists, update the is_active column to True and return the group.
72
+ """
73
+ group_id = generate_deterministic_uuid(data["rule_version"], data["dedup_key"])
74
+ # If the group already exists, update the is_active column to True and return the group
75
+ duplicate_group = (
76
+ db.query(DuplicateGroup).filter(DuplicateGroup.id == group_id).one_or_none()
77
+ )
78
+ if duplicate_group:
79
+ duplicate_group.update(db, data={"is_active": True})
80
+ return duplicate_group
81
+
82
+ # If the group does not exist, create a new one
83
+ data["id"] = group_id
84
+ return super().create(db, data=data, check_name=check_name)
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
9
9
  from celery.result import AsyncResult
10
10
  from loguru import logger
11
11
  from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
12
- from sqlalchemy.dialects.postgresql import JSONB
12
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
13
13
  from sqlalchemy.ext.declarative import declared_attr
14
14
  from sqlalchemy.ext.mutable import MutableDict, MutableList
15
15
  from sqlalchemy.orm import Query, RelationshipProperty, Session, backref, relationship
@@ -69,6 +69,7 @@ from fides.api.models.pre_approval_webhook import (
69
69
  PreApprovalWebhook,
70
70
  PreApprovalWebhookReply,
71
71
  )
72
+ from fides.api.models.privacy_request.duplicate_group import DuplicateGroup
72
73
  from fides.api.models.privacy_request.execution_log import (
73
74
  COMPLETED_EXECUTION_LOG_STATUSES,
74
75
  EXITED_EXECUTION_LOG_STATUSES,
@@ -225,7 +226,16 @@ class PrivacyRequest(
225
226
  location = Column(String, nullable=True)
226
227
 
227
228
  # Duplicate detection - group ID to link duplicates together
228
- duplicate_request_group_id = Column(String, index=True, nullable=True)
229
+ duplicate_request_group_id = Column(
230
+ UUID(as_uuid=True),
231
+ ForeignKey("duplicate_group.id"),
232
+ nullable=True,
233
+ )
234
+ duplicate_group = relationship(
235
+ DuplicateGroup,
236
+ back_populates="privacy_requests",
237
+ uselist=False,
238
+ )
229
239
 
230
240
  # A PrivacyRequest can be soft deleted, so we store when it was deleted
231
241
  deleted_at = Column(DateTime(timezone=True), nullable=True)
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum as EnumType
3
3
  from typing import Any, Dict, List, Optional, Type, Union
4
+ from uuid import UUID
4
5
 
5
6
  from fideslang.validation import FidesKey
6
7
  from pydantic import ConfigDict, Field, field_serializer, field_validator
@@ -343,7 +344,7 @@ class PrivacyRequestResponse(FidesSchema):
343
344
  deleted_by: Optional[str] = None
344
345
  finalized_at: Optional[datetime] = None
345
346
  finalized_by: Optional[str] = None
346
- duplicate_request_group_id: Optional[str] = None
347
+ duplicate_request_group_id: Optional[UUID] = None
347
348
 
348
349
  model_config = ConfigDict(from_attributes=True, use_enum_values=True)
349
350
 
@@ -551,13 +551,20 @@ class AsyncPollingStrategy(AsyncDSRStrategy):
551
551
  self.result_request.result_path,
552
552
  )
553
553
 
554
- # Ensure we have the expected polling result type
555
- if not isinstance(polling_result, PollingResult):
556
- raise PrivacyRequestError("Polling result must be PollingResult instance")
557
-
558
- # Store results on the sub-request
559
- self._store_sub_request_result(polling_result, sub_request, polling_task)
554
+ # Checks if we have a polling result, response could be empty in case there was no data to access
555
+ if polling_result:
556
+ # Ensure we have the expected polling result type
557
+ if not isinstance(polling_result, PollingResult):
558
+ raise PrivacyRequestError(
559
+ "Polling result must be PollingResult instance"
560
+ )
560
561
 
562
+ # Store results on the sub-request
563
+ self._store_sub_request_result(polling_result, sub_request, polling_task)
564
+ else:
565
+ logger.info(
566
+ f"No result response for sub-request {sub_request.id} for task {polling_task.id}"
567
+ )
561
568
  # Mark as complete using existing method
562
569
  sub_request.update_status(self.session, ExecutionLogStatus.complete.value)
563
570
 
@@ -0,0 +1,357 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Optional
3
+
4
+ from loguru import logger
5
+ from sqlalchemy.orm import Session
6
+
7
+ from fides.api.models.privacy_request.duplicate_group import (
8
+ DuplicateGroup,
9
+ generate_rule_version,
10
+ )
11
+ from fides.api.models.privacy_request.privacy_request import PrivacyRequest
12
+ from fides.api.schemas.policy import ActionType
13
+ from fides.api.schemas.privacy_request import PrivacyRequestStatus
14
+ from fides.api.task.conditional_dependencies.schemas import (
15
+ Condition,
16
+ ConditionGroup,
17
+ ConditionLeaf,
18
+ GroupOperator,
19
+ Operator,
20
+ )
21
+ from fides.api.task.conditional_dependencies.sql_translator import (
22
+ SQLConditionTranslator,
23
+ )
24
+ from fides.config.duplicate_detection_settings import DuplicateDetectionSettings
25
+
26
+ ACTIONED_REQUEST_STATUSES = [
27
+ PrivacyRequestStatus.approved,
28
+ PrivacyRequestStatus.in_processing,
29
+ PrivacyRequestStatus.complete,
30
+ PrivacyRequestStatus.requires_manual_finalization,
31
+ PrivacyRequestStatus.requires_input,
32
+ PrivacyRequestStatus.paused,
33
+ PrivacyRequestStatus.awaiting_email_send,
34
+ PrivacyRequestStatus.error,
35
+ ]
36
+
37
+
38
+ class DuplicateDetectionService:
39
+ def __init__(self, db: Session):
40
+ self.db = db
41
+
42
+ def _create_identity_conditions(
43
+ self, current_request: PrivacyRequest, config: DuplicateDetectionSettings
44
+ ) -> list[Condition]:
45
+ """Creates conditions for matching identity fields.
46
+
47
+ For identity field matching using the EAV pattern in ProvidedIdentity, we need to match both field_name
48
+ and hashed_value. This function creates the required nested conditions for each identity field.
49
+ Also adds a condition for the policy_id to ensure that we are only matching requests for the same policy.
50
+ """
51
+ conditions: list[Condition] = []
52
+ current_identities: dict[str, str] = {
53
+ pi.field_name: pi.hashed_value
54
+ for pi in current_request.provided_identities # type: ignore [attr-defined]
55
+ if pi.field_name in config.match_identity_fields
56
+ }
57
+ if len(current_identities) != len(config.match_identity_fields):
58
+ missing_fields = [
59
+ field
60
+ for field in config.match_identity_fields
61
+ if field not in current_identities.keys()
62
+ ]
63
+ logger.debug(
64
+ f"Some identity fields were not found in the current request: {missing_fields}"
65
+ )
66
+ return []
67
+
68
+ for field_name, hashed_value in current_identities.items():
69
+ identity_condition = ConditionGroup(
70
+ logical_operator=GroupOperator.and_,
71
+ conditions=[
72
+ ConditionLeaf(
73
+ field_address="privacyrequest.provided_identities.field_name",
74
+ operator=Operator.eq,
75
+ value=field_name,
76
+ ),
77
+ ConditionLeaf(
78
+ field_address="privacyrequest.provided_identities.hashed_value",
79
+ operator=Operator.eq,
80
+ value=hashed_value,
81
+ ),
82
+ ],
83
+ )
84
+ conditions.append(identity_condition)
85
+ policy_condition = ConditionLeaf(
86
+ field_address="privacyrequest.policy_id",
87
+ operator=Operator.eq,
88
+ value=current_request.policy_id,
89
+ )
90
+ conditions.append(policy_condition)
91
+ return conditions
92
+
93
+ def _create_time_window_condition(self, time_window_days: int) -> Condition:
94
+ """Creates a condition for matching requests within a time window."""
95
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=time_window_days)
96
+ condition = ConditionLeaf(
97
+ field_address="privacyrequest.created_at",
98
+ operator=Operator.gte,
99
+ value=cutoff_date.isoformat(),
100
+ )
101
+ return condition
102
+
103
+ def create_duplicate_detection_conditions(
104
+ self,
105
+ current_request: PrivacyRequest,
106
+ config: DuplicateDetectionSettings,
107
+ ) -> Optional[ConditionGroup]:
108
+ """
109
+ Create conditions for duplicate detection based on configuration.
110
+
111
+ Args:
112
+ current_request: The current privacy request to find duplicates for
113
+ config: Duplicate detection configuration settings
114
+
115
+ Returns:
116
+ A ConditionGroup with AND operator, or None if no conditions can be created
117
+ """
118
+ if len(config.match_identity_fields) == 0:
119
+ return None
120
+
121
+ identity_conditions = self._create_identity_conditions(current_request, config)
122
+ if not identity_conditions:
123
+ return None # Only proceed if we have identity conditions
124
+
125
+ time_window_condition = self._create_time_window_condition(
126
+ config.time_window_days
127
+ )
128
+
129
+ # Combine all conditions with AND operator
130
+ all_conditions: list[Condition] = [*identity_conditions, time_window_condition]
131
+ return ConditionGroup(
132
+ logical_operator=GroupOperator.and_, conditions=all_conditions
133
+ )
134
+
135
+ def find_duplicate_privacy_requests(
136
+ self,
137
+ current_request: PrivacyRequest,
138
+ config: DuplicateDetectionSettings,
139
+ ) -> list[PrivacyRequest]:
140
+ """
141
+ Find potential duplicate privacy requests based on duplicate detection configuration.
142
+
143
+ Uses the SQLConditionTranslator to build queries from conditions, which handles
144
+ the ProvidedIdentity relationship using SQLAlchemy's .any() method.
145
+
146
+ Args:
147
+ current_request: The privacy request to check for duplicates
148
+ config: Duplicate detection configuration settings
149
+
150
+ Returns:
151
+ List of PrivacyRequest objects that match the duplicate criteria,
152
+ does not include the current request
153
+ """
154
+ condition = self.create_duplicate_detection_conditions(current_request, config)
155
+
156
+ if condition is None:
157
+ return []
158
+
159
+ translator = SQLConditionTranslator(self.db)
160
+ query = translator.generate_query_from_condition(condition)
161
+
162
+ query = query.filter(PrivacyRequest.id != current_request.id)
163
+ return query.all()
164
+
165
+ def generate_dedup_key(
166
+ self, request: PrivacyRequest, config: DuplicateDetectionSettings
167
+ ) -> str:
168
+ """
169
+ Generate a dedup key for a request based on the duplicate detection settings.
170
+ """
171
+ current_identities: dict[str, str] = {
172
+ pi.field_name: pi.hashed_value
173
+ for pi in request.provided_identities # type: ignore [attr-defined]
174
+ if pi.field_name in config.match_identity_fields
175
+ }
176
+ if len(current_identities) != len(config.match_identity_fields):
177
+ raise ValueError(
178
+ "This request does not contain the required identity fields for duplicate detection."
179
+ )
180
+ return "|".join(
181
+ [
182
+ current_identities[field]
183
+ for field in sorted(config.match_identity_fields)
184
+ ]
185
+ )
186
+
187
+ def verified_identity_cases(
188
+ self, request: PrivacyRequest, duplicates: list[PrivacyRequest]
189
+ ) -> bool:
190
+ """
191
+ Apply verified identity rules to determine if a request is a duplicate request.
192
+ - If this request does not have a verified identity, it may be a duplicate if another request in the group is verified.
193
+ - If this is the first request to be verified, it is not a duplicate request
194
+ - If other requests identities were verified before this request, it is a duplicate request
195
+ Args:
196
+ request: The privacy request to check
197
+ duplicates: The list of duplicate requests
198
+
199
+ Returns:
200
+ True if the request is a duplicate request, False otherwise
201
+ """
202
+ verified_in_group = [
203
+ duplicate for duplicate in duplicates if duplicate.identity_verified_at
204
+ ]
205
+
206
+ # The request identity is not verified.
207
+ if not request.identity_verified_at:
208
+ if len(verified_in_group) > 0:
209
+ logger.debug(
210
+ f"Request {request.id} is a duplicate: it is not verified and duplicating verified request(s) {verified_in_group}."
211
+ )
212
+ return True
213
+
214
+ min_created_at = min(
215
+ (d.created_at for d in duplicates if d.created_at), default=None
216
+ ) or datetime.now(timezone.utc)
217
+ request_created_at = (
218
+ request.created_at
219
+ if request.created_at is not None
220
+ else datetime.now(timezone.utc)
221
+ )
222
+ if request_created_at < min_created_at:
223
+ logger.debug(
224
+ f"Request {request.id} is not a duplicate: it is the first request to be created in the group."
225
+ )
226
+ return False
227
+ logger.debug(
228
+ f"Request {request.id} is a duplicate: it is not verified and is not the first request to be created in the group."
229
+ )
230
+ return True
231
+
232
+ # The request identity is verified.
233
+ if not verified_in_group:
234
+ logger.debug(
235
+ f"Request {request.id} is not a duplicate: it is verified and no other requests in the group are verified."
236
+ )
237
+ return False
238
+
239
+ # If this request is the first with a verified identity, it is not a duplicate.
240
+ min_verified_at = min(
241
+ (d.identity_verified_at for d in duplicates if d.identity_verified_at),
242
+ default=None,
243
+ ) or datetime.now(timezone.utc)
244
+ request_verified_at = (
245
+ request.identity_verified_at
246
+ if request.identity_verified_at is not None
247
+ else datetime.now(timezone.utc)
248
+ )
249
+ if request_verified_at < min_verified_at:
250
+ logger.debug(
251
+ f"Request {request.id} is not a duplicate: it is the first request to be verified in the group."
252
+ )
253
+ return False
254
+ logger.debug(
255
+ f"Request {request.id} is a duplicate: it is verified but not the first request to be verified in the group."
256
+ )
257
+ return True
258
+
259
+ # pylint: disable=too-many-return-statements
260
+ def is_duplicate_request(
261
+ self, request: PrivacyRequest, config: DuplicateDetectionSettings
262
+ ) -> bool:
263
+ """
264
+ Determine if a request is a duplicate request and assigns a duplicate request group id.
265
+
266
+ The hierarchy is:
267
+ 1. Actioned requests: if this request duplicates an actioned request, it is a duplicate.
268
+ 2. Verified identity requests:
269
+ a. if this request has a verified identity:
270
+ - If none of the duplicates have a verified identity, it is not a duplicate.
271
+ - If duplicates have verified identities, but this request is the first with a verified identity, it is not a duplicate.
272
+ b. if this request does not have a verified identity:
273
+ - If no duplicates have a verified identity, and this was the first created request, it is not a duplicate.
274
+ 3. First created request: if this is the first created request in the group, it is not a duplicate.
275
+ 4. If no canonical requests are found (meaning all requests are marked as duplicates), this request is not a duplicate.
276
+ - Could occur if configuration changes and previous requests were already marked as duplicates.
277
+
278
+ Args:
279
+ request: The privacy request to check
280
+ config: Duplicate detection configuration settings
281
+ Returns:
282
+ True if the request is a duplicate request, False otherwise
283
+ """
284
+ duplicates = self.find_duplicate_privacy_requests(request, config)
285
+ rule_version = generate_rule_version(config)
286
+ try:
287
+ dedup_key = self.generate_dedup_key(request, config)
288
+ except ValueError as e:
289
+ logger.debug(f"Request {request.id} is not a duplicate: {e}")
290
+ return False
291
+
292
+ _, duplicate_group = DuplicateGroup.get_or_create(
293
+ db=self.db, data={"rule_version": rule_version, "dedup_key": dedup_key}
294
+ )
295
+ if duplicate_group is None:
296
+ logger.error(
297
+ f"Failed to create duplicate group for request {request.id} with dedup key {dedup_key}"
298
+ )
299
+ return False
300
+ logger.info(
301
+ f"Duplicate group {duplicate_group.id} created for request {request.id} with dedup key {dedup_key}"
302
+ )
303
+ request.update(
304
+ db=self.db, data={"duplicate_request_group_id": duplicate_group.id}
305
+ )
306
+
307
+ # if this is the only request in the group, it is not a duplicate
308
+ if len(duplicates) == 0:
309
+ logger.debug(
310
+ f"Request {request.id} is not a duplicate: no matching requests were found."
311
+ )
312
+ return False
313
+
314
+ if request.status == PrivacyRequestStatus.duplicate:
315
+ logger.warning(
316
+ f"Request {request.id} is a duplicate request that was requeued. This should not happen."
317
+ )
318
+ request.add_error_execution_log(
319
+ db=self.db,
320
+ connection_key=None,
321
+ dataset_name="Duplicate Request Detection",
322
+ collection_name=None,
323
+ message=f"Request {request.id} is a duplicate request that was requeued. This should not happen.",
324
+ action_type=(
325
+ request.policy.get_action_type() # type: ignore [arg-type]
326
+ if request.policy
327
+ else ActionType.access
328
+ ),
329
+ )
330
+ return True
331
+
332
+ # only compare to non-duplicate requests for the following cases
333
+ canonical_requests = [
334
+ duplicate
335
+ for duplicate in duplicates
336
+ if duplicate.status != PrivacyRequestStatus.duplicate
337
+ ]
338
+ # If no non-duplicate requests are found, this request is not a duplicate.
339
+ if len(canonical_requests) == 0:
340
+ logger.debug(
341
+ f"Request {request.id} is not a duplicate: all matching requests have been marked as duplicate requests."
342
+ )
343
+ return False
344
+
345
+ # If any requests in group are actioned, this request is a duplicate.
346
+ actioned_in_group = [
347
+ duplicate
348
+ for duplicate in canonical_requests
349
+ if duplicate.status in ACTIONED_REQUEST_STATUSES
350
+ ]
351
+ if len(actioned_in_group) > 0:
352
+ logger.debug(
353
+ f"Request {request.id} is a duplicate: it is duplicating actioned request(s) {actioned_in_group}."
354
+ )
355
+ return True
356
+ # Check against verified identity rules.
357
+ return self.verified_identity_cases(request, canonical_requests)
@@ -1,6 +1,6 @@
1
1
  from enum import Enum
2
2
  from inspect import Signature, signature
3
- from typing import TYPE_CHECKING, Callable, Dict, List, Union
3
+ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Union
4
4
 
5
5
  from loguru import logger
6
6
 
@@ -319,7 +319,12 @@ def validate_polling_result_override_function(f: Callable) -> None:
319
319
 
320
320
  # Check return type annotation - handle both direct class and string literals
321
321
  return_annotation = sig.return_annotation
322
- if return_annotation not in (PollingResult, "PollingResult"):
322
+ if return_annotation not in (
323
+ PollingResult,
324
+ "PollingResult",
325
+ Optional[PollingResult],
326
+ "Optional[PollingResult]",
327
+ ):
323
328
  raise InvalidSaaSRequestOverrideException(
324
329
  "Polling result override function must return a PollingResult"
325
330
  )
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d78390d6134d8328.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d78390d6134d8328.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-c2c11809187b9f84.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-090643377c8254e6.js" defer=""></script><script src="/_next/static/chunks/pages/_app-ee588a308812715d.js" defer=""></script><script src="/_next/static/chunks/pages/404-eb019192ce498f32.js" defer=""></script><script src="/_next/static/wCNFtmYQhEDMaMPeBB4BM/_buildManifest.js" defer=""></script><script src="/_next/static/wCNFtmYQhEDMaMPeBB4BM/_ssgManifest.js" defer=""></script><style>.data-ant-cssinjs-cache-path{content:"";}</style></head><body><div id="__next"><div style="height:100%;display:flex"></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/404","query":{},"buildId":"wCNFtmYQhEDMaMPeBB4BM","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>
1
+ <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><meta name="next-head-count" content="2"/><link data-next-font="" rel="preconnect" href="/" crossorigin="anonymous"/><link rel="preload" href="/_next/static/css/d78390d6134d8328.css" as="style"/><link rel="stylesheet" href="/_next/static/css/d78390d6134d8328.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-d8c0ff59e7dbc75d.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-090643377c8254e6.js" defer=""></script><script src="/_next/static/chunks/pages/_app-2986534ccbf5e4a5.js" defer=""></script><script src="/_next/static/chunks/pages/404-e30ec05084303d6b.js" defer=""></script><script src="/_next/static/wfI6N9ecEPXinU3OlxHLy/_buildManifest.js" defer=""></script><script src="/_next/static/wfI6N9ecEPXinU3OlxHLy/_ssgManifest.js" defer=""></script><style>.data-ant-cssinjs-cache-path{content:"";}</style></head><body><div id="__next"><div style="height:100%;display:flex"></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/404","query":{},"buildId":"wfI6N9ecEPXinU3OlxHLy","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>