pangea-sdk 3.1.0__py3-none-any.whl → 3.3.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pangea/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "3.1.0"
1
+ __version__ = "3.3.0"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
pangea/asyncio/request.py CHANGED
@@ -51,12 +51,14 @@ class PangeaRequestAsync(PangeaRequestBase):
51
51
  if self.config_id and data.get("config_id", None) is None:
52
52
  data["config_id"] = self.config_id
53
53
 
54
+ transfer_method = data.get("transfer_method", None)
55
+
54
56
  if (
55
57
  files is not None
56
58
  and type(data) is dict
57
- and data.get("transfer_method", None) == TransferMethod.DIRECT.value
59
+ and (transfer_method == TransferMethod.DIRECT.value or transfer_method == TransferMethod.POST_URL.value)
58
60
  ):
59
- requests_response = await self._post_presigned_url(
61
+ requests_response = await self._full_post_presigned_url(
60
62
  endpoint, result_class=result_class, data=data, files=files
61
63
  )
62
64
  else:
@@ -72,6 +74,83 @@ class PangeaRequestAsync(PangeaRequestBase):
72
74
 
73
75
  return self._check_response(pangea_response)
74
76
 
77
+ async def get(
78
+ self, path: str, result_class: Type[PangeaResponseResult], check_response: bool = True
79
+ ) -> PangeaResponse:
80
+ """Makes the GET call to a Pangea Service endpoint.
81
+
82
+ Args:
83
+ endpoint(str): The Pangea Service API endpoint.
84
+ path(str): Additional URL path
85
+
86
+ Returns:
87
+ PangeaResponse which contains the response in its entirety and
88
+ various properties to retrieve individual fields
89
+ """
90
+
91
+ url = self._url(path)
92
+ self.logger.debug(json.dumps({"service": self.service, "action": "get", "url": url}))
93
+
94
+ async with self.session.get(url, headers=self._headers()) as requests_response:
95
+ pangea_response = PangeaResponse(
96
+ requests_response, result_class=result_class, json=await requests_response.json()
97
+ )
98
+
99
+ self.logger.debug(
100
+ json.dumps(
101
+ {"service": self.service, "action": "get", "url": url, "response": pangea_response.json},
102
+ default=default_encoder,
103
+ )
104
+ )
105
+
106
+ if check_response is False:
107
+ return pangea_response
108
+
109
+ return self._check_response(pangea_response)
110
+
111
+ async def poll_result_by_id(
112
+ self, request_id: str, result_class: Union[Type[PangeaResponseResult], dict], check_response: bool = True
113
+ ):
114
+ path = self._get_poll_path(request_id)
115
+ self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_once", "url": path}))
116
+ return await self.get(path, result_class, check_response=check_response)
117
+
118
+ async def poll_result_once(self, response: PangeaResponse, check_response: bool = True):
119
+ request_id = response.request_id
120
+ if not request_id:
121
+ raise pe.PangeaException("Poll result error error: response did not include a 'request_id'")
122
+
123
+ if response.status != ResponseStatus.ACCEPTED.value:
124
+ raise pe.PangeaException("Response already proccesed")
125
+
126
+ return await self.poll_result_by_id(request_id, response.result_class, check_response=check_response)
127
+
128
+ async def post_presigned_url(self, url: str, data: Dict, files: List[Tuple]):
129
+ # Send form request with file and upload_details as body
130
+ resp = await self._http_post(url=url, data=data, files=files, presigned_url_post=True)
131
+ self.logger.debug(
132
+ json.dumps(
133
+ {"service": self.service, "action": "post presigned", "url": url, "response": resp.text},
134
+ default=default_encoder,
135
+ )
136
+ )
137
+
138
+ if resp.status < 200 or resp.status >= 300:
139
+ raise pe.PresignedUploadError(f"presigned POST failure: {resp.status}", resp.text)
140
+
141
+ async def put_presigned_url(self, url: str, files: List[Tuple]):
142
+ # Send put request with file as body
143
+ resp = await self._http_put(url=url, files=files)
144
+ self.logger.debug(
145
+ json.dumps(
146
+ {"service": self.service, "action": "put presigned", "url": url, "response": resp.text},
147
+ default=default_encoder,
148
+ )
149
+ )
150
+
151
+ if resp.status_code < 200 or resp.status_code >= 300:
152
+ raise pe.PresignedUploadError(f"presigned PUT failure: {resp.status_code}", resp.text)
153
+
75
154
  async def _http_post(
76
155
  self,
77
156
  url: str,
@@ -105,7 +184,21 @@ class PangeaRequestAsync(PangeaRequestBase):
105
184
 
106
185
  return await self.session.post(url, headers=headers, data=data_send)
107
186
 
108
- async def _post_presigned_url(
187
+ async def _http_put(
188
+ self,
189
+ url: str,
190
+ files: List[Tuple],
191
+ headers: Dict = {},
192
+ ) -> aiohttp.ClientResponse:
193
+ self.logger.debug(
194
+ json.dumps({"service": self.service, "action": "http_put", "url": url}, default=default_encoder)
195
+ )
196
+ form = FormData()
197
+ name, value = files[0]
198
+ form.add_field(name, value[1], filename=value[0], content_type=value[2])
199
+ return self.session.put(url, headers=headers, data=form)
200
+
201
+ async def _full_post_presigned_url(
109
202
  self,
110
203
  endpoint: str,
111
204
  result_class: Type[PangeaResponseResult],
@@ -115,61 +208,52 @@ class PangeaRequestAsync(PangeaRequestBase):
115
208
  if len(files) == 0:
116
209
  raise AttributeError("files attribute should have at least 1 file")
117
210
 
211
+ response = await self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
212
+ data_to_presigned = response.accepted_result.accepted_status.upload_details
213
+ presigned_url = response.accepted_result.accepted_status.upload_url
214
+
215
+ await self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files)
216
+ return response.raw_response
217
+
218
+ async def request_presigned_url(
219
+ self,
220
+ endpoint: str,
221
+ result_class: Type[PangeaResponseResult],
222
+ data: Union[str, Dict] = {},
223
+ ) -> PangeaResponse:
118
224
  # Send request
119
225
  try:
120
226
  # This should return 202 (AcceptedRequestException)
121
227
  resp = await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
122
228
  raise pe.PresignedURLException("Should return 202", resp)
123
-
124
229
  except pe.AcceptedRequestException as e:
125
230
  accepted_exception = e
126
231
  except Exception as e:
127
232
  raise e
128
233
 
129
234
  # 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
235
+ return await self._poll_presigned_url(accepted_exception.response)
152
236
 
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")
237
+ async def _poll_presigned_url(self, response: PangeaResponse) -> AcceptedResult:
238
+ if response.http_status != 202:
239
+ raise AttributeError("Response should be 202")
156
240
 
157
- if initial_exc.accepted_result.accepted_status.upload_url:
158
- return initial_exc.accepted_result
241
+ if response.accepted_result.accepted_status.upload_url:
242
+ return response
159
243
 
160
244
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "start"}))
161
245
  retry_count = 1
162
246
  start = time.time()
163
- loop_exc = initial_exc
247
+ loop_resp = response
164
248
 
165
249
  while (
166
- loop_exc.accepted_result is not None
167
- and not loop_exc.accepted_result.accepted_status.upload_url
250
+ loop_resp.accepted_result is not None
251
+ and not loop_resp.accepted_result.accepted_status.upload_url
168
252
  and not self._reach_timeout(start)
169
253
  ):
170
254
  await asyncio.sleep(self._get_delay(retry_count, start))
171
255
  try:
172
- await self.poll_result_once(initial_exc.response, check_response=False)
256
+ await self.poll_result_once(response, check_response=False)
173
257
  msg = "Polling presigned url return 200 instead of 202"
174
258
  self.logger.debug(
175
259
  json.dumps(
@@ -179,6 +263,7 @@ class PangeaRequestAsync(PangeaRequestBase):
179
263
  raise pe.PangeaException(msg)
180
264
  except pe.AcceptedRequestException as e:
181
265
  retry_count += 1
266
+ loop_resp = e.response
182
267
  loop_exc = e
183
268
  except Exception as e:
184
269
  self.logger.debug(
@@ -190,8 +275,8 @@ class PangeaRequestAsync(PangeaRequestBase):
190
275
 
191
276
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "exit"}))
192
277
 
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
278
+ if loop_resp.accepted_result is not None and not loop_resp.accepted_result.accepted_status.upload_url:
279
+ return loop_resp
195
280
  else:
196
281
  raise loop_exc
197
282
 
