modAL 1.5.4.dev9__py3-none-any.whl → 1.5.4.dev11__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.
@@ -2,6 +2,7 @@
2
2
  import asyncio
3
3
  import base64
4
4
  import json
5
+ import random
5
6
  import time
6
7
  from typing import Any
7
8
 
@@ -17,8 +18,14 @@ AUTH_DENIED_EXCEPTIONS = (AuthError, PermissionDeniedError)
17
18
  class _AuthTokenManager:
18
19
  """Handles fetching and refreshing of the input plane auth token."""
19
20
 
20
- # Start refreshing this many seconds before the token expires
21
- REFRESH_WINDOW = 5 * 60
21
+ # Fraction of the token's lifetime to use before refreshing.
22
+ REFRESH_FRACTION = 0.5
23
+ # Width of the random band added on top of REFRESH_FRACTION, so that clients issued
24
+ # same-lifetime tokens at the same moment (a fan-out of containers starting together)
25
+ # spread their refreshes out instead of refreshing in lockstep every cycle. The band
26
+ # only ever delays a refresh, so the margin before expiry stays at no less than
27
+ # 1 - REFRESH_FRACTION - REFRESH_JITTER of the lifetime.
28
+ REFRESH_JITTER = 0.1
22
29
  # Bound each AuthTokenGet attempt so a refresh can't hang.
23
30
  AUTH_TOKEN_TIMEOUT = 5.0
24
31
  AUTH_TOKEN_RETRY_TOTAL_TIMEOUT = 3 * AUTH_TOKEN_TIMEOUT
@@ -33,9 +40,10 @@ class _AuthTokenManager:
33
40
  self._stub = stub
34
41
  self._token = ""
35
42
  self._expiry = 0.0
43
+ self._refresh_at = 0.0
36
44
  self._retry_after = 0.0
37
45
  self._backoff = self.FAILURE_BACKOFF_BASE
38
- self._lock: asyncio.Lock | None = None
46
+ self._refresh_task: asyncio.Task | None = None
39
47
 
40
48
  async def get_token(self) -> str:
41
49
  """
@@ -45,21 +53,20 @@ class _AuthTokenManager:
45
53
  concurrently by multiple coroutines, all requests will block until the token has been fetched. But only one
46
54
  coroutine will actually make a request to the control plane to fetch the new token. This ensures we do not hit
47
55
  the control plane with more requests than needed.
48
- 3. Has a valid cached token, but it is going to expire in the next 5 minutes. In this case we fetch a new token
49
- and cache it. If `get_token` is called concurrently, only one request will fetch the new token, and the others
50
- will be given the old (but still valid) token - i.e. they will not block.
56
+ 3. Has a valid cached token that has used up its refresh fraction of its lifetime. In this case we fetch a new
57
+ token and cache it. If `get_token` is called concurrently, only one request will fetch the new token, and the
58
+ others will be given the old (but still valid) token - i.e. they will not block.
51
59
  """
52
60
  if not self._token or self._is_expired():
53
61
  # We either have no token or it is expired - block everyone until we get a new token
54
62
  await self._try_refresh_token()
55
63
  elif self._should_refresh():
56
- # The token hasn't expired yet, but will soon, so it needs a refresh.
57
- lock = await self._get_lock()
58
- if lock.locked():
59
- # The lock is taken, so someone else is refreshing. Continue to use the old token.
64
+ # The token is still valid, but has used up its refresh fraction of its lifetime.
65
+ if self._refresh_task is not None:
66
+ # A refresh is in flight, so someone else is refreshing. Continue to use the old token.
60
67
  return self._token
61
68
  else:
62
- # The lock is not taken, so we need to fetch a new token.
69
+ # Nobody is refreshing, so we need to fetch a new token.
63
70
  await self._try_refresh_token()
64
71
 
65
72
  return self._token
@@ -76,58 +83,63 @@ class _AuthTokenManager:
76
83
 
77
84
  async def _refresh_token(self):
78
85
  """
79
- Fetch a new token from the control plane. If called concurrently, only one coroutine will make a request for a
80
- new token. The others will block on a lock, until the first coroutine has fetched the new token.
86
+ Fetch a new token from the control plane. Concurrent callers share a single request: the first caller starts it
87
+ and the others await the same task, so a burst of callers results in one RPC whether it succeeds or fails. The
88
+ task is owned by the event loop rather than by the caller that started it, so it runs to completion even if that
89
+ caller goes away.
81
90
  """
82
- lock = await self._get_lock()
83
- async with lock:
84
- # Double check inside lock - maybe another coroutine refreshed already. This happens the first time we fetch
85
- # the token. The first coroutine will fetch the token, while the others block on the lock, waiting for the
86
- # new token. Once we have a new token, the other coroutines will unblock and return from here.
91
+ if self._refresh_task is None:
92
+ # Maybe another coroutine refreshed already, or we're cooling down after a failure.
87
93
  if self._token and not self._should_refresh():
88
94
  return
89
- try:
90
- resp: api_pb2.AuthTokenGetResponse = await self._stub.AuthTokenGet(
91
- api_pb2.AuthTokenGetRequest(),
92
- # No cached token to fall back on, so a failure is user-visible: retry transient errors.
93
- # Otherwise one attempt, and _retry_after handles the cooldown.
94
- retry=Retry(
95
- attempt_timeout=self.AUTH_TOKEN_TIMEOUT,
96
- total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
97
- max_retries=0 if self._token else DEFAULT_MAX_RETRIES,
98
- ),
95
+ # Note: the task is created in async code so it binds to the synchronicity event loop.
96
+ self._refresh_task = asyncio.create_task(self._fetch_token())
97
+ self._refresh_task.add_done_callback(self._refresh_task_done)
98
+ # Shielded so a cancelled caller doesn't cancel the refresh for everyone else.
99
+ await asyncio.shield(self._refresh_task)
100
+
101
+ def _refresh_task_done(self, task: "asyncio.Task"):
102
+ if self._refresh_task is task:
103
+ self._refresh_task = None
104
+ if not task.cancelled():
105
+ # Retrieve the exception, if any, so a failure isn't logged as unhandled when no caller is waiting.
106
+ task.exception()
107
+
108
+ async def _fetch_token(self):
109
+ """Make the AuthTokenGet request and cache its token, or arm the failure backoff."""
110
+ try:
111
+ resp: api_pb2.AuthTokenGetResponse = await self._stub.AuthTokenGet(
112
+ api_pb2.AuthTokenGetRequest(),
113
+ # No cached token to fall back on, so a failure is user-visible: retry transient errors.
114
+ # Otherwise one attempt, and _retry_after handles the cooldown.
115
+ retry=Retry(
116
+ attempt_timeout=self.AUTH_TOKEN_TIMEOUT,
117
+ total_timeout=self.AUTH_TOKEN_RETRY_TOTAL_TIMEOUT,
118
+ max_retries=0 if self._token else DEFAULT_MAX_RETRIES,
119
+ ),
120
+ )
121
+ if not resp.token:
122
+ # Not expected
123
+ raise ExecutionError(
124
+ "Internal error: Did not receive auth token from server. Please contact Modal support."
99
125
  )
100
- if not resp.token:
101
- # Not expected
102
- raise ExecutionError(
103
- "Internal error: Did not receive auth token from server. Please contact Modal support."
104
- )
105
- if exp := self._decode_jwt(resp.token).get("exp"):
106
- expiry = float(exp)
107
- else:
108
- # This should never happen.
109
- logger.warning("x-modal-auth-token does not contain exp field")
110
- expiry = time.time() + self.DEFAULT_EXPIRY_OFFSET
111
- self._token = resp.token
112
- self._expiry = expiry
113
- self._retry_after = 0.0
114
- self._backoff = self.FAILURE_BACKOFF_BASE
115
- except AUTH_DENIED_EXCEPTIONS:
116
- raise
117
- except Exception as e:
118
- # Back off (exponentially on consecutive failures) so we don't hammer a struggling server.
119
- self._retry_after = time.time() + self._backoff
120
- self._backoff = min(self._backoff * 2, self.FAILURE_BACKOFF_MAX)
121
- raise
122
-
123
- async def _get_lock(self) -> asyncio.Lock:
124
- # Note: this function runs no async code but is marked as async to ensure it's
125
- # being run inside the synchronicity event loop and binds the lock to the
126
- # correct event loop on Python 3.9 which eagerly assigns event loops on
127
- # constructions of locks
128
- if self._lock is None:
129
- self._lock = asyncio.Lock()
130
- return self._lock
126
+ if exp := self._decode_jwt(resp.token).get("exp"):
127
+ expiry = float(exp)
128
+ else:
129
+ # This should never happen.
130
+ logger.warning("x-modal-auth-token does not contain exp field")
131
+ expiry = time.time() + self.DEFAULT_EXPIRY_OFFSET
132
+ self._token = resp.token
133
+ self._set_expiry(expiry)
134
+ self._retry_after = 0.0
135
+ self._backoff = self.FAILURE_BACKOFF_BASE
136
+ except AUTH_DENIED_EXCEPTIONS:
137
+ raise
138
+ except Exception as e:
139
+ # Back off (exponentially on consecutive failures) so we don't hammer a struggling server.
140
+ self._retry_after = time.time() + self._backoff
141
+ self._backoff = min(self._backoff * 2, self.FAILURE_BACKOFF_MAX)
142
+ raise
131
143
 
132
144
  @staticmethod
133
145
  def _decode_jwt(token: str) -> dict[str, Any]:
@@ -150,8 +162,20 @@ class _AuthTokenManager:
150
162
  # Fetch only when the token is stale/expiring and we're not in a post-failure backoff.
151
163
  return self._needs_refresh() and not self._in_backoff()
152
164
 
165
+ def _set_expiry(self, expiry: float) -> None:
166
+ """Record a new expiry and schedule the proactive refresh a jittered fraction of the way through.
167
+
168
+ The delay is measured from the remaining lifetime as this client sees it, so a client whose clock is
169
+ skewed relative to the issuer still keeps a margin proportional to the lifetime it observes.
170
+ """
171
+ now = time.time()
172
+ self._expiry = expiry
173
+ remaining = max(expiry - now, 0.0)
174
+ fraction = self.REFRESH_FRACTION + random.random() * self.REFRESH_JITTER
175
+ self._refresh_at = now + remaining * fraction
176
+
153
177
  def _needs_refresh(self):
154
- return time.time() >= (self._expiry - self.REFRESH_WINDOW)
178
+ return time.time() >= self._refresh_at
155
179
 
156
180
  def _is_expired(self):
157
181
  return time.time() >= self._expiry
@@ -292,9 +292,10 @@ class TaskCommandRouterClient:
292
292
  ) -> "TaskCommandRouterClient":
