volcano-sdk-python 0.10.2__py3-none-any.whl → 0.10.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.
@@ -0,0 +1,12 @@
1
+ """Validate runtime callbacks without erasing their static signatures."""
2
+
3
+
4
+ def require_callable(value: object, message: str) -> None:
5
+ """Reject non-callable values supplied by unchecked callers.
6
+
7
+ Raises:
8
+ TypeError: The value is not callable.
9
+
10
+ """
11
+ if not callable(value):
12
+ raise TypeError(message)
volcano_sdk/_transport.py CHANGED
@@ -738,7 +738,7 @@ def _error_type(status: int) -> type[VolcanoError]:
738
738
  return VolcanoError
739
739
 
740
740
 
741
- def response_payload(response: TransportResponse, expected_status: int) -> Any:
741
+ def response_payload(response: TransportResponse, expected_status: int) -> object:
742
742
  status = int(response.status_code)
743
743
  if status != expected_status:
744
744
  payload: Mapping[str, object]
volcano_sdk/auth.py CHANGED
@@ -12,6 +12,7 @@ from http import HTTPStatus
12
12
  from typing import TYPE_CHECKING, Literal, Protocol, TypeVar, cast
13
13
  from urllib.parse import quote, urlencode
14
14
 
15
+ from ._callbacks import require_callable
15
16
  from ._generated.models.auth_confirm_email_change_response_200 import (
16
17
  AuthConfirmEmailChangeResponse200,
17
18
  )
@@ -514,8 +515,7 @@ class Auth:
514
515
  TypeError: The callback is not callable.
515
516
 
516
517
  """
517
- if not callable(callback):
518
- raise TypeError(_INVALID_AUTH_CALLBACK)
518
+ require_callable(callback, _INVALID_AUTH_CALLBACK)
519
519
  return self._client._subscribe_auth_state_change(callback)
520
520
 
521
521
  def set_session(self, session: Session) -> Session:
volcano_sdk/client.py CHANGED
@@ -126,14 +126,14 @@ class VolcanoClient:
126
126
  if _transport is not None
127
127
  else GeneratedTransport(api_url=self._api_url, timeout=timeout)
128
128
  )
129
- self.auth = Auth(self)
130
- self.functions = Functions(self)
131
- self.durable = Durable(self)
132
- self.logs = Logs(self)
133
- self.storage = Storage(self)
134
- self.locks = Locks(self)
129
+ self.auth: Auth = Auth(self)
130
+ self.functions: Functions = Functions(self)
131
+ self.durable: Durable = Durable(self)
132
+ self.logs: Logs = Logs(self)
133
+ self.storage: Storage = Storage(self)
134
+ self.locks: Locks = Locks(self)
135
135
  if _realtime_client_factory is None:
136
- self.realtime = Realtime(self, api_url=self._api_url)
136
+ self.realtime: Realtime = Realtime(self, api_url=self._api_url)
137
137
  else:
138
138
  self.realtime = Realtime(
139
139
  self,
volcano_sdk/database.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Mapping
6
6
  from dataclasses import dataclass, replace
7
- from typing import TYPE_CHECKING, Any, Protocol, Self, TypedDict, cast
7
+ from typing import TYPE_CHECKING, Protocol, Self, TypedDict, TypeGuard, cast
8
8
 
9
9
  from typing_extensions import override
10
10
 
@@ -16,6 +16,30 @@ if TYPE_CHECKING:
16
16
 
17
17
  from ._transport import Transport, invoke, response_payload
18
18
 
19
+ _INVALID_DATABASE_ROWS = "Expected a list of database rows with string keys"
20
+
21
+
22
+ def _is_database_row(value: object) -> TypeGuard[dict[str, object]]:
23
+ if not isinstance(value, dict):
24
+ return False
25
+ row = cast("dict[object, object]", value)
26
+ return all(isinstance(key, str) for key in row)
27
+
28
+
29
+ def _database_rows(payload: object) -> list[dict[str, object]]:
30
+ if not isinstance(payload, Mapping):
31
+ raise TypeError(_INVALID_DATABASE_ROWS)
32
+ values = cast("Mapping[object, object]", payload)
33
+ raw_rows = values.get("data")
34
+ if not isinstance(raw_rows, list):
35
+ raise TypeError(_INVALID_DATABASE_ROWS)
36
+ rows: list[dict[str, object]] = []
37
+ for row in cast("list[object]", raw_rows):
38
+ if not _is_database_row(row):
39
+ raise TypeError(_INVALID_DATABASE_ROWS)
40
+ rows.append(row)
41
+ return rows
42
+
19
43
 
20
44
  def _snapshot_json(value: JSONValue) -> JSONValue:
21
45
  if isinstance(value, Mapping):
@@ -62,9 +86,10 @@ class _OrderClause(TypedDict):
62
86
  class FilterBuilder:
63
87
  """Shared immutable filters for database operations."""
64
88
 
65
- _filters: tuple[_FilterCondition, ...]
89
+ _filters: tuple[_FilterCondition, ...] = ()
66
90
 
67
91
  def _with_filters(self, filters: tuple[_FilterCondition, ...]) -> Self:
92
+ del filters
68
93
  raise NotImplementedError
69
94
 
70
95
  def eq(self, column: str, value: object) -> Self:
@@ -314,12 +339,12 @@ class QueryBuilder(FilterBuilder):
314
339
  if value is not None
315
340
  }
316
341
 
317
- def execute(self) -> list[dict[str, Any]]:
342
+ def execute(self) -> list[dict[str, object]]:
318
343
  """Execute the query and return its rows.
319
344
 
320
345
  Returns
321
346
  -------
322
- list[dict[str, Any]]
347
+ list[dict[str, object]]
323
348
  Rows returned by the select request.
324
349
 
325
350
  """
@@ -332,8 +357,8 @@ class QueryBuilder(FilterBuilder):
332
357
  body=body,
333
358
  )
334
359
  )
335
- payload = response_payload(response, 200)
336
- return list(payload["data"])
360
+ payload: object = response_payload(response, 200)
361
+ return _database_rows(payload)
337
362
 
338
363
 
339
364
  @dataclass(frozen=True, slots=True)
@@ -345,12 +370,12 @@ class InsertBuilder:
345
370
  _table: str
346
371
  _values: dict[str, JSONValue]
347
372
 
348
- def execute(self) -> list[dict[str, Any]]:
373
+ def execute(self) -> list[dict[str, object]]:
349
374
  """Insert one row and return the inserted rows.
350
375
 
