pangea-sdk 3.7.0__tar.gz → 5.3.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/PKG-INFO +43 -24
  2. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/README.md +31 -5
  3. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/__init__.py +2 -1
  4. pangea_sdk-5.3.0/pangea/asyncio/__init__.py +1 -0
  5. pangea_sdk-5.3.0/pangea/asyncio/file_uploader.py +39 -0
  6. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/request.py +154 -40
  7. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/__init__.py +3 -0
  8. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/audit.py +273 -32
  9. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/authn.py +202 -111
  10. pangea_sdk-5.3.0/pangea/asyncio/services/authz.py +285 -0
  11. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/base.py +29 -7
  12. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/embargo.py +2 -2
  13. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/file_scan.py +25 -11
  14. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/intel.py +109 -36
  15. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/asyncio/services/redact.py +73 -6
  16. pangea_sdk-5.3.0/pangea/asyncio/services/sanitize.py +217 -0
  17. pangea_sdk-5.3.0/pangea/asyncio/services/share.py +733 -0
  18. pangea_sdk-5.3.0/pangea/asyncio/services/vault.py +2240 -0
  19. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/config.py +10 -18
  20. pangea_sdk-5.3.0/pangea/crypto/rsa.py +135 -0
  21. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/deep_verify.py +7 -1
  22. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/dump_audit.py +10 -8
  23. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/exceptions.py +8 -0
  24. pangea_sdk-5.3.0/pangea/file_uploader.py +35 -0
  25. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/request.py +224 -113
  26. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/response.py +96 -31
  27. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/__init__.py +3 -0
  28. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/audit/audit.py +286 -72
  29. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/audit/exceptions.py +1 -2
  30. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/audit/models.py +95 -24
  31. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/audit/signing.py +7 -5
  32. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/audit/util.py +4 -3
  33. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/authn/authn.py +158 -70
  34. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/authn/models.py +173 -17
  35. pangea_sdk-5.3.0/pangea/services/authz.py +400 -0
  36. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/base.py +49 -15
  37. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/embargo.py +3 -4
  38. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/file_scan.py +33 -17
  39. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/intel.py +158 -34
  40. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/services/redact.py +153 -6
  41. pangea_sdk-5.3.0/pangea/services/sanitize.py +388 -0
  42. pangea_sdk-5.3.0/pangea/services/share/file_format.py +170 -0
  43. pangea_sdk-5.3.0/pangea/services/share/share.py +1440 -0
  44. pangea_sdk-5.3.0/pangea/services/vault/models/asymmetric.py +169 -0
  45. pangea_sdk-5.3.0/pangea/services/vault/models/common.py +727 -0
  46. pangea_sdk-5.3.0/pangea/services/vault/models/keys.py +94 -0
  47. pangea_sdk-5.3.0/pangea/services/vault/models/secret.py +48 -0
  48. pangea_sdk-5.3.0/pangea/services/vault/models/symmetric.py +109 -0
  49. pangea_sdk-5.3.0/pangea/services/vault/vault.py +2237 -0
  50. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/tools.py +6 -7
  51. pangea_sdk-5.3.0/pangea/utils.py +195 -0
  52. pangea_sdk-5.3.0/pangea/verify_audit.py +519 -0
  53. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pyproject.toml +38 -17
  54. pangea_sdk-3.7.0/pangea/asyncio/services/vault.py +0 -1281
  55. pangea_sdk-3.7.0/pangea/services/vault/models/asymmetric.py +0 -67
  56. pangea_sdk-3.7.0/pangea/services/vault/models/common.py +0 -429
  57. pangea_sdk-3.7.0/pangea/services/vault/models/secret.py +0 -24
  58. pangea_sdk-3.7.0/pangea/services/vault/models/symmetric.py +0 -63
  59. pangea_sdk-3.7.0/pangea/services/vault/vault.py +0 -1296
  60. pangea_sdk-3.7.0/pangea/utils.py +0 -135
  61. pangea_sdk-3.7.0/pangea/verify_audit.py +0 -332
  62. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/audit_logger.py +0 -0
  63. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/deprecated.py +0 -0
  64. {pangea_sdk-3.7.0 → pangea_sdk-5.3.0}/pangea/py.typed +0 -0