293
293
  """Initialize a TaskCommandRouterClient for a V2 sandbox.
294
294
 
295
- `access` is the router access returned by SandboxCreateV2, which lets a
296
- freshly created sandbox connect without a round-trip. When it is absent —
297
- a re-attached sandbox, or the server could not mint a token — we ask for it.
295
+ `access` is the router access returned by SandboxCreateV2/SandboxRestoreV2,
296
+ which lets a freshly scheduled sandbox connect without a round-trip. When it
297
+ is absent — a re-attached sandbox, or the server could not mint a token — we
298
+ ask for it.
298
299
  """
299
300
  if access is not None:
300
301
  url, jwt = access.url, access.jwt
modal/client.pyi CHANGED
@@ -37,7 +37,7 @@ class _Client:
37
37
  server_url: str,
38
38
  client_type: int,
39
39
  credentials: typing.Optional[tuple[str, str]],
40
- version: str = "1.5.4.dev9",
40
+ version: str = "1.5.4.dev11",
41
41
  ):
42
42
  """mdmd:hidden
43
43
  The Modal client object is not intended to be instantiated directly by users.
@@ -207,7 +207,7 @@ class Client:
207
207
  server_url: str,
208
208
  client_type: int,
209
209
  credentials: typing.Optional[tuple[str, str]],
210
- version: str = "1.5.4.dev9",
210
+ version: str = "1.5.4.dev11",
211
211
  ):
212
212
  """mdmd:hidden
