modal 1.1.5.dev83__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 +146 -121
- 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 +26 -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/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 +215 -96
- modal/app.pyi +78 -37
- 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/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 +23 -5
- 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 +73 -47
- modal/image.pyi +33 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -45
- 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 -108
- modal/sandbox.pyi +226 -63
- 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.dev83.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 +86 -30
- modal_proto/api_grpc.py +10 -25
- modal_proto/api_pb2.py +1080 -1047
- modal_proto/api_pb2.pyi +253 -79
- modal_proto/api_pb2_grpc.py +14 -48
- modal_proto/api_pb2_grpc.pyi +6 -18
- modal_proto/modal_api_grpc.py +175 -176
- modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
- 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} +110 -63
- 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.dev83.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_grpc.py +0 -105
- modal_proto/sandbox_router_pb2.py +0 -148
- modal_proto/sandbox_router_pb2_grpc.py +0 -203
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev83.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(
|
|
@@ -1690,6 +1719,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1690
1719
|
*commands: Union[str, list[str]],
|
|
1691
1720
|
env: Optional[dict[str, Optional[str]]] = None,
|
|
1692
1721
|
secrets: Optional[Collection[_Secret]] = None,
|
|
1722
|
+
volumes: Optional[dict[Union[str, PurePosixPath], _Volume]] = None,
|
|
1693
1723
|
gpu: GPU_T = None,
|
|
1694
1724
|
force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
|
|
1695
1725
|
) -> "_Image":
|
|
@@ -1712,6 +1742,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1712
1742
|
secrets=secrets,
|
|
1713
1743
|
gpu_config=parse_gpu_config(gpu),
|
|
1714
1744
|
force_build=self.force_build or force_build,
|
|
1745
|
+
validated_volumes=validate_only_modal_volumes(volumes, "Image.run_commands"),
|
|
1715
1746
|
)
|
|
1716
1747
|
|
|
1717
1748
|
@staticmethod
|
|
@@ -1722,9 +1753,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
1722
1753
|
"""A Micromamba base image. Micromamba allows for fast building of small Conda-based containers."""
|
|
1723
1754
|
|
|
1724
1755
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
|
1725
|
-
nonlocal python_version
|
|
1726
|
-
if version == "2023.12" and python_version is None:
|
|
1727
|
-
python_version = "3.9" # Backcompat for old hardcoded default param
|
|
1728
1756
|
validated_python_version = _validate_python_version(python_version, version)
|
|
1729
1757
|
micromamba_version = _base_image_config("micromamba", version)
|
|
1730
1758
|
tag = f"mambaorg/micromamba:{micromamba_version}"
|
|
@@ -2279,8 +2307,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2279
2307
|
# It may be possible to support lambdas eventually, but for now we don't handle them well, so reject quickly
|
|
2280
2308
|
raise InvalidError("Image.run_function does not support lambda functions.")
|
|
2281
2309
|
|
|
2282
|
-
scheduler_placement = SchedulerPlacement(region=region) if region else None
|
|
2283
|
-
|
|
2284
2310
|
info = FunctionInfo(raw_f)
|
|
2285
2311
|
|
|
2286
2312
|
function = _Function.from_local(
|
|
@@ -2292,7 +2318,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2292
2318
|
volumes=volumes,
|
|
2293
2319
|
network_file_systems=network_file_systems,
|
|
2294
2320
|
cloud=cloud,
|
|
2295
|
-
|
|
2321
|
+
region=region,
|
|
2296
2322
|
memory=memory,
|
|
2297
2323
|
timeout=timeout,
|
|
2298
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")
|
|
@@ -668,6 +675,7 @@ class _Image(modal._object._Object):
|
|
|
668
675
|
*commands: typing.Union[str, list[str]],
|
|
669
676
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
670
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,
|
|
671
679
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
672
680
|
force_build: bool = False,
|
|
673
681
|
) -> _Image:
|
|
@@ -1033,8 +1041,6 @@ class _Image(modal._object._Object):
|
|
|
1033
1041
|
"""mdmd:hidden"""
|
|
1034
1042
|
...
|
|
1035
1043
|
|
|
1036
|
-
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
|
1037
|
-
|
|
1038
1044
|
class Image(modal.object.Object):
|
|
1039
1045
|
"""Base class for container images to run functions in.
|
|
1040
1046
|
|
|
@@ -1091,6 +1097,7 @@ class Image(modal.object.Object):
|
|
|
1091
1097
|
] = None,
|
|
1092
1098
|
force_build: bool = False,
|
|
1093
1099
|
build_args: dict[str, str] = {},
|
|
1100
|
+
validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume.Volume]]] = None,
|
|
1094
1101
|
_namespace: int = 1,
|
|
1095
1102
|
_do_assert_no_mount_layers: bool = True,
|
|
1096
1103
|
): ...
|
|
@@ -1225,24 +1232,19 @@ class Image(modal.object.Object):
|
|
|
1225
1232
|
"""
|
|
1226
1233
|
...
|
|
1227
1234
|
|
|
1228
|
-
class __from_id_spec(typing_extensions.Protocol):
|
|
1229
|
-
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:
|
|
1230
1237
|
"""Construct an Image from an id and look up the Image result.
|
|
1231
1238
|
|
|
1232
1239
|
The ID of an Image object can be accessed using `.object_id`.
|
|
1233
1240
|
"""
|
|
1234
1241
|
...
|
|
1235
1242
|
|
|
1236
|
-
async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None)
|
|
1237
|
-
"""Construct an Image from an id and look up the Image result.
|
|
1238
|
-
|
|
1239
|
-
The ID of an Image object can be accessed using `.object_id`.
|
|
1240
|
-
"""
|
|
1241
|
-
...
|
|
1243
|
+
async def aio(self, /, image_id: str, client: typing.Optional[modal.client.Client] = None): ...
|
|
1242
1244
|
|
|
1243
|
-
from_id: __from_id_spec
|
|
1245
|
+
from_id: typing.ClassVar[__from_id_spec[typing_extensions.Self]]
|
|
1244
1246
|
|
|
1245
|
-
class __build_spec(typing_extensions.Protocol
|
|
1247
|
+
class __build_spec(typing_extensions.Protocol):
|
|
1246
1248
|
def __call__(self, /, app: modal.app.App) -> Image:
|
|
1247
1249
|
"""Eagerly build an image.
|
|
1248
1250
|
|
|
@@ -1254,8 +1256,8 @@ class Image(modal.object.Object):
|
|
|
1254
1256
|
```python
|
|
1255
1257
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
1256
1258
|
|
|
1257
|
-
app = modal.App("build-image")
|
|
1258
|
-
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
|
|
1259
1261
|
image.build(app)
|
|
1260
1262
|
|
|
1261
1263
|
# Save the image id
|
|
@@ -1268,7 +1270,7 @@ class Image(modal.object.Object):
|
|
|
1268
1270
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
1269
1271
|
|
|
1270
1272
|
```python notest
|
|
1271
|
-
app = modal.App.lookup("sandbox-example")
|
|
1273
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
1272
1274
|
|
|
1273
1275
|
with modal.enable_output():
|
|
1274
1276
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -1307,8 +1309,8 @@ class Image(modal.object.Object):
|
|
|
1307
1309
|
```python
|
|
1308
1310
|
image = modal.Image.debian_slim().uv_pip_install("scipy", "numpy")
|
|
1309
1311
|
|
|
1310
|
-
app = modal.App("build-image")
|
|
1311
|
-
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
|
|
1312
1314
|
image.build(app)
|
|
1313
1315
|
|
|
1314
1316
|
# Save the image id
|
|
@@ -1321,7 +1323,7 @@ class Image(modal.object.Object):
|
|
|
1321
1323
|
Alternatively, you can pre-build a image and use it in a sandbox.
|
|
1322
1324
|
|
|
1323
1325
|
```python notest
|
|
1324
|
-
app = modal.App.lookup("sandbox-example")
|
|
1326
|
+
app = modal.App.lookup("sandbox-example", create_if_missing=True)
|
|
1325
1327
|
|
|
1326
1328
|
with modal.enable_output():
|
|
1327
1329
|
image = modal.Image.debian_slim().uv_pip_install("scipy")
|
|
@@ -1349,7 +1351,7 @@ class Image(modal.object.Object):
|
|
|
1349
1351
|
"""
|
|
1350
1352
|
...
|
|
1351
1353
|
|
|
1352
|
-
build: __build_spec
|
|
1354
|
+
build: __build_spec
|
|
1353
1355
|
|
|
1354
1356
|
def pip_install(
|
|
1355
1357
|
self,
|
|
@@ -1648,6 +1650,7 @@ class Image(modal.object.Object):
|
|
|
1648
1650
|
*commands: typing.Union[str, list[str]],
|
|
1649
1651
|
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1650
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,
|
|
1651
1654
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
|
1652
1655
|
force_build: bool = False,
|
|
1653
1656
|
) -> Image:
|
|
@@ -2002,7 +2005,7 @@ class Image(modal.object.Object):
|
|
|
2002
2005
|
"""
|
|
2003
2006
|
...
|
|
2004
2007
|
|
|
2005
|
-
class ___logs_spec(typing_extensions.Protocol
|
|
2008
|
+
class ___logs_spec(typing_extensions.Protocol):
|
|
2006
2009
|
def __call__(self, /) -> typing.Generator[str, None, None]:
|
|
2007
2010
|
"""Streams logs from an image, or returns logs from an already completed image.
|
|
2008
2011
|
|
|
@@ -2017,7 +2020,7 @@ class Image(modal.object.Object):
|
|
|
2017
2020
|
"""
|
|
2018
2021
|
...
|
|
2019
2022
|
|
|
2020
|
-
_logs: ___logs_spec
|
|
2023
|
+
_logs: ___logs_spec
|
|
2021
2024
|
|
|
2022
2025
|
class __hydrate_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
2023
2026
|
def __call__(self, /, client: typing.Optional[modal.client.Client] = None) -> SUPERSELF:
|