351
376
  Returns
352
377
  -------
353
- list[dict[str, Any]]
378
+ list[dict[str, object]]
354
379
  Inserted rows returned by the server.
355
380
 
356
381
  """
@@ -362,8 +387,8 @@ class InsertBuilder:
362
387
  body={"table": self._table, "values": _snapshot_row(self._values)},
363
388
  )
364
389
  )
365
- payload = response_payload(response, 200)
366
- return list(payload["data"])
390
+ payload: object = response_payload(response, 200)
391
+ return _database_rows(payload)
367
392
 
368
393
 
369
394
  @dataclass(frozen=True, slots=True)
@@ -380,12 +405,12 @@ class UpdateBuilder(FilterBuilder):
380
405
  def _with_filters(self, filters: tuple[_FilterCondition, ...]) -> UpdateBuilder:
381
406
  return replace(self, _filters=filters)
382
407
 
383
- def execute(self) -> list[dict[str, Any]]:
408
+ def execute(self) -> list[dict[str, object]]:
384
409
  """Update matching rows and return them.
385
410
 
386
411
  Returns
387
412
  -------
388
- list[dict[str, Any]]
413
+ list[dict[str, object]]
389
414
  Updated rows returned by the server.
390
415
 
391
416
  """
@@ -401,8 +426,8 @@ class UpdateBuilder(FilterBuilder):
401
426
  },
402
427
  )
403
428
  )
404
- payload = response_payload(response, 200)
405
- return list(payload["data"])
429
+ payload: object = response_payload(response, 200)
430
+ return _database_rows(payload)
406
431
 
407
432
 
408
433
  @dataclass(frozen=True, slots=True)
@@ -418,12 +443,12 @@ class DeleteBuilder(FilterBuilder):
418
443
  def _with_filters(self, filters: tuple[_FilterCondition, ...]) -> DeleteBuilder:
419
444
  return replace(self, _filters=filters)
420
445
 
421
- def execute(self) -> list[dict[str, Any]]:
446
+ def execute(self) -> list[dict[str, object]]:
422
447
  """Delete matching rows and return them.
423
448
 
424
449
  Returns
425
450
  -------
426
- list[dict[str, Any]]
451
+ list[dict[str, object]]
427
452
  Deleted rows returned by the server.
428
453
 
429
454
  """
@@ -435,8 +460,8 @@ class DeleteBuilder(FilterBuilder):
435
460
  body={"table": self._table, "filters": list(self._filters)},
436
461
  )
437
462
  )
438
- payload = response_payload(response, 200)
439
- return list(payload["data"])
463
+ payload: object = response_payload(response, 200)
464
+ return _database_rows(payload)
440
465
 
441
466
 
442
467
  @dataclass(frozen=True, slots=True)
volcano_sdk/durable.py CHANGED
@@ -4,11 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Mapping, Sequence
6
6
  from datetime import datetime
7
- from typing import TYPE_CHECKING, Protocol, cast
7
+ from typing import Protocol, cast
8
8
  from uuid import UUID
9
9
 
10
10
  from ._transport import (
11
11
  DurableExecutionListRequest,
12
+ Transport,
12
13
  TransportResponse,
13
14
  invoke,
14
15
  response_payload,
@@ -21,9 +22,6 @@ from .models import (
21
22
  JSONValue,
22
23
  )
23
24
 
24
- if TYPE_CHECKING:
25
- from .client import VolcanoClient
26
-
27
25
  _INVALID_EXECUTION_PAYLOAD = "Expected a complete durable execution"
28
26
  _INVALID_EXECUTION_PAGE = "Expected a complete durable execution page"
29
27
  # The spec's maxLength on X-Volcano-Execution-Name. Checked here so an
@@ -49,6 +47,16 @@ _HTTP_ACCEPTED = 202
49
47
  _HTTP_OK = 200
50
48
 
51
49
 
50
+ class DurableClientContext(Protocol):
51
+ """Client capabilities required by durable execution requests."""
52
+
53
+ _transport: Transport
54
+
55
+ def _function_token(self) -> str: ...
56
+
57
+ def _session_token(self) -> str: ...
58
+
59
+
52
60
  class DurableTransport(Protocol):
53
61
  """Transport operations required by the durable facade."""
54
62
 
@@ -100,7 +108,7 @@ class DurableTransport(Protocol):
100
108
  class Durable:
101
109
  """Start and follow executions of deployed durable functions."""
102
110
 
103
- def __init__(self, client: VolcanoClient) -> None:
111
+ def __init__(self, client: DurableClientContext) -> None:
104
112
  """Bind durable operations to a Volcano client."""
105
113
  self._client = client
106
114
 
@@ -28,12 +28,37 @@ from __future__ import annotations
28
28
  import functools
29
29
  import importlib
30
30
  from dataclasses import dataclass
31
- from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar, cast, overload
31
+ from typing import (
32
+ TYPE_CHECKING,
33
+ Any,
34
+ Generic,
35
+ ParamSpec,
36
+ Protocol,
37
+ TypeAlias,
38
+ TypeVar,
39
+ cast,
40
+ overload,
41
+ )
42
+
43
+ from ._callbacks import require_callable
32
44
 
33
45
  if TYPE_CHECKING:
34
- from collections.abc import Callable, Sequence
46
+ from collections.abc import Callable, Mapping, Sequence
47
+
48
+ from aws_durable_execution_sdk_python.config import (
49
+ CompletionConfig,
50
+ ParallelConfig,
51
+ StepConfig,
52
+ StepSemantics,
53
+ )
54
+ from aws_durable_execution_sdk_python.config import Duration as EngineDuration
55
+ from aws_durable_execution_sdk_python.retries import (
56
+ RetryDecision,
57
+ RetryStrategyConfig,
58
+ )
35
59
 
36
60
  T = TypeVar("T")
61
+ _P = ParamSpec("_P")
37
62
  # A duration: "30s", "5m", "2h", "1d", a compound string like "1m30s", a whole
38
63
  # number of seconds, or the mapping form.
39
64
  Duration: TypeAlias = "str | int | dict[str, int]"
@@ -133,16 +158,18 @@ class _Engine:
133
158
  except ImportError as error:
134
159
  raise DurableRuntimeMissingError(error) from error
135
160
  self.durable_execution = root.durable_execution
136
- self.duration = config.Duration
137
- self.step_config = config.StepConfig
138
- self.step_semantics = config.StepSemantics
161
+ self.duration: type[EngineDuration] = config.Duration
162
+ self.step_config: type[StepConfig] = config.StepConfig
163
+ self.step_semantics: type[StepSemantics] = config.StepSemantics
139
164
  self.map_config = config.MapConfig
140
- self.parallel_config = config.ParallelConfig
141
- self.completion_config = config.CompletionConfig
165
+ self.parallel_config: type[ParallelConfig] = config.ParallelConfig
166
+ self.completion_config: type[CompletionConfig] = config.CompletionConfig
142
167
  self.parallel_branch = config.ParallelBranch
143
168
  self.create_retry_strategy = retries.create_retry_strategy
144
- self.retry_strategy_config = retries.RetryStrategyConfig
145
- self.retry_decision = retries.RetryDecision
169
+ self.retry_strategy_config: type[RetryStrategyConfig] = (
170
+ retries.RetryStrategyConfig
171
+ )
172
+ self.retry_decision: type[RetryDecision] = retries.RetryDecision
146
173
  self.create_wait_strategy = waits.create_wait_strategy
147
174
  self.wait_strategy_config = waits.WaitStrategyConfig
148
175
  self.wait_for_condition_config = waits.WaitForConditionConfig
@@ -225,6 +252,40 @@ class ParallelBranch(Generic[T]):
225
252
  name: str | None = None
226
253
 
227
254
 
255
+ class DurableLogger(Protocol):
256
+ """Replay-aware logging methods exposed by durable contexts and steps."""
257
+
258
+ def debug(
259
+ self, msg: object, *args: object, extra: Mapping[str, object] | None = None
260
+ ) -> None:
261
+ """Log a debug message unless the operation is replaying."""
262
+ ...
263
+
264
+ def info(
265
+ self, msg: object, *args: object, extra: Mapping[str, object] | None = None
266
+ ) -> None:
267
+ """Log an informational message unless the operation is replaying."""
268
+ ...
269
+
270
+ def warning(
271
+ self, msg: object, *args: object, extra: Mapping[str, object] | None = None
272
+ ) -> None:
273
+ """Log a warning unless the operation is replaying."""
274
+ ...
275
+
276
+ def error(
277
+ self, msg: object, *args: object, extra: Mapping[str, object] | None = None
278
+ ) -> None:
279
+ """Log an error unless the operation is replaying."""
280
+ ...
281
+
282
+ def exception(
283
+ self, msg: object, *args: object, extra: Mapping[str, object] | None = None
284
+ ) -> None:
285
+ """Log an exception unless the operation is replaying."""
286
+ ...
287
+
288
+
228
289
  @dataclass(frozen=True, slots=True)
229
290
  class StepScope:
230
291
  """What a step's function is given: logging, and which attempt it is on.
