modal 0.71.12__py3-none-any.whl → 0.72.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.
@@ -1,7 +1,8 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import re
3
3
  import shlex
4
- from typing import Sequence
4
+ from pathlib import Path
5
+ from typing import Optional, Sequence
5
6
 
6
7
  from ..exception import InvalidError
7
8
 
@@ -62,3 +63,36 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
62
63
  current_command = ""
63
64
 
64
65
  return list(copy_source_patterns)
66
+
67
+
68
+ def find_dockerignore_file(context_directory: Path, dockerfile_path: Optional[Path] = None) -> Optional[Path]:
69
+ """
70
+ Find dockerignore file relative to current context directory
71
+ and if dockerfile path is provided, check if specific <dockerfile_name>.dockerignore
72
+ file exists in the same directory as <dockerfile_name>
73
+ Finds the most specific dockerignore file that exists.
74
+ """
75
+
76
+ def valid_dockerignore_file(fp):
77
+ # fp has to exist
78
+ if not fp.exists():
79
+ return False
80
+ # fp has to be subpath to current working directory
81
+ if not fp.is_relative_to(context_directory):
82
+ return False
83
+
84
+ return True
85
+
86
+ generic_name = ".dockerignore"
87
+ possible_locations = []
88
+ if dockerfile_path:
89
+ specific_name = f"{dockerfile_path.name}.dockerignore"
90
+ # 1. check if specific <dockerfile_name>.dockerignore file exists in the same directory as <dockerfile_name>
91
+ possible_locations.append(dockerfile_path.parent / specific_name)
92
+ # 2. check if generic .dockerignore file exists in the same directory as <dockerfile_name>
93
+ possible_locations.append(dockerfile_path.parent / generic_name)
94
+
95
+ # 3. check if generic .dockerignore file exists in current working directory
96
+ possible_locations.append(context_directory / generic_name)
97
+
98
+ return next((e for e in possible_locations if valid_dockerignore_file(e)), None)
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.12"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.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.71.12"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.0"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
@@ -62,6 +62,27 @@ class _CustomPatternMatcher(_AbstractPatternMatcher):
62
62
  class FilePatternMatcher(_AbstractPatternMatcher):
63
63
  """Allows matching file paths against a list of patterns."""
64
64
 
65
+ patterns: list[Pattern]
66
+ _delayed_init: Callable[[], None] = None
67
+
68
+ def _set_patterns(self, patterns: Sequence[str]) -> None:
69
+ self.patterns = []
70
+ for pattern in list(patterns):
71
+ pattern = pattern.strip()
72
+ if not pattern:
73
+ continue
74
+ pattern = os.path.normpath(pattern)
75
+ new_pattern = Pattern()
76
+ if pattern[0] == "!":
77
+ if len(pattern) == 1:
78
+ raise ValueError('Illegal exclusion pattern: "!"')
79
+ new_pattern.exclusion = True
80
+ pattern = pattern[1:]
81
+ # In Python, we can proceed without explicit syntax checking
82
+ new_pattern.cleaned_pattern = pattern
83
+ new_pattern.dirs = pattern.split(os.path.sep)
84
+ self.patterns.append(new_pattern)
85
+
65
86
  def __init__(self, *pattern: str) -> None:
66
87
  """Initialize a new FilePatternMatcher instance.
67
88
 
@@ -71,24 +92,25 @@ class FilePatternMatcher(_AbstractPatternMatcher):
71
92
  Raises:
72
93
  ValueError: If an illegal exclusion pattern is provided.
73
94
  """
74
- self.patterns: list[Pattern] = []
75
- self.exclusions = False
76
- for p in list(pattern):
77
- p = p.strip()
78
- if not p:
79
- continue
80
- p = os.path.normpath(p)
81
- new_pattern = Pattern()
82
- if p[0] == "!":
83
- if len(p) == 1:
84
- raise ValueError('Illegal exclusion pattern: "!"')
85
- new_pattern.exclusion = True
86
- p = p[1:]
87
- self.exclusions = True
88
- # In Python, we can proceed without explicit syntax checking
89
- new_pattern.cleaned_pattern = p
90
- new_pattern.dirs = p.split(os.path.sep)
91
- self.patterns.append(new_pattern)
95
+ self._set_patterns(pattern)
96
+
97
+ @classmethod
98
+ def from_file(cls, file_path: Path) -> "FilePatternMatcher":
99
+ """Initialize a new FilePatternMatcher instance from a file.
100
+
101
+ The patterns in the file will be read lazily when the matcher is first used.
102
+
103
+ Args:
104
+ file_path (Path): The path to the file containing patterns.
105
+ """
106
+ uninitialized = cls.__new__(cls)
107
+
108
+ def _delayed_init():
109
+ uninitialized._set_patterns(file_path.read_text("utf8").splitlines())
110
+ uninitialized._delayed_init = None
111
+
112
+ uninitialized._delayed_init = _delayed_init
113
+ return uninitialized
92
114
 
93
115
  def _matches(self, file_path: str) -> bool:
94
116
  """Check if the file path or any of its parent directories match the patterns.
@@ -97,6 +119,7 @@ class FilePatternMatcher(_AbstractPatternMatcher):
97
119
  library. The reason is that `Matches()` in the original library is
98
120
  deprecated due to buggy behavior.
99
121
  """
122
+
100
123
  matched = False
101
124
  file_path = os.path.normpath(file_path)
102
125
  if file_path == ".":
@@ -146,6 +169,8 @@ class FilePatternMatcher(_AbstractPatternMatcher):
146
169
  assert matcher(Path("foo.py"))
147
170
  ```
148
171
  """
172
+ if self._delayed_init:
173
+ self._delayed_init()
149
174
  return self._matches(str(file_path))
150
175
 
151
176
 
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
@@ -33,6 +33,7 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
33
33
  from ._utils.deprecation import deprecation_error, deprecation_warning
34
34
  from ._utils.docker_utils import (
35
35
  extract_copy_command_patterns,
36
+ find_dockerignore_file,
36
37
  )
37
38
  from ._utils.function_utils import FunctionInfo
38
39
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
@@ -71,6 +72,17 @@ LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "requirements"
71
72
  CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
72
73
 
73
74
 
75
+ class _AutoDockerIgnoreSentinel:
76
+ def __repr__(self) -> str:
77
+ return f"{__name__}.AUTO_DOCKERIGNORE"
78
+
79
+ def __call__(self, _: Path) -> bool:
80
+ raise NotImplementedError("This is only a placeholder. Do not call")
81
+
82
+
83
+ AUTO_DOCKERIGNORE = _AutoDockerIgnoreSentinel()
84
+
85
+
74
86
  def _validate_python_version(
75
87
  python_version: Optional[str], builder_version: ImageBuilderVersion, allow_micro_granularity: bool = True
76
88
  ) -> str:
@@ -266,6 +278,47 @@ def _create_context_mount(
266
278
  return _Mount._add_local_dir(Path("./"), PurePosixPath("/"), ignore=ignore_with_include)
267
279
 
268
280
 
281
+ def _create_context_mount_function(
282
+ ignore: Union[Sequence[str], Callable[[Path], bool]],
283
+ dockerfile_cmds: list[str] = [],
284
+ dockerfile_path: Optional[Path] = None,
285
+ context_mount: Optional[_Mount] = None,
286
+ ):
287
+ if dockerfile_path and dockerfile_cmds:
288
+ raise InvalidError("Cannot provide both dockerfile and docker commands")
289
+
290
+ if context_mount:
291
+ if ignore is not AUTO_DOCKERIGNORE:
292
+ raise InvalidError("Cannot set both `context_mount` and `ignore`")
293
+
294
+ def identity_context_mount_fn() -> Optional[_Mount]:
295
+ return context_mount
296
+
297
+ return identity_context_mount_fn
298
+ elif ignore is AUTO_DOCKERIGNORE:
299
+
300
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
301
+ context_dir = Path.cwd()
302
+ dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
303
+ ignore_fn = (
304
+ FilePatternMatcher(*dockerignore_file.read_text("utf8").splitlines())
305
+ if dockerignore_file
306
+ else _ignore_fn(())
307
+ )
308
+
309
+ cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
310
+ return _create_context_mount(cmds, ignore_fn=ignore_fn, context_dir=context_dir)
311
+
312
+ return auto_created_context_mount_fn
313
+
314
+ def auto_created_context_mount_fn() -> Optional[_Mount]:
315
+ # use COPY commands and ignore patterns to construct implicit context mount
316
+ cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
317
+ return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
318
+
319
+ return auto_created_context_mount_fn
320
+
321
+
269
322
  class _ImageRegistryConfig:
270
323
  """mdmd:hidden"""
271
324
 
@@ -683,6 +736,7 @@ class _Image(_Object, type_prefix="im"):
683
736
  **Usage:**
684
737
 
685
738
  ```python
