pangea-sdk 3.0.0__tar.gz → 3.2.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/PKG-INFO +2 -1
  2. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/__init__.py +1 -1
  3. pangea_sdk-3.2.0/pangea/asyncio/request.py +283 -0
  4. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/audit.py +82 -3
  5. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/file_scan.py +6 -2
  6. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/exceptions.py +20 -2
  7. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/request.py +158 -35
  8. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/response.py +26 -4
  9. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/audit/audit.py +163 -62
  10. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/audit/models.py +32 -0
  11. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/authn/models.py +2 -2
  12. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/file_scan.py +21 -3
  13. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/vault/models/common.py +11 -0
  14. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/utils.py +24 -1
  15. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pyproject.toml +2 -1
  16. pangea_sdk-3.0.0/pangea/asyncio/request.py +0 -167
  17. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/README.md +0 -0
  18. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/__init__.py +0 -0
  19. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/authn.py +0 -0
  20. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/base.py +0 -0
  21. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/embargo.py +0 -0
  22. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/intel.py +0 -0
  23. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/redact.py +0 -0
  24. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/asyncio/services/vault.py +0 -0
  25. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/audit_logger.py +0 -0
  26. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/config.py +0 -0
  27. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/deep_verify.py +0 -0
  28. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/deprecated.py +0 -0
  29. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/dump_audit.py +0 -0
  30. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/__init__.py +0 -0
  31. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/audit/exceptions.py +0 -0
  32. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/audit/signing.py +0 -0
  33. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/audit/util.py +0 -0
  34. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/authn/authn.py +0 -0
  35. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/base.py +0 -0
  36. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/embargo.py +0 -0
  37. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/intel.py +0 -0
  38. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/redact.py +0 -0
  39. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/vault/models/asymmetric.py +0 -0
  40. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/vault/models/secret.py +0 -0
  41. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/vault/models/symmetric.py +0 -0
  42. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/services/vault/vault.py +0 -0
  43. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/tools.py +0 -0
  44. {pangea_sdk-3.0.0 → pangea_sdk-3.2.0}/pangea/verify_audit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pangea-sdk
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Pangea API SDK
5
5
  License: MIT
6
6
  Keywords: Pangea,SDK,Audit
@@ -20,6 +20,7 @@ Requires-Dist: alive-progress (>=2.4.1,<3.0.0)
20
20
  Requires-Dist: asyncio (>=3.4.3,<4.0.0)
21
21
  Requires-Dist: cryptography (==41.0.3)
22
22
  Requires-Dist: deprecated (>=1.2.13,<2.0.0)
23
+ Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
23
24
  Requires-Dist: pydantic (>=1.10.2,<2.0.0)
24
25
  Requires-Dist: pytest (>=7.2.0,<8.0.0)
