modal 0.66.17__py3-none-any.whl → 0.66.39__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.
modal/functions.py CHANGED
@@ -299,13 +299,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
299
299
  """Functions are the basic units of serverless execution on Modal.
300
300
 
301
301
  Generally, you will not construct a `Function` directly. Instead, use the
302
- `@app.function()` decorator on the `App` object (formerly called "Stub")
303
- for your application.
302
+ `App.function()` decorator to register your Python functions with your App.
304
303
  """
305
304
 
306
305
  # TODO: more type annotations
307
306
  _info: Optional[FunctionInfo]
308
- _used_local_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
307
+ _serve_mounts: typing.FrozenSet[_Mount] # set at load time, only by loader
309
308
  _app: Optional["modal.app._App"] = None
310
309
  _obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
311
310
  _web_url: Optional[str]
@@ -580,6 +579,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
580
579
 
581
580
  if is_local():
582
581
  entrypoint_mounts = info.get_entrypoint_mount()
582
+
583
583
  all_mounts = [
584
584
  _get_client_mount(),
585
585
  *explicit_mounts,
@@ -612,6 +612,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
612
612
  if proxy:
613
613
  # HACK: remove this once we stop using ssh tunnels for this.
614
614
  if image:
615
+ # TODO(elias): this will cause an error if users use prior `.add_local_*` commands without copy=True
615
616
  image = image.apt_install("autossh")
616
617
 
617
618
  function_spec = _FunctionSpec(
@@ -828,7 +829,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
828
829
  )
829
830
  for path, volume in validated_volumes
830
831
  ]
831
- loaded_mount_ids = {m.object_id for m in all_mounts}
832
+ loaded_mount_ids = {m.object_id for m in all_mounts} | {m.object_id for m in image._mount_layers}
832
833
 
833
834
  # Get object dependencies
834
835
  object_dependencies = []
@@ -970,9 +971,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
970
971
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
971
972
  raise
972
973
  function_creation_status.set_response(response)
973
- local_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
974
- local_mounts |= image._used_local_mounts
975
- obj._used_local_mounts = frozenset(local_mounts)
974
+ serve_mounts = set(m for m in all_mounts if m.is_local()) # needed for modal.serve file watching
975
+ serve_mounts |= image._serve_mounts
976
+ obj._serve_mounts = frozenset(serve_mounts)
976
977
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
977
978
 
978
979
  rep = f"Function({tag})"
@@ -1028,7 +1029,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1028
1029
  identity = "class service function for a parameterized class"
1029
1030
  if not self._parent.is_hydrated:
1030
1031
  if self._parent.app._running_app is None:
1031
- reason = ", because the App it is defined on is not running."
1032
+ reason = ", because the App it is defined on is not running"
1032
1033
  else:
1033
1034
  reason = ""
1034
1035
  raise ExecutionError(
@@ -1223,7 +1224,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1223
1224
  self._function_name = None
1224
1225
  self._info = None
1225
1226
  self._use_function_id = ""
1226
- self._used_local_mounts = frozenset()
1227
+ self._serve_mounts = frozenset()
1227
1228
 
1228
1229
  def _hydrate_metadata(self, metadata: Optional[Message]):
1229
1230
  # Overridden concrete implementation of base class method
modal/functions.pyi CHANGED
@@ -110,7 +110,7 @@ OriginalReturnType = typing.TypeVar("OriginalReturnType", covariant=True)
110
110
 
111
111
  class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object._Object):
112
112
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
113
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
113
+ _serve_mounts: typing.FrozenSet[modal.mount._Mount]
114
114
  _app: typing.Optional[modal.app._App]
115
115
  _obj: typing.Optional[modal.cls._Obj]
116
116
  _web_url: typing.Optional[str]
@@ -286,7 +286,7 @@ P_INNER = typing_extensions.ParamSpec("P_INNER")
286
286
 
287
287
  class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.Object):
288
288
  _info: typing.Optional[modal._utils.function_utils.FunctionInfo]
289
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
289
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
290
290
  _app: typing.Optional[modal.app.App]
