pangea-sdk 3.6.1__py3-none-any.whl → 3.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,12 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
4
+
3
5
  import datetime
4
6
  from typing import Dict, Optional, Union
5
7
 
8
+ from pangea.asyncio.services.base import ServiceBaseAsync
9
+ from pangea.config import PangeaConfig
6
10
  from pangea.response import PangeaResponse
7
11
  from pangea.services.vault.models.asymmetric import (
8
12
  AsymmetricGenerateRequest,
@@ -47,8 +51,8 @@ from pangea.services.vault.models.common import (
47
51
  StateChangeRequest,
48
52
  StateChangeResult,
49
53
  SymmetricAlgorithm,
50
- TDict,
51
54
  Tags,
55
+ TDict,
52
56
  UpdateRequest,
53
57
  UpdateResult,
54
58
  )
@@ -69,8 +73,6 @@ from pangea.services.vault.models.symmetric import (
69
73
  SymmetricStoreResult,
70
74
  )
71
75
 
72
- from .base import ServiceBaseAsync
73
-
74
76
 
75
77
  class VaultAsync(ServiceBaseAsync):
76
78
  """Vault service client.
@@ -92,17 +94,31 @@ class VaultAsync(ServiceBaseAsync):
92
94
  vault_config = PangeaConfig(domain="pangea.cloud")
93
95
 
94
96
  # Setup Pangea Vault service
95
- vault = Vault(token=PANGEA_VAULT_TOKEN, config=audit_config)
97
+ vault = Vault(token=PANGEA_VAULT_TOKEN, config=vault_config)
96
98
  """
97
99
 
98
100
  service_name = "vault"
99
101
 
100
102
  def __init__(
101
103
  self,
102
- token,
103
- config=None,
104
- logger_name="pangea",
105
- ):
104
+ token: str,
105
+ config: PangeaConfig | None = None,
106
+ logger_name: str = "pangea",
107
+ ) -> None:
108
+ """
109
+ Vault client
110
+
111
+ Initializes a new Vault client.
112
+
113
+ Args:
114
+ token: Pangea API token.
115
+ config: Configuration.
116
+ logger_name: Logger name.
117
+
118
+ Examples:
119
+ config = PangeaConfig(domain="pangea_domain")
120
+ vault = VaultAsync(token="pangea_token", config=config)
121
+ """
106
122
  super().__init__(token, config, logger_name)
107
123
 
108
124
  # Delete endpoint
@@ -859,8 +875,8 @@ class VaultAsync(ServiceBaseAsync):
859
875
 
860
876
  Default is `deactivated`.
861
877
  public_key (EncodedPublicKey, optional): The public key (in PEM format)
862
- private_key: (EncodedPrivateKey, optional): The private key (in PEM format)
863
- key: (EncodedSymmetricKey, optional): The key material (in base64)
878
+ private_key (EncodedPrivateKey, optional): The private key (in PEM format)
879
+ key (EncodedSymmetricKey, optional): The key material (in base64)
864
880
 
865
881
  Raises:
866
882
  PangeaAPIException: If an API Error happens
@@ -1222,7 +1238,7 @@ class VaultAsync(ServiceBaseAsync):
1222
1238
  )
1223
1239
  """
1224
1240
 
1225
- input = EncryptStructuredRequest(
1241
+ input: EncryptStructuredRequest[TDict] = EncryptStructuredRequest(
1226
1242
  id=id, structured_data=structured_data, filter=filter, version=version, additional_data=additional_data
1227
1243
  )
1228
1244
  return await self.request.post(
@@ -1271,7 +1287,7 @@ class VaultAsync(ServiceBaseAsync):
1271
1287
  )
1272
1288
  """
1273
1289
 
