modal 0.68.55__py3-none-any.whl → 0.69.0__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.
@@ -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/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.55"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.69.0"
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.55"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.69.0"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
@@ -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[ReturnType_INNER, P_INNER]):
465
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_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[ReturnType, P]
469
+ remote: __remote_spec[P, ReturnType]
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[ReturnType_INNER, P_INNER]):
482
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_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[ReturnType, P]
486
+ _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
487
487
 
488
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
488
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_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[ReturnType, P]
492
+ spawn: __spawn_spec[P, ReturnType]
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.55
3
+ Version: 0.69.0
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=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=vm42D4zsYXDY9gOa1rQo7XqdqI1tvCkFF2l7SMXP1xU,7280
22
+ modal/client.pyi,sha256=ENRyoE3k_U_QuPDY05eVI2GtuzNXWIk-vXgehbI4WjE,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
@@ -35,16 +35,16 @@ modal/exception.py,sha256=GEV6xMnVnkle0gsFZVLB4B7cUMyw8HzVDvAvPr34ZV4,5185
35
35
  modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
36
  modal/file_io.py,sha256=pDOFNQU5m-x-k3oJauck4fOp3bZ55Vc-_LvSaN5_Bow,16465
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=oMmcExtQxHwPej06jQ3uBe1tUlSR3VbAx7u3Vm-Ohhg,25317
40
+ modal/functions.pyi,sha256=snttn47K81lKhmrCLWNVZelZTDhNsbxtw4l1DlLDR74,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
@@ -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
@@ -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=aBl2c-5ZPst-0n_E9DeJEfYYSifFGrDOwo9tpDwhyK8,470
166
167
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
167
- modal_version/_version_generated.py,sha256=7PSDevj3mj7LXnOBilNi_mB_5O3184HSxTHZsjyvBDc,149
168
- modal-0.68.55.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
169
- modal-0.68.55.dist-info/METADATA,sha256=fcu55_VjRcoK-DO_lyRq5FVajpeyT-YOeWdjHLHIUpA,2329
170
- modal-0.68.55.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
171
- modal-0.68.55.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
172
- modal-0.68.55.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
173
- modal-0.68.55.dist-info/RECORD,,
168
+ modal_version/_version_generated.py,sha256=ej60GT5kCy2xu-Nl85c2H_C-xPKpmO4Qq9WS8px9udc,148
169
+ modal-0.69.0.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
+ modal-0.69.0.dist-info/METADATA,sha256=wbnWG0YWB7VwZLgG4dlmiSCrCNuDpJCu27lz6qrU9X4,2328
171
+ modal-0.69.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
+ modal-0.69.0.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
+ modal-0.69.0.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
+ modal-0.69.0.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 = 69
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 = 55 # git: dd65611
4
+ build_number = 0 # git: 8b60408