modal 0.68.42__py3-none-any.whl → 0.70.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/_container_entrypoint.py +1 -1
- modal/_runtime/asgi.py +11 -4
- modal/_runtime/container_io_manager.py +12 -19
- modal/_utils/docker_utils.py +64 -0
- modal/_utils/function_utils.py +24 -13
- modal/cli/launch.py +2 -0
- modal/cli/programs/vscode.py +27 -2
- modal/client.pyi +2 -2
- modal/file_io.py +16 -10
- modal/file_pattern_matcher.py +11 -1
- modal/functions.py +1 -1
- modal/functions.pyi +6 -6
- modal/gpu.py +22 -0
- modal/image.py +95 -39
- modal/image.pyi +11 -2
- modal/mount.py +10 -9
- modal/mount.pyi +4 -4
- modal/partial_function.py +4 -4
- modal/runner.py +9 -5
- modal/running_app.py +27 -1
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/METADATA +2 -2
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/RECORD +35 -34
- modal_proto/api.proto +9 -0
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +785 -765
- modal_proto/api_pb2.pyi +30 -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.68.42.dist-info → modal-0.70.2.dist-info}/LICENSE +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/WHEEL +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/entry_points.txt +0 -0
- {modal-0.68.42.dist-info → modal-0.70.2.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
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),
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
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
|
-
|
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
|
-
|
1547
|
-
|
1600
|
+
if context_mount:
|
1601
|
+
if ignore:
|
1602
|
+
raise InvalidError("Cannot set both `context_mount` and `ignore`")
|
1548
1603
|
|
1549
|
-
|
1550
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
@@ -105,7 +106,7 @@ class _MountFile(_MountEntry):
|
|
105
106
|
return str(self.local_file)
|
106
107
|
|
107
108
|
def get_files_to_upload(self):
|
108
|
-
local_file = self.local_file.
|
109
|
+
local_file = self.local_file.resolve()
|
109
110
|
if not local_file.exists():
|
110
111
|
raise FileNotFoundError(local_file)
|
111
112
|
|
@@ -131,6 +132,8 @@ class _MountDir(_MountEntry):
|
|
131
132
|
return str(self.local_dir.expanduser().absolute())
|
132
133
|
|
133
134
|
def get_files_to_upload(self):
|
135
|
+
# we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
|
136
|
+
# see test_mount_directory_with_symlinked_file
|
134
137
|
local_dir = self.local_dir.expanduser().absolute()
|
135
138
|
|
136
139
|
if not local_dir.exists():
|
@@ -145,10 +148,11 @@ class _MountDir(_MountEntry):
|
|
145
148
|
gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
|
146
149
|
|
147
150
|
for local_filename in gen:
|
148
|
-
|
149
|
-
|
151
|
+
local_path = Path(local_filename)
|
152
|
+
if not self.ignore(local_path):
|
153
|
+
local_relpath = local_path.expanduser().absolute().relative_to(local_dir)
|
150
154
|
mount_path = self.remote_path / local_relpath.as_posix()
|
151
|
-
yield
|
155
|
+
yield local_path.resolve(), mount_path
|
152
156
|
|
153
157
|
def watch_entry(self):
|
154
158
|
return self.local_dir.resolve().expanduser(), None
|
@@ -322,12 +326,9 @@ class _Mount(_Object, type_prefix="mo"):
|
|
322
326
|
@staticmethod
|
323
327
|
def _add_local_dir(
|
324
328
|
local_path: Path,
|
325
|
-
remote_path:
|
326
|
-
ignore:
|
329
|
+
remote_path: PurePosixPath,
|
330
|
+
ignore: Callable[[Path], bool] = modal.file_pattern_matcher._NOTHING,
|
327
331
|
):
|
328
|
-
if isinstance(ignore, list):
|
329
|
-
ignore = FilePatternMatcher(*ignore)
|
330
|
-
|
331
332
|
return _Mount._new()._extend(
|
332
333
|
_MountDir(
|
333
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.
|
98
|
-
ignore: typing.
|
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.
|
180
|
-
ignore: typing.
|
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
|
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
|
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
|
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
|
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.
|
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.
|
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
|
-
|
65
|
-
|
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
|
-
|
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.
|
3
|
+
Version: 0.70.2
|
4
4
|
Summary: Python client library for Modal
|
5
5
|
Author: Modal Labs
|
6
6
|
Author-email: support@modal.com
|
@@ -21,7 +21,7 @@ Requires-Dist: fastapi
|
|
21
21
|
Requires-Dist: grpclib (==0.4.7)
|
22
22
|
Requires-Dist: protobuf (!=4.24.0,<6.0,>=3.19)
|
23
23
|
Requires-Dist: rich (>=12.0.0)
|
24
|
-
Requires-Dist: synchronicity (~=0.9.
|
24
|
+
Requires-Dist: synchronicity (~=0.9.8)
|
25
25
|
Requires-Dist: toml
|
26
26
|
Requires-Dist: typer (>=0.9)
|
27
27
|
Requires-Dist: types-certifi
|