@@ -235,7 +296,7 @@ class StepScope:
235
296
  would invite exactly the mistake the engine then rejects.
236
297
  """
237
298
 
238
- log: Any
299
+ log: DurableLogger
239
300
  # 1 on the first attempt.
240
301
  attempt: int
241
302
 
@@ -380,7 +441,7 @@ class DurableContext:
380
441
  self._context = context
381
442
  self._engine = engine
382
443
  # Logs, suppressed while an operation is being replayed.
383
- self.log = context.logger
444
+ self.log: DurableLogger = context.logger
384
445
 
385
446
  def step(
386
447
  self,
@@ -407,7 +468,7 @@ class DurableContext:
407
468
  """
408
469
  step_name, step_func = _named(name, func, "step")
409
470
 
410
- def run(scope: Any) -> Any:
471
+ def run(scope: Any) -> T:
411
472
  return step_func(StepScope(scope.logger, scope.attempt))
412
473
 
413
474
  # The engine hands back whatever the step returned, untyped. T is the
@@ -454,7 +515,7 @@ class DurableContext:
454
515
  child_name, child_func = _named(name, func, "child")
455
516
  engine = self._engine
456
517
 
457
- def run(context: Any) -> Any:
518
+ def run(context: Any) -> T:
458
519
  return child_func(DurableContext(context, engine))
459
520
 
460
521
  return cast("T", self._context.run_in_child_context(run, child_name))
@@ -532,7 +593,7 @@ class DurableContext:
532
593
  raise TypeError(_INVALID_ITEMS)
533
594
  engine = self._engine
534
595
 
535
- def run(context: Any, item: Any, index: int, _all: Any) -> Any:
596
+ def run(context: Any, item: Any, index: int, _all: Any) -> T:
536
597
  return map_func(item, DurableContext(context, engine), index)
537
598
 
538
599
  return BatchResult(
@@ -590,8 +651,7 @@ class DurableContext:
590
651
  return named(DurableContext(context, engine))
591
652
 
592
653
  return engine.parallel_branch(func=run_named, name=branch.name)
593
- if not callable(branch):
594
- raise TypeError(_INVALID_BRANCH)
654
+ require_callable(branch, _INVALID_BRANCH)
595
655
  bare = branch
596
656
 
597
657
  def run_bare(context: Any) -> Any:
@@ -608,7 +668,7 @@ class DurableContext:
608
668
  )
609
669
  return config
610
670
 
611
- def _step_config(self, *, retry: Retry, at_most_once: bool) -> Any:
671
+ def _step_config(self, *, retry: Retry, at_most_once: bool) -> StepConfig:
612
672
  engine = self._engine
613
673
  config: dict[str, Any] = {}
614
674
  if at_most_once:
@@ -618,7 +678,7 @@ class DurableContext:
618
678
  config["retry_strategy"] = strategy
619
679
  return engine.step_config(**config)
620
680
 
621
- def _retry_strategy(self, retry: Retry) -> Any:
681
+ def _retry_strategy(self, retry: object) -> Any:
622
682
  engine = self._engine
623
683
  if retry is None or retry is True:
624
684
  return None
@@ -635,11 +695,11 @@ class DurableContext:
635
695
  return retry
636
696
  raise TypeError(_INVALID_RETRY)
637
697
 
638
- def _never_retry(self) -> Callable[[Exception, int], Any]:
698
+ def _never_retry(self) -> Callable[[Exception, int], RetryDecision]:
639
699
  engine = self._engine
640
700
  no_delay = engine.duration.from_seconds(0)
641
701
 
642
- def never_retry(_error: Exception, _attempt: int) -> Any:
702
+ def never_retry(_error: Exception, _attempt: int) -> RetryDecision:
643
703
  return engine.retry_decision(should_retry=False, delay=no_delay)
644
704
 
645
705
  return never_retry
@@ -656,13 +716,15 @@ class DurableContext:
656
716
  ),
