modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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 (143) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +171 -138
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +30 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/name_utils.py +2 -3
  31. modal/_utils/package_utils.py +0 -1
  32. modal/_utils/rand_pb_testing.py +8 -1
  33. modal/_utils/task_command_router_client.py +524 -0
  34. modal/_vendor/cloudpickle.py +144 -48
  35. modal/app.py +285 -105
  36. modal/app.pyi +216 -53
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +6 -3
  39. modal/builder/PREVIEW.txt +2 -1
  40. modal/builder/base-images.json +4 -2
  41. modal/cli/_download.py +19 -3
  42. modal/cli/cluster.py +4 -2
  43. modal/cli/config.py +3 -1
  44. modal/cli/container.py +5 -4
  45. modal/cli/dict.py +5 -2
  46. modal/cli/entry_point.py +26 -2
  47. modal/cli/environment.py +2 -16
  48. modal/cli/launch.py +1 -76
  49. modal/cli/network_file_system.py +5 -20
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/vscode.py +1 -1
  52. modal/cli/queues.py +5 -4
  53. modal/cli/run.py +24 -204
  54. modal/cli/secret.py +1 -2
  55. modal/cli/shell.py +375 -0
  56. modal/cli/utils.py +1 -13
  57. modal/cli/volume.py +11 -17
  58. modal/client.py +16 -125
  59. modal/client.pyi +94 -144
  60. modal/cloud_bucket_mount.py +3 -1
  61. modal/cloud_bucket_mount.pyi +4 -0
  62. modal/cls.py +101 -64
  63. modal/cls.pyi +9 -8
  64. modal/config.py +21 -1
  65. modal/container_process.py +288 -12
  66. modal/container_process.pyi +99 -38
  67. modal/dict.py +72 -33
  68. modal/dict.pyi +88 -57
  69. modal/environments.py +16 -8
  70. modal/environments.pyi +6 -2
  71. modal/exception.py +154 -16
  72. modal/experimental/__init__.py +24 -53
  73. modal/experimental/flash.py +161 -74
  74. modal/experimental/flash.pyi +97 -49
  75. modal/file_io.py +50 -92
  76. modal/file_io.pyi +117 -89
  77. modal/functions.pyi +70 -87
  78. modal/image.py +82 -47
  79. modal/image.pyi +51 -30
  80. modal/io_streams.py +500 -149
  81. modal/io_streams.pyi +279 -189
  82. modal/mount.py +60 -46
  83. modal/mount.pyi +41 -17
  84. modal/network_file_system.py +19 -11
  85. modal/network_file_system.pyi +72 -39
  86. modal/object.pyi +114 -22
  87. modal/parallel_map.py +42 -44
  88. modal/parallel_map.pyi +9 -17
  89. modal/partial_function.pyi +4 -2
  90. modal/proxy.py +14 -6
  91. modal/proxy.pyi +10 -2
  92. modal/queue.py +45 -38
  93. modal/queue.pyi +88 -52
  94. modal/runner.py +96 -96
  95. modal/runner.pyi +44 -27
  96. modal/sandbox.py +225 -107
  97. modal/sandbox.pyi +226 -60
  98. modal/secret.py +58 -56
  99. modal/secret.pyi +28 -13
  100. modal/serving.py +7 -11
  101. modal/serving.pyi +7 -8
  102. modal/snapshot.py +29 -15
  103. modal/snapshot.pyi +18 -10
  104. modal/token_flow.py +1 -1
  105. modal/token_flow.pyi +4 -6
  106. modal/volume.py +102 -55
  107. modal/volume.pyi +125 -66
  108. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  109. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  110. modal_proto/api.proto +141 -70
  111. modal_proto/api_grpc.py +42 -26
  112. modal_proto/api_pb2.py +1123 -1103
  113. modal_proto/api_pb2.pyi +331 -83
  114. modal_proto/api_pb2_grpc.py +80 -48
  115. modal_proto/api_pb2_grpc.pyi +26 -18
  116. modal_proto/modal_api_grpc.py +175 -174
  117. modal_proto/task_command_router.proto +164 -0
  118. modal_proto/task_command_router_grpc.py +138 -0
  119. modal_proto/task_command_router_pb2.py +180 -0
  120. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
  121. modal_proto/task_command_router_pb2_grpc.py +272 -0
  122. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  123. modal_version/__init__.py +1 -1
  124. modal_version/__main__.py +1 -1
  125. modal/cli/programs/launch_instance_ssh.py +0 -94
  126. modal/cli/programs/run_marimo.py +0 -95
  127. modal-1.1.5.dev66.dist-info/RECORD +0 -191
  128. modal_proto/modal_options_grpc.py +0 -3
  129. modal_proto/options.proto +0 -19
  130. modal_proto/options_grpc.py +0 -3
  131. modal_proto/options_pb2.py +0 -35
  132. modal_proto/options_pb2.pyi +0 -20
  133. modal_proto/options_pb2_grpc.py +0 -4
  134. modal_proto/options_pb2_grpc.pyi +0 -7
  135. modal_proto/sandbox_router.proto +0 -125
  136. modal_proto/sandbox_router_grpc.py +0 -89
  137. modal_proto/sandbox_router_pb2.py +0 -128
  138. modal_proto/sandbox_router_pb2_grpc.py +0 -169
  139. modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
  140. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  141. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  142. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  143. {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/_functions.py CHANGED
@@ -13,13 +13,14 @@ 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
- from grpclib import GRPCError, Status
16
+ from grpclib import Status
17
17
  from synchronicity.combined_types import MethodWithAio
18
18
 
19
19
  from modal_proto import api_pb2
20
20
  from modal_proto.modal_api_grpc import ModalClientModal
21
21
 
22
- 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
23
24
  from ._pty import get_pty_info
24
25
  from ._resolver import Resolver
25
26
  from ._resources import convert_fn_config_to_resources_config
@@ -37,6 +38,7 @@ from ._utils.async_utils import (
37
38
  aclosing,
38
39
  async_merge,
39
40
  callable_to_agen,
41
+ deprecate_aio_usage,
40
42
  synchronizer,
41
43
  warn_if_generator_is_not_consumed,
42
44
  )
@@ -53,7 +55,7 @@ from ._utils.function_utils import (
53
55
  get_function_type,
54
56
  is_async,
55
57
  )
56
- from ._utils.grpc_utils import RetryWarningMessage, retry_transient_errors
58
+ from ._utils.grpc_utils import Retry, RetryWarningMessage
57
59
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
58
60
  from .call_graph import InputInfo, _reconstruct_call_graph
59
61
  from .client import _Client
@@ -89,13 +91,13 @@ from .parallel_map import (
89
91
  from .proxy import _Proxy
90
92
  from .retries import Retries, RetryManager
91
93
  from .schedule import Schedule
92
- from .scheduler_placement import SchedulerPlacement
93
94
  from .secret import _Secret
94
95
  from .volume import _Volume
95
96
 
96
97
  if TYPE_CHECKING:
97
98
  import modal.app
98
99
  import modal.cls
100
+ import modal.functions
99
101
 
100
102
  MAX_INTERNAL_FAILURE_COUNT = 8
101
103
  TERMINAL_STATUSES = (
@@ -164,21 +166,22 @@ class _Invocation:
164
166
 
165
167
  if from_spawn_map:
166
168
  request.from_spawn_map = True
167
- response = await retry_transient_errors(
168
- client.stub.FunctionMap,
169
+ response = await client.stub.FunctionMap(
169
170
  request,
170
- max_retries=None,
171
- max_delay=30.0,
172
- retry_warning_message=RetryWarningMessage(
173
- message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
174
- "more function calls. This may be due to hitting rate limits or function backlog limits.",
175
- warning_interval=10,
176
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
171
+ retry=Retry(
172
+ max_retries=None,
173
+ max_delay=30.0,
174
+ warning_message=RetryWarningMessage(
175
+ message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
176
+ "more function calls. This may be due to hitting rate limits or function backlog limits.",
177
+ warning_interval=10,
178
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
179
+ ),
180
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
177
181
  ),
178
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
179
182
  )
180
183
  else:
181
- response = await retry_transient_errors(client.stub.FunctionMap, request)
184
+ response = await client.stub.FunctionMap(request)
182
185
 
183
186
  function_call_id = response.function_call_id
184
187
  if response.pipelined_inputs:
@@ -198,10 +201,7 @@ class _Invocation:
198
201
  request_put = api_pb2.FunctionPutInputsRequest(
199
202
  function_id=function_id, inputs=[item], function_call_id=function_call_id
200
203
  )
201
- inputs_response: api_pb2.FunctionPutInputsResponse = await retry_transient_errors(
202
- client.stub.FunctionPutInputs,
203
- request_put,
204
- )
204
+ inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
205
205
  processed_inputs = inputs_response.inputs
206
206
  if not processed_inputs:
207
207
  raise Exception("Could not create function call - the input queue seems to be full")
@@ -243,10 +243,9 @@ class _Invocation:
243
243
  start_idx=index,
244
244
  end_idx=index,
245
245
  )
246
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
247
- self.stub.FunctionGetOutputs,
246
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
248
247
  request,
249
- attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
248
+ retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
250
249
  )
251
250
 
252
251
  if len(response.outputs) > 0:
@@ -266,10 +265,7 @@ class _Invocation:
266
265
 
267
266
  item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
268
267
  request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
269
- await retry_transient_errors(
270
- self.stub.FunctionRetryInputs,
271
- request,
272
- )
268
+ await self.stub.FunctionRetryInputs(request)
273
269
 
274
270
  async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
275
271
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
@@ -373,10 +369,8 @@ class _Invocation:
373
369
  start_idx=current_index,
374
370
  end_idx=batch_end_index,
375
371
  )
376
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
377
- self.stub.FunctionGetOutputs,
378
- request,
379
- attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
372
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
373
+ request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
380
374
  )
381
375
 
382
376
  outputs = list(response.outputs)
@@ -448,7 +442,7 @@ class _InputPlaneInvocation:
448
442
  )