1274
- input = EncryptStructuredRequest(
1290
+ input: EncryptStructuredRequest[TDict] = EncryptStructuredRequest(
1275
1291
  id=id, structured_data=structured_data, filter=filter, version=version, additional_data=additional_data
1276
1292
  )
1277
1293
  return await self.request.post(
pangea/config.py CHANGED
@@ -9,65 +9,57 @@ from typing import Optional
9
9
  class PangeaConfig:
10
10
  """Holds run time configuration information used by SDK components."""
11
11
 
12
+ domain: str = "aws.us.pangea.cloud"
12
13
  """
13
14
  Used to set Pangea domain (and port if needed), it should not include service subdomain
14
15
  just for particular use cases when environment = "local", domain could be set to an url including:
15
16
  scheme (http:// or https://), subdomain, domain and port.
16
-
17
17
  """
18
- domain: str = "aws.us.pangea.cloud"
19
18
 
19
+ environment: str = "production"
20
20
  """
21
21
  Used to generate service url.
22
22
  It should be only 'production' or 'local' in cases of particular services that can run locally as Redact.
23
-
24
23
  """
25
- environment: str = "production"
26
24
 
25
+ config_id: Optional[str] = None
27
26
  """
28
27
  Only used for services that support multiconfig (e.g.: Audit service)
29
28
 
30
29
  @deprecated("config_id will be deprecated from PangeaConfig. Set it on service initialization instead")
31
30
  """
32
- config_id: Optional[str] = None
33
31
 
32
+ insecure: bool = False
34
33
  """
35
34
  Set to true to use plain http
36
-
37
35
  """
38
- insecure: bool = False
39
36
 
37
+ request_retries: int = 3
40
38
  """
41
39
  Number of retries on the initial request
42
-
43
40
  """
44
- request_retries: int = 3
45
41
 
42
+ request_backoff: float = 0.5
46
43
  """
47
44
  Backoff strategy passed to 'requests'
48
-
49
45
  """
50
- request_backoff: float = 0.5
51
46
 
47
+ request_timeout: int = 5
52
48
  """
53
49
  Timeout used on initial request attempts
54
-
55
50
  """
56
- request_timeout: int = 5
57
51
 
52
+ poll_result_timeout: int = 30
58
53
  """
59
54
  Timeout used to poll results after 202 (in secs)
60
-
61
55
  """
62
- poll_result_timeout: int = 30
63
56
 
57
+ queued_retry_enabled: bool = True
64
58
  """
65
59
  Enable queued request retry support
66
60
  """
67
- queued_retry_enabled: bool = True
68
61
 
62
+ custom_user_agent: Optional[str] = None
69
63
  """
70
64
  Extra user agent to be added to request user agent
71
-
72
65
  """
73
- custom_user_agent: Optional[str] = None
pangea/dump_audit.py CHANGED
@@ -10,6 +10,7 @@ from datetime import datetime
10
10
  from typing import Tuple
11
11
 
12
12
  import dateutil.parser
13
+
13
14
  from pangea.response import PangeaResponse
14
15
  from pangea.services import Audit
15
16
  from pangea.services.audit.models import SearchEvent, SearchOrder, SearchOrderBy, SearchOutput, SearchResultOutput
pangea/exceptions.py CHANGED
@@ -30,6 +30,14 @@ class PresignedUploadError(PangeaException):
30
30
  self.body = body
31
31
 
32
32
 
33
+ class DownloadFileError(PangeaException):
34
+ body: str
35
+
36
+ def __init__(self, message: str, body: str):
37
+ super().__init__(message)
38
+ self.body = body
39
+
40
+
33
41
  class PangeaAPIException(PangeaException):
34
42
  """Exceptions raised during API calls"""
35
43
 
pangea/request.py CHANGED
@@ -5,16 +5,30 @@ import copy
5
5
  import json
6
6
  import logging
7
7
  import time
8
- from typing import Dict, List, Optional, Tuple, Type, Union
8
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union
9
+
10
+ import requests
11
+ from requests.adapters import HTTPAdapter, Retry
12
+ from requests_toolbelt import MultipartDecoder # type: ignore
13
+ from typing_extensions import TypeVar
9
14
 
10
- import aiohttp
11
15
  import pangea
12
16
  import pangea.exceptions as pe
13
- import requests
14
17
  from pangea.config import PangeaConfig
15
- from pangea.response import PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
18
+ from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
16
19
  from pangea.utils import default_encoder
17
- from requests.adapters import HTTPAdapter, Retry
20
+
21
+ if TYPE_CHECKING:
22
+ import aiohttp
23
+
24
+
25
+ class MultipartResponse(object):
26
+ pangea_json: Dict[str, str]
27
+ attached_files: List = []
28
+
29
+ def __init__(self, pangea_json: Dict[str, str], attached_files: List = []):
30
+ self.pangea_json = pangea_json
31
+ self.attached_files = attached_files
18
32
 
19
33
 
20
34
  class PangeaRequestBase(object):
@@ -30,9 +44,10 @@ class PangeaRequestBase(object):
30
44
  self._queued_retry_enabled = config.queued_retry_enabled
31
45
 
32
46
  # Custom headers
33
- self._extra_headers = {} # type: ignore[var-annotated]
47
+ self._extra_headers: Dict = {}
34
48
  self._user_agent = ""
35
- self.set_custom_user_agent(config.custom_user_agent) # type: ignore[arg-type]
49
+
50
+ self.set_custom_user_agent(config.custom_user_agent)
36
51
  self._session: Optional[Union[requests.Session, aiohttp.ClientSession]] = None
37
52
 
38
53
  self.logger = logger
@@ -57,7 +72,7 @@ class PangeaRequestBase(object):
57
72
  if isinstance(headers, dict):
58
73
  self._extra_headers = headers
59
74
 
60
- def set_custom_user_agent(self, user_agent: str):
75
+ def set_custom_user_agent(self, user_agent: Optional[str]):
61
76
  self.config.custom_user_agent = user_agent
62
77
  self._user_agent = f"pangea-python/{pangea.__version__}"
63
78
  if self.config.custom_user_agent:
@@ -110,6 +125,16 @@ class PangeaRequestBase(object):
110
125
  self._extra_headers.update(headers)
111
126
  return self._extra_headers
112
127
 
128
+ def _get_filename_from_content_disposition(self, content_disposition: str) -> Optional[str]:
129
+ filename_parts = content_disposition.split("name=")
130
+ if len(filename_parts) > 1:
131
+ return filename_parts[1].split(";")[0].strip('"')
132
+ else:
133
+ return None
134
+
135
+ def _get_filename_from_url(self, url: str) -> Optional[str]:
136
+ return url.split("/")[-1].split("?")[0]
137
+
113
138
  def _check_response(self, response: PangeaResponse) -> PangeaResponse:
114
139
  status = response.status
115
140
  summary = response.summary
@@ -149,7 +174,7 @@ class PangeaRequestBase(object):
149
174
  elif status == ResponseStatus.TREE_NOT_FOUND.value:
150
175
  raise pe.TreeNotFoundException(summary, response)
151
176
  elif status == ResponseStatus.IP_NOT_FOUND.value:
152
- raise pe.IPNotFoundException(summary) # type: ignore[call-arg]
177
+ raise pe.IPNotFoundException(summary, response)
153
178
  elif status == ResponseStatus.BAD_OFFSET.value:
154
179
  raise pe.BadOffsetException(summary, response)
155
180
  elif status == ResponseStatus.FORBIDDEN_VAULT_OPERATION.value:
@@ -157,7 +182,7 @@ class PangeaRequestBase(object):
157
182
  elif status == ResponseStatus.VAULT_ITEM_NOT_FOUND.value:
158
183
  raise pe.VaultItemNotFound(summary, response)
159
184
  elif status == ResponseStatus.NOT_FOUND.value:
160
- raise pe.NotFound(response.raw_response.url if response.raw_response is not None else "", response) # type: ignore[arg-type]
185
+ raise pe.NotFound(str(response.raw_response.url) if response.raw_response is not None else "", response) # type: ignore[arg-type]
161
186
  elif status == ResponseStatus.INTERNAL_SERVER_ERROR.value:
162
187
  raise pe.InternalServerError(response)
163
188
  elif status == ResponseStatus.ACCEPTED.value:
@@ -165,6 +190,9 @@ class PangeaRequestBase(object):
165
190
  raise pe.PangeaAPIException(f"{summary} ", response)
166
191
 
167
192
 
193
+ TResult = TypeVar("TResult", bound=PangeaResponseResult, default=PangeaResponseResult)
194
+
195
+
168
196
  class PangeaRequest(PangeaRequestBase):
169
197
  """An object that makes direct calls to Pangea Service APIs.
170
198
 
@@ -180,12 +208,12 @@ class PangeaRequest(PangeaRequestBase):
180
208
  def post(
181
209
  self,
182
210
  endpoint: str,
183
- result_class: Type[PangeaResponseResult],
211
+ result_class: Type[TResult],
184
212
  data: Union[str, Dict] = {},
185
213
  files: Optional[List[Tuple]] = None,
186
214
  poll_result: bool = True,
187
215
  url: Optional[str] = None,
188
- ) -> PangeaResponse:
216
+ ) -> PangeaResponse[TResult]:
189
217
  """Makes the POST call to a Pangea Service endpoint.
190
218
 
191
219
  Args:
@@ -200,13 +228,13 @@ class PangeaRequest(PangeaRequestBase):
200
228
  url = self._url(endpoint)
201
229
 
202
230
  # Set config ID if available
203
- if self.config_id and data.get("config_id", None) is None: # type: ignore[union-attr]
204
- data["config_id"] = self.config_id # type: ignore[index]
231
+ if self.config_id and isinstance(data, dict) and data.get("config_id", None) is None:
232
+ data["config_id"] = self.config_id
205
233
 
206
234
  self.logger.debug(
207
235
  json.dumps({"service": self.service, "action": "post", "url": url, "data": data}, default=default_encoder)
208
236
  )
209
- transfer_method = data.get("transfer_method", None) # type: ignore[union-attr]
237
+ transfer_method = data.get("transfer_method", None) if isinstance(data, dict) else None
210
238
 
211
239
  if files is not None and type(data) is dict and (transfer_method == TransferMethod.POST_URL.value):
212
240
  requests_response = self._full_post_presigned_url(
@@ -218,15 +246,68 @@ class PangeaRequest(PangeaRequestBase):
218
246
  )
219
247
 
220
248
  self._check_http_errors(requests_response)
221
- json_resp = requests_response.json()
222
- self.logger.debug(json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp}))
223
249
 
224
- pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp) # type: ignore[var-annotated]
250
+ if "multipart/form-data" in requests_response.headers.get("content-type", ""):
251
+ multipart_response = self._process_multipart_response(requests_response)
252
+ pangea_response: PangeaResponse = PangeaResponse(
253
+ requests_response,
254
+ result_class=result_class,
255
+ json=multipart_response.pangea_json,
256
+ attached_files=multipart_response.attached_files,
257
+ )
258
+ else:
259
+ try:
260
+ json_resp = requests_response.json()
261
+ self.logger.debug(
262
+ json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp})
263
+ )
264
+
265
+ pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp)
266
+ except requests.exceptions.JSONDecodeError as e:
267
+ raise pe.PangeaException(f"Failed to decode json response. {e}. Body: {requests_response.text}")
268
+
225
269
  if poll_result:
226
270
  pangea_response = self._handle_queued_result(pangea_response)
227
271
 
228
272
  return self._check_response(pangea_response)
229
273
 
274
+ def _get_pangea_json(self, decoder: MultipartDecoder) -> Optional[Dict]:
275
+ # Iterate through parts
276
+ for i, part in enumerate(decoder.parts):
277
+ if i == 0:
278
+ json_str = part.content.decode("utf-8")
279
+ return json.loads(json_str)
280
+
281
+ return None
282
+
283
+ def _get_attached_files(self, decoder: MultipartDecoder) -> List[AttachedFile]:
284
+ files = []
285
+
286
+ for i, part in enumerate(decoder.parts):
287
+ content_type = part.headers.get(b"Content-Type", b"").decode("utf-8")
288
+ # if "application/octet-stream" in content_type:
289
+ if i > 0:
290
+ content_disposition = part.headers.get(b"Content-Disposition", b"").decode("utf-8")
291
+ name = self._get_filename_from_content_disposition(content_disposition)
292
+ if name is None:
293
+ name = f"default_file_name_{i}"
294
+
295
+ files.append(AttachedFile(name, part.content, content_type))
296
+
297
+ return files
298
+
299
+ def _process_multipart_response(self, resp: requests.Response) -> MultipartResponse:
300
+ # Parse the multipart response
301
+ decoder = MultipartDecoder.from_response(resp)
302
+
303
+ pangea_json = self._get_pangea_json(decoder)
304
+ self.logger.debug(
305
+ json.dumps({"service": self.service, "action": "multipart response", "response": pangea_json})
306
+ )
307
+
308
+ attached_files = self._get_attached_files(decoder)
309
+ return MultipartResponse(pangea_json, attached_files) # type: ignore
310
+
230
311
  def _check_http_errors(self, resp: requests.Response):
231
312
  if resp.status_code == 503:
232
313
  raise pe.ServiceTemporarilyUnavailable(resp.json())
@@ -268,48 +349,8 @@ class PangeaRequest(PangeaRequestBase):
268
349
 
269
350
  return data, files
270
351
 
271
- def _post_presigned_url(
272
- self,
273
- endpoint: str,
274
- result_class: Type[PangeaResponseResult],
275
- data: Union[str, Dict] = {},
276
- files: Optional[List[Tuple]] = None,
277
- ):
278
- if len(files) == 0: # type: ignore[arg-type]
279
- raise AttributeError("files attribute should have at least 1 file")
280
-
281
- # Send request
282
- try:
283
- # This should return 202 (AcceptedRequestException)
284
- resp = self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
285
- raise pe.PresignedURLException("Should return 202", resp)
286
-
287
- except pe.AcceptedRequestException as e:
288
- accepted_exception = e
289
- except Exception as e:
290
- raise e
291
-
292
- # Receive 202 with accepted_status
293
- result = self._poll_presigned_url(accepted_exception) # type: ignore[arg-type]
294
- data_to_presigned = result.accepted_status.upload_details # type: ignore[attr-defined]
295
- presigned_url = result.accepted_status.upload_url # type: ignore[attr-defined]
296
-
297
- # Send multipart request with file and upload_details as body
298
- resp = self._http_post(url=presigned_url, data=data_to_presigned, files=files, multipart_post=False) # type: ignore[assignment]
299
- self.logger.debug(
300
- json.dumps(
301
- {"service": self.service, "action": "post presigned", "url": presigned_url, "response": resp.text}, # type: ignore[attr-defined]
302
- default=default_encoder,
303
- )
304
- )
305
-
306
- if resp.status_code < 200 or resp.status_code >= 300: # type: ignore[attr-defined]
307
- raise pe.PresignedUploadError(f"presigned POST failure: {resp.status_code}", resp.text) # type: ignore[attr-defined]
308
-
309
- return accepted_exception.response.raw_response
310
-
311
- def _handle_queued_result(self, response: PangeaResponse) -> PangeaResponse:
312
- if self._queued_retry_enabled and response.raw_response.status_code == 202: # type: ignore[union-attr]
352
+ def _handle_queued_result(self, response: PangeaResponse[TResult]) -> PangeaResponse[TResult]:
353
+ if self._queued_retry_enabled and response.http_status == 202:
313
354
  self.logger.debug(
314
355
  json.dumps(
315
356
  {"service": self.service, "action": "poll_result", "response": response.json},
@@ -320,7 +361,7 @@ class PangeaRequest(PangeaRequestBase):
320
361
 
321
362
  return response
322
363
 
323
- def get(self, path: str, result_class: Type[PangeaResponseResult], check_response: bool = True) -> PangeaResponse:
364
+ def get(self, path: str, result_class: Type[TResult], check_response: bool = True) -> PangeaResponse[TResult]:
324
365
  """Makes the GET call to a Pangea Service endpoint.
325
366
 
326
367
  Args:
@@ -336,7 +377,9 @@ class PangeaRequest(PangeaRequestBase):
336
377
  self.logger.debug(json.dumps({"service": self.service, "action": "get", "url": url}))
337
378
  requests_response = self.session.get(url, headers=self._headers())
338
379
  self._check_http_errors(requests_response)
339
- pangea_response = PangeaResponse(requests_response, result_class=result_class, json=requests_response.json()) # type: ignore[var-annotated]
380
+ pangea_response: PangeaResponse = PangeaResponse(
381
+ requests_response, result_class=result_class, json=requests_response.json()
382
+ )
340
383
 
341
384
  self.logger.debug(
342
385
  json.dumps(
@@ -350,20 +393,61 @@ class PangeaRequest(PangeaRequestBase):
350
393
 
351
394
  return self._check_response(pangea_response)
352
395
 
396
+ def download_file(self, url: str, filename: Optional[str] = None) -> AttachedFile:
397
+ self.logger.debug(
398
+ json.dumps(
399
+ {
400
+ "service": self.service,
401
+ "action": "download_file",
402
+ "url": url,
403
+ "filename": filename,
404
+ "status": "start",
405
+ }
406
+ )
407
+ )
408
+ response = self.session.get(url, headers={})
409
+ if response.status_code == 200:
410
+ if filename is None:
411
+ content_disposition = response.headers.get(b"Content-Disposition", b"").decode("utf-8")
412
+ filename = self._get_filename_from_content_disposition(content_disposition)
413
+ if filename is None:
414
+ filename = self._get_filename_from_url(url)
415
+ if filename is None:
416
+ filename = "default_filename"
417
+
418
+ content_type = response.headers.get(b"Content-Type", b"").decode("utf-8")
419
+
420
+ self.logger.debug(
421
+ json.dumps(
422
+ {
423
+ "service": self.service,
424
+ "action": "download_file",
425
+ "url": url,
426
+ "filename": filename,
427
+ "status": "success",
428
+ }
429
+ )
430
+ )
431
+ return AttachedFile(filename=filename, file=response.content, content_type=content_type)
432
+ else:
433
+ raise pe.DownloadFileError(f"Failed to download file. Status: {response.status_code}", response.text)
434
+
353
435
  def poll_result_by_id(
354
- self, request_id: str, result_class: Union[Type[PangeaResponseResult], dict], check_response: bool = True
355
- ):
436
+ self, request_id: str, result_class: Type[TResult], check_response: bool = True
437
+ ) -> PangeaResponse[TResult]:
356
438
  path = self._get_poll_path(request_id)
357
439
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_once", "url": path}))
358
- return self.get(path, result_class, check_response=check_response) # type: ignore[arg-type]
440
+ return self.get(path, result_class, check_response=check_response)
359
441
 
360
- def poll_result_once(self, response: PangeaResponse, check_response: bool = True):
442
+ def poll_result_once(
443
+ self, response: PangeaResponse[TResult], check_response: bool = True
444
+ ) -> PangeaResponse[TResult]:
361
445
  request_id = response.request_id
362
446
  if not request_id:
363
447
  raise pe.PangeaException("Poll result error: response did not include a 'request_id'")
364
448
 
365
449
  if response.status != ResponseStatus.ACCEPTED.value:
366
- raise pe.PangeaException("Response already proccesed")
450
+ raise pe.PangeaException("Response already processed")
367
451
 
368
452
  return self.poll_result_by_id(request_id, response.result_class, check_response=check_response)
369
453
 
@@ -422,7 +506,8 @@ class PangeaRequest(PangeaRequestBase):
422
506
  self.logger.debug(
423
507
  json.dumps({"service": self.service, "action": "http_put", "url": url}, default=default_encoder)
424
508
  )
425
- return self.session.put(url, headers=headers, files=files)
509
+ _, value = files[0]
510
+ return self.session.put(url, headers=headers, data=value[1])
426
511
 
427
512
  def _full_post_presigned_url(
428
513
  self,
@@ -431,17 +516,22 @@ class PangeaRequest(PangeaRequestBase):
431
516
  data: Union[str, Dict] = {},
432
517
  files: Optional[List[Tuple]] = None,
433
518
  ):
434
- if len(files) == 0: # type: ignore[arg-type]
519
+ if files is None or len(files) == 0:
435
520
  raise AttributeError("files attribute should have at least 1 file")
436
521
 
437
522
  response = self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
438
- data_to_presigned = response.accepted_result.post_form_data # type: ignore[union-attr]
439
- presigned_url = response.accepted_result.post_url # type: ignore[union-attr]
523
+ if response.accepted_result is None:
524
+ raise pe.PangeaException("No accepted_result field when requesting presigned url")
525
+ if response.accepted_result.post_url is None:
526
+ raise pe.PresignedURLException("No presigned url", response)
527
+
528
+ data_to_presigned = response.accepted_result.post_form_data
529
+ presigned_url = response.accepted_result.post_url
440
530
 
441
- self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files) # type: ignore[arg-type]
531
+ self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files)
442
532
  return response.raw_response
443
533
 
444
- def _poll_result_retry(self, response: PangeaResponse) -> PangeaResponse:
534
+ def _poll_result_retry(self, response: PangeaResponse[TResult]) -> PangeaResponse[TResult]:
445
535
  retry_count = 1
446
536
  start = time.time()
447
537
 
@@ -453,7 +543,7 @@ class PangeaRequest(PangeaRequestBase):
453
543
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_retry", "step": "exit"}))
454
544
  return self._check_response(response)
455
545
 
456
- def _poll_presigned_url(self, response: PangeaResponse) -> PangeaResponse:
546
+ def _poll_presigned_url(self, response: PangeaResponse[TResult]) -> PangeaResponse[TResult]:
457
547
  if response.http_status != 202:
458
548
  raise AttributeError("Response should be 202")
459
549