657
717
  )
658
718
 
659
- def _optional_duration(self, value: Duration | None, field_name: str) -> Any:
719
+ def _optional_duration(
720
+ self, value: Duration | None, field_name: str
721
+ ) -> EngineDuration | None:
660
722
  return None if value is None else self._duration(value, field_name)
661
723
 
662
- def _duration(self, value: object, field_name: str) -> Any:
724
+ def _duration(self, value: object, field_name: str) -> EngineDuration:
663
725
  return self._engine.duration.from_seconds(_to_seconds(value, field_name))
664
726
 
665
- def _wait_duration(self, value: object) -> Any:
727
+ def _wait_duration(self, value: object) -> EngineDuration:
666
728
  seconds = _to_seconds(value, "wait")
667
729
  if seconds < _MIN_WAIT_SECONDS:
668
730
  message = f"wait must be at least {_MIN_WAIT_SECONDS} second"
@@ -714,8 +776,7 @@ def durable(
714
776
  return durable(func, logger=logger)
715
777
 
716
778
  return decorate
717
- if not callable(handler):
718
- raise TypeError(_REQUIRES_HANDLER)
779
+ require_callable(handler, _REQUIRES_HANDLER)
719
780
 
720
781
  return _wrap_durable(handler, logger)
721
782
 
@@ -758,10 +819,10 @@ def _validate_wait_options(options: WaitUntilOptions) -> None:
758
819
 
759
820
 
760
821
  def _named(
761
- name: str | Callable[..., Any] | None,
762
- func: Callable[..., Any] | None,
822
+ name: str | Callable[_P, T] | None,
823
+ func: Callable[_P, T] | None,
763
824
  operation: str,
764
- ) -> tuple[str | None, Callable[..., Any]]:
825
+ ) -> tuple[str | None, Callable[_P, T]]:
765
826
  """Accept both the named and unnamed form of an operation.
766
827
 
767
828
  The name is what the operation is recorded under, so it is worth
@@ -776,7 +837,7 @@ def _named(
776
837
  return None, _callable(name, operation)
777
838
 
778
839
 
779
- def _callable(func: object, operation: str) -> Callable[..., Any]:
840
+ def _callable(func: Callable[_P, T] | None, operation: str) -> Callable[_P, T]:
780
841
  if not callable(func):
781
842
  message = f"{operation}() requires a function to run"
782
843
  raise TypeError(message)
volcano_sdk/functions.py CHANGED
@@ -7,9 +7,16 @@ import re
7
7
  from collections.abc import Callable, Mapping
8
8
  from typing import TYPE_CHECKING, Protocol, TypeVar, cast
9
9
 
10
- from . import _function_resolution
11
- from ._function_resolution import FunctionResolution
12
- from ._transport import TransportResponse, invoke, response_payload
10
+ from ._function_resolution import (
11
+ FunctionResolution,
12
+ forget,
13
+ lookup,
14
+ resolve_lock,
15
+ store,
16
+ store_missing,
17
+ valid_invoke_url,
18
+ )
19
+ from ._transport import Transport, TransportResponse, invoke, response_payload
13
20
  from .errors import (
14
21
  AuthenticationError,
15
22
  NotFoundError,
@@ -19,7 +26,9 @@ from .errors import (
19
26
  from .models import FunctionResponse, JSONValue, _freeze_json
20
27
 
21
28
  if TYPE_CHECKING:
22
- from .client import VolcanoClient
29
+ from ._session_operations import SessionOperations
30
+ from .auth import Auth
31
+ from .models import Session
23
32
 
24
33
  _FUNCTION_NAME = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
25
34
  _INVALID_FUNCTION_NAME = (
@@ -39,8 +48,23 @@ _FUNCTION_INVOKED_HEADER = "X-Volcano-Function-Invoked"
39
48
  _Result = TypeVar("_Result")
40
49
 
41
50
 
51
+ class FunctionsContext(Protocol):
52
+ """Client capabilities required by function invocation."""
53
+
54
+ _transport: Transport
55
+ auth: Auth
56
+
57
+ def _capture_session_binding(
58
+ self,
59
+ ) -> tuple[int, SessionOperations, Session | None]: ...
60
+
61
+ def _function_token(self) -> str: ...
62
+
63
+ def _api_base_url(self) -> str: ...
64
+
65
+
42
66
  class _FunctionAuth:
43
- def __init__(self, client: VolcanoClient) -> None:
67
+ def __init__(self, client: FunctionsContext) -> None:
44
68
  self._client = client
45
69
  self._binding = client._capture_session_binding()
46
70
  self._fallback_token = client._function_token()
@@ -116,7 +140,7 @@ class FunctionsTransport(Protocol):
116
140
  class Functions:
117
141
  """Invoke deployed Volcano functions by name."""
118
142
 
119
- def __init__(self, client: VolcanoClient) -> None:
143
+ def __init__(self, client: FunctionsContext) -> None:
120
144
  """Bind function calls to a Volcano client."""
121
145
  self._client = client
122
146
 
@@ -149,9 +173,7 @@ class Functions:
149
173
  if _stale_mapping(response):
150
174
  # The function was deleted and recreated, so the cached identity no
151
175
  # longer exists. Resolve again before giving up.
152
- _function_resolution.forget(
153
- self._client._api_base_url(), authorization, name
154
- )
176
+ forget(self._client._api_base_url(), authorization, name)
155
177
  _, resolution = auth.run(
156
178
  lambda token: (token, self._resolve(transport, token, name))
157
179
  )
@@ -211,7 +233,7 @@ class Functions:
211
233
 
212
234
  # Hold the name's lock across the round trip so concurrent callers wait
213
235
  # for one resolve instead of each opening their own.
214
- with _function_resolution.resolve_lock(api_url, authorization, name):
236
+ with resolve_lock(api_url, authorization, name):
215
237
  cached = self._cached(api_url, authorization, name)
216
238
  if cached is not None:
217
239
  return cached
@@ -221,7 +243,7 @@ class Functions:
221
243
  def _cached(
222
244
  api_url: str, authorization: str, name: str
223
245
  ) -> FunctionResolution | None:
224
- cached = _function_resolution.lookup(api_url, authorization, name)
246
+ cached = lookup(api_url, authorization, name)
225
247
  if cached is None:
226
248
  return None
227
249
  if cached.resolution is None:
@@ -248,12 +270,10 @@ class Functions:
248
270
  try:
249
271
  payload = response_payload(resolved, _HTTP_SUCCESS_MIN)
250
272
  except NotFoundError as error:
251
- _function_resolution.store_missing(api_url, authorization, name, error)
273
+ store_missing(api_url, authorization, name, error)
252
274
  raise
253
275
  resolution = self._resolution(payload, api_url)
254
- _function_resolution.store(
255
- api_url, authorization, name, resolution, self._cache_ttl(payload)
256
- )
276
+ store(api_url, authorization, name, resolution, self._cache_ttl(payload))
257
277
  return resolution
258
278
 
259
279
  @staticmethod
@@ -268,9 +288,7 @@ class Functions:
268
288
  # local development; the function is reached through the API instead.
269
289
  return FunctionResolution(
270
290
  function_id=function_id,
271
- invoke_url=_function_resolution.valid_invoke_url(
272
- values.get("invoke_url"), api_url
273
- ),
291
+ invoke_url=valid_invoke_url(values.get("invoke_url"), api_url),
274
292
  )
275
293
 
276
294
  @staticmethod
volcano_sdk/locks.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from contextlib import contextmanager, suppress
6
7
  from datetime import datetime
7
8
  from typing import TYPE_CHECKING, Protocol, cast
@@ -22,6 +23,7 @@ _MIN_LOCK_TTL_SECONDS = 5
22
23
  _MAX_LOCK_TTL_SECONDS = 7_776_000
23
24
  _INVALID_LOCK_TTL = "ttl must be an integer between 5 seconds and 90 days"
24
25
  _MISSING_RENEWAL_FAILURE = "lock guard rejected renewal without a failure"
26
+ _INVALID_LOCK_RESPONSE = "Expected a complete lock response"
25
27
 
26
28
 
27
29
  class LocksContext(Protocol):
@@ -82,6 +84,26 @@ def _parse_datetime(value: object) -> datetime | None:
82
84
  return datetime.fromisoformat(str(value))
83
85
 
84
86
 
87
+ def _lock_values(payload: object) -> Mapping[object, object]:
88
+ if not isinstance(payload, Mapping):
89
+ raise TypeError(_INVALID_LOCK_RESPONSE)
90
+ return cast("Mapping[object, object]", payload)
91
+
92
+
93
+ def _fencing_token(value: object) -> int | None:
94
+ if value is None or type(value) is int:
95
+ return value
96
+ raise TypeError(_INVALID_LOCK_RESPONSE)
97
+
98
+
99
+ def _lease_fields(payload: Mapping[object, object]) -> tuple[datetime, int]:
100
+ expires_at = payload.get("expires_at")
101
+ fencing_token = payload.get("fencing_token")
102
+ if not isinstance(expires_at, str) or type(fencing_token) is not int:
103
+ raise TypeError(_INVALID_LOCK_RESPONSE)
104
+ return datetime.fromisoformat(expires_at), fencing_token
105
+
106
+
85
107
  def _validate_ttl(ttl: object) -> None:
86
108
  if (
87
109
  isinstance(ttl, bool)
@@ -125,11 +147,14 @@ class Locks:
125
147
  key=key,
126
148
  request_id=_request_uuid(request_id, "request_id"),
127
149
  )
128
- payload = response_payload(response, 200)
150
+ payload = _lock_values(response_payload(response, 200))
151
+ held = payload.get("held")
152
+ if not isinstance(held, bool):
153
+ raise TypeError(_INVALID_LOCK_RESPONSE)
129
154
  return LockState(
130
- held=payload["held"],
155
+ held=held,
131
156
  expires_at=_parse_datetime(payload.get("expires_at")),
132
- fencing_token=payload.get("fencing_token"),
157
+ fencing_token=_fencing_token(payload.get("fencing_token")),
133
158
  )
134
159
 
135
160
  def acquire(
@@ -173,17 +198,18 @@ class Locks:
173
198
  raise
174
199
  started_at = _lease_now()
175
200
  payload = self._acquire_payload(key, ttl, token, request_id, authorization)
201
+ expires_at, fencing_token = _lease_fields(payload)
176
202
  lease = LockLease(
177
203
  key=key,
178
204
  token=token,
179
- expires_at=_parse_datetime(payload.get("expires_at")),
180
- fencing_token=cast("int | None", payload.get("fencing_token")),
205
+ expires_at=expires_at,
206
+ fencing_token=fencing_token,
181
207
  )
182
208
  return lease, started_at
183
209
 
184
210
  def _acquire_payload(
185
211
  self, key: str, ttl: int, token: str, request_id: str, authorization: str
186
- ) -> dict[str, object]:
212
+ ) -> Mapping[object, object]:
187
213
  response = invoke(
188
214
  self._client._transport.acquire_project_lock,
189
215
  authorization=authorization,
@@ -192,7 +218,7 @@ class Locks:
192
218
  token=token,
193
219
  request_id=request_id,
194
220
  )
195
- return cast("dict[str, object]", response_payload(response, 201))
221
+ return _lock_values(response_payload(response, 201))
196
222
 
197
223
  def renew(
198
224
  self, key: str, lease: LockLease, *, ttl: int, request_id: str | None = None
@@ -216,12 +242,13 @@ class Locks:
216
242
  ttl=ttl,
217
243
  token=lease.token,
218
244
  )
219
- payload = response_payload(response, 200)
245
+ payload = _lock_values(response_payload(response, 200))
246
+ expires_at, fencing_token = _lease_fields(payload)
220
247
  return LockLease(
221
248
  key=key,
222
249
  token=lease.token,
223
- expires_at=_parse_datetime(payload.get("expires_at")),
224
- fencing_token=payload.get("fencing_token"),
250
+ expires_at=expires_at,
251
+ fencing_token=fencing_token,
225
252
  )
226
253
 
227
254
  def release(
volcano_sdk/logs.py CHANGED
@@ -11,16 +11,23 @@ from ._log_response import (
11
11
  response_values,
12
12
  search_metadata,
13
13
  )
14
- from ._transport import TransportResponse, invoke, response_payload
14
+ from ._transport import Transport, TransportResponse, invoke, response_payload
15
15
  from .models import JSONValue, LogActivityResponse, LogSearchResponse, _freeze_json
16
16
 
17
17
  if TYPE_CHECKING:
18
- from .client import VolcanoClient
18
+ from .auth import Auth
19
19
 
20
20
  _INVALID_PROJECT_ID = "project_id must be a non-empty string"
21
21
  _INVALID_LOG_REQUEST = "Log request must be a mapping"
22
22
 
23
23
 
24
+ class LogsContext(Protocol):
25
+ """Client capabilities required by project log reads."""
26
+
27
+ _transport: Transport
28
+ auth: Auth
29
+
30
+
24
31
  class LogsTransport(Protocol):
25
32
  """Transport operations required by the logs facade."""
26
33
 
@@ -48,7 +55,7 @@ class LogsTransport(Protocol):
48
55
  class Logs:
49
56
  """Search retained project logs and activity."""
50
57
 
51
- def __init__(self, client: VolcanoClient) -> None:
58
+ def __init__(self, client: LogsContext) -> None:
52
59
  """Bind log reads to a Volcano client."""
53
60
  self._client = client
54
61
 
volcano_sdk/realtime.py CHANGED
@@ -8,11 +8,21 @@ import inspect
8
8
  from collections.abc import Awaitable, Callable, Mapping
9
9
  from dataclasses import dataclass, field, replace
10
10
  from types import MappingProxyType
11
- from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, cast
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Literal,
15
+ Protocol,
16
+ TypeAlias,
17
+ TypeVar,
18
+ cast,
19
+ overload,
20
+ )
12
21
  from urllib.parse import quote, urlsplit, urlunsplit
13
22
 
14
23
  from typing_extensions import override
15
24
 
25
+ from ._callbacks import require_callable
16
26
  from ._realtime_fetch_worker import (
17
27
  PostgresFetchJob,
18
28
  PostgresFetchOutcome,
@@ -25,6 +35,7 @@ from ._transport import (
25
35
  invoke_async,
26
36
  response_payload,
27
37
  )
38
+ from .database import _database_rows
28
39
  from .models import JSONValue, _freeze_json
29
40
 
30
41
  if TYPE_CHECKING:
@@ -34,6 +45,8 @@ if TYPE_CHECKING:
34
45
  from .models import Session
35
46
 
36
47
  _PostgresFetchRequest: TypeAlias = PostgresFetchRequest
48
+ _SubscriptionT = TypeVar("_SubscriptionT")
49
+ _DefaultT = TypeVar("_DefaultT")
37
50
 
38
51
  MessageCallback = Callable[[Any], Any]
39
52
  RealtimeCallback = Callable[[Any], Any]
@@ -459,9 +472,20 @@ def _centrifuge_client(
459
472
  )
460
473
 
461
474
 
462
- class _ProjectAwareSubscriptions(dict[str, Any]):
475
+ class _ProjectAwareSubscriptions(dict[str, _SubscriptionT]):
476
+ @overload
477
+ def get(self, key: str, default: None = None) -> _SubscriptionT | None: ...
478
+
479
+ @overload
480
+ def get(self, key: str, default: _SubscriptionT) -> _SubscriptionT: ...
481
+
482
+ @overload
483
+ def get(self, key: str, default: _DefaultT) -> _SubscriptionT | _DefaultT: ...
484
+
463
485
  @override
464
- def get(self, key: str, default: Any = None) -> Any:
486
+ def get(
487
+ self, key: str, default: _DefaultT | None = None
488
+ ) -> _SubscriptionT | _DefaultT | None:
465
489
  subscription = super().get(key)
466
490
  if subscription is not None:
467
491
  return subscription
@@ -473,15 +497,22 @@ class _ProjectAwareSubscriptions(dict[str, Any]):
473
497
  return max(matches, key=lambda match: len(match[0]))[1] if matches else default
474
498
 
475
499
 
500
+ def _project_subscriptions(value: object) -> _ProjectAwareSubscriptions[object]:
501
+ if not isinstance(value, dict):
502
+ raise TypeError(SUBSCRIPTION_REGISTRY_UNAVAILABLE)
503
+ subscriptions = _ProjectAwareSubscriptions[object]()
504
+ for channel, subscription in cast("Mapping[object, object]", value).items():
505
+ if not isinstance(channel, str):
506
+ raise TypeError(SUBSCRIPTION_REGISTRY_UNAVAILABLE)
507
+ subscriptions[channel] = subscription
508
+ return subscriptions
509
+
510
+
476
511
  class _VolcanoCentrifugeConnection:
477
512
  def __init__(self, connection: CentrifugeConnection) -> None:
478
513
  self._connection = connection
479
514
  state = vars(connection)
480
- subscriptions = state.get("_subs")
481
- if not isinstance(subscriptions, dict):
482
- raise TypeError(SUBSCRIPTION_REGISTRY_UNAVAILABLE)
483
- typed_subscriptions = cast("dict[str, Any]", subscriptions)
484
- state["_subs"] = _ProjectAwareSubscriptions(typed_subscriptions)
515
+ state["_subs"] = _project_subscriptions(state.get("_subs"))
485
516
 
486
517
  async def connect(self) -> None:
487
518
  await self._connection.connect()
@@ -1361,8 +1392,7 @@ class Realtime:
1361
1392
  "limit": len(row_ids),
1362
1393
  },
1363
1394
  )
1364
- payload = response_payload(response, 200)
1365
- rows = list(payload["data"])
1395
+ rows = _database_rows(response_payload(response, 200))
1366
1396
  return tuple(
1367
1397
  next(
1368
1398
  (row for row in rows if row.get("id") == request.row_id),
@@ -1409,8 +1439,7 @@ class Realtime:
1409
1439
  event: str,
1410
1440
  callback: RealtimeCallback,
1411
1441
  ) -> UnsubscribeCallback:
1412
- if not callable(callback):
1413
- raise TypeError(CALLBACK_NOT_CALLABLE)
1442
+ require_callable(callback, CALLBACK_NOT_CALLABLE)
1414
1443
  self._next_callback_id += 1
1415
1444
  callback_id = self._next_callback_id
1416
1445
  self._connection_callbacks[event][callback_id] = callback
@@ -1666,19 +1695,31 @@ class Realtime:
1666
1695
  subscription = await self._prepare_subscription(channel, generation)
1667
1696
  if channel._subscribed:
1668
1697
  return
1669
- channel._paused = False
1670
- await subscription.subscribe()
1671
- channel._readiness_task = asyncio.create_task(
1672
- _wait_subscription(channel, subscription)
1673
- )
1674
- try:
1675
- await channel._readiness_task
1676
- finally:
1677
- channel._readiness_task = None
1698
+ await self._resume_subscription(channel, subscription)
1699
+ await self._wait_subscription_readiness(channel, subscription)
1678
1700
  except BaseException as error:
1679
1701
  await self._cleanup_failed_subscription(channel, subscription, error)
1680
1702
  raise
1681
1703
 
1704
+ @staticmethod
1705
+ async def _resume_subscription(
1706
+ channel: Channel, subscription: CentrifugeSubscription
1707
+ ) -> None:
1708
+ channel._paused = False
1709
+ await subscription.subscribe()
1710
+
1711
+ @staticmethod
1712
+ async def _wait_subscription_readiness(
1713
+ channel: Channel, subscription: CentrifugeSubscription
1714
+ ) -> None:
1715
+ channel._readiness_task = asyncio.create_task(
1716
+ _wait_subscription(channel, subscription)
1717
+ )
1718
+ try:
1719
+ await channel._readiness_task
1720
+ finally:
1721
+ channel._readiness_task = None
1722
+
1682
1723
  async def _cleanup_failed_subscription(
1683
1724
  self,
1684
1725
  channel: Channel,
volcano_sdk/storage.py CHANGED
@@ -11,7 +11,7 @@ from dataclasses import dataclass
11
11
  from datetime import datetime
12
12
  from io import SEEK_END, BytesIO
13
13
  from tempfile import TemporaryFile
14
- from typing import TYPE_CHECKING, Any, BinaryIO, Protocol, cast
14
+ from typing import TYPE_CHECKING, Any, BinaryIO, Protocol, TypeGuard, cast
15
15
  from urllib.parse import quote
16
16
 
17
17
  from ._transport import (
@@ -50,6 +50,7 @@ _HTTP_PARTIAL_CONTENT = 206
50
50
  _UPLOAD_SPOOL_READ_SIZE = 1_048_576
51
51
  _UPLOAD_SOURCE_UNAVAILABLE = "Upload source is temporarily unavailable"
52
52
  _INVALID_SIMPLE_UPLOAD = "Upload data must be bytes or a readable binary stream"
53
+ _INVALID_UPLOAD_RESPONSE = "Expected a storage upload response object"
53
54
 
54
55
 
55
56
  def _optional_datetime(value: object) -> datetime | None:
@@ -154,11 +155,15 @@ def _upload_session_status(payload: object) -> UploadSessionStatus:
154
155
  )
155
156
 
156
157
 
158
+ def _is_object_sequence(value: object) -> TypeGuard[Sequence[object]]:
159
+ return isinstance(value, Sequence)
160
+
161
+
157
162
  def _storage_paths(paths: object) -> tuple[str, ...]:
158
163
  if isinstance(paths, str):
159
164
  raw_paths: tuple[object, ...] = (paths,)
160
- elif isinstance(paths, Sequence):
161
- raw_paths = tuple(cast("Sequence[object]", paths))
165
+ elif _is_object_sequence(paths):
166
+ raw_paths = tuple(paths)
162
167
  else:
163
168
  raise TypeError(_INVALID_STORAGE_PATHS)
164
169
  if not raw_paths or any(
@@ -494,7 +499,12 @@ class StorageBucket:
494
499
  binding=binding,
495
500
  )
496
501
  payload = response_payload(response, 201)
497
- return dict(payload)
502
+ if not isinstance(payload, Mapping):
503
+ raise TypeError(_INVALID_UPLOAD_RESPONSE)
504
+ values = cast("Mapping[object, object]", payload)
505
+ if not all(isinstance(key, str) for key in values):
506
+ raise TypeError(_INVALID_UPLOAD_RESPONSE)
507
+ return dict(cast("Mapping[str, Any]", values))
498
508
 
499
509
  def download(self, path: str, *, byte_range: str | None = None) -> bytes:
500
510
  """Download bytes from a path in this bucket.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: volcano-sdk-python
