modAL 1.6.3.dev5__py3-none-any.whl → 1.6.3.dev6__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.
@@ -116,7 +116,7 @@ class InterceptionContext:
116
116
  """Adds one response payload to an expected queue of responses for a method.
117
117
 
118
118
  These responses will be used once each instead of calling the MockServicer's
119
- implementation of the method.
119
+ implementation of the method. An exception payload is raised instead of being sent.
120
120
 
121
121
  The interception context will throw an exception on exit if not all of the added
122
122
  responses have been consumed.
@@ -184,6 +184,8 @@ class InterceptionContext:
184
184
  async def responder(servicer_self, stream):
185
185
  await stream.recv_message() # get the input message so we can track that
186
186
  for msg in next_response_messages:
187
+ if isinstance(msg, Exception):
188
+ raise msg
187
189
  await stream.send_message(msg)
188
190
 
189
191
  return responder
modal/builder/PREVIEW.txt CHANGED
@@ -1,21 +1,18 @@
1
- aiohappyeyeballs==2.6.1
2
- aiohttp==3.12.7
3
- aiosignal==1.3.2
1
+ aiohappyeyeballs==2.7.1
2
+ aiohttp==3.14.5
3
+ aiosignal==1.4.0
4
4
  async-timeout==5.0.1 ; python_version < "3.11"
5
- attrs==25.3.0
6
- cbor2==5.7.0
7
- certifi==2025.4.26
8
- frozenlist==1.6.0
9
- grpclib==0.4.8 ; python_version < "3.14"
10
- grpclib==0.4.9 ; python_version >= "3.14"
11
- h2==4.2.0
12
- hpack==4.1.0
5
+ attrs==26.1.0
6
+ cbor2==5.9.0
7
+ certifi==2026.10.19
8
+ frozenlist==1.8.0
9
+ grpclib==0.4.9
10
+ h2==4.4.1
11
+ hpack==4.2.0
13
12
  hyperframe==6.1.0
14
- idna==3.10
15
- multidict==6.4.4
16
- propcache==0.3.1
17
- protobuf==6.31.1 ; python_version <= "3.14"
18
- protobuf==7.35.1 ; python_version >= "3.15"
19
- typing_extensions==4.13.2 ; python_version <= "3.14"
20
- typing_extensions==4.16.0 ; python_version >= "3.15"
21
- yarl==1.20.0
13
+ idna==3.20
14
+ multidict==7.1.0
15
+ propcache==0.5.4
16
+ protobuf==7.36.2
17
+ typing_extensions==4.16.0
18
+ yarl==1.25.1
modal/client.pyi CHANGED
@@ -80,7 +80,7 @@ class _Client:
80
80
  server_url: str,
81
81
  client_type: int,
82
82
  credentials: typing.Optional[tuple[str, str]],
83
- version: str = "1.6.3.dev5",
83
+ version: str = "1.6.3.dev6",
84
84
  *,
85
85
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
86
86
  ):
@@ -290,7 +290,7 @@ class Client:
290
290
  server_url: str,
291
291
  client_type: int,
292
292
  credentials: typing.Optional[tuple[str, str]],
293
- version: str = "1.6.3.dev5",
293
+ version: str = "1.6.3.dev6",
294
294
  *,
295
295
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
296
296
  ):
modal/sandbox/_common.py CHANGED
@@ -386,7 +386,7 @@ class SandboxCreateArgs:
386
386
  secrets: Collection[_Secret] | None = None
387
387
  env: dict[str, str | None] | None = None
388
388
  name: str | None = None
389
- timeout: int = 300
389
+ timeout: int | None = 300
390
390
  idle_timeout: int | None = None
391
391
  workdir: str | None = None
392
392
  gpu: str | None = None
@@ -417,6 +417,7 @@ class SandboxCreateArgs:
417
417
  custom_domain: str | None = None
418
418
  include_oidc_identity_token: bool = False
419
419
  runtime: SandboxRuntime | None = None
420
+ persistent: bool = False
420
421
 
421
422
 
422
423
  class SandboxCreateCommon:
@@ -428,6 +429,8 @@ class SandboxCreateCommon:
428
429
 
429
430
  def __init__(self, create_args: SandboxCreateArgs, *, secrets: Sequence[_Secret]) -> None:
430
431
  a = create_args
432
+ if a.timeout is None:
433
+ raise InvalidError("`timeout=None` is only supported for experimental persistent Sandboxes.")
431
434
  _validate_exec_args(a.args)
432
435
  if a.name is not None:
433
436
  check_object_name(a.name, "Sandbox")
@@ -452,6 +455,7 @@ class SandboxCreateCommon:
452
455
  raise InvalidError("`inbound_cidr_allowlist` cannot be used when `block_network` is enabled")
453
456
 
454
457
  self.create_args = a
458
+ self.timeout = a.timeout
455
459
  self.secrets = secrets
456
460
  self.image = a.image or _default_image
457
461
  self.network_file_systems = validate_network_file_systems(a.network_file_systems)
@@ -503,7 +507,7 @@ class SandboxCreateCommon:
503
507
  secret_ids=[secret.object_id for secret in self.secrets],
504
508
  # Ephemeral disk requests are not supported on Sandboxes.
505
509
  resources=convert_fn_config_to_resources_config(cpu=a.cpu, memory=a.memory, gpu=a.gpu, ephemeral_disk=None),
506
- timeout_secs=a.timeout,
510
+ timeout_secs=self.timeout,
507
511
  idle_timeout_secs=a.idle_timeout,
508
512
  workdir=a.workdir,
509
513
  cloud_provider_str=a.cloud if a.cloud else None, # pyright: ignore[reportArgumentType]
@@ -637,6 +641,10 @@ class SandboxImpl(Protocol):
637
641
  """
638
642
  ...
639
643
 
644
+ def clone(self) -> "SandboxImpl":
645
+ """Create an implementation of the same type for another handle to the same Sandbox."""
646
+ ...
647
+
640
648
  @classmethod
