modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +171 -138
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +30 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +2 -3
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +285 -105
- modal/app.pyi +216 -53
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +24 -53
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +82 -47
- modal/image.pyi +51 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -46
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -107
- modal/sandbox.pyi +226 -60
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +141 -70
- modal_proto/api_grpc.py +42 -26
- modal_proto/api_pb2.py +1123 -1103
- modal_proto/api_pb2.pyi +331 -83
- modal_proto/api_pb2_grpc.py +80 -48
- modal_proto/api_pb2_grpc.pyi +26 -18
- modal_proto/modal_api_grpc.py +175 -174
- modal_proto/task_command_router.proto +164 -0
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev66.dist-info/RECORD +0 -191
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- modal_proto/sandbox_router.proto +0 -125
- modal_proto/sandbox_router_grpc.py +0 -89
- modal_proto/sandbox_router_pb2.py +0 -128
- modal_proto/sandbox_router_pb2_grpc.py +0 -169
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/image.py
CHANGED
|
@@ -21,40 +21,50 @@ from typing import (
|
|
|
21
21
|
get_args,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
import typing_extensions
|
|
24
25
|
from google.protobuf.message import Message
|
|
25
|
-
from grpclib.exceptions import
|
|
26
|
+
from grpclib.exceptions import StreamTerminatedError
|
|
26
27
|
from typing_extensions import Self
|
|
27
28
|
|
|
28
29
|
from modal._serialization import serialize_data_format
|
|
29
30
|
from modal_proto import api_pb2
|
|
30
31
|
|
|
32
|
+
from ._load_context import LoadContext
|
|
31
33
|
from ._object import _Object, live_method_gen
|
|
32
34
|
from ._resolver import Resolver
|
|
33
35
|
from ._serialization import get_preferred_payload_format, serialize
|
|
34
|
-
from ._utils.async_utils import synchronize_api
|
|
36
|
+
from ._utils.async_utils import deprecate_aio_usage, synchronize_api, synchronizer
|
|
35
37
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
36
38
|
from ._utils.docker_utils import (
|
|
37
39
|
extract_copy_command_patterns,
|
|
38
40
|
find_dockerignore_file,
|
|
39
41
|
)
|
|
40
42
|
from ._utils.function_utils import FunctionInfo
|
|
41
|
-
from ._utils.
|
|
43
|
+
from ._utils.mount_utils import validate_only_modal_volumes
|
|
42
44
|
from .client import _Client
|
|
43
45
|
from .cloud_bucket_mount import _CloudBucketMount
|
|
44
46
|
from .config import config, logger, user_config_path
|
|
45
47
|
from .environments import _get_environment_cached
|
|
46
|
-
from .exception import
|
|
48
|
+
from .exception import (
|
|
49
|
+
ExecutionError,
|
|
50
|
+
InternalError,
|
|
51
|
+
InvalidError,
|
|
52
|
+
NotFoundError,
|
|
53
|
+
RemoteError,
|
|
54
|
+
ServiceError,
|
|
55
|
+
VersionError,
|
|
56
|
+
)
|
|
47
57
|
from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
|
|
48
58
|
from .gpu import GPU_T, parse_gpu_config
|
|
49
59
|
from .mount import _Mount, python_standalone_mount_name
|
|
50
60
|
from .network_file_system import _NetworkFileSystem
|
|
51
61
|
from .output import _get_output_manager
|
|
52
|
-
from .scheduler_placement import SchedulerPlacement
|
|
53
62
|
from .secret import _Secret
|
|
54
63
|
from .volume import _Volume
|
|
55
64
|
|
|
56
65
|
if typing.TYPE_CHECKING:
|
|
57
66
|
import modal._functions
|
|
67
|
+
import modal.client
|
|
58
68
|
|
|
59
69
|
# This is used for both type checking and runtime validation
|
|
60
70
|
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
|
|
@@ -64,11 +74,11 @@ ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVI
|
|
|
64
74
|
# Python versions in mount.py where we specify the "standalone Python versions" we create mounts for.
|
|
65
75
|
# Consider consolidating these multiple sources of truth?
|
|
66
76
|
SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
|
|
67
|
-
"PREVIEW": ["3.
|
|
68
|
-
"2025.06": ["3.
|
|
69
|
-
"2024.10": ["3.
|
|
70
|
-
"2024.04": ["3.
|
|
71
|
-
"2023.12": ["3.
|
|
77
|
+
"PREVIEW": ["3.10", "3.11", "3.12", "3.13", "3.14"],
|
|
78
|
+
"2025.06": ["3.10", "3.11", "3.12", "3.13", "3.14"],
|
|
79
|
+
"2024.10": ["3.10", "3.11", "3.12", "3.13"],
|
|
80
|
+
"2024.04": ["3.10", "3.11", "3.12"],
|
|
81
|
+
"2023.12": ["3.10", "3.11", "3.12"],
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "builder"
|
|
@@ -373,9 +383,7 @@ async def _image_await_build_result(image_id: str, client: _Client) -> api_pb2.I
|
|
|
373
383
|
while result_response is None:
|
|
374
384
|
try:
|
|
375
385
|
await join()
|
|
376
|
-
except (
|
|
377
|
-
if isinstance(exc, GRPCError) and exc.status not in RETRYABLE_GRPC_STATUS_CODES:
|
|
378
|
-
raise exc
|
|
386
|
+
except (ServiceError, InternalError, StreamTerminatedError) as exc:
|
|
379
387
|
retry_count += 1
|
|
380
388
|
if retry_count >= 3:
|
|
381
389
|
raise exc
|
|
@@ -433,12 +441,16 @@ class _Image(_Object, type_prefix="im"):
|
|
|
433
441
|
|
|
434
442
|
base_image = self
|
|
435
443
|
|
|
436
|
-
async def _load(
|
|
444
|
+
async def _load(
|
|
445
|
+
self2: "_Image", resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
446
|
+
):
|
|
437
447
|
self2._hydrate_from_other(base_image) # same image id as base image as long as it's lazy
|
|
438
448
|
self2._deferred_mounts = tuple(base_image._deferred_mounts) + (mount,)
|
|
439
449
|
self2._serve_mounts = base_image._serve_mounts | ({mount} if mount.is_local() else set())
|
|
440
450
|
|
|
441
|
-
img = _Image._from_loader(
|
|
451
|
+
img = _Image._from_loader(
|
|
452
|
+
_load, "Image(local files)", deps=lambda: [base_image, mount], load_context_overrides=LoadContext.empty()
|
|
453
|
+
)
|
|
442
454
|
img._added_python_source_set = base_image._added_python_source_set
|
|
443
455
|
return img
|
|
444
456
|
|
|
@@ -487,6 +499,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
487
499
|
context_mount_function: Optional[Callable[[], Optional[_Mount]]] = None,
|
|
488
500
|
force_build: bool = False,
|
|
489
501
|
build_args: dict[str, str] = {},
|
|
502
|
+
validated_volumes: Optional[Sequence[tuple[str, _Volume]]] = None,
|
|
490
503
|
# For internal use only.
|
|
491
504
|
_namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
492
505
|
_do_assert_no_mount_layers: bool = True,
|
|
@@ -494,6 +507,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
494
507
|
if base_images is None:
|
|
495
508
|
base_images = {}
|
|
496
509
|
|
|
510
|
+
if validated_volumes is None:
|
|
511
|
+
validated_volumes = []
|
|
512
|
+
|
|
497
513
|
if secrets is None:
|
|
498
514
|
secrets = []
|
|
499
515
|
if gpu_config is None:
|
|
@@ -514,20 +530,22 @@ class _Image(_Object, type_prefix="im"):
|
|
|
514
530
|
deps += (build_function,)
|
|
515
531
|
if image_registry_config and image_registry_config.secret:
|
|
516
532
|
deps += (image_registry_config.secret,)
|
|
533
|
+
for _, vol in validated_volumes:
|
|
534
|
+
deps += (vol,)
|
|
517
535
|
return deps
|
|
518
536
|
|
|
519
|
-
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
|
537
|
+
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
520
538
|
context_mount = context_mount_function() if context_mount_function else None
|
|
521
539
|
if context_mount:
|
|
522
|
-
await resolver.load(context_mount)
|
|
540
|
+
await resolver.load(context_mount, load_context)
|
|
523
541
|
|
|
524
542
|
if _do_assert_no_mount_layers:
|
|
525
543
|
for image in base_images.values():
|
|
526
544
|
# base images can't have
|
|
527
545
|
image._assert_no_mount_layers()
|
|
528
546
|
|
|
529
|
-
assert
|
|
530
|
-
environment = await _get_environment_cached(
|
|
547
|
+
assert load_context.app_id # type narrowing
|
|
548
|
+
environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
|
|
531
549
|
# A bit hacky,but assume that the environment provides a valid builder version
|
|
532
550
|
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
|
533
551
|
builder_version = _get_image_builder_version(image_builder_version)
|
|
@@ -592,6 +610,17 @@ class _Image(_Object, type_prefix="im"):
|
|
|
592
610
|
build_function_id = ""
|
|
593
611
|
_build_function = None
|
|
594
612
|
|
|
613
|
+
# Relies on dicts being ordered (true as of Python 3.6).
|
|
614
|
+
volume_mounts = [
|
|
615
|
+
api_pb2.VolumeMount(
|
|
616
|
+
mount_path=path,
|
|
617
|
+
volume_id=volume.object_id,
|
|
618
|
+
allow_background_commits=True,
|
|
619
|
+
read_only=volume._read_only,
|
|
620
|
+
)
|
|
621
|
+
for path, volume in validated_volumes
|
|
622
|
+
]
|
|
623
|
+
|
|
595
624
|
image_definition = api_pb2.Image(
|
|
596
625
|
base_images=base_images_pb2s,
|
|
597
626
|
dockerfile_commands=dockerfile.commands,
|
|
@@ -604,10 +633,11 @@ class _Image(_Object, type_prefix="im"):
|
|
|
604
633
|
runtime_debug=config.get("function_runtime_debug"),
|
|
605
634
|
build_function=_build_function,
|
|
606
635
|
build_args=build_args,
|
|
636
|
+
volume_mounts=volume_mounts,
|
|
607
637
|
)
|
|
608
638
|
|
|
609
639
|
req = api_pb2.ImageGetOrCreateRequest(
|
|
610
|
-
app_id=
|
|
640
|
+
app_id=load_context.app_id,
|
|
611
641
|
image=image_definition,
|
|
612
642
|
existing_image_id=existing_object_id or "", # TODO: ignored
|
|
613
643
|
build_function_id=build_function_id,
|
|
@@ -619,7 +649,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
619
649
|
allow_global_deployment=os.environ.get("MODAL_IMAGE_ALLOW_GLOBAL_DEPLOYMENT") == "1",
|
|
620
650
|
ignore_cache=config.get("ignore_cache"),
|
|
621
651
|
)
|
|
622
|
-
resp = await
|
|
652
|
+
resp = await load_context.client.stub.ImageGetOrCreate(req)
|
|
623
653
|
image_id = resp.image_id
|
|
624
654
|
result: api_pb2.GenericResult
|
|
625
655
|
metadata: Optional[api_pb2.ImageMetadata] = None
|
|
@@ -632,7 +662,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
632
662
|
else:
|
|
633
663
|
# not built or in the process of building - wait for build
|
|
634
664
|
logger.debug("Waiting for image %s" % image_id)
|
|
635
|
-
resp = await _image_await_build_result(image_id,
|
|
665
|
+
resp = await _image_await_build_result(image_id, load_context.client)
|
|
636
666
|
result = resp.result
|
|
637
667
|
if resp.HasField("metadata"):
|
|
638
668
|
metadata = resp.metadata
|
|
@@ -662,7 +692,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
662
692
|
else:
|
|
663
693
|
raise RemoteError("Unknown status %s!" % result.status)
|
|
664
694
|
|
|
665
|
-
self._hydrate(image_id,
|
|
695
|
+
self._hydrate(image_id, load_context.client, metadata)
|
|
666
696
|
local_mounts = set()
|
|
667
697
|
for base in base_images.values():
|
|
668
698
|
local_mounts |= base._serve_mounts
|
|
@@ -671,7 +701,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
671
701
|
self._serve_mounts = frozenset(local_mounts)
|
|
672
702
|
|
|
673
703
|
rep = f"Image({dockerfile_function})"
|
|
674
|
-
obj = _Image._from_loader(_load, rep, deps=_deps)
|
|
704
|
+
obj = _Image._from_loader(_load, rep, deps=_deps, load_context_overrides=LoadContext.empty())
|
|
675
705
|
obj.force_build = force_build
|
|
676
706
|
obj._added_python_source_set = frozenset.union(
|
|
677
707
|
frozenset(), *(base._added_python_source_set for base in base_images.values())
|
|
@@ -838,23 +868,25 @@ class _Image(_Object, type_prefix="im"):
|
|
|
838
868
|
img._added_python_source_set |= set(modules)
|
|
839
869
|
return img
|
|
840
870
|
|
|
841
|
-
@
|
|
842
|
-
|
|
871
|
+
@deprecate_aio_usage((2025, 11, 14), "Image.from_id")
|
|
872
|
+
@classmethod
|
|
873
|
+
def from_id(cls, image_id: str, client: Optional["modal.client.Client"] = None) -> typing_extensions.Self:
|
|
843
874
|
"""Construct an Image from an id and look up the Image result.
|
|
844
875
|
|
|
845
876
|
The ID of an Image object can be accessed using `.object_id`.
|
|
846
877
|
"""
|
|
847
|
-
|
|
848
|
-
client = await _Client.from_env()
|
|
878
|
+
_client = typing.cast(_Client, synchronizer._translate_in(client))
|
|
849
879
|
|
|
850
|
-
async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
|
|
851
|
-
resp = await
|
|
852
|
-
self._hydrate(resp.image_id,
|
|
880
|
+
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
|
|
881
|
+
resp = await load_context.client.stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
|
|
882
|
+
self._hydrate(resp.image_id, load_context.client, resp.metadata)
|
|
853
883
|
|
|
854
884
|
rep = f"Image.from_id({image_id!r})"
|
|
855
|
-
obj = _Image._from_loader(_load, rep)
|
|
856
885
|
|
|
857
|
-
|
|
886
|
+
obj = _Image._from_loader(_load, rep, load_context_overrides=LoadContext(client=_client))
|
|
887
|
+
obj._object_id = image_id
|
|
888
|
+
|
|
889
|
+
return typing.cast(typing_extensions.Self, synchronizer._translate_out(obj))
|
|
858
890
|
|
|
859
891
|
async def build(self, app: "modal.app._App") -> "_Image":
|
|
860
892
|
"""Eagerly build an image.
|
|
@@ -867,8 +899,8 @@ class _Image(_Object, type_prefix="im"):
|
|
|
867
899
|
```python
|
|
868
900
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
869
901
|
|
|
870
|
-
app = modal.App("build-image")
|
|
871
|
-
with modal.enable_output()
|
|
902
|
+
app = modal.App.lookup("build-image", create_if_missing=True)
|
|
903
|
+
with modal.enable_output(): # To see logs in your local terminal
|
|
872
904
|
image.build(app)
|
|
873
905
|
|
|
874
906
|
# Save the image id
|
|
@@ -881,7 +913,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
881
913
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
882
914
|
|
|
883
915
|
```python notest
|
|
884
|
-
app = modal.App.lookup("sandbox-example")
|
|
916
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
885
917
|
|
|
886
918
|
with modal.enable_output():
|
|
887
919
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -911,11 +943,8 @@ class _Image(_Object, type_prefix="im"):
|
|
|
911
943
|
if app.app_id is None:
|
|
912
944
|
raise InvalidError("App has not been initialized yet. Use the content manager `app.run()` or `App.lookup`")
|
|
913
945
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
resolver = Resolver(app_client, app_id=app_id)
|
|
918
|
-
await resolver.load(self)
|
|
946
|
+
resolver = Resolver()
|
|
947
|
+
await resolver.load(self, app._root_load_context)
|
|
919
948
|
return self
|
|
920
949
|
|
|
921
950
|
def pip_install(
|
|
@@ -1441,6 +1470,15 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1441
1470
|
The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context. The
|
|
1442
1471
|
`uv_project_dir` is relative to the current working directory of where `modal` is called.
|
|
1443
1472
|
|
|
1473
|
+
NOTE: This does *not* install the project itself into the environment (this is equivalent to the
|
|
1474
|
+
`--no-install-project` flag in the `uv sync` command) and you would be expected to add any local python source
|
|
1475
|
+
files using `Image.add_local_python_source` or similar methods after this call.
|
|
1476
|
+
|
|
1477
|
+
This ensures that updates to your project code wouldn't require reinstalling third-party dependencies
|
|
1478
|
+
after every change.
|
|
1479
|
+
|
|
1480
|
+
uv workspaces are currently not supported.
|
|
1481
|
+
|
|
1444
1482
|
Added in v1.1.0.
|
|
1445
1483
|
"""
|
|
1446
1484
|
|
|
@@ -1681,6 +1719,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1681
1719
|
*commands: Union[str, list[str]],
|
|
1682
1720
|
env: Optional[dict[str, Optional[str]]] = None,
|
|
1683
1721
|
secrets: Optional[Collection[_Secret]] = None,
|
|
1722
|
+
volumes: Optional[dict[Union[str, PurePosixPath], _Volume]] = None,
|
|
1684
1723
|
gpu: GPU_T = None,
|
|
1685
1724
|
force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
|
|
1686
1725
|
) -> "_Image":
|
|
@@ -1703,6 +1742,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1703
1742
|
secrets=secrets,
|
|
1704
1743
|
gpu_config=parse_gpu_config(gpu),
|
|
1705
1744
|
force_build=self.force_build or force_build,
|
|
1745
|
+
validated_volumes=validate_only_modal_volumes(volumes, "Image.run_commands"),
|
|
1706
1746
|
)
|
|
1707
1747
|
|
|
1708
1748
|
@staticmethod
|
|
@@ -1713,9 +1753,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1713
1753
|
"""A Micromamba base image. Micromamba allows for fast building of small Conda-based containers."""
|
|
1714
1754
|
|
|
1715
1755
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
1716
|
-
nonlocal python_version
|
|
1717
|
-
if version == "2023.12" and python_version is None:
|
|
1718
|
-
python_version = "3.9" # Backcompat for old hardcoded default param
|
|
1719
1756
|
validated_python_version = _validate_python_version(python_version, version)
|
|
1720
1757
|
micromamba_version = _base_image_config("micromamba", version)
|
|
1721
1758
|
tag = f"mambaorg/micromamba:{micromamba_version}"
|
|
@@ -2270,8 +2307,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2270
2307
|
# It may be possible to support lambdas eventually, but for now we don't handle them well, so reject quickly
|
|
2271
2308
|
raise InvalidError("Image.run_function does not support lambda functions.")
|
|
2272
2309
|
|
|
2273
|
-
scheduler_placement = SchedulerPlacement(region=region) if region else None
|
|
2274
|
-
|
|
2275
2310
|
info = FunctionInfo(raw_f)
|
|
2276
2311
|
|
|
2277
2312
|
function = _Function.from_local(
|
|
@@ -2283,7 +2318,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2283
2318
|
volumes=volumes,
|
|
2284
2319
|
network_file_systems=network_file_systems,
|
|
2285
2320
|
cloud=cloud,
|
|
2286
|
-
|
|
2321
|
+
region=region,
|
|
2287
2322
|
memory=memory,
|
|
2288
2323
|
timeout=timeout,
|
|
2289
2324
|
cpu=cpu,
|
modal/image.pyi
CHANGED
|
@@ -124,6 +124,8 @@ async def _image_await_build_result(
|
|
|
124
124
|
image_id: str, client: modal.client._Client
|
|
125
125
|
) -> modal_proto.api_pb2.ImageJoinStreamingResponse: ...
|
|
126
126
|
|
|
127
|
+
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
128
|
+
|
|
127
129
|
class _Image(modal._object._Object):
|
|
128
130
|
"""Base class for container images to run functions in.
|
|
129
131
|
|
|
@@ -176,6 +178,7 @@ class _Image(modal._object._Object):
|
|
|
176
178
|
] = None,
|
|
177
179
|
force_build: bool = False,
|
|
178
180
|
build_args: dict[str, str] = {},
|
|
181
|
+
validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume._Volume]]] = None,
|
|
179
182
|
_namespace: int = 1,
|
|
180
183
|
_do_assert_no_mount_layers: bool = True,
|
|
181
184
|
): ...
|
|
@@ -310,13 +313,17 @@ class _Image(modal._object._Object):
|
|
|
310
313
|
"""
|
|
311
314
|
...
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
317
|
+
def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
|
|
318
|
+
"""Construct an Image from an id and look up the Image result.
|
|
316
319
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
The ID of an Image object can be accessed using `.object_id`.
|
|
321
|
+
"""
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None): ...
|
|
325
|
+
|
|
326
|
+
from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
|
|
320
327
|
|
|
321
328
|
async def build(self, app: modal.app._App) -> _Image:
|
|
322
329
|
"""Eagerly build an image.
|
|
@@ -329,8 +336,8 @@ class _Image(modal._object._Object):
|
|
|
329
336
|
```python
|
|
330
337
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
331
338
|
|
|
332
|
-
app = modal.App("build-image")
|
|
333
|
-
with modal.enable_output()
|
|
339
|
+
app = modal.App.lookup("build-image", create_if_missing=True)
|
|
340
|
+
with modal.enable_output(): # To see logs in your local terminal
|
|
334
341
|
image.build(app)
|
|
335
342
|
|
|
336
343
|
# Save the image id
|
|
@@ -343,7 +350,7 @@ class _Image(modal._object._Object):
|
|
|
343
350
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
344
351
|
|
|
345
352
|
```python notest
|
|
346
|
-
app = modal.App.lookup("sandbox-example")
|
|
353
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
347
354
|
|
|
348
355
|
with modal.enable_output():
|
|
349
356
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -587,6 +594,15 @@ class _Image(modal._object._Object):
|
|
|
587
594
|
The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context. The
|
|
588
595
|
`uv_project_dir` is relative to the current working directory of where `modal` is called.
|
|
589
596
|
|
|
597
|
+
NOTE: This does *not* install the project itself into the environment (this is equivalent to the
|
|
598
|
+
`--no-install-project` flag in the `uv sync` command) and you would be expected to add any local python source
|
|
599
|
+
files using `Image.add_local_python_source` or similar methods after this call.
|
|
600
|
+
|
|
601
|
+
This ensures that updates to your project code wouldn't require reinstalling third-party dependencies
|
|
602
|
+
after every change.
|
|
603
|
+
|
|
604
|
+
uv workspaces are currently not supported.
|
|
605
|
+
|
|
590
606
|
Added in v1.1.0.
|
|
591
607
|
"""
|
|
592
608
|
...
|
|
@@ -659,6 +675,7 @@ class _Image(modal._object._Object):
|
|
|
659
675
|
*commands: typing.Union[str, list[str]],
|
|
660
676
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
661
677
|
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
678
|
+
volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]] = None,
|
|
662
679
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
663
680
|
force_build: bool = False,
|
|
664
681
|
) -> _Image:
|
|
@@ -1024,8 +1041,6 @@ class _Image(modal._object._Object):
|
|
|
1024
1041
|
"""mdmd:hidden"""
|
|
1025
1042
|
...
|
|
1026
1043
|
|
|
1027
|
-
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
1028
|
-
|
|
1029
1044
|
class Image(modal.object.Object):
|
|
1030
1045
|
"""Base class for container images to run functions in.
|
|
1031
1046
|
|
|
@@ -1082,6 +1097,7 @@ class Image(modal.object.Object):
|
|
|
1082
1097
|
] = None,
|
|
1083
1098
|
force_build: bool = False,
|
|
1084
1099
|
build_args: dict[str, str] = {},
|
|
1100
|
+
validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume.Volume]]] = None,
|
|
1085
1101
|
_namespace: int = 1,
|
|
1086
1102
|
_do_assert_no_mount_layers: bool = True,
|
|
1087
1103
|
): ...
|
|
@@ -1216,24 +1232,19 @@ class Image(modal.object.Object):
|
|
|
1216
1232
|
"""
|
|
1217
1233
|
...
|
|
1218
1234
|
|
|
1219
|
-
class __from_id_spec(typing_extensions.Protocol):
|
|
1220
|
-
def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) ->
|
|
1235
|
+
class __from_id_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
1236
|
+
def __call__(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
|
|
1221
1237
|
"""Construct an Image from an id and look up the Image result.
|
|
1222
1238
|
|
|
1223
1239
|
The ID of an Image object can be accessed using `.object_id`.
|
|
1224
1240
|
"""
|
|
1225
1241
|
...
|
|
1226
1242
|
|
|
1227
|
-
async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None)
|
|
1228
|
-
"""Construct an Image from an id and look up the Image result.
|
|
1229
|
-
|
|
1230
|
-
The ID of an Image object can be accessed using `.object_id`.
|
|
1231
|
-
"""
|
|
1232
|
-
...
|
|
1243
|
+
async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None): ...
|
|
1233
1244
|
|
|
1234
|
-
from_id: __from_id_spec
|
|
1245
|
+
from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
|
|
1235
1246
|
|
|
1236
|
-
class __build_spec(typing_extensions.Protocol
|
|
1247
|
+
class __build_spec(typing_extensions.Protocol):
|
|
1237
1248
|
def __call__(self, /, app: modal.app.App) -> Image:
|
|
1238
1249
|
"""Eagerly build an image.
|
|
1239
1250
|
|
|
@@ -1245,8 +1256,8 @@ class Image(modal.object.Object):
|
|
|
1245
1256
|
```python
|
|
1246
1257
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
1247
1258
|
|
|
1248
|
-
app = modal.App("build-image")
|
|
1249
|
-
with modal.enable_output()
|
|
1259
|
+
app = modal.App.lookup("build-image", create_if_missing=True)
|
|
1260
|
+
with modal.enable_output(): # To see logs in your local terminal
|
|
1250
1261
|
image.build(app)
|
|
1251
1262
|
|
|
1252
1263
|
# Save the image id
|
|
@@ -1259,7 +1270,7 @@ class Image(modal.object.Object):
|
|
|
1259
1270
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
1260
1271
|
|
|
1261
1272
|
```python notest
|
|
1262
|
-
app = modal.App.lookup("sandbox-example")
|
|
1273
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
1263
1274
|
|
|
1264
1275
|
with modal.enable_output():
|
|
1265
1276
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -1298,8 +1309,8 @@ class Image(modal.object.Object):
|
|
|
1298
1309
|
```python
|
|
1299
1310
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
1300
1311
|
|
|
1301
|
-
app = modal.App("build-image")
|
|
1302
|
-
with modal.enable_output()
|
|
1312
|
+
app = modal.App.lookup("build-image", create_if_missing=True)
|
|
1313
|
+
with modal.enable_output(): # To see logs in your local terminal
|
|
1303
1314
|
image.build(app)
|
|
1304
1315
|
|
|
1305
1316
|
# Save the image id
|
|
@@ -1312,7 +1323,7 @@ class Image(modal.object.Object):
|
|
|
1312
1323
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
1313
1324
|
|
|
1314
1325
|
```python notest
|
|
1315
|
-
app = modal.App.lookup("sandbox-example")
|
|
1326
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
1316
1327
|
|
|
1317
1328
|
with modal.enable_output():
|
|
1318
1329
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -1340,7 +1351,7 @@ class Image(modal.object.Object):
|
|
|
1340
1351
|
"""
|
|
1341
1352
|
...
|
|
1342
1353
|
|
|
1343
|
-
build: __build_spec
|
|
1354
|
+
build: __build_spec
|
|
1344
1355
|
|
|
1345
1356
|
def pip_install(
|
|
1346
1357
|
self,
|
|
@@ -1558,6 +1569,15 @@ class Image(modal.object.Object):
|
|
|
1558
1569
|
The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context. The
|
|
1559
1570
|
`uv_project_dir` is relative to the current working directory of where `modal` is called.
|
|
1560
1571
|
|
|
1572
|
+
NOTE: This does *not* install the project itself into the environment (this is equivalent to the
|
|
1573
|
+
`--no-install-project` flag in the `uv sync` command) and you would be expected to add any local python source
|
|
1574
|
+
files using `Image.add_local_python_source` or similar methods after this call.
|
|
1575
|
+
|
|
1576
|
+
This ensures that updates to your project code wouldn't require reinstalling third-party dependencies
|
|
1577
|
+
after every change.
|
|
1578
|
+
|
|
1579
|
+
uv workspaces are currently not supported.
|
|
1580
|
+
|
|
1561
1581
|
Added in v1.1.0.
|
|
1562
1582
|
"""
|
|
1563
1583
|
...
|
|
@@ -1630,6 +1650,7 @@ class Image(modal.object.Object):
|
|
|
1630
1650
|
*commands: typing.Union[str, list[str]],
|
|
1631
1651
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1632
1652
|
secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
|
|
1653
|
+
volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume]] = None,
|
|
1633
1654
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
1634
1655
|
force_build: bool = False,
|
|
1635
1656
|
) -> Image:
|
|
@@ -1984,7 +2005,7 @@ class Image(modal.object.Object):
|
|
|
1984
2005
|
"""
|
|
1985
2006
|
...
|
|
1986
2007
|
|
|
1987
|
-
class ___logs_spec(typing_extensions.Protocol
|
|
2008
|
+
class ___logs_spec(typing_extensions.Protocol):
|
|
1988
2009
|
def __call__(self, /) -> typing.Generator[str, None, None]:
|
|
1989
2010
|
"""Streams logs from an image, or returns logs from an already completed image.
|
|
1990
2011
|
|
|
@@ -1999,7 +2020,7 @@ class Image(modal.object.Object):
|
|
|
1999
2020
|
"""
|
|
2000
2021
|
...
|
|
2001
2022
|
|
|
2002
|
-
_logs: ___logs_spec
|
|
2023
|
+
_logs: ___logs_spec
|
|
2003
2024
|
|
|
2004
2025
|
class __hydrate_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
2005
2026
|
def __call__(self, /, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
|