volcano-sdk-python 0.7.2__py3-none-any.whl → 0.8.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.
@@ -54,13 +54,16 @@ class LockGuard:
54
54
  *,
55
55
  ttl: int,
56
56
  started_at: float,
57
+ lease_started_at: float | None = None,
57
58
  ) -> None:
58
59
  """Track one acquired lease against its local monotonic deadline."""
59
60
  self._state_lock = threading.Lock()
60
61
  self._lease = lease
61
62
  self._ttl = ttl
62
63
  self._absolute_deadline = started_at + MAX_LOCK_LIFETIME_SECONDS
63
- self._lease_deadline = min(started_at + ttl, self._absolute_deadline)
64
+ # A successful retry renews the TTL, not the maximum ownership lifetime.
65
+ lease_started_at = started_at if lease_started_at is None else lease_started_at
66
+ self._lease_deadline = min(lease_started_at + ttl, self._absolute_deadline)
64
67
  self._failure: Exception | None = None
65
68
  self._lost = threading.Event()
66
69
 
volcano_sdk/_transport.py CHANGED
@@ -13,14 +13,10 @@ from uuid import UUID, uuid4
13
13
  import httpx
14
14
 
15
15
  from ._generated.api.authentication import (
16
- auth_confirm_email_change,
17
- auth_convert_anonymous,
18
- auth_get_user,
19
16
  auth_logout,
20
17
  auth_refresh,
21
18
  auth_signin,
22
19
  auth_signup,
23
- auth_update_user,
24
20
  )