291
291
  _obj: typing.Optional[modal.cls.Obj]
292
292
  _web_url: typing.Optional[str]
@@ -450,11 +450,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
450
450
 
451
451
  _call_generator_nowait: ___call_generator_nowait_spec
452
452
 
453
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
453
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
454
454
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
455
455
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
456
456
 
457
- remote: __remote_spec[P, ReturnType]
457
+ remote: __remote_spec[ReturnType, P]
458
458
 
459
459
  class __remote_gen_spec(typing_extensions.Protocol):
460
460
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -466,17 +466,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
466
466
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
467
467
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
468
468
 
469
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
469
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
470
470
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
471
471
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
472
472
 
473
- _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
473
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
474
474
 
475
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
475
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
476
476
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
477
477
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
478
478
 
479
- spawn: __spawn_spec[P, ReturnType]
479
+ spawn: __spawn_spec[ReturnType, P]
480
480
 
481
481
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
482
482
 
modal/gpu.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from dataclasses import dataclass
3
- from typing import Optional, Union
3
+ from typing import Callable, Optional, Union
4
4
 
5
5
  from modal_proto import api_pb2
6
6
 
@@ -147,25 +147,27 @@ class Any(_GPUConfig):
147
147
  return f"GPU(Any, count={self.count})"
148
148
 
149
149
 
150
- STRING_TO_GPU_CONFIG = {
150
+ STRING_TO_GPU_CONFIG: dict[str, Callable] = {
151
151
  "t4": T4,
152
152
  "l4": L4,
153
153
  "a100": A100,
154
+ "a100-80gb": lambda: A100(size="80GB"),
154
155
  "h100": H100,
155
156
  "a10g": A10G,
156
157
  "any": Any,
157
158
  }
158
- display_string_to_config = "\n".join(
159
- f'- "{key}" → `{cls()}`' for key, cls in STRING_TO_GPU_CONFIG.items() if key != "inf2"
160
- )
159
+ display_string_to_config = "\n".join(f'- "{key}" → `{c()}`' for key, c in STRING_TO_GPU_CONFIG.items() if key != "inf2")
161
160
  __doc__ = f"""
162
161
  **GPU configuration shortcodes**
163
162
 
164
163
  The following are the valid `str` values for the `gpu` parameter of
165
- [`@app.function`](/docs/reference/modal.Stub#function).
164
+ [`@app.function`](/docs/reference/modal.App#function).
166
165
 
167
166
  {display_string_to_config}
168
167
 
168
+ The shortcodes also support specifying count by suffixing `:N` to acquire `N` GPUs.
169
+ For example, `a10g:4` will provision 4 A10G GPUs.
170
+
169
171
  Other configurations can be created using the constructors documented below.
170
172
  """
171
173
 
modal/image.py CHANGED
@@ -273,12 +273,24 @@ class _Image(_Object, type_prefix="im"):
273
273
 
274
274
  force_build: bool
275
275
  inside_exceptions: List[Exception]
276
- _used_local_mounts: typing.FrozenSet[_Mount] # used for mounts watching
276
+ _serve_mounts: typing.FrozenSet[_Mount] # used for mounts watching in `modal serve`
277
+ _deferred_mounts: Sequence[
278
+ _Mount
279
+ ] # added as mounts on any container referencing the Image, see `def _mount_layers`
277
280
  _metadata: Optional[api_pb2.ImageMetadata] = None # set on hydration, private for now
278
281
 
279
282
  def _initialize_from_empty(self):
280
283
  self.inside_exceptions = []
281
- self._used_local_mounts = frozenset()
284
+ self._serve_mounts = frozenset()
285
+ self._deferred_mounts = ()
286
+ self.force_build = False
287
+
288
+ def _initialize_from_other(self, other: "_Image"):
289
+ # used by .clone()
290
+ self.inside_exceptions = other.inside_exceptions
291
+ self.force_build = other.force_build
292
+ self._serve_mounts = other._serve_mounts
293
+ self._deferred_mounts = other._deferred_mounts
282
294
 
283
295
  def _hydrate_metadata(self, message: Optional[Message]):
284
296
  env_image_id = config.get("image_id") # set as an env var in containers
@@ -292,6 +304,51 @@ class _Image(_Object, type_prefix="im"):
292
304
  assert isinstance(message, api_pb2.ImageMetadata)
293
305
  self._metadata = message
294
306
 
307
+ def _add_mount_layer_or_copy(self, mount: _Mount, copy: bool = False):
308
+ if copy:
309
+ return self.copy_mount(mount, remote_path="/")
310
+
311
+ base_image = self
312
+
313
+ async def _load(self2: "_Image", resolver: Resolver, existing_object_id: Optional[str]):
314
+ self2._hydrate_from_other(base_image) # same image id as base image as long as it's lazy
315
+ self2._deferred_mounts = tuple(base_image._deferred_mounts) + (mount,)
316
+ self2._serve_mounts = base_image._serve_mounts | ({mount} if mount.is_local() else set())
317
+
318
+ return _Image._from_loader(_load, "Image(local files)", deps=lambda: [base_image, mount])
319
+
320
+ @property
321
+ def _mount_layers(self) -> typing.Tuple[_Mount]:
322
+ """Non-evaluated mount layers on the image
323
+
324
+ When the image is used by a Modal container, these mounts need to be attached as well to
325
+ represent the full image content, as they haven't yet been represented as a layer in the
326
+ image.
327
+
328
+ When the image is used as a base image for a new layer (that is not itself a mount layer)
329
+ these mounts need to first be inserted as a copy operation (.copy_mount) into the image.
330
+ """
331
+ return self._deferred_mounts
332
+
333
+ def _assert_no_mount_layers(self):
334
+ if self._mount_layers:
335
+ raise InvalidError(
336
+ "An image tried to run a build step after using `image.add_local_*` to include local files.\n"
337
+ "\n"
338
+ "Run `image.add_local_*` commands last in your image build to avoid rebuilding images with every local "
339
+ "file change. Modal will then add these files to containers on startup instead, saving build time.\n"
340
+ "If you need to run other build steps after adding local files, set `copy=True` to copy the files"
341
+ "directly into the image, at the expense of some added build time.\n"
342
+ "\n"
343
+ "Example:\n"
344
+ "\n"
345
+ "my_image = (\n"
346
+ " Image.debian_slim()\n"
347
+ ' .add_local_python_packages("mypak", copy=True)\n'
348
+ ' .run_commands("python -m mypak") # this now works!\n'
349
+ ")\n"
350
+ )
351
+
295
352
  @staticmethod
296
353
  def _from_args(
297
354
  *,
@@ -306,9 +363,11 @@ class _Image(_Object, type_prefix="im"):
306
363
  force_build: bool = False,
307
364
  # For internal use only.
308
365
  _namespace: int = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
366
+ _do_assert_no_mount_layers: bool = True,
309
367
  ):
310
368
  if base_images is None:
311
369
  base_images = {}
370
+
312
371
  if secrets is None:
313
372
  secrets = []
314
373
  if gpu_config is None:
@@ -334,6 +393,11 @@ class _Image(_Object, type_prefix="im"):
334
393
  return deps
335
394
 
336
395
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
396
+ if _do_assert_no_mount_layers:
397
+ for image in base_images.values():
398
+ # base images can't have
399
+ image._assert_no_mount_layers()
400
+
337
401
  environment = await _get_environment_cached(resolver.environment_name or "", resolver.client)
338
402
  # A bit hacky,but assume that the environment provides a valid builder version
339
403
  image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
@@ -349,7 +413,9 @@ class _Image(_Object, type_prefix="im"):
349
413
  "No commands were provided for the image — have you tried using modal.Image.debian_slim()?"
350
414
  )
351
415
  if dockerfile.commands and build_function:
352
- raise InvalidError("Cannot provide both a build function and Dockerfile commands!")
416
+ raise InvalidError(
417
+ "Cannot provide both build function and Dockerfile commands in the same image layer!"
418
+ )
353
419
 