3
- Version: 0.10.2
3
+ Version: 0.10.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
@@ -11,6 +11,8 @@ License-File: LICENSE
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
14
16
  Classifier: Programming Language :: Python :: 3.14
15
17
  Requires-Python: >=3.11
16
18
  Requires-Dist: attrs>=26.1.0
@@ -330,6 +332,10 @@ the clock or a random value.
330
332
  | `ctx.child(name, fn)` | Groups operations under one recorded context. |
331
333
  | `ctx.log` | The execution's logger, suppressed while an operation is replayed. |
332
334
 
335
+ `ctx.log` and a step's `scope.log` expose `debug`, `info`, `warning`, `error`,
336
+ and `exception`. They accept a message, formatting arguments, and an optional
337
+ `extra` mapping. Messages are suppressed during replay.
338
+
333
339
  `ctx.map` and `ctx.parallel` both return a batch result: `items` (the items that
334
340
  finished, each with `index`, `status`, `result`, `error`), `results`, `errors`,
335
341
  `succeeded`, `failed`, `completed`, `completion_reason`, and `throw_if_failed()`.
@@ -966,7 +972,8 @@ does not need it otherwise.
966
972
 
967
973
  ## Compatibility
968
974
 
969
- CI tests the SDK on Python 3.11 and 3.14. Its public facade is intentionally
975
+ CI tests the SDK on every Python version from 3.11 through 3.14.
976
+ Its public facade is intentionally
970
977
  independent of generated httpx types. The bundled `openapi/openapi.yaml` matches
971
978
  the public bundle from [Hosting #991](https://github.com/Kong/volcano-hosting/pull/991)
972
979
  at commit `ef03f689e`.
@@ -1,4 +1,5 @@
1
1
  volcano_sdk/__init__.py,sha256=azhzz2nWUoy_j8sPRuovhs8Fy8CVuwJpLAvT5bhgT5g,2172
2
+ volcano_sdk/_callbacks.py,sha256=VuhphT6RM7IfIGPSAVXGC-Xm6ml-aUOSN-GQ9YVDycg,329
2
3
  volcano_sdk/_function_resolution.py,sha256=3qfLiyxMUKsOCyaoZpL4sDQkfwq56CPGGwcRW1aorSg,5919
3
4
  volcano_sdk/_lock_guard.py,sha256=YxuJDQOCxHYf7mk6Cp_CPE-mYKC9IoGt_Cxe7SVL8aw,5922
4
5
  volcano_sdk/_lock_renewer.py,sha256=cgogAmoRM_An-PdymXumuGmxm62MGqOsQU4c_PYlUjg,1000
@@ -7,21 +8,21 @@ volcano_sdk/_log_response.py,sha256=wZaugr0eD0ZLSa-M6TUEwdIDsI_wblyDXucYEwNK14c,
7
8
  volcano_sdk/_realtime_fetch_worker.py,sha256=f3mUPe5qZmGuhA9jfpqEUtpLZmvlJ1FhI1XkNz238FI,10406
8
9
  volcano_sdk/_session.py,sha256=w7B1rwbhH3yoE8kFA6gSpUpQb24rPwppotMu-IPHXdI,3025
9
10
  volcano_sdk/_session_operations.py,sha256=s_m-Gl-knjDrjUOFl6z2lJW4IGHAgCSCjRNA7LCB5j0,5727
10
- volcano_sdk/_transport.py,sha256=hPciv-WJ7A7Siu6SNU6Ix6x_avPGTucxj8rzJePj43w,61904
11
- volcano_sdk/auth.py,sha256=f1Dfyq6gpoIzajUKooiVGqgWgvVtCsF_dIG_uA23Hx0,52224
12
- volcano_sdk/client.py,sha256=MiwpMsAMyRebPZ7qhktx698TNmQR2y-Gytgf-FNG3TU,12643
11
+ volcano_sdk/_transport.py,sha256=XcDk8NAEAHmtH3nmlMWIixcIiEFb-FP_n8VU44QcuXg,61907
12
+ volcano_sdk/auth.py,sha256=OxK9s6XW9jNGA_cXos-cHEZYcmYsxrojruSyLS0mWVs,52237
13
+ volcano_sdk/client.py,sha256=KH52e5WC4ii-hnipB9vNzSDEyfvvpOMBCdtwzRqNUOI,12701
13
14
  volcano_sdk/connection_string.py,sha256=p87P-fd9wVfia-LLRRsFTkW6tzx9FXn-GAy9OIOoDvE,2664
14
- volcano_sdk/database.py,sha256=60Rt4oeaS_WHutC6wGPFunVvFHIo08rwaLiHGuFNRKM,12860
15
- volcano_sdk/durable.py,sha256=3PuhxRNJy6m7IBhpc_Np6wnlRZVTmChdjjqVp_wLoxk,13250
16
- volcano_sdk/durable_authoring.py,sha256=NqBY18LDqZoNsnChx_5XjT-1h9OoAniAu7Yu911ebKw,34029
15
+ volcano_sdk/database.py,sha256=XuXGqlfEPvlOBB33jyTVNhRGe4vrgzOBne2rTRzkQis,13806
16
+ volcano_sdk/durable.py,sha256=YQa_5tT4kxkHTyP4nRMhjWyExnSICXD_RAipSQ1cax8,13422
17
+ volcano_sdk/durable_authoring.py,sha256=L8qK694e0aPLnRLIDQm9dbYGy4M0NPf9wZYECkVrOfA,35842
17
18
  volcano_sdk/errors.py,sha256=ffs66FIN_jhyh5SMj7KdXb-zVV3DSgVK6xJEPNA5h9Y,1613
18
- volcano_sdk/functions.py,sha256=pdKdgDNo13MbEedeDYtAFazK4r7AZtzYQqrfz-xV5Pw,12977
19
- volcano_sdk/locks.py,sha256=ptZq7CvXtlK0VU42Qq6DcQQMMjMqQl1zw9cxqdALMfU,9922
20
- volcano_sdk/logs.py,sha256=6gYFKLdPcG62KrVugpgw57Lqx0WcxKEZ-zosRz-DeW4,3943
19
+ volcano_sdk/functions.py,sha256=7loNEF1tkojAnp_C0rBWWcvO3l22Tx2JQn28l6PoV2Q,13264
20
+ volcano_sdk/locks.py,sha256=Ctr4OKAmJqCPD03lkPEjH7JzYfdpESHEYFOmTj0z1Ds,10894
21
+ volcano_sdk/logs.py,sha256=l71mT5ejIq2_9O_aug5_pZuZPAYkdJQa03RhPvSDH9g,4075
21
22
  volcano_sdk/models.py,sha256=ZIo2YdZ-Ba7rCmHBo3eV9LGjpEftVTeVmrrfq-zGujs,11182
22
23
  volcano_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
- volcano_sdk/realtime.py,sha256=k1vqSDZUfN-lO1zKSGd0lM-Cf9q2ebkdngu8oniLZF8,63091
24
- volcano_sdk/storage.py,sha256=fZKa2Jf0w_JVytQ3UjfFSUZRAJllwSHuqbH3ywzhb8Q,27617
24
+ volcano_sdk/realtime.py,sha256=qulBz-rUm8SFId_jt40z7Q-wDtTb572gi1SKMDqbAto,64213
25
+ volcano_sdk/storage.py,sha256=kh5xN5Pu1gp3fOKyJlmrMGE9ZnN7U9jkLp_l2Ovj1_Y,28081
25
26
  volcano_sdk/_generated/__init__.py,sha256=ZlB8wMYdN6EjpUS5W_lX3jsxq9G1kOcmUr-j3Fccfn8,164
26
27
  volcano_sdk/_generated/client.py,sha256=GDzw6T2uRmPlUb1DImpBGxwy0dNbhmBknfvvIQW0GuU,12387
27
28
  volcano_sdk/_generated/errors.py,sha256=trp-p5qn1_JLRxGZhdHtICaNPaCrcDCe4TgIihBravk,546
@@ -809,7 +810,7 @@ volcano_sdk/_generated/models/usage_data_point.py,sha256=d4U08JbVTiIMM_l2BmRILL1
809
810
  volcano_sdk/_generated/models/variable.py,sha256=t8XaBfTwIH42L8RL4DFDtdqbhVfDEnRS5Axq2q6Tpvk,6405
810
811
  volcano_sdk/_generated/models/variable_deploy_source.py,sha256=G_aCiFHzQx8WhJFs0GvYZEFclfTrfkDvd3kT1e3vty8,463
811
812
  volcano_sdk/_generated/models/variable_status.py,sha256=THSsKBSiAYG4kAtBzGA4TZqxjgx8hdL0Q4qCdZfOj00,391
812
- volcano_sdk_python-0.10.2.dist-info/METADATA,sha256=iZ4NdMioMG22X20j6pclTVxcptrI5soVYsx_GYHfpqw,45435
813
- volcano_sdk_python-0.10.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
814
- volcano_sdk_python-0.10.2.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
815
- volcano_sdk_python-0.10.2.dist-info/RECORD,,
813
+ volcano_sdk_python-0.10.4.dist-info/METADATA,sha256=HGwNfdnW1A-y9EZipIB3jxJbC8y9PDbHDjJBQY0O8-A,45774
814
+ volcano_sdk_python-0.10.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
815
+ volcano_sdk_python-0.10.4.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
816
+ volcano_sdk_python-0.10.4.dist-info/RECORD,,