449
443
 
450
444
  metadata = await client.get_input_plane_metadata(input_plane_region)
451
- response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
445
+ response = await stub.AttemptStart(request, metadata=metadata)
452
446
  attempt_token = response.attempt_token
453
447
 
454
448
  return _InputPlaneInvocation(
@@ -468,10 +462,9 @@ class _InputPlaneInvocation:
468
462
  requested_at=time.time(),
469
463
  )
470
464
  metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
471
- await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
472
- self.stub.AttemptAwait,
465
+ await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
473
466
  await_request,
474
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
467
+ retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
475
468
  metadata=metadata,
476
469
  )
477
470
 
@@ -511,11 +504,7 @@ class _InputPlaneInvocation:
511
504
  input=self.input_item,
512
505
  attempt_token=self.attempt_token,
513
506
  )
514
- retry_response = await retry_transient_errors(
515
- self.stub.AttemptRetry,
516
- retry_request,
517
- metadata=metadata,
518
- )
507
+ retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
519
508
  return retry_response.attempt_token
520
509
 
521
510
  async def run_generator(self):
@@ -549,6 +538,7 @@ class _InputPlaneInvocation:
549
538
  async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
550
539
  if not input_plane_region:
551
540
  return []
541
+ assert client._auth_token_manager, "Client is not open"
552
542
  token = await client._auth_token_manager.get_token()