354
420
  base_images_pb2s = [
355
421
  api_pb2.BaseImage(
@@ -482,12 +548,12 @@ class _Image(_Object, type_prefix="im"):
482
548
  self._hydrate(image_id, resolver.client, result_response.metadata)
483
549
  local_mounts = set()
484
550
  for base in base_images.values():
485
- local_mounts |= base._used_local_mounts
551
+ local_mounts |= base._serve_mounts
486
552
  if context_mount and context_mount.is_local():
487
553
  local_mounts.add(context_mount)
488
- self._used_local_mounts = frozenset(local_mounts)
554
+ self._serve_mounts = frozenset(local_mounts)
489
555
 
490
- rep = "Image()"
556
+ rep = f"Image({dockerfile_function})"
491
557
  obj = _Image._from_loader(_load, rep, deps=_deps)
492
558
  obj.force_build = force_build
493
559
  return obj
@@ -553,6 +619,27 @@ class _Image(_Object, type_prefix="im"):
553
619
  context_mount=mount,
554
620
  )
555
621
 
622
+ def _add_local_python_packages(self, *packages: Union[str, Path], copy: bool = False) -> "_Image":
623
+ """Adds Python package files to containers
624
+
625
+ Adds all files from the specified Python packages to containers running the Image.
626
+
627
+ Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
628
+ of any executed Modal Functions.
629
+
630
+ By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
631
+ which speeds up deployment.
632
+
633
+ Set `copy=True` to copy the files into an Image layer at build time instead. This can slow down iteration since
634
+ it requires a rebuild of the Image and any subsequent build steps whenever the included files change, but it is
635
+ required if you want to run additional build steps after this one.
636
+
637
+ **Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
638
+ To add full directories with finer control, use `.add_local_dir()` instead.
639
+ """
640
+ mount = _Mount.from_local_python_packages(*packages)
641
+ return self._add_mount_layer_or_copy(mount, copy=copy)
642
+
556
643
  def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
557
644
  """Copy a directory into the image as a part of building the image.
558
645
 
modal/image.pyi CHANGED
@@ -58,11 +58,17 @@ class DockerfileSpec:
58
58
  class _Image(modal.object._Object):
59
59
  force_build: bool
60
60
  inside_exceptions: typing.List[Exception]
61
- _used_local_mounts: typing.FrozenSet[modal.mount._Mount]
61
+ _serve_mounts: typing.FrozenSet[modal.mount._Mount]
62
+ _deferred_mounts: typing.Sequence[modal.mount._Mount]
62
63
  _metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
63
64
 
64
65
  def _initialize_from_empty(self): ...
66
+ def _initialize_from_other(self, other: _Image): ...
65
67
  def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
68
+ def _add_mount_layer_or_copy(self, mount: modal.mount._Mount, copy: bool = False): ...
69
+ @property
70
+ def _mount_layers(self) -> typing.Tuple[modal.mount._Mount]: ...
71
+ def _assert_no_mount_layers(self): ...
66
72
  @staticmethod
67
73
  def _from_args(
68
74
  *,
@@ -78,6 +84,7 @@ class _Image(modal.object._Object):
78
84
  context_mount: typing.Optional[modal.mount._Mount] = None,
79
85
  force_build: bool = False,
80
86
  _namespace: int = 1,
87
+ _do_assert_no_mount_layers: bool = True,
81
88
  ): ...
82
89
  def extend(
83
90
  self,
@@ -90,11 +97,13 @@ class _Image(modal.object._Object):
90
97
  context_mount: typing.Optional[modal.mount._Mount] = None,
91
98
  force_build: bool = False,
92
99
  _namespace: int = 1,
100
+ _do_assert_no_mount_layers: bool = True,
93
101
  ) -> _Image: ...
94
102
  def copy_mount(self, mount: modal.mount._Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> _Image: ...
95
103
  def copy_local_file(
96
104
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
97
105
  ) -> _Image: ...
106
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> _Image: ...
98
107
  def copy_local_dir(
99
108
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
100
109
  ) -> _Image: ...
@@ -299,12 +308,18 @@ class _Image(modal.object._Object):
299
308
  class Image(modal.object.Object):
300
309
  force_build: bool
301
310
  inside_exceptions: typing.List[Exception]
302
- _used_local_mounts: typing.FrozenSet[modal.mount.Mount]
311
+ _serve_mounts: typing.FrozenSet[modal.mount.Mount]
312
+ _deferred_mounts: typing.Sequence[modal.mount.Mount]
303
313
  _metadata: typing.Optional[modal_proto.api_pb2.ImageMetadata]
304
314
 
305
315
  def __init__(self, *args, **kwargs): ...
306
316
  def _initialize_from_empty(self): ...
317
+ def _initialize_from_other(self, other: Image): ...
307
318
  def _hydrate_metadata(self, message: typing.Optional[google.protobuf.message.Message]): ...
319
+ def _add_mount_layer_or_copy(self, mount: modal.mount.Mount, copy: bool = False): ...
320
+ @property
321
+ def _mount_layers(self) -> typing.Tuple[modal.mount.Mount]: ...
322
+ def _assert_no_mount_layers(self): ...
308
323
  @staticmethod
309
324
  def _from_args(
310
325
  *,
@@ -320,6 +335,7 @@ class Image(modal.object.Object):
320
335
  context_mount: typing.Optional[modal.mount.Mount] = None,
321
336
  force_build: bool = False,
322
337
  _namespace: int = 1,
338
+ _do_assert_no_mount_layers: bool = True,
323
339
  ): ...
324
340
  def extend(
325
341
  self,
@@ -332,11 +348,13 @@ class Image(modal.object.Object):
332
348
  context_mount: typing.Optional[modal.mount.Mount] = None,
333
349
  force_build: bool = False,
334
350
  _namespace: int = 1,
351
+ _do_assert_no_mount_layers: bool = True,
335
352
  ) -> Image: ...
336
353
  def copy_mount(self, mount: modal.mount.Mount, remote_path: typing.Union[str, pathlib.Path] = ".") -> Image: ...
337
354
  def copy_local_file(
338
355
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
339
356
  ) -> Image: ...
357
+ def _add_local_python_packages(self, *module_names, copy: bool = False) -> Image: ...
340
358
  def copy_local_dir(
341
359
  self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
342
360
  ) -> Image: ...
modal/mount.py CHANGED
@@ -622,12 +622,13 @@ class _Mount(_Object, type_prefix="mo"):
622
622
  client: Optional[_Client] = None,
623
623
  ) -> None:
624
624
  check_object_name(deployment_name, "Mount")
625
+ environment_name = _get_environment_name(environment_name, resolver=None)
625
626
  self._deployment_name = deployment_name
626
627
  self._namespace = namespace
627
628
  self._environment_name = environment_name
628
629
  if client is None:
629
630
  client = await _Client.from_env()
630
- resolver = Resolver(client=client)
631
+ resolver = Resolver(client=client, environment_name=environment_name)
631
632
  await resolver.load(self)
632
633
 
633
634
  def _get_metadata(self) -> api_pb2.MountHandleMetadata:
@@ -171,34 +171,6 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
171
171
  tc.infinite_loop(lambda: client.stub.SharedVolumeHeartbeat(request), sleep=_heartbeat_sleep)
172
172
  yield cls._new_hydrated(response.shared_volume_id, client, None, is_another_app=True)
173
173
 
174
- @staticmethod
175
- def persisted(
176
- label: str,
177
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
178
- environment_name: Optional[str] = None,
179
- cloud: Optional[str] = None,
180
- ):
181
- """mdmd:hidden"""
182
- message = (
183
- "`NetworkFileSystem.persisted` is deprecated."
184
- " Please use `NetworkFileSystem.from_name(name, create_if_missing=True)` instead."
185
- )
186
- deprecation_error((2024, 3, 1), message)
187
-
188
- def persist(
189
- self,
190
- label: str,
191
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
192
- environment_name: Optional[str] = None,
193
- cloud: Optional[str] = None,
194
- ):
195
- """mdmd:hidden"""
196
- message = (
197
- "`NetworkFileSystem().persist('my-volume')` is deprecated."
198
- " Please use `NetworkFileSystem.from_name('my-volume', create_if_missing=True)` instead."
199
- )
200
- deprecation_error((2024, 2, 29), message)
201
-
202
174
  @staticmethod
203
175
  async def lookup(
204
176
  label: str,
@@ -26,13 +26,6 @@ class _NetworkFileSystem(modal.object._Object):
26
26
  _heartbeat_sleep: float = 300,
27
27
  ) -> typing.AsyncContextManager[_NetworkFileSystem]: ...
28
28
  @staticmethod
29
- def persisted(
30
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
31
- ): ...
32
- def persist(
33
- self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
34
- ): ...
35
- @staticmethod
36
29
  async def lookup(
37
30
  label: str,
38
31
  namespace=1,
@@ -85,13 +78,6 @@ class NetworkFileSystem(modal.object.Object):
85
78
  environment_name: typing.Optional[str] = None,
86
79
  _heartbeat_sleep: float = 300,
87
80
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[NetworkFileSystem]: ...
88
- @staticmethod
89
- def persisted(
90
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
91
- ): ...
92
- def persist(
93
- self, label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
94
- ): ...
95
81
 
96
82
  class __lookup_spec(typing_extensions.Protocol):
97
83
  def __call__(
modal/partial_function.py CHANGED
@@ -199,6 +199,7 @@ class _MethodDecoratorType:
199
199
  ...
200
200
 
201
201
 
202
+ # TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
202
203
  def _method(
203
204
  _warn_parentheses_missing=None,
204
205
  *,
@@ -207,7 +208,6 @@ def _method(
207
208
  is_generator: Optional[bool] = None,
208
209
  keep_warm: Optional[int] = None, # Deprecated: Use keep_warm on @app.cls() instead
209
210
  ) -> _MethodDecoratorType:
210
- # TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
211
211
  """Decorator for methods that should be transformed into a Modal Function registered against this class's App.
212
212
 
213
213
  **Usage:**
@@ -379,6 +379,11 @@ def _asgi_app(
379
379
  f"Modal will drop support for default parameters in a future release.",
380
380
  )
381
381
 
382
+ if inspect.iscoroutinefunction(raw_f):
383
+ raise InvalidError(
384
+ f"ASGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
385
+ )
386
+
382
387
  if not wait_for_response:
383
388
  deprecation_error(
384
389
  (2024, 5, 13),
@@ -448,6 +453,11 @@ def _wsgi_app(
448
453
  f"Modal will drop support for default parameters in a future release.",
449
454
  )
450
455
 
456
+ if inspect.iscoroutinefunction(raw_f):
457
+ raise InvalidError(
458
+ f"WSGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
459
+ )
460
+
451
461
  if not wait_for_response:
452
462
  deprecation_error(
453
463
  (2024, 5, 13),
modal/queue.py CHANGED
@@ -175,12 +175,6 @@ class _Queue(_Object, type_prefix="qu"):
175
175
 
176
176
  return _Queue._from_loader(_load, "Queue()", is_another_app=True, hydrate_lazily=True)
177
177
 
178
- @staticmethod
179
- def persisted(label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None):
180
- """mdmd:hidden"""
181
- message = "`Queue.persisted` is deprecated. Please use `Queue.from_name(name, create_if_missing=True)` instead."
182
- deprecation_error((2024, 3, 1), message)
183
-
184
178
  @staticmethod
185
179
  async def lookup(
186
180
  label: str,
modal/queue.pyi CHANGED
@@ -22,8 +22,6 @@ class _Queue(modal.object._Object):
22
22
  label: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
23
23
  ) -> _Queue: ...
24
24
  @staticmethod
25
- def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
26
- @staticmethod
27
25
  async def lookup(
28
26
  label: str,
29
27
  namespace=1,
@@ -104,8 +102,6 @@ class Queue(modal.object.Object):
104
102
  def from_name(
105
103
  label: str, namespace=1, environment_name: typing.Optional[str] = None, create_if_missing: bool = False
106
104
  ) -> Queue: ...
107
- @staticmethod
108
- def persisted(label: str, namespace=1, environment_name: typing.Optional[str] = None): ...
109
105
 
110
106
  class __lookup_spec(typing_extensions.Protocol):
111
107
  def __call__(
modal/sandbox.py CHANGED
@@ -148,7 +148,7 @@ class _Sandbox(_Object, type_prefix="sb"):
148
148
  definition = api_pb2.Sandbox(
149
149
  entrypoint_args=entrypoint_args,
150
150
  image_id=image.object_id,
151
- mount_ids=[mount.object_id for mount in mounts],
151
+ mount_ids=[mount.object_id for mount in mounts] + [mount.object_id for mount in image._mount_layers],
152
152
  secret_ids=[secret.object_id for secret in secrets],
153
153
  timeout_secs=timeout,
154
154
  workdir=workdir,
modal/volume.py CHANGED
@@ -79,15 +79,6 @@ class FileEntry:
79
79
  size=proto.size,
80
80
  )
81
81
 
82
- def __getattr__(self, name: str):
83
- deprecation_error(
84
- (2024, 4, 15),
85
- (
86
- f"The FileEntry dataclass was introduced to replace a private Protobuf message. "
87
- f"This dataclass does not have the {name} attribute."
88
- ),
89
- )
90
-
91
82
 
92
83
  class _Volume(_Object, type_prefix="vo"):
93
84
  """A writeable volume that can be used to share files between one or more Modal functions.
@@ -222,19 +213,6 @@ class _Volume(_Object, type_prefix="vo"):
222
213
  tc.infinite_loop(lambda: client.stub.VolumeHeartbeat(request), sleep=_heartbeat_sleep)
223
214
  yield cls._new_hydrated(response.volume_id, client, None, is_another_app=True)
224
215
 
225
- @staticmethod
226
- def persisted(
227
- label: str,
228
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
229
- environment_name: Optional[str] = None,
230
- cloud: Optional[str] = None,
231
- ):
232
- """mdmd:hidden"""
233
- message = (
234
- "`Volume.persisted` is deprecated. Please use `Volume.from_name(name, create_if_missing=True)` instead."
235
- )
236
- deprecation_error((2024, 3, 1), message)
237
-
238
216
  @staticmethod
239
217
  async def lookup(
240
218
  label: str,
modal/volume.pyi CHANGED
@@ -25,7 +25,6 @@ class FileEntry:
25
25
 
26
26
  @classmethod
27
27
  def _from_proto(cls, proto: modal_proto.api_pb2.FileEntry) -> FileEntry: ...
28
- def __getattr__(self, name: str): ...
29
28
  def __init__(self, path: str, type: FileEntryType, mtime: int, size: int) -> None: ...
30
29
  def __repr__(self): ...
31
30
  def __eq__(self, other): ...
@@ -56,10 +55,6 @@ class _Volume(modal.object._Object):
56
55
  _heartbeat_sleep: float = 300,
57
56
  ) -> typing.AsyncContextManager[_Volume]: ...
58
57
  @staticmethod
59
- def persisted(
60
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
61
- ): ...
62
- @staticmethod
63
58
  async def lookup(
64
59
  label: str,
65
60
  namespace=1,
@@ -149,10 +144,6 @@ class Volume(modal.object.Object):
149
144
  version: typing.Optional[int] = None,
150
145
  _heartbeat_sleep: float = 300,
151
146
  ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[Volume]: ...
152
- @staticmethod
153
- def persisted(
154
- label: str, namespace=1, environment_name: typing.Optional[str] = None, cloud: typing.Optional[str] = None
155
- ): ...
156
147
 
157
148
  class __lookup_spec(typing_extensions.Protocol):
158
149
  def __call__(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.66.17
3
+ Version: 0.66.39
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -15,7 +15,6 @@ Requires-Python: >=3.9
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: aiohttp
18
- Requires-Dist: aiostream (~=0.5.2)
19
18
  Requires-Dist: certifi
20
19
  Requires-Dist: click (>=8.1.0)
21
20
  Requires-Dist: fastapi