ethyca-fides 2.56.3b1__py2.py3-none-any.whl → 2.56.3b2__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.
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/METADATA +1 -1
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/RECORD +94 -91
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/69ad6d844e21_add_comments_and_comment_references.py +84 -0
- fides/api/alembic/migrations/versions/6ea2171c544f_change_attachment_storage_key_to_.py +77 -0
- fides/api/models/attachment.py +109 -49
- fides/api/models/comment.py +109 -0
- fides/api/service/connectors/query_configs/saas_query_config.py +21 -15
- fides/api/service/storage/storage_uploader_service.py +4 -10
- fides/api/tasks/storage.py +106 -15
- fides/api/util/aws_util.py +19 -0
- fides/api/util/collection_util.py +117 -0
- fides/api/util/saas_util.py +32 -56
- fides/data/language/languages.yml +2 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{_app-3c1a7742661d3a9e.js → _app-3b7bbcdb61d952e7.js} +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/{index-c9fa68dc0fa42c81.js → index-94e6d589c4edf360.js} +1 -1
- fides/ui-build/static/admin/_next/static/{LOp6RUpN795nyhXOv95wz → n4uO6TqGfiKHQ-X5XYkoy}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/ant-poc.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/LICENSE +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.56.3b1.dist-info → ethyca_fides-2.56.3b2.dist-info}/top_level.txt +0 -0
- /fides/ui-build/static/admin/_next/static/{LOp6RUpN795nyhXOv95wz → n4uO6TqGfiKHQ-X5XYkoy}/_ssgManifest.js +0 -0
|
@@ -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,119 @@ 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 (must be a dict or list)
|
|
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
|
+
Raises:
|
|
215
|
+
FidesopsException: If input is not a dict or list
|
|
216
|
+
"""
|
|
217
|
+
items = {}
|
|
218
|
+
|
|
219
|
+
if isinstance(data, dict):
|
|
220
|
+
for k, v in data.items():
|
|
221
|
+
new_key = f"{prefix}{separator}{k}" if prefix else k
|
|
222
|
+
if isinstance(v, (dict, list)):
|
|
223
|
+
items.update(flatten_dict(v, new_key, separator))
|
|
224
|
+
else:
|
|
225
|
+
items[new_key] = v
|
|
226
|
+
elif isinstance(data, list):
|
|
227
|
+
for i, v in enumerate(data):
|
|
228
|
+
new_key = f"{prefix}{separator}{i}"
|
|
229
|
+
if isinstance(v, (dict, list)):
|
|
230
|
+
items.update(flatten_dict(v, new_key, separator))
|
|
231
|
+
else:
|
|
232
|
+
items[new_key] = v
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"Input to flatten_dict must be a dict or list, got {type(data).__name__}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return items
|
fides/api/util/saas_util.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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)
|
|
@@ -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/bd5a72c010fa9c14.css" as="style"/><link rel="stylesheet" href="/_next/static/css/bd5a72c010fa9c14.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-4df2ba5ee2d40f0a.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-ce7f38a12ea8c223.js" defer=""></script><script src="/_next/static/chunks/pages/_app-
|
|
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/bd5a72c010fa9c14.css" as="style"/><link rel="stylesheet" href="/_next/static/css/bd5a72c010fa9c14.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-4df2ba5ee2d40f0a.js" defer=""></script><script src="/_next/static/chunks/framework-c92fc3344e6fd165.js" defer=""></script><script src="/_next/static/chunks/main-ce7f38a12ea8c223.js" defer=""></script><script src="/_next/static/chunks/pages/_app-3b7bbcdb61d952e7.js" defer=""></script><script src="/_next/static/chunks/pages/404-1087258931760074.js" defer=""></script><script src="/_next/static/n4uO6TqGfiKHQ-X5XYkoy/_buildManifest.js" defer=""></script><script src="/_next/static/n4uO6TqGfiKHQ-X5XYkoy/_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":"n4uO6TqGfiKHQ-X5XYkoy","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script></body></html>
|