553
543
  return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
554
544
 
@@ -601,10 +591,25 @@ class _FunctionSpec:
601
591
  cpu: Optional[Union[float, tuple[float, float]]]
602
592
  memory: Optional[Union[int, tuple[int, int]]]
603
593
  ephemeral_disk: Optional[int]
604
- scheduler_placement: Optional[SchedulerPlacement]
594
+ scheduler_placement: Optional[api_pb2.SchedulerPlacement]
605
595
  proxy: Optional[_Proxy]
606
596
 
607
597
 
598
+ def _get_supported_input_output_formats(is_web_endpoint: bool, is_generator: bool, restrict_output: bool):
599
+ if is_web_endpoint:
600
+ supported_input_formats = [api_pb2.DATA_FORMAT_ASGI]
601
+ supported_output_formats = [api_pb2.DATA_FORMAT_ASGI, api_pb2.DATA_FORMAT_GENERATOR_DONE]
602
+ else:
603
+ supported_input_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
604
+ if restrict_output:
605
+ supported_output_formats = [api_pb2.DATA_FORMAT_CBOR]
606
+ else:
607
+ supported_output_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
608
+ if is_generator:
609
+ supported_output_formats.append(api_pb2.DATA_FORMAT_GENERATOR_DONE)
610
+ return supported_input_formats, supported_output_formats
611
+
612
+
608
613
  P = typing_extensions.ParamSpec("P")
609
614
  ReturnType = typing.TypeVar("ReturnType", covariant=True)
