modal 1.2.1.dev8__py3-none-any.whl → 1.2.2.dev19__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.
Files changed (70) hide show
  1. modal/_clustered_functions.py +1 -3
  2. modal/_container_entrypoint.py +4 -1
  3. modal/_functions.py +33 -49
  4. modal/_grpc_client.py +148 -0
  5. modal/_output.py +3 -4
  6. modal/_partial_function.py +22 -2
  7. modal/_runtime/container_io_manager.py +21 -22
  8. modal/_utils/async_utils.py +12 -3
  9. modal/_utils/auth_token_manager.py +1 -4
  10. modal/_utils/blob_utils.py +3 -4
  11. modal/_utils/function_utils.py +4 -0
  12. modal/_utils/grpc_utils.py +80 -51
  13. modal/_utils/mount_utils.py +26 -1
  14. modal/_utils/task_command_router_client.py +536 -0
  15. modal/app.py +7 -5
  16. modal/cli/cluster.py +4 -2
  17. modal/cli/config.py +3 -1
  18. modal/cli/container.py +5 -4
  19. modal/cli/entry_point.py +1 -0
  20. modal/cli/launch.py +1 -2
  21. modal/cli/network_file_system.py +1 -4
  22. modal/cli/queues.py +1 -2
  23. modal/cli/secret.py +1 -2
  24. modal/client.py +5 -115
  25. modal/client.pyi +2 -91
  26. modal/cls.py +1 -2
  27. modal/config.py +3 -1
  28. modal/container_process.py +287 -11
  29. modal/container_process.pyi +95 -32
  30. modal/dict.py +12 -12
  31. modal/environments.py +1 -2
  32. modal/exception.py +4 -0
  33. modal/experimental/__init__.py +2 -3
  34. modal/experimental/flash.py +27 -57
  35. modal/experimental/flash.pyi +6 -20
  36. modal/file_io.py +13 -27
  37. modal/functions.pyi +6 -6
  38. modal/image.py +24 -3
  39. modal/image.pyi +4 -0
  40. modal/io_streams.py +433 -127
  41. modal/io_streams.pyi +236 -171
  42. modal/mount.py +4 -4
  43. modal/network_file_system.py +5 -6
  44. modal/parallel_map.py +29 -31
  45. modal/parallel_map.pyi +3 -9
  46. modal/partial_function.pyi +4 -1
  47. modal/queue.py +17 -18
  48. modal/runner.py +12 -11
  49. modal/sandbox.py +148 -42
  50. modal/sandbox.pyi +139 -0
  51. modal/secret.py +4 -5
  52. modal/snapshot.py +1 -4
  53. modal/token_flow.py +1 -1
  54. modal/volume.py +22 -22
  55. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/METADATA +1 -1
  56. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/RECORD +70 -68
  57. modal_proto/api.proto +2 -24
  58. modal_proto/api_grpc.py +0 -32
  59. modal_proto/api_pb2.py +838 -878
  60. modal_proto/api_pb2.pyi +8 -70
  61. modal_proto/api_pb2_grpc.py +0 -67
  62. modal_proto/api_pb2_grpc.pyi +0 -22
  63. modal_proto/modal_api_grpc.py +175 -177
  64. modal_proto/sandbox_router.proto +0 -4
  65. modal_proto/sandbox_router_pb2.pyi +0 -4
  66. modal_version/__init__.py +1 -1
  67. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/WHEEL +0 -0
  68. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/entry_points.txt +0 -0
  69. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/licenses/LICENSE +0 -0
  70. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,6 @@ from .._object import _get_environment_name
13
13
  from .._partial_function import _clustered
14
14
  from .._runtime.container_io_manager import _ContainerIOManager
15
15
  from .._utils.async_utils import synchronize_api, synchronizer
16
- from .._utils.grpc_utils import retry_transient_errors
17
16
  from ..app import _App
18
17
  from ..client import _Client
19
18
  from ..cls import _Cls
