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/_functions.py CHANGED
@@ -6,21 +6,21 @@ import textwrap
6
6
  import time
7
7
  import typing
8
8
  import warnings
9
- from collections.abc import AsyncGenerator, Sequence, Sized
9
+ from collections.abc import AsyncGenerator, Collection, Sequence, Sized
10
10
  from dataclasses import dataclass
11
11
  from pathlib import PurePosixPath
12
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union
12
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
13
13
 
14
14
  import typing_extensions
15
15
  from google.protobuf.message import Message
16
16
  from grpclib import GRPCError, Status
17
17
  from synchronicity.combined_types import MethodWithAio
18
- from synchronicity.exceptions import UserCodeException
19
18
 
20
19
  from modal_proto import api_pb2
21
20
  from modal_proto.modal_api_grpc import ModalClientModal
22
21
 
23
- from ._object import _get_environment_name, _Object, live_method, live_method_gen
22
+ from ._load_context import LoadContext
23
+ from ._object import _Object, live_method, live_method_gen
24
24
  from ._pty import get_pty_info
25
25
  from ._resolver import Resolver
26
26
  from ._resources import convert_fn_config_to_resources_config
@@ -41,21 +41,20 @@ from ._utils.async_utils import (
41
41
  synchronizer,
42
42
  warn_if_generator_is_not_consumed,
43
43
  )
44
- from ._utils.deprecation import deprecation_warning
44
+ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
45
+ from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
45
46
  from ._utils.function_utils import (
46
47
  ATTEMPT_TIMEOUT_GRACE_PERIOD,
47
48
  OUTPUTS_TIMEOUT,
48
49
  FunctionCreationStatus,
49
50
  FunctionInfo,
50
- IncludeSourceMode,
51
51
  _create_input,
52
52
  _process_result,
53
53
  _stream_function_call_data,
54
54
  get_function_type,
55
- get_include_source_mode,
56
55
  is_async,
57
56
  )
58
- from ._utils.grpc_utils import RetryWarningMessage, retry_transient_errors
57
+ from ._utils.grpc_utils import Retry, RetryWarningMessage
59
58
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
60
59
  from .call_graph import InputInfo, _reconstruct_call_graph
61
60
  from .client import _Client
@@ -63,8 +62,6 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
63
62
  from .config import config