@@ -1,30 +1,23 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pangea-sdk
3
- Version: 3.7.0
3
+ Version: 5.3.0
4
4
  Summary: Pangea API SDK
5
- Home-page: https://pangea.cloud/docs/sdk/python/
6
5
  License: MIT
7
6
  Keywords: Pangea,SDK,Audit
8
7
  Author: Glenn Gallien
9
8
  Author-email: glenn.gallien@pangea.cloud
10
- Requires-Python: >=3.7.2,<4.0.0
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
9
+ Requires-Python: >=3.9
17
10
  Classifier: Topic :: Software Development
18
11
  Classifier: Topic :: Software Development :: Libraries
19
- Requires-Dist: aiohttp (>=3.8.6,<4.0.0)
20
- Requires-Dist: asyncio (>=3.4.3,<4.0.0)
21
- Requires-Dist: cryptography (>=42.0.5,<43.0.0)
22
- Requires-Dist: deprecated (>=1.2.14,<2.0.0)
23
- Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
24
- Requires-Dist: pydantic (>=1.10.14,<2.0.0)
25
- Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
26
- Requires-Dist: requests (>=2.31.0,<3.0.0)
27
- Project-URL: Repository, https://github.com/pangeacyber/pangea-python/tree/main/packages/pangea-sdk
12
+ Requires-Dist: aiohttp
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: deprecated
15
+ Requires-Dist: google-crc32c
16
+ Requires-Dist: pydantic
17
+ Requires-Dist: python-dateutil
18
+ Requires-Dist: requests
19
+ Requires-Dist: requests-toolbelt
20
+ Requires-Dist: typing-extensions
28
21
  Description-Content-Type: text/markdown
29
22
 
30
23
  <a href="https://pangea.cloud?utm_source=github&utm_medium=python-sdk" target="_blank" rel="noopener noreferrer">
@@ -34,15 +27,17 @@ Description-Content-Type: text/markdown
34
27
  <br />
35
28
 
36
29
  [![documentation](https://img.shields.io/badge/documentation-pangea-blue?style=for-the-badge&labelColor=551B76)][Documentation]
37
- [![Slack](https://img.shields.io/badge/Slack-4A154B?style=for-the-badge&logo=slack&logoColor=white)][Slack]
30
+ [![Discourse](https://img.shields.io/badge/Discourse-4A154B?style=for-the-badge&logo=discourse&logoColor=white)][Discourse]
38
31
 
39
32
  # Pangea Python SDK
40
33
 
41
- A Python SDK for integrating with Pangea services. Supports Python v3.7 and
34
+ A Python SDK for integrating with Pangea services. Supports Python v3.9 and
42
35
  above.
43
36
 
44
37
  ## Installation
45
38
 
39
+ #### GA releases
40
+
46
41
  Via pip:
47
42
 
48
43
  ```bash
@@ -55,10 +50,33 @@ Via poetry:
55
50
  $ poetry add pangea-sdk
56
51
  ```
57
52
 
53
+ <a name="beta-releases"></a>
54
+
55
+ #### Beta releases
56
+
57
+ Pre-release versions may be available with the `b` (beta) denotation in the
58
+ version number. These releases serve to preview Beta and Early Access services
59
+ and APIs. Per Semantic Versioning, they are considered unstable and do not carry
60
+ the same compatibility guarantees as stable releases.
61
+ [Beta changelog](https://github.com/pangeacyber/pangea-python/blob/beta/CHANGELOG.md).
62
+
63
+ Via pip:
64
+
65
+ ```bash
66
+ $ pip3 install pangea-sdk==5.2.0b2
67
+ ```
68
+
69
+ Via poetry:
70
+
71
+ ```bash
72
+ $ poetry add pangea-sdk==5.2.0b2
73
+ ```
74
+
58
75
  ## Usage
59
76
 
60
77
  - [Documentation][]
61
- - [Examples][]
78
+ - [GA Examples][]
79
+ - [Beta Examples][]
62
80
 
63
81
  General usage would be to create a token for a service through the
64
82
  [Pangea Console][] and then construct an API client for that respective service.
@@ -202,8 +220,9 @@ It accepts multiple file formats:
202
220
 
203
221
 
204
222
  [Documentation]: https://pangea.cloud/docs/sdk/python/
205
- [Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
223
+ [GA Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
224
+ [Beta Examples]: https://github.com/pangeacyber/pangea-python/tree/beta/examples
206
225
  [Pangea Console]: https://console.pangea.cloud/
207
- [Slack]: https://pangea.cloud/join-slack/
226
+ [Discourse]: https://l.pangea.cloud/Jd4wlGs
208
227
  [Secure Audit Log]: https://pangea.cloud/docs/audit
209
228
 
@@ -5,15 +5,17 @@
5
5
  <br />
6
6
 
7
7
  [![documentation](https://img.shields.io/badge/documentation-pangea-blue?style=for-the-badge&labelColor=551B76)][Documentation]
8
- [![Slack](https://img.shields.io/badge/Slack-4A154B?style=for-the-badge&logo=slack&logoColor=white)][Slack]
8
+ [![Discourse](https://img.shields.io/badge/Discourse-4A154B?style=for-the-badge&logo=discourse&logoColor=white)][Discourse]
9
9
 
10
10
  # Pangea Python SDK
11
11
 
12
- A Python SDK for integrating with Pangea services. Supports Python v3.7 and
12
+ A Python SDK for integrating with Pangea services. Supports Python v3.9 and
13
13
  above.
14
14
 
15
15
  ## Installation
16
16
 
17
+ #### GA releases
18
+
17
19
  Via pip:
18
20
 
19
21
  ```bash
@@ -26,10 +28,33 @@ Via poetry:
26
28
  $ poetry add pangea-sdk
27
29
  ```
28
30
 
31
+ <a name="beta-releases"></a>
32
+
33
+ #### Beta releases
34
+
35
+ Pre-release versions may be available with the `b` (beta) denotation in the
36
+ version number. These releases serve to preview Beta and Early Access services
37
+ and APIs. Per Semantic Versioning, they are considered unstable and do not carry
38
+ the same compatibility guarantees as stable releases.
39
+ [Beta changelog](https://github.com/pangeacyber/pangea-python/blob/beta/CHANGELOG.md).
40
+
41
+ Via pip:
42
+
43
+ ```bash
44
+ $ pip3 install pangea-sdk==5.2.0b2
45
+ ```
46
+
47
+ Via poetry:
48
+
49
+ ```bash
50
+ $ poetry add pangea-sdk==5.2.0b2
51
+ ```
52
+
29
53
  ## Usage
30
54
 
31
55
  - [Documentation][]
32
- - [Examples][]
56
+ - [GA Examples][]
57
+ - [Beta Examples][]
33
58
 
34
59
  General usage would be to create a token for a service through the
35
60
  [Pangea Console][] and then construct an API client for that respective service.
@@ -173,7 +198,8 @@ It accepts multiple file formats:
173
198
 
174
199
 
175
200
  [Documentation]: https://pangea.cloud/docs/sdk/python/
176
- [Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
201
+ [GA Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
202
+ [Beta Examples]: https://github.com/pangeacyber/pangea-python/tree/beta/examples
177
203
  [Pangea Console]: https://console.pangea.cloud/
178
- [Slack]: https://pangea.cloud/join-slack/
204
+ [Discourse]: https://l.pangea.cloud/Jd4wlGs
179
205
  [Secure Audit Log]: https://pangea.cloud/docs/audit
@@ -1,6 +1,7 @@
1
- __version__ = "3.7.0"
1
+ __version__ = "5.3.0"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
5
+ from pangea.file_uploader import FileUploader
5
6
  from pangea.request import PangeaRequest
6
7
  from pangea.response import PangeaResponse
@@ -0,0 +1 @@
1
+ from .file_uploader import FileUploaderAsync
@@ -0,0 +1,39 @@
1
+ # Copyright 2022 Pangea Cyber Corporation
2
+ # Author: Pangea Cyber Corporation
3
+ import io
4
+ import logging
5
+ from typing import Dict, Optional
6
+
7
+ from pangea.asyncio.request import PangeaRequestAsync
8
+ from pangea.request import PangeaConfig
9
+ from pangea.response import TransferMethod
10
+
11
+
12
+ class FileUploaderAsync:
13
+ def __init__(self) -> None:
14
+ self.logger = logging.getLogger("pangea")
15
+ self._request: PangeaRequestAsync = PangeaRequestAsync(
16
+ config=PangeaConfig(),
17
+ token="",
18
+ service="FileUploader",
19
+ logger=self.logger,
20
+ )
21
+
22
+ async def upload_file(
23
+ self,
24
+ url: str,
25
+ file: io.BufferedReader,
26
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
27
+ file_details: Optional[Dict] = None,
28
+ ) -> None:
29
+ if transfer_method == TransferMethod.PUT_URL:
30
+ files = [("file", ("filename", file, "application/octet-stream"))]
31
+ await self._request.put_presigned_url(url=url, files=files)
32
+ elif transfer_method == TransferMethod.POST_URL:
33
+ files = [("file", ("filename", file, "application/octet-stream"))]
34
+ await self._request.post_presigned_url(url=url, data=file_details, files=files) # type: ignore[arg-type]
35
+ else:
36
+ raise ValueError(f"Transfer method not supported: {transfer_method}")
37
+
38
+ async def close(self) -> None:
39
+ await self._request.session.close()
@@ -1,20 +1,24 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
3
4
 
4
5
  import asyncio
5
6
  import json
6
7
  import time
7
- from typing import Dict, List, Optional, Tuple, Type, Union
8
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
8
9
 
9
10
  import aiohttp
10
- import pangea.exceptions as pe
11
11
  from aiohttp import FormData
12
+ from pydantic import BaseModel
13
+ from typing_extensions import Any, TypeVar
12
14
 
13
- # from requests.adapters import HTTPAdapter, Retry
14
- from pangea.request import PangeaRequestBase
15
- from pangea.response import AcceptedResult, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
15
+ import pangea.exceptions as pe
16
+ from pangea.request import MultipartResponse, PangeaRequestBase
17
+ from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
16
18
  from pangea.utils import default_encoder
17
19
 
20
+ TResult = TypeVar("TResult", bound=PangeaResponseResult)
21
+
18
22
 
19
23
  class PangeaRequestAsync(PangeaRequestBase):
20
24
  """An object that makes direct calls to Pangea Service APIs.
@@ -28,12 +32,12 @@ class PangeaRequestAsync(PangeaRequestBase):
28
32
  async def post(
29
33
  self,
30
34
  endpoint: str,
31
- result_class: Type[PangeaResponseResult],
32
- data: Union[str, Dict] = {},
35
+ result_class: Type[TResult],
36
+ data: str | BaseModel | dict[str, Any] | None = None,
33
37
  files: Optional[List[Tuple]] = None,
34
38
  poll_result: bool = True,
35
39
  url: Optional[str] = None,
36
- ) -> PangeaResponse:
40
+ ) -> PangeaResponse[TResult]:
37
41
  """Makes the POST call to a Pangea Service endpoint.
38
42
 
39
43
  Args:
@@ -44,6 +48,13 @@ class PangeaRequestAsync(PangeaRequestBase):
44
48
  PangeaResponse which contains the response in its entirety and
45
49
  various properties to retrieve individual fields
46
50
  """
51
+
52
+ if isinstance(data, BaseModel):
53
+ data = data.model_dump(exclude_none=True)
54
+
55
+ if data is None:
56
+ data = {}
57
+
47
58
  if url is None:
48
59
  url = self._url(endpoint)
49
60
 
@@ -56,7 +67,7 @@ class PangeaRequestAsync(PangeaRequestBase):
56
67
  )
57
68
  transfer_method = data.get("transfer_method", None) # type: ignore[union-attr]
58
69
 
59
- if files is not None and type(data) is dict and (transfer_method == TransferMethod.POST_URL.value):
70
+ if files and type(data) is dict and (transfer_method == TransferMethod.POST_URL.value):
60
71
  requests_response = await self._full_post_presigned_url(
61
72
  endpoint, result_class=result_class, data=data, files=files
62
73
  )
@@ -66,18 +77,32 @@ class PangeaRequestAsync(PangeaRequestBase):
66
77
  )
67
78
 
68
79
  await self._check_http_errors(requests_response)
69
- json_resp = await requests_response.json()
70
- self.logger.debug(json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp}))
71
80
 
72
- pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp) # type: ignore[var-annotated]
81
+ if "multipart/form-data" in requests_response.headers.get("content-type", ""):
82
+ multipart_response = await self._process_multipart_response(requests_response)
83
+ pangea_response: PangeaResponse = PangeaResponse(
84
+ requests_response,
85
+ result_class=result_class,
86
+ json=multipart_response.pangea_json,
87
+ attached_files=multipart_response.attached_files,
88
+ )
89
+ else:
90
+ try:
91
+ json_resp = await requests_response.json()
92
+ self.logger.debug(
93
+ json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp})
94
+ )
95
+
96
+ pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp)
97
+ except aiohttp.ContentTypeError as e:
98
+ raise pe.PangeaException(f"Failed to decode json response. {e}. Body: {await requests_response.text()}")
99
+
73
100
  if poll_result:
74
101
  pangea_response = await self._handle_queued_result(pangea_response)
75
102
 
76
103
  return self._check_response(pangea_response)
77
104
 
78
- async def get(
79
- self, path: str, result_class: Type[PangeaResponseResult], check_response: bool = True
80
- ) -> PangeaResponse:
105
+ async def get(self, path: str, result_class: Type[TResult], check_response: bool = True) -> PangeaResponse[TResult]:
81
106
  """Makes the GET call to a Pangea Service endpoint.
82
107
 
83
108
  Args:
@@ -94,7 +119,7 @@ class PangeaRequestAsync(PangeaRequestBase):
94
119
 
95
120
  async with self.session.get(url, headers=self._headers()) as requests_response:
96
121
  await self._check_http_errors(requests_response)
97
- pangea_response = PangeaResponse( # type: ignore[var-annotated]
122
+ pangea_response = PangeaResponse(
98
123
  requests_response, result_class=result_class, json=await requests_response.json()
99
124
  )
100
125
 
@@ -115,11 +140,11 @@ class PangeaRequestAsync(PangeaRequestBase):
115
140
  raise pe.ServiceTemporarilyUnavailable(await resp.json())
116
141
 
117
142
  async def poll_result_by_id(
118
- self, request_id: str, result_class: Union[Type[PangeaResponseResult], dict], check_response: bool = True
119
- ):
143
+ self, request_id: str, result_class: Type[TResult], check_response: bool = True
144
+ ) -> PangeaResponse[TResult]:
120
145
  path = self._get_poll_path(request_id)
121
146
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_once", "url": path}))
122
- return await self.get(path, result_class, check_response=check_response) # type: ignore[arg-type]
147
+ return await self.get(path, result_class, check_response=check_response)
123
148
 
124
149
  async def poll_result_once(self, response: PangeaResponse, check_response: bool = True):
125
150
  request_id = response.request_id
@@ -144,7 +169,7 @@ class PangeaRequestAsync(PangeaRequestBase):
144
169
  if resp.status < 200 or resp.status >= 300:
145
170
  raise pe.PresignedUploadError(f"presigned POST failure: {resp.status}", await resp.text())
146
171
 
147
- async def put_presigned_url(self, url: str, files: List[Tuple]):
172
+ async def put_presigned_url(self, url: str, files: Sequence[Tuple]):
148
173
  # Send put request with file as body
149
174
  resp = await self._http_put(url=url, files=files)
150
175
  self.logger.debug(
@@ -157,12 +182,97 @@ class PangeaRequestAsync(PangeaRequestBase):
157
182
  if resp.status < 200 or resp.status >= 300:
158
183
  raise pe.PresignedUploadError(f"presigned PUT failure: {resp.status}", await resp.text())
159
184
 
185
+ async def download_file(self, url: str, filename: str | None = None) -> AttachedFile:
186
+ """
187
+ Download file
188
+
189
+ Download a file from the specified URL and save it with the given
190
+ filename.
191
+
192
+ Args:
193
+ url: URL of the file to download
194
+ filename: Name to save the downloaded file as. If not provided, the
195
+ filename will be determined from the Content-Disposition header or
196
+ the URL.
197
+ """
198
+
199
+ self.logger.debug(
200
+ json.dumps(
201
+ {
202
+ "service": self.service,
203
+ "action": "download_file",
204
+ "url": url,
205
+ "filename": filename,
206
+ "status": "start",
207
+ }
208
+ )
209
+ )
210
+ async with self.session.get(url, headers={}) as response:
211
+ if response.status == 200:
212
+ if filename is None:
213
+ content_disposition = response.headers.get("Content-Disposition", "")
214
+ filename = self._get_filename_from_content_disposition(content_disposition)
215
+ if filename is None:
216
+ filename = self._get_filename_from_url(url)
217
+ if filename is None:
218
+ filename = "default_filename"
219
+
220
+ content_type = response.headers.get("Content-Type", "")
221
+ self.logger.debug(
222
+ json.dumps(
223
+ {
224
+ "service": self.service,
225
+ "action": "download_file",
226
+ "url": url,
227
+ "filename": filename,
228
+ "status": "success",
229
+ }
230
+ )
231
+ )
232
+
233
+ return AttachedFile(filename=filename, file=await response.read(), content_type=content_type)
234
+ raise pe.DownloadFileError(f"Failed to download file. Status: {response.status}", await response.text())
235
+
236
+ async def _get_pangea_json(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> Optional[Dict[str, Any]]:
237
+ # Iterate through parts
238
+ async for part in reader:
239
+ if isinstance(part, aiohttp.BodyPartReader):
240
+ return await part.json()
241
+ return None
242
+
243
+ async def _get_attached_files(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> List[AttachedFile]:
244
+ files = []
245
+ i = 0
246
+
247
+ async for part in reader:
248
+ content_type = part.headers.get("Content-Type", "")
249
+ content_disposition = part.headers.get("Content-Disposition", "")
250
+ name = self._get_filename_from_content_disposition(content_disposition)
251
+ if name is None:
252
+ name = f"default_file_name_{i}"
253
+ i += 1
254
+ files.append(AttachedFile(name, await part.read(), content_type)) # type: ignore[union-attr]
255
+
256
+ return files
257
+
258
+ async def _process_multipart_response(self, resp: aiohttp.ClientResponse) -> MultipartResponse:
259
+ # Parse the multipart response
260
+ multipart_reader = aiohttp.MultipartReader.from_response(resp)
261
+
262
+ pangea_json = await self._get_pangea_json(multipart_reader)
263
+ self.logger.debug(
264
+ json.dumps({"service": self.service, "action": "multipart response", "response": pangea_json})
265
+ )
266
+
267
+ attached_files = await self._get_attached_files(multipart_reader)
268
+ return MultipartResponse(pangea_json, attached_files) # type: ignore[arg-type]
269
+
160
270
  async def _http_post(
161
271
  self,
162
272
  url: str,
163
273
  headers: Dict = {},
164
274
  data: Union[str, Dict] = {},
165
- files: Optional[List[Tuple]] = None,
275
+ files: Optional[List[Tuple]] = [],
166
276
  presigned_url_post: bool = False,
167
277
  ) -> aiohttp.ClientResponse:
168
278
  if files:
@@ -187,32 +297,38 @@ class PangeaRequestAsync(PangeaRequestBase):
187
297
  async def _http_put(
188
298
  self,
189
299
  url: str,
190
- files: List[Tuple],
300
+ files: Sequence[Tuple],
191
301
  headers: Dict = {},
192
302
  ) -> aiohttp.ClientResponse:
193
303
  self.logger.debug(
194
304
  json.dumps({"service": self.service, "action": "http_put", "url": url}, default=default_encoder)
195
305
  )
196
- form = FormData()
197
- name, value = files[0]
198
- form.add_field(name, value[1], filename=value[0], content_type=value[2])
199
- return await self.session.put(url, headers=headers, data=form)
306
+ _, value = files[0]
307
+ return await self.session.put(url, headers=headers, data=value[1])
200
308
 
201
309
  async def _full_post_presigned_url(
202
310
  self,
203
311
  endpoint: str,
204
312
  result_class: Type[PangeaResponseResult],
205
313
  data: Union[str, Dict] = {},
206
- files: Optional[List[Tuple]] = None,
314
+ files: List[Tuple] = [],
207
315
  ):
208
- if len(files) == 0: # type: ignore[arg-type]
316
+ if len(files) == 0:
209
317
  raise AttributeError("files attribute should have at least 1 file")
210
318
 
211
319
  response = await self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
212
- data_to_presigned = response.accepted_result.post_form_data # type: ignore[union-attr]
213
- presigned_url = response.accepted_result.post_url # type: ignore[union-attr]
320
+ if response.success: # This should only happen when uploading a zero bytes file
321
+ return response.raw_response
214
322
 
215
- await self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files) # type: ignore[arg-type]
323
+ if response.accepted_result is None:
324
+ raise pe.PangeaException("No accepted_result field when requesting presigned url")
325
+ if response.accepted_result.post_url is None:
326
+ raise pe.PresignedURLException("No presigned url", response)
327
+
328
+ data_to_presigned = response.accepted_result.post_form_data
329
+ presigned_url = response.accepted_result.post_url
330
+
331
+ await self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files)
216
332
  return response.raw_response
217
333
 
218
334
  async def request_presigned_url(
@@ -223,23 +339,22 @@ class PangeaRequestAsync(PangeaRequestBase):
223
339
  ) -> PangeaResponse:
224
340
  # Send request
225
341
  try:
226
- # This should return 202 (AcceptedRequestException)
227
- resp = await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
228
- raise pe.PresignedURLException("Should return 202", resp)
342
+ # This should return 202 (AcceptedRequestException) at least zero size file is sent
343
+ return await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
229
344
  except pe.AcceptedRequestException as e:
230
345
  accepted_exception = e
231
346
  except Exception as e:
232
347
  raise e
233
348
 
234
349
  # Receive 202
235
- return await self._poll_presigned_url(accepted_exception.response) # type: ignore[return-value]
350
+ return await self._poll_presigned_url(accepted_exception.response)
236
351
 
237
- async def _poll_presigned_url(self, response: PangeaResponse) -> AcceptedResult:
352
+ async def _poll_presigned_url(self, response: PangeaResponse[TResult]) -> PangeaResponse[TResult]:
238
353
  if response.http_status != 202:
239
354
  raise AttributeError("Response should be 202")
240
355
 
241
356
  if response.accepted_result is not None and response.accepted_result.has_upload_url:
242
- return response # type: ignore[return-value]
357
+ return response
243
358
 
244
359
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "start"}))
245
360
  retry_count = 1
@@ -276,9 +391,8 @@ class PangeaRequestAsync(PangeaRequestBase):
276
391
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "exit"}))
277
392
 
278
393
  if loop_resp.accepted_result is not None and not loop_resp.accepted_result.has_upload_url:
279
- return loop_resp # type: ignore[return-value]
280
- else:
281
- raise loop_exc
394
+ return loop_resp
395
+ raise loop_exc
282
396
 
283
397
  async def _handle_queued_result(self, response: PangeaResponse) -> PangeaResponse:
284
398
  if self._queued_retry_enabled and response.http_status == 202:
@@ -1,7 +1,10 @@
1
1
  from .audit import AuditAsync
2
2
  from .authn import AuthNAsync
3
+ from .authz import AuthZAsync
3
4
  from .embargo import EmbargoAsync
4
5
  from .file_scan import FileScanAsync
5
6
  from .intel import DomainIntelAsync, FileIntelAsync, IpIntelAsync, UrlIntelAsync, UserIntelAsync
6
7
  from .redact import RedactAsync
8
+ from .sanitize import SanitizeAsync
9
+ from .share import ShareAsync
7
10
  from .vault import VaultAsync