modal 0.68.51__py3-none-any.whl → 0.70.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.
@@ -464,7 +464,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
464
464
  batch_wait_ms = function_def.batch_linger_ms or 0
465
465
 
466
466
  # Get ids and metadata for objects (primarily functions and classes) on the app
467
- container_app: RunningApp = container_io_manager.get_app_objects()
467
+ container_app: RunningApp = container_io_manager.get_app_objects(container_args.app_layout)
468
468
 
469
469
  # Initialize objects on the app.
470
470
  # This is basically only functions and classes - anything else is deprecated and will be unsupported soon
@@ -21,7 +21,6 @@ from typing import (
21
21
  )
22
22
 
23
23
  from google.protobuf.empty_pb2 import Empty
24
- from google.protobuf.message import Message
25
24
  from grpclib import Status
26
25
  from synchronicity.async_wrap import asynccontextmanager
27
26
 
@@ -31,12 +30,12 @@ from modal._traceback import extract_traceback, print_exception
31
30
  from modal._utils.async_utils import TaskContext, asyncify, synchronize_api, synchronizer
32
31
  from modal._utils.blob_utils import MAX_OBJECT_SIZE_BYTES, blob_download, blob_upload
33
32
  from modal._utils.function_utils import _stream_function_call_data
34
- from modal._utils.grpc_utils import get_proto_oneof, retry_transient_errors
33
+ from modal._utils.grpc_utils import retry_transient_errors
35
34
  from modal._utils.package_utils import parse_major_minor_version
36
35
  from modal.client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
37
36
  from modal.config import config, logger
38
37
  from modal.exception import ClientClosed, InputCancellation, InvalidError, SerializationError
39
- from modal.running_app import RunningApp
38
+ from modal.running_app import RunningApp, running_app_from_layout
40
39
  from modal_proto import api_pb2
41
40
 
42
41
  if TYPE_CHECKING:
@@ -450,25 +449,19 @@ class _ContainerIOManager:
450
449
 
451
450
  await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
452
451
 
453
- async def get_app_objects(self) -> RunningApp:
454
- req = api_pb2.AppGetObjectsRequest(app_id=self.app_id, include_unindexed=True, only_class_function=True)
455
- resp = await retry_transient_errors(self._client.stub.AppGetObjects, req)
456
- logger.debug(f"AppGetObjects received {len(resp.items)} objects for app {self.app_id}")
452
+ async def get_app_objects(self, app_layout: api_pb2.AppLayout) -> RunningApp:
453
+ if len(app_layout.objects) == 0:
454
+ # TODO(erikbern): this should never happen! let's keep it in here for a short second
455
+ # until we've sanity checked that this is, in fact, dead code.
456
+ req = api_pb2.AppGetLayoutRequest(app_id=self.app_id)
457
+ resp = await retry_transient_errors(self._client.stub.AppGetLayout, req)
458
+ app_layout = resp.app_layout
457
459
 
