modal 1.0.3.dev10__py3-none-any.whl → 1.2.3.dev7__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 (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/mount.py CHANGED
@@ -8,23 +8,24 @@ import re
8
8
  import time
9
9
  import typing
10
10
  import warnings
11
- from collections.abc import AsyncGenerator
11
+ from collections.abc import AsyncGenerator, Generator
12
12
  from pathlib import Path, PurePosixPath
13
13
  from typing import Callable, Optional, Sequence, Union
14
14
 
15
15
  from google.protobuf.message import Message
16
+ from grpclib import GRPCError
16
17
 
17
18
  import modal.exception
18
19
  import modal.file_pattern_matcher
19
20
  from modal_proto import api_pb2
20
21
  from modal_version import __version__
21
22
 
22
- from ._object import _get_environment_name, _Object
23
+ from ._load_context import LoadContext
24
+ from ._object import _Object
23
25
  from ._resolver import Resolver
24
- from ._utils.async_utils import aclosing, async_map, synchronize_api
26
+ from ._utils.async_utils import TaskContext, aclosing, async_map, synchronize_api
25
27
  from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
26
- from ._utils.deprecation import deprecation_warning
27
- from ._utils.grpc_utils import retry_transient_errors
28
+ from ._utils.grpc_utils import Retry
28
29
  from ._utils.name_utils import check_object_name
29
30
  from ._utils.package_utils import get_module_mount_info
30
31
  from .client import _Client
@@ -115,7 +116,8 @@ class _MountFile(_MountEntry):
115
116
  def get_files_to_upload(self):
116
117
  local_file = self.local_file.resolve()
117
118
  if not local_file.exists():
118
- raise FileNotFoundError(local_file)
119
+ msg = f"local file {local_file} does not exist"
120
+ raise FileNotFoundError(msg)
119
121
 
120
122
  rel_filename = self.remote_path
121
123
  yield local_file, rel_filename
@@ -132,25 +134,48 @@ class _MountFile(_MountEntry):
132
134
  class _MountDir(_MountEntry):
133
135
  local_dir: Path
134
136
  remote_path: PurePosixPath
135
- ignore: Callable[[Path], bool]
137
+ ignore: Union[Callable[[Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher]
136
138
  recursive: bool
137
139
 
138
140
  def description(self):
139
141
  return str(self.local_dir.expanduser().absolute())
140
142
 
143
+ def _walk_and_prune(self, top_dir: Path) -> Generator[str, None, None]:
144
+ """Walk directories and prune ignored directories early."""
145
+ for root, dirs, files in os.walk(top_dir, topdown=True):
146
+ # with topdown=True, os.walk allows modifying the dirs list in-place, and will only
147
+ # recurse into dirs that are not ignored.
148
+ dirs[:] = [d for d in dirs if not self.ignore(Path(os.path.join(root, d)).relative_to(top_dir))]
149
+ for file in files:
150
+ yield os.path.join(root, file)
151
+
152
+ def _walk_all(self, top_dir: Path) -> Generator[str, None, None]:
153
+ """Walk all directories without early pruning - safe for complex/inverted ignore patterns."""
154
+ for root, _, files in os.walk(top_dir):
155
+ for file in files:
156
+ yield os.path.join(root, file)
157
+
141
158
  def get_files_to_upload(self):
142
159
  # we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
143
160
  # see test_mount_directory_with_symlinked_file
144
161
  local_dir = self.local_dir.expanduser().absolute()
145
162
 
146
163
  if not local_dir.exists():
147
- raise FileNotFoundError(local_dir)
164
+ msg = f"local dir {local_dir} does not exist"
165
+ raise FileNotFoundError(msg)
148
166
 
149
167
  if not local_dir.is_dir():
150
- raise NotADirectoryError(local_dir)
168
+ msg = f"local dir {local_dir} is not a directory"
169
+ raise NotADirectoryError(msg)
151
170
 
152
171
  if self.recursive:
153
- gen = (os.path.join(root, name) for root, dirs, files in os.walk(local_dir) for name in files)
172
+ if (
173
+ isinstance(self.ignore, modal.file_pattern_matcher._AbstractPatternMatcher)
174
+ and self.ignore.can_prune_directories()
175
+ ):
176
+ gen = self._walk_and_prune(local_dir)
177
+ else:
178
+ gen = self._walk_all(local_dir)
154
179
  else:
155
180
  gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
156
181
 
@@ -286,7 +311,8 @@ class _Mount(_Object, type_prefix="mo"):
286
311
  _entries: Optional[list[_MountEntry]] = None
287
312
  _deployment_name: Optional[str] = None
288
313
  _namespace: Optional[int] = None
289
- _environment_name: Optional[str] = None
314
+
315
+ _allow_overwrite: bool = False
290
316
  _content_checksum_sha256_hex: Optional[str] = None
291
317
 
292
318
  @staticmethod
@@ -300,7 +326,12 @@ class _Mount(_Object, type_prefix="mo"):
300
326
  return None
301
327
  return (_Mount._type_prefix, "local", frozenset(included_files))
302
328
 
303
- obj = _Mount._from_loader(_Mount._load_mount, rep, deduplication_key=mount_content_deduplication_key)
329
+ obj = _Mount._from_loader(
330
+ _Mount._load_mount,
331
+ rep,
332
+ deduplication_key=mount_content_deduplication_key,
333
+ load_context_overrides=LoadContext.empty(),
334
+ )
304
335
  obj._entries = entries
305
336
  obj._is_local = True
306
337
  return obj
@@ -386,39 +417,6 @@ class _Mount(_Object, type_prefix="mo"):
386
417
  ),
387
418
  )
388
419
 
389
- @staticmethod
390
- def from_local_dir(
391
- local_path: Union[str, Path],
392
- *,
393
- # Where the directory is placed within in the mount
394
- remote_path: Union[str, PurePosixPath, None] = None,
395
- # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
396
- # Defaults to including all files.
397
- condition: Optional[Callable[[str], bool]] = None,
398
- # add files from subdirectories as well
399
- recursive: bool = True,
400
- ) -> "_Mount":
401
- """
402
- **Deprecated:** Use image.add_local_dir() instead
403
-
404
- Create a `Mount` from a local directory.
405
-
406
- **Usage**
407
-
408
- ```python notest
409
- assets = modal.Mount.from_local_dir(
410
- "~/assets",
411
- condition=lambda pth: not ".venv" in pth,
412
- remote_path="/assets",
413
- )
414
- ```
415
- """
416
- deprecation_warning(
417
- (2025, 1, 8),
418
- MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_dir"),
419
- )
420
- return _Mount._from_local_dir(local_path, remote_path=remote_path, condition=condition, recursive=recursive)
421
-
422
420
  @staticmethod
423
421
  def _from_local_dir(
424
422
  local_path: Union[str, Path],
@@ -454,29 +452,6 @@ class _Mount(_Object, type_prefix="mo"):
454
452
  ),
455
453
  )