739
+ from pathlib import Path
686
740
  from modal import FilePatternMatcher
687
741
 
688
742
  image = modal.Image.debian_slim().add_local_dir(
@@ -697,18 +751,25 @@ class _Image(_Object, type_prefix="im"):
697
751
  ignore=lambda p: p.is_relative_to(".venv"),
698
752
  )
699
753
 
700
- image = modal.Image.debian_slim().copy_local_dir(
754
+ image = modal.Image.debian_slim().add_local_dir(
701
755
  "~/assets",
702
756
  remote_path="/assets",
703
757
  ignore=FilePatternMatcher("**/*.txt"),
704
758
  )
705
759
 
706
760
  # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
707
- image = modal.Image.debian_slim().copy_local_dir(
761
+ image = modal.Image.debian_slim().add_local_dir(
708
762
  "~/assets",
709
763
  remote_path="/assets",
710
764
  ignore=~FilePatternMatcher("**/*.py"),
711
765
  )
766
+
767
+ # You can also read ignore patterns from a file.
768
+ image = modal.Image.debian_slim().add_local_dir(
769
+ "~/assets",
770
+ remote_path="/assets",
771
+ ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
772
+ )
712
773
  ```
713
774
  """
714
775
  if not PurePosixPath(remote_path).is_absolute():
@@ -792,6 +853,7 @@ class _Image(_Object, type_prefix="im"):
792
853
  **Usage:**
793
854
 
794
855
  ```python
856
+ from pathlib import Path
795
857
  from modal import FilePatternMatcher
796
858
 
797
859
  image = modal.Image.debian_slim().copy_local_dir(
@@ -818,6 +880,13 @@ class _Image(_Object, type_prefix="im"):
818
880
  remote_path="/assets",
819
881
  ignore=~FilePatternMatcher("**/*.py"),
820
882
  )
883
+
884
+ # You can also read ignore patterns from a file.
885
+ image = modal.Image.debian_slim().copy_local_dir(
886
+ "~/assets",
887
+ remote_path="/assets",
888
+ ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
889
+ )
821
890
  ```
822
891
  """
823
892
 
@@ -1205,28 +1274,53 @@ class _Image(_Object, type_prefix="im"):
1205
1274
  # modal.Mount with local files to supply as build context for COPY commands
1206
1275
  context_mount: Optional[_Mount] = None,
1207
1276
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
1208
- ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1277
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
1209
1278
  ) -> "_Image":
1210
- """Extend an image with arbitrary Dockerfile-like commands."""
1211
- cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1212
- if not cmds:
1213
- return self
1279
+ """
1280
+ Extend an image with arbitrary Dockerfile-like commands.
1214
1281
 
1215
- if context_mount:
1216
- if ignore:
1217
- raise InvalidError("Cannot set both `context_mount` and `ignore`")
1282
+ **Usage:**
1218
1283
 
1219
- def identity_context_mount_fn() -> Optional[_Mount]:
1220
- return context_mount
1284
+ ```python
1285
+ from pathlib import Path
1286
+ from modal import FilePatternMatcher
1221
1287
 
1222
- context_mount_function = identity_context_mount_fn
1223
- else:
1288
+ # By default a .dockerignore file is used if present in the current working directory
1289
+ image = modal.Image.debian_slim().dockerfile_commands(
1290
+ ["COPY data /data"],
1291
+ )
1224
1292
 
1225
- def auto_created_context_mount_fn() -> Optional[_Mount]:
1226
- # use COPY commands and ignore patterns to construct implicit context mount
1227
- return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1293
+ image = modal.Image.debian_slim().dockerfile_commands(
1294
+ ["COPY data /data"],
1295
+ ignore=["*.venv"],
1296
+ )
1228
1297
 
1229
- context_mount_function = auto_created_context_mount_fn
1298
+ image = modal.Image.debian_slim().dockerfile_commands(
1299
+ ["COPY data /data"],
1300
+ ignore=lambda p: p.is_relative_to(".venv"),
1301
+ )
1302
+
1303
+ image = modal.Image.debian_slim().dockerfile_commands(
1304
+ ["COPY data /data"],
1305
+ ignore=FilePatternMatcher("**/*.txt"),
1306
+ )
1307
+
1308
+ # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
1309
+ image = modal.Image.debian_slim().dockerfile_commands(
1310
+ ["COPY data /data"],
1311
+ ignore=~FilePatternMatcher("**/*.py"),
1312
+ )
1313
+
1314
+ # You can also read ignore patterns from a file.
1315
+ image = modal.Image.debian_slim().dockerfile_commands(
1316
+ ["COPY data /data"],
1317
+ ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
1318
+ )
1319
+ ```
1320
+ """
1321
+ cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
1322
+ if not cmds:
1323
+ return self
1230
1324
 
1231
1325
  def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
1232
1326
  return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)
@@ -1236,7 +1330,9 @@ class _Image(_Object, type_prefix="im"):
1236
1330
  dockerfile_function=build_dockerfile,
1237
1331
  secrets=secrets,
1238
1332
  gpu_config=parse_gpu_config(gpu),
1239
- context_mount_function=context_mount_function,
1333
+ context_mount_function=_create_context_mount_function(
1334
+ ignore=ignore, dockerfile_cmds=cmds, context_mount=context_mount
1335
+ ),
1240
1336
  force_build=self.force_build or force_build,
1241
1337
  )
1242
1338
 
@@ -1601,35 +1697,58 @@ class _Image(_Object, type_prefix="im"):
1601
1697
  secrets: Sequence[_Secret] = [],
1602
1698
  gpu: GPU_T = None,
1603
1699
  add_python: Optional[str] = None,
1604
- ignore: Union[Sequence[str], Callable[[Path], bool]] = (),
1700
+ ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
1605
1701
  ) -> "_Image":
1606
1702
  """Build a Modal image from a local Dockerfile.
1607
1703
 
1608
1704
  If your Dockerfile does not have Python installed, you can use the `add_python` parameter
1609
1705
  to specify a version of Python to add to the image.
1610
1706
 
1611
- **Example**
1707
+ **Usage:**
1612
1708
 
1613
1709
  ```python
1614
- image = modal.Image.from_dockerfile("./Dockerfile", add_python="3.12")
1615
- ```
1616
- """
1710
+ from pathlib import Path
1711
+ from modal import FilePatternMatcher
1617
1712
 
1618
- if context_mount:
1619
- if ignore:
1620
- raise InvalidError("Cannot set both `context_mount` and `ignore`")
1713
+ # By default a .dockerignore file is used if present in the current working directory
1714
+ image = modal.Image.from_dockerfile(
1715
+ "./Dockerfile",
1716
+ add_python="3.12",
1717
+ )
1621
1718
 
1622
- def identity_context_mount_fn() -> Optional[_Mount]:
1623
- return context_mount
1719
+ image = modal.Image.from_dockerfile(
1720
+ "./Dockerfile",
1721
+ add_python="3.12",
1722
+ ignore=["*.venv"],
1723
+ )
1624
1724
 
1625
- context_mount_function = identity_context_mount_fn
1626
- else:
1725
+ image = modal.Image.from_dockerfile(
1726
+ "./Dockerfile",
1727
+ add_python="3.12",
1728
+ ignore=lambda p: p.is_relative_to(".venv"),
1729
+ )
1627
1730
 
1628
- def auto_created_context_mount_fn() -> Optional[_Mount]:
1629
- lines = Path(path).read_text("utf8").splitlines()
1630
- return _create_context_mount(lines, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
1731
+ image = modal.Image.from_dockerfile(
1732
+ "./Dockerfile",
1733
+ add_python="3.12",
1734
+ ignore=FilePatternMatcher("**/*.txt"),
1735
+ )
1631
1736
 
1632
- context_mount_function = auto_created_context_mount_fn
1737
+ # When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
1738
+ image = modal.Image.from_dockerfile(
1739
+ "./Dockerfile",
1740
+ add_python="3.12",
1741
+ ignore=~FilePatternMatcher("**/*.py"),
1742
+ )
1743
+
1744
+ # You can also read ignore patterns from a file.
1745
+ image = modal.Image.from_dockerfile(
1746
+ "./Dockerfile",
1747
+ add_python="3.12",
1748
+ ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
1749
+ )
1750
+ ```
1751
+ """
1633
1752
 
1634
1753
  # --- Build the base dockerfile
1635
1754
 
@@ -1641,7 +1760,9 @@ class _Image(_Object, type_prefix="im"):
1641
1760
  gpu_config = parse_gpu_config(gpu)
1642
1761
  base_image = _Image._from_args(
1643
1762
  dockerfile_function=build_dockerfile_base,
1644
- context_mount_function=context_mount_function,
1763
+ context_mount_function=_create_context_mount_function(
1764
+ ignore=ignore, dockerfile_path=Path(path), context_mount=context_mount
1765
+ ),
1645
1766
  gpu_config=gpu_config,
1646
1767
  secrets=secrets,
1647
1768
  )
modal/image.pyi CHANGED
@@ -16,6 +16,12 @@ import typing_extensions
16
16
 
17
17
  ImageBuilderVersion = typing.Literal["2023.12", "2024.04", "2024.10"]
18
18
 
19
+ class _AutoDockerIgnoreSentinel:
20
+ def __repr__(self) -> str: ...
21
+ def __call__(self, _: pathlib.Path) -> bool: ...
22
+
23
+ AUTO_DOCKERIGNORE: _AutoDockerIgnoreSentinel
24
+
19
25
  def _validate_python_version(
20
26
  python_version: typing.Optional[str],
21
27
  builder_version: typing.Literal["2023.12", "2024.04", "2024.10"],
@@ -49,6 +55,12 @@ def _create_context_mount(
49
55
  ignore_fn: typing.Callable[[pathlib.Path], bool],
50
56
  context_dir: pathlib.Path,
51
57
  ) -> typing.Optional[modal.mount._Mount]: ...
58
+ def _create_context_mount_function(
59
+ ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]],
60
+ dockerfile_cmds: list[str] = [],
61
+ dockerfile_path: typing.Optional[pathlib.Path] = None,
62
+ context_mount: typing.Optional[modal.mount._Mount] = None,
63
+ ): ...
52
64
 
53
65
  class _ImageRegistryConfig:
54
66
  def __init__(self, registry_auth_type: int = 0, secret: typing.Optional[modal.secret._Secret] = None): ...
@@ -202,7 +214,9 @@ class _Image(modal.object._Object):
202
214
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
203
215
  context_mount: typing.Optional[modal.mount._Mount] = None,
204
216
  force_build: bool = False,
205
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
217
+ ignore: typing.Union[
218
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
219
+ ] = modal.image.AUTO_DOCKERIGNORE,
206
220
  ) -> _Image: ...
207
221
  def entrypoint(self, entrypoint_commands: list[str]) -> _Image: ...
208
222
  def shell(self, shell_commands: list[str]) -> _Image: ...
@@ -288,7 +302,9 @@ class _Image(modal.object._Object):
288
302
  secrets: collections.abc.Sequence[modal.secret._Secret] = [],
289
303
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
290
304
  add_python: typing.Optional[str] = None,
291
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
305
+ ignore: typing.Union[
306
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
307
+ ] = modal.image.AUTO_DOCKERIGNORE,
292
308
  ) -> _Image: ...
293
309
  @staticmethod
294
310
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image: ...
@@ -470,7 +486,9 @@ class Image(modal.object.Object):
470
486
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
471
487
  context_mount: typing.Optional[modal.mount.Mount] = None,
472
488
  force_build: bool = False,
473
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
489
+ ignore: typing.Union[
490
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
491
+ ] = modal.image.AUTO_DOCKERIGNORE,
474
492
  ) -> Image: ...
475
493
  def entrypoint(self, entrypoint_commands: list[str]) -> Image: ...
476
494
  def shell(self, shell_commands: list[str]) -> Image: ...
@@ -556,7 +574,9 @@ class Image(modal.object.Object):
556
574
  secrets: collections.abc.Sequence[modal.secret.Secret] = [],
557
575
  gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
558
576
  add_python: typing.Optional[str] = None,
559
- ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]] = (),
577
+ ignore: typing.Union[
578
+ collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
579
+ ] = modal.image.AUTO_DOCKERIGNORE,
560
580
  ) -> Image: ...
561
581
  @staticmethod
562
582
  def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.12
3
+ Version: 0.72.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=vEE0cK5QPF6_cdW5AJvcuWxz5KmeprHwBEtlDkVRHgE,45582
19
19
  modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
22
- modal/client.pyi,sha256=fidFDEnApJ5cdIG6f0JFX3SIzsgBEH7Ubu26iPbYew4,7280
22
+ modal/client.pyi,sha256=yhGbIx_ZDcPa1Lo4cm7CWmiSkF8-hiMxea6OG8xvOEA,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,12 +35,12 @@ modal/exception.py,sha256=4JyO-SACaLNDe2QC48EjsK8GMkZ8AgEurZ8j1YdRu8E,5263
35
35
  modal/experimental.py,sha256=npfKbyMpI41uZZs9HW_QiB3E4ykWfDXZbACXXbw6qeA,2385
36
36
  modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
37
37
  modal/file_io.pyi,sha256=NrIoB0YjIqZ8MDMe826xAnybT0ww_kxQM3iPLo82REU,8898
38
- modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms,5571
38
+ modal/file_pattern_matcher.py,sha256=uksEpQG4LSNdW57NQVqP9oOWPpD2-c9QVaAN_dmzKIQ,6415
39
39
  modal/functions.py,sha256=3uJPbrEAWhpFfLfUnoRjGmvEUC-_wVh-8yNJBx8eVeM,68249
40
- modal/functions.pyi,sha256=3ESJ61f8oEDycDmrpnuNB2vjFKuLBG_aqyliXPTdY7M,25407
40
+ modal/functions.pyi,sha256=LiSDgH-X7jcZ56pAoLMwo3x9Dzdp_3Sd7W5MVAJPoCg,25407
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
- modal/image.py,sha256=oKqqLhc3Ap2XMG5MKVlERKkMTwJPkNMNcSzxoZh4zuw,85259
43
- modal/image.pyi,sha256=Pa1_LVr3FyNsnu_MhBO08fBgCeLazTEe25phYdu0bzE,25365
42
+ modal/image.py,sha256=BAalPHfJn3TM5RW63AK-TRQjq82K2KLgzXXxWwfYbxI,89366
43
+ modal/image.pyi,sha256=NfZyLkl4rmxpc5fokaO4mmEeGFOwGn0AndV1vKwBdbs,26027
44
44
  modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
46
  modal/mount.py,sha256=wOr-2vmKImsE3lHBII8hL2gYy5ng46R58QwId4JultQ,29313
@@ -88,7 +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=FLz1q0YicL6i_Iq-4inkgDVFfEINVG6YPT2s_P6ly0o,2264
91
+ modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
92
92
  modal/_utils/function_utils.py,sha256=q68HhFH16MwhHRnGD8jvIgqDjduRQVp3a_qMWXPyrgU,25518
93
93
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
94
94
  modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
@@ -163,12 +163,12 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
163
163
  modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
164
164
  modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
165
165
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
- modal_version/__init__.py,sha256=BEBWj9tcbFUwzEjUrqly601rauw5cYsHdcmJHs3iu0s,470
166
+ modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
167
167
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
168
- modal_version/_version_generated.py,sha256=tsI6q2yoqEFbt-h7LyODo6hXvuxvd0qyoes8iA52jUA,149
169
- modal-0.71.12.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
- modal-0.71.12.dist-info/METADATA,sha256=7OCMKt2uJg0IqSqDJ8V-vcMrWoLr5dWLVp5SQ0NOXvk,2329
171
- modal-0.71.12.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
- modal-0.71.12.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
- modal-0.71.12.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
- modal-0.71.12.dist-info/RECORD,,
168
+ modal_version/_version_generated.py,sha256=asLMznMvSNVF3O1wHlqhvUWpAwCW2KJ8H_bGT-L07Sc,148
169
+ modal-0.72.0.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
+ modal-0.72.0.dist-info/METADATA,sha256=EdsneAri5o5Dm_88RJRdew5jM-Lfib7aLReAaJ5E0hY,2328
171
+ modal-0.72.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
+ modal-0.72.0.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
+ modal-0.72.0.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
+ modal-0.72.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 = 71
10
+ minor_number = 72
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 2025
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 12 # git: 54a0b70
4
+ build_number = 0 # git: bbea6a9