pangea-sdk 6.4.0__tar.gz → 6.5.0__tar.gz
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_sdk-6.4.0 → pangea_sdk-6.5.0}/PKG-INFO +17 -18
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/README.md +1 -2
- pangea_sdk-6.5.0/pangea/__init__.py +7 -0
- pangea_sdk-6.5.0/pangea/_constants.py +4 -0
- pangea_sdk-6.5.0/pangea/_typing.py +5 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/__init__.py +2 -1
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/file_uploader.py +3 -2
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/request.py +47 -10
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/__init__.py +19 -2
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/authn.py +25 -8
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/base.py +21 -6
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/file_scan.py +1 -1
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/prompt_guard.py +3 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/config.py +4 -2
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/file_uploader.py +4 -1
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/request.py +72 -12
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/response.py +5 -1
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/__init__.py +19 -2
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/audit.py +2 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/util.py +2 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authn/authn.py +4 -5
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/base.py +3 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/file_scan.py +3 -2
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/prompt_guard.py +3 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/vault.py +3 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pyproject.toml +21 -19
- pangea_sdk-6.4.0/pangea/__init__.py +0 -15
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/ai_guard.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/audit.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/authz.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/embargo.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/intel.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/redact.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/sanitize.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/share.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/vault.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/audit_logger.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/crypto/rsa.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/deep_verify.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/deprecated.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/dump_audit.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/exceptions.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/py.typed +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/ai_guard.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/exceptions.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/models.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/signing.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authn/models.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authz.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/embargo.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/intel.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/redact.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/sanitize.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/share/file_format.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/share/share.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/asymmetric.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/common.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/keys.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/secret.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/symmetric.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/tools.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/utils.py +0 -0
- {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/verify_audit.py +0 -0
@@ -1,24 +1,25 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: pangea-sdk
|
3
|
-
Version: 6.
|
3
|
+
Version: 6.5.0
|
4
4
|
Summary: Pangea API SDK
|
5
|
-
License: MIT
|
6
5
|
Keywords: Pangea,SDK,Audit
|
7
6
|
Author: Glenn Gallien
|
8
|
-
Author-email: glenn.gallien@pangea.cloud
|
9
|
-
|
7
|
+
Author-email: Glenn Gallien <glenn.gallien@pangea.cloud>
|
8
|
+
License-Expression: MIT
|
10
9
|
Classifier: Topic :: Software Development
|
11
10
|
Classifier: Topic :: Software Development :: Libraries
|
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
|
21
|
-
Requires-Dist: yarl
|
11
|
+
Requires-Dist: aiohttp>=3.12.15,<4.0.0
|
12
|
+
Requires-Dist: cryptography>=45.0.6,<46.0.0
|
13
|
+
Requires-Dist: deprecated>=1.2.18,<2.0.0
|
14
|
+
Requires-Dist: google-crc32c>=1.7.1,<2.0.0
|
15
|
+
Requires-Dist: pydantic>=2.11.7,<3.0.0
|
16
|
+
Requires-Dist: python-dateutil>=2.9.0.post0,<3.0.0
|
17
|
+
Requires-Dist: requests>=2.32.4,<3.0.0
|
18
|
+
Requires-Dist: requests-toolbelt>=1.0.0,<2.0.0
|
19
|
+
Requires-Dist: typing-extensions>=4.14.1,<5.0.0
|
20
|
+
Requires-Dist: yarl>=1.20.1,<2.0.0
|
21
|
+
Requires-Python: >=3.9.2, <4.0.0
|
22
|
+
Project-URL: repository, https://github.com/pangeacyber/pangea-python
|
22
23
|
Description-Content-Type: text/markdown
|
23
24
|
|
24
25
|
<a href="https://pangea.cloud?utm_source=github&utm_medium=python-sdk" target="_blank" rel="noopener noreferrer">
|
@@ -116,8 +117,7 @@ The SDK supports the following configuration options via `PangeaConfig`:
|
|
116
117
|
Use `base_url_template` for more control over the URL, such as setting
|
117
118
|
service-specific paths. Defaults to `aws.us.pangea.cloud`.
|
118
119
|
- `request_retries` — Number of retries on the initial request.
|
119
|
-
- `request_backoff` —
|
120
|
-
- `request_timeout` — Timeout used on initial request attempts.
|
120
|
+
- `request_backoff` — A backoff factor to apply between request attempts.
|
121
121
|
- `poll_result_timeout` — Timeout used to poll results after 202 (in secs).
|
122
122
|
- `queued_retry_enabled` — Enable queued request retry support.
|
123
123
|
- `custom_user_agent` — Custom user agent to be used in the request headers.
|
@@ -243,4 +243,3 @@ It accepts multiple file formats:
|
|
243
243
|
[Beta Examples]: https://github.com/pangeacyber/pangea-python/tree/beta/examples
|
244
244
|
[Pangea Console]: https://console.pangea.cloud/
|
245
245
|
[Secure Audit Log]: https://pangea.cloud/docs/audit
|
246
|
-
|
@@ -93,8 +93,7 @@ The SDK supports the following configuration options via `PangeaConfig`:
|
|
93
93
|
Use `base_url_template` for more control over the URL, such as setting
|
94
94
|
service-specific paths. Defaults to `aws.us.pangea.cloud`.
|
95
95
|
- `request_retries` — Number of retries on the initial request.
|
96
|
-
- `request_backoff` —
|
97
|
-
- `request_timeout` — Timeout used on initial request attempts.
|
96
|
+
- `request_backoff` — A backoff factor to apply between request attempts.
|
98
97
|
- `poll_result_timeout` — Timeout used to poll results after 202 (in secs).
|
99
98
|
- `queued_retry_enabled` — Enable queued request retry support.
|
100
99
|
- `custom_user_agent` — Custom user agent to be used in the request headers.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
__version__ = "6.5.0"
|
2
|
+
|
3
|
+
from pangea.config import PangeaConfig
|
4
|
+
from pangea.file_uploader import FileUploader
|
5
|
+
from pangea.response import PangeaResponse, PangeaResponseResult, TransferMethod
|
6
|
+
|
7
|
+
__all__ = ("FileUploader", "PangeaConfig", "PangeaResponse", "PangeaResponseResult", "TransferMethod")
|
@@ -6,9 +6,10 @@ from __future__ import annotations
|
|
6
6
|
import io
|
7
7
|
import logging
|
8
8
|
|
9
|
+
from pangea import PangeaConfig, TransferMethod
|
9
10
|
from pangea.asyncio.request import PangeaRequestAsync
|
10
|
-
|
11
|
-
|
11
|
+
|
12
|
+
__all__ = ("FileUploaderAsync",)
|
12
13
|
|
13
14
|
|
14
15
|
class FileUploaderAsync:
|
@@ -10,6 +10,7 @@ import asyncio
|
|
10
10
|
import json
|
11
11
|
import time
|
12
12
|
from collections.abc import Iterable, Mapping
|
13
|
+
from random import random
|
13
14
|
from typing import Dict, List, Optional, Sequence, Tuple, Type, Union, cast
|
14
15
|
|
15
16
|
import aiohttp
|
@@ -19,10 +20,13 @@ from pydantic_core import to_jsonable_python
|
|
19
20
|
from typing_extensions import Any, TypeAlias, TypeVar, override
|
20
21
|
|
21
22
|
import pangea.exceptions as pe
|
23
|
+
from pangea._constants import MAX_RETRY_DELAY, RETRYABLE_HTTP_CODES
|
22
24
|
from pangea.request import MultipartResponse, PangeaRequestBase
|
23
25
|
from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
|
24
26
|
from pangea.utils import default_encoder
|
25
27
|
|
28
|
+
__all__ = ("PangeaRequestAsync",)
|
29
|
+
|
26
30
|
_FileName: TypeAlias = Union[str, None]
|
27
31
|
_FileContent: TypeAlias = Union[str, bytes]
|
28
32
|
_FileContentType: TypeAlias = str
|
@@ -462,13 +466,46 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
462
466
|
|
463
467
|
@override
|
464
468
|
def _init_session(self) -> aiohttp.ClientSession:
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
469
|
+
return aiohttp.ClientSession(middlewares=[self._retry_middleware])
|
470
|
+
|
471
|
+
def _calculate_retry_timeout(self, remaining_retries: int) -> float:
|
472
|
+
max_retries = self.config.request_retries
|
473
|
+
nb_retries = min(max_retries - remaining_retries, 1000)
|
474
|
+
sleep_seconds = min(self.config.request_backoff * pow(2.0, nb_retries), MAX_RETRY_DELAY)
|
475
|
+
jitter = 1 - 0.25 * random()
|
476
|
+
timeout = sleep_seconds * jitter
|
477
|
+
return max(timeout, 0)
|
478
|
+
|
479
|
+
async def _retry_middleware(
|
480
|
+
self, request: aiohttp.ClientRequest, handler: aiohttp.ClientHandlerType
|
481
|
+
) -> aiohttp.ClientResponse:
|
482
|
+
max_retries = self.config.request_retries
|
483
|
+
request_ids = set[str]()
|
484
|
+
retries_taken = 0
|
485
|
+
for retries_taken in range(max_retries + 1):
|
486
|
+
remaining_retries = max_retries - retries_taken
|
487
|
+
|
488
|
+
if len(request_ids) > 0:
|
489
|
+
request.headers["X-Pangea-Retried-Request-Ids"] = ",".join(request_ids)
|
490
|
+
|
491
|
+
response = await handler(request)
|
492
|
+
|
493
|
+
request_id = response.headers.get("x-request-id")
|
494
|
+
if request_id:
|
495
|
+
request_ids.add(request_id)
|
496
|
+
|
497
|
+
if not response.ok and remaining_retries > 0 and self._should_retry(response):
|
498
|
+
await self._sleep_for_retry(retries_taken=retries_taken, max_retries=max_retries)
|
499
|
+
continue
|
500
|
+
|
501
|
+
break
|
502
|
+
|
503
|
+
return response
|
504
|
+
|
505
|
+
def _should_retry(self, response: aiohttp.ClientResponse) -> bool:
|
506
|
+
return response.status in RETRYABLE_HTTP_CODES
|
507
|
+
|
508
|
+
async def _sleep_for_retry(self, *, retries_taken: int, max_retries: int) -> None:
|
509
|
+
remaining_retries = max_retries - retries_taken
|
510
|
+
timeout = self._calculate_retry_timeout(remaining_retries)
|
511
|
+
await asyncio.sleep(timeout)
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# ruff: noqa: F401
|
2
|
-
|
3
1
|
from .ai_guard import AIGuardAsync
|
4
2
|
from .audit import AuditAsync
|
5
3
|
from .authn import AuthNAsync
|
@@ -12,3 +10,22 @@ from .redact import RedactAsync
|
|
12
10
|
from .sanitize import SanitizeAsync
|
13
11
|
from .share import ShareAsync
|
14
12
|
from .vault import VaultAsync
|
13
|
+
|
14
|
+
__all__ = (
|
15
|
+
"AIGuardAsync",
|
16
|
+
"AuditAsync",
|
17
|
+
"AuthNAsync",
|
18
|
+
"AuthZAsync",
|
19
|
+
"DomainIntelAsync",
|
20
|
+
"EmbargoAsync",
|
21
|
+
"FileIntelAsync",
|
22
|
+
"FileScanAsync",
|
23
|
+
"IpIntelAsync",
|
24
|
+
"PromptGuardAsync",
|
25
|
+
"RedactAsync",
|
26
|
+
"SanitizeAsync",
|
27
|
+
"ShareAsync",
|
28
|
+
"UrlIntelAsync",
|
29
|
+
"UserIntelAsync",
|
30
|
+
"VaultAsync",
|
31
|
+
)
|
@@ -10,9 +10,9 @@ from collections.abc import Mapping
|
|
10
10
|
from typing import Dict, List, Literal, Optional, Union
|
11
11
|
|
12
12
|
import pangea.services.authn.models as m
|
13
|
+
from pangea import PangeaResponse, PangeaResponseResult
|
13
14
|
from pangea.asyncio.services.base import ServiceBaseAsync
|
14
15
|
from pangea.config import PangeaConfig
|
15
|
-
from pangea.response import PangeaResponse, PangeaResponseResult
|
16
16
|
|
17
17
|
__all__ = ["AuthNAsync"]
|
18
18
|
|
@@ -66,11 +66,18 @@ class AuthNAsync(ServiceBaseAsync):
|
|
66
66
|
authn = AuthNAsync(token="pangea_token", config=config)
|
67
67
|
"""
|
68
68
|
super().__init__(token, config, logger_name=logger_name)
|
69
|
-
self.
|
70
|
-
self.flow = AuthNAsync.FlowAsync(token, config, logger_name=logger_name)
|
69
|
+
self.agreements = AuthNAsync.AgreementsAsync(token, config, logger_name=logger_name)
|
71
70
|
self.client = AuthNAsync.ClientAsync(token, config, logger_name=logger_name)
|
71
|
+
self.flow = AuthNAsync.FlowAsync(token, config, logger_name=logger_name)
|
72
72
|
self.session = AuthNAsync.SessionAsync(token, config, logger_name=logger_name)
|
73
|
-
self.
|
73
|
+
self.user = AuthNAsync.UserAsync(token, config, logger_name=logger_name)
|
74
|
+
|
75
|
+
async def close(self) -> None:
|
76
|
+
await self.agreements.close()
|
77
|
+
await self.client.close()
|
78
|
+
await self.flow.close()
|
79
|
+
await self.session.close()
|
80
|
+
await self.user.close()
|
74
81
|
|
75
82
|
class SessionAsync(ServiceBaseAsync):
|
76
83
|
service_name = _SERVICE_NAME
|
@@ -179,10 +186,15 @@ class AuthNAsync(ServiceBaseAsync):
|
|
179
186
|
logger_name: str = "pangea",
|
180
187
|
) -> None:
|
181
188
|
super().__init__(token, config, logger_name=logger_name)
|
182
|
-
self.session = AuthNAsync.ClientAsync.SessionAsync(token, config, logger_name=logger_name)
|
183
189
|
self.password = AuthNAsync.ClientAsync.PasswordAsync(token, config, logger_name=logger_name)
|
190
|
+
self.session = AuthNAsync.ClientAsync.SessionAsync(token, config, logger_name=logger_name)
|
184
191
|
self.token_endpoints = AuthNAsync.ClientAsync.TokenAsync(token, config, logger_name=logger_name)
|
185
192
|
|
193
|
+
async def close(self) -> None:
|
194
|
+
await self.password.close()
|
195
|
+
await self.session.close()
|
196
|
+
await self.token_endpoints.close()
|
197
|
+
|
186
198
|
async def userinfo(self, code: str) -> PangeaResponse[m.ClientUserinfoResult]:
|
187
199
|
"""
|
188
200
|
Get User (client token)
|
@@ -470,9 +482,14 @@ class AuthNAsync(ServiceBaseAsync):
|
|
470
482
|
logger_name: str = "pangea",
|
471
483
|
) -> None:
|
472
484
|
super().__init__(token, config, logger_name=logger_name)
|
473
|
-
self.profile = AuthNAsync.UserAsync.ProfileAsync(token, config, logger_name=logger_name)
|
474
485
|
self.authenticators = AuthNAsync.UserAsync.AuthenticatorsAsync(token, config, logger_name=logger_name)
|
475
486
|
self.invites = AuthNAsync.UserAsync.InvitesAsync(token, config, logger_name=logger_name)
|
487
|
+
self.profile = AuthNAsync.UserAsync.ProfileAsync(token, config, logger_name=logger_name)
|
488
|
+
|
489
|
+
async def close(self) -> None:
|
490
|
+
await self.authenticators.close()
|
491
|
+
await self.invites.close()
|
492
|
+
await self.profile.close()
|
476
493
|
|
477
494
|
async def create(
|
478
495
|
self,
|
@@ -932,7 +949,7 @@ class AuthNAsync(ServiceBaseAsync):
|
|
932
949
|
return await self.request.post(
|
933
950
|
"v2/user/group/assign",
|
934
951
|
data={"id": user_id, "group_ids": group_ids},
|
935
|
-
result_class=
|
952
|
+
result_class=PangeaResponseResult,
|
936
953
|
)
|
937
954
|
|
938
955
|
async def remove(self, user_id: str, group_id: str) -> PangeaResponse[PangeaResponseResult]:
|
@@ -946,7 +963,7 @@ class AuthNAsync(ServiceBaseAsync):
|
|
946
963
|
return await self.request.post(
|
947
964
|
"v2/user/group/remove",
|
948
965
|
data={"id": user_id, "group_id": group_id},
|
949
|
-
result_class=
|
966
|
+
result_class=PangeaResponseResult,
|
950
967
|
)
|
951
968
|
|
952
969
|
async def list(self, user_id: str) -> PangeaResponse[m.GroupList]:
|
@@ -6,14 +6,20 @@
|
|
6
6
|
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
+
from types import TracebackType
|
9
10
|
from typing import Dict, Optional, Type, Union
|
10
11
|
|
11
12
|
from typing_extensions import override
|
12
13
|
|
14
|
+
from pangea import PangeaResponse, PangeaResponseResult
|
15
|
+
from pangea._typing import T
|
13
16
|
from pangea.asyncio.request import PangeaRequestAsync
|
14
17
|
from pangea.exceptions import AcceptedRequestException
|
15
|
-
from pangea.
|
16
|
-
from pangea.
|
18
|
+
from pangea.request import PangeaRequest
|
19
|
+
from pangea.response import AttachedFile
|
20
|
+
from pangea.services.base import ServiceBase
|
21
|
+
|
22
|
+
__all__ = ("ServiceBaseAsync",)
|
17
23
|
|
18
24
|
|
19
25
|
class ServiceBaseAsync(ServiceBase):
|
@@ -85,8 +91,17 @@ class ServiceBaseAsync(ServiceBase):
|
|
85
91
|
return await self.request.download_file(url=url, filename=filename)
|
86
92
|
|
87
93
|
async def close(self):
|
94
|
+
"""Close the underlying aiohttp client."""
|
95
|
+
|
88
96
|
await self.request.session.close()
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
97
|
+
|
98
|
+
async def __aenter__(self: T) -> T:
|
99
|
+
return self
|
100
|
+
|
101
|
+
async def __aexit__(
|
102
|
+
self,
|
103
|
+
exc_type: type[BaseException] | None,
|
104
|
+
exc: BaseException | None,
|
105
|
+
exc_tb: TracebackType | None,
|
106
|
+
) -> None:
|
107
|
+
await self.close()
|
@@ -11,9 +11,9 @@ import logging
|
|
11
11
|
from typing import Dict, List, Optional, Tuple
|
12
12
|
|
13
13
|
import pangea.services.file_scan as m
|
14
|
+
from pangea import PangeaConfig
|
14
15
|
from pangea.asyncio.request import PangeaRequestAsync
|
15
16
|
from pangea.asyncio.services.base import ServiceBaseAsync
|
16
|
-
from pangea.request import PangeaConfig
|
17
17
|
from pangea.response import PangeaResponse, TransferMethod
|
18
18
|
from pangea.utils import FileUploadParams, get_file_upload_params
|
19
19
|
|
@@ -6,6 +6,8 @@ from typing import Any, Optional
|
|
6
6
|
|
7
7
|
from pydantic import BaseModel, model_validator
|
8
8
|
|
9
|
+
__all__ = ("PangeaConfig",)
|
10
|
+
|
9
11
|
|
10
12
|
class PangeaConfig(BaseModel):
|
11
13
|
"""Holds run time configuration information used by SDK components."""
|
@@ -34,12 +36,12 @@ class PangeaConfig(BaseModel):
|
|
34
36
|
|
35
37
|
request_backoff: float = 0.5
|
36
38
|
"""
|
37
|
-
|
39
|
+
A backoff factor to apply between request attempts.
|
38
40
|
"""
|
39
41
|
|
40
42
|
request_timeout: int = 5
|
41
43
|
"""
|
42
|
-
|
44
|
+
Unused.
|
43
45
|
"""
|
44
46
|
|
45
47
|
poll_result_timeout: int = 30
|
@@ -8,9 +8,12 @@ import io
|
|
8
8
|
import logging
|
9
9
|
from typing import Dict, Optional
|
10
10
|
|
11
|
-
from pangea.
|
11
|
+
from pangea.config import PangeaConfig
|
12
|
+
from pangea.request import PangeaRequest
|
12
13
|
from pangea.response import TransferMethod
|
13
14
|
|
15
|
+
__all__ = ("FileUploader",)
|
16
|
+
|
14
17
|
|
15
18
|
class FileUploader:
|
16
19
|
def __init__(self):
|
@@ -11,18 +11,20 @@ import json
|
|
11
11
|
import logging
|
12
12
|
import time
|
13
13
|
from collections.abc import Iterable, Mapping
|
14
|
+
from random import random
|
14
15
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union, cast
|
15
16
|
|
16
17
|
import requests
|
17
18
|
from pydantic import BaseModel
|
18
19
|
from pydantic_core import to_jsonable_python
|
19
|
-
from requests.adapters import HTTPAdapter
|
20
|
+
from requests.adapters import HTTPAdapter
|
20
21
|
from requests_toolbelt import MultipartDecoder # type: ignore[import-untyped]
|
21
22
|
from typing_extensions import TypeAlias, TypeVar, override
|
22
23
|
from yarl import URL
|
23
24
|
|
24
25
|
import pangea
|
25
26
|
import pangea.exceptions as pe
|
27
|
+
from pangea._constants import MAX_RETRY_DELAY, RETRYABLE_HTTP_CODES
|
26
28
|
from pangea.config import PangeaConfig
|
27
29
|
from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
|
28
30
|
from pangea.utils import default_encoder
|
@@ -46,6 +48,70 @@ _Files: TypeAlias = Union[Mapping[str, _FileSpec], Iterable[tuple[str, _FileSpec
|
|
46
48
|
_HeadersUpdateMapping: TypeAlias = Mapping[str, str]
|
47
49
|
|
48
50
|
|
51
|
+
class PangeaHTTPAdapter(HTTPAdapter):
|
52
|
+
"""Custom HTTP adapter that keeps track of retried request IDs."""
|
53
|
+
|
54
|
+
@override
|
55
|
+
def __init__(self, config: PangeaConfig, *args, **kwargs):
|
56
|
+
super().__init__(*args, **kwargs)
|
57
|
+
self.config = config
|
58
|
+
|
59
|
+
@override
|
60
|
+
def send(
|
61
|
+
self,
|
62
|
+
request: requests.PreparedRequest,
|
63
|
+
stream: bool = False,
|
64
|
+
timeout: None | float | tuple[float, float] | tuple[float, None] = None,
|
65
|
+
verify: bool | str = True,
|
66
|
+
cert: None | bytes | str | tuple[bytes | str, bytes | str] = None,
|
67
|
+
proxies: Mapping[str, str] | None = None,
|
68
|
+
) -> requests.Response:
|
69
|
+
max_retries = self.config.request_retries
|
70
|
+
request_ids = set[str]()
|
71
|
+
retries_taken = 0
|
72
|
+
for retries_taken in range(max_retries + 1):
|
73
|
+
remaining_retries = max_retries - retries_taken
|
74
|
+
|
75
|
+
if len(request_ids) > 0:
|
76
|
+
request.headers["X-Pangea-Retried-Request-Ids"] = ",".join(request_ids)
|
77
|
+
|
78
|
+
response = super().send(request, stream, timeout, verify, cert, proxies)
|
79
|
+
|
80
|
+
request_id = response.headers.get("x-request-id")
|
81
|
+
if request_id:
|
82
|
+
request_ids.add(request_id)
|
83
|
+
|
84
|
+
try:
|
85
|
+
response.raise_for_status()
|
86
|
+
except requests.HTTPError as error:
|
87
|
+
if remaining_retries > 0 and self._should_retry(error.response):
|
88
|
+
error.response.close()
|
89
|
+
self._sleep_for_retry(retries_taken=retries_taken, max_retries=max_retries, config=self.config)
|
90
|
+
continue
|
91
|
+
|
92
|
+
break
|
93
|
+
|
94
|
+
break
|
95
|
+
|
96
|
+
return response
|
97
|
+
|
98
|
+
def _calculate_retry_timeout(self, remaining_retries: int, config: PangeaConfig) -> float:
|
99
|
+
max_retries = config.request_retries
|
100
|
+
nb_retries = min(max_retries - remaining_retries, 1000)
|
101
|
+
sleep_seconds = min(config.request_backoff * pow(2.0, nb_retries), MAX_RETRY_DELAY)
|
102
|
+
jitter = 1 - 0.25 * random()
|
103
|
+
timeout = sleep_seconds * jitter
|
104
|
+
return max(timeout, 0)
|
105
|
+
|
106
|
+
def _sleep_for_retry(self, *, retries_taken: int, max_retries: int, config: PangeaConfig) -> None:
|
107
|
+
remaining_retries = max_retries - retries_taken
|
108
|
+
timeout = self._calculate_retry_timeout(remaining_retries, config)
|
109
|
+
time.sleep(timeout)
|
110
|
+
|
111
|
+
def _should_retry(self, response: requests.Response) -> bool:
|
112
|
+
return response.status_code in RETRYABLE_HTTP_CODES
|
113
|
+
|
114
|
+
|
49
115
|
class MultipartResponse:
|
50
116
|
pangea_json: Dict[str, str]
|
51
117
|
attached_files: List = []
|
@@ -111,8 +177,8 @@ class PangeaRequestBase:
|
|
111
177
|
|
112
178
|
return self._queued_retry_enabled
|
113
179
|
|
114
|
-
def _get_delay(self, retry_count, start):
|
115
|
-
delay = retry_count * retry_count
|
180
|
+
def _get_delay(self, retry_count: int, start: float) -> float:
|
181
|
+
delay: float = retry_count * retry_count
|
116
182
|
now = time.time()
|
117
183
|
# if with this delay exceed timeout, reduce delay
|
118
184
|
if now - start + delay >= self.config.poll_result_timeout:
|
@@ -120,10 +186,10 @@ class PangeaRequestBase:
|
|
120
186
|
|
121
187
|
return delay
|
122
188
|
|
123
|
-
def _reach_timeout(self, start):
|
189
|
+
def _reach_timeout(self, start: float) -> bool:
|
124
190
|
return time.time() - start >= self.config.poll_result_timeout
|
125
191
|
|
126
|
-
def _get_poll_path(self, request_id: str):
|
192
|
+
def _get_poll_path(self, request_id: str) -> str:
|
127
193
|
return f"request/{request_id}"
|
128
194
|
|
129
195
|
def _url(self, path: str) -> str:
|
@@ -628,13 +694,7 @@ class PangeaRequest(PangeaRequestBase):
|
|
628
694
|
|
629
695
|
@override
|
630
696
|
def _init_session(self) -> requests.Session:
|
631
|
-
|
632
|
-
total=self.config.request_retries,
|
633
|
-
backoff_factor=self.config.request_backoff,
|
634
|
-
status_forcelist=[500, 502, 503, 504],
|
635
|
-
)
|
636
|
-
|
637
|
-
adapter = HTTPAdapter(max_retries=retry_config)
|
697
|
+
adapter = PangeaHTTPAdapter(config=self.config)
|
638
698
|
session = requests.Session()
|
639
699
|
|
640
700
|
session.mount("http://", adapter)
|
@@ -18,6 +18,8 @@ from typing_extensions import TypeVar
|
|
18
18
|
|
19
19
|
from pangea.utils import format_datetime
|
20
20
|
|
21
|
+
__all__ = ("PangeaResponse", "PangeaResponseResult", "TransferMethod")
|
22
|
+
|
21
23
|
|
22
24
|
class AttachedFile:
|
23
25
|
filename: str
|
@@ -243,7 +245,9 @@ class PangeaResponse(ResponseHeader, Generic[T]):
|
|
243
245
|
|
244
246
|
@property
|
245
247
|
def http_status(self) -> int: # type: ignore[return]
|
246
|
-
|
248
|
+
# Must be an explicit None check because Response's boolean
|
249
|
+
# representation is equal to whether or not the response is OK.
|
250
|
+
if self.raw_response is not None:
|
247
251
|
if isinstance(self.raw_response, aiohttp.ClientResponse):
|
248
252
|
return self.raw_response.status
|
249
253
|
else:
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# ruff: noqa: F401
|
2
|
-
|
3
1
|
from .ai_guard import AIGuard
|
4
2
|
from .audit.audit import Audit
|
5
3
|
from .authn.authn import AuthN
|
@@ -12,3 +10,22 @@ from .redact import Redact
|
|
12
10
|
from .sanitize import Sanitize
|
13
11
|
from .share.share import Share
|
14
12
|
from .vault.vault import Vault
|
13
|
+
|
14
|
+
__all__ = (
|
15
|
+
"AIGuard",
|
16
|
+
"Audit",
|
17
|
+
"AuthN",
|
18
|
+
"AuthZ",
|
19
|
+
"DomainIntel",
|
20
|
+
"Embargo",
|
21
|
+
"FileIntel",
|
22
|
+
"FileScan",
|
23
|
+
"IpIntel",
|
24
|
+
"PromptGuard",
|
25
|
+
"Redact",
|
26
|
+
"Sanitize",
|
27
|
+
"Share",
|
28
|
+
"UrlIntel",
|
29
|
+
"UserIntel",
|
30
|
+
"Vault",
|
31
|
+
)
|
@@ -10,11 +10,10 @@ from collections.abc import Mapping
|
|
10
10
|
from typing import Dict, List, Literal, Optional, Union
|
11
11
|
|
12
12
|
import pangea.services.authn.models as m
|
13
|
-
from pangea
|
14
|
-
from pangea.response import PangeaResponse, PangeaResponseResult
|
13
|
+
from pangea import PangeaConfig, PangeaResponse, PangeaResponseResult
|
15
14
|
from pangea.services.base import ServiceBase
|
16
15
|
|
17
|
-
__all__ =
|
16
|
+
__all__ = ("AuthN",)
|
18
17
|
|
19
18
|
|
20
19
|
_SERVICE_NAME = "authn"
|
@@ -930,7 +929,7 @@ class AuthN(ServiceBase):
|
|
930
929
|
return self.request.post(
|
931
930
|
"v2/user/group/assign",
|
932
931
|
data={"id": user_id, "group_ids": group_ids},
|
933
|
-
result_class=
|
932
|
+
result_class=PangeaResponseResult,
|
934
933
|
)
|
935
934
|
|
936
935
|
def remove(self, user_id: str, group_id: str) -> PangeaResponse[PangeaResponseResult]:
|
@@ -944,7 +943,7 @@ class AuthN(ServiceBase):
|
|
944
943
|
return self.request.post(
|
945
944
|
"v2/user/group/remove",
|
946
945
|
data={"id": user_id, "group_id": group_id},
|
947
|
-
result_class=
|
946
|
+
result_class=PangeaResponseResult,
|
948
947
|
)
|
949
948
|
|
950
949
|
def list(self, user_id: str) -> PangeaResponse[m.GroupList]:
|
@@ -18,6 +18,9 @@ from pangea.exceptions import AcceptedRequestException
|
|
18
18
|
from pangea.request import PangeaRequest
|
19
19
|
from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult
|
20
20
|
|
21
|
+
__all__ = ("ServiceBase",)
|
22
|
+
|
23
|
+
|
21
24
|
TResult = TypeVar("TResult", bound=PangeaResponseResult, default=PangeaResponseResult)
|
22
25
|
|
23
26
|
|
@@ -10,8 +10,9 @@ import io
|
|
10
10
|
import logging
|
11
11
|
from typing import Dict, List, Optional, Tuple
|
12
12
|
|
13
|
-
from pangea
|
14
|
-
from pangea.
|
13
|
+
from pangea import PangeaConfig, PangeaResponse, PangeaResponseResult, TransferMethod
|
14
|
+
from pangea.request import PangeaRequest
|
15
|
+
from pangea.response import APIRequestModel
|
15
16
|
from pangea.services.base import ServiceBase
|
16
17
|
from pangea.utils import FileUploadParams, get_file_upload_params
|
17
18
|
|
@@ -88,6 +88,9 @@ if TYPE_CHECKING:
|
|
88
88
|
from pangea.request import TResult
|
89
89
|
|
90
90
|
|
91
|
+
__all__ = ("ExportEncryptionAlgorithm", "ItemType", "ItemVersionState", "TransformAlphabet", "Vault")
|
92
|
+
|
93
|
+
|
91
94
|
VaultItem = Annotated[
|
92
95
|
Union[AsymmetricKey, SymmetricKey, Secret, ClientSecret, Folder, PangeaToken], Field(discriminator="type")
|
93
96
|
]
|
@@ -1,14 +1,12 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pangea-sdk"
|
3
|
-
version = "6.
|
3
|
+
version = "6.5.0"
|
4
4
|
description = "Pangea API SDK"
|
5
5
|
authors = [
|
6
6
|
{name = "Glenn Gallien", email = "glenn.gallien@pangea.cloud"}
|
7
7
|
]
|
8
8
|
license = "MIT"
|
9
9
|
readme = "README.md"
|
10
|
-
homepage = "https://pangea.cloud/docs/sdk/python/"
|
11
|
-
repository = "https://github.com/pangeacyber/pangea-python/tree/main/packages/pangea-sdk"
|
12
10
|
keywords = ["Pangea", "SDK", "Audit"]
|
13
11
|
classifiers = [
|
14
12
|
"Topic :: Software Development",
|
@@ -16,8 +14,8 @@ classifiers = [
|
|
16
14
|
]
|
17
15
|
requires-python = ">=3.9.2,<4.0.0"
|
18
16
|
dependencies = [
|
19
|
-
"aiohttp (>=3.12.
|
20
|
-
"cryptography (>=45.0.
|
17
|
+
"aiohttp (>=3.12.15,<4.0.0)",
|
18
|
+
"cryptography (>=45.0.6,<46.0.0)",
|
21
19
|
"deprecated (>=1.2.18,<2.0.0)",
|
22
20
|
"google-crc32c (>=1.7.1,<2.0.0)",
|
23
21
|
"pydantic (>=2.11.7,<3.0.0)",
|
@@ -28,24 +26,28 @@ dependencies = [
|
|
28
26
|
"yarl (>=1.20.1,<2.0.0)"
|
29
27
|
]
|
30
28
|
|
31
|
-
[
|
32
|
-
|
33
|
-
|
29
|
+
[project.urls]
|
30
|
+
repository = "https://github.com/pangeacyber/pangea-python"
|
31
|
+
|
32
|
+
[dependency-groups]
|
33
|
+
dev = [
|
34
|
+
"docstring-parser ==0.17.0",
|
35
|
+
"pytest-asyncio ==1.1.0",
|
36
|
+
"pytest_httpserver ==1.1.3",
|
37
|
+
"types-Deprecated ==1.2.15.20250304",
|
38
|
+
"types-python-dateutil ==2.9.0.20250809",
|
39
|
+
"types-requests ==2.32.4.20250809",
|
34
40
|
]
|
35
41
|
|
36
|
-
[
|
37
|
-
|
38
|
-
|
39
|
-
pytest = "8.3.5"
|
40
|
-
pytest-asyncio = "1.1.0"
|
41
|
-
types-Deprecated = "^1.2.9.3"
|
42
|
-
types-python-dateutil = "^2.8.19.14"
|
43
|
-
types-requests = "2.32.4.20250611"
|
42
|
+
[build-system]
|
43
|
+
requires = ["uv_build==0.8.11"]
|
44
|
+
build-backend = "uv_build"
|
44
45
|
|
45
46
|
[tool.mypy]
|
46
47
|
python_version = "3.9"
|
47
48
|
color_output = true
|
48
49
|
error_summary = true
|
50
|
+
implicit_reexport = false
|
49
51
|
pretty = true
|
50
52
|
show_column_numbers = true
|
51
53
|
warn_unused_ignores = true
|
@@ -64,6 +66,6 @@ asyncio_mode = "auto"
|
|
64
66
|
asyncio_default_fixture_loop_scope = "session"
|
65
67
|
filterwarnings = ["error"]
|
66
68
|
|
67
|
-
[build-
|
68
|
-
|
69
|
-
|
69
|
+
[tool.uv.build-backend]
|
70
|
+
module-name = "pangea"
|
71
|
+
module-root = ""
|
@@ -1,15 +0,0 @@
|
|
1
|
-
__version__ = "6.4.0"
|
2
|
-
|
3
|
-
from pangea.asyncio.request import PangeaRequestAsync
|
4
|
-
from pangea.config import PangeaConfig
|
5
|
-
from pangea.file_uploader import FileUploader
|
6
|
-
from pangea.request import PangeaRequest
|
7
|
-
from pangea.response import PangeaResponse
|
8
|
-
|
9
|
-
__all__ = (
|
10
|
-
"FileUploader",
|
11
|
-
"PangeaConfig",
|
12
|
-
"PangeaRequest",
|
13
|
-
"PangeaRequestAsync",
|
14
|
-
"PangeaResponse",
|
15
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|