modal 0.71.0__py3-none-any.whl → 0.71.1__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.
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.0"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.1"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.0"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.1"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/file_io.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import asyncio
3
+ import enum
3
4
  import io
5
+ from dataclasses import dataclass
4
6
  from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Sequence, TypeVar, Union, cast
5
7
 
6
8
  if TYPE_CHECKING:
@@ -11,6 +13,7 @@ import json
11
13
  from grpclib.exceptions import GRPCError, StreamTerminatedError
12
14
 
13
15
  from modal._utils.grpc_utils import retry_transient_errors
16
+ from modal.io_streams_helper import consume_stream_with_retries
14
17
  from modal_proto import api_pb2
15
18
 
16
19
  from ._utils.async_utils import synchronize_api
@@ -94,6 +97,20 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
94
97
  await file._wait(resp.exec_id)
95
98
 
96
99
 
100
+ class FileWatchEventType(enum.Enum):
101
+ Unknown = "Unknown"
102
+ Access = "Access"
103
+ Create = "Create"
104
+ Modify = "Modify"
105
+ Remove = "Remove"
106
+
107
+
108
+ @dataclass
109
+ class FileWatchEvent:
110
+ paths: list[str]
111
+ type: FileWatchEventType
112
+
113
+
97
114
  # The FileIO class is designed to mimic Python's io.FileIO
98
115
  # See https://github.com/python/cpython/blob/main/Lib/_pyio.py#L1459
99
116
  class _FileIO(Generic[T]):
@@ -124,6 +141,7 @@ class _FileIO(Generic[T]):
124
141
  _task_id: str = ""
125
142
  _file_descriptor: str = ""
126
143
  _client: Optional[_Client] = None
144
+ _watch_output_buffer: list[Optional[bytes]] = []
127
145
 
128
146
  def _validate_mode(self, mode: str) -> None:
129
147
  if not any(char in mode for char in "rwax"):
@@ -167,6 +185,44 @@ class _FileIO(Generic[T]):
167
185
  for message in batch.output:
168
186
  yield message
169
187
 
188
+ async def _consume_watch_output(self, exec_id: str) -> None:
189
+ def item_handler(item: Optional[bytes]):
190
+ self._watch_output_buffer.append(item)
191
+
192
+ def completion_check(item: Optional[bytes]):
193
+ return item is None
194
+
195
+ await consume_stream_with_retries(
196
+ self._consume_output(exec_id),
197
+ item_handler,
198
+ completion_check,
199
+ )
200
+
201
+ async def _parse_watch_output(self, event: bytes) -> Optional[FileWatchEvent]:
202
+ try:
203
+ event_json = json.loads(event.decode())
204
+ return FileWatchEvent(type=FileWatchEventType(event_json["event_type"]), paths=event_json["paths"])
205
+ except (json.JSONDecodeError, KeyError, ValueError):
206
+ # skip invalid events
207
+ return None
208
+
209
+ async def _stream_watch_output(self) -> AsyncIterator[FileWatchEvent]:
210
+ buffer = b""
211
+ while True:
212
+ if len(self._watch_output_buffer) > 0:
213
+ item = self._watch_output_buffer.pop(0)
214
+ if item is None:
215
+ break
216
+ buffer += item
217
+ # a single event may be split across multiple messages, the end of an event is marked by two newlines
218
+ if buffer.endswith(b"\n\n"):
219
+ event = await self._parse_watch_output(buffer.strip())
220
+ if event is not None:
221
+ yield event
222
+ buffer = b""
223
+ else:
224
+ await asyncio.sleep(0.1)
225
+
170
226
  async def _wait(self, exec_id: str) -> bytes:
171
227
  # The logic here is similar to how output is read from `exec`
172
228
  output = b""
@@ -391,6 +447,36 @@ class _FileIO(Generic[T]):
391
447
  )
392
448
  await self._wait(resp.exec_id)
393
449
 
450
+ @classmethod
451
+ async def watch(
452
+ cls,
453
+ path: str,
454
+ client: _Client,
455
+ task_id: str,
456
+ filter: Optional[list[FileWatchEventType]] = None,
457
+ recursive: bool = False,
458
+ timeout: Optional[int] = None,
459
+ ) -> AsyncIterator[FileWatchEvent]:
460
+ self = cls.__new__(cls)
461
+ self._client = client
462
+ self._task_id = task_id
463
+ resp = await self._make_request(
464
+ api_pb2.ContainerFilesystemExecRequest(
465
+ file_watch_request=api_pb2.ContainerFileWatchRequest(
466
+ path=path,
467
+ recursive=recursive,
468
+ timeout_secs=timeout,
469
+ ),
470
+ task_id=self._task_id,
471
+ )
472
+ )
473
+ task = asyncio.create_task(self._consume_watch_output(resp.exec_id))
474
+ async for event in self._stream_watch_output():
475
+ if filter and event.type not in filter:
476
+ continue
477
+ yield event
478
+ task.cancel()
479
+
394
480
  async def _close(self) -> None:
395
481
  # Buffer is flushed by the runner on close
396
482
  resp = await self._make_request(
modal/file_io.pyi CHANGED
@@ -1,4 +1,5 @@
1
1
  import _typeshed
2
+ import enum
2
3
  import modal.client
3
4
  import modal_proto.api_pb2
4
5
  import typing
@@ -13,14 +14,33 @@ async def _replace_bytes(
13
14
  file: _FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
14
15
  ) -> None: ...
15
16
 
17
+ class FileWatchEventType(enum.Enum):
18
+ Unknown = "Unknown"
19
+ Access = "Access"
20
+ Create = "Create"
21
+ Modify = "Modify"
22
+ Remove = "Remove"
23
+
24
+ class FileWatchEvent:
25
+ paths: list[str]
26
+ type: FileWatchEventType
27
+
28
+ def __init__(self, paths: list[str], type: FileWatchEventType) -> None: ...
29
+ def __repr__(self): ...
30
+ def __eq__(self, other): ...
31
+
16
32
  class _FileIO(typing.Generic[T]):
17
33
  _task_id: str
18
34
  _file_descriptor: str
19
35
  _client: typing.Optional[modal.client._Client]
36
+ _watch_output_buffer: list[typing.Optional[bytes]]
20
37
 
21
38
  def _validate_mode(self, mode: str) -> None: ...
22
39
  def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
23
40
  def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
41
+ async def _consume_watch_output(self, exec_id: str) -> None: ...
42
+ async def _parse_watch_output(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
43
+ def _stream_watch_output(self) -> typing.AsyncIterator[FileWatchEvent]: ...
24
44
  async def _wait(self, exec_id: str) -> bytes: ...
25
45
  def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
26
46
  async def _open_file(self, path: str, mode: str) -> None: ...
@@ -49,6 +69,16 @@ class _FileIO(typing.Generic[T]):
49
69
  async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None: ...
50
70
  @classmethod
51
71
  async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None: ...
72
+ @classmethod
73
+ def watch(
74
+ cls,
75
+ path: str,
76
+ client: modal.client._Client,
77
+ task_id: str,
78
+ filter: typing.Optional[list[FileWatchEventType]] = None,
79
+ recursive: bool = False,
80
+ timeout: typing.Optional[int] = None,
81
+ ) -> typing.AsyncIterator[FileWatchEvent]: ...
52
82
  async def _close(self) -> None: ...
53
83
  async def close(self) -> None: ...
54
84
  def _check_writable(self) -> None: ...
@@ -79,6 +109,7 @@ class FileIO(typing.Generic[T]):
79
109
  _task_id: str
80
110
  _file_descriptor: str
81
111
  _client: typing.Optional[modal.client.Client]
112
+ _watch_output_buffer: list[typing.Optional[bytes]]
82
113
 
83
114
  def __init__(self, /, *args, **kwargs): ...
84
115
  def _validate_mode(self, mode: str) -> None: ...
@@ -90,6 +121,24 @@ class FileIO(typing.Generic[T]):
90
121
 
91
122
  _consume_output: ___consume_output_spec
92
123
 
124
+ class ___consume_watch_output_spec(typing_extensions.Protocol):
125
+ def __call__(self, exec_id: str) -> None: ...
126
+ async def aio(self, exec_id: str) -> None: ...
127
+
128
+ _consume_watch_output: ___consume_watch_output_spec
129
+
130
+ class ___parse_watch_output_spec(typing_extensions.Protocol):
131
+ def __call__(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
132
+ async def aio(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
133
+
134
+ _parse_watch_output: ___parse_watch_output_spec
135
+
136
+ class ___stream_watch_output_spec(typing_extensions.Protocol):
137
+ def __call__(self) -> typing.Iterator[FileWatchEvent]: ...
138
+ def aio(self) -> typing.AsyncIterator[FileWatchEvent]: ...
139
+
140
+ _stream_watch_output: ___stream_watch_output_spec
141
+
93
142
  class ___wait_spec(typing_extensions.Protocol):
94
143
  def __call__(self, exec_id: str) -> bytes: ...
95
144
  async def aio(self, exec_id: str) -> bytes: ...
@@ -173,6 +222,16 @@ class FileIO(typing.Generic[T]):
173
222
  def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None: ...
174
223
  @classmethod
175
224
  def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None: ...
225
+ @classmethod
226
+ def watch(
227
+ cls,
228
+ path: str,
229
+ client: modal.client.Client,
230
+ task_id: str,
231
+ filter: typing.Optional[list[FileWatchEventType]] = None,
232
+ recursive: bool = False,
233
+ timeout: typing.Optional[int] = None,
234
+ ) -> typing.Iterator[FileWatchEvent]: ...
176
235
 
177
236
  class ___close_spec(typing_extensions.Protocol):
178
237
  def __call__(self) -> None: ...
modal/io_streams.py CHANGED
@@ -14,7 +14,8 @@ from typing import (
14
14
  from grpclib import Status
15
15
  from grpclib.exceptions import GRPCError, StreamTerminatedError
16
16
 
17
- from modal.exception import ClientClosed, InvalidError
17
+ from modal.exception import InvalidError
18
+ from modal.io_streams_helper import consume_stream_with_retries
18
19
  from modal_proto import api_pb2
19
20
 
20
21
  from ._utils.async_utils import synchronize_api
@@ -176,34 +177,21 @@ class _StreamReader(Generic[T]):
176
177
  if self._stream_type == StreamType.DEVNULL:
177
178
  return
178
179
 
179
- completed = False
180
- retries_remaining = 10
181
- while not completed:
182
- try:
183
- iterator = _container_process_logs_iterator(self._object_id, self._file_descriptor, self._client)
180
+ def item_handler(item: Optional[bytes]):
181
+ if self._stream_type == StreamType.STDOUT and item is not None:
182
+ print(item.decode("utf-8"), end="")
183
+ elif self._stream_type == StreamType.PIPE:
184
+ self._container_process_buffer.append(item)
184
185
 
185
- async for message in iterator:
186
- if self._stream_type == StreamType.STDOUT and message:
187
- print(message.decode("utf-8"), end="")
188
- elif self._stream_type == StreamType.PIPE:
189
- self._container_process_buffer.append(message)
190
- if message is None:
191
- completed = True
192
- break
186
+ def completion_check(item: Optional[bytes]):
187
+ return item is None
193
188
 
194
- except (GRPCError, StreamTerminatedError, ClientClosed) as exc:
195
- if retries_remaining > 0:
196
- retries_remaining -= 1
197
- if isinstance(exc, GRPCError):
198
- if exc.status in RETRYABLE_GRPC_STATUS_CODES:
199
- await asyncio.sleep(1.0)
200
- continue
201
- elif isinstance(exc, StreamTerminatedError):
202
- continue
203
- elif isinstance(exc, ClientClosed):
204
- # If the client was closed, the user has triggered a cleanup.
205
- break
206
- raise exc
189
+ iterator = _container_process_logs_iterator(self._object_id, self._file_descriptor, self._client)
190
+ await consume_stream_with_retries(
191
+ iterator,
192
+ item_handler,
193
+ completion_check,
194
+ )
207
195
 
208
196
  async def _stream_container_process(self) -> AsyncGenerator[tuple[Optional[bytes], str], None]:
209
197
  """Streams the container process buffer to the reader."""
@@ -0,0 +1,53 @@
1
+ # Copyright Modal Labs 2024
2
+ import asyncio
3
+ from typing import AsyncIterator, Callable, TypeVar
4
+
5
+ from grpclib.exceptions import GRPCError, StreamTerminatedError
6
+
7
+ from modal.exception import ClientClosed
8
+
9
+ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ async def consume_stream_with_retries(
15
+ stream: AsyncIterator[T],
16
+ item_handler: Callable[[T], None],
17
+ completion_check: Callable[[T], bool],
18
+ max_retries: int = 10,
19
+ retry_delay: float = 1.0,
20
+ ) -> None:
21
+ """mdmd:hidden
22
+ Helper function to consume a stream with retry logic for transient errors.
23
+
24
+ Args:
25
+ stream_generator: Function that returns an AsyncIterator to consume
26
+ item_handler: Callback function to handle each item from the stream
27
+ completion_check: Callback function to check if the stream is complete
28
+ max_retries: Maximum number of retry attempts
29
+ retry_delay: Delay in seconds between retries
30
+ """
31
+ completed = False
32
+ retries_remaining = max_retries
33
+
34
+ while not completed:
35
+ try:
36
+ async for item in stream:
37
+ item_handler(item)
38
+ if completion_check(item):
39
+ completed = True
40
+ break
41
+
42
+ except (GRPCError, StreamTerminatedError, ClientClosed) as exc:
43
+ if retries_remaining > 0:
44
+ retries_remaining -= 1
45
+ if isinstance(exc, GRPCError):
46
+ if exc.status in RETRYABLE_GRPC_STATUS_CODES:
47
+ await asyncio.sleep(retry_delay)
48
+ continue
49
+ elif isinstance(exc, StreamTerminatedError):
50
+ continue
51
+ elif isinstance(exc, ClientClosed):
52
+ break
53
+ raise
modal/sandbox.py CHANGED
@@ -2,7 +2,7 @@
2
2
  import asyncio
3
3
  import os
4
4
  from collections.abc import AsyncGenerator, Sequence
5
- from typing import TYPE_CHECKING, Literal, Optional, Union, overload
5
+ from typing import TYPE_CHECKING, AsyncIterator, Literal, Optional, Union, overload
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  import _typeshed
@@ -26,7 +26,7 @@ from .client import _Client
26
26
  from .config import config
27
27
  from .container_process import _ContainerProcess
28
28
  from .exception import ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
29
- from .file_io import _FileIO
29
+ from .file_io import FileWatchEvent, FileWatchEventType, _FileIO
30
30
  from .gpu import GPU_T
31
31
  from .image import _Image
32
32
  from .io_streams import StreamReader, StreamWriter, _StreamReader, _StreamWriter
@@ -579,6 +579,17 @@ class _Sandbox(_Object, type_prefix="sb"):
579
579
  task_id = await self._get_task_id()
580
580
  return await _FileIO.rm(path, self._client, task_id, recursive)
581
581
 
582
+ async def watch(
583
+ self,
584
+ path: str,
585
+ filter: Optional[list[FileWatchEventType]] = None,
586
+ recursive: Optional[bool] = None,
587
+ timeout: Optional[int] = None,
588
+ ) -> AsyncIterator[FileWatchEvent]:
589
+ task_id = await self._get_task_id()
590
+ async for event in _FileIO.watch(path, self._client, task_id, filter, recursive, timeout):
591
+ yield event
592
+
582
593
  @property
583
594
  def stdout(self) -> _StreamReader[str]:
584
595
  """
modal/sandbox.pyi CHANGED
@@ -131,6 +131,13 @@ class _Sandbox(modal.object._Object):
131
131
  async def ls(self, path: str) -> list[str]: ...
132
132
  async def mkdir(self, path: str, parents: bool = False) -> None: ...
133
133
  async def rm(self, path: str, recursive: bool = False) -> None: ...
134
+ def watch(
135
+ self,
136
+ path: str,
137
+ filter: typing.Optional[list[modal.file_io.FileWatchEventType]] = None,
138
+ recursive: typing.Optional[bool] = None,
139
+ timeout: typing.Optional[int] = None,
140
+ ) -> typing.AsyncIterator[modal.file_io.FileWatchEvent]: ...
134
141
  @property
135
142
  def stdout(self) -> modal.io_streams._StreamReader[str]: ...
136
143
  @property
@@ -388,6 +395,24 @@ class Sandbox(modal.object.Object):
388
395
 
389
396
  rm: __rm_spec
390
397
 
398
+ class __watch_spec(typing_extensions.Protocol):
399
+ def __call__(
400
+ self,
401
+ path: str,
402
+ filter: typing.Optional[list[modal.file_io.FileWatchEventType]] = None,
403
+ recursive: typing.Optional[bool] = None,
404
+ timeout: typing.Optional[int] = None,
405
+ ) -> typing.Iterator[modal.file_io.FileWatchEvent]: ...
406
+ def aio(
407
+ self,
408
+ path: str,
409
+ filter: typing.Optional[list[modal.file_io.FileWatchEventType]] = None,
410
+ recursive: typing.Optional[bool] = None,
411
+ timeout: typing.Optional[int] = None,
412
+ ) -> typing.AsyncIterator[modal.file_io.FileWatchEvent]: ...
413
+
414
+ watch: __watch_spec
415
+
391
416
  @property
392
417
  def stdout(self) -> modal.io_streams.StreamReader[str]: ...
393
418
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.0
3
+ Version: 0.71.1
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -19,7 +19,7 @@ modal/app.py,sha256=vEE0cK5QPF6_cdW5AJvcuWxz5KmeprHwBEtlDkVRHgE,45582
19
19
  modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
22
- modal/client.pyi,sha256=5QiH7ZslySP0KWVsNLVAIkWDXnolJ_QvFtGMGNPnKq0,7278
22
+ modal/client.pyi,sha256=uq7W4l6HMPzZo73ALVZrAkQ436KKcQIR861Uoh144uI,7278
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=3hjb0JcoPjxKZNeK22f5rR43bZRBjoRI7_EMZXY7YrE,31172
@@ -33,16 +33,17 @@ modal/environments.py,sha256=wbv9ttFCbzATGfwcmvYiG608PfHovx0AQmawsg-jmic,6660
33
33
  modal/environments.pyi,sha256=rF7oaaELoSNuoD6qImGnIbuGPtgWwR5SlcExyYJ61hQ,3515
34
34
  modal/exception.py,sha256=GEV6xMnVnkle0gsFZVLB4B7cUMyw8HzVDvAvPr34ZV4,5185
35
35
  modal/experimental.py,sha256=npfKbyMpI41uZZs9HW_QiB3E4ykWfDXZbACXXbw6qeA,2385
36
- modal/file_io.py,sha256=QXACorWL9NAfvRcnBcP34e5H7u9xf7g5zt2BHj9quC8,16713
37
- modal/file_io.pyi,sha256=GMhCCRyMftXYI3HqI9EdGPOx70CbCNi-VC5Sfy5TYnc,7631
36
+ modal/file_io.py,sha256=ZR8VBCDsDt5uB9TNN9XbEh7sniJzM_5YL47m8WP0m5c,19617
37
+ modal/file_io.pyi,sha256=79Fg75BjmMEeCX0Lx-Z8C4XSNPCotwNdK6ZLIDFm2f4,9770
38
38
  modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms,5571
39
39
  modal/functions.py,sha256=aXXXr3rk7BCeh5OWMvxGksGm8FQoYCyrBDGV74FPoPE,67827
40
40
  modal/functions.pyi,sha256=oMmcExtQxHwPej06jQ3uBe1tUlSR3VbAx7u3Vm-Ohhg,25317
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
42
  modal/image.py,sha256=Krvcsclomp9YsqSNwFj2FoAyg10OvU47RDnsNCwjGbQ,84550
43
43
  modal/image.pyi,sha256=1fgGvsL5Rb0Sa-_2OCgIyJ_QgHcL0_9MWD_oY7cyFFM,24937
44
- modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
44
+ modal/io_streams.py,sha256=Xxc5grJiO94nBA48FFWH3S3g6SPR0xFVgZ_DZ1oFmvI,14428
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
+ modal/io_streams_helper.py,sha256=B5Ui56ph7LkRpZX0tAF80Q-gOMsvPPLx5bpIPX0kgDc,1772
46
47
  modal/mount.py,sha256=wOr-2vmKImsE3lHBII8hL2gYy5ng46R58QwId4JultQ,29313
47
48
  modal/mount.pyi,sha256=FiNV1wIKFvd0ZMZ0tm1mz6ZSA5Hjsge-kFSA5tPWfcI,10503
48
49
  modal/network_file_system.py,sha256=INj1TfN_Fsmabmlte7anvey1epodjbMmjBW_TIJSST4,14406
@@ -63,8 +64,8 @@ modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
63
64
  modal/runner.py,sha256=mhqgRdjD5cUDpBQIokiX7OCfVblpGV6aWmZ-WvWJgGg,24114
64
65
  modal/runner.pyi,sha256=YmP4EOCNjjkwSIPi2Gl6hF_ji_ytkxz9dw3iB9KXaOI,5275
65
66
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
66
- modal/sandbox.py,sha256=9ytERSasudhklfNgy9PSRNDKrBoTc1o69ZR8UEIZs3w,28120
67
- modal/sandbox.pyi,sha256=k8_vHjN3oigxSCF13Cm2HfcSHuliGuSb8ryd3CGqwoA,19815
67
+ modal/sandbox.py,sha256=eSxTEjeHQRVARIJEsnQLngN1lkpROXWZT0FCWBernyI,28573
68
+ modal/sandbox.pyi,sha256=lceWDeXqzdeRc1iIuM5YmpoZlBJcVBpQO1Jc3AT1AQI,20809
68
69
  modal/schedule.py,sha256=0ZFpKs1bOxeo5n3HZjoL7OE2ktsb-_oGtq-WJEPO4tY,2615
69
70
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
70
71
  modal/secret.py,sha256=lebVTZi4fC9PXQpLVmsvgQLzy-2Kzxv1WBD0Jr2wsxQ,10117
@@ -165,10 +166,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
165
166
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
167
  modal_version/__init__.py,sha256=BEBWj9tcbFUwzEjUrqly601rauw5cYsHdcmJHs3iu0s,470
167
168
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
168
- modal_version/_version_generated.py,sha256=nv6-XZehjGHOf7mEMKsZRHnY2RHvH3znARFkIyPYuuE,148
169
- modal-0.71.0.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
- modal-0.71.0.dist-info/METADATA,sha256=RQZNox5Y9gkQ5k4s3nqGDp3aP3qsddaPODPdusTpe74,2328
171
- modal-0.71.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
- modal-0.71.0.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
- modal-0.71.0.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
- modal-0.71.0.dist-info/RECORD,,
169
+ modal_version/_version_generated.py,sha256=-6j4dF5udT1J3FkOZ3fPAMw-HT0ESmNAwXdT64NOp4A,148
170
+ modal-0.71.1.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
171
+ modal-0.71.1.dist-info/METADATA,sha256=k-Q6SXYTk8SGVYiU8blPMqGZsGxgQbjOHj6kuKSef0M,2328
172
+ modal-0.71.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
173
+ modal-0.71.1.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
174
+ modal-0.71.1.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
175
+ modal-0.71.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 0 # git: b06252b
4
+ build_number = 1 # git: e8fb7ee
File without changes