@@ -116,7 +115,7 @@ async def get_app_objects(
116
115
 
117
116
  app = await _App.lookup(app_name, environment_name=environment_name, client=client)
118
117
  req = api_pb2.AppGetLayoutRequest(app_id=app.app_id)
119
- app_layout_resp = await retry_transient_errors(client.stub.AppGetLayout, req)
118
+ app_layout_resp = await client.stub.AppGetLayout(req)
120
119
 
121
120
  app_objects: dict[str, Union[_Function, _Cls]] = {}
122
121
 
@@ -361,4 +360,4 @@ async def image_delete(
361
360
  client = await _Client.from_env()
362
361
 
363
362
  req = api_pb2.ImageDeleteRequest(image_id=image_id)
364
- await retry_transient_errors(client.stub.ImageDelete, req)
363
+ await client.stub.ImageDelete(req)
@@ -16,7 +16,6 @@ from modal_proto import api_pb2
16
16
 
17
17
  from .._tunnel import _forward as _forward_tunnel
18
18
  from .._utils.async_utils import synchronize_api, synchronizer
19
- from .._utils.grpc_utils import retry_transient_errors
20
19
  from ..client import _Client
21
20
  from ..config import logger
22
21
  from ..exception import InvalidError
@@ -126,10 +125,8 @@ class _FlashManager:
126
125
  f"due to error: {port_check_error}, num_failures: {self.num_failures}"
127
126
  )
128
127
  self.num_failures += 1
129
- await retry_transient_errors(
130
- self.client.stub.FlashContainerDeregister,
131
- api_pb2.FlashContainerDeregisterRequest(),
132
- )
128
+ await self.client.stub.FlashContainerDeregister(api_pb2.FlashContainerDeregisterRequest())
129
+
133
130
  except asyncio.CancelledError:
134
131
  logger.warning("[Modal Flash] Shutting down...")
135
132
  break
@@ -148,8 +145,7 @@ class _FlashManager:
148
145
 
149
146
  async def stop(self):
150
147
  self.heartbeat_task.cancel()
151
- await retry_transient_errors(
152
- self.client.stub.FlashContainerDeregister,
148
+ await self.client.stub.FlashContainerDeregister(
153
149
  api_pb2.FlashContainerDeregisterRequest(),
154
150
  )
155
151
 
@@ -321,7 +317,7 @@ class _FlashPrometheusAutoscaler:
321
317
 
322
318
  async def _compute_target_containers(self, current_replicas: int) -> int:
323
319
  """
324
- Gets internal metrics from container to autoscale up or down.
320
+ Gets metrics from container to autoscale up or down.
325
321
  """
326
322
  containers = await self._get_all_containers()
327
323
  if len(containers) > current_replicas:
@@ -334,7 +330,7 @@ class _FlashPrometheusAutoscaler:
334
330
  if current_replicas == 0:
335
331
  return 1
336
332
 
337
- # Get metrics based on autoscaler type (prometheus or internal)
333
+ # Get metrics based on autoscaler type
338
334
  sum_metric, n_containers_with_metrics = await self._get_scaling_info(containers)
339
335
 
340
336
  desired_replicas = self._calculate_desired_replicas(
@@ -406,39 +402,26 @@ class _FlashPrometheusAutoscaler:
406
402
  return desired_replicas
407
403
 
408
404
  async def _get_scaling_info(self, containers) -> tuple[float, int]:
409
- """Get metrics using either internal container metrics API or prometheus HTTP endpoints."""
410
- if self.metrics_endpoint == "internal":
411
- container_metrics_results = await asyncio.gather(
412
- *[self._get_container_metrics(container.task_id) for container in containers]
413
- )
414
- container_metrics_list = []
415
- for container_metric in container_metrics_results:
416
- if container_metric is None:
417
- continue
418
- container_metrics_list.append(getattr(container_metric.metrics, self.target_metric))
419
-
420
- sum_metric = sum(container_metrics_list)
421
- n_containers_with_metrics = len(container_metrics_list)
422
- else:
423
- sum_metric = 0
424
- n_containers_with_metrics = 0
425
-
426
- container_metrics_list = await asyncio.gather(
427
- *[
428
- self._get_metrics(f"https://{container.host}:{container.port}/{self.metrics_endpoint}")
429
- for container in containers
430
- ]
431
- )
405
+ """Get metrics using container exposed metrics endpoints."""
406
+ sum_metric = 0
407
+ n_containers_with_metrics = 0
408
+
409
+ container_metrics_list = await asyncio.gather(
410
+ *[
411
+ self._get_metrics(f"https://{container.host}:{container.port}/{self.metrics_endpoint}")
412
+ for container in containers
413
+ ]
414
+ )
432
415
 
433
- for container_metrics in container_metrics_list:
434
- if (
435
- container_metrics is None
436
- or self.target_metric not in container_metrics
437
- or len(container_metrics[self.target_metric]) == 0
438
- ):
439
- continue
440
- sum_metric += container_metrics[self.target_metric][0].value
441
- n_containers_with_metrics += 1
416
+ for container_metrics in container_metrics_list:
417
+ if (
418
+ container_metrics is None
419
+ or self.target_metric not in container_metrics
420
+ or len(container_metrics[self.target_metric]) == 0
421
+ ):
422
+ continue
423
+ sum_metric += container_metrics[self.target_metric][0].value
424
+ n_containers_with_metrics += 1
442
425
 
443
426
  return sum_metric, n_containers_with_metrics
444
427
 
@@ -474,23 +457,14 @@ class _FlashPrometheusAutoscaler:
474
457
 
475
458
  return metrics
476
459
 
477
- async def _get_container_metrics(self, container_id: str) -> Optional[api_pb2.TaskGetAutoscalingMetricsResponse]:
478
- req = api_pb2.TaskGetAutoscalingMetricsRequest(task_id=container_id)
479
- try:
480
- resp = await retry_transient_errors(self.client.stub.TaskGetAutoscalingMetrics, req)
481
- return resp
482
- except Exception as e:
483
- logger.warning(f"[Modal Flash] Error getting metrics for container {container_id}: {e}")
484
- return None
485
-
486
460
  async def _get_all_containers(self):
487
461
  req = api_pb2.FlashContainerListRequest(function_id=self.fn.object_id)
488
- resp = await retry_transient_errors(self.client.stub.FlashContainerList, req)
462
+ resp = await self.client.stub.FlashContainerList(req)
489
463
  return resp.containers
490
464
 
491
465
  async def _set_target_slots(self, target_slots: int):
492
466
  req = api_pb2.FlashSetTargetSlotsMetricsRequest(function_id=self.fn.object_id, target_slots=target_slots)
493
- await retry_transient_errors(self.client.stub.FlashSetTargetSlotsMetrics, req)
467
+ await self.client.stub.FlashSetTargetSlotsMetrics(req)
494
468
  return
495
469
 
496
470
  def _make_scaling_decision(
@@ -572,14 +546,10 @@ async def flash_prometheus_autoscaler(
572
546
  app_name: str,
573
547
  cls_name: str,
574
548
  # Endpoint to fetch metrics from. Must be in Prometheus format. Example: "/metrics"
575
- # If metrics_endpoint is "internal", we will use containers' internal metrics to autoscale instead.
576
549
  metrics_endpoint: str,
577
550
  # Target metric to autoscale on. Example: "vllm:num_requests_running"
578
- # If metrics_endpoint is "internal", target_metrics options are: [cpu_usage_percent, memory_usage_percent]
579
551
  target_metric: str,
580
552
  # Target metric value. Example: 25
581
- # If metrics_endpoint is "internal", target_metric_value is a percentage value between 0.1 and 1.0 (inclusive),
582
- # indicating container's usage of that metric.
583
553
  target_metric_value: float,
584
554
  min_containers: Optional[int] = None,
585
555
  max_containers: Optional[int] = None,
@@ -645,5 +615,5 @@ async def flash_get_containers(app_name: str, cls_name: str) -> list[dict[str, A
645
615
  assert fn is not None
646
616
  await fn.hydrate(client=client)
647
617
  req = api_pb2.FlashContainerListRequest(function_id=fn.object_id)
648
- resp = await retry_transient_errors(client.stub.FlashContainerList, req)
618
+ resp = await client.stub.FlashContainerList(req)
649
619
  return resp.containers
@@ -1,5 +1,4 @@
1
1
  import modal.client
2
- import modal_proto.api_pb2
3
2
  import subprocess
4
3
  import typing
5
4
  import typing_extensions
@@ -139,7 +138,7 @@ class _FlashPrometheusAutoscaler:
139
138
  async def start(self): ...
140
139
  async def _run_autoscaler_loop(self): ...
141
140
  async def _compute_target_containers(self, current_replicas: int) -> int:
142
- """Gets internal metrics from container to autoscale up or down."""
141
+ """Gets metrics from container to autoscale up or down."""
143
142
  ...
144
143
 
145
144
  def _calculate_desired_replicas(
@@ -154,13 +153,10 @@ class _FlashPrometheusAutoscaler:
154
153
  ...
155
154
 
156
155
  async def _get_scaling_info(self, containers) -> tuple[float, int]:
157
- """Get metrics using either internal container metrics API or prometheus HTTP endpoints."""
156
+ """Get metrics using container exposed metrics endpoints."""
158
157
  ...
159
158
 
160
159
  async def _get_metrics(self, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
161
- async def _get_container_metrics(
162
- self, container_id: str
163
- ) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
164
160
  async def _get_all_containers(self): ...
165
161
  async def _set_target_slots(self, target_slots: int): ...
166
162
  def _make_scaling_decision(
@@ -226,11 +222,11 @@ class FlashPrometheusAutoscaler:
226
222
 
227
223
  class ___compute_target_containers_spec(typing_extensions.Protocol[SUPERSELF]):
228
224
  def __call__(self, /, current_replicas: int) -> int:
229
- """Gets internal metrics from container to autoscale up or down."""
225
+ """Gets metrics from container to autoscale up or down."""
230
226
  ...
231
227
 
232
228
  async def aio(self, /, current_replicas: int) -> int:
233
- """Gets internal metrics from container to autoscale up or down."""
229
+ """Gets metrics from container to autoscale up or down."""
234
230
  ...
235
231
 
236
232
  _compute_target_containers: ___compute_target_containers_spec[typing_extensions.Self]
@@ -248,11 +244,11 @@ class FlashPrometheusAutoscaler:
248
244
 
249
245
  class ___get_scaling_info_spec(typing_extensions.Protocol[SUPERSELF]):
250
246
  def __call__(self, /, containers) -> tuple[float, int]:
251
- """Get metrics using either internal container metrics API or prometheus HTTP endpoints."""
247
+ """Get metrics using container exposed metrics endpoints."""
252
248
  ...
253
249
 
254
250
  async def aio(self, /, containers) -> tuple[float, int]:
255
- """Get metrics using either internal container metrics API or prometheus HTTP endpoints."""
251
+ """Get metrics using container exposed metrics endpoints."""
256
252
  ...
257
253
 
258
254
  _get_scaling_info: ___get_scaling_info_spec[typing_extensions.Self]
@@ -263,16 +259,6 @@ class FlashPrometheusAutoscaler:
263
259
 
264
260
  _get_metrics: ___get_metrics_spec[typing_extensions.Self]
265
261
 
266
- class ___get_container_metrics_spec(typing_extensions.Protocol[SUPERSELF]):
267
- def __call__(
268
- self, /, container_id: str
269
- ) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
270
- async def aio(
271
- self, /, container_id: str
272
- ) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
273
-
274
- _get_container_metrics: ___get_container_metrics_spec[typing_extensions.Self]
275
-
276
262
  class ___get_all_containers_spec(typing_extensions.Protocol[SUPERSELF]):
277
263
  def __call__(self, /): ...
278
264
  async def aio(self, /): ...
modal/file_io.py CHANGED
@@ -13,7 +13,6 @@ import json
13
13
  from grpclib.exceptions import GRPCError, StreamTerminatedError
14
14
 
15
15
  from modal._utils.async_utils import TaskContext
16
- from modal._utils.grpc_utils import retry_transient_errors
17
16
  from modal.exception import ClientClosed
18
17
  from modal_proto import api_pb2
19
18
 
@@ -57,8 +56,7 @@ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optio
57
56
  if start is not None and end is not None:
58
57
  if start >= end:
59
58
  raise ValueError("start must be less than end")
60
- resp = await retry_transient_errors(
61
- file._client.stub.ContainerFilesystemExec,
59
+ resp = await file._client.stub.ContainerFilesystemExec(
62
60
  api_pb2.ContainerFilesystemExecRequest(
63
61
  file_delete_bytes_request=api_pb2.ContainerFileDeleteBytesRequest(
64
62
  file_descriptor=file._file_descriptor,
@@ -85,8 +83,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
85
83
  raise InvalidError("start must be less than end")
86
84
  if len(data) > WRITE_CHUNK_SIZE:
87
85
  raise InvalidError("Write request payload exceeds 16 MiB limit")
88
- resp = await retry_transient_errors(
89
- file._client.stub.ContainerFilesystemExec,
86
+ resp = await file._client.stub.ContainerFilesystemExec(
90
87
  api_pb2.ContainerFilesystemExecRequest(
91
88
  file_write_replace_bytes_request=api_pb2.ContainerFileWriteReplaceBytesRequest(
92
89
  file_descriptor=file._file_descriptor,
@@ -261,8 +258,7 @@ class _FileIO(Generic[T]):
261
258
  raise TypeError("Expected str when in text mode")
262
259
 
263
260
  async def _open_file(self, path: str, mode: str) -> None:
264
- resp = await retry_transient_errors(
265
- self._client.stub.ContainerFilesystemExec,
261
+ resp = await self._client.stub.ContainerFilesystemExec(
266
262
  api_pb2.ContainerFilesystemExecRequest(
267
263
  file_open_request=api_pb2.ContainerFileOpenRequest(path=path, mode=mode),
268
264
  task_id=self._task_id,
@@ -285,8 +281,7 @@ class _FileIO(Generic[T]):
285
281
  return self
286
282
 
287
283
  async def _make_read_request(self, n: Optional[int]) -> bytes:
288
- resp = await retry_transient_errors(
289
- self._client.stub.ContainerFilesystemExec,
284
+ resp = await self._client.stub.ContainerFilesystemExec(
290
285
  api_pb2.ContainerFilesystemExecRequest(
291
286
  file_read_request=api_pb2.ContainerFileReadRequest(file_descriptor=self._file_descriptor, n=n),
292
287
  task_id=self._task_id,
@@ -309,8 +304,7 @@ class _FileIO(Generic[T]):
309
304
  """Read a single line from the current position."""
310
305
  self._check_closed()
311
306
  self._check_readable()
312
- resp = await retry_transient_errors(
313
- self._client.stub.ContainerFilesystemExec,
307
+ resp = await self._client.stub.ContainerFilesystemExec(
314
308
  api_pb2.ContainerFilesystemExecRequest(
315
309
  file_read_line_request=api_pb2.ContainerFileReadLineRequest(file_descriptor=self._file_descriptor),
316
310
  task_id=self._task_id,
@@ -351,8 +345,7 @@ class _FileIO(Generic[T]):
351
345
  raise ValueError("Write request payload exceeds 1 GiB limit")
352
346
  for i in range(0, len(data), WRITE_CHUNK_SIZE):
353
347
  chunk = data[i : i + WRITE_CHUNK_SIZE]
354
- resp = await retry_transient_errors(
355
- self._client.stub.ContainerFilesystemExec,
348
+ resp = await self._client.stub.ContainerFilesystemExec(
356
349
  api_pb2.ContainerFilesystemExecRequest(
357
350
  file_write_request=api_pb2.ContainerFileWriteRequest(
358
351
  file_descriptor=self._file_descriptor,
@@ -367,8 +360,7 @@ class _FileIO(Generic[T]):
367
360
  """Flush the buffer to disk."""
368
361
  self._check_closed()
369
362
  self._check_writable()
370
- resp = await retry_transient_errors(
371
- self._client.stub.ContainerFilesystemExec,
363
+ resp = await self._client.stub.ContainerFilesystemExec(
372
364
  api_pb2.ContainerFilesystemExecRequest(
373
365
  file_flush_request=api_pb2.ContainerFileFlushRequest(file_descriptor=self._file_descriptor),
374
366
  task_id=self._task_id,
@@ -393,8 +385,7 @@ class _FileIO(Generic[T]):
393
385
  (relative to the current position) and 2 (relative to the file's end).
394
386
  """
395
387
  self._check_closed()
396
- resp = await retry_transient_errors(
397
- self._client.stub.ContainerFilesystemExec,
388
+ resp = await self._client.stub.ContainerFilesystemExec(
398
389
  api_pb2.ContainerFilesystemExecRequest(
399
390
  file_seek_request=api_pb2.ContainerFileSeekRequest(
400
391
  file_descriptor=self._file_descriptor,
@@ -410,8 +401,7 @@ class _FileIO(Generic[T]):
410
401
  async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
411
402
  """List the contents of the provided directory."""
412
403
  self = _FileIO(client, task_id)
413
- resp = await retry_transient_errors(
414
- self._client.stub.ContainerFilesystemExec,
404
+ resp = await self._client.stub.ContainerFilesystemExec(
415
405
  api_pb2.ContainerFilesystemExecRequest(
416
406
  file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
417
407
  task_id=task_id,
@@ -427,8 +417,7 @@ class _FileIO(Generic[T]):
427
417
  async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
428
418
  """Create a new directory."""
429
419
  self = _FileIO(client, task_id)
430
- resp = await retry_transient_errors(
431
- self._client.stub.ContainerFilesystemExec,
420
+ resp = await self._client.stub.ContainerFilesystemExec(
432
421
  api_pb2.ContainerFilesystemExecRequest(
433
422
  file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
434
423
  task_id=self._task_id,
@@ -440,8 +429,7 @@ class _FileIO(Generic[T]):
440
429
  async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
441
430
  """Remove a file or directory in the Sandbox."""
442
431
  self = _FileIO(client, task_id)
443
- resp = await retry_transient_errors(
444
- self._client.stub.ContainerFilesystemExec,
432
+ resp = await self._client.stub.ContainerFilesystemExec(
445
433
  api_pb2.ContainerFilesystemExecRequest(
446
434
  file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
447
435
  task_id=self._task_id,
@@ -460,8 +448,7 @@ class _FileIO(Generic[T]):
460
448
  timeout: Optional[int] = None,
461
449
  ) -> AsyncIterator[FileWatchEvent]:
462
450
  self = _FileIO(client, task_id)
463
- resp = await retry_transient_errors(
464
- self._client.stub.ContainerFilesystemExec,
451
+ resp = await self._client.stub.ContainerFilesystemExec(
465
452
  api_pb2.ContainerFilesystemExecRequest(
466
453
  file_watch_request=api_pb2.ContainerFileWatchRequest(
467
454
  path=path,
@@ -503,8 +490,7 @@ class _FileIO(Generic[T]):
503
490
 
504
491
  async def _close(self) -> None:
505
492
  # Buffer is flushed by the runner on close
506
- resp = await retry_transient_errors(
507
- self._client.stub.ContainerFilesystemExec,
493
+ resp = await self._client.stub.ContainerFilesystemExec(
508
494
  api_pb2.ContainerFilesystemExecRequest(
509
495
  file_close_request=api_pb2.ContainerFileCloseRequest(file_descriptor=self._file_descriptor),
510
496
  task_id=self._task_id,
modal/functions.pyi CHANGED
@@ -401,7 +401,7 @@ class Function(
401
401
 
402
402
  _call_generator: ___call_generator_spec[typing_extensions.Self]
403
403
 
404
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
404
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
405
405
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
406
406
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
407
407
  ...
@@ -410,7 +410,7 @@ class Function(
410
410
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
411
411
  ...
412
412
 
413
- remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
413
+ remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
414
414
 
415
415
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
416
416
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -437,7 +437,7 @@ class Function(
437
437
  """
438
438
  ...
439
439
 
440
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
440
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
441
441
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
442
442
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
443
443
 
@@ -461,7 +461,7 @@ class Function(
461
461
  ...
462
462
 
463
463
  _experimental_spawn: ___experimental_spawn_spec[
464
- modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
464
+ modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
465
465
  ]
466
466
 
467
467
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -470,7 +470,7 @@ class Function(
470
470
 
471
471
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
472
472
 
473
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
473
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
474
474
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
475
475
  """Calls the function with the given arguments, without waiting for the results.
476
476
 
@@ -491,7 +491,7 @@ class Function(
491
491
  """
492
492
  ...
493
493
 
494
- spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
494
+ spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
495
495
 
496
496
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
497
497
  """Return the inner Python object wrapped by this Modal Function."""
modal/image.py CHANGED
@@ -38,7 +38,8 @@ from ._utils.docker_utils import (
38
38
  find_dockerignore_file,
39
39
  )
40
40
  from ._utils.function_utils import FunctionInfo
41
- from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
41
+ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
42
+ from ._utils.mount_utils import validate_only_modal_volumes
42
43
  from .client import _Client
43
44
  from .cloud_bucket_mount import _CloudBucketMount
44
45
  from .config import config, logger, user_config_path
@@ -487,6 +488,7 @@ class _Image(_Object, type_prefix="im"):
487
488
  context_mount_function: Optional[Callable[[], Optional[_Mount]]] = None,
488
489
  force_build: bool = False,
489
490
  build_args: dict[str, str] = {},
491
+ validated_volumes: Optional[Sequence[tuple[str, _Volume]]] = None,
490
492
  # For internal use only.
491
493
  _namespace: "api_pb2.DeploymentNamespace.ValueType" = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
492
494
  _do_assert_no_mount_layers: bool = True,
@@ -494,6 +496,9 @@ class _Image(_Object, type_prefix="im"):
494
496
  if base_images is None:
495
497
  base_images = {}
496
498
 
499
+ if validated_volumes is None:
500
+ validated_volumes = []
501
+
497
502
  if secrets is None:
498
503
  secrets = []
499
504
  if gpu_config is None:
@@ -514,6 +519,8 @@ class _Image(_Object, type_prefix="im"):
514
519
  deps += (build_function,)
515
520
  if image_registry_config and image_registry_config.secret:
516
521
  deps += (image_registry_config.secret,)
522
+ for _, vol in validated_volumes:
523
+ deps += (vol,)
517
524
  return deps
518
525
 
519
526
  async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
@@ -592,6 +599,17 @@ class _Image(_Object, type_prefix="im"):
592
599
  build_function_id = ""
593
600
  _build_function = None
594
601
 
602
+ # Relies on dicts being ordered (true as of Python 3.6).
603
+ volume_mounts = [
604
+ api_pb2.VolumeMount(
605
+ mount_path=path,
606
+ volume_id=volume.object_id,
607
+ allow_background_commits=True,
608
+ read_only=volume._read_only,
609
+ )
610
+ for path, volume in validated_volumes
611
+ ]
612
+
595
613
  image_definition = api_pb2.Image(
596
614
  base_images=base_images_pb2s,
597
615
  dockerfile_commands=dockerfile.commands,
@@ -604,6 +622,7 @@ class _Image(_Object, type_prefix="im"):
604
622
  runtime_debug=config.get("function_runtime_debug"),
605
623
  build_function=_build_function,
606
624
  build_args=build_args,
625
+ volume_mounts=volume_mounts,
607
626
  )
608
627
 
609
628
  req = api_pb2.ImageGetOrCreateRequest(
@@ -619,7 +638,7 @@ class _Image(_Object, type_prefix="im"):
619
638
  allow_global_deployment=os.environ.get("MODAL_IMAGE_ALLOW_GLOBAL_DEPLOYMENT") == "1",
620
639
  ignore_cache=config.get("ignore_cache"),
621
640
  )
622
- resp = await retry_transient_errors(resolver.client.stub.ImageGetOrCreate, req)
641
+ resp = await resolver.client.stub.ImageGetOrCreate(req)
623
642
  image_id = resp.image_id
624
643
  result: api_pb2.GenericResult
625
644
  metadata: Optional[api_pb2.ImageMetadata] = None
@@ -848,7 +867,7 @@ class _Image(_Object, type_prefix="im"):
848
867
  client = await _Client.from_env()
849
868
 
850
869
  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))
870
+ resp = await client.stub.ImageFromId(api_pb2.ImageFromIdRequest(image_id=image_id))
852
871
  self._hydrate(resp.image_id, resolver.client, resp.metadata)
853
872
 
854
873
  rep = f"Image.from_id({image_id!r})"
@@ -1690,6 +1709,7 @@ class _Image(_Object, type_prefix="im"):
1690
1709
  *commands: Union[str, list[str]],
1691
1710
  env: Optional[dict[str, Optional[str]]] = None,
1692
1711
  secrets: Optional[Collection[_Secret]] = None,
1712
+ volumes: Optional[dict[Union[str, PurePosixPath], _Volume]] = None,
1693
1713
  gpu: GPU_T = None,
1694
1714
  force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
1695
1715
  ) -> "_Image":
@@ -1712,6 +1732,7 @@ class _Image(_Object, type_prefix="im"):
1712
1732
  secrets=secrets,
1713
1733
  gpu_config=parse_gpu_config(gpu),
1714
1734
  force_build=self.force_build or force_build,
1735
+ validated_volumes=validate_only_modal_volumes(volumes, "Image.run_commands"),
1715
1736
  )
1716
1737
 
1717
1738
  @staticmethod
modal/image.pyi CHANGED
@@ -176,6 +176,7 @@ class _Image(modal._object._Object):
176
176
  ] = None,
177
177
  force_build: bool = False,
178
178
  build_args: dict[str, str] = {},
179
+ validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume._Volume]]] = None,
179
180
  _namespace: int = 1,
180
181
  _do_assert_no_mount_layers: bool = True,
181
182
  ): ...
@@ -668,6 +669,7 @@ class _Image(modal._object._Object):
668
669
  *commands: typing.Union[str, list[str]],
669
670
  env: typing.Optional[dict[str, typing.Optional[str]]] = None,
670
671
  secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
672
+ volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume._Volume]] = None,
671
673
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
672
674
  force_build: bool = False,
673
675
  ) -> _Image:
@@ -1091,6 +1093,7 @@ class Image(modal.object.Object):
1091
1093
  ] = None,
1092
1094
  force_build: bool = False,
1093
1095
  build_args: dict[str, str] = {},
1096
+ validated_volumes: typing.Optional[collections.abc.Sequence[tuple[str, modal.volume.Volume]]] = None,
1094
1097
  _namespace: int = 1,
1095
1098
  _do_assert_no_mount_layers: bool = True,
1096
1099
  ): ...
@@ -1648,6 +1651,7 @@ class Image(modal.object.Object):
1648
1651
  *commands: typing.Union[str, list[str]],
1649
1652
  env: typing.Optional[dict[str, typing.Optional[str]]] = None,
1650
1653
  secrets: typing.Optional[collections.abc.Collection[modal.secret.Secret]] = None,
1654
+ volumes: typing.Optional[dict[typing.Union[str, pathlib.PurePosixPath], modal.volume.Volume]] = None,
1651
1655
  gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
1652
1656
  force_build: bool = False,
1653
1657
  ) -> Image: