volcano-sdk-python 0.13.1__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/_function_resolution.py +13 -9
- volcano_sdk/_log_response.py +54 -8
- volcano_sdk/_session.py +10 -8
- volcano_sdk/_session_operations.py +23 -9
- volcano_sdk/_transport.py +38 -28
- volcano_sdk/auth.py +179 -144
- volcano_sdk/client.py +43 -22
- volcano_sdk/database.py +23 -17
- volcano_sdk/functions.py +111 -15
- volcano_sdk/logs.py +14 -4
- volcano_sdk/storage.py +29 -27
- {volcano_sdk_python-0.13.1.dist-info → volcano_sdk_python-0.13.3.dist-info}/METADATA +1 -1
- {volcano_sdk_python-0.13.1.dist-info → volcano_sdk_python-0.13.3.dist-info}/RECORD +15 -15
- {volcano_sdk_python-0.13.1.dist-info → volcano_sdk_python-0.13.3.dist-info}/WHEEL +0 -0
- {volcano_sdk_python-0.13.1.dist-info → volcano_sdk_python-0.13.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,6 +15,8 @@ from dataclasses import dataclass
|
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
from urllib.parse import urlsplit
|
|
17
17
|
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from .errors import NotFoundError
|
|
20
22
|
|
|
@@ -106,28 +108,30 @@ def valid_invoke_url(value: object, api_url: str) -> str | None:
|
|
|
106
108
|
return None
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def _absolute_url_scheme(value: str) -> str:
|
|
110
|
-
"""Return the lowercase scheme of an absolute URL, or
|
|
111
|
+
def _absolute_url_scheme(value: str) -> str | None:
|
|
112
|
+
"""Return the lowercase scheme of an absolute URL, or None when unusable.
|
|
111
113
|
|
|
112
|
-
Unparseable input yields
|
|
114
|
+
Unparseable input yields None rather than raising, so a malformed URL reads
|
|
113
115
|
as unusable to every caller.
|
|
114
116
|
|
|
115
117
|
Returns
|
|
116
118
|
-------
|
|
117
|
-
str
|
|
118
|
-
The lowercase scheme, or
|
|
119
|
+
str or None
|
|
120
|
+
The lowercase scheme, or None for an invalid authority or URL.
|
|
119
121
|
|
|
120
122
|
"""
|
|
121
123
|
if not value or any(character.isspace() for character in value):
|
|
122
|
-
return
|
|
124
|
+
return None
|
|
123
125
|
try:
|
|
124
126
|
parsed = urlsplit(value)
|
|
125
127
|
# Reading the authority is the validation: an unclosed IPv6 literal or
|
|
126
128
|
# a port out of range raises here rather than at request time.
|
|
127
129
|
host, _port = parsed.hostname, parsed.port
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
# urlsplit accepts controls that HTTPX rejects when invoking the URL.
|
|
131
|
+
_ = httpx.URL(value)
|
|
132
|
+
except (ValueError, httpx.InvalidURL):
|
|
133
|
+
return None
|
|
134
|
+
return parsed.scheme.lower() if host else None
|
|
131
135
|
|
|
132
136
|
|
|
133
137
|
def lookup(api_url: str, authorization: str, name: str) -> CachedOutcome | None:
|
volcano_sdk/_log_response.py
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import math
|
|
5
6
|
from collections.abc import Mapping
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, TypeGuard
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .models import JSONValue
|
|
@@ -11,6 +12,38 @@ if TYPE_CHECKING:
|
|
|
11
12
|
INVALID_LOG_RESPONSE = "Expected a complete log response"
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _is_object_mapping(value: object) -> TypeGuard[Mapping[object, object]]:
|
|
16
|
+
return isinstance(value, Mapping)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_object_list(value: object) -> TypeGuard[list[object]]:
|
|
20
|
+
return isinstance(value, list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_object_tuple(value: object) -> TypeGuard[tuple[object, ...]]:
|
|
24
|
+
return isinstance(value, tuple)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_json_scalar(value: object) -> TypeGuard[str | int | float | bool | None]:
|
|
28
|
+
if isinstance(value, float):
|
|
29
|
+
return math.isfinite(value)
|
|
30
|
+
return value is None or isinstance(value, (str, int, bool))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_json_value(value: object) -> TypeGuard[JSONValue]:
|
|
34
|
+
if _is_json_scalar(value):
|
|
35
|
+
return True
|
|
36
|
+
if _is_object_list(value):
|
|
37
|
+
return all(_is_json_value(item) for item in value)
|
|
38
|
+
if _is_object_tuple(value):
|
|
39
|
+
return all(_is_json_value(item) for item in value)
|
|
40
|
+
if _is_object_mapping(value):
|
|
41
|
+
return all(
|
|
42
|
+
isinstance(key, str) and _is_json_value(item) for key, item in value.items()
|
|
43
|
+
)
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
14
47
|
def response_values(payload: object) -> Mapping[str, object]:
|
|
15
48
|
"""Require a JSON object for the response envelope.
|
|
16
49
|
|
|
@@ -21,9 +54,25 @@ def response_values(payload: object) -> Mapping[str, object]:
|
|
|
21
54
|
TypeError: The response envelope is not an object.
|
|
22
55
|
|
|
23
56
|
"""
|
|
24
|
-
if not
|
|
57
|
+
if not _is_object_mapping(payload):
|
|
25
58
|
raise TypeError(INVALID_LOG_RESPONSE)
|
|
26
|
-
|
|
59
|
+
values: dict[str, object] = {}
|
|
60
|
+
for key, value in payload.items():
|
|
61
|
+
if not isinstance(key, str):
|
|
62
|
+
raise TypeError(INVALID_LOG_RESPONSE)
|
|
63
|
+
values[key] = value
|
|
64
|
+
return values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _row_values(item: object) -> Mapping[str, JSONValue]:
|
|
68
|
+
if not _is_object_mapping(item):
|
|
69
|
+
raise TypeError(INVALID_LOG_RESPONSE)
|
|
70
|
+
row: dict[str, JSONValue] = {}
|
|
71
|
+
for key, value in item.items():
|
|
72
|
+
if not isinstance(key, str) or not _is_json_value(value):
|
|
73
|
+
raise TypeError(INVALID_LOG_RESPONSE)
|
|
74
|
+
row[key] = value
|
|
75
|
+
return row
|
|
27
76
|
|
|
28
77
|
|
|
29
78
|
def response_data(values: Mapping[str, object]) -> tuple[Mapping[str, JSONValue], ...]:
|
|
@@ -37,12 +86,9 @@ def response_data(values: Mapping[str, object]) -> tuple[Mapping[str, JSONValue]
|
|
|
37
86
|
|
|
38
87
|
"""
|
|
39
88
|
raw_data = values.get("data")
|
|
40
|
-
if not
|
|
41
|
-
raise TypeError(INVALID_LOG_RESPONSE)
|
|
42
|
-
data = cast("list[object]", raw_data)
|
|
43
|
-
if any(not isinstance(item, Mapping) for item in data):
|
|
89
|
+
if not _is_object_list(raw_data):
|
|
44
90
|
raise TypeError(INVALID_LOG_RESPONSE)
|
|
45
|
-
return tuple(
|
|
91
|
+
return tuple(_row_values(item) for item in raw_data)
|
|
46
92
|
|
|
47
93
|
|
|
48
94
|
def search_metadata(values: Mapping[str, object]) -> tuple[int, bool, str | None]:
|
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,
|
|
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 =
|
|
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
|
|
44
|
+
if not _is_claim_mapping(payload):
|
|
42
45
|
return None
|
|
43
|
-
|
|
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
|
-
|
|
96
|
+
claimed = future
|
|
97
|
+
self._refresh_owner = get_ident()
|
|
95
98
|
else:
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
|
|
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
|
-
|
|
114
|
+
claimed = future
|
|
115
|
+
self._sign_out_owner = get_ident()
|
|
109
116
|
else:
|
|
110
|
-
|
|
117
|
+
claimed = None
|
|
111
118
|
preceding = self.refreshing
|
|
112
119
|
pending = preceding is not None and not preceding.done()
|
|
113
|
-
if
|
|
114
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
857
|
-
raw =
|
|
858
|
-
if not
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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 =
|
|
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().
|
|
1837
|
-
|
|
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
|
|