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.

Files changed (139) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +146 -121
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +26 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/package_utils.py +0 -1
  31. modal/_utils/rand_pb_testing.py +8 -1
  32. modal/_utils/task_command_router_client.py +524 -0
  33. modal/_vendor/cloudpickle.py +144 -48
  34. modal/app.py +215 -96
  35. modal/app.pyi +78 -37
  36. modal/billing.py +5 -0
  37. modal/builder/2025.06.txt +6 -3
  38. modal/builder/PREVIEW.txt +2 -1
  39. modal/builder/base-images.json +4 -2
  40. modal/cli/_download.py +19 -3
  41. modal/cli/cluster.py +4 -2
  42. modal/cli/config.py +3 -1
  43. modal/cli/container.py +5 -4
  44. modal/cli/dict.py +5 -2
  45. modal/cli/entry_point.py +26 -2
  46. modal/cli/environment.py +2 -16
  47. modal/cli/launch.py +1 -76
  48. modal/cli/network_file_system.py +5 -20
  49. modal/cli/queues.py +5 -4
  50. modal/cli/run.py +24 -204
  51. modal/cli/secret.py +1 -2
  52. modal/cli/shell.py +375 -0
  53. modal/cli/utils.py +1 -13
  54. modal/cli/volume.py +11 -17
  55. modal/client.py +16 -125
  56. modal/client.pyi +94 -144
  57. modal/cloud_bucket_mount.py +3 -1
  58. modal/cloud_bucket_mount.pyi +4 -0
  59. modal/cls.py +101 -64
  60. modal/cls.pyi +9 -8
  61. modal/config.py +21 -1
  62. modal/container_process.py +288 -12
  63. modal/container_process.pyi +99 -38
  64. modal/dict.py +72 -33
  65. modal/dict.pyi +88 -57
  66. modal/environments.py +16 -8
  67. modal/environments.pyi +6 -2
  68. modal/exception.py +154 -16
  69. modal/experimental/__init__.py +23 -5
  70. modal/experimental/flash.py +161 -74
  71. modal/experimental/flash.pyi +97 -49
  72. modal/file_io.py +50 -92
  73. modal/file_io.pyi +117 -89
  74. modal/functions.pyi +70 -87
  75. modal/image.py +73 -47
  76. modal/image.pyi +33 -30
  77. modal/io_streams.py +500 -149
  78. modal/io_streams.pyi +279 -189
  79. modal/mount.py +60 -45
  80. modal/mount.pyi +41 -17
  81. modal/network_file_system.py +19 -11
  82. modal/network_file_system.pyi +72 -39
  83. modal/object.pyi +114 -22
  84. modal/parallel_map.py +42 -44
  85. modal/parallel_map.pyi +9 -17
  86. modal/partial_function.pyi +4 -2
  87. modal/proxy.py +14 -6
  88. modal/proxy.pyi +10 -2
  89. modal/queue.py +45 -38
  90. modal/queue.pyi +88 -52
  91. modal/runner.py +96 -96
  92. modal/runner.pyi +44 -27
  93. modal/sandbox.py +225 -108
  94. modal/sandbox.pyi +226 -63
  95. modal/secret.py +58 -56
  96. modal/secret.pyi +28 -13
  97. modal/serving.py +7 -11
  98. modal/serving.pyi +7 -8
  99. modal/snapshot.py +29 -15
  100. modal/snapshot.pyi +18 -10
  101. modal/token_flow.py +1 -1
  102. modal/token_flow.pyi +4 -6
  103. modal/volume.py +102 -55
  104. modal/volume.pyi +125 -66
  105. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  106. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  107. modal_proto/api.proto +86 -30
  108. modal_proto/api_grpc.py +10 -25
  109. modal_proto/api_pb2.py +1080 -1047
  110. modal_proto/api_pb2.pyi +253 -79
  111. modal_proto/api_pb2_grpc.py +14 -48
  112. modal_proto/api_pb2_grpc.pyi +6 -18
  113. modal_proto/modal_api_grpc.py +175 -176
  114. modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
  115. modal_proto/task_command_router_grpc.py +138 -0
  116. modal_proto/task_command_router_pb2.py +180 -0
  117. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
  118. modal_proto/task_command_router_pb2_grpc.py +272 -0
  119. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  120. modal_version/__init__.py +1 -1
  121. modal_version/__main__.py +1 -1
  122. modal/cli/programs/launch_instance_ssh.py +0 -94
  123. modal/cli/programs/run_marimo.py +0 -95
  124. modal-1.1.5.dev83.dist-info/RECORD +0 -191
  125. modal_proto/modal_options_grpc.py +0 -3
  126. modal_proto/options.proto +0 -19
  127. modal_proto/options_grpc.py +0 -3
  128. modal_proto/options_pb2.py +0 -35
  129. modal_proto/options_pb2.pyi +0 -20
  130. modal_proto/options_pb2_grpc.py +0 -4
  131. modal_proto/options_pb2_grpc.pyi +0 -7
  132. modal_proto/sandbox_router_grpc.py +0 -105
  133. modal_proto/sandbox_router_pb2.py +0 -148
  134. modal_proto/sandbox_router_pb2_grpc.py +0 -203
  135. modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
  136. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  137. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  138. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  139. {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 GRPCError, StreamTerminatedError
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.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
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 ExecutionError, InvalidError, NotFoundError, RemoteError, VersionError
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.9", "3.10", "3.11", "3.12", "3.13"],
68
- "2025.06": ["3.9", "3.10", "3.11", "3.12", "3.13"],
69
- "2024.10": ["3.9", "3.10", "3.11", "3.12", "3.13"],
70
- "2024.04": ["3.9", "3.10", "3.11", "3.12"],
71
- "2023.12": ["3.9", "3.10", "3.11", "3.12"],
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 (StreamTerminatedError, GRPCError) as exc:
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(self2: "_Image", resolver: Resolver, existing_object_id: Optional[str]):
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(_load, "Image(local files)", deps=lambda: [base_image, mount])
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 resolver.app_id # type narrowing
530
- environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
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=resolver.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 retry_transient_errors(resolver.client.stub.ImageGetOrCreate, req)
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, resolver.client)
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, resolver.client, metadata)
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
- @staticmethod
842
- async def from_id(image_id: str, client: Optional[_Client] = None) -> "_Image":
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
- if client is None:
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 retry_transient_errors(client.stub.ImageFromId, api_pb2.ImageFromIdRequest(image_id=image_id))
852
- self._hydrate(resp.image_id, resolver.client, resp.metadata)
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
- return obj
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(), app.run():
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
- app_id = app.app_id
915
- app_client = app._client or await _Client.from_env()
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
- scheduler_placement=scheduler_placement,
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
- @staticmethod
314
- async def from_id(image_id: str, client: typing.Optional[modal.client._Client] = None) -> _Image:
315
- """Construct an Image from an id and look up the Image result.
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
- The ID of an Image object can be accessed using `.object_id`.
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(), app.run():
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) -> Image:
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) -> Image:
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[SUPERSELF]):
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(), app.run():
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(), app.run():
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[typing_extensions.Self]
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[SUPERSELF]):
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[typing_extensions.Self]
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: