modal 1.1.0__py3-none-any.whl → 1.1.1__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 (75) hide show
  1. modal/__main__.py +2 -2
  2. modal/_clustered_functions.py +3 -0
  3. modal/_clustered_functions.pyi +3 -2
  4. modal/_functions.py +78 -26
  5. modal/_object.py +9 -1
  6. modal/_output.py +14 -25
  7. modal/_runtime/gpu_memory_snapshot.py +158 -54
  8. modal/_utils/async_utils.py +6 -4
  9. modal/_utils/auth_token_manager.py +1 -1
  10. modal/_utils/blob_utils.py +16 -21
  11. modal/_utils/function_utils.py +16 -4
  12. modal/_utils/time_utils.py +8 -4
  13. modal/app.py +0 -4
  14. modal/app.pyi +0 -4
  15. modal/cli/_traceback.py +3 -2
  16. modal/cli/app.py +4 -4
  17. modal/cli/cluster.py +4 -4
  18. modal/cli/config.py +2 -2
  19. modal/cli/container.py +2 -2
  20. modal/cli/dict.py +4 -4
  21. modal/cli/entry_point.py +2 -2
  22. modal/cli/import_refs.py +3 -3
  23. modal/cli/network_file_system.py +8 -9
  24. modal/cli/profile.py +2 -2
  25. modal/cli/queues.py +5 -5
  26. modal/cli/secret.py +5 -5
  27. modal/cli/utils.py +3 -4
  28. modal/cli/volume.py +8 -9
  29. modal/client.py +8 -1
  30. modal/client.pyi +9 -2
  31. modal/container_process.py +2 -2
  32. modal/dict.py +47 -3
  33. modal/dict.pyi +55 -0
  34. modal/exception.py +4 -0
  35. modal/experimental/__init__.py +1 -1
  36. modal/experimental/flash.py +18 -2
  37. modal/experimental/flash.pyi +19 -0
  38. modal/functions.pyi +0 -1
  39. modal/image.py +26 -10
  40. modal/image.pyi +12 -4
  41. modal/mount.py +1 -1
  42. modal/object.pyi +4 -0
  43. modal/parallel_map.py +432 -4
  44. modal/parallel_map.pyi +28 -0
  45. modal/queue.py +46 -3
  46. modal/queue.pyi +53 -0
  47. modal/sandbox.py +105 -25
  48. modal/sandbox.pyi +108 -18
  49. modal/secret.py +48 -5
  50. modal/secret.pyi +55 -0
  51. modal/token_flow.py +3 -3
  52. modal/volume.py +49 -18
  53. modal/volume.pyi +50 -8
  54. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/METADATA +2 -2
  55. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/RECORD +75 -75
  56. modal_proto/api.proto +140 -14
  57. modal_proto/api_grpc.py +80 -0
  58. modal_proto/api_pb2.py +927 -756
  59. modal_proto/api_pb2.pyi +488 -34
  60. modal_proto/api_pb2_grpc.py +166 -0
  61. modal_proto/api_pb2_grpc.pyi +52 -0
  62. modal_proto/modal_api_grpc.py +5 -0
  63. modal_version/__init__.py +1 -1
  64. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  65. /modal/{requirements → builder}/2023.12.txt +0 -0
  66. /modal/{requirements → builder}/2024.04.txt +0 -0
  67. /modal/{requirements → builder}/2024.10.txt +0 -0
  68. /modal/{requirements → builder}/2025.06.txt +0 -0
  69. /modal/{requirements → builder}/PREVIEW.txt +0 -0
  70. /modal/{requirements → builder}/README.md +0 -0
  71. /modal/{requirements → builder}/base-images.json +0 -0
  72. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/WHEEL +0 -0
  73. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/entry_points.txt +0 -0
  74. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/licenses/LICENSE +0 -0
  75. {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/top_level.txt +0 -0
modal/dict.pyi CHANGED
@@ -1,13 +1,37 @@
1
1
  import collections.abc
2
+ import datetime
3
+ import google.protobuf.message
2
4
  import modal._object
3
5
  import modal.client
4
6
  import modal.object
7
+ import modal_proto.api_pb2
5
8
  import synchronicity.combined_types
6
9
  import typing
7
10
  import typing_extensions
8
11
 
9
12
  def _serialize_dict(data): ...
10
13
 
14
+ class DictInfo:
15
+ """Information about the Dict object."""
16
+
17
+ name: typing.Optional[str]
18
+ created_at: datetime.datetime
19
+ created_by: typing.Optional[str]
20
+
21
+ def __init__(
22
+ self, name: typing.Optional[str], created_at: datetime.datetime, created_by: typing.Optional[str]
23
+ ) -> None:
24
+ """Initialize self. See help(type(self)) for accurate signature."""
25
+ ...
26
+
27
+ def __repr__(self):
28
+ """Return repr(self)."""
29
+ ...
30
+
31
+ def __eq__(self, other):
32
+ """Return self==value."""
33
+ ...
34
+
11
35
  class _Dict(modal._object._Object):
12
36
  """Distributed dictionary for storage in Modal apps.
13
37
 
@@ -49,10 +73,18 @@ class _Dict(modal._object._Object):
49
73
 
50
74
  For more examples, see the [guide](https://modal.com/docs/guide/dicts-and-queues#modal-dicts).
51
75
  """
76
+
77
+ _name: typing.Optional[str]
78
+ _metadata: typing.Optional[modal_proto.api_pb2.DictMetadata]
79
+
52
80
  def __init__(self, data={}):
53
81
  """mdmd:hidden"""
54
82
  ...
55
83
 
84
+ @property
85
+ def name(self) -> typing.Optional[str]: ...
86
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
87
+ def _get_metadata(self) -> modal_proto.api_pb2.DictMetadata: ...
56
88
  @classmethod
57
89
  def ephemeral(
58
90
  cls: type[_Dict],
@@ -131,6 +163,10 @@ class _Dict(modal._object._Object):
131
163
  client: typing.Optional[modal.client._Client] = None,
132
164
  environment_name: typing.Optional[str] = None,
133
165
  ): ...
166
+ async def info(self) -> DictInfo:
167
+ """Return information about the Dict object."""
168
+ ...
169
+
134
170
  async def clear(self) -> None:
135
171
  """Remove all items from the Dict."""
136
172
  ...
@@ -264,10 +300,18 @@ class Dict(modal.object.Object):
264
300
 
265
301
  For more examples, see the [guide](https://modal.com/docs/guide/dicts-and-queues#modal-dicts).
266
302
  """
303
+
304
+ _name: typing.Optional[str]
305
+ _metadata: typing.Optional[modal_proto.api_pb2.DictMetadata]
306
+
267
307
  def __init__(self, data={}):
268
308
  """mdmd:hidden"""
269
309
  ...
270
310
 
311
+ @property
312
+ def name(self) -> typing.Optional[str]: ...
313
+ def _hydrate_metadata(self, metadata: typing.Optional[google.protobuf.message.Message]): ...
314
+ def _get_metadata(self) -> modal_proto.api_pb2.DictMetadata: ...
271
315
  @classmethod
272
316
  def ephemeral(
273
317
  cls: type[Dict],
@@ -388,6 +432,17 @@ class Dict(modal.object.Object):
388
432
 
389
433
  delete: __delete_spec
390
434
 
435
+ class __info_spec(typing_extensions.Protocol[SUPERSELF]):
436
+ def __call__(self, /) -> DictInfo:
437
+ """Return information about the Dict object."""
438
+ ...
439
+
440
+ async def aio(self, /) -> DictInfo:
441
+ """Return information about the Dict object."""
442
+ ...
443
+
444
+ info: __info_spec[typing_extensions.Self]
445
+
391
446
  class __clear_spec(typing_extensions.Protocol[SUPERSELF]):
392
447
  def __call__(self, /) -> None:
393
448
  """Remove all items from the Dict."""
modal/exception.py CHANGED
@@ -26,6 +26,10 @@ class Error(Exception):
26
26
  """
27
27
 
28
28
 
29
+ class AlreadyExistsError(Error):
30
+ """Raised when a resource creation conflicts with an existing resource."""
31
+
32
+
29
33
  class RemoteError(Error):
30
34
  """Raised when an error occurs on the Modal server."""
31
35
 
@@ -20,7 +20,7 @@ from ..cls import _Cls, _Obj
20
20
  from ..exception import InvalidError
21
21
  from ..image import DockerfileSpec, ImageBuilderVersion, _Image, _ImageRegistryConfig
22
22
  from ..secret import _Secret
23
- from .flash import flash_forward, flash_prometheus_autoscaler # noqa: F401
23
+ from .flash import flash_forward, flash_get_containers, flash_prometheus_autoscaler # noqa: F401
24
24
 
25
25
 
26
26
  def stop_fetching_inputs():
@@ -51,7 +51,7 @@ class _FlashManager:
51
51
  timeout=10,
52
52
  )
53
53
  if first_registration:
54
- logger.warning(f"[Modal Flash] Listening at {resp.url}")
54
+ logger.warning(f"[Modal Flash] Listening at {resp.url} over {self.tunnel.url}")
55
55
  first_registration = False
56
56
  except asyncio.CancelledError:
57
57
  logger.warning("[Modal Flash] Shutting down...")
@@ -225,7 +225,6 @@ class _FlashPrometheusAutoscaler:
225
225
 
226
226
  await self.cls.update_autoscaler(
227
227
  min_containers=actual_target_containers,
228
- max_containers=actual_target_containers,
229
228
  )
230
229
 
231
230
  if time.time() - autoscaling_time < self.autoscaling_interval_seconds:
@@ -447,3 +446,20 @@ async def flash_prometheus_autoscaler(
447
446
  )
448
447
  await autoscaler.start()
449
448
  return autoscaler
449
+
450
+
451
+ @synchronizer.create_blocking
452
+ async def flash_get_containers(app_name: str, cls_name: str) -> list[dict[str, Any]]:
453
+ """
454
+ Return a list of flash containers for a deployed Flash service.
455
+
456
+ This is a highly experimental method that can break or be removed at any time without warning.
457
+ Do not use this method unless explicitly instructed to do so by Modal support.
458
+ """
459
+ client = await _Client.from_env()
460
+ fn = _Cls.from_name(app_name, cls_name)._class_service_function
461
+ assert fn is not None
462
+ await fn.hydrate(client=client)
463
+ req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
464
+ resp = await retry_transient_errors(client.stub.FlashContainerList, req)
465
+ return resp.containers
@@ -250,3 +250,22 @@ class __flash_prometheus_autoscaler_spec(typing_extensions.Protocol):
250
250
  ...
251
251
 
252
252
  flash_prometheus_autoscaler: __flash_prometheus_autoscaler_spec
253
+
254
+ class __flash_get_containers_spec(typing_extensions.Protocol):
255
+ def __call__(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
256
+ """Return a list of flash containers for a deployed Flash service.
257
+
258
+ This is a highly experimental method that can break or be removed at any time without warning.
259
+ Do not use this method unless explicitly instructed to do so by Modal support.
260
+ """
261
+ ...
262
+
263
+ async def aio(self, /, app_name: str, cls_name: str) -> list[dict[str, typing.Any]]:
264
+ """Return a list of flash containers for a deployed Flash service.
265
+
266
+ This is a highly experimental method that can break or be removed at any time without warning.
267
+ Do not use this method unless explicitly instructed to do so by Modal support.
268
+ """
269
+ ...
270
+
271
+ flash_get_containers: __flash_get_containers_spec
modal/functions.pyi CHANGED
@@ -109,7 +109,6 @@ class Function(
109
109
  experimental_options: typing.Optional[dict[str, str]] = None,
110
110
  _experimental_proxy_ip: typing.Optional[str] = None,
111
111
  _experimental_custom_scaling_factor: typing.Optional[float] = None,
112
- _experimental_enable_gpu_snapshot: bool = False,
113
112
  ) -> Function:
114
113
  """mdmd:hidden"""
115
114
  ...
modal/image.py CHANGED
@@ -71,7 +71,7 @@ SUPPORTED_PYTHON_SERIES: dict[ImageBuilderVersion, list[str]] = {
71
71
  "2023.12": ["3.9", "3.10", "3.11", "3.12"],
72
72
  }
73
73
 
74
- LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "requirements"
74
+ LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "builder"
75
75
  CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
76
76
 
77
77
 
@@ -658,7 +658,13 @@ class _Image(_Object, type_prefix="im"):
658
658
  msg += " (Hint: Use `modal.enable_output()` to see logs from the process building the Image.)"
659
659
  raise RemoteError(msg)
660
660
  elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TERMINATED:
661
- raise RemoteError(f"Image build for {image_id} terminated due to external shut-down. Please try again.")
661
+ msg = f"Image build for {image_id} terminated due to external shut-down. Please try again."
662
+ if result.exception:
663
+ msg = (
664
+ f"Image build for {image_id} terminated due to external shut-down with the exception:\n"
665
+ f"{result.exception}"
666
+ )
667
+ raise RemoteError(msg)
662
668
  elif result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
663
669
  raise RemoteError(
664
670
  f"Image build for {image_id} timed out. Please try again with a larger `timeout` parameter."
@@ -1183,11 +1189,11 @@ class _Image(_Object, type_prefix="im"):
1183
1189
  else:
1184
1190
  commands.append(f"COPY --from=ghcr.io/astral-sh/uv:{uv_version} /uv {UV_ROOT}/uv")
1185
1191
 
1186
- # NOTE: Using `which python` assumes:
1192
+ # NOTE: Using $(command -v python) assumes:
1187
1193
  # - python is on the PATH and uv is installing into the first python in the PATH
1188
- # - the shell supports backticks for substitution
1189
- # - `which` command is on the PATH
1190
- uv_pip_args = ["--python `which python`", "--compile-bytecode"]
1194
+ # - the shell supports $() for substitution
1195
+ # - `command` command is on the PATH
1196
+ uv_pip_args = ["--python $(command -v python)", "--compile-bytecode"]
1191
1197
  context_files = {}
1192
1198
 
1193
1199
  if find_links:
@@ -1350,6 +1356,8 @@ class _Image(_Object, type_prefix="im"):
1350
1356
  image = modal.Image.debian_slim().uv_sync()
1351
1357
  ```
1352
1358
 
1359
+ The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context.
1360
+
1353
1361
  Added in v1.1.0.
1354
1362
  """
1355
1363
 
@@ -1385,7 +1393,13 @@ class _Image(_Object, type_prefix="im"):
1385
1393
  # a requirement in `uv.lock`
1386
1394
  return
1387
1395
 
1388
- dependencies = pyproject_toml_content["project"]["dependencies"]
1396
+ try:
1397
+ dependencies = pyproject_toml_content["project"]["dependencies"]
1398
+ except KeyError as e:
1399
+ raise InvalidError(
1400
+ f"Invalid pyproject.toml file: missing key {e} in {pyproject_toml}. "
1401
+ "See https://packaging.python.org/en/latest/guides/writing-pyproject-toml for guidelines."
1402
+ )
1389
1403
 
1390
1404
  for group in groups:
1391
1405
  if (
@@ -1451,7 +1465,7 @@ class _Image(_Object, type_prefix="im"):
1451
1465
  commands.append(f"COPY /.uv.lock {UV_ROOT}/uv.lock")
1452
1466
 
1453
1467
  if frozen:
1454
- # Do not update `uv.lock` when we have one when `frozen=True`. This it ehd efault because this
1468
+ # Do not update `uv.lock` when we have one when `frozen=True`. This is the default because this
1455
1469
  # ensures that the runtime environment matches the local `uv.lock`.
1456
1470
  #
1457
1471
  # If `frozen=False`, then `uv sync` will update the the dependencies in the `uv.lock` file
@@ -1555,7 +1569,7 @@ class _Image(_Object, type_prefix="im"):
1555
1569
  self,
1556
1570
  entrypoint_commands: list[str],
1557
1571
  ) -> "_Image":
1558
- """Set the entrypoint for the image."""
1572
+ """Set the ENTRYPOINT for the image."""
1559
1573
  if not isinstance(entrypoint_commands, list) or not all(isinstance(x, str) for x in entrypoint_commands):
1560
1574
  raise InvalidError("entrypoint_commands must be a list of strings.")
1561
1575
  args_str = _flatten_str_args("entrypoint", "entrypoint_commands", entrypoint_commands)
@@ -2231,7 +2245,9 @@ class _Image(_Object, type_prefix="im"):
2231
2245
  )
2232
2246
 
2233
2247
  def cmd(self, cmd: list[str]) -> "_Image":
2234
- """Set the default entrypoint argument (`CMD`) for the image.
2248
+ """Set the default command (`CMD`) to run when a container is started.
2249
+
2250
+ Used with `modal.Sandbox`. Has no effect on `modal.Function`.
2235
2251
 
2236
2252
  **Example**
2237
2253
 
modal/image.pyi CHANGED
@@ -524,6 +524,8 @@ class _Image(modal._object._Object):
524
524
  image = modal.Image.debian_slim().uv_sync()
525
525
  ```
526
526
 
527
+ The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context.
528
+
527
529
  Added in v1.1.0.
528
530
  """
529
531
  ...
@@ -584,7 +586,7 @@ class _Image(modal._object._Object):
584
586
  ...
585
587
 
586
588
  def entrypoint(self, entrypoint_commands: list[str]) -> _Image:
587
- """Set the entrypoint for the image."""
589
+ """Set the ENTRYPOINT for the image."""
588
590
  ...
589
591
 
590
592
  def shell(self, shell_commands: list[str]) -> _Image:
@@ -912,7 +914,9 @@ class _Image(modal._object._Object):
912
914
  ...
913
915
 
914
916
  def cmd(self, cmd: list[str]) -> _Image:
915
- """Set the default entrypoint argument (`CMD`) for the image.
917
+ """Set the default command (`CMD`) to run when a container is started.
918
+
919
+ Used with `modal.Sandbox`. Has no effect on `modal.Function`.
916
920
 
917
921
  **Example**
918
922
 
@@ -1364,6 +1368,8 @@ class Image(modal.object.Object):
1364
1368
  image = modal.Image.debian_slim().uv_sync()
1365
1369
  ```
1366
1370
 
1371
+ The `pyproject.toml` and `uv.lock` in `uv_project_dir` are automatically added to the build context.
1372
+
1367
1373
  Added in v1.1.0.
1368
1374
  """
1369
1375
  ...
@@ -1424,7 +1430,7 @@ class Image(modal.object.Object):
1424
1430
  ...
1425
1431
 
1426
1432
  def entrypoint(self, entrypoint_commands: list[str]) -> Image:
1427
- """Set the entrypoint for the image."""
1433
+ """Set the ENTRYPOINT for the image."""
1428
1434
  ...
1429
1435
 
1430
1436
  def shell(self, shell_commands: list[str]) -> Image:
@@ -1752,7 +1758,9 @@ class Image(modal.object.Object):
1752
1758
  ...
1753
1759
 
1754
1760
  def cmd(self, cmd: list[str]) -> Image:
1755
- """Set the default entrypoint argument (`CMD`) for the image.
1761
+ """Set the default command (`CMD`) to run when a container is started.
1762
+
1763
+ Used with `modal.Sandbox`. Has no effect on `modal.Function`.
1756
1764
 
1757
1765
  **Example**
1758
1766
 
modal/mount.py CHANGED
@@ -882,7 +882,7 @@ async def _create_single_client_dependency_mount(
882
882
 
883
883
  with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpd:
884
884
  print(f"📦 Building {mount_name}.")
885
- requirements = os.path.join(os.path.dirname(__file__), f"requirements/{builder_version}.txt")
885
+ requirements = os.path.join(os.path.dirname(__file__), f"builder/{builder_version}.txt")
886
886
  cmd = " ".join(
887
887
  [
888
888
  "uv",
modal/object.pyi CHANGED
@@ -31,6 +31,7 @@ class Object:
31
31
  _client: typing.Optional[modal.client.Client]
32
32
  _is_hydrated: bool
33
33
  _is_rehydrated: bool
34
+ _name: typing.Optional[str]
34
35
 
35
36
  def __init__(self, *args, **kwargs):
36
37
  """mdmd:hidden"""
@@ -54,6 +55,7 @@ class Object:
54
55
  hydrate_lazily: bool = False,
55
56
  deps: typing.Optional[collections.abc.Callable[..., collections.abc.Sequence[Object]]] = None,
56
57
  deduplication_key: typing.Optional[collections.abc.Callable[[], collections.abc.Hashable]] = None,
58
+ name: typing.Optional[str] = None,
57
59
  ): ...
58
60
  def aio(
59
61
  self,
@@ -75,6 +77,7 @@ class Object:
75
77
  deduplication_key: typing.Optional[
76
78
  collections.abc.Callable[[], collections.abc.Awaitable[collections.abc.Hashable]]
77
79
  ] = None,
80
+ name: typing.Optional[str] = None,
78
81
  ): ...
79
82
 
80
83
  _init: ___init_spec[typing_extensions.Self]
@@ -107,6 +110,7 @@ class Object:
107
110
  hydrate_lazily: bool = False,
108
111
  deps: typing.Optional[collections.abc.Callable[..., collections.abc.Sequence[Object]]] = None,
109
112
  deduplication_key: typing.Optional[collections.abc.Callable[[], collections.abc.Hashable]] = None,
113
+ name: typing.Optional[str] = None,
110
114
  ): ...
111
115
  @staticmethod
112
116
  def _get_type_from_id(object_id: str) -> type[Object]: ...