25
26
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
@@ -1,4 +1,4 @@
1
- __version__ = "3.0.0"
1
+ __version__ = "3.2.0"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
@@ -0,0 +1,283 @@
1
+ # Copyright 2022 Pangea Cyber Corporation
2
+ # Author: Pangea Cyber Corporation
3
+
4
+ import asyncio
5
+ import json
6
+ import time
7
+ from typing import Dict, List, Optional, Tuple, Type, Union
8
+
9
+ import aiohttp
10
+ import pangea.exceptions as pe
11
+ from aiohttp import FormData
12
+
13
+ # from requests.adapters import HTTPAdapter, Retry
14
+ from pangea.request import PangeaRequestBase
15
+ from pangea.response import AcceptedResult, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
16
+ from pangea.utils import default_encoder
17
+
18
+
19
+ class PangeaRequestAsync(PangeaRequestBase):
20
+ """An object that makes direct calls to Pangea Service APIs.
21
+
22
+ Wraps Get/Post calls to support both API requests. If `queued_retry_enabled`
23
+ is enabled, the progress of long running Post requests will queried until
24
+ completion or until the `poll_result_timeout` is reached. Both values can
25
+ be set in PangeaConfig.
26
+ """
27
+
28
+ async def post(
29
+ self,
30
+ endpoint: str,
31
+ result_class: Type[PangeaResponseResult],
32
+ data: Union[str, Dict] = {},
33
+ files: Optional[List[Tuple]] = None,
34
+ poll_result: bool = True,
35
+ url: Optional[str] = None,
36
+ ) -> PangeaResponse:
37
+ """Makes the POST call to a Pangea Service endpoint.
38
+
39
+ Args:
40
+ endpoint(str): The Pangea Service API endpoint.
41
+ data(dict): The POST body payload object
42
+
43
+ Returns:
44
+ PangeaResponse which contains the response in its entirety and
45
+ various properties to retrieve individual fields
46
+ """
47
+ if url is None:
48
+ url = self._url(endpoint)
49
+
50
+ # Set config ID if available
51
+ if self.config_id and data.get("config_id", None) is None:
52
+ data["config_id"] = self.config_id
53
+
54
+ if (
55
+ files is not None
56
+ and type(data) is dict
57
+ and data.get("transfer_method", None) == TransferMethod.DIRECT.value
58
+ ):
59
+ requests_response = await self._post_presigned_url(
60
+ endpoint, result_class=result_class, data=data, files=files
61
+ )
62
+ else:
63
+ requests_response = await self._http_post(
64
+ url, headers=self._headers(), data=data, files=files, presigned_url_post=False
65
+ )
66
+
67
+ pangea_response = PangeaResponse(
68
+ requests_response, result_class=result_class, json=await requests_response.json()
69
+ )
70
+ if poll_result:
71
+ pangea_response = await self._handle_queued_result(pangea_response)
72
+
73
+ return self._check_response(pangea_response)
74
+
75
+ async def _http_post(
76
+ self,
77
+ url: str,
78
+ headers: Dict = {},
79
+ data: Union[str, Dict] = {},
80
+ files: Optional[List[Tuple]] = None,
81
+ presigned_url_post: bool = False,
82
+ ) -> aiohttp.ClientResponse:
83
+ self.logger.debug(
84
+ json.dumps(
85
+ {"service": self.service, "action": "http_post", "url": url, "data": data}, default=default_encoder
86
+ )
87
+ )
88
+
89
+ if files:
90
+ form = FormData()
91
+ if presigned_url_post:
92
+ for k, v in data.items():
93
+ form.add_field(k, v)
94
+ for name, value in files:
95
+ form.add_field("file", value[1], filename=value[0], content_type=value[2])
96
+ else:
97
+ data_send = json.dumps(data, default=default_encoder) if isinstance(data, dict) else data
98
+ form.add_field("request", data_send, content_type="application/json")
99
+ for name, value in files:
100
+ form.add_field(name, value[1], filename=value[0], content_type=value[2])
101
+
102
+ data_send = form
103
+ else:
104
+ data_send = json.dumps(data, default=default_encoder) if isinstance(data, dict) else data
105
+
106
+ return await self.session.post(url, headers=headers, data=data_send)
107
+
108
+ async def _post_presigned_url(
109
+ self,
110
+ endpoint: str,
111
+ result_class: Type[PangeaResponseResult],
112
+ data: Union[str, Dict] = {},
113
+ files: Optional[List[Tuple]] = None,
114
+ ):
115
+ if len(files) == 0:
116
+ raise AttributeError("files attribute should have at least 1 file")
117
+
118
+ # Send request
119
+ try:
120
+ # This should return 202 (AcceptedRequestException)
121
+ resp = await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
122
+ raise pe.PresignedURLException("Should return 202", resp)
123
+
124
+ except pe.AcceptedRequestException as e:
125
+ accepted_exception = e
126
+ except Exception as e:
127
+ raise e
128
+
129
+ # Receive 202 with accepted_status
130
+ result = await self._poll_presigned_url(accepted_exception)
131
+ data_to_presigned = result.accepted_status.upload_details
132
+ presigned_url = result.accepted_status.upload_url
133
+
134
+ # Send multipart request with file and upload_details as body
135
+ resp = await self._http_post(url=presigned_url, data=data_to_presigned, files=files, presigned_url_post=True)
136
+ self.logger.debug(
137
+ json.dumps(
138
+ {
139
+ "service": self.service,
140
+ "action": "post presigned",
141
+ "url": presigned_url,
142
+ "response": await resp.text(),
143
+ },
144
+ default=default_encoder,
145
+ )
146
+ )
147
+
148
+ if resp.status < 200 or resp.status >= 300:
149
+ raise pe.PresignedUploadError(f"presigned POST failure: {resp.status}", await resp.text())
150
+
151
+ return accepted_exception.response.raw_response
152
+
153
+ async def _poll_presigned_url(self, initial_exc: pe.AcceptedRequestException) -> AcceptedResult:
154
+ if type(initial_exc) is not pe.AcceptedRequestException:
155
+ raise AttributeError("Exception should be of type AcceptedRequestException")
156
+
157
+ if initial_exc.accepted_result.accepted_status.upload_url:
158
+ return initial_exc.accepted_result
159
+
160
+ self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "start"}))
161
+ retry_count = 1
162
+ start = time.time()
163
+ loop_exc = initial_exc
164
+
165
+ while (
166
+ loop_exc.accepted_result is not None
167
+ and not loop_exc.accepted_result.accepted_status.upload_url
168
+ and not self._reach_timeout(start)
169
+ ):
170
+ await asyncio.sleep(self._get_delay(retry_count, start))
171
+ try:
172
+ await self.poll_result_once(initial_exc.response, check_response=False)
173
+ msg = "Polling presigned url return 200 instead of 202"
174
+ self.logger.debug(
175
+ json.dumps(
176
+ {"service": self.service, "action": "poll_presigned_url", "step": "exit", "cause": {msg}}
177
+ )
178
+ )
179
+ raise pe.PangeaException(msg)
180
+ except pe.AcceptedRequestException as e:
181
+ retry_count += 1
182
+ loop_exc = e
183
+ except Exception as e:
184
+ self.logger.debug(
185
+ json.dumps(
186
+ {"service": self.service, "action": "poll_presigned_url", "step": "exit", "cause": {str(e)}}
187
+ )
188
+ )
189
+ raise pe.PresignedURLException("Failed to pull Presigned URL", loop_exc.response, e)
190
+
191
+ self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "exit"}))
192
+
193
+ if loop_exc.accepted_result is not None and not loop_exc.accepted_result.accepted_status.upload_url:
194
+ return loop_exc.accepted_result
195
+ else:
196
+ raise loop_exc
197
+
198
+ async def _handle_queued_result(self, response: PangeaResponse) -> PangeaResponse:
199
+ if self._queued_retry_enabled and response.http_status == 202:
200
+ self.logger.debug(
201
+ json.dumps(
202
+ {"service": self.service, "action": "poll_result", "response": response.json},
203
+ default=default_encoder,
204
+ )
205
+ )
206
+ response = await self._poll_result_retry(response)
207
+
208
+ return response
209
+
210
+ async def get(
211
+ self, path: str, result_class: Type[PangeaResponseResult], check_response: bool = True
212
+ ) -> PangeaResponse:
213
+ """Makes the GET call to a Pangea Service endpoint.
214
+
215
+ Args:
216
+ endpoint(str): The Pangea Service API endpoint.
217
+ path(str): Additional URL path
218
+
219
+ Returns:
220
+ PangeaResponse which contains the response in its entirety and
221
+ various properties to retrieve individual fields
222
+ """
223
+
224
+ url = self._url(path)
225
+ self.logger.debug(json.dumps({"service": self.service, "action": "get", "url": url}))
226
+
227
+ async with self.session.get(url, headers=self._headers()) as requests_response:
228
+ pangea_response = PangeaResponse(
229
+ requests_response, result_class=result_class, json=await requests_response.json()
230
+ )
231
+
232
+ self.logger.debug(
233
+ json.dumps(
234
+ {"service": self.service, "action": "get", "url": url, "response": pangea_response.json},
235
+ default=default_encoder,
236
+ )
237
+ )
238
+
239
+ if check_response is False:
240
+ return pangea_response
241
+
242
+ return self._check_response(pangea_response)
243
+
244
+ async def poll_result_by_id(
245
+ self, request_id: str, result_class: Union[Type[PangeaResponseResult], dict], check_response: bool = True
246
+ ):
247
+ path = self._get_poll_path(request_id)
248
+ self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_once", "url": path}))
249
+ return await self.get(path, result_class, check_response=check_response)
250
+
251
+ async def poll_result_once(self, response: PangeaResponse, check_response: bool = True):
252
+ request_id = response.request_id
253
+ if not request_id:
254
+ raise pe.PangeaException("Poll result error error: response did not include a 'request_id'")
255
+
256
+ if response.status != ResponseStatus.ACCEPTED.value:
257
+ raise pe.PangeaException("Response already proccesed")
258
+
259
+ return await self.poll_result_by_id(request_id, response.result_class, check_response=check_response)
260
+
261
+ async def _poll_result_retry(self, response: PangeaResponse) -> PangeaResponse:
262
+ retry_count = 1
263
+ start = time.time()
264
+
265
+ while response.status == ResponseStatus.ACCEPTED.value and not self._reach_timeout(start):
266
+ await asyncio.sleep(self._get_delay(retry_count, start))
267
+ response = await self.poll_result_once(response, check_response=False)
268
+ retry_count += 1
269
+
270
+ self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_retry", "step": "exit"}))
271
+ return self._check_response(response)
272
+
273
+ def _init_session(self) -> aiohttp.ClientSession:
274
+ # retry_config = Retry(
275
+ # total=self.config.request_retries,
276
+ # backoff_factor=self.config.request_backoff,
277
+ # status_forcelist=[500, 502, 503, 504],
278
+ # )
279
+ # adapter = HTTPAdapter(max_retries=retry_config)
280
+ # TODO: Add retry config
281
+
282
+ session = aiohttp.ClientSession()
283
+ return session
@@ -1,13 +1,15 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
3
  import datetime