610
615
  OriginalReturnType = typing.TypeVar(
@@ -654,7 +659,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
654
659
  @staticmethod
655
660
  def from_local(
656
661
  info: FunctionInfo,
657
- app,
662
+ app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
658
663
  image: _Image,
659
664
  env: Optional[dict[str, Optional[str]]] = None,
660
665
  secrets: Optional[Collection[_Secret]] = None,
@@ -680,7 +685,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
680
685
  batch_max_size: Optional[int] = None,
681
686
  batch_wait_ms: Optional[int] = None,
682
687
  cloud: Optional[str] = None,
683
- scheduler_placement: Optional[SchedulerPlacement] = None,
688
+ region: Optional[Union[str, Sequence[str]]] = None,
689
+ nonpreemptible: bool = False,
684
690
  is_builder_function: bool = False,
685
691
  is_auto_snapshot: bool = False,
686
692
  enable_memory_snapshot: bool = False,
@@ -690,13 +696,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
690
696
  # Experimental: Clustered functions
691
697
  cluster_size: Optional[int] = None,
692
698
  rdma: Optional[bool] = None,
693
- max_inputs: Optional[int] = None,
699
+ single_use_containers: bool = False,
694
700
  ephemeral_disk: Optional[int] = None,
695
701
  include_source: bool = True,
696
702
  experimental_options: Optional[dict[str, str]] = None,
697
703
  _experimental_proxy_ip: Optional[str] = None,
698
704
  _experimental_custom_scaling_factor: Optional[float] = None,
699
705
  restrict_output: bool = False,
706
+ http_config: Optional[api_pb2.HTTPConfig] = None,
700
707
  ) -> "_Function":
701
708
  """mdmd:hidden
702
709
 
@@ -736,10 +743,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
736
743
  if is_generator:
737
744
  raise InvalidError("Generator functions do not support retries.")
738
745
 
746
+ if timeout is None: # type: ignore[unreachable] # Help users who aren't using type checkers
747
+ raise InvalidError("The `timeout` parameter cannot be set to None: https://modal.com/docs/guide/timeouts")
748
+
739
749
  secrets = secrets or []
740
750
  if env:
741
751
  secrets = [*secrets, _Secret.from_dict(env)]
742
752
 
753
+ scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
754
+ if region or nonpreemptible:
755
+ regions = [region] if isinstance(region, str) else (list(region) if region else None)
756
+ scheduler_placement = api_pb2.SchedulerPlacement(regions=regions, nonpreemptible=nonpreemptible)
757
+
743
758
  function_spec = _FunctionSpec(
744
759
  mounts=all_mounts,
745
760
  secrets=secrets,
@@ -772,6 +787,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
772
787
  scaledown_window=scaledown_window,
773
788
  )
774
789
 
790
+ # For clustered functions, container settings must be multiples of cluster_size
791
+ if cluster_size is not None and cluster_size > 1:
792
+ for field in ["min_containers", "max_containers", "buffer_containers"]:
793
+ value = getattr(autoscaler_settings, field)
794
+ if value and value % cluster_size != 0:
795
+ raise InvalidError(
796
+ f"`{field}` ({value}) must be a multiple of `cluster_size` ({cluster_size}) "
797
+ f"for clustered Functions"
798
+ )
799
+
775
800
  if _experimental_custom_scaling_factor is not None and (
776
801
  _experimental_custom_scaling_factor < 0 or _experimental_custom_scaling_factor > 1
777
802
  ):
@@ -797,14 +822,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
797
822
  if arg.default is not inspect.Parameter.empty:
798
823
  raise InvalidError(f"Modal batched function {func_name} does not accept default arguments.")
799
824
 
800
- if max_inputs is not None:
801
- if not isinstance(max_inputs, int):
802
- raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
803
- if max_inputs <= 0:
804
- raise InvalidError("`max_inputs` must be positive")
805
- if max_inputs > 1:
806
- raise InvalidError("Only `max_inputs=1` is currently supported")
807
-
808
825
  # Validate volumes
809
826
  validated_volumes = validate_volumes(volumes)
810
827
  cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
@@ -833,15 +850,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
833
850
  is_web_endpoint=is_web_endpoint,
834
851
  ignore_first_argument=True,
835
852
  )
836
- if is_web_endpoint:
837
- method_input_formats = [api_pb2.DATA_FORMAT_ASGI]
838
- method_output_formats = [api_pb2.DATA_FORMAT_ASGI]
839
- else:
840
- method_input_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
841
- if restrict_output:
842
- method_output_formats = [api_pb2.DATA_FORMAT_CBOR]
843
- else:
844
- method_output_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
853
+ method_input_formats, method_output_formats = _get_supported_input_output_formats(
854
+ is_web_endpoint, partial_function.params.is_generator or False, restrict_output
855
+ )
845
856
 
846
857
  method_definition = api_pb2.MethodDefinition(
847
858
  webhook_config=partial_function.params.webhook_config,
@@ -877,22 +888,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
877
888
  # classes don't have data formats themselves - input/output formats are set per method above
878
889
  supported_input_formats = []
879
890
  supported_output_formats = []
880
- elif webhook_config is not None:
881
- supported_input_formats = [api_pb2.DATA_FORMAT_ASGI]
882
- supported_output_formats = [api_pb2.DATA_FORMAT_ASGI]
883
891
  else:
884
- supported_input_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
885
- if restrict_output:
886
- supported_output_formats = [api_pb2.DATA_FORMAT_CBOR]
887
- else:
888
- supported_output_formats = [api_pb2.DATA_FORMAT_PICKLE, api_pb2.DATA_FORMAT_CBOR]
889
-
890
- async def _preload(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
891
- assert resolver.client and resolver.client.stub
892
+ is_web_endpoint = webhook_config is not None and webhook_config.type != api_pb2.WEBHOOK_TYPE_UNSPECIFIED
893
+ supported_input_formats, supported_output_formats = _get_supported_input_output_formats(
894
+ is_web_endpoint, is_generator, restrict_output
895
+ )
892
896
 
893
- assert resolver.app_id
897
+ async def _preload(
898
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
899
+ ):
900
+ assert load_context.app_id
894
901
  req = api_pb2.FunctionPrecreateRequest(
895
- app_id=resolver.app_id,
902
+ app_id=load_context.app_id,
896
903
  function_name=info.function_name,
897
904
  function_type=function_type,
898
905
  existing_function_id=existing_object_id or "",
@@ -908,11 +915,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
908
915
  elif webhook_config:
909
916
  req.webhook_config.CopyFrom(webhook_config)
910
917
 
911
- response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
912
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
918
+ response = await load_context.client.stub.FunctionPrecreate(req)
919
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
913
920
 
914
- async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
915
- assert resolver.client and resolver.client.stub
921
+ async def _load(
922
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
923
+ ):
916
924
  with FunctionCreationStatus(resolver, tag) as function_creation_status:
917
925
  timeout_secs = timeout
918
926
 
@@ -984,6 +992,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
984
992
  function_definition = api_pb2.Function(
985
993
  module_name=info.module_name or "",
986
994
  function_name=info.function_name,
995
+ implementation_name=info.implementation_name,
987
996
  mount_ids=loaded_mount_ids,
988
997
  secret_ids=[secret.object_id for secret in secrets],
989
998
  image_id=(image.object_id if image else ""),
@@ -1019,9 +1028,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1019
1028
  object_dependencies=object_dependencies,
1020
1029
  block_network=block_network,
1021
1030
  untrusted=restrict_modal_access,
1022
- max_inputs=max_inputs or 0,
1031
+ single_use_containers=single_use_containers,
1032
+ max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
1023
1033
  cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
1024
- scheduler_placement=scheduler_placement.proto if scheduler_placement else None,
1034
+ scheduler_placement=scheduler_placement,
1025
1035
  is_class=info.is_service_class(),
1026
1036
  class_parameter_info=info.class_parameter_info(),
1027
1037
  i6pn_enabled=i6pn_enabled,
@@ -1043,12 +1053,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1043
1053
  function_schema=function_schema,
1044
1054
  supported_input_formats=supported_input_formats,
1045
1055
  supported_output_formats=supported_output_formats,
1056
+ http_config=http_config,
1046
1057
  )
1047
1058
 
1048
1059
  if isinstance(gpu, list):
1049
1060
  function_data = api_pb2.FunctionData(
1050
1061
  module_name=function_definition.module_name,
1051
1062
  function_name=function_definition.function_name,
1063
+ implementation_name=function_definition.implementation_name,
1052
1064
  function_type=function_definition.function_type,
1053
1065
  warm_pool_size=function_definition.warm_pool_size,
1054
1066
  concurrency_limit=function_definition.concurrency_limit,
@@ -1080,6 +1092,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1080
1092
  untrusted=function_definition.untrusted,
1081
1093
  supported_input_formats=supported_input_formats,
1082
1094
  supported_output_formats=supported_output_formats,
1095
+ http_config=http_config,
1083
1096
  )
1084
1097
 
1085
1098
  ranked_functions = []
@@ -1108,24 +1121,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1108
1121
  ),
1109
1122
  )
1110
1123
 
1111
- assert resolver.app_id
1124
+ assert load_context.app_id
1112
1125
  assert (function_definition is None) != (function_data is None) # xor
1113
1126
  request = api_pb2.FunctionCreateRequest(
1114
- app_id=resolver.app_id,
1127
+ app_id=load_context.app_id,
1115
1128
  function=function_definition,
1116
1129
  function_data=function_data,
1117
1130
  existing_function_id=existing_object_id or "",
1118
1131
  )
1119
1132
  try:
1120
- response: api_pb2.FunctionCreateResponse = await retry_transient_errors(
1121
- resolver.client.stub.FunctionCreate, request
1122
- )
1123
- except GRPCError as exc:
1124
- if exc.status == Status.INVALID_ARGUMENT:
1125
- raise InvalidError(exc.message)
1126
- if exc.status == Status.FAILED_PRECONDITION:
1127
- raise InvalidError(exc.message)
1128
- if exc.message and "Received :status = '413'" in exc.message:
1133
+ response: api_pb2.FunctionCreateResponse = await load_context.client.stub.FunctionCreate(request)
1134
+ except Exception as exc:
1135
+ if "Received :status = '413'" in str(exc):
1129
1136
  raise InvalidError(f"Function {info.function_name} is too large to deploy.")
1130
1137
  raise
1131
1138
  function_creation_status.set_response(response)
@@ -1134,10 +1141,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1134
1141
  serve_mounts = {m for m in all_mounts if m.is_local()}
1135
1142
  serve_mounts |= image._serve_mounts
1136
1143
  obj._serve_mounts = frozenset(serve_mounts)
1137
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1144
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1138
1145
 
1139
1146
  rep = f"Function({tag})"
1140
- obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps)
1147
+ # Pass a *reference* to the App's LoadContext - this is important since the App is
1148
+ # the only way to infer a LoadContext for an `@app.function`, and the App doesn't
1149
+ # get its client until *after* the Function is created.
1150
+ load_context = app._root_load_context if app else LoadContext.empty()
1151
+ obj = _Function._from_loader(_load, rep, preload=_preload, deps=_deps, load_context_overrides=load_context)
1141
1152
 
1142
1153
  obj._raw_f = info.raw_f
1143
1154
  obj._info = info
@@ -1179,7 +1190,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1179
1190
 
1180
1191
  parent = self
1181
1192
 
1182
- async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1193
+ async def _load(
1194
+ param_bound_func: _Function,
1195
+ resolver: Resolver,
1196
+ load_context: LoadContext,
1197
+ existing_object_id: Optional[str],
1198
+ ):
1183
1199
  if not parent.is_hydrated:
1184
1200
  # While the base Object.hydrate() method appears to be idempotent, it's not always safe
1185
1201
  await parent.hydrate()
@@ -1212,7 +1228,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1212
1228
  param_bound_func._hydrate_from_other(parent)
1213
1229
  return
1214
1230
 
1215
- environment_name = _get_environment_name(None, resolver)
1216
1231
  assert parent is not None and parent.is_hydrated
1217
1232
 
1218
1233
  if options:
@@ -1252,11 +1267,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1252
1267
  function_id=parent.object_id,
1253
1268
  serialized_params=serialized_params,
1254
1269
  function_options=options_pb,
1255
- environment_name=environment_name
1270
+ environment_name=load_context.environment_name
1256
1271
  or "", # TODO: investigate shouldn't environment name always be specified here?
1257
1272
  )
1258
1273
 
1259
- response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1274
+ response = await parent._client.stub.FunctionBindParams(req)
1260
1275
  param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1261
1276
 
1262
1277
  def _deps():
@@ -1269,7 +1284,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1269
1284
  return [dep for dep in all_deps if not dep.is_hydrated]
1270
1285
  return []
1271
1286
 
1272
- fun: _Function = _Function._from_loader(_load, "Function(parametrized)", hydrate_lazily=True, deps=_deps)
1287
+ fun: _Function = _Function._from_loader(
1288
+ _load,
1289
+ "Function(parametrized)",
1290
+ hydrate_lazily=True,
1291
+ deps=_deps,
1292
+ load_context_overrides=self._load_context_overrides,
1293
+ )
1273
1294
 
1274
1295
  fun._info = self._info
1275
1296
  fun._obj = obj
@@ -1320,7 +1341,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1320
1341
  scaledown_window=scaledown_window,
1321
1342
  )
1322
1343
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1323
- await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
1344
+ await self.client.stub.FunctionUpdateSchedulingParams(request)
1324
1345
 
1325
1346
  # One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
1326
1347
  # and then we could return them here (would need some ad hoc dataclass, which I don't love)
@@ -1367,34 +1388,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1367
1388
  cls,
1368
1389
  app_name: str,
1369
1390
  name: str,
1370
- namespace=None, # mdmd:line-hidden
1371
- environment_name: Optional[str] = None,
1391
+ *,
1392
+ load_context_overrides: LoadContext,
1372
1393
  ):
1373
1394
  # internal function lookup implementation that allows lookup of class "service functions"
1374
1395
  # in addition to non-class functions
1375
- async def _load_remote(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
1376
- assert resolver.client and resolver.client.stub
1396
+ async def _load_remote(
1397
+ self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
1398
+ ):
1377
1399
  request = api_pb2.FunctionGetRequest(
1378
1400
  app_name=app_name,
1379
1401
  object_tag=name,
1380
- environment_name=_get_environment_name(environment_name, resolver) or "",
1402
+ environment_name=load_context.environment_name,
1381
1403
  )
1382
1404
  try:
1383
- response = await retry_transient_errors(resolver.client.stub.FunctionGet, request)
1405
+ response = await load_context.client.stub.FunctionGet(request)
1384
1406
  except NotFoundError as exc:
1385
1407
  # refine the error message
1386
- env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
1408
+ env_context = (
1409
+ f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
1410
+ )
1387
1411
  raise NotFoundError(
1388
1412
  f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
1389
1413
  ) from None
1390
1414
 
1391
1415
  print_server_warnings(response.server_warnings)
1392
1416
 
1393
- self._hydrate(response.function_id, resolver.client, response.handle_metadata)
1417
+ self._hydrate(response.function_id, load_context.client, response.handle_metadata)
1394
1418
 
1395
- environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
1419
+ environment_rep = (
1420
+ f", environment_name={load_context_overrides.environment_name!r}"
1421
+ if load_context_overrides._environment_name # slightly ugly - checking if _environment_name is overridden
1422
+ else ""
1423
+ )
1396
1424
  rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
1397
- return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
1425
+ return cls._from_loader(
1426
+ _load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
1427
+ )
1398
1428
 
1399
1429
  @classmethod
1400
1430
  def from_name(
@@ -1404,6 +1434,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1404
1434
  *,
1405
1435
  namespace=None, # mdmd:line-hidden
1406
1436
  environment_name: Optional[str] = None,
1437
+ client: Optional[_Client] = None,
1407
1438
  ) -> "_Function":
1408
1439
  """Reference a Function from a deployed App by its name.
1409
1440
 
@@ -1427,7 +1458,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1427
1458
  )