@@ -207,57 +292,6 @@ class PangeaRequestAsync(PangeaRequestBase):
207
292
 
208
293
  return response
209
294
 
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
295
  async def _poll_result_retry(self, response: PangeaResponse) -> PangeaResponse:
262
296
  retry_count = 1
263
297
  start = time.time()
@@ -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,
@@ -1,9 +1,11 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
3
 
4
+ from typing import Optional, Type, Union
5
+
4
6
  from pangea.asyncio.request import PangeaRequestAsync
5
7
  from pangea.exceptions import AcceptedRequestException
6
- from pangea.response import PangeaResponse
8
+ from pangea.response import PangeaResponse, PangeaResponseResult
7
9
  from pangea.services.base import ServiceBase
8
10
 
9
11
 
@@ -21,7 +23,13 @@ class ServiceBaseAsync(ServiceBase):
21
23
 
22
24
  return self._request
23
25
 
24
- async def poll_result(self, exception: AcceptedRequestException) -> PangeaResponse:
26
+ async def poll_result(
27
+ self,
28
+ exception: Optional[AcceptedRequestException] = None,
29
+ response: Optional[PangeaResponse] = None,
30
+ request_id: Optional[str] = None,
31
+ result_class: Union[Type[PangeaResponseResult], dict] = dict,
32
+ ) -> PangeaResponse:
25
33
  """
26
34
  Poll result
27
35
 
@@ -39,7 +47,16 @@ class ServiceBaseAsync(ServiceBase):
39
47
  Examples:
40
48
  response = service.poll_result(exception)
41
49
  """
42
- return await self.request.poll_result_once(exception.response, check_response=True)
50
+ if exception is not None:
51
+ return await self.request.poll_result_once(exception.response, check_response=True)
52
+ elif response is not None:
53
+ return await self.request.poll_result_once(response, check_response=True)
54
+ elif request_id is not None:
55
+ return await self.request.poll_result_by_id(
56
+ request_id=request_id, result_class=result_class, check_response=True
57
+ )
58
+ else:
59
+ raise AttributeError("Need to set exception, response or request_id")
43
60
 
44
61
  async def close(self):
45
62
  await self.request.session.close()
@@ -1,11 +1,14 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
3
  import io
4
- from typing import Optional
4
+ import logging
5
+ from typing import Dict, Optional
5
6
 
6
7
  import pangea.services.file_scan as m
7
- from pangea.response import PangeaResponse
8
- from pangea.utils import get_presigned_url_upload_params
8
+ from pangea.asyncio.request import PangeaRequestAsync
9
+ from pangea.request import PangeaConfig
10
+ from pangea.response import PangeaResponse, TransferMethod
11
+ from pangea.utils import FileUploadParams, get_file_upload_params
9
12
 
10
13
  from .base import ServiceBaseAsync
11
14
 
@@ -46,6 +49,7 @@ class FileScanAsync(ServiceBaseAsync):
46
49
  raw: Optional[bool] = None,
47
50
  provider: Optional[str] = None,
48
51
  sync_call: bool = True,
52
+ transfer_method: TransferMethod = TransferMethod.DIRECT,
49
53
  ) -> PangeaResponse[m.FileScanResult]:
