pangea-sdk 3.8.0b1__py3-none-any.whl → 5.3.0__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.
- pangea/__init__.py +1 -1
- pangea/asyncio/file_uploader.py +1 -1
- pangea/asyncio/request.py +49 -31
- pangea/asyncio/services/__init__.py +2 -0
- pangea/asyncio/services/audit.py +192 -31
- pangea/asyncio/services/authn.py +187 -109
- pangea/asyncio/services/authz.py +285 -0
- pangea/asyncio/services/base.py +21 -2
- pangea/asyncio/services/embargo.py +2 -2
- pangea/asyncio/services/file_scan.py +24 -9
- pangea/asyncio/services/intel.py +108 -34
- pangea/asyncio/services/redact.py +72 -4
- pangea/asyncio/services/sanitize.py +217 -0
- pangea/asyncio/services/share.py +246 -73
- pangea/asyncio/services/vault.py +1710 -750
- pangea/crypto/rsa.py +135 -0
- pangea/deep_verify.py +7 -1
- pangea/dump_audit.py +9 -8
- pangea/request.py +83 -59
- pangea/response.py +49 -31
- pangea/services/__init__.py +2 -0
- pangea/services/audit/audit.py +205 -42
- pangea/services/audit/models.py +56 -8
- pangea/services/audit/signing.py +6 -5
- pangea/services/audit/util.py +3 -3
- pangea/services/authn/authn.py +140 -70
- pangea/services/authn/models.py +167 -11
- pangea/services/authz.py +400 -0
- pangea/services/base.py +39 -8
- pangea/services/embargo.py +2 -2
- pangea/services/file_scan.py +32 -15
- pangea/services/intel.py +157 -32
- pangea/services/redact.py +152 -4
- pangea/services/sanitize.py +388 -0
- pangea/services/share/share.py +683 -107
- pangea/services/vault/models/asymmetric.py +120 -18
- pangea/services/vault/models/common.py +439 -141
- pangea/services/vault/models/keys.py +94 -0
- pangea/services/vault/models/secret.py +27 -3
- pangea/services/vault/models/symmetric.py +68 -22
- pangea/services/vault/vault.py +1690 -749
- pangea/tools.py +6 -7
- pangea/utils.py +16 -27
- pangea/verify_audit.py +270 -83
- {pangea_sdk-3.8.0b1.dist-info → pangea_sdk-5.3.0.dist-info}/METADATA +43 -35
- pangea_sdk-5.3.0.dist-info/RECORD +56 -0
- {pangea_sdk-3.8.0b1.dist-info → pangea_sdk-5.3.0.dist-info}/WHEEL +1 -1
- pangea_sdk-3.8.0b1.dist-info/RECORD +0 -50
@@ -0,0 +1,285 @@
|
|
1
|
+
# Copyright 2022 Pangea Cyber Corporation
|
2
|
+
# Author: Pangea Cyber Corporation
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
from typing import Any, Dict, List, Optional
|
7
|
+
|
8
|
+
from pangea.asyncio.services.base import ServiceBaseAsync
|
9
|
+
from pangea.config import PangeaConfig
|
10
|
+
from pangea.response import PangeaResponse
|
11
|
+
from pangea.services.authz import (
|
12
|
+
CheckRequest,
|
13
|
+
CheckResult,
|
14
|
+
ItemOrder,
|
15
|
+
ListResourcesRequest,
|
16
|
+
ListResourcesResult,
|
17
|
+
ListSubjectsRequest,
|
18
|
+
ListSubjectsResult,
|
19
|
+
Resource,
|
20
|
+
Subject,
|
21
|
+
Tuple,
|
22
|
+
TupleCreateRequest,
|
23
|
+
TupleCreateResult,
|
24
|
+
TupleDeleteRequest,
|
25
|
+
TupleDeleteResult,
|
26
|
+
TupleListFilter,
|
27
|
+
TupleListRequest,
|
28
|
+
TupleListResult,
|
29
|
+
TupleOrderBy,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
class AuthZAsync(ServiceBaseAsync):
|
34
|
+
"""AuthZ service client.
|
35
|
+
|
36
|
+
Provides methods to interact with the Pangea AuthZ Service.
|
37
|
+
Documentation for the AuthZ Service API can be found at
|
38
|
+
<https://pangea.cloud/docs/api/authz>.
|
39
|
+
|
40
|
+
Examples:
|
41
|
+
import os
|
42
|
+
from pangea.asyncio.services import AuthZAsync
|
43
|
+
from pangea.config import PangeaConfig
|
44
|
+
|
45
|
+
PANGEA_TOKEN = os.getenv("PANGEA_AUTHZ_TOKEN")
|
46
|
+
|
47
|
+
authz_config = PangeaConfig(domain="aws.us.pangea.cloud")
|
48
|
+
|
49
|
+
# Setup Pangea AuthZ service client
|
50
|
+
authz = AuthZAsync(token=PANGEA_TOKEN, config=authz_config)
|
51
|
+
"""
|
52
|
+
|
53
|
+
service_name = "authz"
|
54
|
+
|
55
|
+
def __init__(
|
56
|
+
self, token: str, config: PangeaConfig | None = None, logger_name: str = "pangea", config_id: str | None = None
|
57
|
+
) -> None:
|
58
|
+
"""
|
59
|
+
AuthZ client
|
60
|
+
|
61
|
+
Initializes a new AuthZ client.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
token: Pangea API token.
|
65
|
+
config: Configuration.
|
66
|
+
logger_name: Logger name.
|
67
|
+
config_id: Configuration ID.
|
68
|
+
|
69
|
+
Examples:
|
70
|
+
config = PangeaConfig(domain="aws.us.pangea.cloud")
|
71
|
+
authz = AuthZAsync(token="pangea_token", config=config)
|
72
|
+
"""
|
73
|
+
|
74
|
+
super().__init__(token, config, logger_name, config_id=config_id)
|
75
|
+
|
76
|
+
async def tuple_create(self, tuples: List[Tuple]) -> PangeaResponse[TupleCreateResult]:
|
77
|
+
"""Create tuples.
|
78
|
+
|
79
|
+
Create tuples in the AuthZ Service. The request will fail if there is no schema
|
80
|
+
or the tuples do not validate against the schema.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
tuples (List[Tuple]): List of tuples to be created.
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
PangeaAPIException: If an API Error happens.
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Pangea Response with empty result.
|
90
|
+
Available response fields can be found in our
|
91
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/tuple/create).
|
92
|
+
|
93
|
+
Examples:
|
94
|
+
await authz.tuple_create(
|
95
|
+
tuples=[
|
96
|
+
Tuple(
|
97
|
+
resource=Resource(type="file", id="file_1"),
|
98
|
+
relation="owner",
|
99
|
+
subject=Subject(type="user", id="user_1"),
|
100
|
+
)
|
101
|
+
]
|
102
|
+
)
|
103
|
+
"""
|
104
|
+
|
105
|
+
input_data = TupleCreateRequest(tuples=tuples)
|
106
|
+
return await self.request.post(
|
107
|
+
"v1/tuple/create", TupleCreateResult, data=input_data.model_dump(exclude_none=True)
|
108
|
+
)
|
109
|
+
|
110
|
+
async def tuple_list(
|
111
|
+
self,
|
112
|
+
filter: TupleListFilter,
|
113
|
+
size: Optional[int] = None,
|
114
|
+
last: Optional[str] = None,
|
115
|
+
order: Optional[ItemOrder] = None,
|
116
|
+
order_by: Optional[TupleOrderBy] = None,
|
117
|
+
) -> PangeaResponse[TupleListResult]:
|
118
|
+
"""List tuples.
|
119
|
+
|
120
|
+
Return a paginated list of filtered tuples. The filter is given in terms
|
121
|
+
of a tuple. Fill out the fields that you want to filter. If the filter
|
122
|
+
is empty it will return all the tuples.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
filter (TupleListFilter): The filter for listing tuples.
|
126
|
+
size (Optional[int]): The size of the result set. Default is None.
|
127
|
+
last (Optional[str]): The last token from a previous response. Default is None.
|
128
|
+
order (Optional[ItemOrder]): Order results asc(ending) or desc(ending).
|
129
|
+
order_by (Optional[TupleOrderBy]): Which field to order results by.
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
PangeaAPIException: If an API Error happens.
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
Pangea Response with a list of tuples and the last token.
|
136
|
+
Available response fields can be found in our
|
137
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/tuple/list).
|
138
|
+
|
139
|
+
Examples:
|
140
|
+
await authz.tuple_list(TupleListFilter(subject_type="user", subject_id="user_1"))
|
141
|
+
"""
|
142
|
+
input_data = TupleListRequest(
|
143
|
+
filter=filter.model_dump(exclude_none=True), size=size, last=last, order=order, order_by=order_by
|
144
|
+
)
|
145
|
+
return await self.request.post("v1/tuple/list", TupleListResult, data=input_data.model_dump(exclude_none=True))
|
146
|
+
|
147
|
+
async def tuple_delete(self, tuples: List[Tuple]) -> PangeaResponse[TupleDeleteResult]:
|
148
|
+
"""Delete tuples.
|
149
|
+
|
150
|
+
Delete tuples in the AuthZ Service.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
tuples (List[Tuple]): List of tuples to be deleted.
|
154
|
+
|
155
|
+
Raises:
|
156
|
+
PangeaAPIException: If an API Error happens.
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Pangea Response with empty result.
|
160
|
+
Available response fields can be found in our
|
161
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/tuple/delete).
|
162
|
+
|
163
|
+
Examples:
|
164
|
+
await authz.tuple_delete(
|
165
|
+
tuples=[
|
166
|
+
Tuple(
|
167
|
+
resource=Resource(type="file", id="file_1"),
|
168
|
+
relation="owner",
|
169
|
+
subject=Subject(type="user", id="user_1"),
|
170
|
+
)
|
171
|
+
]
|
172
|
+
)
|
173
|
+
"""
|
174
|
+
|
175
|
+
input_data = TupleDeleteRequest(tuples=tuples)
|
176
|
+
return await self.request.post(
|
177
|
+
"v1/tuple/delete", TupleDeleteResult, data=input_data.model_dump(exclude_none=True)
|
178
|
+
)
|
179
|
+
|
180
|
+
async def check(
|
181
|
+
self,
|
182
|
+
resource: Resource,
|
183
|
+
action: str,
|
184
|
+
subject: Subject,
|
185
|
+
debug: Optional[bool] = None,
|
186
|
+
attributes: Optional[Dict[str, Any]] = None,
|
187
|
+
) -> PangeaResponse[CheckResult]:
|
188
|
+
"""Perform a check request.
|
189
|
+
|
190
|
+
Check if a subject has permission to perform an action on the resource.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
resource (Resource): The resource to check.
|
194
|
+
action (str): The action to check.
|
195
|
+
subject (Subject): The subject to check.
|
196
|
+
debug (Optional[bool]): Setting this value to True will provide a detailed analysis of the check.
|
197
|
+
attributes (Optional[Dict[str, Any]]): Additional attributes for the check.
|
198
|
+
|
199
|
+
Raises:
|
200
|
+
PangeaAPIException: If an API Error happens.
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
Pangea Response with the result of the check.
|
204
|
+
Available response fields can be found in our
|
205
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/check).
|
206
|
+
|
207
|
+
Examples:
|
208
|
+
await authz.check(
|
209
|
+
resource=Resource(type="file", id="file_1"),
|
210
|
+
action="update",
|
211
|
+
subject=Subject(type="user", id="user_1"),
|
212
|
+
debug=True,
|
213
|
+
)
|
214
|
+
"""
|
215
|
+
|
216
|
+
input_data = CheckRequest(resource=resource, action=action, subject=subject, debug=debug, attributes=attributes)
|
217
|
+
return await self.request.post("v1/check", CheckResult, data=input_data.model_dump(exclude_none=True))
|
218
|
+
|
219
|
+
async def list_resources(
|
220
|
+
self, type: str, action: str, subject: Subject, attributes: Optional[Dict[str, Any]] = None
|
221
|
+
) -> PangeaResponse[ListResourcesResult]:
|
222
|
+
"""List resources.
|
223
|
+
|
224
|
+
Given a type, action, and subject, list all the resources in the
|
225
|
+
type that the subject has access to the action with.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
type (str): The type to filter resources.
|
229
|
+
action (str): The action to filter resources.
|
230
|
+
subject (Subject): The subject to filter resources.
|
231
|
+
attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
PangeaAPIException: If an API Error happens.
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
Pangea Response with a list of resource IDs.
|
238
|
+
Available response fields can be found in our
|
239
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/list-resources).
|
240
|
+
|
241
|
+
Examples:
|
242
|
+
await authz.list_resources(
|
243
|
+
type="file",
|
244
|
+
action="update",
|
245
|
+
subject=Subject(type="user", id="user_1"),
|
246
|
+
)
|
247
|
+
"""
|
248
|
+
|
249
|
+
input_data = ListResourcesRequest(type=type, action=action, subject=subject, attributes=attributes)
|
250
|
+
return await self.request.post(
|
251
|
+
"v1/list-resources", ListResourcesResult, data=input_data.model_dump(exclude_none=True)
|
252
|
+
)
|
253
|
+
|
254
|
+
async def list_subjects(
|
255
|
+
self, resource: Resource, action: str, attributes: Optional[Dict[str, Any]] = None
|
256
|
+
) -> PangeaResponse[ListSubjectsResult]:
|
257
|
+
"""List subjects.
|
258
|
+
|
259
|
+
Given a resource and an action, return the list of subjects who have
|
260
|
+
access to the action for the given resource.
|
261
|
+
|
262
|
+
Args:
|
263
|
+
resource (Resource): The resource to filter subjects.
|
264
|
+
action (str): The action to filter subjects.
|
265
|
+
attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
|
266
|
+
|
267
|
+
Raises:
|
268
|
+
PangeaAPIException: If an API Error happens.
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
Pangea Response with a list of subjects.
|
272
|
+
Available response fields can be found in our
|
273
|
+
[API Documentation](https://pangea.cloud/docs/api/authz#/v1/list-subjects).
|
274
|
+
|
275
|
+
Examples:
|
276
|
+
await authz.list_subjects(
|
277
|
+
resource=Resource(type="file", id="file_1"),
|
278
|
+
action="update",
|
279
|
+
)
|
280
|
+
"""
|
281
|
+
|
282
|
+
input_data = ListSubjectsRequest(resource=resource, action=action, attributes=attributes)
|
283
|
+
return await self.request.post(
|
284
|
+
"v1/list-subjects", ListSubjectsResult, data=input_data.model_dump(exclude_none=True)
|
285
|
+
)
|
pangea/asyncio/services/base.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
# Copyright 2022 Pangea Cyber Corporation
|
2
2
|
# Author: Pangea Cyber Corporation
|
3
|
+
from __future__ import annotations
|
3
4
|
|
4
5
|
from typing import Dict, Optional, Type, Union
|
5
6
|
|
7
|
+
from typing_extensions import override
|
8
|
+
|
6
9
|
from pangea.asyncio.request import PangeaRequestAsync
|
7
10
|
from pangea.exceptions import AcceptedRequestException
|
8
11
|
from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult
|
@@ -23,6 +26,7 @@ class ServiceBaseAsync(ServiceBase):
|
|
23
26
|
|
24
27
|
return self._request
|
25
28
|
|
29
|
+
@override
|
26
30
|
async def poll_result( # type: ignore[override]
|
27
31
|
self,
|
28
32
|
exception: Optional[AcceptedRequestException] = None,
|
@@ -36,7 +40,8 @@ class ServiceBaseAsync(ServiceBase):
|
|
36
40
|
Returns request's result that has been accepted by the server
|
37
41
|
|
38
42
|
Args:
|
39
|
-
exception
|
43
|
+
exception: Exception that was previously raised by the SDK on a call
|
44
|
+
that is being processed.
|
40
45
|
|
41
46
|
Returns:
|
42
47
|
PangeaResponse
|
@@ -58,7 +63,21 @@ class ServiceBaseAsync(ServiceBase):
|
|
58
63
|
else:
|
59
64
|
raise AttributeError("Need to set exception, response or request_id")
|
60
65
|
|
61
|
-
|
66
|
+
@override
|
67
|
+
async def download_file(self, url: str, filename: str | None = None) -> AttachedFile: # type: ignore[override]
|
68
|
+
"""
|
69
|
+
Download file
|
70
|
+
|
71
|
+
Download a file from the specified URL and save it with the given
|
72
|
+
filename.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
url: URL of the file to download
|
76
|
+
filename: Name to save the downloaded file as. If not provided, the
|
77
|
+
filename will be determined from the Content-Disposition header or
|
78
|
+
the URL.
|
79
|
+
"""
|
80
|
+
|
62
81
|
return await self.request.download_file(url=url, filename=filename)
|
63
82
|
|
64
83
|
async def close(self):
|
@@ -57,7 +57,7 @@ class EmbargoAsync(ServiceBaseAsync):
|
|
57
57
|
response = embargo.ip_check("190.6.64.94")
|
58
58
|
"""
|
59
59
|
input = e.IPCheckRequest(ip=ip)
|
60
|
-
return await self.request.post("v1/ip/check", e.EmbargoResult, data=input.
|
60
|
+
return await self.request.post("v1/ip/check", e.EmbargoResult, data=input.model_dump())
|
61
61
|
|
62
62
|
async def iso_check(self, iso_code: str) -> PangeaResponse[e.EmbargoResult]:
|
63
63
|
"""
|
@@ -84,4 +84,4 @@ class EmbargoAsync(ServiceBaseAsync):
|
|
84
84
|
response = embargo.iso_check("CU")
|
85
85
|
"""
|
86
86
|
input = e.ISOCheckRequest(iso_code=iso_code)
|
87
|
-
return await self.request.post("v1/iso/check", result_class=e.EmbargoResult, data=input.
|
87
|
+
return await self.request.post("v1/iso/check", result_class=e.EmbargoResult, data=input.model_dump())
|
@@ -59,12 +59,14 @@ class FileScanAsync(ServiceBaseAsync):
|
|
59
59
|
OperationId: file_scan_post_v1_scan
|
60
60
|
|
61
61
|
Args:
|
62
|
-
file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
|
63
62
|
file_path (str, optional): filepath to be opened and scanned
|
63
|
+
file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
|
64
64
|
verbose (bool, optional): Echo the API parameters in the response
|
65
65
|
raw (bool, optional): Include raw data from this provider
|
66
66
|
provider (str, optional): Scan file using this provider
|
67
67
|
sync_call (bool, optional): True to wait until server returns a result, False to return immediately and retrieve result asynchronously
|
68
|
+
transfer_method (TransferMethod, optional): Transfer method used to upload the file data.
|
69
|
+
source_url (str, optional): A URL where the Pangea APIs can fetch the contents of the input file.
|
68
70
|
|
69
71
|
Raises:
|
70
72
|
PangeaAPIException: If an API Error happens
|
@@ -77,7 +79,7 @@ class FileScanAsync(ServiceBaseAsync):
|
|
77
79
|
Examples:
|
78
80
|
try:
|
79
81
|
with open("./path/to/file.pdf", "rb") as f:
|
80
|
-
response = client.file_scan(file=f, verbose=True, provider="crowdstrike")
|
82
|
+
response = await client.file_scan(file=f, verbose=True, provider="crowdstrike")
|
81
83
|
print(f"Response: {response.result}")
|
82
84
|
except pe.PangeaAPIException as e:
|
83
85
|
print(f"Request Error: {e.response.summary}")
|
@@ -85,6 +87,15 @@ class FileScanAsync(ServiceBaseAsync):
|
|
85
87
|
print(f"\\t{err.detail} \\n")
|
86
88
|
"""
|
87
89
|
|
90
|
+
if transfer_method == TransferMethod.SOURCE_URL and source_url is None:
|
91
|
+
raise ValueError("`source_url` argument is required when using `TransferMethod.SOURCE_URL`.")
|
92
|
+
|
93
|
+
if source_url is not None and transfer_method != TransferMethod.SOURCE_URL:
|
94
|
+
raise ValueError(
|
95
|
+
"`transfer_method` should be `TransferMethod.SOURCE_URL` when using the `source_url` argument."
|
96
|
+
)
|
97
|
+
|
98
|
+
files: Optional[List[Tuple]] = None
|
88
99
|
if file or file_path:
|
89
100
|
if file_path:
|
90
101
|
file = open(file_path, "rb")
|
@@ -95,9 +106,9 @@ class FileScanAsync(ServiceBaseAsync):
|
|
95
106
|
size = params.size
|
96
107
|
else:
|
97
108
|
crc, sha, size = None, None, None
|
98
|
-
files
|
99
|
-
|
100
|
-
raise ValueError("Need to set file_path or
|
109
|
+
files = [("upload", ("filename", file, "application/octet-stream"))]
|
110
|
+
elif source_url is None:
|
111
|
+
raise ValueError("Need to set one of `file_path`, `file`, or `source_url` arguments.")
|
101
112
|
|
102
113
|
input = m.FileScanRequest(
|
103
114
|
verbose=verbose,
|
@@ -109,8 +120,12 @@ class FileScanAsync(ServiceBaseAsync):
|
|
109
120
|
transfer_method=transfer_method,
|
110
121
|
source_url=source_url,
|
111
122
|
)
|
112
|
-
data = input.
|
113
|
-
|
123
|
+
data = input.model_dump(exclude_none=True)
|
124
|
+
try:
|
125
|
+
return await self.request.post("v1/scan", m.FileScanResult, data=data, files=files, poll_result=sync_call)
|
126
|
+
finally:
|
127
|
+
if file_path and file:
|
128
|
+
file.close()
|
114
129
|
|
115
130
|
async def request_upload_url(
|
116
131
|
self,
|
@@ -131,7 +146,7 @@ class FileScanAsync(ServiceBaseAsync):
|
|
131
146
|
input.sha256 = params.sha256_hex
|
132
147
|
input.size = params.size
|
133
148
|
|
134
|
-
data = input.
|
149
|
+
data = input.model_dump(exclude_none=True)
|
135
150
|
return await self.request.request_presigned_url("v1/scan", m.FileScanResult, data=data)
|
136
151
|
|
137
152
|
|
@@ -154,7 +169,7 @@ class FileUploaderAsync:
|
|
154
169
|
) -> None:
|
155
170
|
if transfer_method == TransferMethod.PUT_URL:
|
156
171
|
files = [("file", ("filename", file, "application/octet-stream"))]
|
157
|
-
await self._request.put_presigned_url(url=url, files=files)
|
172
|
+
await self._request.put_presigned_url(url=url, files=files)
|
158
173
|
elif transfer_method == TransferMethod.POST_URL:
|
159
174
|
files = [("file", ("filename", file, "application/octet-stream"))]
|
160
175
|
await self._request.post_presigned_url(url=url, data=file_details, files=files) # type: ignore[arg-type]
|