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.
- volcano_sdk/_callbacks.py +12 -0
- volcano_sdk/_transport.py +1 -1
- volcano_sdk/auth.py +2 -2
- volcano_sdk/client.py +7 -7
- volcano_sdk/database.py +43 -18
- volcano_sdk/durable.py +13 -5
- volcano_sdk/durable_authoring.py +90 -29
- volcano_sdk/functions.py +36 -18
- volcano_sdk/locks.py +37 -10
- volcano_sdk/logs.py +10 -3
- volcano_sdk/realtime.py +62 -21
- volcano_sdk/storage.py +14 -4
- {volcano_sdk_python-0.10.2.dist-info → volcano_sdk_python-0.10.4.dist-info}/METADATA +9 -2
- {volcano_sdk_python-0.10.2.dist-info → volcano_sdk_python-0.10.4.dist-info}/RECORD +16 -15
- {volcano_sdk_python-0.10.2.dist-info → volcano_sdk_python-0.10.4.dist-info}/WHEEL +0 -0
- {volcano_sdk_python-0.10.2.dist-info → volcano_sdk_python-0.10.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -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) ->
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
111
|
+
def __init__(self, client: DurableClientContext) -> None:
|
|
104
112
|
"""Bind durable operations to a Volcano client."""
|
|
105
113
|
self._client = client
|
|
106
114
|
|
volcano_sdk/durable_authoring.py
CHANGED
|
@@ -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
|
|
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 =
|
|
145
|
-
|
|
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:
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
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:
|
|
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],
|
|
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) ->
|
|
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(
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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[
|
|
762
|
-
func: Callable[
|
|
822
|
+
name: str | Callable[_P, T] | None,
|
|
823
|
+
func: Callable[_P, T] | None,
|
|
763
824
|
operation: str,
|
|
764
|
-
) -> tuple[str | None, Callable[
|
|
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:
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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 .
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
273
|
+
store_missing(api_url, authorization, name, error)
|
|
252
274
|
raise
|
|
253
275
|
resolution = self._resolution(payload, api_url)
|
|
254
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
180
|
-
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
|
-
) ->
|
|
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
|
|
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=
|
|
224
|
-
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 .
|
|
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:
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1670
|
-
|
|
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
|
|
161
|
-
raw_paths = tuple(
|
|
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
|
-
|
|
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.
|
|
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
|
|
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=
|
|
11
|
-
volcano_sdk/auth.py,sha256=
|
|
12
|
-
volcano_sdk/client.py,sha256=
|
|
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=
|
|
15
|
-
volcano_sdk/durable.py,sha256=
|
|
16
|
-
volcano_sdk/durable_authoring.py,sha256=
|
|
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=
|
|
19
|
-
volcano_sdk/locks.py,sha256=
|
|
20
|
-
volcano_sdk/logs.py,sha256=
|
|
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=
|
|
24
|
-
volcano_sdk/storage.py,sha256=
|
|
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.
|
|
813
|
-
volcano_sdk_python-0.10.
|
|
814
|
-
volcano_sdk_python-0.10.
|
|
815
|
-
volcano_sdk_python-0.10.
|
|
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,,
|
|
File without changes
|
{volcano_sdk_python-0.10.2.dist-info → volcano_sdk_python-0.10.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|