213
213
  The Modal client object is not intended to be instantiated directly by users.
modal/sandbox.py CHANGED
@@ -2288,6 +2288,9 @@ class _Sandbox(_Object, type_prefix="sb"):
2288
2288
 
2289
2289
  sandbox = await _Sandbox.from_id(restore_resp.sandbox_id, client)
2290
2290
  sandbox._task_id = restore_resp.task_id
2291
+ # Seeded after from_id, whose hydration resets the field.
2292
+ if restore_resp.HasField("command_router_access"):
2293
+ sandbox._init_command_router_access = restore_resp.command_router_access
2291
2294
  return sandbox
2292
2295
 
2293
2296
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.5.4.dev9
3
+ Version: 1.5.4.dev11
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -34,7 +34,7 @@ modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
34
34
  modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
35
35
  modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
36
36
  modal/client.py,sha256=NjpAL0fIKabEaN0uXJPEoI1OTjFtXSp4tSck-M-WF3k,16360
37
- modal/client.pyi,sha256=XKsjCTpM6Q265diR31Ob1HVD6pkl_4rJ9_AXAQQ2NxA,17997
37
+ modal/client.pyi,sha256=QFNaYNlNXJCAo7pTbX4AgqHCxU5VOx_qpUcuCBHYI3E,17999
38
38
  modal/cloud_bucket_mount.py,sha256=NrdNV9r9aL3QFrlt-Dhtd7_K3f8v1igrHf2FRwgJ1CI,6827
39
39
  modal/cloud_bucket_mount.pyi,sha256=0E8d53pMh2KsFa-rMh_i6LxlzW4cMV8drVmmkPHSqZk,8149
40
40
  modal/cls.py,sha256=TxRxHdY4L6AZ6I16DNPgDryAkGtlFadD-Mm0ovtfN58,41206
@@ -76,7 +76,7 @@ modal/retries.py,sha256=tSGB5bGM5xZZUDXMkuelIEoQJY0vOukOssZhwc6tXAo,5014
76
76
  modal/runner.py,sha256=oqKyahvUEDpbGcNkxoCtKqFUShKWhoYEPecn7pk-fc4,31090
77
77
  modal/runner.pyi,sha256=HjkJBkv3GPIe9occmuQUBRgVuw82vrTL0urM8zV4TaY,9760
78
78
  modal/running_app.py,sha256=xSR1VVlhxEFytmjx2x697_0GpD25Vc9crhp6r117_QY,1145
79
- modal/sandbox.py,sha256=BLpMLDRF39a3FmY_Eusg_ejvW6yCsjHJY0hNsWPRko4,125868
79
+ modal/sandbox.py,sha256=p-QgzNALkRb0q_qRc-K28HpgAe1LRyomc3URd3QsRBY,126078
80
80
  modal/sandbox.pyi,sha256=PXlInwHCuZYNXkRO0AKMpX3yKTAmhliN-ajiXltcsb0,146622
81
81
  modal/sandbox_fs.py,sha256=GSuPlrLSdQ7fG7c_TGMd-VWs_s6RqQxMvLivdvC6s4s,27002
82
82
  modal/sandbox_fs.pyi,sha256=TJWx8h7x3yRC7KUxeF7q62_0a7w1IHufnJL7AYEojAg,45285
@@ -118,7 +118,7 @@ modal/_runtime/user_code_imports.py,sha256=5MgewJzAeZqop47FQyB-lEpHBfC50ApHgSb86
118
118
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
119
119
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
120
120
  modal/_utils/async_utils.py,sha256=2pBzV_ErMH055TGxT0re2szYUN6k8X94RkLwIp_JbZw,46393
121
- modal/_utils/auth_token_manager.py,sha256=_XAYZsMDgtRqVeyNBjcr88pMnZbg49n8D8wxDhqLltg,7335
121
+ modal/_utils/auth_token_manager.py,sha256=qhnbTv6EO5_BxvUUlyCrNt8sQaUIZ77CdSrzBH9sLaU,8563
122
122
  modal/_utils/blob_utils.py,sha256=KyqXWF82PdfA6pvEu99QwGfSqyHQxPUvjyi760GXkdY,26242
123
123
  modal/_utils/browser_utils.py,sha256=Yqq0qU4Id6oaDOdeT1JEmbCN7vj4baNJJraJhRWU8ZI,1085
124
124
  modal/_utils/bytes_io_segment_payload.py,sha256=2OrIrjrsJR38a9D7JCITLqGvIPaiWViTQhxtcZlFn_M,4328
@@ -142,7 +142,7 @@ modal/_utils/proxy_support.py,sha256=hLA_XojYxzPysOiLqyUbB-SgMLmn_X8WtXTk92xh7g4
142
142
  modal/_utils/rand_pb_testing.py,sha256=kwAKIros_g6XPXluiJAktQ79YPkD9SGopKGl83GQ1l0,4361
143
143
  modal/_utils/sandbox_fs_utils.py,sha256=rsfo1uDbVSoiY21XERDzsmp-DALcNmlPfJ6jn_2p5T0,15680
144
144
  modal/_utils/shell_utils.py,sha256=_2yHrlx4Crx5JWQXECLPQ51kclu8H-U3N7jOkBN7q5U,3390
145
- modal/_utils/task_command_router_client.py,sha256=q0MyeG5Q6diOSkN89FIg_uODQ1Ix2C4IrOvBYh2fOq0,46046
145
+ modal/_utils/task_command_router_client.py,sha256=QJxxzceZDrr9BEDVs524tZ17tgO3wEVDw-4gF8-P96U,46073
146
146
  modal/_utils/time_utils.py,sha256=JaFyW8y4iimzhrGT5KlQmlXH4oMZrZOGCtUBZAV-gfU,10965
147
147
  modal/_vendor/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
148
148
  modal/_vendor/a2wsgi_wsgi.py,sha256=RVLIYVJA8a8o96QxEXYT5tuog2zev2S24XOuAVTXZAs,21872
@@ -198,7 +198,7 @@ modal/experimental/flash.py,sha256=GAJ1c-EGL8M3_loAkqr5gdUfySMHY3U_oc6RywfZ8-0,3
198
198
  modal/experimental/flash.pyi,sha256=lpTqJFZRMGWscw0xpYZVdBMnAYjj16asXE4_64A6itQ,17774
199
199
  modal/experimental/ipython.py,sha256=kJ6o9wkAY_MiSb7paewI7f1MJAeqdi8TX5pBdhr_mUI,2925
200
200
  modal/skills/modal/SKILL.md,sha256=WzLjYJ1utjc_ZOf6qZTPwB6wxxwRYB5PibdvIf_FLy0,5316
201
- modal-1.5.4.dev9.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
201
+ modal-1.5.4.dev11.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
202
202
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
203
203
  modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
204
204
  modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
@@ -209,23 +209,23 @@ modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,2
209
209
  modal_docs/mdmd/mdmd.py,sha256=J49V8ak-hnF7SyBJDc-p3a_aEZpaAImkz2Rf74mDvA4,25070
210
210
  modal_docs/mdmd/signatures.py,sha256=OASroKle763Kfumn4Ek4PABJXEGP56JMCTug44QoBx4,11327
211
211
  modal_docs/mdmd/types.py,sha256=tv7fvziPr-MeDzOp4e2pK5h3d1DXTUOO74rPqDoBn6g,736
212
- modal_proto/__init__.py,sha256=tFdSv9eaHBvYcwhgq-XqIlYDNDw57clrvO91Fb-oUV4,130
213
- modal_proto/api_grpc.py,sha256=J8CYJap2EuMDb1ct4beDGBxfN5d6uyJHTbdVySIZVrE,183310
214
- modal_proto/api_pb2.py,sha256=DU9_PKaAnqpEhWPwNtMi7V4feZ6niWhycTnBN1uevgU,500582
215
- modal_proto/api_pb2.pyi,sha256=glpbAVDbXk0M7j4C4dfTsgnpsmN4z1tS_Mn9fHPcoU8,692212
216
- modal_proto/api_pb2_grpc.py,sha256=nTPxiGWnzwoI5CDwOzQBs-YNm2MbbR4hY-0k-1O4q54,393438
217
- modal_proto/api_pb2_grpc.pyi,sha256=e2XqayYGQCGmq3oy2Oy4nT29vZZ3wZYSJeZS2OZ-IEM,91991
218
- modal_proto/modal_api_grpc.py,sha256=brzTrJA6gH0678l8r4BF7Uj6XNcw-4PDorV0AYfs4QA,28921
212
+ modal_proto/__init__.py,sha256=Hkou4lcD5tArEkW_9v9KZhLY7fl0fA9-O28beKU55pg,396
213
+ modal_proto/api_grpc.py,sha256=T7G1w30VHACC466T9WgEuAUKzk1T55K4xhh9yrx30jE,184199
214
+ modal_proto/api_pb2.py,sha256=Cv-bAV11PEAgWXYeQHcLBgmjvgjDvwR1MrrdT7hNXr0,503011
215
+ modal_proto/api_pb2.pyi,sha256=RrSAYQpmuc2pCz3sF3zbTcmC7R1oOppakVUdwm-IKOs,695248
216
+ modal_proto/api_pb2_grpc.py,sha256=iVzwMoa9O8vZKvPX4gCFq3f5QYTMpHFsLa-mEc-Zwis,395203
217
+ modal_proto/api_pb2_grpc.pyi,sha256=Clzhm_sI17aGhz31Rtl2MpCdfeM_8PlGu2O4aH92fbk,92749
218
+ modal_proto/modal_api_grpc.py,sha256=36yBgbLRHAKnchTZlRHVQyYWbFTTc_xrx6j8JHz1Ozo,29059
219
219
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
220
220
  modal_proto/task_command_router_grpc.py,sha256=UurJEgLeAc2CCdEsCOFE9DgIQy1ra6bXHXJLNBK81Mg,21284
221
221
  modal_proto/task_command_router_pb2.py,sha256=4cyDT_uPGi54blsO663bnSzUe7aBFqi9F4iM8N1JPaU,41130
222
222
  modal_proto/task_command_router_pb2.pyi,sha256=5xfhnwtTDTmJo1Nv0tPBJrYzb0aeilY2Lm92wWT_6yw,46211
223
223
  modal_proto/task_command_router_pb2_grpc.py,sha256=zuFkJgKcswB2ftjnyEgs70dp61VMA7gK0h0uzR3gYFw,42563