458
- tag_to_object_id = {}
459
- object_handle_metadata = {}
460
- for item in resp.items:
461
- handle_metadata: Optional[Message] = get_proto_oneof(item.object, "handle_metadata_oneof")
462
- object_handle_metadata[item.object.object_id] = handle_metadata
463
- if item.tag:
464
- tag_to_object_id[item.tag] = item.object.object_id
465
-
466
- return RunningApp(
460
+ return running_app_from_layout(
467
461
  self.app_id,
462
+ app_layout,
463
+ self._client,
468
464
  environment_name=self._environment_name,
469
- tag_to_object_id=tag_to_object_id,
470
- object_handle_metadata=object_handle_metadata,
471
- client=self._client,
472
465
  )
473
466
 
474
467
  async def get_serialized_function(self) -> tuple[Optional[Any], Optional[Callable[..., Any]]]:
@@ -0,0 +1,58 @@
1
+ # Copyright Modal Labs 2024
2
+ import re
3
+ import shlex
4
+ from typing import Sequence
5
+
6
+ from ..exception import InvalidError
7
+
8
+
9
+ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
10
+ """
11
+ Extract all COPY command sources from a Dockerfile.
12
+ Combines multiline COPY commands into a single line.
13
+ """
14
+ copy_source_patterns: set[str] = set()
15
+ current_command = ""
16
+ copy_pattern = re.compile(r"^\s*COPY\s+(.+)$", re.IGNORECASE)
17
+
18
+ # First pass: handle line continuations and collect full commands
19
+ for line in dockerfile_lines:
20
+ line = line.strip()
21
+ if not line or line.startswith("#"):
22
+ # ignore comments and empty lines
23
+ continue
24
+
25
+ if current_command:
26
+ # Continue previous line
27
+ current_command += " " + line.rstrip("\\").strip()
28
+ else:
29
+ # Start new command
30
+ current_command = line.rstrip("\\").strip()
31
+
32
+ if not line.endswith("\\"):
33
+ # Command is complete
34
+
35
+ match = copy_pattern.match(current_command)
36
+ if match:
37
+ args = match.group(1)
38
+ parts = shlex.split(args)
39
+
40
+ if len(parts) >= 2:
41
+ # Last part is destination, everything else is a mount source
42
+ sources = parts[:-1]
43
+
44
+ for source in sources:
45
+ special_pattern = re.compile(r"^\s*--|\$\s*")
46
+ if special_pattern.match(source):
47
+ raise InvalidError(
48
+ f"COPY command: {source} using special flags/arguments/variables are not supported"
49
+ )
50
+
51
+ if source == ".":
52
+ copy_source_patterns.add("./**")
53
+ else:
54
+ copy_source_patterns.add(source)
55
+
56
+ current_command = ""
57
+
58
+ return list(copy_source_patterns)
modal/cli/launch.py CHANGED
@@ -77,6 +77,7 @@ def vscode(
77
77
  cpu: int = 8,
78
78
  memory: int = 32768,
79
79
  gpu: Optional[str] = None,
80
+ image: str = "debian:12",
80
81
  timeout: int = 3600,
81
82
  mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
82
83
  volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
@@ -86,6 +87,7 @@ def vscode(
86
87
  "cpu": cpu,
87
88
  "memory": memory,
88
89
  "gpu": gpu,
90
+ "image": image,
89
91
  "timeout": timeout,
90
92
  "mount": mount,
91
93
  "volume": volume,
@@ -15,9 +15,34 @@ from modal import App, Image, Mount, Queue, Secret, Volume, forward
15
15
  # Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
16
16
  args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
17
17
 
18
+ CODE_SERVER_INSTALLER = "https://code-server.dev/install.sh"
19
+ CODE_SERVER_ENTRYPOINT = (
20
+ "https://raw.githubusercontent.com/coder/code-server/refs/tags/v4.96.1/ci/release-image/entrypoint.sh"
21
+ )
22
+ FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz"
23
+
18
24
 
19
25
  app = App()
20
- app.image = Image.from_registry("codercom/code-server", add_python="3.11").dockerfile_commands("ENTRYPOINT []")
26
+ app.image = (
27
+ Image.from_registry(args.get("image"), add_python="3.11")
28
+ .apt_install("curl", "dumb-init", "git", "git-lfs")
29
+ .run_commands(
30
+ f"curl -fsSL {CODE_SERVER_INSTALLER} | sh",
31
+ f"curl -fsSL {CODE_SERVER_ENTRYPOINT} > /code-server.sh",
32
+ "chmod u+x /code-server.sh",
33
+ )
34
+ .run_commands(
35
+ 'ARCH="$(dpkg --print-architecture)"'
36
+ f' && curl -fsSL "{FIXUD_INSTALLER}" | tar -C /usr/local/bin -xzf - '
37
+ " && chown root:root /usr/local/bin/fixuid"
38
+ " && chmod 4755 /usr/local/bin/fixuid"
39
+ " && mkdir -p /etc/fixuid"
40
+ ' && echo "user: root" >> /etc/fixuid/config.yml'
41
+ ' && echo "group: root" >> /etc/fixuid/config.yml'
42
+ )
43
+ .run_commands("mkdir /home/coder")
44
+ .env({"ENTRYPOINTD": ""})
45
+ )
21
46
 
22
47
 
23
48
  mount = (
@@ -71,7 +96,7 @@ def run_vscode(q: Queue):
71
96
  url = tunnel.url
72
97
  threading.Thread(target=wait_for_port, args=((url, token), q)).start()
73
98
  subprocess.run(
74
- ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."],
99
+ ["/code-server.sh", "--bind-addr", "0.0.0.0:8080", "."],
75
100
  env={**os.environ, "SHELL": "/bin/bash", "PASSWORD": token},
76
101
  )
77
102
  q.put("done")
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.68.51"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.70.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.68.51"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.70.1"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/file_io.py CHANGED
@@ -18,7 +18,8 @@ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
18
18
  from .client import _Client
19
19
  from .exception import FilesystemExecutionError, InvalidError
20
20
 
21
- LARGE_FILE_SIZE_LIMIT = 16 * 1024 * 1024 # 16 MiB
21
+ WRITE_CHUNK_SIZE = 16 * 1024 * 1024 # 16 MiB
22
+ WRITE_FILE_SIZE_LIMIT = 1024 * 1024 * 1024 # 1 GiB
22
23
  READ_FILE_SIZE_LIMIT = 100 * 1024 * 1024 # 100 MiB
23
24
 
24
25
  ERROR_MAPPING = {
@@ -77,7 +78,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
77
78
  if start is not None and end is not None:
78
79
  if start >= end:
79
80
  raise InvalidError("start must be less than end")
80
- if len(data) > LARGE_FILE_SIZE_LIMIT:
81
+ if len(data) > WRITE_CHUNK_SIZE:
81
82
  raise InvalidError("Write request payload exceeds 16 MiB limit")
82
83
  resp = await file._make_request(
83
84
  api_pb2.ContainerFilesystemExecRequest(
@@ -288,15 +289,20 @@ class _FileIO(Generic[T]):
288
289
  self._validate_type(data)
289
290
  if isinstance(data, str):
290
291
  data = data.encode("utf-8")
291
- if len(data) > LARGE_FILE_SIZE_LIMIT:
292
- raise ValueError("Write request payload exceeds 16 MiB limit")
293
- resp = await self._make_request(
294
- api_pb2.ContainerFilesystemExecRequest(
295
- file_write_request=api_pb2.ContainerFileWriteRequest(file_descriptor=self._file_descriptor, data=data),
296
- task_id=self._task_id,
292
+ if len(data) > WRITE_FILE_SIZE_LIMIT:
293
+ raise ValueError("Write request payload exceeds 1 GiB limit")
294
+ for i in range(0, len(data), WRITE_CHUNK_SIZE):
295
+ chunk = data[i : i + WRITE_CHUNK_SIZE]
296
+ resp = await self._make_request(
297
+ api_pb2.ContainerFilesystemExecRequest(
298
+ file_write_request=api_pb2.ContainerFileWriteRequest(
299
+ file_descriptor=self._file_descriptor,
300
+ data=chunk,
301
+ ),
302
+ task_id=self._task_id,
303
+ )
297
304
  )
298
- )
299
- await self._wait(resp.exec_id)
305
+ await self._wait(resp.exec_id)
300
306
 
301
307
  async def flush(self) -> None:
302
308
  """Flush the buffer to disk."""
@@ -12,7 +12,7 @@ then asking it whether file paths match any of its patterns.
12
12
  import os
13
13
  from abc import abstractmethod
14
14
  from pathlib import Path
15
- from typing import Callable, Optional
15
+ from typing import Callable, Optional, Sequence, Union
16
16
 
17
17
  from ._utils.pattern_utils import Pattern
18
18
 
@@ -152,3 +152,13 @@ class FilePatternMatcher(_AbstractPatternMatcher):
152
152
  # with_repr allows us to use this matcher as a default value in a function signature
153
153
  # and get a nice repr in the docs and auto-generated type stubs:
154
154
  NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py")).with_repr(f"{__name__}.NON_PYTHON_FILES")
155
+ _NOTHING = (~FilePatternMatcher()).with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
156
+
157
+
158
+ def _ignore_fn(ignore: Union[Sequence[str], Callable[[Path], bool]]) -> Callable[[Path], bool]:
159
+ # if a callable is passed, return it
160
+ # otherwise, treat input as a sequence of patterns and return a callable pattern matcher for those
161
+ if callable(ignore):
162
+ return ignore
163
+
164
+ return FilePatternMatcher(*ignore)
modal/functions.pyi CHANGED
@@ -462,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
462
462
 
463
463
  _call_generator_nowait: ___call_generator_nowait_spec
464
464
 
465
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
465
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
466
466
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
467
467
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
468
468
 
469
- remote: __remote_spec[P, ReturnType]
469
+ remote: __remote_spec[ReturnType, P]
470
470
 
471
471
  class __remote_gen_spec(typing_extensions.Protocol):
472
472
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -479,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
479
479
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
480
480
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
481
481
 
482
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
482
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
483
483
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
484
484
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
485
485
 
486
- _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
486
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
487
487
 
488
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
488
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
489
489
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
490
490
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
491
491
 
492
- spawn: __spawn_spec[P, ReturnType]
492
+ spawn: __spawn_spec[ReturnType, P]
493
493
 
494
494
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
495
495
 
modal/image.py CHANGED
@@ -31,6 +31,9 @@ from ._serialization import serialize
31
31
  from ._utils.async_utils import synchronize_api
32
32
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
33
33
  from ._utils.deprecation import deprecation_error, deprecation_warning
34
+ from ._utils.docker_utils import (
35
+ extract_copy_command_patterns,
36
+ )
34
37
  from ._utils.function_utils import FunctionInfo
35
38
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
36
39
  from .client import _Client
@@ -38,7 +41,7 @@ from .cloud_bucket_mount import _CloudBucketMount
38
41
  from .config import config, logger, user_config_path
39
42
  from .environments import _get_environment_cached
40
43
  from .exception import InvalidError, NotFoundError, RemoteError, VersionError
41
- from .file_pattern_matcher import NON_PYTHON_FILES
44
+ from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
42
45
  from .gpu import GPU_T, parse_gpu_config
43
46
  from .mount import _Mount, python_standalone_mount_name
44
47
  from .network_file_system import _NetworkFileSystem
@@ -236,6 +239,33 @@ def _get_image_builder_version(server_version: ImageBuilderVersion) -> ImageBuil
236
239
  return version
237
240
 
238
241
 
242
+ def _create_context_mount(
243
+ docker_commands: Sequence[str],
244
+ ignore_fn: Callable[[Path], bool],
245
+ context_dir: Path,
246
+ ) -> Optional[_Mount]:
247
+ """
248
+ Creates a context mount from a list of docker commands.
249
+
250
+ 1. Paths are evaluated relative to context_dir.
251
+ 2. First selects inclusions based on COPY commands in the list of commands.
252
+ 3. Then ignore any files as per the ignore predicate.
253
+ """
254
+ copy_patterns = extract_copy_command_patterns(docker_commands)
255
+ if not copy_patterns:
256
+ return None # no mount needed
257
+ include_fn = FilePatternMatcher(*copy_patterns)
258
+
259
+ def ignore_with_include(source: Path) -> bool:
260
+ relative_source = source.relative_to(context_dir)
261
+ if not include_fn(relative_source) or ignore_fn(relative_source):
262
+ return True
263
+
264
+ return False
265
+
266
+ return _Mount._add_local_dir(Path("./"), PurePosixPath("/"), ignore=ignore_with_include)
267
+
268
+
239
269
  class _ImageRegistryConfig:
240
270
  """mdmd:hidden"""
241
271
 
@@ -396,7 +426,7 @@ class _Image(_Object, type_prefix="im"):
396
426
  build_function: Optional["modal.functions._Function"] = None,
397
427
  build_function_input: Optional[api_pb2.FunctionInput] = None,
398
428
  image_registry_config: Optional[_ImageRegistryConfig] = None,
399
- context_mount: Optional[_Mount] = None,
429
+ context_mount_function: Optional[Callable[[], Optional[_Mount]]] = None,
400
430
  force_build: bool = False,
401
431
  # For internal use only.
402
432
  _namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
@@ -423,13 +453,15 @@ class _Image(_Object, type_prefix="im"):
423
453
  deps = tuple(base_images.values()) + tuple(secrets)
424
454
  if build_function:
425
455
  deps += (build_function,)
426
- if context_mount:
427
- deps += (context_mount,)
428
456
  if image_registry_config and image_registry_config.secret:
429
457
  deps += (image_registry_config.secret,)
430
458
  return deps
431
459
 
432
460
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
461
+ context_mount = context_mount_function() if context_mount_function else None
462
+ if context_mount:
463
+ await resolver.load(context_mount)
464
+
433
465
  if _do_assert_no_mount_layers:
434
466
  for image in base_images.values():
435
467
  # base images can't have
@@ -596,7 +628,7 @@ class _Image(_Object, type_prefix="im"):
596
628
  return _Image._from_args(
597
629
  base_images={"base": self},
598
630
  dockerfile_function=build_dockerfile,
599
- context_mount=mount,
631
+ context_mount_function=lambda: mount,
600
632
  )
601
633
 
602
634
  def add_local_file(self, local_path: Union[str, Path], remote_path: str, *, copy: bool = False) -> "_Image":
@@ -684,7 +716,7 @@ class _Image(_Object, type_prefix="im"):
684
716
  # + make default remote_path="./"
685
717
  raise InvalidError("image.add_local_dir() currently only supports absolute remote_path values")
686
718
 
687
- mount = _Mount._add_local_dir(Path(local_path), Path(remote_path), ignore)
719
+ mount = _Mount._add_local_dir(Path(local_path), PurePosixPath(remote_path), ignore=_ignore_fn(ignore))
688
720
  return self._add_mount_layer_or_copy(mount, copy=copy)
689
721
 
690
722
  def copy_local_file(self, local_path: Union[str, Path], remote_path: Union[str, Path] = "./") -> "_Image":
@@ -695,7 +727,6 @@ class _Image(_Object, type_prefix="im"):
695
727
  """
696
728
  # TODO(elias): add pending deprecation with suggestion to use add_* instead
697
729
  basename = str(Path(local_path).name)
698
- mount = _Mount.from_local_file(local_path, remote_path=f"/{basename}")
699
730
 
700
731
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
701
732
  return DockerfileSpec(commands=["FROM base", f"COPY {basename} {remote_path}"], context_files={})
@@ -703,7 +734,7 @@ class _Image(_Object, type_prefix="im"):
703
734
  return _Image._from_args(
704
735
  base_images={"base": self},
705
736
  dockerfile_function=build_dockerfile,
706
- context_mount=mount,
737
+ context_mount_function=lambda: _Mount.from_local_file(local_path, remote_path=f"/{basename}"),
707
738
  )
708
739
 
709
740
  def add_local_python_source(
@@ -790,15 +821,15 @@ class _Image(_Object, type_prefix="im"):
790
821
  ```
791
822
  """
792
823
 
793
- mount = _Mount._add_local_dir(Path(local_path), Path("/"), ignore)
794
-
795
824
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
796
825
  return DockerfileSpec(commands=["FROM base", f"COPY . {remote_path}"], context_files={})
797
826
 
798
827
  return _Image._from_args(
799
828
  base_images={"base": self},
800
829
  dockerfile_function=build_dockerfile,
801
- context_mount=mount,
830
+ context_mount_function=lambda: _Mount._add_local_dir(
831
+ Path(local_path), PurePosixPath("/"), ignore=_ignore_fn(ignore)
832
+ ),
802
833
  )
803
834
 
804
835
  def pip_install(
@@ -1156,12 +1187,29 @@ class _Image(_Object, type_prefix="im"):
1156
1187
  # modal.Mount with local files to supply as build context for COPY commands
1157
1188
  context_mount: Optional[_Mount] = None,
1158
1189
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
1190
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1159
1191
  ) -> "_Image":
1160
1192
  """Extend an image with arbitrary Dockerfile-like commands."""
1161
1193
  cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1162
1194
  if not cmds:
1163
1195
  return self
1164
1196
 
1197
+ if context_mount:
1198
+ if ignore:
1199
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
1200
+
1201
+ def identity_context_mount_fn() -> Optional[_Mount]:
1202
+ return context_mount
1203
+
1204
+ context_mount_function = identity_context_mount_fn
1205
+ else:
1206
+
1207
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
1208
+ # use COPY commands and ignore patterns to construct implicit context mount
1209
+ return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1210
+
1211
+ context_mount_function = auto_created_context_mount_fn
1212
+
1165
1213
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1166
1214
  return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)
1167
1215
 
@@ -1170,7 +1218,7 @@ class _Image(_Object, type_prefix="im"):
1170
1218
  dockerfile_function=build_dockerfile,
1171
1219
  secrets=secrets,
1172
1220
  gpu_config=parse_gpu_config(gpu),
1173
- context_mount=context_mount,
1221
+ context_mount_function=context_mount_function,
1174
1222
  force_build=self.force_build or force_build,
1175
1223
  )
1176
1224
 
@@ -1400,11 +1448,15 @@ class _Image(_Object, type_prefix="im"):
1400
1448
  modal.Image.from_registry("nvcr.io/nvidia/pytorch:22.12-py3")
1401
1449
  ```
1402
1450
  """
1403
- context_mount = None
1404
- if add_python:
1405
- context_mount = _Mount.from_name(
1406
- python_standalone_mount_name(add_python),
1407
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1451
+
1452
+ def context_mount_function() -> Optional[_Mount]:
1453
+ return (
1454
+ _Mount.from_name(
1455
+ python_standalone_mount_name(add_python),
1456
+ namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1457
+ )
1458
+ if add_python
1459
+ else None
1408
1460
  )
1409
1461
 
1410
1462
  if "image_registry_config" not in kwargs and secret is not None:
@@ -1417,7 +1469,7 @@ class _Image(_Object, type_prefix="im"):
1417
1469
 
1418
1470
  return _Image._from_args(
1419
1471
  dockerfile_function=build_dockerfile,
1420
- context_mount=context_mount,
1472
+ context_mount_function=context_mount_function,
1421
1473
  force_build=force_build,
1422
1474
  **kwargs,
1423
1475
  )
@@ -1531,6 +1583,7 @@ class _Image(_Object, type_prefix="im"):
1531
1583
  secrets: Sequence[_Secret] = [],
1532
1584
  gpu: GPU_T = None,
1533
1585
  add_python: Optional[str] = None,
1586
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1534
1587
  ) -> "_Image":
1535
1588
  """Build a Modal image from a local Dockerfile.
1536
1589
 
@@ -1542,22 +1595,23 @@ class _Image(_Object, type_prefix="im"):
1542
1595
  ```python
1543
1596
  image = modal.Image.from_dockerfile("./Dockerfile", add_python="3.12")
1544
1597
  ```
1598
+ """
1545
1599
 
1546
- If your Dockerfile uses `COPY` instructions which copy data from the local context of the
1547
- build into the image, this local data must be uploaded to Modal via a context mount:
1600
+ if context_mount:
1601
+ if ignore:
1602
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
1548
1603
 
1549
- ```python
1550
- image = modal.Image.from_dockerfile(
1551
- "./Dockerfile",
1552
- context_mount=modal.Mount.from_local_dir(
1553
- local_path="src",
1554
- remote_path=".", # to current WORKDIR
1555
- ),
1556
- )
1557
- ```
1604
+ def identity_context_mount_fn() -> Optional[_Mount]:
1605
+ return context_mount
1558
1606
 
1559
- The context mount will allow a `COPY src/ src/` instruction to succeed in Modal's remote builder.
1560
- """
1607
+ context_mount_function = identity_context_mount_fn
1608
+ else:
1609
+
1610
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
1611
+ lines = Path(path).read_text("utf8").splitlines()
1612
+ return _create_context_mount(lines, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1613
+
1614
+ context_mount_function = auto_created_context_mount_fn
1561
1615
 
1562
1616
  # --- Build the base dockerfile
1563
1617
 
@@ -1569,7 +1623,7 @@ class _Image(_Object, type_prefix="im"):
1569
1623
  gpu_config = parse_gpu_config(gpu)
1570
1624
  base_image = _Image._from_args(
1571
1625
  dockerfile_function=build_dockerfile_base,
1572
- context_mount=context_mount,
1626
+ context_mount_function=context_mount_function,
1573
1627
  gpu_config=gpu_config,
1574
1628
  secrets=secrets,
1575
1629
  )
@@ -1578,13 +1632,15 @@ class _Image(_Object, type_prefix="im"):
1578
1632
  # This happening in two steps is probably a vestigial consequence of previous limitations,
1579
1633
  # but it will be difficult to merge them without forcing rebuilds of images.
1580
1634
 
1581
- if add_python:
1582
- context_mount = _Mount.from_name(
1583
- python_standalone_mount_name(add_python),
1584
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1635
+ def add_python_mount():
1636
+ return (
1637
+ _Mount.from_name(
1638
+ python_standalone_mount_name(add_python),
1639
+ namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
1640
+ )
1641
+ if add_python
1642
+ else None
1585
1643
  )
1586
- else:
1587
- context_mount = None
1588
1644
 
1589
1645
  def build_dockerfile_python(version: ImageBuilderVersion) -> DockerfileSpec:
1590
1646
  commands = _Image._registry_setup_commands("base", version, [], add_python)
@@ -1595,7 +1651,7 @@ class _Image(_Object, type_prefix="im"):
1595
1651
  return _Image._from_args(
1596
1652
  base_images={"base": base_image},
1597
1653
  dockerfile_function=build_dockerfile_python,
1598
- context_mount=context_mount,
1654
+ context_mount_function=add_python_mount,
1599
1655
  force_build=force_build,
1600
1656
  )
1601
1657
 
modal/image.pyi CHANGED
@@ -44,6 +44,11 @@ def _make_pip_install_args(
44
44
  def _get_image_builder_version(
45
45
  server_version: typing.Literal["2023.12", "2024.04", "2024.10"],
46
46
  ) -> typing.Literal["2023.12", "2024.04", "2024.10"]: ...
47
+ def _create_context_mount(
48
+ docker_commands: collections.abc.Sequence[str],
49
+ ignore_fn: typing.Callable[[pathlib.Path], bool],
50
+ context_dir: pathlib.Path,
51
+ ) -> typing.Optional[modal.mount._Mount]: ...
47
52
 
48
53
  class _ImageRegistryConfig:
49
54
  def __init__(self, registry_auth_type: int = 0, secret: typing.Optional[modal.secret._Secret] = None): ...
@@ -87,7 +92,7 @@ class _Image(modal.object._Object):
87
92
  build_function: typing.Optional[modal.functions._Function] = None,
88
93
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
89
94
  image_registry_config: typing.Optional[_ImageRegistryConfig] = None,
90
- context_mount: typing.Optional[modal.mount._Mount] = None,
95
+ context_mount_function: typing.Optional[typing.Callable[[], typing.Optional[modal.mount._Mount]]] = None,
91
96
  force_build: bool = False,
92
97
  _namespace: int = 1,
93
98
  _do_assert_no_mount_layers: bool = True,
@@ -195,6 +200,7 @@ class _Image(modal.object._Object):
195
200
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
196
201
  context_mount: typing.Optional[modal.mount._Mount] = None,
197
202
  force_build: bool = False,
203
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
198
204
  ) -> _Image: ...
199
205
  def entrypoint(self, entrypoint_commands: list[str]) -> _Image: ...
200
206
  def shell(self, shell_commands: list[str]) -> _Image: ...
@@ -280,6 +286,7 @@ class _Image(modal.object._Object):
280
286
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
281
287
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
282
288
  add_python: typing.Optional[str] = None,
289
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
283
290
  ) -> _Image: ...
284
291
  @staticmethod
285
292
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image: ...
@@ -346,7 +353,7 @@ class Image(modal.object.Object):
346
353
  build_function: typing.Optional[modal.functions.Function] = None,
347
354
  build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
348
355
  image_registry_config: typing.Optional[_ImageRegistryConfig] = None,
349
- context_mount: typing.Optional[modal.mount.Mount] = None,
356
+ context_mount_function: typing.Optional[typing.Callable[[], typing.Optional[modal.mount.Mount]]] = None,
350
357
  force_build: bool = False,
351
358
  _namespace: int = 1,
352
359
  _do_assert_no_mount_layers: bool = True,
@@ -454,6 +461,7 @@ class Image(modal.object.Object):
454
461
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
455
462
  context_mount: typing.Optional[modal.mount.Mount] = None,
456
463
  force_build: bool = False,
464
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
457
465
  ) -> Image: ...
458
466
  def entrypoint(self, entrypoint_commands: list[str]) -> Image: ...
459
467
  def shell(self, shell_commands: list[str]) -> Image: ...
@@ -539,6 +547,7 @@ class Image(modal.object.Object):
539
547
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
540
548
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
541
549
  add_python: typing.Optional[str] = None,
550
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
542
551
  ) -> Image: ...
543
552
  @staticmethod
544
553
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image: ...
modal/mount.py CHANGED
@@ -16,6 +16,7 @@ from typing import Callable, Optional, Sequence, Union
16
16
  from google.protobuf.message import Message
17
17
 
18
18
  import modal.exception
19
+ import modal.file_pattern_matcher
19
20
  from modal_proto import api_pb2
20
21
  from modal_version import __version__
21
22
 
@@ -325,12 +326,9 @@ class _Mount(_Object, type_prefix="mo"):
325
326
  @staticmethod
326
327
  def _add_local_dir(
327
328
  local_path: Path,
328
- remote_path: Path,
329
- ignore: Union[Sequence[str], Callable[[Path], bool]] = [],
329
+ remote_path: PurePosixPath,
330
+ ignore: Callable[[Path], bool] = modal.file_pattern_matcher._NOTHING,
330
331
  ):
331
- if isinstance(ignore, list):
332
- ignore = FilePatternMatcher(*ignore)
333
-
334
332
  return _Mount._new()._extend(
335
333
  _MountDir(
336
334
  local_dir=local_path,
modal/mount.pyi CHANGED
@@ -94,8 +94,8 @@ class _Mount(modal.object._Object):
94
94
  @staticmethod
95
95
  def _add_local_dir(
96
96
  local_path: pathlib.Path,
97
- remote_path: pathlib.Path,
98
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
97
+ remote_path: pathlib.PurePosixPath,
98
+ ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
99
99
  ): ...
100
100
  def add_local_dir(
101
101
  self,
@@ -176,8 +176,8 @@ class Mount(modal.object.Object):
176
176
  @staticmethod
177
177
  def _add_local_dir(
178
178
  local_path: pathlib.Path,
179
- remote_path: pathlib.Path,
180
- ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool]] = [],
179
+ remote_path: pathlib.PurePosixPath,
180
+ ignore: typing.Callable[[pathlib.Path], bool] = modal.file_pattern_matcher._NOTHING,
181
181
  ): ...
182
182
  def add_local_dir(
183
183
  self,
modal/partial_function.py CHANGED
@@ -253,7 +253,7 @@ def _web_endpoint(
253
253
  custom_domains: Optional[
254
254
  Iterable[str]
255
255
  ] = None, # Create an endpoint using a custom domain fully-qualified domain name (FQDN).
256
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
256
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
257
257
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
258
258
  ) -> Callable[[Callable[P, ReturnType]], _PartialFunction[P, ReturnType, ReturnType]]:
259
259
  """Register a basic web endpoint with this application.
@@ -316,7 +316,7 @@ def _asgi_app(
316
316
  *,
317
317
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
318
318
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
319
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
319
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
320
320
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
321
321
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
322
322
  """Decorator for registering an ASGI app with a Modal function.
@@ -392,7 +392,7 @@ def _wsgi_app(
392
392
  *,
393
393
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
394
394
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
395
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
395
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
396
396
  wait_for_response: bool = True, # DEPRECATED: this must always be True now
397
397
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
398
398
  """Decorator for registering a WSGI app with a Modal function.
@@ -469,7 +469,7 @@ def _web_server(
469
469
  startup_timeout: float = 5.0, # Maximum number of seconds to wait for the web server to start.
470
470
  label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
471
471
  custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
472
- requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests to the endpoint
472
+ requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
473
473
  ) -> Callable[[Callable[..., Any]], _PartialFunction]:
474
474
  """Decorator that registers an HTTP web server inside the container.
475
475
 
modal/runner.py CHANGED
@@ -30,7 +30,7 @@ from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliU
30
30
  from .functions import _Function
31
31
  from .object import _get_environment_name, _Object
32
32
  from .output import _get_output_manager, enable_output
33
- from .running_app import RunningApp
33
+ from .running_app import RunningApp, running_app_from_layout
34
34
  from .sandbox import _Sandbox
35
35
  from .secret import _Secret
36
36
  from .stream_type import StreamType
@@ -54,15 +54,19 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
54
54
 
55
55
  async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
56
56
  # Get all the objects first
57
- obj_req = api_pb2.AppGetObjectsRequest(app_id=existing_app_id, only_class_function=True)
57
+ obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
58
58
  obj_resp, _ = await gather_cancel_on_exc(
59
- retry_transient_errors(client.stub.AppGetObjects, obj_req),
59
+ retry_transient_errors(client.stub.AppGetLayout, obj_req),
60
60
  # Cache the environment associated with the app now as we will use it later
61
61
  _get_environment_cached(environment_name, client),
62
62
  )
63
63
  app_page_url = f"https://modal.com/apps/{existing_app_id}" # TODO (elias): this should come from the backend
64
- object_ids = {item.tag: item.object.object_id for item in obj_resp.items}
65
- return RunningApp(existing_app_id, app_page_url=app_page_url, tag_to_object_id=object_ids, client=client)
64
+ return running_app_from_layout(
65
+ existing_app_id,
66
+ obj_resp.app_layout,
67
+ client,
68
+ app_page_url=app_page_url,
69
+ )
66
70
 
67
71
 
68
72
  async def _init_local_app_new(
modal/running_app.py CHANGED
@@ -4,16 +4,42 @@ from typing import Optional
4
4
 
5
5
  from google.protobuf.message import Message
6
6
 
7
+ from modal._utils.grpc_utils import get_proto_oneof
8
+ from modal_proto import api_pb2
9
+
7
10
  from .client import _Client
8
11
 
9
12
 
10
13
  @dataclass
11
14
  class RunningApp:
12
15
  app_id: str
16
+ client: _Client
13
17
  environment_name: Optional[str] = None
14
18
  app_page_url: Optional[str] = None
15
19
  app_logs_url: Optional[str] = None
16
20
  tag_to_object_id: dict[str, str] = field(default_factory=dict)
17
21
  object_handle_metadata: dict[str, Optional[Message]] = field(default_factory=dict)
18
22
  interactive: bool = False
19
- client: Optional[_Client] = None
23
+
24
+
25
+ def running_app_from_layout(
26
+ app_id: str,
27
+ app_layout: api_pb2.AppLayout,
28
+ client: _Client,
29
+ environment_name: Optional[str] = None,
30
+ app_page_url: Optional[str] = None,
31
+ ) -> RunningApp:
32
+ tag_to_object_id = dict(**app_layout.function_ids, **app_layout.class_ids)
33
+ object_handle_metadata = {}
34
+ for obj in app_layout.objects:
35
+ handle_metadata: Optional[Message] = get_proto_oneof(obj, "handle_metadata_oneof")
36
+ object_handle_metadata[obj.object_id] = handle_metadata
37
+
38
+ return RunningApp(
39
+ app_id,
40
+ client,
41
+ environment_name=environment_name,
42
+ tag_to_object_id=tag_to_object_id,
43
+ object_handle_metadata=object_handle_metadata,
44
+ app_page_url=app_page_url,
45
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.51
3
+ Version: 0.70.1
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -2,7 +2,7 @@ modal/__init__.py,sha256=3NJLLHb0TRc2tc68kf8NHzORx38GbtbZvPEWDWrQ6N4,2234
2
2
  modal/__main__.py,sha256=scYhGFqh8OJcVDo-VOxIT6CCwxOgzgflYWMnIZiMRqE,2871
3
3
  modal/_clustered_functions.py,sha256=kTf-9YBXY88NutC1akI-gCbvf01RhMPCw-zoOI_YIUE,2700
4
4
  modal/_clustered_functions.pyi,sha256=vllkegc99A0jrUOWa8mdlSbdp6uz36TsHhGxysAOpaQ,771
5
- modal/_container_entrypoint.py,sha256=wk10vA5vRZZsVwQ0yINOLd0i-NwH7x6XbhTslumvGjo,28910
5
+ modal/_container_entrypoint.py,sha256=7EAMe-85a-uWDaRAjRy9m4nnCAmjRJnh9x7FgW_QjLU,28935
6
6
  modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
7
7
  modal/_location.py,sha256=S3lSxIU3h9HkWpkJ3Pwo0pqjIOSB1fjeSgUsY3x7eec,1202
8
8
  modal/_output.py,sha256=0fWX_KQwhER--U81ys16CL-pA5A-LN20C0EZjElKGJQ,25410
@@ -19,7 +19,7 @@ modal/app.py,sha256=JWefPs4yB70BKQwSZejB_4_muhxn63cC9UmnNvpQ9XY,45526
19
19
  modal/app.pyi,sha256=FYPCEJNhof4YF6HIuNP_2yG6s2PgZnKW9tO1hFE6sfA,25194
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=9v4KsQG_wdVZQP4Eyhhr9CO0T5snxcLcgDCI9ZNQq9g,7280
22
+ modal/client.pyi,sha256=_USFlgo8YNSNDQ-4lgJrafvJME1RR1aLK-iNsW_gvGk,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,18 +33,18 @@ 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=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
- modal/file_io.py,sha256=pDOFNQU5m-x-k3oJauck4fOp3bZ55Vc-_LvSaN5_Bow,16465
36
+ modal/file_io.py,sha256=QXACorWL9NAfvRcnBcP34e5H7u9xf7g5zt2BHj9quC8,16713
37
37
  modal/file_io.pyi,sha256=GMhCCRyMftXYI3HqI9EdGPOx70CbCNi-VC5Sfy5TYnc,7631
38
- modal/file_pattern_matcher.py,sha256=V6P74Vc7LAuBFe_uepIaZmoDJiuAvqjFibe0GcMJwxo,5119
38
+ modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms,5571
39
39
  modal/functions.py,sha256=aXXXr3rk7BCeh5OWMvxGksGm8FQoYCyrBDGV74FPoPE,67827
40
- modal/functions.pyi,sha256=snttn47K81lKhmrCLWNVZelZTDhNsbxtw4l1DlLDR74,25317
40
+ modal/functions.pyi,sha256=oMmcExtQxHwPej06jQ3uBe1tUlSR3VbAx7u3Vm-Ohhg,25317
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
- modal/image.py,sha256=sv45bYaF5Jlmk8mQE3EDADYyXLi14hOe2CUM0Zb8Xao,82243
43
- modal/image.pyi,sha256=VY_4HnDBhW8u_Zd3n-YBZ1H9idbTorWGwzsAzY7-B70,24213
42
+ modal/image.py,sha256=Krvcsclomp9YsqSNwFj2FoAyg10OvU47RDnsNCwjGbQ,84550
43
+ modal/image.pyi,sha256=1fgGvsL5Rb0Sa-_2OCgIyJ_QgHcL0_9MWD_oY7cyFFM,24937
44
44
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
- modal/mount.py,sha256=Miu9V5LB80uoMasSXxxf0aYTC7H1G08PjnjmmjQdyRc,29346
47
- modal/mount.pyi,sha256=7dKl_JeVka3g4oKw7D-FFRU-Zpadt9LJEcfNUnhj540,10491
46
+ modal/mount.py,sha256=wOr-2vmKImsE3lHBII8hL2gYy5ng46R58QwId4JultQ,29313
47
+ modal/mount.pyi,sha256=FiNV1wIKFvd0ZMZ0tm1mz6ZSA5Hjsge-kFSA5tPWfcI,10503
48
48
  modal/network_file_system.py,sha256=INj1TfN_Fsmabmlte7anvey1epodjbMmjBW_TIJSST4,14406
49
49
  modal/network_file_system.pyi,sha256=61M-sdWrtaRjmuNVsvusI6kf1Qw-jUOVXvEAeOkM8Aw,7751
50
50
  modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
@@ -52,7 +52,7 @@ modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
52
52
  modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
53
53
  modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
54
54
  modal/parallel_map.pyi,sha256=pOhT0P3DDYlwLx0fR3PTsecA7DI8uOdXC1N8i-ZkyOY,2328
55
- modal/partial_function.py,sha256=pDDNR6KTaIIPpuKQaoO1vgP83_LTwxMhtOn6sVRrmC8,27862
55
+ modal/partial_function.py,sha256=KSpHhu7Gsbe4h-5mcJjxnE4328biKOB7yVouHZbObA8,27798
56
56
  modal/partial_function.pyi,sha256=pO6kf8i5HVsZ7CF0z_KkzLk4Aeq7NJhFJ_VNIycRXaU,9260
57
57
  modal/proxy.py,sha256=ZrOsuQP7dSZFq1OrIxalNnt0Zvsnp1h86Th679sSL40,1417
58
58
  modal/proxy.pyi,sha256=UvygdOYneLTuoDY6hVaMNCyZ947Tmx93IdLjErUqkvM,368
@@ -60,9 +60,9 @@ modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  modal/queue.py,sha256=zMUQtdAyqZzBg-2iAo3c3G54HLP7TEWfVhiQXLjewb4,18556
61
61
  modal/queue.pyi,sha256=gGV97pWelSSYqMV9Bl4ys3mSP7q82fS71oqSWeAwyDE,9818
62
62
  modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
63
- modal/runner.py,sha256=qfkB0OM97kb_-oP-D5KPj_jUwfd8ePUA3R_zLkjSTBQ,24586
63
+ modal/runner.py,sha256=1nPBsIfef2sOr2ebQ348EmDemvYFDhp1-_Gr3BKsjdM,24542
64
64
  modal/runner.pyi,sha256=BvMS1ZVzWSn8B8q0KnIZOJKPkN5L-i5b-USbV6SWWHQ,5177
65
- modal/running_app.py,sha256=CshNvGDJtagOdKW54uYjY8HY73j2TpnsL9jkPFZAsfA,560
65
+ modal/running_app.py,sha256=FSr0XoL4mPLPCBMj2TozWuEvcvApdY_t68nUowwf8x4,1372
66
66
  modal/sandbox.py,sha256=c-Qli3QJPN7bBQzsTk4iS51zurNlq--InZ2eRR-B6No,28106
67
67
  modal/sandbox.pyi,sha256=k8_vHjN3oigxSCF13Cm2HfcSHuliGuSb8ryd3CGqwoA,19815
68
68
  modal/schedule.py,sha256=0ZFpKs1bOxeo5n3HZjoL7OE2ktsb-_oGtq-WJEPO4tY,2615
@@ -78,7 +78,7 @@ modal/volume.py,sha256=T-pLxCYqmqRO6OolpAXlPxomMu0RWjti2e4kUpaj2cQ,29229
78
78
  modal/volume.pyi,sha256=eekb2dnAAwFK_NO9ciAOOTthl8NP1iAmMFrCGgjDA2k,11100
79
79
  modal/_runtime/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
80
80
  modal/_runtime/asgi.py,sha256=Mjs859pSgOmtZL-YmEsSKN557v1A2Ax_5-ERgPfj55E,21920
81
- modal/_runtime/container_io_manager.py,sha256=ctgyNFiHjq1brCrabXmlurkAXjnrCeWPRvTVa735vRw,44215
81
+ modal/_runtime/container_io_manager.py,sha256=8k84QF8dNPY344tm0jz6DySCnIwIlUH66KKKh95FjWY,43878
82
82
  modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
83
83
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
84
84
  modal/_runtime/user_code_imports.py,sha256=n4CQOojzSdf0jwSKSy6MEnVX3IWl3t3Dq54-x9VS2Ds,14663
@@ -88,6 +88,7 @@ modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,2
88
88
  modal/_utils/blob_utils.py,sha256=N66LtZI8PpCkZ7maA7GLW5CAmYUoNJdG-GjaAUR4_NQ,14509
89
89
  modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
90
90
  modal/_utils/deprecation.py,sha256=dycySRBxyZf3ITzEqPNM6MxXTk9-0VVLA8oCPQ5j_Os,3426
91
+ modal/_utils/docker_utils.py,sha256=rft2WVGhEgYik2zFRZbX63X57b2jnLyy88byykDH2Xo,1963
91
92
  modal/_utils/function_utils.py,sha256=4LYFbNY5aHc96QitwP4Ty-dBl45SD1HjfZrvBFUF-ko,25343
92
93
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
93
94
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
@@ -114,7 +115,7 @@ modal/cli/dict.py,sha256=HaEcjfll7i3Uj3Fg56aj4407if5UljsYfr6fIq-D2W8,4589
114
115
  modal/cli/entry_point.py,sha256=aaNxFAqZcmtSjwzkYIA_Ba9CkL4cL4_i2gy5VjoXxkM,4228
115
116
  modal/cli/environment.py,sha256=Ayddkiq9jdj3XYDJ8ZmUqFpPPH8xajYlbexRkzGtUcg,4334
116
117
  modal/cli/import_refs.py,sha256=wnqE5AMeyAN3IZmQvJCp54KRnJh8Nq_5fMqB6u6GEL8,9147
117
- modal/cli/launch.py,sha256=uyI-ouGvYRjHLGxGQ2lYBZq32BiRT1i0L8ksz5iy7K8,2935
118
+ modal/cli/launch.py,sha256=44oOlGB0KYDBMfuIlkhW2uzjkWbHhDRR64UOEnGwsJ4,2989
118
119
  modal/cli/network_file_system.py,sha256=o6VLTgN4xn5XUiNPBfxYec-5uWCgYrDmfFFLM1ZW_eE,8180
119
120
  modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
120
121
  modal/cli/queues.py,sha256=MIh2OsliNE2QeL1erubfsRsNuG4fxqcqWA2vgIfQ4Mg,4494
@@ -125,7 +126,7 @@ modal/cli/utils.py,sha256=hZmjyzcPjDnQSkLvycZD2LhGdcsfdZshs_rOU78EpvI,3717
125
126
  modal/cli/volume.py,sha256=Oxc8WGP8wm2a_S87bp-P4OnPwoT1wIYQhbgkCvvFIdI,9998
126
127
  modal/cli/programs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
127
128
  modal/cli/programs/run_jupyter.py,sha256=RRr07CqZrStMbLdBM3PpzU6KM8t9FtLbdIPthg2-Mpw,2755
128
- modal/cli/programs/vscode.py,sha256=acKvTUNA2uIGITKWYS9mkb_W8WUuNmvOSOvf1KBtUco,2479
129
+ modal/cli/programs/vscode.py,sha256=m80wuQyTALTc7y-kAVqmMjtrcb6muCtpuhxsJm4Va2Y,3453
129
130
  modal/extensions/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
130
131
  modal/extensions/ipython.py,sha256=Xvzy-A7cvwMSDa9p4c4CEMLOX2_Xsg9DkM1J9uyu7jc,983
131
132
  modal/requirements/2023.12.312.txt,sha256=zWWUVgVQ92GXBKNYYr2-5vn9rlnXcmkqlwlX5u1eTYw,400
@@ -162,12 +163,12 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
162
163
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
163
164
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
164
165
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
165
- modal_version/__init__.py,sha256=RT6zPoOdFO99u5Wcxxaoir4ZCuPTbQ22cvzFAXl3vUY,470
166
+ modal_version/__init__.py,sha256=N9Kh4DrM2649_INTJG4Lp3NKdux7cxGuiDtXpq_hkFY,470
166
167
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
167
- modal_version/_version_generated.py,sha256=_MXrw7oLbjNkVo7PEwZ4eqLvDc5hjGnVakvt0sUYAqs,149
168
- modal-0.68.51.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
169
- modal-0.68.51.dist-info/METADATA,sha256=sOaSpFtEj0fKHVaoIWuzRStpQYil7KWQzZP26N7je6E,2329
170
- modal-0.68.51.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
171
- modal-0.68.51.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
172
- modal-0.68.51.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
173
- modal-0.68.51.dist-info/RECORD,,
168
+ modal_version/_version_generated.py,sha256=ryiYirApbEdrIBIic3kQU9ffkciEs4sM79E_mwfmbaA,148
169
+ modal-0.70.1.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
+ modal-0.70.1.dist-info/METADATA,sha256=oWSDkTZ74lyZLLIR5lolTo9A0A_TvjPukqd5aHLQZFk,2328
171
+ modal-0.70.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
+ modal-0.70.1.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
+ modal-0.70.1.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
+ modal-0.70.1.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -7,7 +7,7 @@ from ._version_generated import build_number
7
7
  major_number = 0
8
8
 
9
9
  # Bump this manually on breaking changes, then reset the number in _version_generated.py
10
- minor_number = 68
10
+ minor_number = 70
11
11
 
12
12
  # Right now, automatically increment the patch number in CI
13
13
  __version__ = f"{major_number}.{minor_number}.{max(build_number, 0)}"
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 51 # git: f957415
4
+ build_number = 1 # git: 060c28e