456
454
 
457
- @staticmethod
458
- def from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
459
- """
460
- **Deprecated**: Use image.add_local_file() instead
461
-
462
- Create a `Mount` mounting a single local file.
463
-
464
- **Usage**
465
-
466
- ```python notest
467
- # Mount the DBT profile in user's home directory into container.
468
- dbt_profiles = modal.Mount.from_local_file(
469
- local_path="~/profiles.yml",
470
- remote_path="/root/dbt_profile/profiles.yml",
471
- )
472
- ```
473
- """
474
- deprecation_warning(
475
- (2025, 1, 8),
476
- MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_file"),
477
- )
478
- return _Mount._from_local_file(local_path, remote_path)
479
-
480
455
  @staticmethod
481
456
  def _from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
482
457
  return _Mount._new().add_local_file(local_path, remote_path=remote_path)
@@ -508,6 +483,7 @@ class _Mount(_Object, type_prefix="mo"):
508
483
  async def _load_mount(
509
484
  self: "_Mount",
510
485
  resolver: Resolver,
486
+ load_context: LoadContext,
511
487
  existing_object_id: Optional[str],
512
488
  ):
513
489
  t0 = time.monotonic()
@@ -549,7 +525,7 @@ class _Mount(_Object, type_prefix="mo"):
549
525
 
550
526
  request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
551
527
  accounted_hashes.add(file_spec.sha256_hex)
552
- response = await retry_transient_errors(resolver.client.stub.MountPutFile, request, base_delay=1)
528
+ response = await load_context.client.stub.MountPutFile(request, retry=Retry(base_delay=1))
553
529
 
554
530
  if response.exists:
555
531
  n_finished += 1
@@ -563,7 +539,7 @@ class _Mount(_Object, type_prefix="mo"):
563
539
  async with blob_upload_concurrency:
564
540
  with file_spec.source() as fp:
565
541
  blob_id = await blob_upload_file(
566
- fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
542
+ fp, load_context.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
567
543
  )
568
544
  logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
569
545
  request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
@@ -575,7 +551,7 @@ class _Mount(_Object, type_prefix="mo"):
575
551
 
576
552
  start_time = time.monotonic()
577
553
  while time.monotonic() - start_time < MOUNT_PUT_FILE_CLIENT_TIMEOUT:
578
- response = await retry_transient_errors(resolver.client.stub.MountPutFile, request2, base_delay=1)
554
+ response = await load_context.client.stub.MountPutFile(request2, retry=Retry(base_delay=1))
579
555
  if response.exists:
580
556
  n_finished += 1
581
557
  return mount_file
@@ -583,7 +559,7 @@ class _Mount(_Object, type_prefix="mo"):
583
559
  raise modal.exception.MountUploadTimeoutError(f"Mounting of {file_spec.source_description} timed out")
584
560
 
585
561
  # Upload files, or check if they already exist.
586
- n_concurrent_uploads = 512
562
+ n_concurrent_uploads = 64
587
563
  files: list[api_pb2.MountFile] = []
588
564
  async with aclosing(
589
565
  async_map(_Mount._get_files(self._entries), _put_file, concurrency=n_concurrent_uploads)
@@ -597,70 +573,36 @@ class _Mount(_Object, type_prefix="mo"):
597
573
  # Build the mount.
598
574
  status_row.message(f"Creating mount {message_label}: Finalizing index of {len(files)} files")
599
575
  if self._deployment_name:
576
+ creation_type = (
577
+ api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
578
+ if self._allow_overwrite
579
+ else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
580
+ )
600
581
  req = api_pb2.MountGetOrCreateRequest(
601
582
  deployment_name=self._deployment_name,
602
583
  namespace=self._namespace,
603
- environment_name=self._environment_name,
604
- object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
584
+ environment_name=load_context.environment_name,
585
+ object_creation_type=creation_type,
605
586
  files=files,
606
587
  )
607
- elif resolver.app_id is not None:
588
+ elif load_context.app_id is not None:
608
589
  req = api_pb2.MountGetOrCreateRequest(
609
590
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
610
591
  files=files,
611
- app_id=resolver.app_id,
592
+ app_id=load_context.app_id,
612
593
  )
613
594
  else:
614
595
  req = api_pb2.MountGetOrCreateRequest(
615
596
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
616
597
  files=files,
617
- environment_name=resolver.environment_name,
598
+ environment_name=load_context.environment_name,
618
599
  )
619
600
 
620
- resp = await retry_transient_errors(resolver.client.stub.MountGetOrCreate, req, base_delay=1)
601
+ resp = await load_context.client.stub.MountGetOrCreate(req, retry=Retry(base_delay=1))
621
602
  status_row.finish(f"Created mount {message_label}")
622
603
 
623
604
  logger.debug(f"Uploaded {total_uploads} new files and {total_bytes} bytes in {time.monotonic() - t0}s")
624
- self._hydrate(resp.mount_id, resolver.client, resp.handle_metadata)
625
-
626
- @staticmethod
627
- def from_local_python_packages(
628
- *module_names: str,
629
- remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
630
- # Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
631
- # Defaults to including all files.
632
- condition: Optional[Callable[[str], bool]] = None,
633
- ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
634
- ) -> "_Mount":
635
- """
636
- **Deprecated**: Use image.add_local_python_source instead
637
-
638
- Returns a `modal.Mount` that makes local modules listed in `module_names` available inside the container.
639
- This works by mounting the local path of each module's package to a directory inside the container
640
- that's on `PYTHONPATH`.
641
-
642
- **Usage**
643
-
644
- ```python notest
645
- import modal
646
- import my_local_module
647
-
648
- app = modal.App()
649
-
650
- @app.function(mounts=[
651
- modal.Mount.from_local_python_packages("my_local_module", "my_other_module"),
652
- ])
653
- def f():
654
- my_local_module.do_stuff()
655
- ```
656
- """
657
- deprecation_warning(
658
- (2025, 1, 8),
659
- MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_python_source"),
660
- )
661
- return _Mount._from_local_python_packages(
662
- *module_names, remote_dir=remote_dir, condition=condition, ignore=ignore
663
- )
605
+ self._hydrate(resp.mount_id, load_context.client, resp.handle_metadata)
664
606
 
665
607
  @staticmethod
666
608
  def _from_local_python_packages(
@@ -693,58 +635,42 @@ class _Mount(_Object, type_prefix="mo"):
693
635
  *,
694
636
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
695
637
  environment_name: Optional[str] = None,
638
+ client: Optional[_Client] = None,
696
639
  ) -> "_Mount":
697
640
  """mdmd:hidden"""
698
641
 
699
- async def _load(provider: _Mount, resolver: Resolver, existing_object_id: Optional[str]):
642
+ async def _load(provider: _Mount, resolver: Resolver, load_context, existing_object_id: Optional[str]):
700
643
  req = api_pb2.MountGetOrCreateRequest(
701
644
  deployment_name=name,
702
645
  namespace=namespace,
703
- environment_name=_get_environment_name(environment_name, resolver),
646
+ environment_name=load_context.environment_name,
704
647
  )
705
- response = await resolver.client.stub.MountGetOrCreate(req)
706
- provider._hydrate(response.mount_id, resolver.client, response.handle_metadata)
707
-
708
- return _Mount._from_loader(_load, "Mount()", hydrate_lazily=True)
709
-
710
- @classmethod
711
- async def lookup(
712
- cls: type["_Mount"],
713
- name: str,
714
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
715
- client: Optional[_Client] = None,
716
- environment_name: Optional[str] = None,
717
- ) -> "_Mount":
718
- """mdmd:hidden"""
719
- deprecation_warning(
720
- (2025, 1, 27),
721
- "`modal.Mount.lookup` is deprecated and will be removed in a future release."
722
- " It can be replaced with `modal.Mount.from_name`."
723
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
648
+ response = await load_context.client.stub.MountGetOrCreate(req)
649
+ provider._hydrate(response.mount_id, load_context.client, response.handle_metadata)
650
+
651
+ return _Mount._from_loader(
652
+ _load,
653
+ "Mount()",
654
+ hydrate_lazily=True,
655
+ load_context_overrides=LoadContext(environment_name=environment_name, client=client),
724
656
  )
725
- obj = _Mount.from_name(name, namespace=namespace, environment_name=environment_name)
726
- if client is None:
727
- client = await _Client.from_env()
728
- resolver = Resolver(client=client)
729
- await resolver.load(obj)
730
- return obj
731
657
 
732
658
  async def _deploy(
733
659
  self: "_Mount",
734
660
  deployment_name: Optional[str] = None,
735
661
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
662
+ *,
736
663
  environment_name: Optional[str] = None,
664
+ allow_overwrite: bool = False,
737
665
  client: Optional[_Client] = None,
738
666
  ) -> None:
739
667
  check_object_name(deployment_name, "Mount")
740
- environment_name = _get_environment_name(environment_name, resolver=None)
741
668
  self._deployment_name = deployment_name
742
669
  self._namespace = namespace
743
- self._environment_name = environment_name
744
- if client is None:
745
- client = await _Client.from_env()
746
- resolver = Resolver(client=client, environment_name=environment_name)
747
- await resolver.load(self)
670
+ self._allow_overwrite = allow_overwrite
671
+ resolver = Resolver()
672
+ root_metadata = LoadContext(client=client, environment_name=environment_name)
673
+ await resolver.load(self, root_metadata)
748
674
 
749
675
  def _get_metadata(self) -> api_pb2.MountHandleMetadata:
750
676
  if self._content_checksum_sha256_hex is None:
@@ -810,3 +736,142 @@ def _is_modal_path(remote_path: PurePosixPath):
810
736
  if is_modal_path:
811
737
  return True
812
738
  return False
739
+
740
+
741
+ REMOTE_PACKAGES_PATH = "/__modal/deps"
742
+ REMOTE_SITECUSTOMIZE_PATH = "/pkg/sitecustomize.py"
743
+
744
+ SITECUSTOMIZE_CONTENT = f"""
745
+ # This file is automatically generated by Modal.
746
+ # It ensures that Modal's python dependencies are available in the Python PATH,
747
+ # while prioritizing user-installed packages.
748
+ import sys; sys.path.append('{REMOTE_PACKAGES_PATH}')
749
+ """.strip()
750
+
751
+
752
+ async def _create_single_client_dependency_mount(
753
+ client: _Client,
754
+ builder_version: str,
755
+ python_version: str,
756
+ arch: str,
757
+ platform: str,
758
+ uv_python_platform: str,
759
+ check_if_exists: bool = True,
760
+ allow_overwrite: bool = False,
761
+ dry_run: bool = False,
762
+ ):
763
+ import tempfile
764
+
765
+ profile_environment = config.get("environment")
766
+ abi_tag = "cp" + python_version.replace(".", "")
767
+ mount_name = f"{builder_version}-{abi_tag}-{platform}-{arch}"
768
+
769
+ if check_if_exists:
770
+ try:
771
+ await Mount.from_name(mount_name, namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL).hydrate.aio(client)
772
+ print(f"➖ Found existing mount {mount_name} in global namespace.")
773
+ return
774
+ except modal.exception.NotFoundError:
775
+ pass
776
+
777
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpd:
778
+ print(f"📦 Building {mount_name}.")
779
+ requirements = os.path.join(os.path.dirname(__file__), f"builder/{builder_version}.txt")
780
+ cmd = " ".join(
781
+ [
782
+ "uv",
783
+ "pip",
784
+ "install",
785
+ "--strict",
786
+ "--no-deps",
787
+ "--no-cache",
788
+ "-r",
789
+ requirements,
790
+ "--compile-bytecode",
791
+ "--target",
792
+ tmpd,
793
+ "--python-platform",
794
+ uv_python_platform,
795
+ "--python-version",
796
+ python_version,
797
+ ]
798
+ )
799
+ proc = await asyncio.create_subprocess_shell(
800
+ cmd,
801
+ stdout=asyncio.subprocess.PIPE,
802
+ stderr=asyncio.subprocess.PIPE,
803
+ )
804
+ await proc.wait()
805
+ if proc.returncode:
806
+ stdout, stderr = await proc.communicate()
807
+ print(stdout.decode("utf-8"))
808
+ print(stderr.decode("utf-8"))
809
+ raise RuntimeError(f"Subprocess failed with {proc.returncode}")
810
+
811
+ print(f"🌐 Downloaded and unpacked {mount_name} packages to {tmpd}.")
812
+
813
+ python_mount = Mount._from_local_dir(tmpd, remote_path=REMOTE_PACKAGES_PATH)
814
+
815
+ with tempfile.NamedTemporaryFile() as sitecustomize:
816
+ sitecustomize.write(
817
+ SITECUSTOMIZE_CONTENT.encode("utf-8"),
818
+ )
819
+ sitecustomize.flush()
820
+
821
+ python_mount = python_mount.add_local_file(
822
+ sitecustomize.name,
823
+ remote_path=REMOTE_SITECUSTOMIZE_PATH,
824
+ )
825
+
826
+ if not dry_run:
827
+ try:
828
+ await python_mount._deploy.aio(
829
+ mount_name,
830
+ api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
831
+ environment_name=profile_environment,
832
+ allow_overwrite=allow_overwrite,
833
+ client=client,
834
+ )
835
+ print(f"✅ Deployed mount {mount_name} to global namespace.")
836
+ except GRPCError as e:
837
+ print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
838
+ else:
839
+ print(f"Dry run - skipping deployment of mount {mount_name}")
840
+
841
+
842
+ async def _create_client_dependency_mounts(
843
+ client=None,
844
+ python_versions: list[str] = list(PYTHON_STANDALONE_VERSIONS),
845
+ builder_versions: list[str] = ["2025.06"], # Reenable "PREVIEW" during testing
846
+ check_if_exists=True,
847
+ dry_run=False,
848
+ ):
849
+ arch = "x86_64"
850
+ platform_tags = [
851
+ ("manylinux_2_17", f"{arch}-manylinux_2_17"), # glibc >= 2.17
852
+ ("musllinux_1_2", f"{arch}-unknown-linux-musl"), # musl >= 1.2
853
+ ]
854
+ coros = []
855
+ for python_version in python_versions:
856
+ for builder_version in builder_versions:
857
+ for platform, uv_python_platform in platform_tags:
858
+ coros.append(
859
+ _create_single_client_dependency_mount(
860
+ client,
861
+ builder_version,
862
+ python_version,
863
+ arch,
864
+ platform,
865
+ uv_python_platform,
866
+ # This check_if_exists / allow_overwrite parameterization is very awkward
867
+ # Also it doesn't provide a hook for overwriting a non-preview version, which
868
+ # in theory we may need to do at some point (hopefully not, but...)
869
+ check_if_exists=check_if_exists and builder_version != "PREVIEW",
870
+ allow_overwrite=builder_version == "PREVIEW",
871
+ dry_run=dry_run,
872
+ )
873
+ )
874
+ await TaskContext.gather(*coros)
875
+
876
+
877
+ create_client_dependency_mounts = synchronize_api(_create_client_dependency_mounts)