volcano-sdk-python 0.10.3__py3-none-any.whl → 0.11.0__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/_transport.py +1 -1
- volcano_sdk/durable_authoring.py +121 -89
- volcano_sdk/locks.py +37 -10
- volcano_sdk/realtime.py +2 -2
- volcano_sdk/storage.py +12 -2
- {volcano_sdk_python-0.10.3.dist-info → volcano_sdk_python-0.11.0.dist-info}/METADATA +1 -1
- {volcano_sdk_python-0.10.3.dist-info → volcano_sdk_python-0.11.0.dist-info}/RECORD +9 -9
- {volcano_sdk_python-0.10.3.dist-info → volcano_sdk_python-0.11.0.dist-info}/WHEEL +0 -0
- {volcano_sdk_python-0.10.3.dist-info → volcano_sdk_python-0.11.0.dist-info}/licenses/LICENSE +0 -0
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/durable_authoring.py
CHANGED
|
@@ -131,6 +131,24 @@ class DurableRuntimeMissingError(Exception):
|
|
|
131
131
|
self.__cause__ = cause
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
class _DurableEngine(Protocol):
|
|
135
|
+
"""The operations the Volcano facade needs from the optional runtime."""
|
|
136
|
+
|
|
137
|
+
def seconds(self, value: int) -> object: ...
|
|
138
|
+
|
|
139
|
+
def step_options(self, *, retry: Retry, at_most_once: bool) -> object: ...
|
|
140
|
+
|
|
141
|
+
def wait_condition_options(self, options: WaitUntilOptions) -> object: ...
|
|
142
|
+
|
|
143
|
+
def map_options(self, options: BatchOptions | None) -> object: ...
|
|
144
|
+
|
|
145
|
+
def parallel_options(self, options: BatchOptions | None) -> object: ...
|
|
146
|
+
|
|
147
|
+
def named_branch(
|
|
148
|
+
self, run: Callable[[object], object], name: str | None
|
|
149
|
+
) -> object: ...
|
|
150
|
+
|
|
151
|
+
|
|
134
152
|
class _Engine:
|
|
135
153
|
"""The durable protocol, from the AWS durable execution SDK.
|
|
136
154
|
|
|
@@ -186,6 +204,101 @@ class _Engine:
|
|
|
186
204
|
cls._loaded = cls()
|
|
187
205
|
return cls._loaded
|
|
188
206
|
|
|
207
|
+
def seconds(self, value: int) -> EngineDuration:
|
|
208
|
+
"""Build the runtime's duration from whole seconds."""
|
|
209
|
+
return self.duration.from_seconds(value)
|
|
210
|
+
|
|
211
|
+
def step_options(self, *, retry: Retry, at_most_once: bool) -> StepConfig:
|
|
212
|
+
"""Build the runtime's step options."""
|
|
213
|
+
config: dict[str, Any] = {}
|
|
214
|
+
if at_most_once:
|
|
215
|
+
config["step_semantics"] = self.step_semantics.AT_MOST_ONCE_PER_RETRY
|
|
216
|
+
strategy = self._retry_strategy(retry)
|
|
217
|
+
if strategy is not None:
|
|
218
|
+
config["retry_strategy"] = strategy
|
|
219
|
+
return self.step_config(**config)
|
|
220
|
+
|
|
221
|
+
def _retry_strategy(self, retry: object) -> Any:
|
|
222
|
+
if retry is None or retry is True:
|
|
223
|
+
return None
|
|
224
|
+
# False disables the runtime's default retry policy for this step.
|
|
225
|
+
if retry is False:
|
|
226
|
+
return self._never_retry()
|
|
227
|
+
if isinstance(retry, RetryOptions):
|
|
228
|
+
return self.create_retry_strategy(
|
|
229
|
+
self.retry_strategy_config(**self._retry_kwargs(retry))
|
|
230
|
+
)
|
|
231
|
+
if callable(retry):
|
|
232
|
+
return retry
|
|
233
|
+
raise TypeError(_INVALID_RETRY)
|
|
234
|
+
|
|
235
|
+
def _never_retry(self) -> Callable[[Exception, int], RetryDecision]:
|
|
236
|
+
no_delay = self.seconds(0)
|
|
237
|
+
|
|
238
|
+
def never_retry(_error: Exception, _attempt: int) -> RetryDecision:
|
|
239
|
+
return self.retry_decision(should_retry=False, delay=no_delay)
|
|
240
|
+
|
|
241
|
+
return never_retry
|
|
242
|
+
|
|
243
|
+
def _retry_kwargs(self, retry: RetryOptions) -> dict[str, Any]:
|
|
244
|
+
return _engine_kwargs(
|
|
245
|
+
max_attempts=retry.attempts,
|
|
246
|
+
initial_delay=self._optional_duration(retry.initial_delay, "initial_delay"),
|
|
247
|
+
max_delay=self._optional_duration(retry.max_delay, "max_delay"),
|
|
248
|
+
backoff_rate=retry.backoff_rate,
|
|
249
|
+
retryable_errors=(None if retry.retry_on is None else list(retry.retry_on)),
|
|
250
|
+
retryable_error_types=(
|
|
251
|
+
None if retry.retry_on_types is None else list(retry.retry_on_types)
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _optional_duration(
|
|
256
|
+
self, value: Duration | None, field_name: str
|
|
257
|
+
) -> EngineDuration | None:
|
|
258
|
+
return None if value is None else self.seconds(_to_seconds(value, field_name))
|
|
259
|
+
|
|
260
|
+
def wait_condition_options(self, options: WaitUntilOptions) -> object:
|
|
261
|
+
"""Build the runtime's polling options."""
|
|
262
|
+
until = options.until
|
|
263
|
+
|
|
264
|
+
def keep_polling(state: Any) -> bool:
|
|
265
|
+
return not until(state)
|
|
266
|
+
|
|
267
|
+
strategy = self.wait_strategy_config(
|
|
268
|
+
**_engine_kwargs(
|
|
269
|
+
should_continue_polling=keep_polling,
|
|
270
|
+
max_attempts=options.max_attempts,
|
|
271
|
+
initial_delay=self._optional_duration(options.interval, "interval"),
|
|
272
|
+
max_delay=self._optional_duration(options.max_interval, "max_interval"),
|
|
273
|
+
backoff_rate=options.backoff_rate,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return self.wait_for_condition_config(
|
|
277
|
+
wait_strategy=self.create_wait_strategy(strategy),
|
|
278
|
+
initial_state=options.initial_state,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _batch_options(self, options: BatchOptions | None) -> dict[str, Any]:
|
|
282
|
+
resolved = BatchOptions() if options is None else options
|
|
283
|
+
config = _engine_kwargs(max_concurrency=resolved.concurrency)
|
|
284
|
+
if resolved.min_succeeded is not None:
|
|
285
|
+
config["completion_config"] = self.completion_config(
|
|
286
|
+
min_successful=resolved.min_succeeded
|
|
287
|
+
)
|
|
288
|
+
return config
|
|
289
|
+
|
|
290
|
+
def map_options(self, options: BatchOptions | None) -> object:
|
|
291
|
+
"""Build the runtime's map options."""
|
|
292
|
+
return self.map_config(**self._batch_options(options))
|
|
293
|
+
|
|
294
|
+
def parallel_options(self, options: BatchOptions | None) -> ParallelConfig:
|
|
295
|
+
"""Build the runtime's parallel options."""
|
|
296
|
+
return self.parallel_config(**self._batch_options(options))
|
|
297
|
+
|
|
298
|
+
def named_branch(self, run: Callable[[object], object], name: str | None) -> object:
|
|
299
|
+
"""Build a named runtime branch."""
|
|
300
|
+
return self.parallel_branch(func=run, name=name)
|
|
301
|
+
|
|
189
302
|
|
|
190
303
|
@dataclass(frozen=True, slots=True)
|
|
191
304
|
class RetryOptions:
|
|
@@ -436,7 +549,7 @@ class DurableContext:
|
|
|
436
549
|
|
|
437
550
|
__slots__ = ("_context", "_engine", "log")
|
|
438
551
|
|
|
439
|
-
def __init__(self, context: Any, engine:
|
|
552
|
+
def __init__(self, context: Any, engine: _DurableEngine) -> None:
|
|
440
553
|
"""Wrap an engine context."""
|
|
441
554
|
self._context = context
|
|
442
555
|
self._engine = engine
|
|
@@ -478,7 +591,7 @@ class DurableContext:
|
|
|
478
591
|
self._context.step(
|
|
479
592
|
run,
|
|
480
593
|
step_name,
|
|
481
|
-
self.
|
|
594
|
+
self._engine.step_options(retry=retry, at_most_once=at_most_once),
|
|
482
595
|
),
|
|
483
596
|
)
|
|
484
597
|
|
|
@@ -543,29 +656,13 @@ class DurableContext:
|
|
|
543
656
|
check_func = _callable(check, "wait_until")
|
|
544
657
|
_validate_wait_options(options)
|
|
545
658
|
engine = self._engine
|
|
546
|
-
until = options.until
|
|
547
|
-
|
|
548
|
-
def keep_polling(state: Any) -> bool:
|
|
549
|
-
return not until(state)
|
|
550
659
|
|
|
551
660
|
def check_state(state: Any, scope: Any) -> Any:
|
|
552
661
|
return check_func(state, StepScope(scope.logger, scope.attempt))
|
|
553
662
|
|
|
554
|
-
strategy = engine.wait_strategy_config(
|
|
555
|
-
**_engine_kwargs(
|
|
556
|
-
should_continue_polling=keep_polling,
|
|
557
|
-
max_attempts=options.max_attempts,
|
|
558
|
-
initial_delay=self._optional_duration(options.interval, "interval"),
|
|
559
|
-
max_delay=self._optional_duration(options.max_interval, "max_interval"),
|
|
560
|
-
backoff_rate=options.backoff_rate,
|
|
561
|
-
)
|
|
562
|
-
)
|
|
563
663
|
return self._context.wait_for_condition(
|
|
564
664
|
check_state,
|
|
565
|
-
engine.
|
|
566
|
-
wait_strategy=engine.create_wait_strategy(strategy),
|
|
567
|
-
initial_state=options.initial_state,
|
|
568
|
-
),
|
|
665
|
+
engine.wait_condition_options(options),
|
|
569
666
|
name,
|
|
570
667
|
)
|
|
571
668
|
|
|
@@ -601,7 +698,7 @@ class DurableContext:
|
|
|
601
698
|
list(items),
|
|
602
699
|
run,
|
|
603
700
|
name,
|
|
604
|
-
engine.
|
|
701
|
+
engine.map_options(options),
|
|
605
702
|
)
|
|
606
703
|
)
|
|
607
704
|
|
|
@@ -623,7 +720,7 @@ class DurableContext:
|
|
|
623
720
|
self._context.parallel(
|
|
624
721
|
[self._branch(branch) for branch in branches],
|
|
625
722
|
name,
|
|
626
|
-
engine.
|
|
723
|
+
engine.parallel_options(options),
|
|
627
724
|
)
|
|
628
725
|
)
|
|
629
726
|
|
|
@@ -650,7 +747,7 @@ class DurableContext:
|
|
|
650
747
|
def run_named(context: Any) -> Any:
|
|
651
748
|
return named(DurableContext(context, engine))
|
|
652
749
|
|
|
653
|
-
return engine.
|
|
750
|
+
return engine.named_branch(run_named, branch.name)
|
|
654
751
|
require_callable(branch, _INVALID_BRANCH)
|
|
655
752
|
bare = branch
|
|
656
753
|
|
|
@@ -659,72 +756,7 @@ class DurableContext:
|
|
|
659
756
|
|
|
660
757
|
return run_bare
|
|
661
758
|
|
|
662
|
-
def
|
|
663
|
-
resolved = BatchOptions() if options is None else options
|
|
664
|
-
config = _engine_kwargs(max_concurrency=resolved.concurrency)
|
|
665
|
-
if resolved.min_succeeded is not None:
|
|
666
|
-
config["completion_config"] = self._engine.completion_config(
|
|
667
|
-
min_successful=resolved.min_succeeded
|
|
668
|
-
)
|
|
669
|
-
return config
|
|
670
|
-
|
|
671
|
-
def _step_config(self, *, retry: Retry, at_most_once: bool) -> StepConfig:
|
|
672
|
-
engine = self._engine
|
|
673
|
-
config: dict[str, Any] = {}
|
|
674
|
-
if at_most_once:
|
|
675
|
-
config["step_semantics"] = engine.step_semantics.AT_MOST_ONCE_PER_RETRY
|
|
676
|
-
strategy = self._retry_strategy(retry)
|
|
677
|
-
if strategy is not None:
|
|
678
|
-
config["retry_strategy"] = strategy
|
|
679
|
-
return engine.step_config(**config)
|
|
680
|
-
|
|
681
|
-
def _retry_strategy(self, retry: object) -> Any:
|
|
682
|
-
engine = self._engine
|
|
683
|
-
if retry is None or retry is True:
|
|
684
|
-
return None
|
|
685
|
-
# retry=False means "fail on the first error", which is not the same as
|
|
686
|
-
# leaving retry unset: the platform retries by default, and a step that
|
|
687
|
-
# is not safe to repeat wants the opposite.
|
|
688
|
-
if retry is False:
|
|
689
|
-
return self._never_retry()
|
|
690
|
-
if isinstance(retry, RetryOptions):
|
|
691
|
-
return engine.create_retry_strategy(
|
|
692
|
-
engine.retry_strategy_config(**self._retry_kwargs(retry))
|
|
693
|
-
)
|
|
694
|
-
if callable(retry):
|
|
695
|
-
return retry
|
|
696
|
-
raise TypeError(_INVALID_RETRY)
|
|
697
|
-
|
|
698
|
-
def _never_retry(self) -> Callable[[Exception, int], RetryDecision]:
|
|
699
|
-
engine = self._engine
|
|
700
|
-
no_delay = engine.duration.from_seconds(0)
|
|
701
|
-
|
|
702
|
-
def never_retry(_error: Exception, _attempt: int) -> RetryDecision:
|
|
703
|
-
return engine.retry_decision(should_retry=False, delay=no_delay)
|
|
704
|
-
|
|
705
|
-
return never_retry
|
|
706
|
-
|
|
707
|
-
def _retry_kwargs(self, retry: RetryOptions) -> dict[str, Any]:
|
|
708
|
-
return _engine_kwargs(
|
|
709
|
-
max_attempts=retry.attempts,
|
|
710
|
-
initial_delay=self._optional_duration(retry.initial_delay, "initial_delay"),
|
|
711
|
-
max_delay=self._optional_duration(retry.max_delay, "max_delay"),
|
|
712
|
-
backoff_rate=retry.backoff_rate,
|
|
713
|
-
retryable_errors=(None if retry.retry_on is None else list(retry.retry_on)),
|
|
714
|
-
retryable_error_types=(
|
|
715
|
-
None if retry.retry_on_types is None else list(retry.retry_on_types)
|
|
716
|
-
),
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
def _optional_duration(
|
|
720
|
-
self, value: Duration | None, field_name: str
|
|
721
|
-
) -> EngineDuration | None:
|
|
722
|
-
return None if value is None else self._duration(value, field_name)
|
|
723
|
-
|
|
724
|
-
def _duration(self, value: object, field_name: str) -> EngineDuration:
|
|
725
|
-
return self._engine.duration.from_seconds(_to_seconds(value, field_name))
|
|
726
|
-
|
|
727
|
-
def _wait_duration(self, value: object) -> EngineDuration:
|
|
759
|
+
def _wait_duration(self, value: object) -> object:
|
|
728
760
|
seconds = _to_seconds(value, "wait")
|
|
729
761
|
if seconds < _MIN_WAIT_SECONDS:
|
|
730
762
|
message = f"wait must be at least {_MIN_WAIT_SECONDS} second"
|
|
@@ -732,7 +764,7 @@ class DurableContext:
|
|
|
732
764
|
if seconds > _MAX_WAIT_SECONDS:
|
|
733
765
|
message = f"wait must be at most {_MAX_WAIT_SECONDS} seconds (366 days)"
|
|
734
766
|
raise TypeError(message)
|
|
735
|
-
return self._engine.
|
|
767
|
+
return self._engine.seconds(seconds)
|
|
736
768
|
|
|
737
769
|
|
|
738
770
|
@overload
|
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/realtime.py
CHANGED
|
@@ -35,6 +35,7 @@ from ._transport import (
|
|
|
35
35
|
invoke_async,
|
|
36
36
|
response_payload,
|
|
37
37
|
)
|
|
38
|
+
from .database import _database_rows
|
|
38
39
|
from .models import JSONValue, _freeze_json
|
|
39
40
|
|
|
40
41
|
if TYPE_CHECKING:
|
|
@@ -1391,8 +1392,7 @@ class Realtime:
|
|
|
1391
1392
|
"limit": len(row_ids),
|
|
1392
1393
|
},
|
|
1393
1394
|
)
|
|
1394
|
-
|
|
1395
|
-
rows = list(payload["data"])
|
|
1395
|
+
rows = _database_rows(response_payload(response, 200))
|
|
1396
1396
|
return tuple(
|
|
1397
1397
|
next(
|
|
1398
1398
|
(row for row in rows if row.get("id") == request.row_id),
|
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,
|
|
14
|
+
from typing import TYPE_CHECKING, 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:
|
|
@@ -462,6 +463,13 @@ def _upload_content_type(value: object) -> str:
|
|
|
462
463
|
return value
|
|
463
464
|
|
|
464
465
|
|
|
466
|
+
def _is_string_keyed_mapping(value: object) -> TypeGuard[Mapping[str, object]]:
|
|
467
|
+
if not isinstance(value, Mapping):
|
|
468
|
+
return False
|
|
469
|
+
mapping = cast("Mapping[object, object]", value)
|
|
470
|
+
return all(isinstance(key, str) for key in mapping)
|
|
471
|
+
|
|
472
|
+
|
|
465
473
|
@dataclass(frozen=True, slots=True)
|
|
466
474
|
class StorageBucket:
|
|
467
475
|
"""Operations scoped to one storage bucket."""
|
|
@@ -475,7 +483,7 @@ class StorageBucket:
|
|
|
475
483
|
data: bytes | BinaryIO,
|
|
476
484
|
*,
|
|
477
485
|
content_type: str | None = None,
|
|
478
|
-
) -> dict[str,
|
|
486
|
+
) -> dict[str, object]:
|
|
479
487
|
"""Upload bytes or the remaining contents of a binary stream.
|
|
480
488
|
|
|
481
489
|
Returns:
|
|
@@ -498,6 +506,8 @@ class StorageBucket:
|
|
|
498
506
|
binding=binding,
|
|
499
507
|
)
|
|
500
508
|
payload = response_payload(response, 201)
|
|
509
|
+
if not _is_string_keyed_mapping(payload):
|
|
510
|
+
raise TypeError(_INVALID_UPLOAD_RESPONSE)
|
|
501
511
|
return dict(payload)
|
|
502
512
|
|
|
503
513
|
def download(self, path: str, *, byte_range: str | None = None) -> bytes:
|
|
@@ -8,21 +8,21 @@ volcano_sdk/_log_response.py,sha256=wZaugr0eD0ZLSa-M6TUEwdIDsI_wblyDXucYEwNK14c,
|
|
|
8
8
|
volcano_sdk/_realtime_fetch_worker.py,sha256=f3mUPe5qZmGuhA9jfpqEUtpLZmvlJ1FhI1XkNz238FI,10406
|
|
9
9
|
volcano_sdk/_session.py,sha256=w7B1rwbhH3yoE8kFA6gSpUpQb24rPwppotMu-IPHXdI,3025
|
|
10
10
|
volcano_sdk/_session_operations.py,sha256=s_m-Gl-knjDrjUOFl6z2lJW4IGHAgCSCjRNA7LCB5j0,5727
|
|
11
|
-
volcano_sdk/_transport.py,sha256=
|
|
11
|
+
volcano_sdk/_transport.py,sha256=XcDk8NAEAHmtH3nmlMWIixcIiEFb-FP_n8VU44QcuXg,61907
|
|
12
12
|
volcano_sdk/auth.py,sha256=OxK9s6XW9jNGA_cXos-cHEZYcmYsxrojruSyLS0mWVs,52237
|
|
13
13
|
volcano_sdk/client.py,sha256=KH52e5WC4ii-hnipB9vNzSDEyfvvpOMBCdtwzRqNUOI,12701
|
|
14
14
|
volcano_sdk/connection_string.py,sha256=p87P-fd9wVfia-LLRRsFTkW6tzx9FXn-GAy9OIOoDvE,2664
|
|
15
15
|
volcano_sdk/database.py,sha256=XuXGqlfEPvlOBB33jyTVNhRGe4vrgzOBne2rTRzkQis,13806
|
|
16
16
|
volcano_sdk/durable.py,sha256=YQa_5tT4kxkHTyP4nRMhjWyExnSICXD_RAipSQ1cax8,13422
|
|
17
|
-
volcano_sdk/durable_authoring.py,sha256=
|
|
17
|
+
volcano_sdk/durable_authoring.py,sha256=92ENW366IdeP7P-ABqo-HB8V7YsG8KkE_ekVWnu9ZXU,36899
|
|
18
18
|
volcano_sdk/errors.py,sha256=ffs66FIN_jhyh5SMj7KdXb-zVV3DSgVK6xJEPNA5h9Y,1613
|
|
19
19
|
volcano_sdk/functions.py,sha256=7loNEF1tkojAnp_C0rBWWcvO3l22Tx2JQn28l6PoV2Q,13264
|
|
20
|
-
volcano_sdk/locks.py,sha256=
|
|
20
|
+
volcano_sdk/locks.py,sha256=Ctr4OKAmJqCPD03lkPEjH7JzYfdpESHEYFOmTj0z1Ds,10894
|
|
21
21
|
volcano_sdk/logs.py,sha256=l71mT5ejIq2_9O_aug5_pZuZPAYkdJQa03RhPvSDH9g,4075
|
|
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=
|
|
25
|
-
volcano_sdk/storage.py,sha256=
|
|
24
|
+
volcano_sdk/realtime.py,sha256=qulBz-rUm8SFId_jt40z7Q-wDtTb572gi1SKMDqbAto,64213
|
|
25
|
+
volcano_sdk/storage.py,sha256=FUuE5SnF5IV-S4OF-sFtMqzidWvUC_Gt5Xw5NW7aUss,28137
|
|
26
26
|
volcano_sdk/_generated/__init__.py,sha256=ZlB8wMYdN6EjpUS5W_lX3jsxq9G1kOcmUr-j3Fccfn8,164
|
|
27
27
|
volcano_sdk/_generated/client.py,sha256=GDzw6T2uRmPlUb1DImpBGxwy0dNbhmBknfvvIQW0GuU,12387
|
|
28
28
|
volcano_sdk/_generated/errors.py,sha256=trp-p5qn1_JLRxGZhdHtICaNPaCrcDCe4TgIihBravk,546
|
|
@@ -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.
|
|
814
|
-
volcano_sdk_python-0.
|
|
815
|
-
volcano_sdk_python-0.
|
|
816
|
-
volcano_sdk_python-0.
|
|
813
|
+
volcano_sdk_python-0.11.0.dist-info/METADATA,sha256=paWdliEZVJYayjiBqVjB_zjpTx8faq62pbpH5ZZOHZM,45774
|
|
814
|
+
volcano_sdk_python-0.11.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
815
|
+
volcano_sdk_python-0.11.0.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
|
|
816
|
+
volcano_sdk_python-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
{volcano_sdk_python-0.10.3.dist-info → volcano_sdk_python-0.11.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|