modal 0.71.9__py3-none-any.whl → 0.72.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/_utils/docker_utils.py +35 -1
- modal/_utils/function_utils.py +3 -3
- modal/app.py +1 -2
- modal/cli/launch.py +1 -1
- modal/cli/programs/run_jupyter.py +5 -10
- modal/cli/programs/vscode.py +5 -10
- modal/cli/volume.py +23 -0
- modal/client.pyi +2 -2
- modal/file_pattern_matcher.py +74 -41
- modal/functions.pyi +6 -6
- modal/image.py +166 -40
- modal/image.pyi +24 -4
- modal/mount.py +48 -2
- modal/mount.pyi +38 -0
- modal/volume.py +12 -0
- modal/volume.pyi +28 -0
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/METADATA +1 -1
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/RECORD +32 -32
- modal_global_objects/mounts/python_standalone.py +1 -1
- modal_proto/api.proto +6 -0
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +83 -71
- modal_proto/api_pb2.pyi +17 -0
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/LICENSE +0 -0
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/WHEEL +0 -0
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/entry_points.txt +0 -0
- {modal-0.71.9.dist-info → modal-0.72.5.dist-info}/top_level.txt +0 -0
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
|
|
@@ -654,7 +707,7 @@ class _Image(_Object, type_prefix="im"):
|
|
654
707
|
if remote_path.endswith("/"):
|
655
708
|
remote_path = remote_path + Path(local_path).name
|
656
709
|
|
657
|
-
mount = _Mount.
|
710
|
+
mount = _Mount._from_local_file(local_path, remote_path)
|
658
711
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
659
712
|
|
660
713
|
def add_local_dir(
|
@@ -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().
|
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().
|
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():
|
@@ -734,7 +795,7 @@ class _Image(_Object, type_prefix="im"):
|
|
734
795
|
return _Image._from_args(
|
735
796
|
base_images={"base": self},
|
736
797
|
dockerfile_function=build_dockerfile,
|
737
|
-
context_mount_function=lambda: _Mount.
|
798
|
+
context_mount_function=lambda: _Mount._from_local_file(local_path, remote_path=f"/{basename}"),
|
738
799
|
)
|
739
800
|
|
740
801
|
def add_local_python_source(
|
@@ -772,7 +833,7 @@ class _Image(_Object, type_prefix="im"):
|
|
772
833
|
)
|
773
834
|
```
|
774
835
|
"""
|
775
|
-
mount = _Mount.
|
836
|
+
mount = _Mount._from_local_python_packages(*modules, ignore=ignore)
|
776
837
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
777
838
|
|
778
839
|
def copy_local_dir(
|
@@ -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
|
-
"""
|
1211
|
-
|
1212
|
-
if not cmds:
|
1213
|
-
return self
|
1279
|
+
"""
|
1280
|
+
Extend an image with arbitrary Dockerfile-like commands.
|
1214
1281
|
|
1215
|
-
|
1216
|
-
if ignore:
|
1217
|
-
raise InvalidError("Cannot set both `context_mount` and `ignore`")
|
1282
|
+
**Usage:**
|
1218
1283
|
|
1219
|
-
|
1220
|
-
|
1284
|
+
```python
|
1285
|
+
from pathlib import Path
|
1286
|
+
from modal import FilePatternMatcher
|
1221
1287
|
|
1222
|
-
|
1223
|
-
|
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
|
-
|
1226
|
-
|
1227
|
-
|
1293
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1294
|
+
["COPY data /data"],
|
1295
|
+
ignore=["*.venv"],
|
1296
|
+
)
|
1228
1297
|
|
1229
|
-
|
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=
|
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
|
|
@@ -1408,9 +1504,14 @@ class _Image(_Object, type_prefix="im"):
|
|
1408
1504
|
_validate_python_version(add_python, builder_version, allow_micro_granularity=False)
|
1409
1505
|
add_python_commands = [
|
1410
1506
|
"COPY /python/. /usr/local",
|
1411
|
-
"RUN ln -s /usr/local/bin/python3 /usr/local/bin/python",
|
1412
1507
|
"ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
|
1413
1508
|
]
|
1509
|
+
if add_python < "3.13":
|
1510
|
+
# Previous versions did not include the `python` binary, but later ones do.
|
1511
|
+
# (The important factor is not the Python version itself, but the standalone dist version.)
|
1512
|
+
# We insert the command in the list at the position it was previously always added
|
1513
|
+
# for backwards compatibility with existing images.
|
1514
|
+
add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
|
1414
1515
|
|
1415
1516
|
# Note: this change is because we install dependencies with uv in 2024.10+
|
1416
1517
|
requirements_prefix = "python -m " if builder_version < "2024.10" else ""
|
@@ -1601,35 +1702,58 @@ class _Image(_Object, type_prefix="im"):
|
|
1601
1702
|
secrets: Sequence[_Secret] = [],
|
1602
1703
|
gpu: GPU_T = None,
|
1603
1704
|
add_python: Optional[str] = None,
|
1604
|
-
ignore: Union[Sequence[str], Callable[[Path], bool]] =
|
1705
|
+
ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
|
1605
1706
|
) -> "_Image":
|
1606
1707
|
"""Build a Modal image from a local Dockerfile.
|
1607
1708
|
|
1608
1709
|
If your Dockerfile does not have Python installed, you can use the `add_python` parameter
|
1609
1710
|
to specify a version of Python to add to the image.
|
1610
1711
|
|
1611
|
-
**
|
1712
|
+
**Usage:**
|
1612
1713
|
|
1613
1714
|
```python
|
1614
|
-
|
1615
|
-
|
1616
|
-
"""
|
1715
|
+
from pathlib import Path
|
1716
|
+
from modal import FilePatternMatcher
|
1617
1717
|
|
1618
|
-
if
|
1619
|
-
|
1620
|
-
|
1718
|
+
# By default a .dockerignore file is used if present in the current working directory
|
1719
|
+
image = modal.Image.from_dockerfile(
|
1720
|
+
"./Dockerfile",
|
1721
|
+
add_python="3.12",
|
1722
|
+
)
|
1621
1723
|
|
1622
|
-
|
1623
|
-
|
1724
|
+
image = modal.Image.from_dockerfile(
|
1725
|
+
"./Dockerfile",
|
1726
|
+
add_python="3.12",
|
1727
|
+
ignore=["*.venv"],
|
1728
|
+
)
|
1624
1729
|
|
1625
|
-
|
1626
|
-
|
1730
|
+
image = modal.Image.from_dockerfile(
|
1731
|
+
"./Dockerfile",
|
1732
|
+
add_python="3.12",
|
1733
|
+
ignore=lambda p: p.is_relative_to(".venv"),
|
1734
|
+
)
|
1627
1735
|
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1736
|
+
image = modal.Image.from_dockerfile(
|
1737
|
+
"./Dockerfile",
|
1738
|
+
add_python="3.12",
|
1739
|
+
ignore=FilePatternMatcher("**/*.txt"),
|
1740
|
+
)
|
1631
1741
|
|
1632
|
-
|
1742
|
+
# When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
|
1743
|
+
image = modal.Image.from_dockerfile(
|
1744
|
+
"./Dockerfile",
|
1745
|
+
add_python="3.12",
|
1746
|
+
ignore=~FilePatternMatcher("**/*.py"),
|
1747
|
+
)
|
1748
|
+
|
1749
|
+
# You can also read ignore patterns from a file.
|
1750
|
+
image = modal.Image.from_dockerfile(
|
1751
|
+
"./Dockerfile",
|
1752
|
+
add_python="3.12",
|
1753
|
+
ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
|
1754
|
+
)
|
1755
|
+
```
|
1756
|
+
"""
|
1633
1757
|
|
1634
1758
|
# --- Build the base dockerfile
|
1635
1759
|
|
@@ -1641,7 +1765,9 @@ class _Image(_Object, type_prefix="im"):
|
|
1641
1765
|
gpu_config = parse_gpu_config(gpu)
|
1642
1766
|
base_image = _Image._from_args(
|
1643
1767
|
dockerfile_function=build_dockerfile_base,
|
1644
|
-
context_mount_function=
|
1768
|
+
context_mount_function=_create_context_mount_function(
|
1769
|
+
ignore=ignore, dockerfile_path=Path(path), context_mount=context_mount
|
1770
|
+
),
|
1645
1771
|
gpu_config=gpu_config,
|
1646
1772
|
secrets=secrets,
|
1647
1773
|
)
|
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[
|
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[
|
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[
|
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[
|
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: ...
|
modal/mount.py
CHANGED
@@ -23,7 +23,7 @@ from modal_version import __version__
|
|
23
23
|
from ._resolver import Resolver
|
24
24
|
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
25
25
|
from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
|
26
|
-
from ._utils.deprecation import renamed_parameter
|
26
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
27
27
|
from ._utils.grpc_utils import retry_transient_errors
|
28
28
|
from ._utils.name_utils import check_object_name
|
29
29
|
from ._utils.package_utils import get_module_mount_info
|
@@ -48,6 +48,11 @@ PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
|
|
48
48
|
"3.13": ("20241008", "3.13.0"),
|
49
49
|
}
|
50
50
|
|
51
|
+
MOUNT_DEPRECATION_MESSAGE_PATTERN = """modal.Mount usage will soon be deprecated.
|
52
|
+
|
53
|
+
Use {replacement} instead, which is functionally and performance-wise equivalent.
|
54
|
+
"""
|
55
|
+
|
51
56
|
|
52
57
|
def client_mount_name() -> str:
|
53
58
|
"""Get the deployed name of the client package mount."""
|
@@ -401,6 +406,23 @@ class _Mount(_Object, type_prefix="mo"):
|
|
401
406
|
)
|
402
407
|
```
|
403
408
|
"""
|
409
|
+
deprecation_warning(
|
410
|
+
(2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_dir"), pending=True
|
411
|
+
)
|
412
|
+
return _Mount._from_local_dir(local_path, remote_path=remote_path, condition=condition, recursive=recursive)
|
413
|
+
|
414
|
+
@staticmethod
|
415
|
+
def _from_local_dir(
|
416
|
+
local_path: Union[str, Path],
|
417
|
+
*,
|
418
|
+
# Where the directory is placed within in the mount
|
419
|
+
remote_path: Union[str, PurePosixPath, None] = None,
|
420
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
421
|
+
# Defaults to including all files.
|
422
|
+
condition: Optional[Callable[[str], bool]] = None,
|
423
|
+
# add files from subdirectories as well
|
424
|
+
recursive: bool = True,
|
425
|
+
) -> "_Mount":
|
404
426
|
return _Mount._new().add_local_dir(
|
405
427
|
local_path, remote_path=remote_path, condition=condition, recursive=recursive
|
406
428
|
)
|
@@ -439,6 +461,13 @@ class _Mount(_Object, type_prefix="mo"):
|
|
439
461
|
)
|
440
462
|
```
|
441
463
|
"""
|
464
|
+
deprecation_warning(
|
465
|
+
(2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_file"), pending=True
|
466
|
+
)
|
467
|
+
return _Mount._from_local_file(local_path, remote_path)
|
468
|
+
|
469
|
+
@staticmethod
|
470
|
+
def _from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
|
442
471
|
return _Mount._new().add_local_file(local_path, remote_path=remote_path)
|
443
472
|
|
444
473
|
@staticmethod
|
@@ -601,7 +630,24 @@ class _Mount(_Object, type_prefix="mo"):
|
|
601
630
|
my_local_module.do_stuff()
|
602
631
|
```
|
603
632
|
"""
|
633
|
+
deprecation_warning(
|
634
|
+
(2024, 1, 8),
|
635
|
+
MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_python_source"),
|
636
|
+
pending=True,
|
637
|
+
)
|
638
|
+
return _Mount._from_local_python_packages(
|
639
|
+
*module_names, remote_dir=remote_dir, condition=condition, ignore=ignore
|
640
|
+
)
|
604
641
|
|
642
|
+
@staticmethod
|
643
|
+
def _from_local_python_packages(
|
644
|
+
*module_names: str,
|
645
|
+
remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
|
646
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
647
|
+
# Defaults to including all files.
|
648
|
+
condition: Optional[Callable[[str], bool]] = None,
|
649
|
+
ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
|
650
|
+
) -> "_Mount":
|
605
651
|
# Don't re-run inside container.
|
606
652
|
|
607
653
|
if condition is not None:
|
@@ -786,7 +832,7 @@ def get_auto_mounts() -> list[_Mount]:
|
|
786
832
|
|
787
833
|
try:
|
788
834
|
# at this point we don't know if the sys.modules module should be mounted or not
|
789
|
-
potential_mount = _Mount.
|
835
|
+
potential_mount = _Mount._from_local_python_packages(module_name)
|
790
836
|
mount_paths = potential_mount._top_level_paths()
|
791
837
|
except ModuleNotMountable:
|
792
838
|
# this typically happens if the module is a built-in, has binary components or doesn't exist
|
modal/mount.pyi
CHANGED
@@ -113,6 +113,14 @@ class _Mount(modal.object._Object):
|
|
113
113
|
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
114
114
|
recursive: bool = True,
|
115
115
|
) -> _Mount: ...
|
116
|
+
@staticmethod
|
117
|
+
def _from_local_dir(
|
118
|
+
local_path: typing.Union[str, pathlib.Path],
|
119
|
+
*,
|
120
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
121
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
122
|
+
recursive: bool = True,
|
123
|
+
) -> _Mount: ...
|
116
124
|
def add_local_file(
|
117
125
|
self,
|
118
126
|
local_path: typing.Union[str, pathlib.Path],
|
@@ -123,6 +131,10 @@ class _Mount(modal.object._Object):
|
|
123
131
|
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
124
132
|
) -> _Mount: ...
|
125
133
|
@staticmethod
|
134
|
+
def _from_local_file(
|
135
|
+
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
136
|
+
) -> _Mount: ...
|
137
|
+
@staticmethod
|
126
138
|
def _description(entries: list[_MountEntry]) -> str: ...
|
127
139
|
@staticmethod
|
128
140
|
def _get_files(
|
@@ -139,6 +151,13 @@ class _Mount(modal.object._Object):
|
|
139
151
|
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
140
152
|
) -> _Mount: ...
|
141
153
|
@staticmethod
|
154
|
+
def _from_local_python_packages(
|
155
|
+
*module_names: str,
|
156
|
+
remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
|
157
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
158
|
+
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
159
|
+
) -> _Mount: ...
|
160
|
+
@staticmethod
|
142
161
|
def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> _Mount: ...
|
143
162
|
@classmethod
|
144
163
|
async def lookup(
|
@@ -195,6 +214,14 @@ class Mount(modal.object.Object):
|
|
195
214
|
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
196
215
|
recursive: bool = True,
|
197
216
|
) -> Mount: ...
|
217
|
+
@staticmethod
|
218
|
+
def _from_local_dir(
|
219
|
+
local_path: typing.Union[str, pathlib.Path],
|
220
|
+
*,
|
221
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
222
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
223
|
+
recursive: bool = True,
|
224
|
+
) -> Mount: ...
|
198
225
|
def add_local_file(
|
199
226
|
self,
|
200
227
|
local_path: typing.Union[str, pathlib.Path],
|
@@ -205,6 +232,10 @@ class Mount(modal.object.Object):
|
|
205
232
|
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
206
233
|
) -> Mount: ...
|
207
234
|
@staticmethod
|
235
|
+
def _from_local_file(
|
236
|
+
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
237
|
+
) -> Mount: ...
|
238
|
+
@staticmethod
|
208
239
|
def _description(entries: list[_MountEntry]) -> str: ...
|
209
240
|
|
210
241
|
class ___get_files_spec(typing_extensions.Protocol):
|
@@ -231,6 +262,13 @@ class Mount(modal.object.Object):
|
|
231
262
|
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
232
263
|
) -> Mount: ...
|
233
264
|
@staticmethod
|
265
|
+
def _from_local_python_packages(
|
266
|
+
*module_names: str,
|
267
|
+
remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
|
268
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
269
|
+
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
270
|
+
) -> Mount: ...
|
271
|
+
@staticmethod
|
234
272
|
def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> Mount: ...
|
235
273
|
@classmethod
|
236
274
|
def lookup(
|
modal/volume.py
CHANGED
@@ -506,6 +506,18 @@ class _Volume(_Object, type_prefix="vo"):
|
|
506
506
|
req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
|
507
507
|
await retry_transient_errors(obj._client.stub.VolumeDelete, req)
|
508
508
|
|
509
|
+
@staticmethod
|
510
|
+
async def rename(
|
511
|
+
old_name: str,
|
512
|
+
new_name: str,
|
513
|
+
*,
|
514
|
+
client: Optional[_Client] = None,
|
515
|
+
environment_name: Optional[str] = None,
|
516
|
+
):
|
517
|
+
obj = await _Volume.lookup(old_name, client=client, environment_name=environment_name)
|
518
|
+
req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
|
519
|
+
await retry_transient_errors(obj._client.stub.VolumeRename, req)
|
520
|
+
|
509
521
|
|
510
522
|
class _VolumeUploadContextManager:
|
511
523
|
"""Context manager for batch-uploading files to a Volume."""
|
modal/volume.pyi
CHANGED
@@ -85,6 +85,14 @@ class _Volume(modal.object._Object):
|
|
85
85
|
async def delete(
|
86
86
|
name: str, client: typing.Optional[modal.client._Client] = None, environment_name: typing.Optional[str] = None
|
87
87
|
): ...
|
88
|
+
@staticmethod
|
89
|
+
async def rename(
|
90
|
+
old_name: str,
|
91
|
+
new_name: str,
|
92
|
+
*,
|
93
|
+
client: typing.Optional[modal.client._Client] = None,
|
94
|
+
environment_name: typing.Optional[str] = None,
|
95
|
+
): ...
|
88
96
|
|
89
97
|
class _VolumeUploadContextManager:
|
90
98
|
_volume_id: str
|
@@ -272,6 +280,26 @@ class Volume(modal.object.Object):
|
|
272
280
|
|
273
281
|
delete: __delete_spec
|
274
282
|
|
283
|
+
class __rename_spec(typing_extensions.Protocol):
|
284
|
+
def __call__(
|
285
|
+
self,
|
286
|
+
old_name: str,
|
287
|
+
new_name: str,
|
288
|
+
*,
|
289
|
+
client: typing.Optional[modal.client.Client] = None,
|
290
|
+
environment_name: typing.Optional[str] = None,
|
291
|
+
): ...
|
292
|
+
async def aio(
|
293
|
+
self,
|
294
|
+
old_name: str,
|
295
|
+
new_name: str,
|
296
|
+
*,
|
297
|
+
client: typing.Optional[modal.client.Client] = None,
|
298
|
+
environment_name: typing.Optional[str] = None,
|
299
|
+
): ...
|
300
|
+
|
301
|
+
rename: __rename_spec
|
302
|
+
|
275
303
|
class VolumeUploadContextManager:
|
276
304
|
_volume_id: str
|
277
305
|
_client: modal.client.Client
|