volcano-sdk-python 0.13.2__py3-none-any.whl → 0.13.4__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 +10 -8
- volcano_sdk/_session_operations.py +23 -9
- volcano_sdk/_transport.py +38 -28
- volcano_sdk/auth.py +12 -2
- volcano_sdk/durable.py +123 -24
- volcano_sdk/functions.py +8 -1
- volcano_sdk/realtime.py +22 -13
- {volcano_sdk_python-0.13.2.dist-info → volcano_sdk_python-0.13.4.dist-info}/METADATA +1 -1
- {volcano_sdk_python-0.13.2.dist-info → volcano_sdk_python-0.13.4.dist-info}/RECORD +11 -11
- {volcano_sdk_python-0.13.2.dist-info → volcano_sdk_python-0.13.4.dist-info}/WHEEL +0 -0
- {volcano_sdk_python-0.13.2.dist-info → volcano_sdk_python-0.13.4.dist-info}/licenses/LICENSE +0 -0
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
|
|
volcano_sdk/auth.py
CHANGED
|
@@ -459,12 +459,22 @@ def _oauth_provider_token_status_from_payload(
|
|
|
459
459
|
)
|
|
460
460
|
|
|
461
461
|
|
|
462
|
+
class _OAuthAPIData(Protocol):
|
|
463
|
+
@property
|
|
464
|
+
def data(self) -> object: ...
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _oauth_api_data(payload: _OAuthAPIData) -> object:
|
|
468
|
+
return payload.data
|
|
469
|
+
|
|
470
|
+
|
|
462
471
|
def _oauth_api_data_from_payload(payload: object) -> JSONValue:
|
|
463
472
|
if not isinstance(payload, CallOAuthProviderAPIResponse200):
|
|
464
473
|
raise VolcanoError(_INVALID_OAUTH_API_RESPONSE)
|
|
465
|
-
|
|
474
|
+
data = _oauth_api_data(payload)
|
|
475
|
+
if not _is_json_value(data):
|
|
466
476
|
raise VolcanoError(_INVALID_OAUTH_API_RESPONSE)
|
|
467
|
-
return _freeze_json(
|
|
477
|
+
return _freeze_json(data)
|
|
468
478
|
|
|
469
479
|
|
|
470
480
|
class _SetSession(Protocol):
|
volcano_sdk/durable.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from datetime import datetime
|
|
7
|
-
from typing import Protocol,
|
|
8
|
+
from typing import Protocol, TypeGuard, runtime_checkable
|
|
8
9
|
from uuid import UUID
|
|
9
10
|
|
|
10
11
|
from ._transport import (
|
|
@@ -46,6 +47,15 @@ _UUID_IDENTIFIERS = {
|
|
|
46
47
|
}
|
|
47
48
|
_HTTP_ACCEPTED = 202
|
|
48
49
|
_HTTP_OK = 200
|
|
50
|
+
_EXECUTION_STATUSES: tuple[DurableExecutionStatus, ...] = (
|
|
51
|
+
"pending",
|
|
52
|
+
"running",
|
|
53
|
+
"succeeded",
|
|
54
|
+
"failed",
|
|
55
|
+
"timed_out",
|
|
56
|
+
"stopped",
|
|
57
|
+
"unknown",
|
|
58
|
+
)
|
|
49
59
|
|
|
50
60
|
|
|
51
61
|
class DurableClientContext(Protocol):
|
|
@@ -318,13 +328,105 @@ def _identifier(value: object, field: str) -> str:
|
|
|
318
328
|
|
|
319
329
|
|
|
320
330
|
def _execution_fields(payload: object) -> Mapping[str, object]:
|
|
321
|
-
if not
|
|
331
|
+
if not _is_object_mapping(payload):
|
|
322
332
|
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
323
|
-
values = cast("Mapping[str, object]", payload)
|
|
324
333
|
for required in ("id", "function_id", "name", "status", "region", "created_at"):
|
|
325
|
-
if not isinstance(
|
|
334
|
+
if not isinstance(payload.get(required), str) or not payload[required]:
|
|
326
335
|
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
327
|
-
return
|
|
336
|
+
return payload
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _is_object_mapping(value: object) -> TypeGuard[Mapping[str, object]]:
|
|
340
|
+
return _is_mapping(value) and all(isinstance(key, str) for key in value)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _is_mapping(value: object) -> TypeGuard[Mapping[object, object]]:
|
|
344
|
+
return isinstance(value, Mapping)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _is_sequence(value: object) -> TypeGuard[list[object] | tuple[object, ...]]:
|
|
348
|
+
return isinstance(value, (list, tuple))
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _execution_status(value: object) -> DurableExecutionStatus:
|
|
352
|
+
for status in _EXECUTION_STATUSES:
|
|
353
|
+
if value == status:
|
|
354
|
+
return status
|
|
355
|
+
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _json_result(value: object) -> JSONValue:
|
|
359
|
+
try:
|
|
360
|
+
if _is_json_value(value, set()):
|
|
361
|
+
return value
|
|
362
|
+
except RecursionError as error:
|
|
363
|
+
raise TypeError(_INVALID_EXECUTION_PAYLOAD) from error
|
|
364
|
+
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _is_json_value(value: object, active: set[int]) -> TypeGuard[JSONValue]:
|
|
368
|
+
if _is_json_scalar(value):
|
|
369
|
+
return True
|
|
370
|
+
if _is_mapping(value):
|
|
371
|
+
return _is_json_mapping(value, active)
|
|
372
|
+
if _is_sequence(value):
|
|
373
|
+
return _is_json_sequence(value, active)
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _is_json_scalar(value: object) -> TypeGuard[str | int | float | bool | None]:
|
|
378
|
+
if value is None or isinstance(value, bool):
|
|
379
|
+
return True
|
|
380
|
+
if isinstance(value, str):
|
|
381
|
+
return _is_utf8(value)
|
|
382
|
+
if isinstance(value, int):
|
|
383
|
+
return _is_json_int(value)
|
|
384
|
+
if isinstance(value, float):
|
|
385
|
+
return math.isfinite(value)
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _is_utf8(value: str) -> bool:
|
|
390
|
+
try:
|
|
391
|
+
_ = value.encode()
|
|
392
|
+
except UnicodeEncodeError:
|
|
393
|
+
return False
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_json_int(value: int) -> bool:
|
|
398
|
+
try:
|
|
399
|
+
_ = int.__str__(value)
|
|
400
|
+
except ValueError:
|
|
401
|
+
return False
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _is_json_mapping(value: Mapping[object, object], active: set[int]) -> bool:
|
|
406
|
+
marker = id(value)
|
|
407
|
+
if marker in active:
|
|
408
|
+
return False
|
|
409
|
+
active.add(marker)
|
|
410
|
+
try:
|
|
411
|
+
return all(
|
|
412
|
+
isinstance(key, str) and _is_utf8(key) and _is_json_value(item, active)
|
|
413
|
+
for key, item in value.items()
|
|
414
|
+
)
|
|
415
|
+
finally:
|
|
416
|
+
active.remove(marker)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _is_json_sequence(
|
|
420
|
+
value: list[object] | tuple[object, ...], active: set[int]
|
|
421
|
+
) -> bool:
|
|
422
|
+
marker = id(value)
|
|
423
|
+
if marker in active:
|
|
424
|
+
return False
|
|
425
|
+
active.add(marker)
|
|
426
|
+
try:
|
|
427
|
+
return all(_is_json_value(item, active) for item in value)
|
|
428
|
+
finally:
|
|
429
|
+
active.remove(marker)
|
|
328
430
|
|
|
329
431
|
|
|
330
432
|
def _durable_execution(payload: object) -> DurableExecution:
|
|
@@ -337,10 +439,10 @@ def _durable_execution(payload: object) -> DurableExecution:
|
|
|
337
439
|
id=str(values["id"]),
|
|
338
440
|
function_id=str(values["function_id"]),
|
|
339
441
|
name=str(values["name"]),
|
|
340
|
-
status=
|
|
442
|
+
status=_execution_status(values["status"]),
|
|
341
443
|
region=str(values["region"]),
|
|
342
444
|
created_at=created_at,
|
|
343
|
-
result=
|
|
445
|
+
result=_json_result(values.get("result")),
|
|
344
446
|
result_expired=result_expired,
|
|
345
447
|
error=_durable_error(values.get("error")),
|
|
346
448
|
completed_at=_datetime(values.get("completed_at")),
|
|
@@ -350,11 +452,10 @@ def _durable_execution(payload: object) -> DurableExecution:
|
|
|
350
452
|
def _durable_error(payload: object) -> DurableExecutionFailure | None:
|
|
351
453
|
if payload is None:
|
|
352
454
|
return None
|
|
353
|
-
if not
|
|
455
|
+
if not _is_object_mapping(payload):
|
|
354
456
|
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
message = values.get("message")
|
|
457
|
+
error_type = payload.get("type")
|
|
458
|
+
message = payload.get("message")
|
|
358
459
|
return DurableExecutionFailure(
|
|
359
460
|
type=None if error_type is None else str(error_type),
|
|
360
461
|
message=None if message is None else str(message),
|
|
@@ -362,23 +463,22 @@ def _durable_error(payload: object) -> DurableExecutionFailure | None:
|
|
|
362
463
|
|
|
363
464
|
|
|
364
465
|
def _durable_execution_page(payload: object) -> DurableExecutionPage:
|
|
365
|
-
if not
|
|
466
|
+
if not _is_object_mapping(payload):
|
|
366
467
|
raise TypeError(_INVALID_EXECUTION_PAGE)
|
|
367
|
-
|
|
368
|
-
raw_data = values.get("data")
|
|
468
|
+
raw_data: object = payload.get("data")
|
|
369
469
|
if raw_data is None:
|
|
370
|
-
raw_data = []
|
|
371
|
-
if not
|
|
470
|
+
raw_data = list[object]()
|
|
471
|
+
if not _is_sequence(raw_data):
|
|
372
472
|
raise TypeError(_INVALID_EXECUTION_PAGE)
|
|
373
|
-
data = tuple(
|
|
374
|
-
has_more =
|
|
473
|
+
data = tuple(raw_data)
|
|
474
|
+
has_more = payload.get("has_more", False)
|
|
375
475
|
if not isinstance(has_more, bool):
|
|
376
476
|
raise TypeError(_INVALID_EXECUTION_PAGE)
|
|
377
477
|
return DurableExecutionPage(
|
|
378
478
|
executions=tuple(_durable_execution(entry) for entry in data),
|
|
379
|
-
page=_count(
|
|
380
|
-
limit=_count(
|
|
381
|
-
total=_count(
|
|
479
|
+
page=_count(payload.get("page")),
|
|
480
|
+
limit=_count(payload.get("limit")),
|
|
481
|
+
total=_count(payload.get("total")),
|
|
382
482
|
has_more=has_more,
|
|
383
483
|
)
|
|
384
484
|
|
|
@@ -402,8 +502,7 @@ def _datetime(value: object) -> datetime | None:
|
|
|
402
502
|
def _parse_datetime(value: object) -> datetime:
|
|
403
503
|
if not isinstance(value, str) or not value:
|
|
404
504
|
raise TypeError(_INVALID_EXECUTION_PAYLOAD)
|
|
405
|
-
text = value.replace("Z", "+00:00") if value.endswith("Z") else value
|
|
406
505
|
try:
|
|
407
|
-
return datetime.fromisoformat(
|
|
506
|
+
return datetime.fromisoformat(value)
|
|
408
507
|
except ValueError as error:
|
|
409
508
|
raise TypeError(_INVALID_EXECUTION_PAYLOAD) from error
|
volcano_sdk/functions.py
CHANGED
|
@@ -359,6 +359,13 @@ def _stale_mapping(response: TransportResponse) -> bool:
|
|
|
359
359
|
)
|
|
360
360
|
|
|
361
361
|
|
|
362
|
+
class _JSONLoader(Protocol):
|
|
363
|
+
def loads(self, s: str, /, *, parse_constant: Callable[[str], None]) -> object: ...
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
_JSON_LOADER: _JSONLoader = json
|
|
367
|
+
|
|
368
|
+
|
|
362
369
|
def _function_data(response: TransportResponse) -> JSONValue:
|
|
363
370
|
if not response.content:
|
|
364
371
|
return _json_value(response.payload)
|
|
@@ -369,7 +376,7 @@ def _function_data(response: TransportResponse) -> JSONValue:
|
|
|
369
376
|
is_json = content_type is not None and "application/json" in content_type.lower()
|
|
370
377
|
if is_json or text.startswith(("{", "[")):
|
|
371
378
|
try:
|
|
372
|
-
decoded
|
|
379
|
+
decoded = _JSON_LOADER.loads(text, parse_constant=_reject_json_constant)
|
|
373
380
|
return _json_value(decoded)
|
|
374
381
|
except ValueError:
|
|
375
382
|
pass
|
volcano_sdk/realtime.py
CHANGED
|
@@ -10,7 +10,6 @@ from itertools import count
|
|
|
10
10
|
from types import MappingProxyType
|
|
11
11
|
from typing import (
|
|
12
12
|
TYPE_CHECKING,
|
|
13
|
-
Any,
|
|
14
13
|
Literal,
|
|
15
14
|
Protocol,
|
|
16
15
|
TypeAlias,
|
|
@@ -47,9 +46,11 @@ if TYPE_CHECKING:
|
|
|
47
46
|
_PostgresFetchRequest: TypeAlias = PostgresFetchRequest
|
|
48
47
|
_SubscriptionT = TypeVar("_SubscriptionT")
|
|
49
48
|
_DefaultT = TypeVar("_DefaultT")
|
|
49
|
+
_MessageT = TypeVar("_MessageT")
|
|
50
50
|
|
|
51
|
-
MessageCallback = Callable[[
|
|
52
|
-
RealtimeCallback = Callable[[
|
|
51
|
+
MessageCallback: TypeAlias = Callable[[_MessageT], object]
|
|
52
|
+
RealtimeCallback: TypeAlias = Callable[[_MessageT], object]
|
|
53
|
+
_StoredCallback = Callable[..., object]
|
|
53
54
|
UnsubscribeCallback = Callable[[], None]
|
|
54
55
|
ChannelType: TypeAlias = Literal["broadcast", "presence", "postgres"]
|
|
55
56
|
PostgresEvent: TypeAlias = Literal["INSERT", "UPDATE", "DELETE"]
|
|
@@ -703,7 +704,7 @@ def _presence_info(info: object) -> RealtimePresenceInfo:
|
|
|
703
704
|
|
|
704
705
|
|
|
705
706
|
async def _run_connection_callback(
|
|
706
|
-
callback:
|
|
707
|
+
callback: _StoredCallback,
|
|
707
708
|
context: object,
|
|
708
709
|
) -> None:
|
|
709
710
|
result = callback(context)
|
|
@@ -738,7 +739,7 @@ class Channel:
|
|
|
738
739
|
self._name: str = name
|
|
739
740
|
self._type: ChannelType = channel_type
|
|
740
741
|
self._fetch_config: _PostgresFetchConfig = fetch_config
|
|
741
|
-
self._callbacks: dict[str, list[
|
|
742
|
+
self._callbacks: dict[str, list[_StoredCallback]] = {}
|
|
742
743
|
self._presence_state: dict[str, RealtimePresenceInfo] = {}
|
|
743
744
|
self._presence_events: list[tuple[str, RealtimePresenceInfo]] = []
|
|
744
745
|
self._presence_syncing: bool = False
|
|
@@ -787,7 +788,7 @@ class Channel:
|
|
|
787
788
|
"""Canonical channel name sent to realtime."""
|
|
788
789
|
return self._name
|
|
789
790
|
|
|
790
|
-
def on(self, event: str, callback:
|
|
791
|
+
def on(self, event: str, callback: Callable[[_MessageT], object]) -> Channel:
|
|
791
792
|
"""Register a callback for messages or presence events.
|
|
792
793
|
|
|
793
794
|
Returns
|
|
@@ -852,7 +853,9 @@ class Channel:
|
|
|
852
853
|
|
|
853
854
|
return unsubscribe
|
|
854
855
|
|
|
855
|
-
def on_presence_sync(
|
|
856
|
+
def on_presence_sync(
|
|
857
|
+
self, callback: Callable[[Mapping[str, RealtimePresenceInfo]], object]
|
|
858
|
+
) -> UnsubscribeCallback:
|
|
856
859
|
"""Observe immutable snapshots of a presence channel's current state.
|
|
857
860
|
|
|
858
861
|
Requires a presence channel.
|
|
@@ -1204,7 +1207,7 @@ class Channel:
|
|
|
1204
1207
|
|
|
1205
1208
|
async def _run_callback(
|
|
1206
1209
|
self,
|
|
1207
|
-
callback:
|
|
1210
|
+
callback: _StoredCallback,
|
|
1208
1211
|
delivery: _CallbackDelivery,
|
|
1209
1212
|
) -> None:
|
|
1210
1213
|
if not self._callback_delivery_is_current(delivery):
|
|
@@ -1406,7 +1409,7 @@ class Realtime:
|
|
|
1406
1409
|
self._channels: dict[str, Channel] = {}
|
|
1407
1410
|
self._callback_tasks: set[asyncio.Task[None]] = set()
|
|
1408
1411
|
self._removing_channels: set[str] = set()
|
|
1409
|
-
self._connection_callbacks: dict[str, dict[int,
|
|
1412
|
+
self._connection_callbacks: dict[str, dict[int, _StoredCallback]] = {
|
|
1410
1413
|
"connect": {},
|
|
1411
1414
|
"disconnect": {},
|
|
1412
1415
|
"error": {},
|
|
@@ -1458,7 +1461,9 @@ class Realtime:
|
|
|
1458
1461
|
for request in requests
|
|
1459
1462
|
)
|
|
1460
1463
|
|
|
1461
|
-
def on_connect(
|
|
1464
|
+
def on_connect(
|
|
1465
|
+
self, callback: Callable[[RealtimeConnectContext], object]
|
|
1466
|
+
) -> UnsubscribeCallback:
|
|
1462
1467
|
"""Register a connection callback.
|
|
1463
1468
|
|
|
1464
1469
|
Returns
|
|
@@ -1469,7 +1474,9 @@ class Realtime:
|
|
|
1469
1474
|
"""
|
|
1470
1475
|
return self._register_connection_callback("connect", callback)
|
|
1471
1476
|
|
|
1472
|
-
def on_disconnect(
|
|
1477
|
+
def on_disconnect(
|
|
1478
|
+
self, callback: Callable[[RealtimeDisconnectContext], object]
|
|
1479
|
+
) -> UnsubscribeCallback:
|
|
1473
1480
|
"""Register a disconnection callback.
|
|
1474
1481
|
|
|
1475
1482
|
Returns
|
|
@@ -1480,7 +1487,9 @@ class Realtime:
|
|
|
1480
1487
|
"""
|
|
1481
1488
|
return self._register_connection_callback("disconnect", callback)
|
|
1482
1489
|
|
|
1483
|
-
def on_error(
|
|
1490
|
+
def on_error(
|
|
1491
|
+
self, callback: Callable[[RealtimeErrorContext], object]
|
|
1492
|
+
) -> UnsubscribeCallback:
|
|
1484
1493
|
"""Register a transport-error callback.
|
|
1485
1494
|
|
|
1486
1495
|
Returns
|
|
@@ -1494,7 +1503,7 @@ class Realtime:
|
|
|
1494
1503
|
def _register_connection_callback(
|
|
1495
1504
|
self,
|
|
1496
1505
|
event: str,
|
|
1497
|
-
callback:
|
|
1506
|
+
callback: _StoredCallback,
|
|
1498
1507
|
) -> UnsubscribeCallback:
|
|
1499
1508
|
require_callable(callback, CALLBACK_NOT_CALLABLE)
|
|
1500
1509
|
callback_id = next(self._callback_ids)
|
|
@@ -6,22 +6,22 @@ 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=
|
|
10
|
-
volcano_sdk/_session_operations.py,sha256=
|
|
11
|
-
volcano_sdk/_transport.py,sha256=
|
|
12
|
-
volcano_sdk/auth.py,sha256=
|
|
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
|
+
volcano_sdk/auth.py,sha256=wO34z_fkwGscosLIPR9aQYyd6NsNMkqxk-8wGJQeJSU,58435
|
|
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
|
|
15
15
|
volcano_sdk/database.py,sha256=xZpn5V1zNdft9sgb2W6vO-Sfo61IOJ5bjLS5LpRZ8Sg,13887
|
|
16
|
-
volcano_sdk/durable.py,sha256=
|
|
16
|
+
volcano_sdk/durable.py,sha256=D-bo3BR_u1G-0gUAFWO2NGSClgaTElJVFV7hk29KR0I,16307
|
|
17
17
|
volcano_sdk/durable_authoring.py,sha256=DBB31rQqtJQqL7BtOiaEAsz75FeQ1tarUTG1ctftDN8,37322
|
|
18
18
|
volcano_sdk/errors.py,sha256=ujND4F6E9OWr0-VLpuTdvnSZJPVhNUyHnvXJCDp39V0,1649
|
|
19
|
-
volcano_sdk/functions.py,sha256=
|
|
19
|
+
volcano_sdk/functions.py,sha256=AA-cJ1Pf5W2_Qx5NRKI1dgPg6wGxCuU8llfpOcsdGh4,16950
|
|
20
20
|
volcano_sdk/locks.py,sha256=Zf2nBoolXK-zXeAJYdqbhmfkn3OiNUpX7XmmDhvGCmI,11779
|
|
21
21
|
volcano_sdk/logs.py,sha256=VC2mM2kykvVcnFRd082HAAdVC4gn_2RXwvc998m8rK8,4692
|
|
22
22
|
volcano_sdk/models.py,sha256=ZIo2YdZ-Ba7rCmHBo3eV9LGjpEftVTeVmrrfq-zGujs,11182
|
|
23
23
|
volcano_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
24
|
-
volcano_sdk/realtime.py,sha256
|
|
24
|
+
volcano_sdk/realtime.py,sha256=HySI4IemLEsPCWxEhi_1THFWAtEzOoTwDunIqxbZDdk,67083
|
|
25
25
|
volcano_sdk/storage.py,sha256=5RjF1LNZ_qEBBmBAfWB-NHvtqm2_Hyc7bSeOPt4xImI,33461
|
|
26
26
|
volcano_sdk/_generated/__init__.py,sha256=ZlB8wMYdN6EjpUS5W_lX3jsxq9G1kOcmUr-j3Fccfn8,164
|
|
27
27
|
volcano_sdk/_generated/client.py,sha256=GDzw6T2uRmPlUb1DImpBGxwy0dNbhmBknfvvIQW0GuU,12387
|
|
@@ -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.
|
|
814
|
-
volcano_sdk_python-0.13.
|
|
815
|
-
volcano_sdk_python-0.13.
|
|
816
|
-
volcano_sdk_python-0.13.
|
|
813
|
+
volcano_sdk_python-0.13.4.dist-info/METADATA,sha256=_vQoCoKrlj5CAQ2fzMPXPf6tnP7hkCn98bJsDBDHOpI,46989
|
|
814
|
+
volcano_sdk_python-0.13.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
815
|
+
volcano_sdk_python-0.13.4.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
|
|
816
|
+
volcano_sdk_python-0.13.4.dist-info/RECORD,,
|
|
File without changes
|
{volcano_sdk_python-0.13.2.dist-info → volcano_sdk_python-0.13.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|