1428
1459
 
1429
1460
  warn_if_passing_namespace(namespace, "modal.Function.from_name")
1430
- return cls._from_name(app_name, name, environment_name=environment_name)
1461
+ return cls._from_name(
1462
+ app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
1463
+ )
1431
1464
 
1432
1465
  @property
1433
1466
  def tag(self) -> str:
@@ -1650,8 +1683,8 @@ Use the `Function.get_web_url()` method instead.
1650
1683
  input_queue,
1651
1684
  self.client,
1652
1685
  )
1653
- metadata = api_pb2.FunctionCallFromIdResponse(function_call_id=function_call_id, num_inputs=num_inputs)
1654
- fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, metadata)
1686
+ fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, None)
1687
+ fc._num_inputs = num_inputs # set the cached value of num_inputs
1655
1688
  return fc
1656
1689
 
1657
1690
  async def _call_function(self, args, kwargs) -> ReturnType:
@@ -1880,10 +1913,9 @@ Use the `Function.get_web_url()` method instead.
1880
1913
  @live_method
1881
1914
  async def get_current_stats(self) -> FunctionStats:
1882
1915
  """Return a `FunctionStats` object describing the current function's queue and runner counts."""
1883
- resp = await retry_transient_errors(
1884
- self.client.stub.FunctionGetCurrentStats,
1916
+ resp = await self.client.stub.FunctionGetCurrentStats(
1885
1917
  api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
1886
- total_timeout=10.0,
1918
+ retry=Retry(total_timeout=10.0),
1887
1919
  )
1888
1920
  return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
1889
1921
 
@@ -1921,19 +1953,16 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1921
1953
  def _invocation(self):
1922
1954
  return _Invocation(self.client.stub, self.object_id, self.client)
1923
1955
 
1924
- def _hydrate_metadata(self, metadata: Optional[Message]):
1925
- if not metadata:
1926
- return
1927
- assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
1928
- self._num_inputs = metadata.num_inputs
1929
-
1930
1956
  @live_method
1931
1957
  async def num_inputs(self) -> int:
1932
1958
  """Get the number of inputs in the function call."""
1933
- # Should have been hydrated.
1934
- assert self._num_inputs is not None
1959
+ if self._num_inputs is None:
1960
+ request = api_pb2.FunctionCallFromIdRequest(function_call_id=self.object_id)
1961
+ resp = await self.client.stub.FunctionCallFromId(request)
1962
+ self._num_inputs = resp.num_inputs # cached
1935
1963
  return self._num_inputs
1936
1964
 
1965
+ @live_method
1937
1966
  async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
1938
1967
  """Get the result of the index-th input of the function call.
1939
1968
  `.spawn()` calls have a single output, so only specifying `index=0` is valid.
@@ -1977,6 +2006,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1977
2006
  async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
1978
2007
  yield item
1979
2008
 
2009
+ @live_method
1980
2010
  async def get_call_graph(self) -> list[InputInfo]:
1981
2011
  """Returns a structure representing the call graph from a given root
1982
2012
  call ID, along with the status of execution for each node.
@@ -1986,9 +2016,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1986
2016
  """
1987
2017
  assert self._client and self._client.stub
1988
2018
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
1989
- response = await retry_transient_errors(self._client.stub.FunctionGetCallGraph, request)
2019
+ response = await self._client.stub.FunctionGetCallGraph(request)
1990
2020
  return _reconstruct_call_graph(response)
1991
2021
 
2022
+ @live_method
1992
2023
  async def cancel(
1993
2024
  self,
1994
2025
  # if true, containers running the inputs are forcibly terminated
@@ -2004,10 +2035,13 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2004
2035
  function_call_id=self.object_id, terminate_containers=terminate_containers
2005
2036
  )
2006
2037
  assert self._client and self._client.stub
2007
- await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
2038
+ await self._client.stub.FunctionCallCancel(request)
2008
2039
 
2009
- @staticmethod
2010
- async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
2040
+ @deprecate_aio_usage((2025, 11, 14), "FunctionCall.from_id")
2041
+ @classmethod
2042
+ def from_id(
2043
+ cls, function_call_id: str, client: Optional["modal.client.Client"] = None
2044
+ ) -> "modal.functions.FunctionCall[Any]":
2011
2045
  """Instantiate a FunctionCall object from an existing ID.
2012
2046
 
2013
2047
  Examples:
@@ -2018,7 +2052,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2018
2052
  fc_id = fc.object_id
2019
2053
 
2020
2054
  # Later, use the ID to re-instantiate the FunctionCall object
2021
- fc = _FunctionCall.from_id(fc_id)
2055
+ fc = FunctionCall.from_id(fc_id)
2022
2056
  result = fc.get()
2023
2057
  ```
2024
2058
 
@@ -2026,20 +2060,19 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2026
2060
  if you no longer have access to the original object returned from `Function.spawn`.
2027
2061
 
2028
2062
  """
2029
- if client is None:
2030
- client = await _Client.from_env()
2063
+ _client = typing.cast(_Client, synchronizer._translate_in(client))
2031
2064
 
2032
- async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]):
2033
- request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id)
2034
- resp = await retry_transient_errors(resolver.client.stub.FunctionCallFromId, request)
2035
- self._hydrate(function_call_id, resolver.client, resp)
2065
+ async def _load(
2066
+ self: _FunctionCall, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
2067
+ ):
2068
+ # this loader doesn't do anything in practice, but it will get the client from the load_context
2069
+ self._hydrate(function_call_id, load_context.client, None)
2036
2070
 
2037
2071
  rep = f"FunctionCall.from_id({function_call_id!r})"
2038
- fc: _FunctionCall[Any] = _FunctionCall._from_loader(_load, rep, hydrate_lazily=True)
2039
- # We already know the object ID, so we can set it directly
2040
- fc._object_id = function_call_id
2041
- fc._client = client
2042
- return fc
2072
+ impl_instance = _FunctionCall._from_loader(
2073
+ _load, rep, hydrate_lazily=True, load_context_overrides=LoadContext(client=_client)
2074
+ )
2075
+ return typing.cast("modal.functions.FunctionCall[Any]", synchronizer._translate_out(impl_instance))
2043
2076
 
2044
2077
  @staticmethod
2045
2078
  async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]: