modal 1.0.1.dev5__py3-none-any.whl → 1.0.2__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/_functions.py CHANGED
@@ -99,6 +99,7 @@ if TYPE_CHECKING:
99
99
  import modal.cls
100
100
  import modal.partial_function
101
101
 
102
+ MAX_INTERNAL_FAILURE_COUNT = 8
102
103
 
103
104
  @dataclasses.dataclass
104
105
  class _RetryContext:
@@ -348,10 +349,14 @@ class _InputPlaneInvocation:
348
349
  stub: ModalClientModal,
349
350
  attempt_token: str,
350
351
  client: _Client,
352
+ input_item: api_pb2.FunctionPutInputsItem,
353
+ function_id: str,
351
354
  ):
352
355
  self.stub = stub
353
356
  self.client = client # Used by the deserializer.
354
357
  self.attempt_token = attempt_token
358
+ self.input_item = input_item
359
+ self.function_id = function_id
355
360
 
356
361
  @staticmethod
357
362
  async def create(
@@ -365,36 +370,55 @@ class _InputPlaneInvocation:
365
370
  stub = await client.get_stub(input_plane_url)
366
371
 
367
372
  function_id = function.object_id
368
- item = await _create_input(args, kwargs, stub, method_name=function._use_method_name)
373
+ input_item = await _create_input(args, kwargs, stub, method_name=function._use_method_name)
369
374
 
370
375
  request = api_pb2.AttemptStartRequest(
371
376
  function_id=function_id,
372
377
  parent_input_id=current_input_id() or "",
373
- input=item,
378
+ input=input_item,
374
379
  )
375
380
  response = await retry_transient_errors(stub.AttemptStart, request)
376
381
  attempt_token = response.attempt_token
377
382
 
378
- return _InputPlaneInvocation(stub, attempt_token, client)
383
+ return _InputPlaneInvocation(stub, attempt_token, client, input_item, function_id)
379
384
 
380
385
  async def run_function(self) -> Any:
381
- # TODO(nathan): add retry logic
386
+ # This will retry when the server returns GENERIC_STATUS_INTERNAL_FAILURE, i.e. lost inputs or worker preemption
387
+ # TODO(ryan): add logic to retry for user defined retry policy
388
+ internal_failure_count = 0
382
389
  while True:
383
- request = api_pb2.AttemptAwaitRequest(
390
+ await_request = api_pb2.AttemptAwaitRequest(
384
391
  attempt_token=self.attempt_token,
385
392
  timeout_secs=OUTPUTS_TIMEOUT,
386
393
  requested_at=time.time(),
387
394
  )
388
- response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
395
+ await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
389
396
  self.stub.AttemptAwait,
390
- request,
397
+ await_request,
391
398
  attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
392
399
  )
393
400
 
394
- if response.HasField("output"):
395
- return await _process_result(
396
- response.output.result, response.output.data_format, self.stub, self.client
401
+ try:
402
+ if await_response.HasField("output"):
403
+ return await _process_result(
404
+ await_response.output.result, await_response.output.data_format, self.stub, self.client
405
+ )
406
+ except InternalFailure as e:
407
+ internal_failure_count += 1
408
+ # Limit the number of times we retry
409
+ if internal_failure_count >= MAX_INTERNAL_FAILURE_COUNT:
410
+ raise e
411
+ # For system failures on the server, we retry immediately,
412
+ # and the failure does not count towards the retry policy.
413
+ retry_request = api_pb2.AttemptRetryRequest(
414
+ function_id=self.function_id,
415
+ parent_input_id=current_input_id() or "",
416
+ input=self.input_item,
417
+ attempt_token=self.attempt_token,
397
418
  )
419
+ # TODO(ryan): Add exponential backoff?
420
+ retry_response = await retry_transient_errors(self.stub.AttemptRetry, retry_request)
421
+ self.attempt_token = retry_response.attempt_token
398
422
 
399
423
 
400
424
  # Wrapper type for api_pb2.FunctionStats
@@ -791,6 +815,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
791
815
  if app and app.name:
792
816
  app_name = app.name
793
817
 
818
+ # on builder > 2024.10 we mount client dependencies at runtime
819
+ mount_client_dependencies = False
820
+ if image._metadata is not None:
821
+ mount_client_dependencies = image._metadata.image_builder_version > "2024.10"
822
+
794
823
  # Relies on dicts being ordered (true as of Python 3.6).
795
824
  volume_mounts = [
796
825
  api_pb2.VolumeMount(
@@ -860,6 +889,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
860
889
  schedule=schedule.proto_message if schedule is not None else None,
861
890
  snapshot_debug=config.get("snapshot_debug"),
862
891
  experimental_options=experimental_options or {},
892
+ mount_client_dependencies=mount_client_dependencies,
863
893
  # ---
864
894
  _experimental_group_size=cluster_size or 0, # Experimental: Clustered functions
865
895
  _experimental_concurrent_cancellations=True,
@@ -8,14 +8,14 @@ from typing import BinaryIO, Callable, Optional
8
8
  # Note: this module needs to import aiohttp in global scope
9
9
  # This takes about 50ms and isn't needed in many cases for Modal execution
10
10
  # To avoid this, we import it in local scope when needed (blob_utils.py)
11
- from aiohttp import BytesIOPayload
11
+ from aiohttp import Payload
12
12
  from aiohttp.abc import AbstractStreamWriter
13
13
 
14
14
  # read ~16MiB chunks by default
15
15
  DEFAULT_SEGMENT_CHUNK_SIZE = 2**24
16
16
 
17
17
 
18
- class BytesIOSegmentPayload(BytesIOPayload):
18
+ class BytesIOSegmentPayload(Payload):
19
19
  """Modified bytes payload for concurrent sends of chunks from the same file.
20
20
 
21
21
  Adds:
@@ -26,6 +26,8 @@ class BytesIOSegmentPayload(BytesIOPayload):
26
26
  Feels like this should be in some standard lib...
27
27
  """
28
28
 
29
+ _value: BinaryIO
30
+
29
31
  def __init__(
30
32
  self,
31
33
  bytes_io: BinaryIO, # should *not* be shared as IO position modification is not locked
@@ -36,6 +38,7 @@ class BytesIOSegmentPayload(BytesIOPayload):
36
38
  ):
37
39
  # not thread safe constructor!
38
40
  super().__init__(bytes_io)
41
+ self._size = segment_length
39
42
  self.initial_seek_pos = bytes_io.tell()
40
43
  self.segment_start = segment_start
41
44
  self.segment_length = segment_length
@@ -46,18 +49,26 @@ class BytesIOSegmentPayload(BytesIOPayload):
46
49
  self.progress_report_cb = progress_report_cb or (lambda *_, **__: None)
47
50
  self.reset_state()
48
51
 
52
+ def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
53
+ self._value.seek(self.initial_seek_pos)
54
+ return self._value.read().decode(encoding, errors)
55
+
49
56
  def reset_state(self):
50
57
  self._md5_checksum = hashlib.md5()
51
58
  self.num_bytes_read = 0
52
59
  self._value.seek(self.initial_seek_pos)
53
60
 
54
61
  @contextmanager
55
- def reset_on_error(self):
62
+ def reset_on_error(self, subtract_progress: bool = False):
56
63
  try:
57
64
  yield
58
65
  except Exception as exc:
59
66
  try:
60
- self.progress_report_cb(reset=True)
67
+ if subtract_progress:
68
+ negative_progress = -self.num_bytes_read
69
+ self.progress_report_cb(advance=negative_progress)
70
+ else:
71
+ self.progress_report_cb(reset=True)
61
72
  except Exception as cb_exc:
62
73
  raise cb_exc from exc
63
74
  raise exc
@@ -72,14 +83,21 @@ class BytesIOSegmentPayload(BytesIOPayload):
72
83
  return self._md5_checksum
73
84
 
74
85
  async def write(self, writer: "AbstractStreamWriter"):
86
+ # On aiohttp < 3.12.0 - this is the method that's being called on a custom payload,
87
+ # but on aiohttp 3.12+ `write_with_length` is called directly.
88
+ await self.write_with_length(writer, None)
89
+
90
+ async def write_with_length(self, writer: AbstractStreamWriter, content_length: Optional[int]):
75
91
  loop = asyncio.get_event_loop()
76
92
 
77
93
  async def safe_read():
78
94
  read_start = self.initial_seek_pos + self.segment_start + self.num_bytes_read
79
95
  self._value.seek(read_start)
80
96
  num_bytes = min(self.chunk_size, self.remaining_bytes())
81
- chunk = await loop.run_in_executor(None, self._value.read, num_bytes)
97
+ if content_length is not None:
98
+ num_bytes = min(num_bytes, content_length)
82
99
 
100
+ chunk = await loop.run_in_executor(None, self._value.read, num_bytes)
83
101
  await loop.run_in_executor(None, self._md5_checksum.update, chunk)
84
102
  self.num_bytes_read += len(chunk)
85
103
  return chunk
modal/client.pyi CHANGED
@@ -27,11 +27,7 @@ class _Client:
27
27
  _snapshotted: bool
28
28
 
29
29
  def __init__(
30
- self,
31
- server_url: str,
32
- client_type: int,
33
- credentials: typing.Optional[tuple[str, str]],
34
- version: str = "1.0.1.dev5",
30
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.0.2"
35
31
  ): ...
36
32
  def is_closed(self) -> bool: ...
37
33
  @property
@@ -90,11 +86,7 @@ class Client:
90
86
  _snapshotted: bool
91
87
 
92
88
  def __init__(
93
- self,
94
- server_url: str,
95
- client_type: int,
96
- credentials: typing.Optional[tuple[str, str]],
97
- version: str = "1.0.1.dev5",
89
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.0.2"
98
90
  ): ...
99
91
  def is_closed(self) -> bool: ...
100
92
  @property
modal/exception.py CHANGED
@@ -161,6 +161,8 @@ def simulate_preemption(wait_seconds: int, jitter_seconds: int = 0):
161
161
  See https://modal.com/docs/guide/preemption for more details on preemption
162
162
  handling.
163
163
  """
164
+ if wait_seconds <= 0:
165
+ raise ValueError("Time to wait must be greater than 0")
164
166
  signal.signal(signal.SIGALRM, _simulate_preemption_interrupt)
165
167
  jitter = random.randrange(0, jitter_seconds) if jitter_seconds else 0
166
168
  signal.alarm(wait_seconds + jitter)
modal/image.py CHANGED
@@ -1442,7 +1442,9 @@ class _Image(_Object, type_prefix="im"):
1442
1442
  if version > "2024.10":
1443
1443
  # for convenience when launching in a sandbox: sleep for 48h
1444
1444
  commands.append(f'CMD ["sleep", "{48 * 3600}"]')
1445
- context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, python_version)}
1445
+ context_files = {}
1446
+ if version <= "2024.10":
1447
+ context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, python_version)}
1446
1448
  return DockerfileSpec(commands=commands, context_files=context_files)
1447
1449
 
1448
1450
  return _Image._from_args(
@@ -1517,12 +1519,15 @@ class _Image(_Object, type_prefix="im"):
1517
1519
 
1518
1520
  # Note: this change is because we install dependencies with uv in 2024.10+
1519
1521
  requirements_prefix = "python -m " if builder_version < "2024.10" else ""
1520
- modal_requirements_commands = [
1521
- f"COPY {CONTAINER_REQUIREMENTS_PATH} {CONTAINER_REQUIREMENTS_PATH}",
1522
- f"RUN python -m pip install --upgrade {_base_image_config('package_tools', builder_version)}",
1523
- f"RUN {requirements_prefix}{_get_modal_requirements_command(builder_version)}",
1524
- ]
1525
- if builder_version > "2023.12":
1522
+ modal_requirements_commands = []
1523
+ if builder_version <= "2024.10":
1524
+ # past 2024.10, client dependencies are mounted at runtime
1525
+ modal_requirements_commands.extend([
1526
+ f"COPY {CONTAINER_REQUIREMENTS_PATH} {CONTAINER_REQUIREMENTS_PATH}",
1527
+ f"RUN python -m pip install --upgrade {_base_image_config('package_tools', builder_version)}",
1528
+ f"RUN {requirements_prefix}{_get_modal_requirements_command(builder_version)}",
1529
+ ])
1530
+ if "2024.10" >= builder_version > "2023.12":
1526
1531
  modal_requirements_commands.append(f"RUN rm {CONTAINER_REQUIREMENTS_PATH}")
1527
1532
 
1528
1533
  return [
@@ -1585,7 +1590,9 @@ class _Image(_Object, type_prefix="im"):
1585
1590
 
1586
1591
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1587
1592
  commands = _Image._registry_setup_commands(tag, version, setup_dockerfile_commands, add_python)
1588
- context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, add_python)}
1593
+ context_files = {}
1594
+ if version <= "2024.10":
1595
+ context_files = {CONTAINER_REQUIREMENTS_PATH: _get_modal_requirements_path(version, add_python)}
1589
1596
  return DockerfileSpec(commands=commands, context_files=context_files)
1590
1597
 
1591
1598
  return _Image._from_args(
@@ -1792,8 +1799,10 @@ class _Image(_Object, type_prefix="im"):
1792
1799
 
1793
1800
  def build_dockerfile_python(version: ImageBuilderVersion) -> DockerfileSpec:
1794
1801
  commands = _Image._registry_setup_commands("base", version, [], add_python)
1795
- requirements_path = _get_modal_requirements_path(version, add_python)
1796
- context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
1802
+ context_files = {}
1803
+ if version <= "2024.10":
1804
+ requirements_path = _get_modal_requirements_path(version, add_python)
1805
+ context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
1797
1806
  return DockerfileSpec(commands=commands, context_files=context_files)
1798
1807
 
1799
1808
  return _Image._from_args(
@@ -1810,22 +1819,35 @@ class _Image(_Object, type_prefix="im"):
1810
1819
  raise TypeError("The `python_version` argument should be a string, not a float.")
1811
1820
 
1812
1821
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1813
- requirements_path = _get_modal_requirements_path(version, python_version)
1814
- context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
1822
+ context_files = {}
1823
+ if version <= "2024.10":
1824
+ requirements_path = _get_modal_requirements_path(version, python_version)
1825
+ context_files = {CONTAINER_REQUIREMENTS_PATH: requirements_path}
1815
1826
  full_python_version = _dockerhub_python_version(version, python_version)
1816
1827
  debian_codename = _base_image_config("debian", version)
1817
1828
 
1818
1829
  commands = [
1819
1830
  f"FROM python:{full_python_version}-slim-{debian_codename}",
1820
- f"COPY {CONTAINER_REQUIREMENTS_PATH} {CONTAINER_REQUIREMENTS_PATH}",
1831
+ ]
1832
+ if version <= "2024.10":
1833
+ commands.extend([
1834
+ f"COPY {CONTAINER_REQUIREMENTS_PATH} {CONTAINER_REQUIREMENTS_PATH}",
1835
+ ])
1836
+ commands.extend([
1821
1837
  "RUN apt-get update",
1822
1838
  "RUN apt-get install -y gcc gfortran build-essential",
1823
1839
  f"RUN pip install --upgrade {_base_image_config('package_tools', version)}",
1824
- f"RUN {_get_modal_requirements_command(version)}",
1840
+ ])
1841
+ if version <= "2024.10":
1842
+ # after 2024.10, modal requirements are mounted at runtime
1843
+ commands.extend([
1844
+ f"RUN {_get_modal_requirements_command(version)}",
1845
+ ])
1846
+ commands.extend([
1825
1847
  # Set debian front-end to non-interactive to avoid users getting stuck with input prompts.
1826
1848
  "RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections",
1827
- ]
1828
- if version > "2023.12":
1849
+ ])
1850
+ if "2024.10" >= version > "2023.12":
1829
1851
  commands.append(f"RUN rm {CONTAINER_REQUIREMENTS_PATH}")
1830
1852
  if version > "2024.10":
1831
1853
  # for convenience when launching in a sandbox: sleep for 48h
modal/sandbox.py CHANGED
@@ -252,7 +252,8 @@ class _Sandbox(_Object, type_prefix="sb"):
252
252
  client: Optional[_Client] = None,
253
253
  ) -> "_Sandbox":
254
254
  """
255
- Create a new Sandbox to run untrusted, arbitrary code.
255
+ Create a new Sandbox to run untrusted, arbitrary code. The Sandbox's corresponding container
256
+ will be created asynchronously.
256
257
 
257
258
  **Usage**
258
259
 
modal/volume.py CHANGED
@@ -38,6 +38,7 @@ from ._utils.async_utils import (
38
38
  async_map,
39
39
  async_map_ordered,
40
40
  asyncnullcontext,
41
+ retry,
41
42
  synchronize_api,
42
43
  )
43
44
  from ._utils.blob_utils import (
@@ -375,10 +376,16 @@ class _Volume(_Object, type_prefix="vo"):
375
376
  "Please remove the glob `*` suffix."
376
377
  )
377
378
 
378
- req = api_pb2.VolumeListFilesRequest(volume_id=self.object_id, path=path, recursive=recursive)
379
- async for batch in self._client.stub.VolumeListFiles.unary_stream(req):
380
- for entry in batch.entries:
381
- yield FileEntry._from_proto(entry)
379
+ if self._is_v1:
380
+ req = api_pb2.VolumeListFilesRequest(volume_id=self.object_id, path=path, recursive=recursive)
381
+ async for batch in self._client.stub.VolumeListFiles.unary_stream(req):
382
+ for entry in batch.entries:
383
+ yield FileEntry._from_proto(entry)
384
+ else:
385
+ req = api_pb2.VolumeListFiles2Request(volume_id=self.object_id, path=path, recursive=recursive)
386
+ async for batch in self._client.stub.VolumeListFiles2.unary_stream(req):
387
+ for entry in batch.entries:
388
+ yield FileEntry._from_proto(entry)
382
389
 
383
390
  @live_method
384
391
  async def listdir(self, path: str, *, recursive: bool = False) -> list[FileEntry]:
@@ -499,7 +506,7 @@ class _Volume(_Object, type_prefix="vo"):
499
506
  num_bytes_written = 0
500
507
 
501
508
  async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
502
- async for chunk in get_response.content:
509
+ async for chunk in get_response.content.iter_any():
503
510
  num_chunk_bytes_written = 0
504
511
 
505
512
  while num_chunk_bytes_written < len(chunk):
@@ -526,8 +533,12 @@ class _Volume(_Object, type_prefix="vo"):
526
533
  @live_method
527
534
  async def remove_file(self, path: str, recursive: bool = False) -> None:
528
535
  """Remove a file or directory from a volume."""
529
- req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
530
- await retry_transient_errors(self._client.stub.VolumeRemoveFile, req)
536
+ if self._is_v1:
537
+ req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
538
+ await retry_transient_errors(self._client.stub.VolumeRemoveFile, req)
539
+ else:
540
+ req = api_pb2.VolumeRemoveFile2Request(volume_id=self.object_id, path=path, recursive=recursive)
541
+ await retry_transient_errors(self._client.stub.VolumeRemoveFile2, req)
531
542
 
532
543
  @live_method
533
544
  async def copy_files(self, src_paths: Sequence[str], dst_path: str) -> None:
@@ -825,7 +836,7 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
825
836
  progress_cb: Optional[Callable[..., Any]] = None,
826
837
  force: bool = False,
827
838
  hash_concurrency: int = multiprocessing.cpu_count(),
828
- put_concurrency: int = multiprocessing.cpu_count(),
839
+ put_concurrency: int = 128,
829
840
  ):
830
841
  """mdmd:hidden"""
831
842
  self._volume_id = volume_id
@@ -1010,6 +1021,16 @@ async def _put_missing_blocks(
1010
1021
  file_progress.pending_blocks.add(missing_block.block_index)
1011
1022
  task_progress_cb = functools.partial(progress_cb, task_id=file_progress.task_id)
1012
1023
 
1024
+ @retry(n_attempts=5, base_delay=0.5, timeout=None)
1025
+ async def put_missing_block_attempt(payload: BytesIOSegmentPayload) -> bytes:
1026
+ with payload.reset_on_error(subtract_progress=True):
1027
+ async with ClientSessionRegistry.get_session().put(
1028
+ missing_block.put_url,
1029
+ data=payload,
1030
+ ) as response:
1031
+ response.raise_for_status()
1032
+ return await response.content.read()
1033
+
1013
1034
  async with put_semaphore:
1014
1035
  with file_spec.source() as source_fp:
1015
1036
  payload = BytesIOSegmentPayload(
@@ -1020,13 +1041,7 @@ async def _put_missing_blocks(
1020
1041
  chunk_size=256 * 1024,
1021
1042
  progress_report_cb=task_progress_cb,
1022
1043
  )
1023
-
1024
- async with ClientSessionRegistry.get_session().put(
1025
- missing_block.put_url,
1026
- data=payload,
1027
- ) as response:
1028
- response.raise_for_status()
1029
- resp_data = await response.content.read()
1044
+ resp_data = await put_missing_block_attempt(payload)
1030
1045
 
1031
1046
  file_progress.pending_blocks.remove(missing_block.block_index)
1032
1047
 
modal/volume.pyi CHANGED
@@ -494,7 +494,7 @@ class _VolumeUploadContextManager2(_AbstractVolumeUploadContextManager):
494
494
  progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
495
495
  force: bool = False,
496
496
  hash_concurrency: int = 4,
497
- put_concurrency: int = 4,
497
+ put_concurrency: int = 128,
498
498
  ): ...
499
499
  async def __aenter__(self): ...
500
500
  async def __aexit__(self, exc_type, exc_val, exc_tb): ...
@@ -534,7 +534,7 @@ class VolumeUploadContextManager2(AbstractVolumeUploadContextManager):
534
534
  progress_cb: typing.Optional[collections.abc.Callable[..., typing.Any]] = None,
535
535
  force: bool = False,
536
536
  hash_concurrency: int = 4,
537
- put_concurrency: int = 4,
537
+ put_concurrency: int = 128,
538
538
  ): ...
539
539
  def __enter__(self): ...
540
540
  async def __aenter__(self): ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.0.1.dev5
3
+ Version: 1.0.2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=sTJcc9EbDuCKSwg3tL6ZckFw9WWdlkXW8mId1IvJCNc,2846
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=2aWxN2v5WUnj-R-sk6BzJ-3AvggkQGQjwhtvbDH3pds,777
5
5
  modal/_container_entrypoint.py,sha256=2Zx9O_EMJg0H77EdnC2vGKs6uFMWwbP1NLFf-qYmWmU,28962
6
- modal/_functions.py,sha256=HSQ8BVar2RsMD4ud83iUKvE7NSyEoVsTc-3quW8YObA,77149
6
+ modal/_functions.py,sha256=f8MhqxCzqBSUfrR_5uo60p1UHwNmPi_oEdZKDkFv2yw,78874
7
7
  modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
8
8
  modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
9
9
  modal/_object.py,sha256=KzzzZoM41UQUiY9TKOrft9BtZKgjWG_ukdlyLGjB4UY,10758
@@ -22,7 +22,7 @@ modal/app.py,sha256=NZ_rJ9TuMfiNiLg8-gOFgufD5flGtXWPHOZI0gdD3hE,46585
22
22
  modal/app.pyi,sha256=4-b_vbe3lNAqQPcMRpQCEDsE1zsVkQRJGUql9B7HvbM,22659
23
23
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
24
24
  modal/client.py,sha256=OwISJvkgMb-rHm9Gc4i-7YcDgGiZgwJ7F_PzwZH7a6Q,16847
25
- modal/client.pyi,sha256=CtjKr0AxTGX585gnCvWEEb1FddlU0E_bIJXP9WfAxnM,8457
25
+ modal/client.pyi,sha256=ewvDRpvkk-WVUWc7vPUKhANVIxoacP58o7JNh3ohaE8,8381
26
26
  modal/cloud_bucket_mount.py,sha256=YOe9nnvSr4ZbeCn587d7_VhE9IioZYRvF9VYQTQux08,5914
27
27
  modal/cloud_bucket_mount.pyi,sha256=30T3K1a89l6wzmEJ_J9iWv9SknoGqaZDx59Xs-ZQcmk,1607
28
28
  modal/cls.py,sha256=dBbeARwOWftlKd1cwtM0cHFtQWSWkwVXwVmOV4w0SyI,37907
@@ -34,14 +34,14 @@ modal/dict.py,sha256=w-Zuk3FXuwkyxKuF1ry86S8j2cvoC8-u4Ga0h-GfV1s,14324
34
34
  modal/dict.pyi,sha256=RBaQyOd1ABRNN7vIf5L_rv94y7Kq5Qn9IlKHSr4j8N0,8120
35
35
  modal/environments.py,sha256=gHFNLG78bqgizpQ4w_elz27QOqmcgAonFsmLs7NjUJ4,6804
36
36
  modal/environments.pyi,sha256=4HbI0kywveaUVI7HqDtZ4HphCTGXYi_wie2hz87up5A,3425
37
- modal/exception.py,sha256=cDiZMzkKZ2nLhaYG0Dt-_1YZs5LZEyUJNF05WqyCX90,5262
37
+ modal/exception.py,sha256=2pgq-j8JP-tB3yU2VmYOzn9CsynU9_h8IU_MgqgKegM,5352
38
38
  modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
39
39
  modal/file_io.pyi,sha256=oB7x-rKq7bmm8cA7Z7W9C9yeko7KK9m9i5GidFnkGK4,9569
40
40
  modal/file_pattern_matcher.py,sha256=wov-otB5M1oTdrYDtR2_VgacYin2srdtAP4McA1Cqzw,6516
41
41
  modal/functions.py,sha256=kcNHvqeGBxPI7Cgd57NIBBghkfbeFJzXO44WW0jSmao,325
42
42
  modal/functions.pyi,sha256=iqdp5ixtOOlm8bF-QYbD_G8VKqSRt_AVLT7AWjpn6pQ,16236
43
43
  modal/gpu.py,sha256=Kbhs_u49FaC2Zi0TjCdrpstpRtT5eZgecynmQi5IZVE,6752
44
- modal/image.py,sha256=bGs4FF9TofGQOy2RRjedD2MTyIsmF-9Gdk-I7BitMRw,91832
44
+ modal/image.py,sha256=-ia4ELkIh4sxOSqdYETAPV8EEb58Nue6DLdAxhM2C_Y,92687
45
45
  modal/image.pyi,sha256=2xjB6XOZDtm_chDdd90UoIj8pnDt5hCg6bOmu5fNaA4,25625
46
46
  modal/io_streams.py,sha256=YDZVQSDv05DeXg5TwcucC9Rj5hQBx2GXdluan9rIUpw,15467
47
47
  modal/io_streams.pyi,sha256=1UK6kWLREASQfq-wL9wSp5iqjLU0egRZPDn4LXs1PZY,5136
@@ -65,7 +65,7 @@ modal/retries.py,sha256=IvNLDM0f_GLUDD5VgEDoN09C88yoxSrCquinAuxT1Sc,5205
65
65
  modal/runner.py,sha256=nvpnU7U2O5d2WqME1QUTTwu-NkSLLwblytlGk7HXPAw,24152
66
66
  modal/runner.pyi,sha256=1AnEu48SUPnLWp3raQ2zJCV5lc85EGLkX2nL0bHWaB0,5162
67
67
  modal/running_app.py,sha256=v61mapYNV1-O-Uaho5EfJlryMLvIT9We0amUOSvSGx8,1188
68
- modal/sandbox.py,sha256=kmhQvM_ACdtQTnLvkMMZ4OJ4ddpPrdbettP06XjavCs,35656
68
+ modal/sandbox.py,sha256=zUldeCA98GLObLT1N9v1IILomcPYeLrQJRCRUMTtbLE,35734
69
69
  modal/sandbox.pyi,sha256=stxwoLcyQNToPISj6umlU8sDUgqzeooLdMs3BwIr740,28195
70
70
  modal/schedule.py,sha256=ewa7hb9NKYnoeSCW2PujZAbGGJL8btX6X3KalCFpc_M,2626
71
71
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
@@ -78,8 +78,8 @@ modal/snapshot.pyi,sha256=dIEBdTPb7O3VwkQ8TMPjfyU17RLuS9i0DnACxxHy8X4,676
78
78
  modal/stream_type.py,sha256=A6320qoAAWhEfwOCZfGtymQTu5AfLfJXXgARqooTPvY,417
79
79
  modal/token_flow.py,sha256=0_4KabXKsuE4OXTJ1OuLOtA-b1sesShztMZkkRFK7tA,7605
80
80
  modal/token_flow.pyi,sha256=ILbRv6JsZq-jK8jcJM7eB74e0PsbzwBm7hyPcV9lBlQ,2121
81
- modal/volume.py,sha256=DeIIK2gYxZ0rShF5NC2DBFKMuIkig1l4fs8VUYNGztY,43236
82
- modal/volume.pyi,sha256=9hPIMRBzGZycVL8uRfGpjSmNu_pCbkGAOyrnE86bU2Y,21113
81
+ modal/volume.py,sha256=LO8hYt2y0BOXsWzjMUUfOhSxBs7eILgTP5Lvi0IRjxM,44092
82
+ modal/volume.pyi,sha256=fM1RRKz521eIzM3Cd7osAhQ3UQEboQEjXWLQKerjSzY,21117
83
83
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
84
84
  modal/_runtime/asgi.py,sha256=_2xSTsDD27Cit7xnMs4lzkJA2wzer2_N4Oa3BkXFzVA,22521
85
85
  modal/_runtime/container_io_manager.py,sha256=6j0jO2-s9ShckM4SK45OapoQxWW9HQwQjFaBkXPJPwU,44763
@@ -93,7 +93,7 @@ modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
93
93
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
94
94
  modal/_utils/async_utils.py,sha256=zjdtdA54zvNL_RuREmN5NWFhhiRcNh8z0jT2rBc5RgY,28001
95
95
  modal/_utils/blob_utils.py,sha256=IexC2Jbtqp_Tkmy62ayfgzTYte0UPCNufB_v-DO21g8,18585
96
- modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
96
+ modal/_utils/bytes_io_segment_payload.py,sha256=vaXPq8b52-x6G2hwE7SrjS58pg_aRm7gV3bn3yjmTzQ,4261
97
97
  modal/_utils/deprecation.py,sha256=EXP1beU4pmEqEzWMLw6E3kUfNfpmNA_VOp6i0EHi93g,4856
98
98
  modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
99
99
  modal/_utils/function_utils.py,sha256=bhrjyOHPPXm6fAyJx3bzI1Yh56j6xh8eeMSFKdAWrHQ,26978
@@ -147,7 +147,7 @@ modal/requirements/2024.10.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddR
147
147
  modal/requirements/PREVIEW.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddRo,296
148
148
  modal/requirements/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
149
149
  modal/requirements/base-images.json,sha256=57vMSqzMbLBxw5tFWSaMiIkkVEps4JfX5PAtXGnkS4U,740
150
- modal-1.0.1.dev5.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
150
+ modal-1.0.2.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
151
151
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
152
152
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
153
153
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -155,10 +155,10 @@ modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,2
155
155
  modal_docs/mdmd/mdmd.py,sha256=Irx49MCCTlBOP4FBdLR--JrpA3-WhsVeriq0LGgsRic,6232
156
156
  modal_docs/mdmd/signatures.py,sha256=XJaZrK7Mdepk5fdX51A8uENiLFNil85Ud0d4MH8H5f0,3218
157
157
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
158
- modal_proto/api.proto,sha256=Qp0gpXAw0H8X8oPrU2Aa-_kewoI53-bUf0LWe_DO4hM,95862
158
+ modal_proto/api.proto,sha256=TZU4O7rl6_3p0kPhwtCFpUzF2ev7nogfT_dJia1q0YA,95957
159
159
  modal_proto/api_grpc.py,sha256=iY5o_Tm4VDP-Wa1JgA_NpQa_Y-4FYB_RN9wdSUExjwI,117469
160
- modal_proto/api_pb2.py,sha256=Jvq4X0FG5xQaiNVr7XlOZQWfmm_9b2JmN8LNT2hJJdU,337902
161
- modal_proto/api_pb2.pyi,sha256=KNcKpRgynsi3y6bVGen_y6FrHnXXUMOyREqbJExqb6Y,461341
160
+ modal_proto/api_pb2.py,sha256=05O5ZucslpXNOq5JMbC8HkqYl4YNJt_h6fqHQuK9yXw,338105
161
+ modal_proto/api_pb2.pyi,sha256=u-YdDR8e6cjSSID-wRuCUeOiEZuhVP9XNY_pTlrC820,462457
162
162
  modal_proto/api_pb2_grpc.py,sha256=NL5prOS_hh_pA1hVvQP_ZRE1w49N-PR8iNPRZ65i6nA,254089
163
163
  modal_proto/api_pb2_grpc.pyi,sha256=Xxgdcnv1mBnu5_AQxJ6fo0yz7GnqVU0HVObNfZWHVfM,59440
164
164
  modal_proto/modal_api_grpc.py,sha256=0ir2lnwT3-IgPcAWw98yWMAiqZPkjvNro9UBk4u8hnk,17763
@@ -170,10 +170,10 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
170
170
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
171
171
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
172
172
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
173
- modal_version/__init__.py,sha256=GKS0Y_AlUk_ykXJ30FJNUPcZmU-9yOH5i5FZjkmr_x0,120
173
+ modal_version/__init__.py,sha256=87JrO7uCroV09Gi_eqSkDSdOw3G7Zr7cvobWWqbfDWw,115
174
174
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
175
- modal-1.0.1.dev5.dist-info/METADATA,sha256=Pd_8kMQqXS66imWq5sY5Wyrbz0qpo-83KZH-SSOIoNo,2454
176
- modal-1.0.1.dev5.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
- modal-1.0.1.dev5.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
- modal-1.0.1.dev5.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
- modal-1.0.1.dev5.dist-info/RECORD,,
175
+ modal-1.0.2.dist-info/METADATA,sha256=wzmoFswDleRkVcnIxz3TT-z-QAN-hhjmbWbNd_oOrwo,2449
176
+ modal-1.0.2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
177
+ modal-1.0.2.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
178
+ modal-1.0.2.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
179
+ modal-1.0.2.dist-info/RECORD,,
modal_proto/api.proto CHANGED
@@ -2153,6 +2153,7 @@ message PTYInfo {
2153
2153
  message PortSpec {
2154
2154
  uint32 port = 1;
2155
2155
  bool unencrypted = 2;
2156
+ optional string tunnel_type = 3;
2156
2157
  }
2157
2158
 
2158
2159
  message PortSpecs {
@@ -2586,6 +2587,7 @@ message SandboxWaitResponse {
2586
2587
  message Schedule {
2587
2588
  message Cron {
2588
2589
  string cron_string = 1;
2590
+ string timezone = 2;
2589
2591
  }
2590
2592
  message Period {
2591
2593
  int32 years = 1;
@@ -2876,6 +2878,7 @@ message TunnelData {
2876
2878
  message TunnelStartRequest {
2877
2879
  uint32 port = 1;
2878
2880
  bool unencrypted = 2;
2881
+ optional string tunnel_type = 3;
2879
2882
  }
2880
2883
 
2881
2884
  message TunnelStartResponse {