volcano-sdk-python 0.13.2__py3-none-any.whl → 0.13.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
volcano_sdk/_session.py CHANGED
@@ -4,8 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import json
7
- from collections.abc import Mapping
8
- from typing import TYPE_CHECKING, cast
7
+ from collections.abc import Callable, Mapping
8
+ from typing import TYPE_CHECKING, TypeGuard
9
9
  from uuid import UUID
10
10
 
11
11
  from .errors import AuthenticationError
@@ -17,6 +17,11 @@ _JWT_PARTS = 3
17
17
  _REFRESH_USER_MISMATCH = "Refreshed session belongs to a different user"
18
18
  _REFRESH_SESSION_MISMATCH = "Refreshed credentials belong to a different server session"
19
19
  _MISSING_SESSION_ID = "Cannot refresh supplied credentials without a session identifier"
20
+ _decode_json: Callable[[str], object] = json.loads
21
+
22
+
23
+ def _is_claim_mapping(value: object) -> TypeGuard[Mapping[object, object]]:
24
+ return isinstance(value, Mapping)
20
25
 
21
26
 
22
27
  def session_id_from_access_token(access_token: str) -> str | None:
@@ -33,15 +38,12 @@ def session_id_from_access_token(access_token: str) -> str | None:
33
38
  return None
34
39
  padding = "=" * (-len(parts[1]) % 4)
35
40
  try:
36
- payload = cast(
37
- "object", json.loads(base64.urlsafe_b64decode(parts[1] + padding).decode())
38
- )
41
+ payload = _decode_json(base64.urlsafe_b64decode(parts[1] + padding).decode())
39
42
  except (ValueError, UnicodeDecodeError, RecursionError):
40
43
  return None
41
- if not isinstance(payload, Mapping):
44
+ if not _is_claim_mapping(payload):
42
45
  return None
43
- values = cast("Mapping[object, object]", payload)
44
- return _normalized_session_id(values.get("session_id"))
46
+ return _normalized_session_id(payload.get("session_id"))
45
47
 
46
48
 
47
49
  def _normalized_session_id(session_id: object) -> str | None:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from concurrent.futures import Future
6
- from threading import Lock
6
+ from threading import Lock, get_ident
7
7
  from typing import TYPE_CHECKING, TypeVar
8
8
 
9
9
  from .errors import SessionChangedError, VolcanoError
@@ -44,6 +44,8 @@ class SessionOperations:
44
44
  self._lock: LockType = Lock()
45
45
  self.refreshing: Future[Session] | None = None
46
46
  self.signing_out: Future[BaseException | None] | None = None
47
+ self._refresh_owner: int | None = None
48
+ self._sign_out_owner: int | None = None
47
49
  self._locally_cleared: bool = False
