ethyca-fides 2.56.3b1__py2.py3-none-any.whl → 2.57.0__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.
Files changed (129) hide show
  1. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/RECORD +120 -116
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/69ad6d844e21_add_comments_and_comment_references.py +84 -0
  5. fides/api/alembic/migrations/versions/6ea2171c544f_change_attachment_storage_key_to_.py +77 -0
  6. fides/api/models/attachment.py +109 -49
  7. fides/api/models/comment.py +109 -0
  8. fides/api/service/connectors/query_configs/bigquery_query_config.py +46 -15
  9. fides/api/service/connectors/query_configs/query_config.py +118 -21
  10. fides/api/service/connectors/query_configs/saas_query_config.py +21 -15
  11. fides/api/service/storage/storage_uploader_service.py +4 -10
  12. fides/api/tasks/storage.py +106 -15
  13. fides/api/util/aws_util.py +19 -0
  14. fides/api/util/collection_util.py +142 -0
  15. fides/api/util/logger_context_utils.py +36 -1
  16. fides/api/util/saas_util.py +32 -56
  17. fides/data/language/languages.yml +2 -0
  18. fides/service/privacy_request/privacy_request_service.py +14 -5
  19. fides/ui-build/static/admin/404.html +1 -1
  20. fides/ui-build/static/admin/_next/static/D8O9QehPDr533GOFed3df/_buildManifest.js +1 -0
  21. fides/ui-build/static/admin/_next/static/chunks/5574-a4047e826a8cd4c3.js +1 -0
  22. fides/ui-build/static/admin/_next/static/chunks/5834-bd9ed01c4ab2ef5c.js +1 -0
  23. fides/ui-build/static/admin/_next/static/chunks/5973-67c7562997f7d5cb.js +1 -0
  24. fides/ui-build/static/admin/_next/static/chunks/6277-515c922445b8edc5.js +1 -0
  25. fides/ui-build/static/admin/_next/static/chunks/9767-4c591bd478c72650.js +1 -0
  26. fides/ui-build/static/admin/_next/static/chunks/pages/{_app-3c1a7742661d3a9e.js → _app-9afd7a4bc9b3eee5.js} +1 -1
  27. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{[id]-fdbafb5a47a6a28c.js → [id]-443e6dd191ce5588.js} +1 -1
  28. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-experience/{new-fc4635c6eea7165f.js → new-36162d5bea29dcc2.js} +1 -1
  29. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{[id]-bdc686761a0d2d60.js → [id]-2d651499c07d12d9.js} +1 -1
  30. fides/ui-build/static/admin/_next/static/chunks/pages/consent/privacy-notices/{new-8618ab5fa45cd074.js → new-36e57d2f2446be82.js} +1 -1
  31. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-740824dfa6823e26.js +1 -0
  32. fides/ui-build/static/admin/_next/static/chunks/pages/dataset-5541ecd2f62711a5.js +1 -0
  33. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{[id]-a6e793e9a8ed00a9.js → [id]-0616437e1a82d3ed.js} +1 -1
  34. fides/ui-build/static/admin/_next/static/chunks/pages/datastore-connection/{new-7d314bb7238af067.js → new-f8218440494e8532.js} +1 -1
  35. fides/ui-build/static/admin/_next/static/chunks/pages/{index-c9fa68dc0fa42c81.js → index-94e6d589c4edf360.js} +1 -1
  36. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-a6448ce6ba7f0cf5.js +1 -0
  37. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-1a6965d78bfb26b0.js +1 -0
  38. fides/ui-build/static/admin/_next/static/chunks/pages/privacy-requests/{[id]-c685d19a131d6960.js → [id]-bbe5854b7d19b7e9.js} +1 -1
  39. fides/ui-build/static/admin/_next/static/chunks/pages/systems/configure/{[id]-c6396feeefca887b.js → [id]-82d9af34546adaa2.js} +1 -1
  40. fides/ui-build/static/admin/_next/static/css/{bd5a72c010fa9c14.css → 957d0e4fea846ff0.css} +1 -1
  41. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  42. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  43. fides/ui-build/static/admin/add-systems.html +1 -1
  44. fides/ui-build/static/admin/ant-poc.html +1 -1
  45. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  46. fides/ui-build/static/admin/consent/configure.html +1 -1
  47. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  48. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  49. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  50. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  51. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  52. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  53. fides/ui-build/static/admin/consent/properties.html +1 -1
  54. fides/ui-build/static/admin/consent/reporting.html +1 -1
  55. fides/ui-build/static/admin/consent.html +1 -1
  56. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  57. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  58. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  59. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  60. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  61. fides/ui-build/static/admin/data-catalog.html +1 -1
  62. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  63. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  64. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  65. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  66. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  67. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  68. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  69. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  70. fides/ui-build/static/admin/datamap.html +1 -1
  71. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  72. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  73. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  74. fides/ui-build/static/admin/dataset/new.html +1 -1
  75. fides/ui-build/static/admin/dataset.html +1 -1
  76. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  77. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  78. fides/ui-build/static/admin/datastore-connection.html +1 -1
  79. fides/ui-build/static/admin/index.html +1 -1
  80. fides/ui-build/static/admin/integrations/[id].html +1 -1
  81. fides/ui-build/static/admin/integrations.html +1 -1
  82. fides/ui-build/static/admin/lib/fides-ext-gpp.js +1 -1
  83. fides/ui-build/static/admin/lib/fides-headless.js +1 -1
  84. fides/ui-build/static/admin/lib/fides-tcf.js +3 -3
  85. fides/ui-build/static/admin/lib/fides.js +2 -2
  86. fides/ui-build/static/admin/login/[provider].html +1 -1
  87. fides/ui-build/static/admin/login.html +1 -1
  88. fides/ui-build/static/admin/messaging/[id].html +1 -1
  89. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  90. fides/ui-build/static/admin/messaging.html +1 -1
  91. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  92. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  93. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  94. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  95. fides/ui-build/static/admin/privacy-requests.html +1 -1
  96. fides/ui-build/static/admin/properties/[id].html +1 -1
  97. fides/ui-build/static/admin/properties/add-property.html +1 -1
  98. fides/ui-build/static/admin/properties.html +1 -1
  99. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  100. fides/ui-build/static/admin/settings/about.html +1 -1
  101. fides/ui-build/static/admin/settings/consent.html +1 -1
  102. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  103. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  104. fides/ui-build/static/admin/settings/domains.html +1 -1
  105. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  106. fides/ui-build/static/admin/settings/locations.html +1 -1
  107. fides/ui-build/static/admin/settings/organization.html +1 -1
  108. fides/ui-build/static/admin/settings/regulations.html +1 -1
  109. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  110. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  111. fides/ui-build/static/admin/systems.html +1 -1
  112. fides/ui-build/static/admin/taxonomy.html +1 -1
  113. fides/ui-build/static/admin/user-management/new.html +1 -1
  114. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  115. fides/ui-build/static/admin/user-management.html +1 -1
  116. fides/ui-build/static/admin/_next/static/LOp6RUpN795nyhXOv95wz/_buildManifest.js +0 -1
  117. fides/ui-build/static/admin/_next/static/chunks/2201-abd6092e6df98c26.js +0 -1
  118. fides/ui-build/static/admin/_next/static/chunks/6277-b55810d66362f56e.js +0 -1
  119. fides/ui-build/static/admin/_next/static/chunks/6362-8f79e403fdc2404a.js +0 -1
  120. fides/ui-build/static/admin/_next/static/chunks/7495-a61f0c9fcb458664.js +0 -1
  121. fides/ui-build/static/admin/_next/static/chunks/pages/dataset/new-fc00dbeb18a7b731.js +0 -1
  122. fides/ui-build/static/admin/_next/static/chunks/pages/dataset-295756e1fd640b59.js +0 -1
  123. fides/ui-build/static/admin/_next/static/chunks/pages/integrations/[id]-20489bceb7b068b8.js +0 -1
  124. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-21c9902e7b098b39.js +0 -1
  125. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/LICENSE +0 -0
  126. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/WHEEL +0 -0
  127. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/entry_points.txt +0 -0
  128. {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.57.0.dist-info}/top_level.txt +0 -0
  129. /fides/ui-build/static/admin/_next/static/{LOp6RUpN795nyhXOv95wz → D8O9QehPDr533GOFed3df}/_ssgManifest.js +0 -0
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=too-many-instance-attributes
2
2
  from __future__ import annotations