4
- from typing import Any, Dict, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Union
5
5
 
6
+ import pangea.exceptions as pexc
6
7
  from pangea.response import PangeaResponse
7
8
  from pangea.services.audit.audit import AuditBase
8
9
  from pangea.services.audit.exceptions import AuditException
9
10
  from pangea.services.audit.models import (
10
11
  Event,
12
+ LogBulkResult,
11
13
  LogResult,
12
14
  RootRequest,
13
15
  RootResult,
@@ -172,9 +174,86 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
172
174
  print(f"\\t{err.detail} \\n")
173
175
  """
174
176
 
175
- input = self._pre_log_process(event, sign_local=sign_local, verify=verify, verbose=verbose)
177
+ input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
176
178
  response = await self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
177
- return self.handle_log_response(response, verify=verify)
179
+ if response.success:
180
+ self._process_log_result(response.result, verify=verify)
181
+ return response
182
+
183
+ async def log_bulk(
184
+ self,
185
+ events: List[Dict[str, Any]],
186
+ sign_local: bool = False,
187
+ verbose: Optional[bool] = None,
188
+ ) -> PangeaResponse[LogBulkResult]:
189
+ """
190
+ Log an entry
191
+
192
+ Create a log entry in the Secure Audit Log.
193
+ Args:
194
+ events (List[dict[str, Any]]): events to be logged
195
+ verify (bool, optional): True to verify logs consistency after response.
196
+ sign_local (bool, optional): True to sign event with local key.
197
+ verbose (bool, optional): True to get a more verbose response.
198
+ Raises:
199
+ AuditException: If an audit based api exception happens
200
+ PangeaAPIException: If an API Error happens
201
+
202
+ Returns:
203
+ A PangeaResponse where the hash of event data and optional verbose
204
+ results are returned in the response.result field.
205
+ Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#log-an-entry).
206
+
207
+ Examples:
208
+ FIXME:
209
+ """
210
+
211
+ input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
212
+ response = await self.request.post("v2/log", LogBulkResult, data=input.dict(exclude_none=True))
213
+ if response.success:
214
+ for result in response.result.results:
215
+ self._process_log_result(result, verify=True)
216
+ return response
217
+
218
+ async def log_bulk_async(
219
+ self,
220
+ events: List[Dict[str, Any]],
221
+ sign_local: bool = False,
222
+ verbose: Optional[bool] = None,
223
+ ) -> PangeaResponse[LogBulkResult]:
224
+ """
225
+ Log an entry
226
+
227
+ Create a log entry in the Secure Audit Log.
228
+ Args:
229
+ events (List[dict[str, Any]]): events to be logged
230
+ verify (bool, optional): True to verify logs consistency after response.
231
+ sign_local (bool, optional): True to sign event with local key.
232
+ verbose (bool, optional): True to get a more verbose response.
233
+ Raises:
234
+ AuditException: If an audit based api exception happens
235
+ PangeaAPIException: If an API Error happens
236
+
237
+ Returns:
238
+ A PangeaResponse where the hash of event data and optional verbose
239
+ results are returned in the response.result field.
240
+ Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#log-an-entry).
241
+
242
+ Examples:
243
+ FIXME:
244
+ """
245
+
246
+ input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
247
+ try:
248
+ response = await self.request.post(
249
+ "v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
250
+ )
251
+ except pexc.AcceptedRequestException as e:
252
+ return e.response
253
+ if response.success:
254
+ for result in response.result.results:
255
+ self._process_log_result(result, verify=True)
256
+ return response
178
257
 
179
258
  async def search(
180
259
  self,
@@ -5,6 +5,7 @@ from typing import Optional
5
5
 
6
6
  import pangea.services.file_scan as m
7
7
  from pangea.response import PangeaResponse
8
+ from pangea.utils import get_presigned_url_upload_params
8
9
 
9
10
  from .base import ServiceBaseAsync
10
11
 
@@ -79,14 +80,17 @@ class FileScanAsync(ServiceBaseAsync):
79
80
  for err in e.errors:
80
81
  print(f"\\t{err.detail} \\n")
81
82
  """
82
- input = m.FileScanRequest(verbose=verbose, raw=raw, provider=provider)
83
83
 
84
84
  if file or file_path:
85
85
  if file_path:
86
86
  file = open(file_path, "rb")
87
- files = [("upload", ("filename.exe", file, "application/octet-stream"))]
87
+ crc, sha, size, _ = get_presigned_url_upload_params(file)
88
+ files = [("upload", ("filename", file, "application/octet-stream"))]
88
89
  else:
89
90
  raise ValueError("Need to set file_path or file arguments")
90
91
 
92
+ input = m.FileScanRequest(
93
+ verbose=verbose, raw=raw, provider=provider, transfer_crc32c=crc, transfer_sha256=sha, transfer_size=size
94
+ )
91
95
  data = input.dict(exclude_none=True)
92
96
  return await self.request.post("v1/scan", m.FileScanResult, data=data, files=files, poll_result=sync_call)
@@ -1,9 +1,9 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
3
 
4
- from typing import List
4
+ from typing import List, Optional
5
5
 
6
- from pangea.response import ErrorField, PangeaResponse
6
+ from pangea.response import AcceptedResult, ErrorField, PangeaResponse
7
7
 
8
8
 
9
9
  class PangeaException(Exception):
@@ -14,6 +14,14 @@ class PangeaException(Exception):
14
14
  self.message = message
15
15
 
16
16
 
17
+ class PresignedUploadError(PangeaException):
18
+ body: str
19
+
20
+ def __init__(self, message: str, body: str):
21
+ super().__init__(message)
22
+ self.body = body
23
+
24
+
17
25
  class PangeaAPIException(PangeaException):
18
26
  """Exceptions raised during API calls"""
19
27
 
@@ -43,6 +51,14 @@ class PangeaAPIException(PangeaException):
43
51
  return self.__repr__()
44
52
 
45
53
 
54
+ class PresignedURLException(PangeaAPIException):
55
+ cause: Optional[Exception] = None
56
+
57
+ def __init__(self, message: str, response: PangeaResponse, cause: Optional[Exception] = None):
58
+ super().__init__(message, response)
59
+ self.cause = cause
60
+
61
+
46
62
  class ValidationException(PangeaAPIException):
47
63
  """Pangea Validation Errors denoting issues with an API request"""
48
64
 
@@ -103,11 +119,13 @@ class AcceptedRequestException(PangeaAPIException):
103
119
  """Accepted request exception. Async response"""
104
120
 
105
121
  request_id: str
122
+ accepted_result: Optional[AcceptedResult] = None
106
123
 
107
124
  def __init__(self, response: PangeaResponse):
108
125
  message = f"summary: {response.summary}. request_id: {response.request_id}."
109
126
  super().__init__(message, response)
110
127
  self.request_id = response.request_id
128
+ self.accepted_result = AcceptedResult(**response.raw_result) if response.raw_result is not None else None
111
129
 
112
130
 
113
131
  class ServiceNotAvailableException(PangeaAPIException):