48
50
  self._verified_pair: tuple[str, str | None] | None = (
49
51
  (verified.access_token, verified.refresh_token)
@@ -91,11 +93,15 @@ class SessionOperations:
91
93
  future = self.refreshing
92
94
  if future is None or future.done():
93
95
  future = self.refreshing = Future()
94
- owner = True
96
+ claimed = future
97
+ self._refresh_owner = get_ident()
95
98
  else:
96
- owner = False
97
- if owner:
98
- self._complete(future, operation)
99
+ claimed = None
100
+ if claimed is None and self._refresh_owner == get_ident() and not future.done():
101
+ message = "Reentrant refresh"
102
+ raise RuntimeError(message)
103
+ if claimed is not None:
104
+ self._complete(claimed, operation)
99
105
  return future.result()
100
106
 
101
107
  def sign_out(
@@ -105,13 +111,21 @@ class SessionOperations:
105
111
  future = self.signing_out
106
112
  if future is None:
107
113
  future = self.signing_out = Future()
108
- owner = True
114
+ claimed = future
115
+ self._sign_out_owner = get_ident()
109
116
  else:
110
- owner = False
117
+ claimed = None
111
118
  preceding = self.refreshing
112
119
  pending = preceding is not None and not preceding.done()
113
- if owner:
114
- self._complete_revocation(future, operation, preceding, pending=pending)
120
+ if (
121
+ claimed is None
122
+ and self._sign_out_owner == get_ident()
123
+ and not future.done()
124
+ ):
125
+ message = "Reentrant sign-out"
126
+ raise RuntimeError(message)
127
+ if claimed is not None:
128
+ self._complete_revocation(claimed, operation, preceding, pending=pending)
115
129
  self._sign_out_result(future)
116
130
 
117
131
  def wait_for_sign_out(self) -> None:
volcano_sdk/_transport.py CHANGED
@@ -11,6 +11,7 @@ from typing import (
11
11
  TYPE_CHECKING,
12
12
  ParamSpec,
13
13
  Protocol,
14
+ TypeGuard,
14
15
  TypeVar,
15
16
  cast,
16
17
  overload,
@@ -288,6 +289,8 @@ HTTP_RATE_LIMITED = 429
288
289
  HTTP_OK = 200
289
290
  HTTP_SERVER_ERROR_MIN = 500
290
291
  HTTP_SERVER_ERROR_MAX = 599
292
+ _RETRY_AFTER_HEADER = "Retry-After"
293
+ _URL_TRAILING_SLASHES = "/"
291
294
  _MALFORMED_USER_PROFILE = "Expected a complete user profile"
292
295
  _MALFORMED_SESSION_PAGE = "Expected a complete session page"
293
296
  _MALFORMED_LINKED_OAUTH_PROVIDERS = "Expected complete linked OAuth providers"
@@ -351,6 +354,17 @@ class _ParsedHTTPResponse(_RawHTTPResponse, Protocol):
351
354
  def parsed(self) -> object: ...
352
355
 
353
356
 
357
+ class _JSONResponse(Protocol):
358
+ def json(self) -> object: ...
359
+
360
+
361
+ class _JSONDecoder(Protocol):
362
+ def __call__(self, document: bytes, /) -> object: ...
363
+
364
+
365
+ _decode_json: _JSONDecoder = json.loads
366
+
367
+
354
368
  @runtime_checkable
355
369
  class _ModelPayload(Protocol):
356
370
  def to_dict(self) -> Mapping[str, object]: ...
@@ -704,7 +718,7 @@ class Transport(Protocol):
704
718
  bucket_name: str,
705
719
  path: str,
706
720
  data: bytes,
707
- content_type: str = "application/octet-stream",
721
+ content_type: str,
708
722
  ) -> TransportResponse: ...
709
723
 
710
724
  def download_storage_object(
@@ -802,14 +816,21 @@ def _required_request_string(kwargs: Mapping[str, object], key: str) -> str:
802
816
  return value
803
817
 
804
818
 
819
+ def _is_object_mapping(value: object) -> TypeGuard[Mapping[object, object]]:
820
+ return isinstance(value, Mapping)
821
+
822
+
823
+ def _is_object_dict(value: object) -> TypeGuard[dict[object, object]]:
824
+ return isinstance(value, dict)
825
+
826
+
805
827
  def _request_headers(kwargs: Mapping[str, object]) -> dict[str, str]:
806
828
  raw_headers = kwargs.get("headers", {})
807
- if not isinstance(raw_headers, Mapping):
829
+ if not _is_object_mapping(raw_headers):
808
830
  field = "headers"
809
831
  raise _InvalidGeneratedRequestError(field)
810
- source = cast("Mapping[object, object]", raw_headers)
811
832
  headers: dict[str, str] = {}
812
- for key, value in source.items():
833
+ for key, value in raw_headers.items():
813
834
  if not isinstance(key, str) or not isinstance(value, str):
814
835
  field = "headers"
815
836
  raise _InvalidGeneratedRequestError(field)
@@ -823,12 +844,11 @@ def _request_params(
823
844
  raw_params = kwargs.get("params")
824
845
  if raw_params is None:
825
846
  return None
826
- if not isinstance(raw_params, Mapping):
847
+ if not _is_object_mapping(raw_params):
827
848
  field = "params"
828
849
  raise _InvalidGeneratedRequestError(field)
829
- source = cast("Mapping[object, object]", raw_params)
830
850
  params: dict[str, str | int | float | bool | None] = {}
831
- for key, value in source.items():
851
+ for key, value in raw_params.items():
832
852
  if not isinstance(key, str) or (
833
853
  value is not None and not isinstance(value, (str, int, float, bool))
834
854
  ):
@@ -853,14 +873,13 @@ def _generated_request(
853
873
  )
854
874
 
855
875
 
856
- def _json_object(response: httpx.Response) -> dict[str, object]:
857
- raw = cast("object", response.json())
858
- if not isinstance(raw, dict):
876
+ def _json_object(response: _JSONResponse) -> dict[str, object]:
877
+ raw = response.json()
878
+ if not _is_object_dict(raw):
859
879
  field = "response body"
860
880
  raise _InvalidGeneratedRequestError(field)
861
- source = cast("Mapping[object, object]", raw)
862
881
  payload: dict[str, object] = {}
863
- for key, value in source.items():
882
+ for key, value in raw.items():
864
883
  if not isinstance(key, str):
865
884
  field = "response body key"
866
885
  raise _InvalidGeneratedRequestError(field)
@@ -873,10 +892,7 @@ def response_payload(response: TransportResponse, expected_status: int) -> objec
873
892
  if status != expected_status:
874
893
  payload: Mapping[object, object]
875
894
  raw_payload = response.payload
876
- if isinstance(raw_payload, dict):
877
- payload = cast("Mapping[object, object]", raw_payload)
878
- else:
879
- payload = {}
895
+ payload = raw_payload if _is_object_dict(raw_payload) else {}
880
896
  message = str(
881
897
  payload.get("error") or payload.get("message") or "Volcano request failed"
882
898
  )
@@ -884,7 +900,7 @@ def response_payload(response: TransportResponse, expected_status: int) -> objec
884
900
  code = str(code_value) if code_value is not None else None
885
901
  retry_after = None
886
902
  if status == HTTP_RATE_LIMITED:
887
- retry_after_value = _header(response.headers, "Retry-After")
903
+ retry_after_value = _header(response.headers, _RETRY_AFTER_HEADER)
888
904
  try:
889
905
  retry_after = (
890
906
  int(retry_after_value) if retry_after_value is not None else None
@@ -908,7 +924,7 @@ class GeneratedTransport:
908
924
  timeout: float = 60.0,
909
925
  httpx_transport: httpx.BaseTransport | None = None,
910
926
  ) -> None:
911
- self._api_url: str = api_url.rstrip("/")
927
+ self._api_url: str = api_url.rstrip(_URL_TRAILING_SLASHES)
912
928
  self._timeout: float = timeout
913
929
  self._httpx_transport: httpx.BaseTransport | None = httpx_transport
914
930
 
@@ -932,7 +948,7 @@ class GeneratedTransport:
932
948
  payload = parsed
933
949
  else:
934
950
  try:
935
- raw = cast("object", json.loads(response.content))
951
+ raw = _decode_json(response.content)
936
952
  payload = raw
937
953
  except (json.JSONDecodeError, UnicodeDecodeError):
938
954
  payload = None
@@ -946,7 +962,7 @@ class GeneratedTransport:
946
962
  @staticmethod
947
963
  def _raw_response(response: _RawHTTPResponse) -> TransportResponse:
948
964
  try:
949
- payload = cast("object", json.loads(response.content))
965
+ payload = _decode_json(response.content)
950
966
  except (json.JSONDecodeError, UnicodeDecodeError):
951
967
  payload = None
952
968
  return _GeneratedTransportResponse(
@@ -1833,14 +1849,8 @@ class GeneratedTransport:
1833
1849
  # invoke contract's { payload } envelope.
1834
1850
  plain_payload = _plain_json(payload)
1835
1851
  with self._client(authorization) as client:
1836
- response = client.get_httpx_client().request(
1837
- method="POST",
1838
- url=invoke_url,
1839
- json={"payload": plain_payload},
1840
- headers={
1841
- "Authorization": f"Bearer {authorization}",
1842
- "Content-Type": "application/json",
1843
- },
1852
+ response = client.get_httpx_client().post(
1853
+ invoke_url, json={"payload": plain_payload}
1844
1854
  )
1845
1855
  return self._raw_response(response)
1846
1856
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: volcano-sdk-python
3
- Version: 0.13.2
3
+ Version: 0.13.3
4
4
  Summary: Official Python SDK for Volcano
5
5
  Project-URL: Homepage, https://volcano.dev
6
6
  Project-URL: Repository, https://github.com/Kong/volcano-sdk-python
@@ -6,9 +6,9 @@ volcano_sdk/_lock_renewer.py,sha256=cgogAmoRM_An-PdymXumuGmxm62MGqOsQU4c_PYlUjg,
6
6
  volcano_sdk/_lock_worker.py,sha256=IJ0TAFyKiLhS40j0LGLUcOnIrdaqCY3_ZVgETtXp0bc,3139
7
7
  volcano_sdk/_log_response.py,sha256=nSVV0Ir1szJftohnBj2hiP6-pf-rqsOv67brlJGdwQ8,3907
8
8
  volcano_sdk/_realtime_fetch_worker.py,sha256=f3mUPe5qZmGuhA9jfpqEUtpLZmvlJ1FhI1XkNz238FI,10406
9
- volcano_sdk/_session.py,sha256=qiuAXTxo5Yz3AM2T_gqPWBD7TmKl2KhvfWwpsmu7h9g,3033
10
- volcano_sdk/_session_operations.py,sha256=MdEWU_SrS56jmYCzyH81jqneb_pbIaJJI8vFb2WZsJo,5807
11
- volcano_sdk/_transport.py,sha256=WbU20NvfH5YPKApZE9bR4zRsCFNXBHTAbaCP8NiHqeY,65315
9
+ volcano_sdk/_session.py,sha256=FiZD11mtOtXSL68WMQQOirxKLRmWk4NIh25zvJxRqek,3124
10
+ volcano_sdk/_session_operations.py,sha256=dS4seAcD-HNYNs2zOXTk66qA9HliqhXyQwEz91nhY-4,6446
11
+ volcano_sdk/_transport.py,sha256=j85miD43LvghpQZOEKJYbia4WbpGKz6a4W2gkzfQgWw,65308
12
12
  volcano_sdk/auth.py,sha256=iwVAnTJV34q3yXWQbgLttD7rIziT3yIl9M3nB3fh-Uk,58253
13
13
  volcano_sdk/client.py,sha256=HYvcsCD5_FGCkNv2Zo38hH9HYk1gEL8Cowy_U_K0rw8,13923
14
14
  volcano_sdk/connection_string.py,sha256=Ba-PrP8oeD1ILG-5zBLz1LPjUDEVoM_7tQwZ5EslB54,2712
@@ -810,7 +810,7 @@ volcano_sdk/_generated/models/usage_data_point.py,sha256=d4U08JbVTiIMM_l2BmRILL1
810
810
  volcano_sdk/_generated/models/variable.py,sha256=t8XaBfTwIH42L8RL4DFDtdqbhVfDEnRS5Axq2q6Tpvk,6405
811
811
  volcano_sdk/_generated/models/variable_deploy_source.py,sha256=G_aCiFHzQx8WhJFs0GvYZEFclfTrfkDvd3kT1e3vty8,463
812
812
  volcano_sdk/_generated/models/variable_status.py,sha256=THSsKBSiAYG4kAtBzGA4TZqxjgx8hdL0Q4qCdZfOj00,391
813
- volcano_sdk_python-0.13.2.dist-info/METADATA,sha256=1h3743OIToGmex6Rb8-s_PAgPuTSJ7e5xrKM718o1Bs,46989
814
- volcano_sdk_python-0.13.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
815
- volcano_sdk_python-0.13.2.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
816
- volcano_sdk_python-0.13.2.dist-info/RECORD,,
813
+ volcano_sdk_python-0.13.3.dist-info/METADATA,sha256=H4a4MssxuRv3OjhTbTgIWsRe3wXKxpyvyCrfN1bpNIc,46989
814
+ volcano_sdk_python-0.13.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
815
+ volcano_sdk_python-0.13.3.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
816
+ volcano_sdk_python-0.13.3.dist-info/RECORD,,