64
63
  from .exception import (
65
64
  ExecutionError,
66
- FunctionTimeoutError,
67
- InternalFailure,
68
65
  InvalidError,
69
66
  NotFoundError,
70
67
  OutputExpiredError,
@@ -75,12 +72,16 @@ from .mount import _get_client_mount, _Mount
75
72
  from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
76
73
  from .output import _get_output_manager
77
74
  from .parallel_map import (
75
+ _experimental_spawn_map_async,
76
+ _experimental_spawn_map_sync,
78
77
  _for_each_async,
79
78
  _for_each_sync,
80
79
  _map_async,
81
80
  _map_invocation,
81
+ _map_invocation_inputplane,
82
82
  _map_sync,
83
83
  _spawn_map_async,
84
+ _spawn_map_invocation,
84
85
  _spawn_map_sync,
85
86
  _starmap_async,
86
87
  _starmap_sync,
@@ -94,12 +95,14 @@ from .secret import _Secret
94
95
  from .volume import _Volume
95
96
 
96
97
  if TYPE_CHECKING:
97
- import modal._partial_function
98
98
  import modal.app
99
99
  import modal.cls
100
- import modal.partial_function
101
100
 
102
101
  MAX_INTERNAL_FAILURE_COUNT = 8
102
+ TERMINAL_STATUSES = (
103
+ api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
104
+ api_pb2.GenericResult.GENERIC_STATUS_TERMINATED,
105
+ )
103
106
 
104
107
 
105
108
  @dataclasses.dataclass
@@ -144,7 +147,13 @@ class _Invocation:
144
147
  stub = client.stub
145
148
 
146
149
  function_id = function.object_id
147
- item = await _create_input(args, kwargs, stub, method_name=function._use_method_name)
150
+ item = await _create_input(
151
+ args,
152
+ kwargs,
153
+ stub,
154
+ function=function,
155
+ function_call_invocation_type=function_call_invocation_type,
156
+ )
148
157
 
149
158
  request = api_pb2.FunctionMapRequest(
150
159
  function_id=function_id,
@@ -156,21 +165,22 @@ class _Invocation:
156
165
 
157
166
  if from_spawn_map:
158
167
  request.from_spawn_map = True
159
- response = await retry_transient_errors(
160
- client.stub.FunctionMap,
168
+ response = await client.stub.FunctionMap(
161
169
  request,
162
- max_retries=None,
163
- max_delay=30.0,
164
- retry_warning_message=RetryWarningMessage(
165
- message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
166
- "more function calls. This may be due to hitting rate limits or function backlog limits.",
167
- warning_interval=10,
168
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
170
+ retry=Retry(
171
+ max_retries=None,
172
+ max_delay=30.0,
173
+ warning_message=RetryWarningMessage(
174
+ message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
175
+ "more function calls. This may be due to hitting rate limits or function backlog limits.",
176
+ warning_interval=10,
177
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
178
+ ),
179
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
169
180
  ),
170
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
171
181
  )
172
182
  else:
173
- response = await retry_transient_errors(client.stub.FunctionMap, request)
183
+ response = await client.stub.FunctionMap(request)
174
184
 
175
185
  function_call_id = response.function_call_id
176
186
  if response.pipelined_inputs:
@@ -190,10 +200,7 @@ class _Invocation:
190
200
  request_put = api_pb2.FunctionPutInputsRequest(
191
201
  function_id=function_id, inputs=[item], function_call_id=function_call_id
192
202
  )
193
- inputs_response: api_pb2.FunctionPutInputsResponse = await retry_transient_errors(
194
- client.stub.FunctionPutInputs,
195
- request_put,
196
- )
203
+ inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
197
204
  processed_inputs = inputs_response.inputs
198
205
  if not processed_inputs:
199
206
  raise Exception("Could not create function call - the input queue seems to be full")
@@ -210,7 +217,11 @@ class _Invocation:
210
217
  return _Invocation(stub, function_call_id, client, retry_context)
211
218
 
212
219
  async def pop_function_call_outputs(
213
- self, timeout: Optional[float], clear_on_success: bool, input_jwts: Optional[list[str]] = None
220
+ self,
221
+ index: int = 0,
222
+ timeout: Optional[float] = None,
223
+ clear_on_success: bool = False,
224
+ input_jwts: Optional[list[str]] = None,
214
225
  ) -> api_pb2.FunctionGetOutputsResponse:
215
226
  t0 = time.time()
216
227
  if timeout is None:
@@ -228,11 +239,12 @@ class _Invocation:
228
239
  clear_on_success=clear_on_success,
229
240
  requested_at=time.time(),
230
241
  input_jwts=input_jwts,
242
+ start_idx=index,
243
+ end_idx=index,
231
244
  )
232
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
233
- self.stub.FunctionGetOutputs,
245
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
234
246
  request,
235
- attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
247
+ retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
236
248
  )
237
249
 
238
250
  if len(response.outputs) > 0:
@@ -252,21 +264,19 @@ class _Invocation:
252
264
 
253
265
  item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
254
266
  request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
255
- await retry_transient_errors(
256
- self.stub.FunctionRetryInputs,
257
- request,
258
- )
267
+ await self.stub.FunctionRetryInputs(request)
259
268
 
260
- async def _get_single_output(self, expected_jwt: Optional[str] = None) -> Any:
269
+ async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
261
270
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
262
271
  item: api_pb2.FunctionGetOutputsItem = (
263
272
  await self.pop_function_call_outputs(
273
+ index=0,
264
274
  timeout=None,
265
275
  clear_on_success=True,
266
276
  input_jwts=[expected_jwt] if expected_jwt else None,
267
277
  )
268
278
  ).outputs[0]
269
- return await _process_result(item.result, item.data_format, self.stub, self.client)
279
+ return item
270
280
 
271
281
  async def run_function(self) -> Any:
272
282
  # Use retry logic only if retry policy is specified and
@@ -278,33 +288,38 @@ class _Invocation:
278
288
  or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
279
289
  or not ctx.sync_client_retries_enabled
280
290
  ):
281
- return await self._get_single_output()
291
+ item = await self._get_single_output()
292
+ return await _process_result(item.result, item.data_format, self.stub, self.client)
282
293
 
283
294
  # User errors including timeouts are managed by the user specified retry policy.
284
295
  user_retry_manager = RetryManager(ctx.retry_policy)
285
296
 
286
297
  while True:
287
- try:
288
- return await self._get_single_output(ctx.input_jwt)
289
- except (UserCodeException, FunctionTimeoutError) as exc:
298
+ item = await self._get_single_output(ctx.input_jwt)
299
+ if item.result.status in TERMINAL_STATUSES:
300
+ return await _process_result(item.result, item.data_format, self.stub, self.client)
301
+
302
+ if item.result.status != api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
303
+ # non-internal failures get a delay before retrying
290
304
  delay_ms = user_retry_manager.get_delay_ms()
291
305
  if delay_ms is None:
292
- raise exc
306
+ # no more retries, this should raise an error when the non-success status is converted
307
+ # to an exception:
308
+ return await _process_result(item.result, item.data_format, self.stub, self.client)
293
309
  await asyncio.sleep(delay_ms / 1000)
294
- except InternalFailure:
295
- # For system failures on the server, we retry immediately,
296
- # and the failure does not count towards the retry policy.
297
- pass
310
+
298
311
  await self._retry_input()
299
312
 
300
- async def poll_function(self, timeout: Optional[float] = None):
313
+ async def poll_function(self, timeout: Optional[float] = None, *, index: int = 0):
301
314
  """Waits up to timeout for a result from a function.
302
315
 
303
316
  If timeout is `None`, waits indefinitely. This function is not
304
317
  cancellation-safe.
305
318
  """
306
319
  response: api_pb2.FunctionGetOutputsResponse = await self.pop_function_call_outputs(
307
- timeout=timeout, clear_on_success=False
320
+ index=index,
321
+ timeout=timeout,
322
+ clear_on_success=False,
308
323
  )
309
324
  if len(response.outputs) == 0 and response.num_unfinished_inputs == 0:
310
325
  # if no unfinished inputs and no outputs, then function expired
@@ -322,7 +337,7 @@ class _Invocation:
322
337
  items_total: Union[int, None] = None
323
338
  async with aclosing(
324
339
  async_merge(
325
- _stream_function_call_data(self.client, self.function_call_id, variant="data_out"),
340
+ _stream_function_call_data(self.client, None, self.function_call_id, variant="data_out"),
326
341
  callable_to_agen(self.run_function),
327
342
  )
328
343
  ) as streamer:
@@ -337,11 +352,45 @@ class _Invocation:
337
352
  if items_total is not None and items_received >= items_total:
338
353
  break
339
354
 
355
+ async def enumerate(self, start_index: int, end_index: int):
356
+ """Iterate over the results of the function call in the range [start_index, end_index)."""
357
+ limit = 49
358
+ current_index = start_index
359
+ while current_index < end_index:
360
+ # batch_end_indx is inclusive, so we subtract 1 to get the last index in the batch.
361
+ batch_end_index = min(current_index + limit, end_index) - 1
362
+ request = api_pb2.FunctionGetOutputsRequest(
363
+ function_call_id=self.function_call_id,
364
+ timeout=0,
365
+ last_entry_id="0-0",
366
+ clear_on_success=False,
367
+ requested_at=time.time(),
368
+ start_idx=current_index,
369
+ end_idx=batch_end_index,
370
+ )
371
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
372
+ request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
373
+ )
374
+
375
+ outputs = list(response.outputs)
376
+ outputs.sort(key=lambda x: x.idx)
377
+ for output in outputs:
378
+ if output.idx != current_index:
379
+ break
380
+ result = await _process_result(output.result, output.data_format, self.stub, self.client)
381
+ yield output.idx, result
382
+ current_index += 1
383
+
384
+ # We're missing current_index, so we need to poll the function for the next result
385
+ if len(outputs) < (batch_end_index - current_index + 1):
386
+ result = await self.poll_function(index=current_index)
387
+ yield current_index, result
388
+ current_index += 1
389
+
340
390
 
341
391
  class _InputPlaneInvocation:
342
392
  """Internal client representation of a single-input call to a Modal Function using the input
343
- plane server API. As of 4/22/2025, this class is experimental and not used in production.
344
- It is OK to make breaking changes to this class."""
393
+ plane server API."""
345
394
 
346
395
  stub: ModalClientModal
347
396
 
@@ -352,12 +401,16 @@ class _InputPlaneInvocation:
352
401
  client: _Client,
353
402
  input_item: api_pb2.FunctionPutInputsItem,
354
403
  function_id: str,
404
+ retry_policy: api_pb2.FunctionRetryPolicy,
405
+ input_plane_region: str,
355
406
  ):
356
407
  self.stub = stub
357
408
  self.client = client # Used by the deserializer.
358
409
  self.attempt_token = attempt_token
359
410
  self.input_item = input_item
360
411
  self.function_id = function_id
412
+ self.retry_policy = retry_policy
413
+ self.input_plane_region = input_plane_region
361
414
 
362
415
  @staticmethod
363
416
  async def create(
@@ -367,25 +420,39 @@ class _InputPlaneInvocation:
367
420
  *,
368
421
  client: _Client,
369
422
  input_plane_url: str,
423
+ input_plane_region: str,
370
424
  ) -> "_InputPlaneInvocation":
371
425
  stub = await client.get_stub(input_plane_url)
372
426
 
373
427
  function_id = function.object_id
374
- input_item = await _create_input(args, kwargs, stub, method_name=function._use_method_name)
428
+ control_plane_stub = client.stub
429
+ # Note: Blob upload is done on the control plane stub, not the input plane stub!
430
+ input_item = await _create_input(
431
+ args,
432
+ kwargs,
433
+ control_plane_stub,
434
+ function=function,
435
+ )
375
436
 
376
437
  request = api_pb2.AttemptStartRequest(
377
438
  function_id=function_id,
378
439
  parent_input_id=current_input_id() or "",
379
440
  input=input_item,
380
441
  )
381
- response = await retry_transient_errors(stub.AttemptStart, request)
442
+
443
+ metadata = await client.get_input_plane_metadata(input_plane_region)
444
+ response = await stub.AttemptStart(request, metadata=metadata)
382
445
  attempt_token = response.attempt_token
383
446
 
384
- return _InputPlaneInvocation(stub, attempt_token, client, input_item, function_id)
447
+ return _InputPlaneInvocation(
448
+ stub, attempt_token, client, input_item, function_id, response.retry_policy, input_plane_region
449
+ )
385
450
 
386
451
  async def run_function(self) -> Any:
452
+ # User errors including timeouts are managed by the user-specified retry policy.
453
+ user_retry_manager = RetryManager(self.retry_policy)
454
+
387
455
  # This will retry when the server returns GENERIC_STATUS_INTERNAL_FAILURE, i.e. lost inputs or worker preemption
388
- # TODO(ryan): add logic to retry for user defined retry policy
389
456
  internal_failure_count = 0
390
457
  while True:
391
458
  await_request = api_pb2.AttemptAwaitRequest(
@@ -393,33 +460,85 @@ class _InputPlaneInvocation:
393
460
  timeout_secs=OUTPUTS_TIMEOUT,
394
461
  requested_at=time.time(),
395
462
  )
396
- await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
397
- self.stub.AttemptAwait,
463
+ metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
464
+ await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
398
465
  await_request,
399
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
466
+ retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
467
+ metadata=metadata,
400
468
  )
401
469
 
402
- try:
403
- if await_response.HasField("output"):
404
- return await _process_result(
405
- await_response.output.result, await_response.output.data_format, self.stub, self.client
406
- )
407
- except InternalFailure as e:
470
+ # Keep awaiting until we get an output.
471
+ if not await_response.HasField("output"):
472
+ continue
473
+
474
+ # If we have a final output, return.
475
+ if await_response.output.result.status in TERMINAL_STATUSES:
476
+ return await _process_result(
477
+ await_response.output.result, await_response.output.data_format, self.client.stub, self.client
478
+ )
479
+
480
+ # We have a failure (internal or application), so see if there are any retries left, and if so, retry.
481
+ if await_response.output.result.status == api_pb2.GenericResult.GENERIC_STATUS_INTERNAL_FAILURE:
408
482
  internal_failure_count += 1
409
- # Limit the number of times we retry
410
- if internal_failure_count >= MAX_INTERNAL_FAILURE_COUNT:
411
- raise e
412
- # For system failures on the server, we retry immediately,
413
- # and the failure does not count towards the retry policy.
414
- retry_request = api_pb2.AttemptRetryRequest(
415
- function_id=self.function_id,
416
- parent_input_id=current_input_id() or "",
417
- input=self.input_item,
483
+ # Limit the number of times we retry internal failures.
484
+ if internal_failure_count < MAX_INTERNAL_FAILURE_COUNT:
485
+ # We immediately retry internal failures and the failure doesn't count towards the retry policy.
486
+ self.attempt_token = await self._retry_input(metadata)
487
+ continue
488
+ elif (delay_ms := user_retry_manager.get_delay_ms()) is not None:
489
+ # We still have user retries left, so sleep and retry.
490
+ await asyncio.sleep(delay_ms / 1000)
491
+ self.attempt_token = await self._retry_input(metadata)
492
+ continue
493
+
494
+ # No more retries left.
495
+ return await _process_result(
496
+ await_response.output.result, await_response.output.data_format, self.client.stub, self.client
497
+ )
498
+
499
+ async def _retry_input(self, metadata: list[tuple[str, str]]) -> str:
500
+ retry_request = api_pb2.AttemptRetryRequest(
501
+ function_id=self.function_id,
502
+ parent_input_id=current_input_id() or "",
503
+ input=self.input_item,
504
+ attempt_token=self.attempt_token,
505
+ )
506
+ retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
507
+ return retry_response.attempt_token
508
+
509
+ async def run_generator(self):
510
+ items_received = 0
511
+ # populated when self.run_function() completes
512
+ items_total: Union[int, None] = None
513
+ async with aclosing(
514
+ async_merge(
515
+ _stream_function_call_data(
516
+ self.client,
517
+ self.stub,
518
+ function_call_id=None,
519
+ variant="data_out",
418
520
  attempt_token=self.attempt_token,
419
- )
420
- # TODO(ryan): Add exponential backoff?
421
- retry_response = await retry_transient_errors(self.stub.AttemptRetry, retry_request)
422
- self.attempt_token = retry_response.attempt_token
521
+ ),
522
+ callable_to_agen(self.run_function),
523
+ )
524
+ ) as streamer:
525
+ async for item in streamer:
526
+ if isinstance(item, api_pb2.GeneratorDone):
527
+ items_total = item.items_total
528
+ else:
529
+ yield item
530
+ items_received += 1
531
+ # The comparison avoids infinite loops if a non-deterministic generator is retried
532
+ # and produces less data in the second run than what was already sent.
533
+ if items_total is not None and items_received >= items_total:
534
+ break
535
+
536
+ @staticmethod
537
+ async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
538
+ if not input_plane_region:
539
+ return []
540
+ token = await client._auth_token_manager.get_token()
541
+ return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
423
542
 
424
543
 
425
544
  # Wrapper type for api_pb2.FunctionStats
@@ -461,7 +580,7 @@ class _FunctionSpec:
461
580
 
462
581
  image: Optional[_Image]
463
582
  mounts: Sequence[_Mount]
464
- secrets: Sequence[_Secret]
583
+ secrets: Collection[_Secret]
465
584
  network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
466
585
  volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
467
586
  # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
@@ -474,6 +593,21 @@ class _FunctionSpec:
474
593
  proxy: Optional[_Proxy]
475
594
 
476
595
 
596
+ def _get_supported_input_output_formats(is_web_endpoint: bool, is_generator: bool, restrict_output: bool):
597
+ if is_web_endpoint:
598
+ supported_input_formats = [api_pb2.DATA_FORMAT_ASGI]
599
+ supported_output_formats = [api_pb2.DATA_FORMAT_ASGI, api_pb2.DATA_FORMAT_GENERATOR_DONE]
600
+ else:
601
+ supported_input_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
602
+ if restrict_output:
603
+ supported_output_formats = [api_pb2.DATA_FORMAT_CBOR]
604
+ else:
605
+ supported_output_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
606
+ if is_generator:
607
+ supported_output_formats.append(api_pb2.DATA_FORMAT_GENERATOR_DONE)
608
+ return supported_input_formats, supported_output_formats
609
+
610
+
477
611
  P = typing_extensions.ParamSpec("P")
478
612
  ReturnType = typing.TypeVar("ReturnType", covariant=True)
479
613
  OriginalReturnType = typing.TypeVar(
@@ -523,9 +657,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
523
657
  @staticmethod
524
658
  def from_local(
525
659
  info: FunctionInfo,
526
- app,
660
+ app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
527
661
  image: _Image,
528
- secrets: Sequence[_Secret] = (),
662
+ env: Optional[dict[str, Optional[str]]] = None,
663
+ secrets: Optional[Collection[_Secret]] = None,
529
664
  schedule: Optional[Schedule] = None,
530
665
  is_generator: bool = False,
531
666
  gpu: Union[GPU_T, list[GPU_T]] = None,
@@ -537,7 +672,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
537
672
  memory: Optional[Union[int, tuple[int, int]]] = None,
538
673
  proxy: Optional[_Proxy] = None,
539
674
  retries: Optional[Union[int, Retries]] = None,
540
- timeout: Optional[int] = None,
675
+ timeout: int = 300,
676
+ startup_timeout: Optional[int] = None,
541
677
  min_containers: Optional[int] = None,
542
678
  max_containers: Optional[int] = None,
543
679
  buffer_containers: Optional[int] = None,
@@ -559,14 +695,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
559
695
  rdma: Optional[bool] = None,
560
696
  max_inputs: Optional[int] = None,
561
697
  ephemeral_disk: Optional[int] = None,
562
- # current default: first-party, future default: main-package
563
- include_source: Optional[bool] = None,
698
+ include_source: bool = True,
564
699
  experimental_options: Optional[dict[str, str]] = None,
565
700
  _experimental_proxy_ip: Optional[str] = None,
566
701
  _experimental_custom_scaling_factor: Optional[float] = None,
567
- _experimental_enable_gpu_snapshot: bool = False,
702
+ restrict_output: bool = False,
568
703
  ) -> "_Function":
569
- """mdmd:hidden"""
704
+ """mdmd:hidden
705
+
706
+ Note: This is not intended to be public API.
707
+ """
570
708
  # Needed to avoid circular imports
571
709
  from ._partial_function import _find_partial_methods_for_user_cls, _PartialFunctionFlags
572
710
 
@@ -585,15 +723,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
585
723
  assert not webhook_config
586
724
  assert not schedule
587
725
 
588
- include_source_mode = get_include_source_mode(include_source)
589
- if include_source_mode != IncludeSourceMode.INCLUDE_NOTHING:
590
- entrypoint_mounts = info.get_entrypoint_mount()
591
- else:
592
- entrypoint_mounts = {}
593
-
726
+ entrypoint_mount = info.get_entrypoint_mount() if include_source else {}
594
727
  all_mounts = [
595
728
  _get_client_mount(),
596
- *entrypoint_mounts.values(),
729
+ *entrypoint_mount.values(),
597
730
  ]
598
731
 
599
732
  retry_policy = _parse_retries(
@@ -606,6 +739,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
606
739
  if is_generator:
607
740
  raise InvalidError("Generator functions do not support retries.")
608
741
 
742
+ if timeout is None: # type: ignore[unreachable] # Help users who aren't using type checkers
743
+ raise InvalidError("The `timeout` parameter cannot be set to None: https://modal.com/docs/guide/timeouts")
744
+
745
+ secrets = secrets or []
746
+ if env:
747
+ secrets = [*secrets, _Secret.from_dict(env)]
748
+
609
749
  function_spec = _FunctionSpec(
610
750
  mounts=all_mounts,
611
751
  secrets=secrets,
@@ -621,34 +761,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
621
761
  proxy=proxy,
622
762
  )
623
763
 
624
- if info.user_cls and not is_auto_snapshot:
625
- build_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.BUILD).items()
626
- for k, pf in build_functions:
627
- build_function = pf.raw_f
628
- snapshot_info = FunctionInfo(build_function, user_cls=info.user_cls)
629
- snapshot_function = _Function.from_local(
630
- snapshot_info,
631
- app=None,
632
- image=image,
633
- secrets=secrets,
634
- gpu=gpu,
635
- network_file_systems=network_file_systems,
636
- volumes=volumes,
637
- memory=memory,
638
- timeout=pf.params.build_timeout,
639
- cpu=cpu,
640
- ephemeral_disk=ephemeral_disk,
641
- is_builder_function=True,
642
- is_auto_snapshot=True,
643
- scheduler_placement=scheduler_placement,
644
- include_source=include_source,
645
- )
646
- image = _Image._from_args(
647
- base_images={"base": image},
648
- build_function=snapshot_function,
649
- force_build=image.force_build or bool(pf.params.force_build),
650
- )
651
-
652
764
  # Note that we also do these checks in FunctionCreate; could drop them here
653
765
  if min_containers is not None and not isinstance(min_containers, int):
654
766
  raise InvalidError(f"`min_containers` must be an int, not {type(min_containers).__name__}")
@@ -708,7 +820,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
708
820
  validated_network_file_systems = validate_network_file_systems(network_file_systems)
709
821
 
710
822
  # Validate image
711
- if image is not None and not isinstance(image, _Image):
823
+ if image is not None and not isinstance(image, _Image): # type: ignore[unreachable]
712
824
  raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
713
825
 
714
826
  method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
@@ -721,17 +833,23 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
721
833
  for method_name, partial_function in interface_methods.items():
722
834
  function_type = get_function_type(partial_function.params.is_generator)
723
835
  function_name = f"{info.user_cls.__name__}.{method_name}"
836
+ is_web_endpoint = partial_function._is_web_endpoint()
724
837
  method_schema = get_callable_schema(
725
838
  partial_function._get_raw_f(),
726
- is_web_endpoint=partial_function._is_web_endpoint(),
839
+ is_web_endpoint=is_web_endpoint,
727
840
  ignore_first_argument=True,
728
841
  )
842
+ method_input_formats, method_output_formats = _get_supported_input_output_formats(
843
+ is_web_endpoint, partial_function.params.is_generator or False, restrict_output
844
+ )
729
845
 
730
846
  method_definition = api_pb2.MethodDefinition(
731
847
  webhook_config=partial_function.params.webhook_config,
732
848
  function_type=function_type,
733
849
  function_name=function_name,
734
850
  function_schema=method_schema,
851
+ supported_input_formats=method_input_formats,
852
+ supported_output_formats=method_output_formats,
735
853
  )
736
854
  method_definitions[method_name] = method_definition
737
855
 
@@ -755,29 +873,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
755
873
 
756
874
  return deps
757
875
 
758
- async def _preload(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
759
- assert resolver.client and resolver.client.stub
876
+ if info.is_service_class():
877
+ # classes don't have data formats themselves - input/output formats are set per method above
878
+ supported_input_formats = []
879
+ supported_output_formats = []
880
+ else:
881
+ is_web_endpoint = webhook_config is not None and webhook_config.type != api_pb2.WEBHOOK_TYPE_UNSPECIFIED
882
+ supported_input_formats, supported_output_formats = _get_supported_input_output_formats(
883
+ is_web_endpoint, is_generator, restrict_output
884
+ )
760
885
 
761
- assert resolver.app_id
886
+ async def _preload(
887
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
888
+ ):
889
+ assert load_context.app_id
762
890
  req = api_pb2.FunctionPrecreateRequest(
763
- app_id=resolver.app_id,
891
+ app_id=load_context.app_id,
764
892
  function_name=info.function_name,
765
893
  function_type=function_type,
766
894
  existing_function_id=existing_object_id or "",
767
895
  function_schema=get_callable_schema(info.raw_f, is_web_endpoint=bool(webhook_config))
768
896
  if info.raw_f
769
897
  else None,
898
+ supported_input_formats=supported_input_formats,
899
+ supported_output_formats=supported_output_formats,
770
900
  )
771
901
  if method_definitions:
772
902
  for method_name, method_definition in method_definitions.items():
773
903
  req.method_definitions[method_name].CopyFrom(method_definition)
774
904
  elif webhook_config:
775
905
  req.webhook_config.CopyFrom(webhook_config)
776
- response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
777
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
778
906
 
779
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
780
- assert resolver.client and resolver.client.stub
907
+ response = await load_context.client.stub.FunctionPrecreate(req)
908
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
909
+
910
+ async def _load(
911
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
912
+ ):
781
913
  with FunctionCreationStatus(resolver, tag) as function_creation_status:
782
914
  timeout_secs = timeout
783
915
 
@@ -827,6 +959,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
827
959
  mount_path=path,
828
960
  volume_id=volume.object_id,
829
961
  allow_background_commits=True,
962
+ read_only=volume._read_only,
830
963
  )
831
964
  for path, volume in validated_volumes_no_cloud_buckets
832
965
  ]
@@ -843,6 +976,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
843
976
  function_schema = (
844
977
  get_callable_schema(info.raw_f, is_web_endpoint=bool(webhook_config)) if info.raw_f else None
845
978
  )
979
+
846
980
  # Create function remotely
847
981
  function_definition = api_pb2.Function(
848
982
  module_name=info.module_name or "",
@@ -863,6 +997,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
863
997
  proxy_id=(proxy.object_id if proxy else None),
864
998
  retry_policy=retry_policy,
865
999
  timeout_secs=timeout_secs or 0,
1000
+ startup_timeout_secs=startup_timeout or timeout_secs,
866
1001
  pty_info=pty_info,
867
1002
  cloud_provider_str=cloud if cloud else "",
868
1003
  runtime=config.get("function_runtime"),
@@ -896,7 +1031,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
896
1031
  _experimental_concurrent_cancellations=True,
897
1032
  _experimental_proxy_ip=_experimental_proxy_ip,
898
1033
  _experimental_custom_scaling=_experimental_custom_scaling_factor is not None,
899
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
900
1034
  # --- These are deprecated in favor of autoscaler_settings
901
1035
  warm_pool_size=min_containers or 0,
902
1036
  concurrency_limit=max_containers or 0,
@@ -904,6 +1038,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
904
1038
  task_idle_timeout_secs=scaledown_window or 0,
905
1039
  # ---
906
1040
  function_schema=function_schema,
1041
+ supported_input_formats=supported_input_formats,
1042
+ supported_output_formats=supported_output_formats,
907
1043
  )
908
1044
 
909
1045
  if isinstance(gpu, list):
@@ -917,6 +1053,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
917
1053
  autoscaler_settings=function_definition.autoscaler_settings,
918
1054
  worker_id=function_definition.worker_id,
919
1055
  timeout_secs=function_definition.timeout_secs,
1056
+ startup_timeout_secs=function_definition.startup_timeout_secs,
920
1057
  web_url=function_definition.web_url,
921
1058
  web_url_info=function_definition.web_url_info,
922
1059
  webhook_config=function_definition.webhook_config,
@@ -933,12 +1070,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
933
1070
  _experimental_group_size=function_definition._experimental_group_size,
934
1071
  _experimental_buffer_containers=function_definition._experimental_buffer_containers,
935
1072
  _experimental_custom_scaling=function_definition._experimental_custom_scaling,
936
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
937
1073
  _experimental_proxy_ip=function_definition._experimental_proxy_ip,
938
1074
  snapshot_debug=function_definition.snapshot_debug,
939
1075
  runtime_perf_record=function_definition.runtime_perf_record,
940
1076
  function_schema=function_schema,
941
1077
  untrusted=function_definition.untrusted,
1078
+ supported_input_formats=supported_input_formats,
1079
+ supported_output_formats=supported_output_formats,
942
1080
  )
943
1081
 
944
1082
  ranked_functions = []
@@ -967,18 +1105,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
967
1105
  ),
968
1106
  )
969
1107
 
970
- assert resolver.app_id
1108
+ assert load_context.app_id
971
1109
  assert (function_definition is None) != (function_data is None) # xor
972
1110
  request = api_pb2.FunctionCreateRequest(
973
- app_id=resolver.app_id,
1111
+ app_id=load_context.app_id,
974
1112
  function=function_definition,
975
1113
  function_data=function_data,
976
1114
  existing_function_id=existing_object_id or "",
977
1115
  )
978
1116
  try:
979
- response: api_pb2.FunctionCreateResponse = await retry_transient_errors(
980
- resolver.client.stub.FunctionCreate, request
981
- )
1117
+ response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
982
1118
  except GRPCError as exc:
983
1119
  if exc.status == Status.INVALID_ARGUMENT:
984
1120
  raise InvalidError(exc.message)
@@ -993,10 +1129,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
993
1129
  serve_mounts = {m for m in all_mounts if m.is_local()}
994
1130
  serve_mounts |= image._serve_mounts
995
1131
  obj._serve_mounts = frozenset(serve_mounts)
996
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1132
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
997
1133
 
998
1134
  rep = f"Function({tag})"
999
- obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps)
1135
+ # Pass a *reference* to the App's LoadContext - this is important since the App is
1136
+ # the only way to infer a LoadContext for an `@app.function`, and the App doesn't
1137
+ # get its client until *after* the Function is created.
1138
+ load_context = app._root_load_context if app else LoadContext.empty()
1139
+ obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
1000
1140
 
1001
1141
  obj._raw_f = info.raw_f
1002
1142
  obj._info = info
@@ -1038,7 +1178,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1038
1178
 
1039
1179
  parent = self
1040
1180
 
1041
- async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1181
+ async def _load(
1182
+ param_bound_func: _Function,
1183
+ resolver: Resolver,
1184
+ load_context: LoadContext,
1185
+ existing_object_id: Optional[str],
1186
+ ):
1042
1187
  if not parent.is_hydrated:
1043
1188
  # While the base Object.hydrate() method appears to be idempotent, it's not always safe
1044
1189
  await parent.hydrate()
@@ -1071,7 +1216,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1071
1216
  param_bound_func._hydrate_from_other(parent)
1072
1217
  return
1073
1218
 
1074
- environment_name = _get_environment_name(None, resolver)
1075
1219
  assert parent is not None and parent.is_hydrated
1076
1220
 
1077
1221
  if options:
@@ -1080,6 +1224,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1080
1224
  mount_path=path,
1081
1225
  volume_id=volume.object_id,
1082
1226
  allow_background_commits=True,
1227
+ read_only=volume._read_only,
1083
1228
  )
1084
1229
  for path, volume in options.validated_volumes
1085
1230
  ]
@@ -1088,6 +1233,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1088
1233
  replace_secret_ids=bool(options.secrets),
1089
1234
  replace_volume_mounts=len(volume_mounts) > 0,
1090
1235
  volume_mounts=volume_mounts,
1236
+ cloud_bucket_mounts=cloud_bucket_mounts_to_proto(options.cloud_bucket_mounts),
1237
+ replace_cloud_bucket_mounts=bool(options.cloud_bucket_mounts),
1091
1238
  resources=options.resources,
1092
1239
  retry_policy=options.retry_policy,
1093
1240
  concurrency_limit=options.max_containers,
@@ -1098,6 +1245,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1098
1245
  target_concurrent_inputs=options.target_concurrent_inputs,
1099
1246
  batch_max_size=options.batch_max_size,
1100
1247
  batch_linger_ms=options.batch_wait_ms,
1248
+ scheduler_placement=options.scheduler_placement,
1249
+ cloud_provider_str=options.cloud,
1101
1250
  )
1102
1251
  else:
1103
1252
  options_pb = None
@@ -1106,19 +1255,30 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1106
1255
  function_id=parent.object_id,
1107
1256
  serialized_params=serialized_params,
1108
1257
  function_options=options_pb,
1109
- environment_name=environment_name
1258
+ environment_name=load_context.environment_name
1110
1259
  or "", # TODO: investigate shouldn't environment name always be specified here?
1111
1260
  )
1112
1261
 
1113
- response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1262
+ response = await parent._client.stub.FunctionBindParams(req)
1114
1263
  param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1115
1264
 
1116
1265
  def _deps():
1117
1266
  if options:
1118
- return [v for _, v in options.validated_volumes] + list(options.secrets)
1267
+ all_deps = (
1268
+ [v for _, v in options.validated_volumes]
1269
+ + list(options.secrets)
1270
+ + [mount.secret for _, mount in options.cloud_bucket_mounts if mount.secret]
1271
+ )
1272
+ return [dep for dep in all_deps if not dep.is_hydrated]
1119
1273
  return []
1120
1274
 
1121
- fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True, deps=_deps)
1275
+ fun: _Function = _Function._from_loader(
1276
+ _load,
1277
+ "Function(parametrized)",
1278
+ hydrate_lazily=True,
1279
+ deps=_deps,
1280
+ load_context_overrides=self._load_context_overrides,
1281
+ )
1122
1282
 
1123
1283
  fun._info = self._info
1124
1284
  fun._obj = obj
@@ -1169,7 +1329,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1169
1329
  scaledown_window=scaledown_window,
1170
1330
  )
1171
1331
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1172
- await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
1332
+ await self.client.stub.FunctionUpdateSchedulingParams(request)
1173
1333
 
1174
1334
  # One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
1175
1335
  # and then we could return them here (would need some ad hoc dataclass, which I don't love)
@@ -1212,34 +1372,47 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1212
1372
  await self.update_autoscaler(min_containers=warm_pool_size)
1213
1373
 
1214
1374
  @classmethod
1215
- def _from_name(cls, app_name: str, name: str, namespace, environment_name: Optional[str]):
1375
+ def _from_name(
1376
+ cls,
1377
+ app_name: str,
1378
+ name: str,
1379
+ *,
1380
+ load_context_overrides: LoadContext,
1381
+ ):
1216
1382
  # internal function lookup implementation that allows lookup of class "service functions"
1217
1383
  # in addition to non-class functions
1218
- async def _load_remote(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1219
- assert resolver.client and resolver.client.stub
1384
+ async def _load_remote(
1385
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
1386
+ ):
1220
1387
  request = api_pb2.FunctionGetRequest(
1221
1388
  app_name=app_name,
1222
1389
  object_tag=name,
1223
- namespace=namespace,
1224
- environment_name=_get_environment_name(environment_name, resolver) or "",
1390
+ environment_name=load_context.environment_name,
1225
1391
  )
1226
1392
  try:
1227
- response = await retry_transient_errors(resolver.client.stub.FunctionGet, request)
1228
- except GRPCError as exc:
1229
- if exc.status == Status.NOT_FOUND:
1230
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
1231
- raise NotFoundError(
1232
- f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc.message}."
1233
- )
1234
- else:
1235
- raise
1393
+ response = await load_context.client.stub.FunctionGet(request)
1394
+ except NotFoundError as exc:
1395
+ # refine the error message
1396
+ env_context = (
1397
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
1398
+ )
1399
+ raise NotFoundError(
1400
+ f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
1401
+ ) from None
1236
1402
 
1237
1403
  print_server_warnings(response.server_warnings)
1238
1404
 
1239
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1405
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1240
1406
 
1241
- rep = f"Function.from_name({app_name}, {name})"
1242
- return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
1407
+ environment_rep = (
1408
+ f", environment_name={load_context_overrides.environment_name!r}"
1409
+ if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
1410
+ else ""
1411
+ )
1412
+ rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
1413
+ return cls._from_loader(
1414
+ _load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1415
+ )
1243
1416
 
1244
1417
  @classmethod
1245
1418
  def from_name(
@@ -1247,14 +1420,15 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1247
1420
  app_name: str,
1248
1421
  name: str,
1249
1422
  *,
1250
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
1423
+ namespace=None, # mdmd:line-hidden
1251
1424
  environment_name: Optional[str] = None,
1425
+ client: Optional[_Client] = None,
1252
1426
  ) -> "_Function":
1253
1427
  """Reference a Function from a deployed App by its name.
1254
1428
 
1255
- In contrast to `modal.Function.lookup`, this is a lazy method
1256
- that defers hydrating the local object with metadata from
1257
- Modal servers until the first time it is actually used.
1429
+ This is a lazy method that defers hydrating the local
1430
+ object with metadata from Modal servers until the first
1431
+ time it is actually used.
1258
1432
 
1259
1433
  ```python
1260
1434
  f = modal.Function.from_name("other-app", "function")
@@ -1271,40 +1445,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1271
1445
  f"instance.{method_name}.remote(...)\n",
1272
1446
  )
1273
1447
 
1274
- return cls._from_name(app_name, name, namespace, environment_name)
1275
-
1276
- @staticmethod
1277
- async def lookup(
1278
- app_name: str,
1279
- name: str,
1280
- namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
1281
- client: Optional[_Client] = None,
1282
- environment_name: Optional[str] = None,
1283
- ) -> "_Function":
1284
- """mdmd:hidden
1285
- Lookup a Function from a deployed App by its name.
1286
-
1287
- DEPRECATED: This method is deprecated in favor of `modal.Function.from_name`.
1288
-
1289
- In contrast to `modal.Function.from_name`, this is an eager method
1290
- that will hydrate the local object with metadata from Modal servers.
1291
-
1292
- ```python notest
1293
- f = modal.Function.lookup("other-app", "function")
1294
- ```
1295
- """
1296
- deprecation_warning(
1297
- (2025, 1, 27),
1298
- "`modal.Function.lookup` is deprecated and will be removed in a future release."
1299
- " It can be replaced with `modal.Function.from_name`."
1300
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
1448
+ warn_if_passing_namespace(namespace, "modal.Function.from_name")
1449
+ return cls._from_name(
1450
+ app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
1301
1451
  )
1302
- obj = _Function.from_name(app_name, name, namespace=namespace, environment_name=environment_name)
1303
- if client is None:
1304
- client = await _Client.from_env()
1305
- resolver = Resolver(client=client)
1306
- await resolver.load(obj)
1307
- return obj
1308
1452
 
1309
1453
  @property
1310
1454
  def tag(self) -> str:
@@ -1360,6 +1504,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1360
1504
  self._info = None
1361
1505
  self._serve_mounts = frozenset()
1362
1506
  self._metadata = None
1507
+ self._experimental_flash_urls = None
1363
1508
 
1364
1509
  def _hydrate_metadata(self, metadata: Optional[Message]):
1365
1510
  # Overridden concrete implementation of base class method
@@ -1377,6 +1522,17 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1377
1522
  self._method_handle_metadata = dict(metadata.method_handle_metadata)
1378
1523
  self._definition_id = metadata.definition_id
1379
1524
  self._input_plane_url = metadata.input_plane_url
1525
+ self._input_plane_region = metadata.input_plane_region
1526
+ # The server may pass back a larger max object size for some input plane users. This applies to input plane
1527
+ # users only - anyone using the control plane will get the standard limit.
1528
+ # There are some cases like FunctionPrecreate where this value is not set at all. We expect that this field
1529
+ # will eventually be hydrated with the correct value, but just to be defensive, if the field is not set we use
1530
+ # MAX_OBJECT_SIZE_BYTES, otherwise it would get set to 0. Accidentally using 0 would cause us to blob upload
1531
+ # everything, so let's avoid that.
1532
+ self._max_object_size_bytes = (
1533
+ metadata.max_object_size_bytes if metadata.HasField("max_object_size_bytes") else MAX_OBJECT_SIZE_BYTES
1534
+ )
1535
+ self._experimental_flash_urls = metadata._experimental_flash_urls
1380
1536
 
1381
1537
  def _get_metadata(self):
1382
1538
  # Overridden concrete implementation of base class method
@@ -1392,6 +1548,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1392
1548
  method_handle_metadata=self._method_handle_metadata,
1393
1549
  function_schema=self._metadata.function_schema if self._metadata else None,
1394
1550
  input_plane_url=self._input_plane_url,
1551
+ input_plane_region=self._input_plane_region,
1552
+ max_object_size_bytes=self._max_object_size_bytes,
1553
+ _experimental_flash_urls=self._experimental_flash_urls,
1554
+ supported_input_formats=self._metadata.supported_input_formats if self._metadata else [],
1555
+ supported_output_formats=self._metadata.supported_output_formats if self._metadata else [],
1395
1556
  )
1396
1557
 
1397
1558
  def _check_no_web_url(self, fn_name: str):
@@ -1422,6 +1583,11 @@ Use the `Function.get_web_url()` method instead.
1422
1583
  """URL of a Function running as a web endpoint."""
1423
1584
  return self._web_url
1424
1585
 
1586
+ @live_method
1587
+ async def _experimental_get_flash_urls(self) -> Optional[list[str]]:
1588
+ """URL of the flash service for the function."""
1589
+ return list(self._experimental_flash_urls) if self._experimental_flash_urls else None
1590
+
1425
1591
  @property
1426
1592
  async def is_generator(self) -> bool:
1427
1593
  """mdmd:hidden"""
@@ -1438,7 +1604,11 @@ Use the `Function.get_web_url()` method instead.
1438
1604
 
1439
1605
  @live_method_gen
1440
1606
  async def _map(
1441
- self, input_queue: _SynchronizedQueue, order_outputs: bool, return_exceptions: bool
1607
+ self,
1608
+ input_queue: _SynchronizedQueue,
1609
+ order_outputs: bool,
1610
+ return_exceptions: bool,
1611
+ wrap_returned_exceptions: bool,
1442
1612
  ) -> AsyncGenerator[Any, None]:
1443
1613
  """mdmd:hidden
1444
1614
 
@@ -1459,19 +1629,51 @@ Use the `Function.get_web_url()` method instead.
1459
1629
  else:
1460
1630
  count_update_callback = None
1461
1631
 
1462
- async with aclosing(
1463
- _map_invocation(
1464
- self,
1465
- input_queue,
1466
- self.client,
1467
- order_outputs,
1468
- return_exceptions,
1469
- count_update_callback,
1470
- api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1471
- )
1472
- ) as stream:
1473
- async for item in stream:
1474
- yield item
1632
+ if self._input_plane_url:
1633
+ async with aclosing(
1634
+ _map_invocation_inputplane(
1635
+ self,
1636
+ input_queue,
1637
+ self.client,
1638
+ order_outputs,
1639
+ return_exceptions,
1640
+ wrap_returned_exceptions,
1641
+ count_update_callback,
1642
+ )
1643
+ ) as stream:
1644
+ async for item in stream:
1645
+ yield item
1646
+ else:
1647
+ async with aclosing(
1648
+ _map_invocation(
1649
+ self,
1650
+ input_queue,
1651
+ self.client,
1652
+ order_outputs,
1653
+ return_exceptions,
1654
+ wrap_returned_exceptions,
1655
+ count_update_callback,
1656
+ api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
1657
+ )
1658
+ ) as stream:
1659
+ async for item in stream:
1660
+ yield item
1661
+
1662
+ @live_method
1663
+ async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
1664
+ self._check_no_web_url("spawn_map")
1665
+ if self._is_generator:
1666
+ raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
1667
+
1668
+ assert self._function_name
1669
+ function_call_id, num_inputs = await _spawn_map_invocation(
1670
+ self,
1671
+ input_queue,
1672
+ self.client,
1673
+ )
1674
+ fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
1675
+ fc._num_inputs = num_inputs # set the cached value of num_inputs
1676
+ return fc
1475
1677
 
1476
1678
  async def _call_function(self, args, kwargs) -> ReturnType:
1477
1679
  invocation: Union[_Invocation, _InputPlaneInvocation]
@@ -1482,6 +1684,7 @@ Use the `Function.get_web_url()` method instead.
1482
1684
  kwargs,
1483
1685
  client=self.client,
1484
1686
  input_plane_url=self._input_plane_url,
1687
+ input_plane_region=self._input_plane_region,
1485
1688
  )
1486
1689
  else:
1487
1690
  invocation = await _Invocation.create(
@@ -1514,13 +1717,24 @@ Use the `Function.get_web_url()` method instead.
1514
1717
  @live_method_gen
1515
1718
  @synchronizer.no_input_translation
1516
1719
  async def _call_generator(self, args, kwargs):
1517
- invocation = await _Invocation.create(
1518
- self,
1519
- args,
1520
- kwargs,
1521
- client=self.client,
1522
- function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1523
- )
1720
+ invocation: Union[_Invocation, _InputPlaneInvocation]
1721
+ if self._input_plane_url:
1722
+ invocation = await _InputPlaneInvocation.create(
1723
+ self,
1724
+ args,
1725
+ kwargs,
1726
+ client=self.client,
1727
+ input_plane_url=self._input_plane_url,
1728
+ input_plane_region=self._input_plane_region,
1729
+ )
1730
+ else:
1731
+ invocation = await _Invocation.create(
1732
+ self,
1733
+ args,
1734
+ kwargs,
1735
+ client=self.client,
1736
+ function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
1737
+ )
1524
1738
  async for res in invocation.run_generator():
1525
1739
  yield res
1526
1740
 
@@ -1584,8 +1798,9 @@ Use the `Function.get_web_url()` method instead.
1584
1798
  # "user code" to run on the synchronicity thread, which seems bad
1585
1799
  if not self._is_local():
1586
1800
  msg = (
1587
- "The definition for this function is missing here so it is not possible to invoke it locally. "
1588
- "If this function was retrieved via `Function.lookup` you need to use `.remote()`."
1801
+ "The definition for this Function is missing, so it is not possible to invoke it locally. "
1802
+ "If this function was retrieved via `Function.from_name`, "
1803
+ "you need to use one of the remote invocation methods instead."
1589
1804
  )
1590
1805
  raise ExecutionError(msg)
1591
1806
 
@@ -1662,8 +1877,9 @@ Use the `Function.get_web_url()` method instead.
1662
1877
  async def spawn(self, *args: P.args, **kwargs: P.kwargs) -> "_FunctionCall[ReturnType]":
1663
1878
  """Calls the function with the given arguments, without waiting for the results.
1664
1879
 
1665
- Returns a [`modal.FunctionCall`](/docs/reference/modal.FunctionCall) object, that can later be polled or
1666
- waited for using [`.get(timeout=...)`](/docs/reference/modal.FunctionCall#get).
1880
+ Returns a [`modal.FunctionCall`](https://modal.com/docs/reference/modal.FunctionCall) object
1881
+ that can later be polled or waited for using
1882
+ [`.get(timeout=...)`](https://modal.com/docs/reference/modal.FunctionCall#get).
1667
1883
  Conceptually similar to `multiprocessing.pool.apply_async`, or a Future/Promise in other contexts.
1668
1884
  """
1669
1885
  self._check_no_web_url("spawn")
@@ -1685,10 +1901,9 @@ Use the `Function.get_web_url()` method instead.
1685
1901
  @live_method
1686
1902
  async def get_current_stats(self) -> FunctionStats:
1687
1903
  """Return a `FunctionStats` object describing the current function's queue and runner counts."""
1688
- resp = await retry_transient_errors(
1689
- self.client.stub.FunctionGetCurrentStats,
1904
+ resp = await self.client.stub.FunctionGetCurrentStats(
1690
1905
  api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
1691
- total_timeout=10.0,
1906
+ retry=Retry(total_timeout=10.0),
1692
1907
  )
1693
1908
  return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
1694
1909
 
@@ -1706,6 +1921,7 @@ Use the `Function.get_web_url()` method instead.
1706
1921
  starmap = MethodWithAio(_starmap_sync, _starmap_async, synchronizer)
1707
1922
  for_each = MethodWithAio(_for_each_sync, _for_each_async, synchronizer)
1708
1923
  spawn_map = MethodWithAio(_spawn_map_sync, _spawn_map_async, synchronizer)
1924
+ experimental_spawn_map = MethodWithAio(_experimental_spawn_map_sync, _experimental_spawn_map_async, synchronizer)
1709
1925
 
1710
1926
 
1711
1927
  class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
@@ -1720,12 +1936,25 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1720
1936
  """
1721
1937
 
1722
1938
  _is_generator: bool = False
1939
+ _num_inputs: Optional[int] = None
1723
1940
 
1724
1941
  def _invocation(self):
1725
1942
  return _Invocation(self.client.stub, self.object_id, self.client)
1726
1943
 
1727
- async def get(self, timeout: Optional[float] = None) -> ReturnType:
1728
- """Get the result of the function call.
1944
+ @live_method
1945
+ async def num_inputs(self) -> int:
1946
+ """Get the number of inputs in the function call."""
1947
+ if self._num_inputs is None:
1948
+ request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
1949
+ resp = await self.client.stub.FunctionCallFromId(request)
1950
+ self._num_inputs = resp.num_inputs # cached
1951
+ return self._num_inputs
1952
+
1953
+ @live_method
1954
+ async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
1955
+ """Get the result of the index-th input of the function call.
1956
+ `.spawn()` calls have a single output, so only specifying `index=0` is valid.
1957
+ A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
1729
1958
 
1730
1959
  This function waits indefinitely by default. It takes an optional
1731
1960
  `timeout` argument that specifies the maximum number of seconds to wait,
@@ -1733,27 +1962,59 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1733
1962
 
1734
1963
  The returned coroutine is not cancellation-safe.
1735
1964
  """
1736
- return await self._invocation().poll_function(timeout=timeout)
1965
+ return await self._invocation().poll_function(timeout=timeout, index=index)
1966
+
1967
+ @live_method_gen
1968
+ async def iter(self, *, start: int = 0, end: Optional[int] = None) -> AsyncIterator[ReturnType]:
1969
+ """Iterate in-order over the results of the function call.
1970
+
1971
+ Optionally, specify a range [start, end) to iterate over.
1972
+
1973
+ Example:
1974
+ ```python
1975
+ @app.function()
1976
+ def my_func(a):
1977
+ return a ** 2
1978
+
1979
+
1980
+ @app.local_entrypoint()
1981
+ def main():
1982
+ fc = my_func.spawn_map([1, 2, 3, 4])
1983
+ assert list(fc.iter()) == [1, 4, 9, 16]
1984
+ assert list(fc.iter(start=1, end=3)) == [4, 9]
1985
+ ```
1986
+
1987
+ If `end` is not provided, it will iterate over all results.
1988
+ """
1989
+ num_inputs = await self.num_inputs()
1990
+ if end is None:
1991
+ end = num_inputs
1992
+ if start < 0 or end > num_inputs:
1993
+ raise ValueError(f"Invalid index range: {start} to {end} for {num_inputs} inputs")
1994
+ async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
1995
+ yield item
1737
1996
 
1997
+ @live_method
1738
1998
  async def get_call_graph(self) -> list[InputInfo]:
1739
1999
  """Returns a structure representing the call graph from a given root
1740
2000
  call ID, along with the status of execution for each node.
1741
2001
 
1742
- See [`modal.call_graph`](/docs/reference/modal.call_graph) reference page
2002
+ See [`modal.call_graph`](https://modal.com/docs/reference/modal.call_graph) reference page
1743
2003
  for documentation on the structure of the returned `InputInfo` items.
1744
2004
  """
1745
2005
  assert self._client and self._client.stub
1746
2006
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
1747
- response = await retry_transient_errors(self._client.stub.FunctionGetCallGraph, request)
2007
+ response = await self._client.stub.FunctionGetCallGraph(request)
1748
2008
  return _reconstruct_call_graph(response)
1749
2009
 
2010
+ @live_method
1750
2011
  async def cancel(
1751
2012
  self,
1752
2013
  # if true, containers running the inputs are forcibly terminated
1753
2014
  terminate_containers: bool = False,
1754
2015
  ):
1755
2016
  """Cancels the function call, which will stop its execution and mark its inputs as
1756
- [`TERMINATED`](/docs/reference/modal.call_graph#modalcall_graphinputstatus).
2017
+ [`TERMINATED`](https://modal.com/docs/reference/modal.call_graph#modalcall_graphinputstatus).
1757
2018
 
1758
2019
  If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
1759
2020
  causing any non-cancelled inputs on those containers to be rescheduled in new containers.
@@ -1762,7 +2023,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1762
2023
  function_call_id=self.object_id, terminate_containers=terminate_containers
1763
2024
  )
1764
2025
  assert self._client and self._client.stub
1765
- await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
2026
+ await self._client.stub.FunctionCallCancel(request)
1766
2027
 
1767
2028
  @staticmethod
1768
2029
  async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
@@ -1784,11 +2045,18 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1784
2045
  if you no longer have access to the original object returned from `Function.spawn`.
1785
2046
 
1786
2047
  """
1787
- if client is None:
1788
- client = await _Client.from_env()
1789
2048
 
1790
- fc: _FunctionCall[Any] = _FunctionCall._new_hydrated(function_call_id, client, None)
1791
- return fc
2049
+ async def _load(
2050
+ self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
2051
+ ):
2052
+ # this loader doesn't do anything in practice, but it will get the client from the load_context
2053
+ self._hydrate(function_call_id, load_context.client, None)
2054
+
2055
+ rep = f"FunctionCall.from_id({function_call_id!r})"
2056
+
2057
+ return _FunctionCall._from_loader(
2058
+ _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=client)
2059
+ )
1792
2060
 
1793
2061
  @staticmethod
1794
2062
  async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]: