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.
Files changed (63) hide show
  1. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/PKG-INFO +17 -18
  2. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/README.md +1 -2
  3. pangea_sdk-6.5.0/pangea/__init__.py +7 -0
  4. pangea_sdk-6.5.0/pangea/_constants.py +4 -0
  5. pangea_sdk-6.5.0/pangea/_typing.py +5 -0
  6. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/__init__.py +2 -1
  7. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/file_uploader.py +3 -2
  8. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/request.py +47 -10
  9. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/__init__.py +19 -2
  10. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/authn.py +25 -8
  11. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/base.py +21 -6
  12. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/file_scan.py +1 -1
  13. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/prompt_guard.py +3 -0
  14. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/config.py +4 -2
  15. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/file_uploader.py +4 -1
  16. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/request.py +72 -12
  17. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/response.py +5 -1
  18. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/__init__.py +19 -2
  19. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/audit.py +2 -0
  20. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/util.py +2 -0
  21. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authn/authn.py +4 -5
  22. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/base.py +3 -0
  23. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/file_scan.py +3 -2
  24. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/prompt_guard.py +3 -0
  25. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/vault.py +3 -0
  26. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pyproject.toml +21 -19
  27. pangea_sdk-6.4.0/pangea/__init__.py +0 -15
  28. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/ai_guard.py +0 -0
  29. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/audit.py +0 -0
  30. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/authz.py +0 -0
  31. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/embargo.py +0 -0
  32. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/intel.py +0 -0
  33. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/redact.py +0 -0
  34. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/sanitize.py +0 -0
  35. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/share.py +0 -0
  36. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/asyncio/services/vault.py +0 -0
  37. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/audit_logger.py +0 -0
  38. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/crypto/rsa.py +0 -0
  39. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/deep_verify.py +0 -0
  40. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/deprecated.py +0 -0
  41. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/dump_audit.py +0 -0
  42. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/exceptions.py +0 -0
  43. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/py.typed +0 -0
  44. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/ai_guard.py +0 -0
  45. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/exceptions.py +0 -0
  46. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/models.py +0 -0
  47. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/audit/signing.py +0 -0
  48. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authn/models.py +0 -0
  49. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/authz.py +0 -0
  50. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/embargo.py +0 -0
  51. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/intel.py +0 -0
  52. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/redact.py +0 -0
  53. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/sanitize.py +0 -0
  54. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/share/file_format.py +0 -0
  55. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/share/share.py +0 -0
  56. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/asymmetric.py +0 -0
  57. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/common.py +0 -0
  58. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/keys.py +0 -0
  59. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/secret.py +0 -0
  60. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/services/vault/models/symmetric.py +0 -0
  61. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/tools.py +0 -0
  62. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/utils.py +0 -0
  63. {pangea_sdk-6.4.0 → pangea_sdk-6.5.0}/pangea/verify_audit.py +0 -0
@@ -1,24 +1,25 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pangea-sdk
3
- Version: 6.4.0
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
- Requires-Python: >=3.9.2,<4.0.0
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 (>=3.12.14,<4.0.0)
13
- Requires-Dist: cryptography (>=45.0.5,<46.0.0)
14
- Requires-Dist: deprecated (>=1.2.18,<2.0.0)
15
- Requires-Dist: google-crc32c (>=1.7.1,<2.0.0)
16
- Requires-Dist: pydantic (>=2.11.7,<3.0.0)
17
- Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
18
- Requires-Dist: requests (>=2.32.4,<3.0.0)
19
- Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
20
- Requires-Dist: typing-extensions (>=4.14.1,<5.0.0)
21
- Requires-Dist: yarl (>=1.20.1,<2.0.0)
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` — Backoff strategy passed to 'requests'.
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` — Backoff strategy passed to 'requests'.
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")
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ MAX_RETRY_DELAY = 8.0
4
+ RETRYABLE_HTTP_CODES = frozenset({500, 502, 503, 504})
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TypeVar
4
+
5
+ T = TypeVar("T")
@@ -1,2 +1,3 @@
1
- # ruff: noqa: F401
2
1
  from .file_uploader import FileUploaderAsync
2
+
3
+ __all__ = ("FileUploaderAsync",)
@@ -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
- from pangea.request import PangeaConfig
11
- from pangea.response import TransferMethod
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
- # retry_config = Retry(
466
- # total=self.config.request_retries,
467
- # backoff_factor=self.config.request_backoff,
468
- # status_forcelist=[500, 502, 503, 504],
469
- # )
470
- # adapter = HTTPAdapter(max_retries=retry_config)
471
- # TODO: Add retry config
472
-
473
- session = aiohttp.ClientSession()
474
- return session
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.user = AuthNAsync.UserAsync(token, config, logger_name=logger_name)
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.agreements = AuthNAsync.AgreementsAsync(token, config, logger_name=logger_name)
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=m.PangeaResponseResult,
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=m.PangeaResponseResult,
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.response import AttachedFile, PangeaResponse, PangeaResponseResult
16
- from pangea.services.base import PangeaRequest, ServiceBase
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
- # Loop over all attributes to check if they are derived from ServiceBaseAsync and close them
90
- for _, value in self.__dict__.items():
91
- if issubclass(type(value), ServiceBaseAsync):
92
- await value.close()
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
 
@@ -12,6 +12,9 @@ if TYPE_CHECKING:
12
12
  from pangea.response import PangeaResponse
13
13
 
14
14
 
15
+ __all__ = ("Message", "PromptGuardAsync")
16
+
17
+
15
18
  class PromptGuardAsync(ServiceBaseAsync):
16
19
  """Prompt Guard service client.
17
20
 
@@ -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
- Backoff strategy passed to 'requests'
39
+ A backoff factor to apply between request attempts.
38
40
  """
39
41
 
40
42
  request_timeout: int = 5
41
43
  """
42
- Timeout used on initial request attempts
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.request import PangeaConfig, PangeaRequest
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, Retry
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
- retry_config = Retry(
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
- if self.raw_response:
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
+ )
@@ -57,6 +57,8 @@ from pangea.services.audit.util import (
57
57
  from pangea.services.base import ServiceBase
58
58
  from pangea.utils import canonicalize_nested_json
59
59
 
60
+ __all__ = ("Audit", "SearchOutput", "SearchResultOutput")
61
+
60
62
 
61
63
  class AuditBase:
62
64
  def __init__(
@@ -20,6 +20,8 @@ import requests
20
20
  from pangea.services.audit.models import Event, EventEnvelope, PublishedRoot
21
21
  from pangea.utils import default_encoder, format_datetime
22
22
 
23
+ __all__ = ("format_datetime",)
24
+
23
25
  Hash = bytes
24
26
 
25
27
 
@@ -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.config import PangeaConfig
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__ = ["AuthN"]
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=m.PangeaResponseResult,
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=m.PangeaResponseResult,
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.request import PangeaConfig, PangeaRequest
14
- from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult, TransferMethod
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
 
@@ -10,6 +10,9 @@ if TYPE_CHECKING:
10
10
  from collections.abc import Iterable
11
11
 
12
12
 
13
+ __all__ = ("Message", "PromptGuard")
14
+
15
+
13
16
  class Message(APIRequestModel):
14
17
  role: str
15
18
  content: str
@@ -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.4.0"
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.14,<4.0.0)",
20
- "cryptography (>=45.0.5,<46.0.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
- [tool.poetry]
32
- packages = [
33
- { include = "pangea" }
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
- [tool.poetry.group.dev.dependencies]
37
- docstring-parser = "^0.15"
38
- mypy = "1.17.0"
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-system]
68
- requires = ["poetry-core>=2.1.3"]
69
- build-backend = "poetry.core.masonry.api"
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