pangea-sdk 3.1.0__py3-none-any.whl → 3.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 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}."