224
224
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=UjAJFqz9DpOFXP04fVm5lB2eAyTtjOg3oKjfWei8yLU,14262
225
- modal_version/__init__.py,sha256=nwgaaUSs19Z9kxmj-Qv_8W5K88StfjfUwGbnX6TZZto,120
225
+ modal_version/__init__.py,sha256=f197D89MvvrXc5P_dA9HTULCibAz73m15IoOoWlbd3A,121
226
226
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
227
- modal-1.5.4.dev9.dist-info/METADATA,sha256=fehlLhzMf8xnGjFBE5lyi_od-Aykmk_lnntqWkpn1Rw,2512
228
- modal-1.5.4.dev9.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
229
- modal-1.5.4.dev9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
230
- modal-1.5.4.dev9.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
231
- modal-1.5.4.dev9.dist-info/RECORD,,
227
+ modal-1.5.4.dev11.dist-info/METADATA,sha256=Zebs41aU_dhmL3bh94SKRb1jqPY9pnIwnlZiXsJkf88,2513
228
+ modal-1.5.4.dev11.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
229
+ modal-1.5.4.dev11.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
230
+ modal-1.5.4.dev11.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
231
+ modal-1.5.4.dev11.dist-info/RECORD,,
modal_proto/__init__.py CHANGED
@@ -1,4 +1,11 @@
1
1
  # Copyright Modal Labs 2024
2
+ """Modal gRPC types for Python.
3
+
4
+ Warning: direct usage of Modal's gRPC API is discouraged,
5
+ and no support or compatibility guarantees are provided.
6
+ Message fields or entire RPCs may be changed or removed
7
+ without notice. We recommend using official SDKs instead.
8
+ """
2
9
  from pkgutil import extend_path
3
10
 
4
11
  __path__ = extend_path(__path__, __name__) # type: ignore[has-type]
modal_proto/api_grpc.py CHANGED
@@ -123,6 +123,10 @@ class ModalClientBase(abc.ABC):
123
123
  async def AuthTokenGet(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.AuthTokenGetRequest, modal_proto.api_pb2.AuthTokenGetResponse]') -> None:
124
124
  pass
125
125
 
126
+ @abc.abstractmethod
127
+ async def AutoscalerOverrideHistory(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.AutoscalerOverrideHistoryRequest, modal_proto.api_pb2.AutoscalerOverrideHistoryResponse]') -> None:
128
+ pass
129
+
126
130
  @abc.abstractmethod
127
131
  async def BlobCreate(self, stream: 'grpclib.server.Stream[modal_proto.api_pb2.BlobCreateRequest, modal_proto.api_pb2.BlobCreateResponse]') -> None:
128
132
  pass
@@ -1117,6 +1121,12 @@ class ModalClientBase(abc.ABC):
1117
1121
  modal_proto.api_pb2.AuthTokenGetRequest,
1118
1122
  modal_proto.api_pb2.AuthTokenGetResponse,
1119
1123
  ),
1124
+ '/modal.client.ModalClient/AutoscalerOverrideHistory': grpclib.const.Handler(
1125
+ self.AutoscalerOverrideHistory,
1126
+ grpclib.const.Cardinality.UNARY_UNARY,
1127
+ modal_proto.api_pb2.AutoscalerOverrideHistoryRequest,
1128
+ modal_proto.api_pb2.AutoscalerOverrideHistoryResponse,
1129
+ ),
1120
1130
  '/modal.client.ModalClient/BlobCreate': grpclib.const.Handler(
1121
1131
  self.BlobCreate,
1122
1132
  grpclib.const.Cardinality.UNARY_UNARY,
@@ -2533,6 +2543,12 @@ class ModalClientStub:
2533
2543
  modal_proto.api_pb2.AuthTokenGetRequest,
2534
2544
  modal_proto.api_pb2.AuthTokenGetResponse,
2535
2545
  )
2546
+ self.AutoscalerOverrideHistory = grpclib.client.UnaryUnaryMethod(
2547
+ channel,
2548
+ '/modal.client.ModalClient/AutoscalerOverrideHistory',
2549
+ modal_proto.api_pb2.AutoscalerOverrideHistoryRequest,
2550
+ modal_proto.api_pb2.AutoscalerOverrideHistoryResponse,
2551
+ )
2536
2552
  self.BlobCreate = grpclib.client.UnaryUnaryMethod(
2537
2553
  channel,
2538
2554
  '/modal.client.ModalClient/BlobCreate',