641
649
  async def lookup_by_name(
642
650
  cls, client: _Client, req: api_pb2.SandboxGetFromNameRequest
@@ -674,6 +682,10 @@ class SandboxImpl(Protocol):
674
682
 
675
683
  async def terminate(self) -> None: ...
676
684
 
685
+ async def pause(self) -> None: ...
686
+
687
+ async def resume(self) -> None: ...
688
+
677
689
  async def close(self) -> None:
678
690
  """Release connections held for the Sandbox once in-flight operations complete."""
679
691
  ...
@@ -907,6 +919,9 @@ async def snapshot_filesystem_via_tcr(
907
919
  return _Image._new_hydrated(res.image_id, client, None)
908
920
 
909
921
 
922
+ _PERSISTENT_ONLY_MESSAGE = "`{}` requires a persistent Sandbox, created with `_experimental_persistent=True`."
923
+
924
+
910
925
  async def mount_image_via_tcr(
911
926
  impl: SandboxImpl,
912
927
  path: PurePosixPath | str,
@@ -0,0 +1,470 @@
1
+ # Copyright Modal Labs 2026
2
+ """Persistent Sandboxes, managed by the `PersistentSandbox*` control-plane RPCs.
3
+
4
+ A persistent Sandbox runs as a sequence of regular V2 Sandboxes, its sessions. The control plane starts each
5
+ session from the exit snapshot of the previous one.
6
+ """
7
+
8
+ import asyncio
9
+ import copy
10
+ import dataclasses
11
+ import time
12
+ import weakref
13
+ from collections.abc import Awaitable, Callable, Collection, Sequence
14
+ from pathlib import PurePosixPath
15
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar
16
+
17
+ from grpclib import GRPCError, Status
18
+
19
+ from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
20
+
21
+ from .._image import _Image
22
+ from .._outbound_policy import _OutboundPolicy
23
+ from .._resolver import Resolver
24
+ from .._tunnel import Tunnel
25
+ from ..client import _Client
26
+ from ..cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
27
+ from ..config import logger
28
+ from ..container_process import _ContainerProcess
29
+ from ..exception import ClientClosed, InvalidError, SandboxTerminatedError
30
+ from ..io_streams import (
31
+ StreamReader,
32
+ StreamWriter,
33
+ _StreamReaderThroughSandboxCommandRouterParams,
34
+ _StreamWriterThroughCommandRouterSandboxParams,
35
+ )
36
+ from ..secret import _resolvable_secrets, _Secret, _secret_sources
37
+ from ..stream_type import StreamType
38
+ from ._common import SandboxCreateArgs, SandboxCreateCommon, SandboxImpl, command_router, sandbox_create_context
39
+ from ._impl_v2 import SandboxImplV2, auth_metadata
40
+ from ._snapshot import _SandboxSnapshot
41
+ from ._task_command_router_client import TaskCommandRouterClient
42
+
43
+ if TYPE_CHECKING:
44
+ import modal.app
45
+
46
+ _T = TypeVar("_T")
47
+
48
+ # Errors caused by the caller rather than by the session, which a new session can't fix.
49
+ _NON_RECOVERABLE_ERRORS = (InvalidError, ClientClosed)
50
+
51
+ # Lifetime limits of each session. A session that hits one is paused, and resumed on next use.
52
+ PERSISTENT_SANDBOX_SESSION_TIMEOUT = 60 * 60
53
+ PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT = 5 * 60
54
+
55
+ # How long to retry resuming while the previous session is still saving its exit snapshot.
56
+ _RESUME_RETRY_TIMEOUT = 120.0
57
+ _RESUME_RETRY_INTERVAL = 1.0
58
+
59
+ # How often `wait()` checks whether the persistent Sandbox has been terminated.
60
+ _WAIT_POLL_INTERVAL = 1.0
61
+
62
+ _TERMINATED_RESULT = api_pb2.GenericResult(status=api_pb2.GenericResult.GENERIC_STATUS_TERMINATED)
63
+
64
+
65
+ class SandboxImplPersistent:
66
+ """Backs a persistent Sandbox, whose object ID is the ID of its first session.
67
+
68
+ Operations are forwarded to the current session. When one fails and the Sandbox hasn't been terminated
69
+ through this handle, the control plane is asked for the running session, resuming the Sandbox from its
70
+ latest exit snapshot if there is none. The operation is then retried once on the new session.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ client: _Client,
76
+ sandbox_id: str,
77
+ *,
78
+ result: api_pb2.GenericResult | None = None,
79
+ persistent_sandbox_id: str = "",
80
+ session: SandboxImplV2 | None = None,
81
+ ) -> None:
82
+ self.client = client
83
+ self.sandbox_id = sandbox_id
84
+ self.result = result
85
+ self._persistent_sandbox_id = persistent_sandbox_id
86
+ self._session = session if session is not None else SandboxImplV2(client, sandbox_id)
87
+ self._replace_lock: asyncio.Lock | None = None
88
+ self._terminated = result is not None
89
+
90
+ @property
91
+ def _delegate(self) -> SandboxImplV2:
92
+ return self._session
93
+
94
+ @property
95
+ def _client(self) -> _Client:
96
+ return self.client
97
+
98
+ @classmethod
99
+ async def create(
100
+ cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
101
+ ) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
102
+ if create_args.timeout is not None or create_args.idle_timeout is not None:
103
+ raise InvalidError("Persistent Sandboxes require `timeout=None` and don't support `idle_timeout`.")
104
+ create_args = dataclasses.replace(
105
+ create_args,
106
+ timeout=PERSISTENT_SANDBOX_SESSION_TIMEOUT,
107
+ idle_timeout=PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT,
108
+ )
109
+ if create_args.name is not None:
110
+ raise InvalidError("Named persistent Sandboxes are not supported yet.")
111
+ if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
112
+ raise InvalidError("GPUs, network file systems and PTYs are not supported by persistent Sandboxes yet.")
113
+ if create_args.enable_snapshot:
114
+ raise InvalidError("Memory snapshots are not supported by persistent Sandboxes.")
115
+ if create_args.env or create_args.tags:
116
+ raise InvalidError("`env` and `tags` are not supported by persistent Sandboxes yet.")
117
+
118
+ secrets = create_args.secrets or []
119
+ common = SandboxCreateCommon(create_args, secrets=_resolvable_secrets(secrets))
120
+ async with sandbox_create_context(app, client) as (client, load_context):
121
+ await common.load_dependencies(Resolver(), load_context, load_ephemeral_bucket_secrets=False)
122
+ cloud_bucket_mount_protos, cloud_bucket_credentials = cloud_bucket_mounts_to_proto(
123
+ common.cloud_bucket_mounts, split_ephemeral_credentials=True
124
+ )
125
+ if cloud_bucket_credentials or any(source.HasField("env") for source in _secret_sources(secrets)):
126
+ raise InvalidError("Ephemeral credentials are not supported by persistent Sandboxes yet.")
127
+ req = api_pb2.PersistentSandboxCreateRequest(
128
+ app_id=load_context.app_id or "",
129
+ definition=common.definition(cloud_bucket_mounts=cloud_bucket_mount_protos),
130
+ )
131
+ resp = await client._stub.PersistentSandboxCreate(req, metadata=await auth_metadata(client))
132
+ app_id = load_context.app_id or ""
133
+ impl = cls(client, resp.sandbox_id, persistent_sandbox_id=resp.persistent_sandbox_id)
134
+ return impl, api_pb2.SandboxHandleMetadata(app_id=app_id)
135
+
136
+ # Persistent Sandboxes are only created through `create`, and can't be looked up yet.
137
+
138
+ @classmethod
139
+ async def lookup_by_name(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxGetFromNameResponse:
140
+ raise NotImplementedError
141
+
142
+ @classmethod
143
+ async def lookup_by_id(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxWaitResponse:
144
+ raise NotImplementedError
145
+
146
+ @classmethod
147
+ async def list_page(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxListResponse:
148
+ raise NotImplementedError
149
+
150
+ @classmethod
151
+ async def restore_from_memory_snapshot(
152
+ cls, *args: Any, **kwargs: Any
153
+ ) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
154
+ raise NotImplementedError
155
+
156
+ def clone(self) -> "SandboxImplPersistent":
157
+ return copy.copy(self)
158
+
159
+ def make_stdio(self, check_open: Callable[[], None]) -> tuple[StreamReader[str], StreamReader[str], StreamWriter]:
160
+ """Create stdio streams of the entrypoint of whichever session is current when they connect."""
161
+ weak_self = weakref.ref(self)
162
+
163
+ async def resolve_router() -> tuple[str, TaskCommandRouterClient]:
164
+ this = weak_self()
165
+ if this is None:
166
+ raise RuntimeError("Sandbox was garbage collected before its stdio reader connected")
167
+ return await command_router(this)
168
+
169
+ def reader(file_descriptor: "api_pb2.FileDescriptor.ValueType") -> StreamReader[str]:
170
+ return StreamReader(
171
+ _StreamReaderThroughSandboxCommandRouterParams(
172
+ file_descriptor=file_descriptor, sandbox_id=self.sandbox_id, resolve_router=resolve_router
173
+ ),
174
+ by_line=True,
175
+ )
176
+
177
+ stdin = StreamWriter(
178
+ _StreamWriterThroughCommandRouterSandboxParams(resolve_router=resolve_router, check_open=check_open)
179
+ )
180
+ return reader(api_pb2.FILE_DESCRIPTOR_STDOUT), reader(api_pb2.FILE_DESCRIPTOR_STDERR), stdin
181
+
182
+ # Session replacement
183
+
184
+ async def _run(self, op: Callable[[SandboxImplV2], Awaitable[_T]]) -> _T:
185
+ """Run `op` on the session, switching to the running session and retrying once if it fails."""
186
+ session = self._delegate
187
+ try:
188
+ return await op(session)
189
+ except _NON_RECOVERABLE_ERRORS:
190
+ raise
191
+ except Exception as exc:
192
+ if self._terminated:
193
+ # A terminated persistent Sandbox is stopped for good, so it isn't resumed on use.
194
+ raise
195
+ replacement = await self._replace_session(session)
196
+ if replacement is None:
197
+ raise
198
+ logger.debug(f"Retrying operation on persistent Sandbox {self._persistent_sandbox_id} after: {exc!r}")
199
+ return await op(replacement)
200
+
201
+ async def _get_running_session_id(self) -> str:
202
+ req = api_pb2.PersistentSandboxGetRequest(persistent_sandbox_id=self._persistent_sandbox_id)
203
+ resp = await self._client._stub.PersistentSandboxGet(req, metadata=await auth_metadata(self._client))
204
+ return resp.sandbox_id
205
+
206
+ async def _resume_session(self) -> str:
207
+ """Resume the persistent Sandbox, returning the ID of its running session."""
208
+ req = api_pb2.PersistentSandboxResumeRequest(persistent_sandbox_id=self._persistent_sandbox_id)
209
+ deadline = time.monotonic() + _RESUME_RETRY_TIMEOUT
210
+ while True:
211
+ try:
212
+ resp = await self._client._stub.PersistentSandboxResume(req, metadata=await auth_metadata(self._client))
213
+ return resp.sandbox_id
214
+ except GRPCError as exc:
215
+ # UNAVAILABLE means the previous session is still pausing or another resume is in progress.
216
+ if exc.status != Status.UNAVAILABLE or time.monotonic() > deadline:
217
+ raise
218
+ await asyncio.sleep(_RESUME_RETRY_INTERVAL)
219
+
220
+ async def _replace_session(self, failed: SandboxImplV2 | None, *, resume: bool = True) -> SandboxImplV2 | None:
221
+ """Switch from the `failed` session, or return None if it is still the running session."""
222
+ # Lazily create the lock on the event loop that first uses the Sandbox.
223
+ if self._replace_lock is None:
224
+ self._replace_lock = asyncio.Lock()
225
+ async with self._replace_lock:
226
+ if self._session is not failed:
227
+ # A concurrent operation has already replaced the session.
228
+ return self._session
229
+
230
+ session_id = await self._get_running_session_id()
231
+ if failed is not None and session_id == failed.sandbox_id:
232
+ return None
233
+ if not session_id:
234
+ if not resume:
235
+ return None
236
+ session_id = await self._resume_session()
237
+ logger.debug(f"Switching persistent Sandbox {self._persistent_sandbox_id} to session {session_id}")
238
+ replacement = SandboxImplV2(self.client, session_id)
239
+ self._session = replacement
240
+ if failed is not None:
241
+ await failed.close()
242
+ return replacement
243
+
244
+ # Operations
245
+
246
+ async def close(self) -> None:
247
+ await self._session.close()
248
+
249
+ def _set_terminated_result(self) -> None:
250
+ self.result = _TERMINATED_RESULT
251
+
252
+ async def wait(self, raise_on_termination: bool) -> None:
253
+ # A persistent Sandbox keeps running across sessions, until it is terminated.
254
+ while not self._terminated:
255
+ await asyncio.sleep(_WAIT_POLL_INTERVAL)
256
+ self._set_terminated_result()
257
+ if raise_on_termination:
258
+ raise SandboxTerminatedError()
259
+
260
+ async def poll(self) -> None:
261
+ if self._terminated:
262
+ self._set_terminated_result()
263
+ else:
264
+ self.result = None
265
+
266
+ async def terminate(self) -> None:
267
+ req = api_pb2.PersistentSandboxTerminateRequest(persistent_sandbox_id=self._persistent_sandbox_id)
268
+ await self._client._stub.PersistentSandboxTerminate(req, metadata=await auth_metadata(self._client))
269
+ self._terminated = True
270
+ self._set_terminated_result()
271
+
272
+ async def pause(self) -> None:
273
+ """Stop the current session, which saves its exit snapshot for the next `resume`."""
274
+ self._check_not_terminated()
275
+ session = self._delegate
276
+ await session.terminate()
277
+ await session.wait(raise_on_termination=False)
278
+
279
+ async def resume(self) -> None:
280
+ """Start a session from the latest exit snapshot, unless one is already running."""
281
+ self._check_not_terminated()
282
+ session = self._delegate
283
+ session_id = await self._resume_session()
284
+ if session_id != session.sandbox_id:
285
+ self._session = SandboxImplV2(self.client, session_id)
286
+ await session.close()
287
+
288
+ def _check_not_terminated(self) -> None:
289
+ if self._terminated:
290
+ raise InvalidError(f"Persistent Sandbox {self._persistent_sandbox_id} has been terminated.")
291
+
292
+ async def tunnels(self, timeout: int) -> dict[int, Tunnel]:
293
+ return await self._run(lambda d: d.tunnels(timeout))
294
+
295
+ async def get_task_id(
296
+ self, raise_if_task_complete: bool = False, timeout: float | None = None, retry_transient: bool = False
297
+ ) -> str:
298
+ return await self._run(
299
+ lambda d: d.get_task_id(
300
+ raise_if_task_complete=raise_if_task_complete, timeout=timeout, retry_transient=retry_transient
301
+ )
302
+ )
303
+
304
+ async def resolve_task_id_for_logs(self) -> str:
305
+ return await self._delegate.resolve_task_id_for_logs()
306
+
307
+ async def get_exit_snapshot(self, timeout: float | None) -> _Image:
308
+ return await self._delegate.get_exit_snapshot(timeout)
309
+
310
+ async def snapshot_memory(self) -> _SandboxSnapshot:
311
+ raise NotImplementedError("Memory snapshots aren't supported for persistent Sandboxes.")
312
+
313
+ async def get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
314
+ # The task ID belongs to the current delegate, so this can't be retried on a replacement.
315
+ return await self._delegate.get_command_router_client(task_id)
316
+
317
+ async def get_tags(self) -> dict[str, str]:
318
+ raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
319
+
320
+ async def set_tags(self, tags: dict[str, str]) -> None:
321
+ raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
322
+
323
+ async def set_name(self, name: str) -> None:
324
+ raise InvalidError("The name of a persistent Sandbox can't be changed.")
325
+
326
+ async def create_connect_token(
327
+ self, req: api_pb2.SandboxCreateConnectTokenRequest
328
+ ) -> api_pb2.SandboxCreateConnectTokenResponse:
329
+ def create(d: SandboxImplV2) -> Awaitable[api_pb2.SandboxCreateConnectTokenResponse]:
330
+ delegate_req = api_pb2.SandboxCreateConnectTokenRequest()
331
+ delegate_req.CopyFrom(req)
332
+ delegate_req.sandbox_id = d.sandbox_id
333
+ return d.create_connect_token(delegate_req)
334
+
335
+ return await self._run(create)
336
+
337
+ async def wait_until_ready(self, timeout: int) -> None:
338
+ await self._run(lambda d: d.wait_until_ready(timeout))
339
+
340
+ async def set_network_access(self, network_access: api_pb2.NetworkAccess) -> None:
341
+ raise NotImplementedError("Updating network access isn't supported for persistent Sandboxes yet.")
342
+
343
+ async def update_outbound_policy(self, outbound_policy: _OutboundPolicy) -> None:
344
+ raise NotImplementedError("Updating the outbound policy isn't supported for persistent Sandboxes yet.")
345
+
346
+ async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
347
+ return await self._run(lambda d: d.snapshot_filesystem(timeout, ttl=ttl, container_id=container_id))
348
+
349
+ async def mount_image(
350
+ self,
351
+ path: PurePosixPath | str,
352
+ image_id: str,
353
+ *,
354
+ encryption_key: bytes | None,
355
+ container_id: str = "",
356
+ ) -> None:
357
+ raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
358
+
359
+ async def unmount_image(self, path: PurePosixPath | str, *, container_id: str = "") -> None:
360
+ raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
361
+
362
+ async def snapshot_directory(
363
+ self,
364
+ path: PurePosixPath | str,
365
+ *,
366
+ timeout: int,
367
+ ttl: int | None,
368
+ encryption_key: bytes | None,
369
+ container_id: str = "",
370
+ ) -> _Image:
371
+ return await self._run(
372
+ lambda d: d.snapshot_directory(
373
+ path, timeout=timeout, ttl=ttl, encryption_key=encryption_key, container_id=container_id
374
+ )
375
+ )
376
+
377
+ async def reload_volumes(self, *, timeout: int, container_id: str = "") -> None:
378
+ await self._run(lambda d: d.reload_volumes(timeout=timeout, container_id=container_id))
379
+
380
+ async def exec(
381
+ self,
382
+ *args: str,
383
+ pty_info: api_pb2.PTYInfo | None,
384
+ stdout: StreamType,
385
+ stderr: StreamType,
386
+ timeout: int | None,
387
+ workdir: str | None,
388
+ env: dict[str, str | None] | None,
389
+ secrets: Collection[_Secret] | None,
390
+ text: bool,
391
+ bufsize: Literal[-1, 1],
392
+ container_id: str | None,
393
+ ) -> _ContainerProcess[bytes] | _ContainerProcess[str]:
394
+ return await self._run(
395
+ lambda d: d.exec(
396
+ *args,
397
+ pty_info=pty_info,
398
+ stdout=stdout,
399
+ stderr=stderr,
400
+ timeout=timeout,
401
+ workdir=workdir,
402
+ env=env,
403
+ secrets=secrets,
404
+ text=text,
405
+ bufsize=bufsize,
406
+ container_id=container_id,
407
+ )
408
+ )
409
+
410
+ # Sidecar containers
411
+
412
+ def validate_sidecar_create(
413
+ self,
414
+ *,
415
+ cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
416
+ has_proxy: bool,
417
+ include_oidc_identity_token: bool,
418
+ ) -> None:
419
+ self._delegate.validate_sidecar_create(
420
+ cloud_bucket_mounts=cloud_bucket_mounts,
421
+ has_proxy=has_proxy,
422
+ include_oidc_identity_token=include_oidc_identity_token,
423
+ )
424
+
425
+ async def create_sidecar(
426
+ self,
427
+ *args: str,
428
+ name: str,
429
+ image_id: str,
430
+ secret_ids: Sequence[str],
431
+ secret_sources: Sequence[api_pb2.SecretSource],
432
+ workdir: str | None,
433
+ volume_mounts: Sequence[api_pb2.VolumeMount],
434
+ cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
435
+ network_access: api_pb2.NetworkAccess,
436
+ include_oidc_identity_token: bool,
437
+ proxy_id: str | None,
438
+ pty_info: api_pb2.PTYInfo | None,
439
+ memory_reserve_consume_mib: int | None,
440
+ ) -> tuple[str, str]:
441
+ return await self._run(
442
+ lambda d: d.create_sidecar(
443
+ *args,
444
+ name=name,
445
+ image_id=image_id,
446
+ secret_ids=secret_ids,
447
+ secret_sources=secret_sources,
448
+ workdir=workdir,
449
+ volume_mounts=volume_mounts,
450
+ cloud_bucket_mounts=cloud_bucket_mounts,
451
+ network_access=network_access,
452
+ include_oidc_identity_token=include_oidc_identity_token,
453
+ proxy_id=proxy_id,
454
+ pty_info=pty_info,
455
+ memory_reserve_consume_mib=memory_reserve_consume_mib,
456
+ )
457
+ )
458
+
459
+ async def get_sidecar(self, name: str, include_terminated: bool) -> sr_pb2.TaskContainerInfo:
460
+ return await self._run(lambda d: d.get_sidecar(name, include_terminated))
461
+
462
+ async def list_sidecars(self, include_terminated: bool) -> list[sr_pb2.TaskContainerInfo]:
463
+ return await self._run(lambda d: d.list_sidecars(include_terminated))
464
+
465
+ async def wait_sidecar(self, container_id: str, timeout: float) -> api_pb2.GenericResult | None:
466
+ # Sidecar containers don't carry over to a replacement delegate.
467
+ return await self._delegate.wait_sidecar(container_id, timeout)
468
+
469
+ async def terminate_sidecar(self, container_id: str) -> None:
470
+ await self._delegate.terminate_sidecar(container_id)
modal/sandbox/_impl_v1.py CHANGED
@@ -40,6 +40,7 @@ from ..io_streams import (
40
40
  from ..secret import _Secret
41
41
  from ..stream_type import StreamType
42
42
  from ._common import (
43
+ _PERSISTENT_ONLY_MESSAGE,
43
44
  _TASK_ID_POLL_INTERVAL,
44
45
  ExitSnapshotPoll,
45
46
  SandboxCreateArgs,
@@ -124,6 +125,9 @@ class SandboxImplV1:
124
125
  def _client(self) -> _Client:
125
126
  return self.client
126
127
 
128
+ def clone(self) -> "SandboxImplV1":
129
+ return type(self)(self.client, self.sandbox_id, result=self.result)
130
+
127
131
  @classmethod
128
132
  async def create(
129
133
  cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
@@ -289,6 +293,12 @@ class SandboxImplV1:
289
293
  req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
290
294
  await self.client._stub.SandboxTerminate(req)
291
295
 
296
+ async def pause(self) -> None:
297
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
298
+
299
+ async def resume(self) -> None:
300
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
301
+
292
302
  async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
293
303
  if not container_id and os.environ.get("MODAL_USE_LEGACY_FILESYSTEM_SNAPSHOT") == "1":
294
304
  if ttl != DEFAULT_SNAPSHOT_TTL:
modal/sandbox/_impl_v2.py CHANGED
@@ -32,6 +32,7 @@ from ..io_streams import (
32
32
  from ..secret import _resolvable_secrets, _Secret, _secret_sources
33
33
  from ..stream_type import StreamType
34
34
  from ._common import (
35
+ _PERSISTENT_ONLY_MESSAGE,
35
36
  _TASK_ID_POLL_INTERVAL,
36
37
  ExitSnapshotPoll,
37
38
  SandboxCreateArgs,
@@ -147,6 +148,9 @@ class SandboxImplV2:
147
148
  def _client(self) -> _Client:
148
149
  return self.client
149
150
 
151
+ def clone(self) -> "SandboxImplV2":
152
+ return type(self)(self.client, self.sandbox_id, result=self.result)
153
+
150
154
  @classmethod
151
155
  async def create(
152
156
  cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
@@ -351,6 +355,12 @@ class SandboxImplV2:
351
355
  req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
352
356
  await self.client._stub.SandboxTerminateV2(req, metadata=await self._metadata())
353
357
 
358
+ async def pause(self) -> None:
359
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
360
+
361
+ async def resume(self) -> None:
362
+ raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
363
+
354
364
  def _create_sidecar_via_control_plane(self) -> bool:
355
365
  # Opt out via the `use_control_plane_sidecar_create` config setting to create
356
366
  # sidecars over the task command router instead.
modal/sandbox/_sandbox.py CHANGED
@@ -46,6 +46,7 @@ from ._common import (
46
46
  default_pty_info,
47
47
  )
48
48
  from ._filesystem import _SandboxFilesystem
49
+ from ._impl_persistent import SandboxImplPersistent
49
50
  from ._impl_v1 import SandboxImplV1, command_router_access_fetcher as v1_command_router_access_fetcher
50
51
  from ._impl_v2 import SandboxImplV2, command_router_access_fetcher as v2_command_router_access_fetcher
51
52
  from ._sidecar import _SidecarManager
@@ -69,6 +70,8 @@ def impl_class_for_sandbox_id(sandbox_id: str) -> type[SandboxImpl]:
69
70
 
70
71
  def impl_class_for_create_args(create_args: SandboxCreateArgs) -> type[SandboxImpl]:
71
72
  """Select the implementation that creates the Sandbox described by `create_args`."""
73
+ if create_args.persistent:
74
+ return SandboxImplPersistent
72
75
  if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
73
76
  # GPUs, network file systems, and PTYs are not supported on V2, so those calls use the V1
74
77
  # backend regardless of the setting.
@@ -183,7 +186,7 @@ class _Sandbox(_Object, type_prefix="sb"):
183
186
  env: dict[str, str | None] | None = None,
184
187
  secrets: Collection[_Secret] | None = None,
185
188
  network_file_systems: dict[str | os.PathLike, _NetworkFileSystem] = {},
186
- timeout: int = 300,
189
+ timeout: int | None = 300,
187
190
  idle_timeout: int | None = None,
188
191
  workdir: str | None = None,
189
192
  gpu: str | None = None,
@@ -209,6 +212,7 @@ class _Sandbox(_Object, type_prefix="sb"):
209
212
  verbose: bool = False,
210
213
  experimental_options: dict[str, Any] | None = None,
211
214
  _experimental_enable_snapshot: bool = False,
215
+ _experimental_persistent: bool = False,
212
216
  client: _Client | None = None,
213
217
  environment_name: str | None = None, # *DEPRECATED*
214
218
  pty_info: api_pb2.PTYInfo | None = None, # *DEPRECATED*
@@ -349,6 +353,7 @@ class _Sandbox(_Object, type_prefix="sb"):
349
353
  custom_domain=custom_domain,
350
354
  include_oidc_identity_token=include_oidc_identity_token,
351
355
  runtime=runtime,
356
+ persistent=_experimental_persistent,
352
357
  )
353
358
  impl, metadata = await impl_class_for_create_args(create_args).create(create_args, app=app, client=client)
354
359
  return _Sandbox._from_impl(impl, metadata)
@@ -495,8 +500,7 @@ class _Sandbox(_Object, type_prefix="sb"):
495
500
  super()._initialize_from_other(other)
496
501
  self._attached = other._attached
497
502
  self._app_id = other._app_id
498
- other_impl = other._impl
499
- self._impl = type(other_impl)(other_impl.client, other_impl.sandbox_id, result=other_impl.result)
503
+ self._impl = other._impl.clone()
500
504
 
501
505
  def _initialize_from_empty(self):
502
506
  super()._initialize_from_empty()
@@ -1029,6 +1033,20 @@ class _Sandbox(_Object, type_prefix="sb"):
1029
1033
  return self.returncode
1030
1034
  return None
1031
1035
 
1036
+ async def _experimental_pause(self) -> None:
1037
+ """Stop the Sandbox's current container, keeping its state so it can be resumed.
1038
+
1039
+ Only supported for experimental persistent Sandboxes.
1040
+ """
1041
+ await self._impl.pause()
1042
+
1043
+ async def _experimental_resume(self) -> None:
1044
+ """Resume a paused Sandbox from its saved state.
1045
+
1046
+ Only supported for experimental persistent Sandboxes.
1047
+ """
1048
+ await self._impl.resume()
1049
+
1032
1050
  async def poll(self) -> int | None:
1033
1051
  """Check if the Sandbox has finished running.
1034
1052
 
modal/sandbox/sandbox.pyi CHANGED
@@ -1333,7 +1333,7 @@ class Sandbox(modal.object.Object):
1333
1333
  network_file_systems: dict[
1334
1334
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
1335
1335
  ] = {},
1336
- timeout: int = 300,
1336
+ timeout: typing.Optional[int] = 300,
1337
1337
  idle_timeout: typing.Optional[int] = None,
1338
1338
  workdir: typing.Optional[str] = None,
1339
1339
  gpu: typing.Optional[str] = None,
@@ -1362,6 +1362,7 @@ class Sandbox(modal.object.Object):
1362
1362
  verbose: bool = False,
1363
1363
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1364
1364
  _experimental_enable_snapshot: bool = False,
1365
+ _experimental_persistent: bool = False,
1365
1366
  client: typing.Optional[modal.client.Client] = None,
1366
1367
  environment_name: typing.Optional[str] = None,
1367
1368
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
@@ -1457,7 +1458,7 @@ class Sandbox(modal.object.Object):
1457
1458
  network_file_systems: dict[
1458
1459
  typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
1459
1460
  ] = {},
1460
- timeout: int = 300,
1461
+ timeout: typing.Optional[int] = 300,
1461
1462
  idle_timeout: typing.Optional[int] = None,
1462
1463
  workdir: typing.Optional[str] = None,
1463
1464
  gpu: typing.Optional[str] = None,
@@ -1486,6 +1487,7 @@ class Sandbox(modal.object.Object):
1486
1487
  verbose: bool = False,
1487
1488
  experimental_options: typing.Optional[dict[str, typing.Any]] = None,
1488
1489
  _experimental_enable_snapshot: bool = False,
1490
+ _experimental_persistent: bool = False,
1489
1491
  client: typing.Optional[modal.client.Client] = None,
1490
1492
  environment_name: typing.Optional[str] = None,
1491
1493
  pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
@@ -2582,6 +2584,40 @@ class Sandbox(modal.object.Object):
2582
2584
 
2583
2585
  terminate: __terminate_spec
2584
2586
 
2587
+ class ___experimental_pause_spec(typing_extensions.Protocol):
2588
+ def __call__(self, /) -> None:
2589
+ """Stop the Sandbox's current container, keeping its state so it can be resumed.
2590
+
2591
+ Only supported for experimental persistent Sandboxes.
2592
+ """
2593
+ ...
2594
+
2595
+ async def aio(self, /) -> None:
2596
+ """Stop the Sandbox's current container, keeping its state so it can be resumed.
2597
+
2598
+ Only supported for experimental persistent Sandboxes.
2599
+ """
2600
+ ...
2601
+
2602
+ _experimental_pause: ___experimental_pause_spec
2603
+
2604
+ class ___experimental_resume_spec(typing_extensions.Protocol):
2605
+ def __call__(self, /) -> None:
2606
+ """Resume a paused Sandbox from its saved state.
2607
+
2608
+ Only supported for experimental persistent Sandboxes.
2609
+ """
2610
+ ...
2611
+
2612
+ async def aio(self, /) -> None:
2613
+ """Resume a paused Sandbox from its saved state.
2614
+
2615
+ Only supported for experimental persistent Sandboxes.
2616
+ """
2617
+ ...
2618
+
2619
+ _experimental_resume: ___experimental_resume_spec
2620
+
2585
2621
  class __poll_spec(typing_extensions.Protocol):
2586
2622
  def __call__(self, /) -> typing.Optional[int]:
2587
2623
  """Check if the Sandbox has finished running.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.3.dev5
3
+ Version: 1.6.3.dev6
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -39,7 +39,7 @@ modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
39
39
  modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
40
40
  modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
41
41
  modal/client.py,sha256=cstrL0wpXCZmcvRs7Sy3QRw9Vz8zkZy8NNk0tPe38sk,22852
42
- modal/client.pyi,sha256=wkF1jBOBV3bFWy8sI4U5HwBKIkFNyf1rjIFiI0E7sAA,23488
42
+ modal/client.pyi,sha256=Sx1Q0OicQDZ2R9L-GLG0eDXWuF9c9tqV9aSvl6PyrLo,23488
43
43
  modal/cloud_bucket_mount.py,sha256=ZOg69dhIFcJcwim4JP-6s1tqwOYAfgS9806jRpspifw,7511
44
44
  modal/cloud_bucket_mount.pyi,sha256=HoPe_NG5jYdVfmTNY-ZJ3-C8Eo_FzJovl9orJRjkGbE,10252
45
45
  modal/cls.py,sha256=JfDAUMrkhasm26lK7kDUNarXZA_7AvZrymf7XNSGTDM,41777
@@ -127,7 +127,7 @@ modal/_utils/display_utils.py,sha256=5XyCCjnMAWeLEEqF0hiLSA6XrrNp-gvXVsLcngN6vAI
127
127
  modal/_utils/docker_utils.py,sha256=k8fkYT6G-X_B3iamjxiSpwSE-k58xGVHtsk2_gR7i2Q,4197
128
128
  modal/_utils/function_utils.py,sha256=dVCCnovfSlYssRuQh0Woq5oqX4Kj_tAK4vJ3USFNUFw,27537
129
129
  modal/_utils/git_utils.py,sha256=Lat_l4O4hwOYgylEtWup3hyqWT8D7JoCIjCflXcX8BE,3148
130
- modal/_utils/grpc_testing.py,sha256=udyK_wRXX_yc9eLt8mIZZqx12XlaiUqORxaGNvfI_MM,8697
130
+ modal/_utils/grpc_testing.py,sha256=59FDtDsSeKrp9gmFI41dP_67yxxxydm06BhixpEnf2k,8828
131
131
  modal/_utils/grpc_utils.py,sha256=Gcl5EyEMEHMaMlGh90F63Oi1l9K6cnmeTCbGCouwJ-A,29998
132
132
  modal/_utils/hash_utils.py,sha256=WHuMRwmB4fnaM-qmyWXR87C2CinvLnmRoygSzsTEunA,3008
133
133
  modal/_utils/http_utils.py,sha256=TO9nnKR0Cc-iWCmkou56wJh9MHgNZwQMosK8WK-GD5Y,3133
@@ -153,7 +153,7 @@ modal/builder/2023.12.txt,sha256=OjsbXFkCSdkzzryZP82Q73osr5wxQ6EUzmGcK7twfkA,502
153
153
  modal/builder/2024.04.txt,sha256=6NnrbIE-mflwMyKyQ0tsWeY8XFE1kSW9oE8DVDoD8QU,544
154
154
  modal/builder/2024.10.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddRo,296
155
155
  modal/builder/2025.06.txt,sha256=90NzKtbJ32RrUtYiUv1XtJTDEaJ1SFcVpJXUE7Udle4,791
156
- modal/builder/PREVIEW.txt,sha256=9iN3AGZSovazQpxSHekDGDgjhroPz9Z-A6hZiZMJ7bs,544
156
+ modal/builder/PREVIEW.txt,sha256=bBLNruBc0v4ulzCraMBVfVm-6kPku3Ag1nFnTy1kr_o,326
157
157
  modal/builder/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
158
158
  modal/builder/base-images.json,sha256=5wyWfI3fQ51PH_dHvmXeTJPrKQBdi5B3DihQIWEXC4Q,1202
159
159
  modal/cli/__init__.py,sha256=6FRleWQxBDT19y7OayO4lBOzuL6Bs9r0rLINYYYbHwQ,769
@@ -198,18 +198,19 @@ modal/experimental/flash_server.py,sha256=iqxVoQij6aM58_h0la6Y_Vy3CCQj8iYmjzt79r
198
198
  modal/experimental/flash_server.pyi,sha256=LuIBqeOJ1NAapKWnJMRzzrwIHlIK2GcOLOI2r-z_QN0,1025
199
199
  modal/experimental/ipython.py,sha256=kJ6o9wkAY_MiSb7paewI7f1MJAeqdi8TX5pBdhr_mUI,2925
200
200
  modal/sandbox/__init__.py,sha256=wHE5KCHUaMSK_nNGbn852PI423v3Q4UQEsFpoqNqMMI,203
201
- modal/sandbox/_common.py,sha256=z82O2XIrRaeanCzNY4CBnk1X1MuRROJjDcFDtSDtaV8,46549
201
+ modal/sandbox/_common.py,sha256=Kuibj5eS8upWLbSxtYdojPZtyCh0iLtdsAJ4RuK2Lk4,47098
202
202
  modal/sandbox/_filesystem.py,sha256=GdkMNVahPpUpfuY04HwWWwZyMID3bcut7PHXNWOIpYg,28299
203
- modal/sandbox/_impl_v1.py,sha256=AiFhTxEXb-B3TWRyDnIeO55UrMwEJAnDIhYf5h5oYrE,20513
204
- modal/sandbox/_impl_v2.py,sha256=O1NpY7c5qF-8RbypnWjdJxUfivxO-XAevDeluoLDtJs,25866
205
- modal/sandbox/_sandbox.py,sha256=K6GVvu4QW07gQWot8ZNX5wpsolYl-ZFVj3-RyfooopQ,61001
203
+ modal/sandbox/_impl_persistent.py,sha256=PwlzsxfQYvsKPHDgmK1LnYSx7CIZSFKCtm_87YBSrnc,20182
204
+ modal/sandbox/_impl_v1.py,sha256=Xy0Mp0_X9T7WAxY20R2NVaobXiC4ciPdsRLC0k4JnCM,20918
205
+ modal/sandbox/_impl_v2.py,sha256=AUCRYVv-qm-eLEOqmsAacwXyRK7VTJ1faPJdwk18q5g,26271
206
+ modal/sandbox/_sandbox.py,sha256=wvchpp_g1zotOMpMhzmiAovaAi6_q_f5_CF2aKzijQ8,61591
206
207
  modal/sandbox/_sidecar.py,sha256=k9SMe2V--gkj22fZRC3EN4GNPtdPFkrwJRxB7W4IFU4,21303
207
208
  modal/sandbox/_snapshot.py,sha256=8mB6FOecXEbgTC9dyogIEGCxTdjemuFsyfVFQyQ2Cp8,3105
208
209
  modal/sandbox/_task_command_router_client.py,sha256=ta_I2EsVruEkRE9YCaCHns4oCb_htz7aQIf0ZSp2wH0,55181
209
210
  modal/sandbox/sandbox.py,sha256=4ekghND9qUGf5wtIQH4mVa6PHGjHgY0UN5RKryOM05k,683
210
- modal/sandbox/sandbox.pyi,sha256=H3Itxk7z1hbrsRIF_zZP8mzAhMJ0pnrr1FvnWUIavZ4,137650
211
+ modal/sandbox/sandbox.pyi,sha256=iWxqScPKLa6oo9vJGukv2XhuCOK0E-bj52trqetBBh8,138895
211
212
  modal/skills/modal/SKILL.md,sha256=Bm-0BPTYppJoeQzA-N-Re8L6-nKnV6gspJXMl4cD0P0,5490
212
- modal-1.6.3.dev5.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
213
+ modal-1.6.3.dev6.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
213
214
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
214
215
  modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
215
216
  modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
@@ -233,10 +234,10 @@ modal_proto/task_command_router_pb2.py,sha256=ysC0d8p3YxwyOpP3B-qDlvE81Yo4ntQiwi
233
234
  modal_proto/task_command_router_pb2.pyi,sha256=FDcd5t3ZCmQHUjefOzg07iglrgKYcBrzFvdKIWsEMHI,50628
234
235
  modal_proto/task_command_router_pb2_grpc.py,sha256=8iGUGwEVbqOsZWPJ7OsQ9OKUGaSCf9Dr2tvb04Ww6d4,44620
235
236
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=26Fq9RQo09B9KLHsgdpRuw-I6Z4-2jrXLQkuDzB-moI,15097
236
- modal_version/__init__.py,sha256=TWF4iwbBqC0_KrbqBk0D3qxFSr5QGvt9zcsCBxWyQ88,120
237
+ modal_version/__init__.py,sha256=ghBy3FJF-r7e--zuLA-Q_y9BZN24mOBqGseTPgAuR7c,120
237
238
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
238
- modal-1.6.3.dev5.dist-info/METADATA,sha256=aOHUajhismM2QZ3trzMWEvzX3-X99wK7Yq0j8gN6S8A,2577
239
- modal-1.6.3.dev5.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
240
- modal-1.6.3.dev5.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
241
- modal-1.6.3.dev5.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
242
- modal-1.6.3.dev5.dist-info/RECORD,,
239
+ modal-1.6.3.dev6.dist-info/METADATA,sha256=QlL5GMEEPP1FCRpr-3VjtJM0hwz63Thio0szsMahzK4,2577
240
+ modal-1.6.3.dev6.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
241
+ modal-1.6.3.dev6.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
242
+ modal-1.6.3.dev6.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
243
+ modal-1.6.3.dev6.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2026
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.6.3.dev5"
4
+ __version__ = "1.6.3.dev6"