modal 1.1.5.dev83__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.
- modal/__init__.py +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +146 -121
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +26 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +215 -96
- modal/app.pyi +78 -37
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +23 -5
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +73 -47
- modal/image.pyi +33 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -45
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -108
- modal/sandbox.pyi +226 -63
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +86 -30
- modal_proto/api_grpc.py +10 -25
- modal_proto/api_pb2.py +1080 -1047
- modal_proto/api_pb2.pyi +253 -79
- modal_proto/api_pb2_grpc.py +14 -48
- modal_proto/api_pb2_grpc.pyi +6 -18
- modal_proto/modal_api_grpc.py +175 -176
- modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev83.dist-info/RECORD +0 -191
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- modal_proto/sandbox_router_grpc.py +0 -105
- modal_proto/sandbox_router_pb2.py +0 -148
- modal_proto/sandbox_router_pb2_grpc.py +0 -203
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev83.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
|
|
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 .
|
|
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
|
|
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
|
|
168
|
-
client.stub.FunctionMap,
|
|
169
|
+
response = await client.stub.FunctionMap(
|
|
169
170
|
request,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
377
|
-
|
|
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
|
|
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
|
|
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
|
|
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,7 +591,7 @@ 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
|
|
|
@@ -669,7 +659,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
669
659
|
@staticmethod
|
|
670
660
|
def from_local(
|
|
671
661
|
info: FunctionInfo,
|
|
672
|
-
app,
|
|
662
|
+
app: Optional["modal.app._App"], # App here should only be None in case of Image.run_function
|
|
673
663
|
image: _Image,
|
|
674
664
|
env: Optional[dict[str, Optional[str]]] = None,
|
|
675
665
|
secrets: Optional[Collection[_Secret]] = None,
|
|
@@ -695,7 +685,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
695
685
|
batch_max_size: Optional[int] = None,
|
|
696
686
|
batch_wait_ms: Optional[int] = None,
|
|
697
687
|
cloud: Optional[str] = None,
|
|
698
|
-
|
|
688
|
+
region: Optional[Union[str, Sequence[str]]] = None,
|
|
689
|
+
nonpreemptible: bool = False,
|
|
699
690
|
is_builder_function: bool = False,
|
|
700
691
|
is_auto_snapshot: bool = False,
|
|
701
692
|
enable_memory_snapshot: bool = False,
|
|
@@ -705,13 +696,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
705
696
|
# Experimental: Clustered functions
|
|
706
697
|
cluster_size: Optional[int] = None,
|
|
707
698
|
rdma: Optional[bool] = None,
|
|
708
|
-
|
|
699
|
+
single_use_containers: bool = False,
|
|
709
700
|
ephemeral_disk: Optional[int] = None,
|
|
710
701
|
include_source: bool = True,
|
|
711
702
|
experimental_options: Optional[dict[str, str]] = None,
|
|
712
703
|
_experimental_proxy_ip: Optional[str] = None,
|
|
713
704
|
_experimental_custom_scaling_factor: Optional[float] = None,
|
|
714
705
|
restrict_output: bool = False,
|
|
706
|
+
http_config: Optional[api_pb2.HTTPConfig] = None,
|
|
715
707
|
) -> "_Function":
|
|
716
708
|
"""mdmd:hidden
|
|
717
709
|
|
|
@@ -758,6 +750,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
758
750
|
if env:
|
|
759
751
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
760
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
|
+
|
|
761
758
|
function_spec = _FunctionSpec(
|
|
762
759
|
mounts=all_mounts,
|
|
763
760
|
secrets=secrets,
|
|
@@ -790,6 +787,16 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
790
787
|
scaledown_window=scaledown_window,
|
|
791
788
|
)
|
|
792
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
|
+
|
|
793
800
|
if _experimental_custom_scaling_factor is not None and (
|
|
794
801
|
_experimental_custom_scaling_factor < 0 or _experimental_custom_scaling_factor > 1
|
|
795
802
|
):
|
|
@@ -815,14 +822,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
815
822
|
if arg.default is not inspect.Parameter.empty:
|
|
816
823
|
raise InvalidError(f"Modal batched function {func_name} does not accept default arguments.")
|
|
817
824
|
|
|
818
|
-
if max_inputs is not None:
|
|
819
|
-
if not isinstance(max_inputs, int):
|
|
820
|
-
raise InvalidError(f"`max_inputs` must be an int, not {type(max_inputs).__name__}")
|
|
821
|
-
if max_inputs <= 0:
|
|
822
|
-
raise InvalidError("`max_inputs` must be positive")
|
|
823
|
-
if max_inputs > 1:
|
|
824
|
-
raise InvalidError("Only `max_inputs=1` is currently supported")
|
|
825
|
-
|
|
826
825
|
# Validate volumes
|
|
827
826
|
validated_volumes = validate_volumes(volumes)
|
|
828
827
|
cloud_bucket_mounts = [(k, v) for k, v in validated_volumes if isinstance(v, _CloudBucketMount)]
|
|
@@ -895,12 +894,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
895
894
|
is_web_endpoint, is_generator, restrict_output
|
|
896
895
|
)
|
|
897
896
|
|
|
898
|
-
async def _preload(
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
assert
|
|
897
|
+
async def _preload(
|
|
898
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
899
|
+
):
|
|
900
|
+
assert load_context.app_id
|
|
902
901
|
req = api_pb2.FunctionPrecreateRequest(
|
|
903
|
-
app_id=
|
|
902
|
+
app_id=load_context.app_id,
|
|
904
903
|
function_name=info.function_name,
|
|
905
904
|
function_type=function_type,
|
|
906
905
|
existing_function_id=existing_object_id or "",
|
|
@@ -916,11 +915,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
916
915
|
elif webhook_config:
|
|
917
916
|
req.webhook_config.CopyFrom(webhook_config)
|
|
918
917
|
|
|
919
|
-
response = await
|
|
920
|
-
self._hydrate(response.function_id,
|
|
918
|
+
response = await load_context.client.stub.FunctionPrecreate(req)
|
|
919
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
921
920
|
|
|
922
|
-
async def _load(
|
|
923
|
-
|
|
921
|
+
async def _load(
|
|
922
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
923
|
+
):
|
|
924
924
|
with FunctionCreationStatus(resolver, tag) as function_creation_status:
|
|
925
925
|
timeout_secs = timeout
|
|
926
926
|
|
|
@@ -992,6 +992,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
992
992
|
function_definition = api_pb2.Function(
|
|
993
993
|
module_name=info.module_name or "",
|
|
994
994
|
function_name=info.function_name,
|
|
995
|
+
implementation_name=info.implementation_name,
|
|
995
996
|
mount_ids=loaded_mount_ids,
|
|
996
997
|
secret_ids=[secret.object_id for secret in secrets],
|
|
997
998
|
image_id=(image.object_id if image else ""),
|
|
@@ -1027,9 +1028,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1027
1028
|
object_dependencies=object_dependencies,
|
|
1028
1029
|
block_network=block_network,
|
|
1029
1030
|
untrusted=restrict_modal_access,
|
|
1030
|
-
|
|
1031
|
+
single_use_containers=single_use_containers,
|
|
1032
|
+
max_inputs=int(single_use_containers), # TODO(michael) remove after worker rollover
|
|
1031
1033
|
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
|
|
1032
|
-
scheduler_placement=scheduler_placement
|
|
1034
|
+
scheduler_placement=scheduler_placement,
|
|
1033
1035
|
is_class=info.is_service_class(),
|
|
1034
1036
|
class_parameter_info=info.class_parameter_info(),
|
|
1035
1037
|
i6pn_enabled=i6pn_enabled,
|
|
@@ -1051,12 +1053,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1051
1053
|
function_schema=function_schema,
|
|
1052
1054
|
supported_input_formats=supported_input_formats,
|
|
1053
1055
|
supported_output_formats=supported_output_formats,
|
|
1056
|
+
http_config=http_config,
|
|
1054
1057
|
)
|
|
1055
1058
|
|
|
1056
1059
|
if isinstance(gpu, list):
|
|
1057
1060
|
function_data = api_pb2.FunctionData(
|
|
1058
1061
|
module_name=function_definition.module_name,
|
|
1059
1062
|
function_name=function_definition.function_name,
|
|
1063
|
+
implementation_name=function_definition.implementation_name,
|
|
1060
1064
|
function_type=function_definition.function_type,
|
|
1061
1065
|
warm_pool_size=function_definition.warm_pool_size,
|
|
1062
1066
|
concurrency_limit=function_definition.concurrency_limit,
|
|
@@ -1088,6 +1092,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1088
1092
|
untrusted=function_definition.untrusted,
|
|
1089
1093
|
supported_input_formats=supported_input_formats,
|
|
1090
1094
|
supported_output_formats=supported_output_formats,
|
|
1095
|
+
http_config=http_config,
|
|
1091
1096
|
)
|
|
1092
1097
|
|
|
1093
1098
|
ranked_functions = []
|
|
@@ -1116,24 +1121,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1116
1121
|
),
|
|
1117
1122
|
)
|
|
1118
1123
|
|
|
1119
|
-
assert
|
|
1124
|
+
assert load_context.app_id
|
|
1120
1125
|
assert (function_definition is None) != (function_data is None) # xor
|
|
1121
1126
|
request = api_pb2.FunctionCreateRequest(
|
|
1122
|
-
app_id=
|
|
1127
|
+
app_id=load_context.app_id,
|
|
1123
1128
|
function=function_definition,
|
|
1124
1129
|
function_data=function_data,
|
|
1125
1130
|
existing_function_id=existing_object_id or "",
|
|
1126
1131
|
)
|
|
1127
1132
|
try:
|
|
1128
|
-
response: api_pb2.FunctionCreateResponse = await
|
|
1129
|
-
|
|
1130
|
-
)
|
|
1131
|
-
except GRPCError as exc:
|
|
1132
|
-
if exc.status == Status.INVALID_ARGUMENT:
|
|
1133
|
-
raise InvalidError(exc.message)
|
|
1134
|
-
if exc.status == Status.FAILED_PRECONDITION:
|
|
1135
|
-
raise InvalidError(exc.message)
|
|
1136
|
-
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):
|
|
1137
1136
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
|
1138
1137
|
raise
|
|
1139
1138
|
function_creation_status.set_response(response)
|
|
@@ -1142,10 +1141,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1142
1141
|
serve_mounts = {m for m in all_mounts if m.is_local()}
|
|
1143
1142
|
serve_mounts |= image._serve_mounts
|
|
1144
1143
|
obj._serve_mounts = frozenset(serve_mounts)
|
|
1145
|
-
self._hydrate(response.function_id,
|
|
1144
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1146
1145
|
|
|
1147
1146
|
rep = f"Function({tag})"
|
|
1148
|
-
|
|
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)
|
|
1149
1152
|
|
|
1150
1153
|
obj._raw_f = info.raw_f
|
|
1151
1154
|
obj._info = info
|
|
@@ -1187,7 +1190,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1187
1190
|
|
|
1188
1191
|
parent = self
|
|
1189
1192
|
|
|
1190
|
-
async def _load(
|
|
1193
|
+
async def _load(
|
|
1194
|
+
param_bound_func: _Function,
|
|
1195
|
+
resolver: Resolver,
|
|
1196
|
+
load_context: LoadContext,
|
|
1197
|
+
existing_object_id: Optional[str],
|
|
1198
|
+
):
|
|
1191
1199
|
if not parent.is_hydrated:
|
|
1192
1200
|
# While the base Object.hydrate() method appears to be idempotent, it's not always safe
|
|
1193
1201
|
await parent.hydrate()
|
|
@@ -1220,7 +1228,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1220
1228
|
param_bound_func._hydrate_from_other(parent)
|
|
1221
1229
|
return
|
|
1222
1230
|
|
|
1223
|
-
environment_name = _get_environment_name(None, resolver)
|
|
1224
1231
|
assert parent is not None and parent.is_hydrated
|
|
1225
1232
|
|
|
1226
1233
|
if options:
|
|
@@ -1260,11 +1267,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1260
1267
|
function_id=parent.object_id,
|
|
1261
1268
|
serialized_params=serialized_params,
|
|
1262
1269
|
function_options=options_pb,
|
|
1263
|
-
environment_name=environment_name
|
|
1270
|
+
environment_name=load_context.environment_name
|
|
1264
1271
|
or "", # TODO: investigate shouldn't environment name always be specified here?
|
|
1265
1272
|
)
|
|
1266
1273
|
|
|
1267
|
-
response = await
|
|
1274
|
+
response = await parent._client.stub.FunctionBindParams(req)
|
|
1268
1275
|
param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
|
|
1269
1276
|
|
|
1270
1277
|
def _deps():
|
|
@@ -1277,7 +1284,13 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1277
1284
|
return [dep for dep in all_deps if not dep.is_hydrated]
|
|
1278
1285
|
return []
|
|
1279
1286
|
|
|
1280
|
-
fun: _Function = _Function._from_loader(
|
|
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
|
+
)
|
|
1281
1294
|
|
|
1282
1295
|
fun._info = self._info
|
|
1283
1296
|
fun._obj = obj
|
|
@@ -1328,7 +1341,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1328
1341
|
scaledown_window=scaledown_window,
|
|
1329
1342
|
)
|
|
1330
1343
|
request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
|
|
1331
|
-
await
|
|
1344
|
+
await self.client.stub.FunctionUpdateSchedulingParams(request)
|
|
1332
1345
|
|
|
1333
1346
|
# One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
|
|
1334
1347
|
# and then we could return them here (would need some ad hoc dataclass, which I don't love)
|
|
@@ -1375,34 +1388,43 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1375
1388
|
cls,
|
|
1376
1389
|
app_name: str,
|
|
1377
1390
|
name: str,
|
|
1378
|
-
|
|
1379
|
-
|
|
1391
|
+
*,
|
|
1392
|
+
load_context_overrides: LoadContext,
|
|
1380
1393
|
):
|
|
1381
1394
|
# internal function lookup implementation that allows lookup of class "service functions"
|
|
1382
1395
|
# in addition to non-class functions
|
|
1383
|
-
async def _load_remote(
|
|
1384
|
-
|
|
1396
|
+
async def _load_remote(
|
|
1397
|
+
self: _Function, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
|
|
1398
|
+
):
|
|
1385
1399
|
request = api_pb2.FunctionGetRequest(
|
|
1386
1400
|
app_name=app_name,
|
|
1387
1401
|
object_tag=name,
|
|
1388
|
-
environment_name=
|
|
1402
|
+
environment_name=load_context.environment_name,
|
|
1389
1403
|
)
|
|
1390
1404
|
try:
|
|
1391
|
-
response = await
|
|
1405
|
+
response = await load_context.client.stub.FunctionGet(request)
|
|
1392
1406
|
except NotFoundError as exc:
|
|
1393
1407
|
# refine the error message
|
|
1394
|
-
env_context =
|
|
1408
|
+
env_context = (
|
|
1409
|
+
f" (in the '{load_context.environment_name}' environment)" if load_context.environment_name else ""
|
|
1410
|
+
)
|
|
1395
1411
|
raise NotFoundError(
|
|
1396
1412
|
f"Lookup failed for Function '{name}' from the '{app_name}' app{env_context}: {exc}."
|
|
1397
1413
|
) from None
|
|
1398
1414
|
|
|
1399
1415
|
print_server_warnings(response.server_warnings)
|
|
1400
1416
|
|
|
1401
|
-
self._hydrate(response.function_id,
|
|
1417
|
+
self._hydrate(response.function_id, load_context.client, response.handle_metadata)
|
|
1402
1418
|
|
|
1403
|
-
environment_rep =
|
|
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
|
+
)
|
|
1404
1424
|
rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
|
|
1405
|
-
return cls._from_loader(
|
|
1425
|
+
return cls._from_loader(
|
|
1426
|
+
_load_remote, rep, is_another_app=True, hydrate_lazily=True, load_context_overrides=load_context_overrides
|
|
1427
|
+
)
|
|
1406
1428
|
|
|
1407
1429
|
@classmethod
|
|
1408
1430
|
def from_name(
|
|
@@ -1412,6 +1434,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1412
1434
|
*,
|
|
1413
1435
|
namespace=None, # mdmd:line-hidden
|
|
1414
1436
|
environment_name: Optional[str] = None,
|
|
1437
|
+
client: Optional[_Client] = None,
|
|
1415
1438
|
) -> "_Function":
|
|
1416
1439
|
"""Reference a Function from a deployed App by its name.
|
|
1417
1440
|
|
|
@@ -1435,7 +1458,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1435
1458
|
)
|
|
1436
1459
|
|
|
1437
1460
|
warn_if_passing_namespace(namespace, "modal.Function.from_name")
|
|
1438
|
-
return cls._from_name(
|
|
1461
|
+
return cls._from_name(
|
|
1462
|
+
app_name, name, load_context_overrides=LoadContext(environment_name=environment_name, client=client)
|
|
1463
|
+
)
|
|
1439
1464
|
|
|
1440
1465
|
@property
|
|
1441
1466
|
def tag(self) -> str:
|
|
@@ -1658,8 +1683,8 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1658
1683
|
input_queue,
|
|
1659
1684
|
self.client,
|
|
1660
1685
|
)
|
|
1661
|
-
|
|
1662
|
-
fc
|
|
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
|
|
1663
1688
|
return fc
|
|
1664
1689
|
|
|
1665
1690
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
@@ -1888,10 +1913,9 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1888
1913
|
@live_method
|
|
1889
1914
|
async def get_current_stats(self) -> FunctionStats:
|
|
1890
1915
|
"""Return a `FunctionStats` object describing the current function's queue and runner counts."""
|
|
1891
|
-
resp = await
|
|
1892
|
-
self.client.stub.FunctionGetCurrentStats,
|
|
1916
|
+
resp = await self.client.stub.FunctionGetCurrentStats(
|
|
1893
1917
|
api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
|
|
1894
|
-
total_timeout=10.0,
|
|
1918
|
+
retry=Retry(total_timeout=10.0),
|
|
1895
1919
|
)
|
|
1896
1920
|
return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
|
|
1897
1921
|
|
|
@@ -1929,19 +1953,16 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1929
1953
|
def _invocation(self):
|
|
1930
1954
|
return _Invocation(self.client.stub, self.object_id, self.client)
|
|
1931
1955
|
|
|
1932
|
-
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
1933
|
-
if not metadata:
|
|
1934
|
-
return
|
|
1935
|
-
assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
|
|
1936
|
-
self._num_inputs = metadata.num_inputs
|
|
1937
|
-
|
|
1938
1956
|
@live_method
|
|
1939
1957
|
async def num_inputs(self) -> int:
|
|
1940
1958
|
"""Get the number of inputs in the function call."""
|
|
1941
|
-
|
|
1942
|
-
|
|
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
|
|
1943
1963
|
return self._num_inputs
|
|
1944
1964
|
|
|
1965
|
+
@live_method
|
|
1945
1966
|
async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
|
|
1946
1967
|
"""Get the result of the index-th input of the function call.
|
|
1947
1968
|
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
@@ -1985,6 +2006,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1985
2006
|
async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
|
|
1986
2007
|
yield item
|
|
1987
2008
|
|
|
2009
|
+
@live_method
|
|
1988
2010
|
async def get_call_graph(self) -> list[InputInfo]:
|
|
1989
2011
|
"""Returns a structure representing the call graph from a given root
|
|
1990
2012
|
call ID, along with the status of execution for each node.
|
|
@@ -1994,9 +2016,10 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1994
2016
|
"""
|
|
1995
2017
|
assert self._client and self._client.stub
|
|
1996
2018
|
request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
|
|
1997
|
-
response = await
|
|
2019
|
+
response = await self._client.stub.FunctionGetCallGraph(request)
|
|
1998
2020
|
return _reconstruct_call_graph(response)
|
|
1999
2021
|
|
|
2022
|
+
@live_method
|
|
2000
2023
|
async def cancel(
|
|
2001
2024
|
self,
|
|
2002
2025
|
# if true, containers running the inputs are forcibly terminated
|
|
@@ -2012,10 +2035,13 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2012
2035
|
function_call_id=self.object_id, terminate_containers=terminate_containers
|
|
2013
2036
|
)
|
|
2014
2037
|
assert self._client and self._client.stub
|
|
2015
|
-
await
|
|
2038
|
+
await self._client.stub.FunctionCallCancel(request)
|
|
2016
2039
|
|
|
2017
|
-
@
|
|
2018
|
-
|
|
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]":
|
|
2019
2045
|
"""Instantiate a FunctionCall object from an existing ID.
|
|
2020
2046
|
|
|
2021
2047
|
Examples:
|
|
@@ -2026,7 +2052,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2026
2052
|
fc_id = fc.object_id
|
|
2027
2053
|
|
|
2028
2054
|
# Later, use the ID to re-instantiate the FunctionCall object
|
|
2029
|
-
fc =
|
|
2055
|
+
fc = FunctionCall.from_id(fc_id)
|
|
2030
2056
|
result = fc.get()
|
|
2031
2057
|
```
|
|
2032
2058
|
|
|
@@ -2034,20 +2060,19 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2034
2060
|
if you no longer have access to the original object returned from `Function.spawn`.
|
|
2035
2061
|
|
|
2036
2062
|
"""
|
|
2037
|
-
|
|
2038
|
-
client = await _Client.from_env()
|
|
2063
|
+
_client = typing.cast(_Client, synchronizer._translate_in(client))
|
|
2039
2064
|
|
|
2040
|
-
async def _load(
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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)
|
|
2044
2070
|
|
|
2045
2071
|
rep = f"FunctionCall.from_id({function_call_id!r})"
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
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))
|
|
2051
2076
|
|
|
2052
2077
|
@staticmethod
|
|
2053
2078
|
async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
|