50
54
  """
51
55
  Scan
@@ -84,13 +88,79 @@ class FileScanAsync(ServiceBaseAsync):
84
88
  if file or file_path:
85
89
  if file_path:
86
90
  file = open(file_path, "rb")
87
- crc, sha, size, _ = get_presigned_url_upload_params(file)
91
+ if transfer_method == TransferMethod.DIRECT or transfer_method == TransferMethod.POST_URL:
92
+ params = get_file_upload_params(file)
93
+ crc = params.crc_hex
94
+ sha = params.sha256_hex
95
+ size = params.size
96
+ else:
97
+ crc, sha, size = None, None, None
88
98
  files = [("upload", ("filename", file, "application/octet-stream"))]
89
99
  else:
90
100
  raise ValueError("Need to set file_path or file arguments")
91
101
 
92
102
  input = m.FileScanRequest(
93
- verbose=verbose, raw=raw, provider=provider, transfer_crc32c=crc, transfer_sha256=sha, transfer_size=size
103
+ verbose=verbose,
104
+ raw=raw,
105
+ provider=provider,
106
+ transfer_crc32c=crc,
107
+ transfer_sha256=sha,
108
+ transfer_size=size,
109
+ transfer_method=transfer_method,
94
110
  )
95
111
  data = input.dict(exclude_none=True)
96
112
  return await self.request.post("v1/scan", m.FileScanResult, data=data, files=files, poll_result=sync_call)
113
+
114
+ async def request_upload_url(
115
+ self,
116
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
117
+ params: Optional[FileUploadParams] = None,
118
+ verbose: Optional[bool] = None,
119
+ raw: Optional[bool] = None,
120
+ provider: Optional[str] = None,
121
+ ) -> PangeaResponse[m.FileScanResult]:
122
+ input = m.FileScanRequest(
123
+ verbose=verbose,
124
+ raw=raw,
125
+ provider=provider,
126
+ transfer_method=transfer_method,
127
+ )
128
+ if params is not None and (
129
+ transfer_method == TransferMethod.POST_URL or transfer_method == TransferMethod.DIRECT
130
+ ):
131
+ input.transfer_crc32c = params.crc_hex
132
+ input.transfer_sha256 = params.sha256_hex
133
+ input.transfer_size = params.size
134
+
135
+ data = input.dict(exclude_none=True)
136
+ return await self.request.request_presigned_url("v1/scan", m.FileScanResult, data=data)
137
+
138
+
139
+ class FileUploaderAsync:
140
+ def __init__(self):
141
+ self.logger = logging.getLogger("pangea")
142
+ self._request: PangeaRequestAsync = PangeaRequestAsync(
143
+ config=PangeaConfig(),
144
+ token="",
145
+ service="FileScanUploader",
146
+ logger=self.logger,
147
+ )
148
+
149
+ async def upload_file(
150
+ self,
151
+ url: str,
152
+ file: io.BufferedReader,
153
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
154
+ file_details: Optional[Dict] = None,
155
+ ):
156
+ if transfer_method == TransferMethod.PUT_URL:
157
+ files = [("file", ("filename", file, "application/octet-stream"))]
158
+ await self._request.put_presigned_url(url=url, files=files)
159
+ elif transfer_method == TransferMethod.POST_URL or transfer_method == TransferMethod.DIRECT:
160
+ files = [("file", ("filename", file, "application/octet-stream"))]
161
+ await self._request.post_presigned_url(url=url, data=file_details, files=files)
162
+ else:
163
+ raise ValueError(f"Transfer method not supported: {transfer_method}")
164
+
165
+ async def close(self):
166
+ await self._request.session.close()
@@ -36,13 +36,8 @@ class RedactAsync(ServiceBaseAsync):
36
36
 
37
37
  service_name = "redact"
38
38
 
39
- def __init__(
40
- self,
41
- token,
42
- config=None,
43
- logger_name="pangea",
44
- ):
45
- super().__init__(token, config, logger_name)
39
+ def __init__(self, token, config=None, logger_name="pangea", config_id: Optional[str] = None):
40
+ super().__init__(token, config, logger_name, config_id=config_id)
46
41
 
47
42
  async def redact(
48
43
  self,
pangea/config.py CHANGED
@@ -10,7 +10,7 @@ class PangeaConfig:
10
10
  """Holds run time configuration information used by SDK components."""
11
11
 
12
12
  """
13
- Used to set pangea domain (and port if needed), it should not include service subdomain
13
+ Used to set Pangea domain (and port if needed), it should not include service subdomain
14
14
  just for particular use cases when environment = "local", domain could be set to an url including:
15
15
  scheme (http:// or https://), subdomain, domain and port.
16
16
 
@@ -19,7 +19,7 @@ class PangeaConfig:
19
19
 
20
20
  """
21
21
  Used to generate service url.
22
- It should be only 'production' or 'local' in case of particular services that can run locally as Redact
22
+ It should be only 'production' or 'local' in cases of particular services that can run locally as Redact.
23
23
 
24
24
  """
25
25
  environment: str = "production"
pangea/exceptions.py CHANGED
@@ -119,7 +119,7 @@ class AcceptedRequestException(PangeaAPIException):
119
119
  """Accepted request exception. Async response"""
120
120
 
121
121
  request_id: str
122
- accepted_result: Optional[AcceptedResult]
122
+ accepted_result: Optional[AcceptedResult] = None
123
123
 
124
124
  def __init__(self, response: PangeaResponse):
125
125
  message = f"summary: {response.summary}. request_id: {response.request_id}."