3
3
 
4
+ from copy import deepcopy
4
5
  from datetime import datetime
5
6
  from itertools import product
6
7
  from typing import Any, Dict, List, Literal, Optional, Tuple, TypeVar
@@ -12,7 +13,6 @@ from loguru import logger
12
13
  from sqlalchemy.orm import Session
13
14
 
14
15
  from fides.api.common_exceptions import FidesopsException
15
- from fides.api.graph.config import ScalarField
16
16
  from fides.api.graph.execution import ExecutionNode
17
17
  from fides.api.models.policy import Policy
18
18
  from fides.api.models.privacy_request import (
@@ -33,7 +33,12 @@ from fides.api.task.refine_target_path import (
33
33
  join_detailed_path,
34
34
  )
35
35
  from fides.api.util import saas_util
36
- from fides.api.util.collection_util import Row, merge_dicts
36
+ from fides.api.util.collection_util import (
37
+ Row,
38
+ flatten_dict,
39
+ merge_dicts,
40
+ unflatten_dict,
41
+ )
37
42
  from fides.api.util.saas_util import (
38
43
  ALL_OBJECT_FIELDS,
39
44
  CUSTOM_PRIVACY_REQUEST_FIELDS,
@@ -45,7 +50,6 @@ from fides.api.util.saas_util import (
45
50
  REPLY_TO_TOKEN,
46
51
  UUID,
47
52
  get_identities,
48
- unflatten_dict,
49
53
  )
50
54
  from fides.common.api.v1.urn_registry import REQUEST_TASK_CALLBACK, V1_URL_PREFIX
51
55
  from fides.config.config_proxy import ConfigProxy
@@ -510,22 +514,24 @@ class SaaSQueryConfig(QueryConfig[SaaSRequestParams]):
510
514
 
511
515
  def all_value_map(self, row: Row) -> Dict[str, Any]:
512
516
  """
513
- Takes a row and preserves only the fields that are defined in the Dataset.
517
+ Takes a row and preserves only the fields that are defined in the collection.
514
518
  Used for scenarios when an update endpoint has required fields other than
515
519
  just the fields being updated.
516
520
  """
521
+ flattened_row = flatten_dict(deepcopy(row))
517
522
 
518
- all_value_map: Dict[str, Any] = {}
519
- for field_path, field in self.field_map().items():
520
- # only map scalar fields
521
- if (
522
- isinstance(field, ScalarField)
523
- and pydash.get(row, field_path.string_path) is not None
524
- ):
525
- all_value_map[field_path.string_path] = pydash.get(
526
- row, field_path.string_path
527
- )
528
- return all_value_map
523
+ # Get root field names defined in the collection
524
+ collection_fields = {
525
+ field_path.string_path.split(".")[0]
526
+ for field_path, _ in self.field_map().items()
527
+ }
528
+
529
+ # Only keep the field values defined in the collection
530
+ return {
531
+ path: value
532
+ for path, value in flattened_row.items()
533
+ if path.split(".")[0] in collection_fields
534
+ }
529
535
 
530
536
  def query_to_str(self, t: T, input_data: Dict[str, List[Any]]) -> str:
531
537
  """Convert query to string"""
@@ -41,9 +41,7 @@ def upload(
41
41
  logger.warning("Storage type not found: {}", storage_key)
42
42
  raise StorageUploadError(f"Storage type not found: {storage_key}")
43
43
  uploader: Any = _get_uploader_from_config_type(config.type) # type: ignore
44
- return uploader(
45
- db, config, data, privacy_request, data_category_field_mapping, data_use_map
46
- )
44
+ return uploader(db, config, data, privacy_request)
47
45
 
48
46
 
49
47
  def get_extension(resp_format: ResponseFormat) -> str:
@@ -88,14 +86,13 @@ def _s3_uploader(
88
86
  config: StorageConfig,
89
87
  data: Dict,
90
88
  privacy_request: PrivacyRequest,
91
- data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
92
- data_use_map: Optional[Dict[str, Set[str]]] = None,
93
89
  ) -> str:
94
90
  """Constructs necessary info needed for s3 before calling upload"""
95
91
  file_key: str = _construct_file_key(privacy_request.id, config)
96
92
 
97
93
  bucket_name = config.details[StorageDetails.BUCKET.value]
98
94
  auth_method = config.details[StorageDetails.AUTH_METHOD.value]
95
+ document = None
99
96
 
100
97
  return upload_to_s3(
101
98
  config.secrets, # type: ignore
@@ -104,9 +101,8 @@ def _s3_uploader(
104
101
  file_key,
105
102
  config.format.value, # type: ignore
106
103
  privacy_request,
104
+ document,
107
105
  auth_method,
108
- data_category_field_mapping,
109
- data_use_map,
110
106
  )
111
107
 
112
108
 
@@ -115,9 +111,7 @@ def _local_uploader(
115
111
  config: StorageConfig,
116
112
  data: Dict,
117
113
  privacy_request: PrivacyRequest,
118
- data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
119
- data_use_map: Optional[Dict[str, Set[str]]] = None,
120
114
  ) -> str:
121
115
  """Uploads data to local storage, used for quick-start/demo purposes"""
122
116
  file_key: str = _construct_file_key(privacy_request.id, config)
123
- return upload_to_local(data, file_key, privacy_request, config.format.value, data_category_field_mapping, data_use_map) # type: ignore
117
+ return upload_to_local(data, file_key, privacy_request, config.format.value) # type: ignore
@@ -5,20 +5,20 @@ import os
5
5
  import secrets
6
6
  import zipfile
7
7
  from io import BytesIO
8
- from typing import Any, Dict, Optional, Set, Union
8
+ from typing import Any, Dict, Optional, Union
9
9
 
10
10
  import pandas as pd
11
11
  from botocore.exceptions import ClientError, ParamValidationError
12
+ from fideslang.validation import AnyHttpUrlString
12
13
  from loguru import logger
13
14
 
14
15
  from fides.api.cryptography.cryptographic_util import bytes_to_b64_str
15
- from fides.api.graph.graph import DataCategoryFieldMapping
16
16
  from fides.api.models.privacy_request import PrivacyRequest
17
17
  from fides.api.schemas.storage.storage import ResponseFormat, StorageSecrets
18
18
  from fides.api.service.privacy_request.dsr_package.dsr_report_builder import (
19
19
  DsrReportBuilder,
20
20
  )
21
- from fides.api.util.aws_util import get_aws_session
21
+ from fides.api.util.aws_util import get_s3_client
22
22
  from fides.api.util.cache import get_cache, get_encryption_cache_key
23
23
  from fides.api.util.encryption.aes_gcm_encryption_scheme import (
24
24
  encrypt_to_bytes_verify_secrets_length,
@@ -101,7 +101,9 @@ def write_to_in_memory_buffer(
101
101
  raise NotImplementedError(f"No handling for response format {resp_format}.")
102
102
 
103
103
 
104
- def create_presigned_url_for_s3(s3_client: Any, bucket_name: str, file_key: str) -> str:
104
+ def create_presigned_url_for_s3(
105
+ s3_client: Any, bucket_name: str, file_key: str
106
+ ) -> AnyHttpUrlString:
105
107
  """ "Generate a presigned URL to share an S3 object
106
108
 
107
109
  :param s3_client: s3 base client
@@ -119,23 +121,108 @@ def create_presigned_url_for_s3(s3_client: Any, bucket_name: str, file_key: str)
119
121
  return response
120
122
 
121
123
 
124
+ def generic_upload_to_s3( # pylint: disable=R0913
125
+ storage_secrets: Dict[StorageSecrets, Any],
126
+ bucket_name: str,
127
+ file_key: str,
128
+ auth_method: str,
129
+ document: bytes,
130
+ ) -> Optional[AnyHttpUrlString]:
131
+ """Uploads arbitrary data to s3 returned from an access request"""
132
+ logger.info("Starting S3 Upload of {}", file_key)
133
+
134
+ try:
135
+ s3_client = get_s3_client(auth_method, storage_secrets)
136
+ try:
137
+ s3_client.put_object(Bucket=bucket_name, Key=file_key, Body=document)
138
+ except Exception as e:
139
+ logger.error("Encountered error while uploading s3 object: {}", e)
140
+ raise e
141
+
142
+ presigned_url: AnyHttpUrlString = create_presigned_url_for_s3(
143
+ s3_client, bucket_name, file_key
144
+ )
145
+
146
+ return presigned_url
147
+ except ClientError as e:
148
+ logger.error(
149
+ "Encountered error while uploading and generating link for s3 object: {}", e
150
+ )
151
+ raise e
152
+ except ParamValidationError as e:
153
+ raise ValueError(f"The parameters you provided are incorrect: {e}")
154
+
155
+
156
+ def generic_retrieve_from_s3(
157
+ storage_secrets: Dict[StorageSecrets, Any],
158
+ bucket_name: str,
159
+ file_key: str,
160
+ auth_method: str,
161
+ ) -> Optional[bytes]:
162
+ """Retrieves arbitrary data from s3"""
163
+ logger.info("Starting S3 Retrieve of {}", file_key)
164
+
165
+ try:
166
+ s3_client = get_s3_client(auth_method, storage_secrets)
167
+ try:
168
+ response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
169
+ return response["Body"].read()
170
+ except Exception as e:
171
+ logger.error("Encountered error while retrieving s3 object: {}", e)
172
+ raise e
173
+ except ClientError as e:
174
+ logger.error("Encountered error while retrieving s3 object: {}", e)
175
+ raise e
176
+ except ParamValidationError as e:
177
+ raise ValueError(f"The parameters you provided are incorrect: {e}")
178
+
179
+
180
+ def generic_delete_from_s3(
181
+ storage_secrets: Dict[StorageSecrets, Any],
182
+ bucket_name: str,
183
+ file_key: str,
184
+ auth_method: str,
185
+ ) -> None:
186
+ """Deletes arbitrary data from s3"""
187
+ logger.info("Starting S3 Delete of {}", file_key)
188
+
189
+ try:
190
+ s3_client = get_s3_client(auth_method, storage_secrets)
191
+ try:
192
+ s3_client.delete_object(Bucket=bucket_name, Key=file_key)
193
+ except Exception as e:
194
+ logger.error("Encountered error while deleting s3 object: {}", e)
195
+ raise e
196
+ except ClientError as e:
197
+ logger.error("Encountered error while deleting s3 object: {}", e)
198
+ raise e
199
+ except ParamValidationError as e:
200
+ raise ValueError(f"The parameters you provided are incorrect: {e}")
201
+
202
+
122
203
  def upload_to_s3( # pylint: disable=R0913
123
204
  storage_secrets: Dict[StorageSecrets, Any],
124
205
  data: Dict,
125
206
  bucket_name: str,
126
207
  file_key: str,
127
208
  resp_format: str,
128
- privacy_request: PrivacyRequest,
209
+ privacy_request: Optional[PrivacyRequest],
210
+ document: Optional[bytes],
129
211
  auth_method: str,
130
- data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
131
- data_use_map: Optional[Dict[str, Set[str]]] = None,
132
- ) -> str:
212
+ ) -> Optional[AnyHttpUrlString]:
133
213
  """Uploads arbitrary data to s3 returned from an access request"""
134
214
  logger.info("Starting S3 Upload of {}", file_key)
135
215
 
216
+ if privacy_request is None and document is not None:
217
+ return generic_upload_to_s3(
218
+ storage_secrets, bucket_name, file_key, auth_method, document
219
+ )
220
+
221
+ if privacy_request is None:
222
+ raise ValueError("Privacy request must be provided")
223
+
136
224
  try:
137
- my_session = get_aws_session(auth_method, storage_secrets)
138
- s3_client = my_session.client("s3")
225
+ s3_client = get_s3_client(auth_method, storage_secrets)
139
226
 
140
227
  # handles file chunking
141
228
  try:
@@ -148,7 +235,7 @@ def upload_to_s3( # pylint: disable=R0913
148
235
  logger.error("Encountered error while uploading s3 object: {}", e)
149
236
  raise e
150
237
 
151
- presigned_url: str = create_presigned_url_for_s3(
238
+ presigned_url: AnyHttpUrlString = create_presigned_url_for_s3(
152
239
  s3_client, bucket_name, file_key
153
240
  )
154
241
 
@@ -162,17 +249,21 @@ def upload_to_s3( # pylint: disable=R0913
162
249
  raise ValueError(f"The parameters you provided are incorrect: {e}")
163
250
 
164
251
 
252
+ def get_local_filename(file_key: str) -> str:
253
+ """Verifies that the local storage directory exists"""
254
+ if not os.path.exists(LOCAL_FIDES_UPLOAD_DIRECTORY):
255
+ os.makedirs(LOCAL_FIDES_UPLOAD_DIRECTORY)
256
+ return f"{LOCAL_FIDES_UPLOAD_DIRECTORY}/{file_key}"
257
+
258
+
165
259
  def upload_to_local(
166
260
  data: Dict,
167
261
  file_key: str,
168
262
  privacy_request: PrivacyRequest,
169
263
  resp_format: str = ResponseFormat.json.value,
170
- data_category_field_mapping: Optional[DataCategoryFieldMapping] = None,
171
- data_use_map: Optional[Dict[str, Set[str]]] = None,
172
264
  ) -> str:
173
265
  """Uploads access request data to a local folder - for testing/demo purposes only"""
174
- if not os.path.exists(LOCAL_FIDES_UPLOAD_DIRECTORY):
175
- os.makedirs(LOCAL_FIDES_UPLOAD_DIRECTORY)
266
+ get_local_filename(file_key)
176
267
 
177
268
  filename = f"{LOCAL_FIDES_UPLOAD_DIRECTORY}/{file_key}"
178
269
  in_memory_file = write_to_in_memory_buffer(resp_format, data, privacy_request)
@@ -70,3 +70,22 @@ def get_aws_session(
70
70
  raise
71
71
  else:
72
72
  return session
73
+
74
+
75
+ def get_s3_client(
76
+ auth_method: str,
77
+ storage_secrets: Optional[Dict[StorageSecrets, Any]],
78
+ assume_role_arn: Optional[str] = None,
79
+ ) -> Session:
80
+ """
81
+ Abstraction to retrieve an AWS S3 client using secrets.
82
+
83
+ If an `assume_role_arn` is provided, the secrets will be used to
84
+ assume that role and return a Session instantiated with that role.
85
+ """
86
+ session = get_aws_session(
87
+ auth_method=auth_method,
88
+ storage_secrets=storage_secrets,
89
+ assume_role_arn=assume_role_arn,
90
+ )
91
+ return session.client("s3")
@@ -1,3 +1,4 @@
1
+ from collections import deque
1
2
  from functools import reduce
2
3
  from typing import Any, Callable, Dict, Iterable, List, Optional, TypeVar, Union
3
4
 
@@ -119,3 +120,144 @@ def extract_key_for_address(
119
120
  request_id_dataset, collection = full_request_id.split(":")
120
121
  dataset = request_id_dataset.split("__", number_of_leading_strings_to_exclude)[-1]
121
122
  return f"{dataset}:{collection}"
123
+
124
+
125
+ def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
126
+ """
127
+ Converts a dictionary of paths/values into a nested dictionary
128
+
129
+ example:
130
+
131
+ {"A.B": "1", "A.C": "2"}
132
+
133
+ becomes
134
+
135
+ {
136
+ "A": {
137
+ "B": "1",
138
+ "C": "2"
139
+ }
140
+ }
141
+ """
142
+ output: Dict[Any, Any] = {}
143
+ queue = deque(flat_dict.items())
144
+
145
+ while queue:
146
+ path, value = queue.popleft()
147
+ keys = path.split(separator)
148
+ target = output
149
+ for i, current_key in enumerate(keys[:-1]):
150
+ next_key = keys[i + 1]
151
+ if next_key.isdigit():
152
+ target = target.setdefault(current_key, [])
153
+ else:
154
+ if isinstance(target, dict):
155
+ target = target.setdefault(current_key, {})
156
+ elif isinstance(target, list):
157
+ while len(target) <= int(current_key):
158
+ target.append({})
159
+ target = target[int(current_key)]
160
+ try:
161
+ if isinstance(target, list):
162
+ target.append(value)
163
+ else:
164
+ # If the value is a dictionary, add its components to the queue for processing
165
+ if isinstance(value, dict):
166
+ target = target.setdefault(keys[-1], {})
167
+ for inner_key, inner_value in value.items():
168
+ new_key = f"{path}{separator}{inner_key}"
169
+ queue.append((new_key, inner_value))
170
+ else:
171
+ target[keys[-1]] = value
172
+ except TypeError as exc:
173
+ raise ValueError(
174
+ f"Error unflattening dictionary, conflicting levels detected: {exc}"
175
+ )
176
+ return output
177
+
178
+
179
+ def flatten_dict(data: Any, prefix: str = "", separator: str = ".") -> Dict[str, Any]:
180
+ """
181
+ Recursively flatten a dictionary or list into a flat dictionary with dot-notation keys.
182
+ Handles nested dictionaries and arrays with proper indices.
183
+
184
+ example:
185
+
186
+ {
187
+ "A": {
188
+ "B": "1",
189
+ "C": "2"
190
+ },
191
+ "D": [
192
+ {"E": "3"},
193
+ {"E": "4"}
194
+ ]
195
+ }
196
+
197
+ becomes
198
+
199
+ {
200
+ "A.B": "1",
201
+ "A.C": "2",
202
+ "D.0.E": "3",
203
+ "D.1.E": "4"
204
+ }
205
+
206
+ Args:
207
+ data: The data to flatten (dict, list, or scalar value)
208
+ prefix: The current key prefix (used in recursion)
209
+ separator: The separator to use between key segments (default: ".")
210
+
211
+ Returns:
212
+ A flattened dictionary with dot-notation keys
213
+ """
214
+ items = {}
215
+
216
+ if isinstance(data, dict):
217
+ for k, v in data.items():
218
+ new_key = f"{prefix}{separator}{k}" if prefix else k
219
+ if isinstance(v, (dict, list)):
220
+ items.update(flatten_dict(v, new_key, separator))
221
+ else:
222
+ items[new_key] = v
223
+ elif isinstance(data, list):
224
+ for i, v in enumerate(data):
225
+ new_key = f"{prefix}{separator}{i}"
226
+ if isinstance(v, (dict, list)):
227
+ items.update(flatten_dict(v, new_key, separator))
228
+ else:
229
+ items[new_key] = v
230
+ else:
231
+ raise ValueError(
232
+ f"Input to flatten_dict must be a dict or list, got {type(data).__name__}"
233
+ )
234
+
235
+ return items
236
+
237
+
238
+ def replace_none_arrays(
239
+ data: Any,
240
+ ) -> Any:
241
+ """
242
+ Recursively replace any arrays containing only None values with empty arrays.
243
+
244
+ Args:
245
+ data: Any Python object (dict, list, etc.)
246
+
247
+ Returns:
248
+ The transformed data structure with None-only arrays replaced by empty arrays
249
+ """
250
+ if isinstance(data, dict):
251
+ # Process each key-value pair in dictionaries
252
+ return {k: replace_none_arrays(v) for k, v in data.items()}
253
+
254
+ if isinstance(data, list):
255
+ # Check if list contains only None values
256
+ if all(item is None for item in data):
257
+ return []
258
+
259
+ # Otherwise process each item in the list
260
+ return [replace_none_arrays(item) for item in data]
261
+
262
+ # Return other data types unchanged
263
+ return data
@@ -1,3 +1,4 @@
1
+ import inspect
1
2
  from abc import abstractmethod
2
3
  from enum import Enum
3
4
  from functools import wraps
@@ -81,12 +82,46 @@ def log_context(
81
82
  def wrapper(*args: Any, **kwargs: Any) -> Any:
82
83
  context = dict(additional_context)
83
84
 
84
- # extract specified param values from kwargs
85
+ # extract specified param values from kwargs and args
85
86
  if capture_args:
87
+ # First, process kwargs as they're explicitly named
86
88
  for arg_name, context_name in capture_args.items():
87
89
  if arg_name in kwargs:
88
90
  context[context_name.value] = kwargs[arg_name]
89
91
 
92
+ # Process args using signature binding for more robust parameter mapping
93
+ if args:
94
+ try:
95
+ # Get the signature and bind the arguments
96
+ sig = inspect.signature(func)
97
+ # This will map positional args to their parameter names correctly
98
+ bound_args = sig.bind_partial(*args, **kwargs)
99
+
100
+ # Now we can iterate through the bound arguments
101
+ for param_name, arg_value in bound_args.arguments.items():
102
+ # Only process if this parameter is in capture_args and wasn't already found in kwargs
103
+ if param_name in capture_args and param_name not in kwargs:
104
+ context_name = capture_args[param_name]
105
+ context[context_name.value] = arg_value
106
+ except TypeError:
107
+ # Handle the case where the arguments don't match the signature
108
+ pass
109
+
110
+ # Handle default parameters that weren't provided in args or kwargs
111
+ if capture_args:
112
+ sig = inspect.signature(func)
113
+ for param_name, param in sig.parameters.items():
114
+ # Check if parameter has a default value and is in capture_args
115
+ # and hasn't been processed yet (not in context)
116
+ if (
117
+ param.default is not param.empty
118
+ and param_name in capture_args
119
+ and capture_args[param_name].value not in context
120
+ ):
121
+ context_name = capture_args[param_name]
122
+ context[context_name.value] = param.default
123
+
124
+ # Process Contextualizable args
90
125
  for arg in args:
91
126
  if isinstance(arg, Contextualizable):
92
127
  arg_context = arg.get_log_context()
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import re
5
5
  import socket
6
- from collections import defaultdict, deque
6
+ from collections import defaultdict
7
7
  from ipaddress import IPv4Address, IPv6Address, ip_address
8
8
  from typing import Any, Dict, List, Optional, Set, Tuple, Union
9
9
 
@@ -256,60 +256,6 @@ def merge_datasets(dataset: GraphDataset, config_dataset: GraphDataset) -> Graph
256
256
  )
257
257
 
258
258
 
259
- def unflatten_dict(flat_dict: Dict[str, Any], separator: str = ".") -> Dict[str, Any]:
260
- """
261
- Converts a dictionary of paths/values into a nested dictionary
262
-
263
- example:
264
-
265
- {"A.B": "1", "A.C": "2"}
266
-
267
- becomes
268
-
269
- {
270
- "A": {
271
- "B": "1",
272
- "C": "2"
273
- }
274
- }
275
- """
276
- output: Dict[Any, Any] = {}
277
- queue = deque(flat_dict.items())
278
-
279
- while queue:
280
- path, value = queue.popleft()
281
- keys = path.split(separator)
282
- target = output
283
- for i, current_key in enumerate(keys[:-1]):
284
- next_key = keys[i + 1]
285
- if next_key.isdigit():
286
- target = target.setdefault(current_key, [])
287
- else:
288
- if isinstance(target, dict):
289
- target = target.setdefault(current_key, {})
290
- elif isinstance(target, list):
291
- while len(target) <= int(current_key):
292
- target.append({})
293
- target = target[int(current_key)]
294
- try:
295
- if isinstance(target, list):
296
- target.append(value)
297
- else:
298
- # If the value is a dictionary, add its components to the queue for processing
299
- if isinstance(value, dict):
300
- target = target.setdefault(keys[-1], {})
301
- for inner_key, inner_value in value.items():
302
- new_key = f"{path}{separator}{inner_key}"
303
- queue.append((new_key, inner_value))
304
- else:
305
- target[keys[-1]] = value
306
- except TypeError as exc:
307
- raise FidesopsException(
308
- f"Error unflattening dictionary, conflicting levels detected: {exc}"
309
- )
310
- return output
311
-
312
-
313
259
  def format_body(
314
260
  headers: Dict[str, Any],
315
261
  body: Optional[str],
@@ -339,7 +285,7 @@ def format_body(
339
285
  if content_type == "application/json":
340
286
  output = body
341
287
  elif content_type == "application/x-www-form-urlencoded":
342
- output = multidimensional_urlencode(json.loads(body))
288
+ output = nullsafe_urlencode(json.loads(body))
343
289
  elif content_type == "text/plain":
344
290
  output = body
345
291
  else:
@@ -470,3 +416,33 @@ def replace_version(saas_config: str, new_version: str) -> str:
470
416
  version_pattern, f"version: {new_version}", saas_config, count=1
471
417
  )
472
418
  return updated_config
419
+
420
+
421
+ def nullsafe_urlencode(data: Any) -> str:
422
+ """
423
+ Wrapper around multidimensional_urlencode that preserves null values as empty strings.
424
+
425
+ This is useful for APIs that expect keys with empty values (e.g., "name=") to represent
426
+ null values, rather than omitting the field entirely.
427
+
428
+ Args:
429
+ data: The data to encode (can be a dict, list, or other nested structure)
430
+
431
+ Returns:
432
+ URL-encoded string with null values properly handled
433
+ """
434
+
435
+ def prepare_null_values(data: Any) -> Any:
436
+ """
437
+ Recursively process data for URL encoding, converting None values to empty strings.
438
+ """
439
+ if data is None:
440
+ return ""
441
+ if isinstance(data, dict):
442
+ return {k: prepare_null_values(v) for k, v in data.items()}
443
+ if isinstance(data, list):
444
+ return [prepare_null_values(item) for item in data]
445
+ return data
446
+
447
+ processed_data = prepare_null_values(data)
448
+ return multidimensional_urlencode(processed_data)
@@ -17,6 +17,8 @@ languages:
17
17
  name: Greek
18
18
  - id: en
19
19
  name: English
20
+ - id: en-GB
21
+ name: English (UK)
20
22
  - id: es
21
23
  name: Spanish
22
24
  - id: es-MX
@@ -332,11 +332,20 @@ class PrivacyRequestService:
332
332
  )
333
333
 
334
334
  if _manual_approval_required(self.config_proxy, privacy_request):
335
- self.approve_privacy_requests(
336
- [privacy_request_id],
337
- reviewed_by=reviewed_by,
338
- suppress_notification=True,
339
- )
335
+ has_webhooks = self.db.query(PreApprovalWebhook).count() > 0
336
+ # If there are pre-approval webhooks, then we do nothing since the
337
+ # create_privacy_request method will have already triggered them,
338
+ # and they will be responsible of approving the request.
339
+
340
+ # If there are no pre-approval webhooks, approve the request immediately
341
+ # since it was originally approved by a person and we don't want them to
342
+ # have to manually re-approve
343
+ if not has_webhooks:
344
+ self.approve_privacy_requests(
345
+ [privacy_request_id],
346
+ reviewed_by=reviewed_by,
347
+ suppress_notification=True,
348
+ )
340
349
 
341
350
  return privacy_request
342
351