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.

Files changed (143) 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 +171 -138
  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 +30 -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/name_utils.py +2 -3
  31. modal/_utils/package_utils.py +0 -1
  32. modal/_utils/rand_pb_testing.py +8 -1
  33. modal/_utils/task_command_router_client.py +524 -0
  34. modal/_vendor/cloudpickle.py +144 -48
  35. modal/app.py +285 -105
  36. modal/app.pyi +216 -53
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +6 -3
  39. modal/builder/PREVIEW.txt +2 -1
  40. modal/builder/base-images.json +4 -2
  41. modal/cli/_download.py +19 -3
  42. modal/cli/cluster.py +4 -2
  43. modal/cli/config.py +3 -1
  44. modal/cli/container.py +5 -4
  45. modal/cli/dict.py +5 -2
  46. modal/cli/entry_point.py +26 -2
  47. modal/cli/environment.py +2 -16
  48. modal/cli/launch.py +1 -76
  49. modal/cli/network_file_system.py +5 -20
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/vscode.py +1 -1
  52. modal/cli/queues.py +5 -4
  53. modal/cli/run.py +24 -204
  54. modal/cli/secret.py +1 -2
  55. modal/cli/shell.py +375 -0
  56. modal/cli/utils.py +1 -13
  57. modal/cli/volume.py +11 -17
  58. modal/client.py +16 -125
  59. modal/client.pyi +94 -144
  60. modal/cloud_bucket_mount.py +3 -1
  61. modal/cloud_bucket_mount.pyi +4 -0
  62. modal/cls.py +101 -64
  63. modal/cls.pyi +9 -8
  64. modal/config.py +21 -1
  65. modal/container_process.py +288 -12
  66. modal/container_process.pyi +99 -38
  67. modal/dict.py +72 -33
  68. modal/dict.pyi +88 -57
  69. modal/environments.py +16 -8
  70. modal/environments.pyi +6 -2
  71. modal/exception.py +154 -16
  72. modal/experimental/__init__.py +24 -53
  73. modal/experimental/flash.py +161 -74
  74. modal/experimental/flash.pyi +97 -49
  75. modal/file_io.py +50 -92
  76. modal/file_io.pyi +117 -89
  77. modal/functions.pyi +70 -87
  78. modal/image.py +82 -47
  79. modal/image.pyi +51 -30
  80. modal/io_streams.py +500 -149
  81. modal/io_streams.pyi +279 -189
  82. modal/mount.py +60 -46
  83. modal/mount.pyi +41 -17
  84. modal/network_file_system.py +19 -11
  85. modal/network_file_system.pyi +72 -39
  86. modal/object.pyi +114 -22
  87. modal/parallel_map.py +42 -44
  88. modal/parallel_map.pyi +9 -17
  89. modal/partial_function.pyi +4 -2
  90. modal/proxy.py +14 -6
  91. modal/proxy.pyi +10 -2
  92. modal/queue.py +45 -38
  93. modal/queue.pyi +88 -52
  94. modal/runner.py +96 -96
  95. modal/runner.pyi +44 -27
  96. modal/sandbox.py +225 -107
  97. modal/sandbox.pyi +226 -60
  98. modal/secret.py +58 -56
  99. modal/secret.pyi +28 -13
  100. modal/serving.py +7 -11
  101. modal/serving.pyi +7 -8
  102. modal/snapshot.py +29 -15
  103. modal/snapshot.pyi +18 -10
  104. modal/token_flow.py +1 -1
  105. modal/token_flow.pyi +4 -6
  106. modal/volume.py +102 -55
  107. modal/volume.pyi +125 -66
  108. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  109. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  110. modal_proto/api.proto +141 -70
  111. modal_proto/api_grpc.py +42 -26
  112. modal_proto/api_pb2.py +1123 -1103
  113. modal_proto/api_pb2.pyi +331 -83
  114. modal_proto/api_pb2_grpc.py +80 -48
  115. modal_proto/api_pb2_grpc.pyi +26 -18
  116. modal_proto/modal_api_grpc.py +175 -174
  117. modal_proto/task_command_router.proto +164 -0
  118. modal_proto/task_command_router_grpc.py +138 -0
  119. modal_proto/task_command_router_pb2.py +180 -0
  120. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
  121. modal_proto/task_command_router_pb2_grpc.py +272 -0
  122. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  123. modal_version/__init__.py +1 -1
  124. modal_version/__main__.py +1 -1
  125. modal/cli/programs/launch_instance_ssh.py +0 -94
  126. modal/cli/programs/run_marimo.py +0 -95
  127. modal-1.1.5.dev66.dist-info/RECORD +0 -191
  128. modal_proto/modal_options_grpc.py +0 -3
  129. modal_proto/options.proto +0 -19
  130. modal_proto/options_grpc.py +0 -3
  131. modal_proto/options_pb2.py +0 -35
  132. modal_proto/options_pb2.pyi +0 -20
  133. modal_proto/options_pb2_grpc.py +0 -4
  134. modal_proto/options_pb2_grpc.pyi +0 -7
  135. modal_proto/sandbox_router.proto +0 -125
  136. modal_proto/sandbox_router_grpc.py +0 -89
  137. modal_proto/sandbox_router_pb2.py +0 -128
  138. modal_proto/sandbox_router_pb2_grpc.py +0 -169
  139. modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
  140. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  141. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  142. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  143. {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 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(
@@ -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
- scheduler_placement=scheduler_placement,
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
- @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")
@@ -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) -> 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:
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) -> Image:
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[SUPERSELF]):
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(), 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
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(), 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
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[typing_extensions.Self]
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[SUPERSELF]):
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[typing_extensions.Self]
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: