volcano-sdk-python 0.13.3__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/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
- if not _is_json_value(payload.data):
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(payload.data)
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
- from collections.abc import Mapping, Sequence
5
+ import math
6
+ from collections.abc import Mapping
6
7
  from datetime import datetime
7
- from typing import Protocol, cast, runtime_checkable
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 isinstance(payload, Mapping):
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(values.get(required), str) or not values[required]:
334
+ if not isinstance(payload.get(required), str) or not payload[required]:
326
335
  raise TypeError(_INVALID_EXECUTION_PAYLOAD)
327
- return values
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=cast("DurableExecutionStatus", str(values["status"])),
442
+ status=_execution_status(values["status"]),
341
443
  region=str(values["region"]),
342
444
  created_at=created_at,
343
- result=cast("JSONValue", values.get("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 isinstance(payload, Mapping):
455
+ if not _is_object_mapping(payload):
354
456
  raise TypeError(_INVALID_EXECUTION_PAYLOAD)
355
- values = cast("Mapping[str, object]", payload)
356
- error_type = values.get("type")
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 isinstance(payload, Mapping):
466
+ if not _is_object_mapping(payload):
366
467
  raise TypeError(_INVALID_EXECUTION_PAGE)
367
- values = cast("Mapping[str, object]", payload)
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 isinstance(raw_data, (list, tuple)):
470
+ raw_data = list[object]()
471
+ if not _is_sequence(raw_data):
372
472
  raise TypeError(_INVALID_EXECUTION_PAGE)
373
- data = tuple(cast("Sequence[object]", raw_data))
374
- has_more = values.get("has_more", False)
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(values.get("page")),
380
- limit=_count(values.get("limit")),
381
- total=_count(values.get("total")),
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(text)
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: object = json.loads(text, parse_constant=_reject_json_constant)
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[[Any], object]
52
- RealtimeCallback = Callable[[Any], object]
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: RealtimeCallback,
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[MessageCallback]] = {}
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: MessageCallback) -> Channel:
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(self, callback: MessageCallback) -> UnsubscribeCallback:
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: MessageCallback,
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, RealtimeCallback]] = {
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(self, callback: RealtimeCallback) -> UnsubscribeCallback:
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(self, callback: RealtimeCallback) -> UnsubscribeCallback:
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(self, callback: RealtimeCallback) -> UnsubscribeCallback:
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: RealtimeCallback,
1506
+ callback: _StoredCallback,
1498
1507
  ) -> UnsubscribeCallback:
1499
1508
  require_callable(callback, CALLBACK_NOT_CALLABLE)
1500
1509
  callback_id = next(self._callback_ids)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: volcano-sdk-python
3
- Version: 0.13.3
3
+ Version: 0.13.4
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
@@ -9,19 +9,19 @@ volcano_sdk/_realtime_fetch_worker.py,sha256=f3mUPe5qZmGuhA9jfpqEUtpLZmvlJ1FhI1X
9
9
  volcano_sdk/_session.py,sha256=FiZD11mtOtXSL68WMQQOirxKLRmWk4NIh25zvJxRqek,3124
10
10
  volcano_sdk/_session_operations.py,sha256=dS4seAcD-HNYNs2zOXTk66qA9HliqhXyQwEz91nhY-4,6446
11
11
  volcano_sdk/_transport.py,sha256=j85miD43LvghpQZOEKJYbia4WbpGKz6a4W2gkzfQgWw,65308
12
- volcano_sdk/auth.py,sha256=iwVAnTJV34q3yXWQbgLttD7rIziT3yIl9M3nB3fh-Uk,58253
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=zdLKsapHg7o5jgG1XUGKD2r-6rpeUs7wUN58i-gTmWU,13887
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=1H2O8bk_057x080gz7f62x9Etn1mtHvWMXktisJo5HA,16796
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=-mUVBwaon5qPFWOsWVyW0jRCeBKbeCd110wBBxD_YcY,66800
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.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,,
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,,