25
21
  from ._generated.api.authentication.auth_cancel_email_change import (
26
22
  _get_kwargs as cancel_email_change_kwargs,
@@ -28,6 +24,18 @@ from ._generated.api.authentication.auth_cancel_email_change import (
28
24
  from ._generated.api.authentication.auth_confirm_email import (
29
25
  _get_kwargs as confirm_email_kwargs,
30
26
  )
27
+ from ._generated.api.authentication.auth_confirm_email_change import (
28
+ _build_response as build_auth_confirm_email_change_response,
29
+ )
30
+ from ._generated.api.authentication.auth_confirm_email_change import (
31
+ _get_kwargs as auth_confirm_email_change_kwargs,
32
+ )
33
+ from ._generated.api.authentication.auth_convert_anonymous import (
34
+ _build_response as build_auth_convert_anonymous_response,
35
+ )
36
+ from ._generated.api.authentication.auth_convert_anonymous import (
37
+ _get_kwargs as auth_convert_anonymous_kwargs,
38
+ )
31
39
  from ._generated.api.authentication.auth_delete_all_my_sessions import (
32
40
  _get_kwargs as delete_all_my_sessions_kwargs,
33
41
  )
@@ -40,6 +48,12 @@ from ._generated.api.authentication.auth_forgot_password import (
40
48
  from ._generated.api.authentication.auth_get_my_sessions import (
41
49
  _get_kwargs as get_my_sessions_kwargs,
42
50
  )
51
+ from ._generated.api.authentication.auth_get_user import (
52
+ _build_response as build_auth_get_user_response,
53
+ )
54
+ from ._generated.api.authentication.auth_get_user import (
55
+ _get_kwargs as auth_get_user_kwargs,
56
+ )
43
57
  from ._generated.api.authentication.auth_request_email_change import (
44
58
  _get_kwargs as request_email_change_kwargs,
45
59
  )
@@ -52,6 +66,12 @@ from ._generated.api.authentication.auth_reset_password import (
52
66
  from ._generated.api.authentication.auth_signup_anonymous import (
53
67
  _get_kwargs as signup_anonymous_kwargs,
54
68
  )
69
+ from ._generated.api.authentication.auth_update_user import (
70
+ _build_response as build_auth_update_user_response,
71
+ )
72
+ from ._generated.api.authentication.auth_update_user import (
73
+ _get_kwargs as auth_update_user_kwargs,
74
+ )
55
75
  from ._generated.api.database_queries import (
56
76
  query_database_select,
57
77
  )
@@ -84,12 +104,17 @@ from ._generated.api.functions.invoke_function import (
84
104
  _get_kwargs as invoke_function_kwargs,
85
105
  )
86
106
  from ._generated.api.locks import (
87
- acquire_project_lock,
88
107
  force_release_project_lock,
89
108
  get_project_lock,
90
109
  release_project_lock,
91
110
  renew_project_lock,
92
111
  )
112
+ from ._generated.api.locks.acquire_project_lock import (
113
+ _build_response as build_lock_acquire_response,
114
+ )
115
+ from ._generated.api.locks.acquire_project_lock import (
116
+ _get_kwargs as lock_acquire_kwargs,
117
+ )
93
118
  from ._generated.api.logs.get_project_log_activity import (
94
119
  _build_response as build_log_activity_response,
95
120
  )
@@ -233,6 +258,7 @@ if TYPE_CHECKING:
233
258
  )
234
259
  from .models import JSONValue
235
260
 
261
+ HTTP_CREATED = 201
236
262
  HTTP_UNAUTHORIZED = 401
237
263
  HTTP_NOT_FOUND = 404
238
264
  HTTP_CONFLICT = 409
@@ -623,6 +649,7 @@ class Transport(Protocol):
623
649
  key: str,
624
650
  ttl: int,
625
651
  token: str,
652
+ request_id: str | None = None,
626
653
  ) -> TransportResponse: ...
627
654
 
628
655
  def release_project_lock(
@@ -631,6 +658,7 @@ class Transport(Protocol):
631
658
  authorization: str,
632
659
  key: str,
633
660
  token: str,
661
+ request_id: str | None = None,
634
662
  ) -> TransportResponse: ...
635
663
 
636
664
 
@@ -829,9 +857,13 @@ class GeneratedTransport:
829
857
  )
830
858
  try:
831
859
  with self._client(authorization) as client:
832
- response = auth_convert_anonymous.sync_detailed(
833
- client=client,
834
- body=body,
860
+ raw_response = client.get_httpx_client().request(
861
+ **auth_convert_anonymous_kwargs(body=body)
862
+ )
863
+ if raw_response.status_code == HTTP_UNAUTHORIZED:
864
+ return self._raw_response(raw_response)
865
+ response = build_auth_convert_anonymous_response(
866
+ client=client, response=raw_response
835
867
  )
836
868
  except (
837
869
  AttributeError,
@@ -928,9 +960,13 @@ class GeneratedTransport:
928
960
  body = AuthConfirmEmailChangeBody(email_change_token=token)
929
961
  try:
930
962
  with self._client(authorization) as client:
931
- response = auth_confirm_email_change.sync_detailed(
932
- client=client,
933
- body=body,
963
+ raw_response = client.get_httpx_client().request(
964
+ **auth_confirm_email_change_kwargs(body=body)
965
+ )
966
+ if raw_response.status_code == HTTP_UNAUTHORIZED:
967
+ return self._raw_response(raw_response)
968
+ response = build_auth_confirm_email_change_response(
969
+ client=client, response=raw_response
934
970
  )
935
971
  except (
936
972
  AttributeError,
@@ -1204,7 +1240,14 @@ class GeneratedTransport:
1204
1240
  def auth_get_user(self, *, authorization: str) -> TransportResponse:
1205
1241
  try:
1206
1242
  with self._client(authorization) as client:
1207
- response = auth_get_user.sync_detailed(client=client)
1243
+ raw_response = client.get_httpx_client().request(
1244
+ **auth_get_user_kwargs()
1245
+ )
1246
+ if raw_response.status_code == HTTP_UNAUTHORIZED:
1247
+ return self._raw_response(raw_response)
1248
+ response = build_auth_get_user_response(
1249
+ client=client, response=raw_response
1250
+ )
1208
1251
  except (
1209
1252
  AttributeError,
1210
1253
  KeyError,
@@ -1239,7 +1282,14 @@ class GeneratedTransport:
1239
1282
  )
1240
1283
  try:
1241
1284
  with self._client(authorization) as client:
1242
- response = auth_update_user.sync_detailed(client=client, body=body)
1285
+ raw_response = client.get_httpx_client().request(
1286
+ **auth_update_user_kwargs(body=body)
1287
+ )
1288
+ if raw_response.status_code == HTTP_UNAUTHORIZED:
1289
+ return self._raw_response(raw_response)
1290
+ response = build_auth_update_user_response(
1291
+ client=client, response=raw_response
1292
+ )
1243
1293
  except (
1244
1294
  AttributeError,
1245
1295
  KeyError,
@@ -1693,15 +1743,19 @@ class GeneratedTransport:
1693
1743
  key: str,
1694
1744
  ttl: int,
1695
1745
  token: str,
1746
+ request_id: str | None = None,
1696
1747
  ) -> TransportResponse:
1697
1748
  with self._client(authorization) as client:
1698
- response = acquire_project_lock.sync_detailed(
1749
+ request_kwargs = lock_acquire_kwargs(
1699
1750
  key,
1700
- client=client,
1701
1751
  body=ProjectLockLeaseRequest(ttl_seconds=ttl),
1702
1752
  x_volcano_lock_token=cast("UUID", token),
1703
- x_volcano_request_id=cast("UUID", str(uuid4())),
1753
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1704
1754
  )
1755
+ raw_response = client.get_httpx_client().request(**request_kwargs)
1756
+ if raw_response.status_code != HTTP_CREATED:
1757
+ return self._raw_response(raw_response)
1758
+ response = build_lock_acquire_response(client=client, response=raw_response)
1705
1759
  return self._response(response)
1706
1760
 
1707
1761
  def get_project_lock(
@@ -1709,12 +1763,13 @@ class GeneratedTransport:
1709
1763
  *,
1710
1764
  authorization: str,
1711
1765
  key: str,
1766
+ request_id: str | None = None,
1712
1767
  ) -> TransportResponse:
1713
1768
  with self._client(authorization) as client:
1714
1769
  response = get_project_lock.sync_detailed(
1715
1770
  key,
1716
1771
  client=client,
1717
- x_volcano_request_id=cast("UUID", str(uuid4())),
1772
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1718
1773
  )
1719
1774
  return self._response(response)
1720
1775
 
@@ -1723,12 +1778,13 @@ class GeneratedTransport:
1723
1778
  *,
1724
1779
  authorization: str,
1725
1780
  key: str,
1781
+ request_id: str | None = None,
1726
1782
  ) -> TransportResponse:
1727
1783
  with self._client(authorization) as client:
1728
1784
  response = force_release_project_lock.sync_detailed(
1729
1785
  key,
1730
1786
  client=client,
1731
- x_volcano_request_id=cast("UUID", str(uuid4())),
1787
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1732
1788
  )
1733
1789
  return self._response(response)
1734
1790
 
@@ -1739,6 +1795,7 @@ class GeneratedTransport:
1739
1795
  key: str,
1740
1796
  ttl: int,
1741
1797
  token: str,
1798
+ request_id: str | None = None,
1742
1799
  ) -> TransportResponse:
1743
1800
  with self._client(authorization) as client:
1744
1801
  response = renew_project_lock.sync_detailed(
@@ -1746,7 +1803,7 @@ class GeneratedTransport:
1746
1803
  client=client,
1747
1804
  body=ProjectLockLeaseRequest(ttl_seconds=ttl),
1748
1805
  x_volcano_lock_token=cast("UUID", token),
1749
- x_volcano_request_id=cast("UUID", str(uuid4())),
1806
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1750
1807
  )
1751
1808
  return self._response(response)
1752
1809
 
@@ -1756,12 +1813,13 @@ class GeneratedTransport:
1756
1813
  authorization: str,
1757
1814
  key: str,
1758
1815
  token: str,
1816
+ request_id: str | None = None,
1759
1817
  ) -> TransportResponse:
1760
1818
  with self._client(authorization) as client:
1761
1819
  response = release_project_lock.sync_detailed(
1762
1820
  key,
1763
1821
  client=client,
1764
1822
  x_volcano_lock_token=cast("UUID", token),
1765
- x_volcano_request_id=cast("UUID", str(uuid4())),
1823
+ x_volcano_request_id=cast("UUID", request_id or str(uuid4())),
1766
1824
  )
1767
1825
  return self._response(response)
volcano_sdk/auth.py CHANGED
@@ -9,6 +9,7 @@ import secrets
9
9
  import threading
10
10
  from collections.abc import Mapping
11
11
  from contextlib import suppress
12
+ from copy import deepcopy
12
13
  from dataclasses import replace
13
14
  from datetime import datetime
14
15
  from http import HTTPStatus
@@ -574,18 +575,22 @@ class Auth:
574
575
  metadata: Mapping[str, object] | None = None,
575
576
  ) -> User:
576
577
  """Attach email credentials to the current anonymous account."""
577
- generation, current = self._client._capture_session()
578
- if current is None:
578
+ binding = self._client._capture_session_binding()
579
+ if binding[2] is None:
579
580
  raise AuthenticationError(_NO_ACTIVE_SESSION)
580
581
  transport = cast("AuthConvertAnonymousTransport", self._client._transport)
581
- response = invoke(
582
- transport.auth_convert_anonymous,
583
- authorization=current.access_token,
584
- email=email,
585
- password=password,
586
- metadata=dict(metadata or {}),
582
+ request_metadata = deepcopy(dict(metadata or {}))
583
+ response = self._session_request(
584
+ lambda access_token: invoke(
585
+ transport.auth_convert_anonymous,
586
+ authorization=access_token,
587
+ email=email,
588
+ password=password,
589
+ metadata=request_metadata,
590
+ ),
591
+ binding=binding,
587
592
  )
588
- return self._update_current_user(response_payload(response, 200), generation)
593
+ return self._update_current_user(response_payload(response, 200), binding)
589
594
 
590
595
  def reset_password_for_email(self, *, email: str) -> None:
591
596
  """Request a reset email without revealing whether the account exists."""
@@ -629,16 +634,19 @@ class Auth:
629
634
 
630
635
  def confirm_email_change(self, *, token: str) -> User:
631
636
  """Confirm a pending email change and return the updated user."""
632
- generation, current = self._client._capture_session()
633
- if current is None:
637
+ binding = self._client._capture_session_binding()
638
+ if binding[2] is None:
634
639
  raise AuthenticationError(_NO_ACTIVE_SESSION)
635
640
  transport = cast("AuthConfirmEmailChangeTransport", self._client._transport)
636
- response = invoke(
637
- transport.auth_confirm_email_change,
638
- authorization=current.access_token,
639
- token=token,
641
+ response = self._session_request(
642
+ lambda access_token: invoke(
643
+ transport.auth_confirm_email_change,
644
+ authorization=access_token,
645
+ token=token,
646
+ ),
647
+ binding=binding,
640
648
  )
641
- return self._update_current_user(response_payload(response, 200), generation)
649
+ return self._update_current_user(response_payload(response, 200), binding)
642
650
 
643
651
  def delete_all_other_sessions(self) -> None:
644
652
  """Delete every other session while preserving the current session."""
@@ -944,7 +952,10 @@ class Auth:
944
952
  )
945
953
  response_payload(response, 200)
946
954
 
947
- def _update_current_user(self, payload: object, generation: int) -> User:
955
+ def _update_current_user(
956
+ self, payload: object, binding: tuple[int, int, Session | None]
957
+ ) -> User:
958
+ generation = self._owned_refresh_session(binding)[0]
948
959
  user, snapshot = _user_from_payload(payload)
949
960
  if not self._client._update_session_user_if_current(snapshot, generation):
950
961
  raise SessionChangedError
@@ -952,12 +963,17 @@ class Auth:
952
963
 
953
964
  def get_user(self) -> User:
954
965
  """Load a server-validated profile for the current session."""
955
- generation, current = self._client._capture_session()
956
- if current is None:
966
+ binding = self._client._capture_session_binding()
967
+ if binding[2] is None:
957
968
  raise AuthenticationError(_NO_ACTIVE_SESSION)
958
969
  transport = cast("AuthGetUserTransport", self._client._transport)
959
- response = invoke(transport.auth_get_user, authorization=current.access_token)
960
- return self._update_current_user(response_payload(response, 200), generation)
970
+ response = self._session_request(
971
+ lambda access_token: invoke(
972
+ transport.auth_get_user, authorization=access_token
973
+ ),
974
+ binding=binding,
975
+ )
976
+ return self._update_current_user(response_payload(response, 200), binding)
961
977
 
962
978
  def update_user(
963
979
  self,
@@ -966,17 +982,21 @@ class Auth:
966
982
  metadata: Mapping[str, object] | None = None,
967
983
  ) -> User:
968
984
  """Update and return the current user's server-validated profile."""
969
- generation, current = self._client._capture_session()
970
- if current is None:
985
+ binding = self._client._capture_session_binding()
986
+ if binding[2] is None:
971
987
  raise AuthenticationError(_NO_ACTIVE_SESSION)
972
988
  transport = cast("AuthUpdateUserTransport", self._client._transport)
973
- response = invoke(
974
- transport.auth_update_user,
975
- authorization=current.access_token,
976
- password=password,
977
- metadata=None if metadata is None else dict(metadata),
989
+ request_metadata = None if metadata is None else deepcopy(dict(metadata))
990
+ response = self._session_request(
991
+ lambda access_token: invoke(
992
+ transport.auth_update_user,
993
+ authorization=access_token,
994
+ password=password,
995
+ metadata=request_metadata,
996
+ ),
997
+ binding=binding,
978
998
  )
979
- return self._update_current_user(response_payload(response, 200), generation)
999
+ return self._update_current_user(response_payload(response, 200), binding)
980
1000
 
981
1001
  def sign_in(self, *, email: str, password: str) -> Session:
982
1002
  """Sign in a user and store the returned session."""
volcano_sdk/locks.py CHANGED
@@ -5,11 +5,12 @@ from __future__ import annotations
5
5
  from contextlib import contextmanager, suppress
6
6
  from datetime import datetime
7
7
  from typing import TYPE_CHECKING, Protocol, cast
8
- from uuid import uuid4
8
+ from uuid import UUID, uuid4
9
9
 
10
10
  from ._lock_guard import LockGuard, _lease_now
11
11
  from ._lock_worker import LockRenewer
12
12
  from ._transport import Transport, invoke, response_payload
13
+ from .errors import ServerError, TransportError
13
14
  from .models import LockLease, LockState
14
15
 
15
16
  if TYPE_CHECKING:
@@ -37,6 +38,7 @@ class LockGetTransport(Protocol):
37
38
  *,
38
39
  authorization: str,
39
40
  key: str,
41
+ request_id: str | None = None,
40
42
  ) -> object:
41
43
  """Get one project-scoped lock."""
42
44
  ...
@@ -52,6 +54,7 @@ class LockRenewTransport(Protocol):
52
54
  key: str,
53
55
  ttl: int,
54
56
  token: str,
57
+ request_id: str | None = None,
55
58
  ) -> object:
56
59
  """Renew one project-scoped lock."""
57
60
  ...
@@ -65,6 +68,7 @@ class LockForceReleaseTransport(Protocol):
65
68
  *,
66
69
  authorization: str,
67
70
  key: str,
71
+ request_id: str | None = None,
68
72
  ) -> object:
69
73
  """Force release one project-scoped lock."""
70
74
  ...
@@ -85,6 +89,17 @@ def _validate_ttl(ttl: object) -> None:
85
89
  raise ValueError(_INVALID_LOCK_TTL)
86
90
 
87
91
 
92
+ def _request_uuid(value: str | None, name: str) -> str:
93
+ if value is None:
94
+ return str(uuid4())
95
+ try:
96
+ UUID(value)
97
+ except (AttributeError, ValueError) as error:
98
+ message = f"{name} must be a UUID string"
99
+ raise ValueError(message) from error
100
+ return value
101
+
102
+
88
103
  class Locks:
89
104
  """Acquire and release project-scoped distributed locks."""
90
105
 
@@ -92,13 +107,14 @@ class Locks:
92
107
  """Create a lock facade backed by a client."""
93
108
  self._client = client
94
109
 
95
- def get(self, key: str) -> LockState:
110
+ def get(self, key: str, *, request_id: str | None = None) -> LockState:
96
111
  """Return the current state of a project-scoped lock."""
97
112
  transport = cast("LockGetTransport", self._client._transport)
98
113
  response = invoke(
99
114
  transport.get_project_lock,
100
115
  authorization=self._client._service_token(),
101
116
  key=key,
117
+ request_id=_request_uuid(request_id, "request_id"),
102
118
  )
103
119
  payload = response_payload(response, 200)
104
120
  return LockState(
@@ -107,26 +123,64 @@ class Locks:
107
123
  fencing_token=payload.get("fencing_token"),
108
124
  )
109
125
 
110
- def acquire(self, key: str, *, ttl: int) -> LockLease:
111
- """Acquire a lock lease for the requested number of seconds."""
126
+ def acquire(
127
+ self,
128
+ key: str,
129
+ *,
130
+ ttl: int,
131
+ token: str | None = None,
132
+ request_id: str | None = None,
133
+ ) -> LockLease:
134
+ """Acquire with one bounded retry using the same ownership token."""
135
+ lease, _ = self._acquire_with_start(
136
+ key, ttl=ttl, token=token, request_id=request_id
137
+ )
138
+ return lease
139
+
140
+ def _acquire_with_start(
141
+ self,
142
+ key: str,
143
+ *,
144
+ ttl: int,
145
+ token: str | None,
146
+ request_id: str | None,
147
+ ) -> tuple[LockLease, float]:
112
148
  _validate_ttl(ttl)
113
- token = str(uuid4())
114
- response = invoke(
115
- self._client._transport.acquire_project_lock,
116
- authorization=self._client._service_token(),
149
+ token = _request_uuid(token, "token")
150
+ request_id = _request_uuid(request_id, "request_id")
151
+ authorization = self._client._service_token()
152
+ started_at = _lease_now()
153
+ try:
154
+ payload = self._acquire_payload(key, ttl, token, request_id, authorization)
155
+ except (TransportError, ServerError) as error:
156
+ if error.status not in (None, 503):
157
+ raise
158
+ started_at = _lease_now()
159
+ payload = self._acquire_payload(key, ttl, token, request_id, authorization)
160
+ lease = LockLease(
117
161
  key=key,
118
- ttl=ttl,
119
162
  token=token,
163
+ expires_at=_parse_datetime(payload.get("expires_at")),
164
+ fencing_token=cast("int | None", payload.get("fencing_token")),
120
165
  )
121
- payload = response_payload(response, 201)
122
- return LockLease(
166
+ return lease, started_at
167
+
168
+ def _acquire_payload(
169
+ self, key: str, ttl: int, token: str, request_id: str, authorization: str
170
+ ) -> dict[str, object]:
171
+ response = invoke(
172
+ self._client._transport.acquire_project_lock,
173
+ authorization=authorization,
123
174
  key=key,
175
+ ttl=ttl,
124
176
  token=token,
125
- expires_at=_parse_datetime(payload.get("expires_at")),
126
- fencing_token=payload.get("fencing_token"),
177
+ request_id=request_id,
127
178
  )
179
+ return cast("dict[str, object]", response_payload(response, 201))
128
180
 
129
- def renew(self, key: str, lease: LockLease, *, ttl: int) -> LockLease:
181
+ def renew(
182
+ self, key: str, lease: LockLease, *, ttl: int, request_id: str | None = None
183
+ ) -> LockLease:
130
184
  """Renew a lock lease and return its immutable replacement."""
131
185
  _validate_ttl(ttl)
132
186
  transport = cast("LockRenewTransport", self._client._transport)
@@ -134,6 +188,7 @@ class Locks:
134
188
  transport.renew_project_lock,
135
189
  authorization=self._client._service_token(),
136
190
  key=key,
191
+ request_id=_request_uuid(request_id, "request_id"),
137
192
  ttl=ttl,
138
193
  token=lease.token,
139
194
  )
@@ -145,35 +200,47 @@ class Locks:
145
200
  fencing_token=payload.get("fencing_token"),
146
201
  )
147
202
 
148
- def release(self, key: str, lease: LockLease) -> None:
203
+ def release(
204
+ self, key: str, lease: LockLease, *, request_id: str | None = None
205
+ ) -> None:
149
206
  """Release a lock lease."""
150
207
  response = invoke(
151
208
  self._client._transport.release_project_lock,
152
209
  authorization=self._client._service_token(),
153
210
  key=key,
211
+ request_id=_request_uuid(request_id, "request_id"),
154
212
  token=lease.token,
155
213
  )
156
214
  response_payload(response, 204)
157
215
 
158
- def force_release(self, key: str) -> None:
216
+ def force_release(self, key: str, *, request_id: str | None = None) -> None:
159
217
  """Release a lock regardless of which token owns it."""
160
218
  transport = cast("LockForceReleaseTransport", self._client._transport)
161
219
  response = invoke(
162
220
  transport.force_release_project_lock,
163
221
  authorization=self._client._service_token(),
164
222
  key=key,
223
+ request_id=_request_uuid(request_id, "request_id"),
165
224
  )
166
225
  response_payload(response, 204)
167
226
 
168
227
  @contextmanager
169
- def with_lock(self, key: str, *, ttl: int) -> Generator[LockGuard, None, None]:
228
+ def with_lock(
229
+ self,
230
+ key: str,
231
+ *,
232
+ ttl: int,
233
+ token: str | None = None,
234
+ request_id: str | None = None,
235
+ ) -> Generator[LockGuard, None, None]:
170
236
  """Hold and automatically renew a lock for the context's lifetime."""
171
237
  _validate_ttl(ttl)
172
238
  started_at = _lease_now()
239
+ lease, lease_started_at = self._acquire_with_start(
240
+ key, ttl=ttl, token=token, request_id=request_id
241
+ )
173
242
  guard = LockGuard(
174
- self.acquire(key, ttl=ttl),
175
- ttl=ttl,
176
- started_at=started_at,
243
+ lease, ttl=ttl, started_at=started_at, lease_started_at=lease_started_at
177
244
  )
178
245
  renewer = LockRenewer(self, key, guard, ttl=ttl)
179
246
  renewer_started = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: volcano-sdk-python
3
- Version: 0.7.2
3
+ Version: 0.8.0
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
@@ -25,6 +25,8 @@ Description-Content-Type: text/markdown
25
25
  Use the Volcano Python SDK to access authentication, databases, storage,
26
26
  functions, locks, logs, and realtime events. Requires Python 3.11 or later.
27
27
 
28
+ Start with the [Python quickstart](https://github.com/Kong/volcano-sdk-python/blob/main/docs/README.md).
29
+
28
30
  ## Install
29
31
 
30
32
  ```shell
@@ -180,6 +182,13 @@ original error. `on_progress` runs after each successful part with cumulative
180
182
  uploaded bytes and the total size.
181
183
  `upload_part()` returns immutable part metadata and can safely retry the same
182
184
  part number to replace that part.
185
+ Acquisition accepts caller-owned UUID `token` and `request_id` values and retries
186
+ an ambiguous transport failure or HTTP 503 once with the same request and credential.
187
+ Retain those IDs to recover an uncertain acquisition. Other lock methods accept
188
+ `request_id`; block-scoped helpers forward initial IDs only to acquisition.
189
+ See the [lock guide](https://github.com/Kong/volcano-sdk-python/blob/main/docs/locks.md)
190
+ for examples and fencing requirements.
191
+
183
192
  `locks.get()` returns immutable lock availability, expiry, and fencing-token
184
193
  state without acquiring the lock.
185
194
  Lock acquisition and renewal require an integer TTL from 5 seconds through 90 days.
@@ -1,17 +1,17 @@
1
1
  volcano_sdk/__init__.py,sha256=rPSFMLjFbxeQvSRkL1Ee8MsNWArIhs8fgNFgtDuMNzI,1954
2
2
  volcano_sdk/_function_resolution.py,sha256=_gC3LY6x71NtJc_o9d9eq3N27Z-T_Un3p2-XRu2-Cqs,5088
3
- volcano_sdk/_lock_guard.py,sha256=EZzcwOPTFTCNUrIWIQ0-nMg9tm_nPkaYkcocgeLbu94,5572
3
+ volcano_sdk/_lock_guard.py,sha256=VhHTgBc4e5EDj8j15vVIhdBOTp6fvD628x3hVBeF39o,5794
4
4
  volcano_sdk/_lock_renewer.py,sha256=tFo1ZeRiPKB5VSbW61uoMpNR67ZDYyMEe-dCjpAM3As,841
5
5
  volcano_sdk/_lock_worker.py,sha256=QhSmwR0WcwgT-7ueJMlbCogLqOakQ5eRAtyWV74fgV0,3057
6
6
  volcano_sdk/_realtime_fetch_worker.py,sha256=zQTgFF4cIUBeAL4J2OpWAlz_sIynv2BLfu588EogAdo,9269
7
- volcano_sdk/_transport.py,sha256=apXZLuT9W17vO-7Ag2E289o2B-uL-Rndv_wRPm69x0I,55481
8
- volcano_sdk/auth.py,sha256=Uhw_jDQvE7AyAjAJ2Tf6qAgT7N0QLDyx8OkJ_u0fE8Q,42525
7
+ volcano_sdk/_transport.py,sha256=AFd47C3X1WAHwq3f9V2ux0WNzwGIn4NBxrwgaIN-xl0,58296
8
+ volcano_sdk/auth.py,sha256=Z5UCrqdvmHBS6n6ZleXya04hYV94rH4SRV1GW2yimXo,43185
9
9
  volcano_sdk/client.py,sha256=KutjBUOEJyK73OibKilrcWazpWeXKEZYUehSAH8qxUE,10366
10
10
  volcano_sdk/connection_string.py,sha256=LlTZigsrCF4IfAqQ8SDTWJv35hnnzNu1MfeVv5JW-Kk,2331
11
11
  volcano_sdk/database.py,sha256=p_5OaqlBjKAtIUfKQCflXM_CGqokrVmpOMsp6KHiJ8A,9493
12
12
  volcano_sdk/errors.py,sha256=ffs66FIN_jhyh5SMj7KdXb-zVV3DSgVK6xJEPNA5h9Y,1613
13
13
  volcano_sdk/functions.py,sha256=zDeAKyCieNnI6wzb9iZMlg9_cbjaYWXYbXHb7jzCle4,10126
14
- volcano_sdk/locks.py,sha256=iWMhdbZQDw44ZNwENaZWu7WelakjpHhjYzKzufD7SNU,6932
14
+ volcano_sdk/locks.py,sha256=eSkidfHQ_wml5-1LT2hBtHxeALP6E1cGYK0FzHjbrNk,9241
15
15
  volcano_sdk/logs.py,sha256=lxORRQBvsnJ0BdMau19MXrIeig5_mEucQSyJ4F1CZxU,4666
16
16
  volcano_sdk/models.py,sha256=43zM6LQ9eJ3Hn6q9PiVSozSmOzPSr4m4OL8G8KVb-gg,8619
17
17
  volcano_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -773,7 +773,7 @@ volcano_sdk/_generated/models/usage_data_point.py,sha256=d4U08JbVTiIMM_l2BmRILL1
773
773
  volcano_sdk/_generated/models/variable.py,sha256=tvGDOMv77gW6j5skufnfrlNAhzCzA1F7nuPCQWx79yY,5926
774
774
  volcano_sdk/_generated/models/variable_deploy_source.py,sha256=G_aCiFHzQx8WhJFs0GvYZEFclfTrfkDvd3kT1e3vty8,463
775
775
  volcano_sdk/_generated/models/variable_status.py,sha256=THSsKBSiAYG4kAtBzGA4TZqxjgx8hdL0Q4qCdZfOj00,391
776
- volcano_sdk_python-0.7.2.dist-info/METADATA,sha256=5NJwDw5xHBhxDWTB3V__IEGWbB49s8hj06lh7wnhYpA,33451
777
- volcano_sdk_python-0.7.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
778
- volcano_sdk_python-0.7.2.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
779
- volcano_sdk_python-0.7.2.dist-info/RECORD,,
776
+ volcano_sdk_python-0.8.0.dist-info/METADATA,sha256=lJ43KIGGSaMGAsRRM_EqJYI9AJXnEibMrg9CqElZU78,34010
777
+ volcano_sdk_python-0.8.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
778
+ volcano_sdk_python-0.8.0.dist-info/licenses/LICENSE,sha256=AIHJy-5MFqM8fHfuGFTTb9a8rvJGypcVgN4aZ-7XTWw,11387
779
+ volcano_sdk_python-0.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any