modal 0.67.1__py3-none-any.whl → 0.67.33__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.
- modal/_clustered_functions.py +2 -2
- modal/_clustered_functions.pyi +2 -2
- modal/_container_entrypoint.py +8 -5
- modal/_output.py +29 -28
- modal/_pty.py +2 -2
- modal/_resolver.py +6 -5
- modal/_resources.py +3 -3
- modal/_runtime/asgi.py +46 -6
- modal/_runtime/container_io_manager.py +22 -26
- modal/_runtime/execution_context.py +2 -2
- modal/_runtime/telemetry.py +1 -2
- modal/_runtime/user_code_imports.py +12 -14
- modal/_serialization.py +3 -7
- modal/_traceback.py +5 -5
- modal/_tunnel.py +5 -4
- modal/_tunnel.pyi +2 -2
- modal/_utils/async_utils.py +53 -17
- modal/_utils/blob_utils.py +22 -7
- modal/_utils/function_utils.py +20 -10
- modal/_utils/grpc_testing.py +7 -6
- modal/_utils/grpc_utils.py +2 -3
- modal/_utils/hash_utils.py +2 -2
- modal/_utils/mount_utils.py +5 -4
- modal/_utils/package_utils.py +2 -3
- modal/_utils/pattern_matcher.py +6 -6
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +2 -1
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +8 -7
- modal/app.py +68 -62
- modal/app.pyi +104 -99
- modal/call_graph.py +6 -6
- modal/cli/_download.py +3 -2
- modal/cli/_traceback.py +4 -4
- modal/cli/app.py +4 -4
- modal/cli/container.py +4 -4
- modal/cli/dict.py +1 -1
- modal/cli/environment.py +2 -3
- modal/cli/import_refs.py +1 -1
- modal/cli/launch.py +2 -2
- modal/cli/network_file_system.py +1 -1
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +2 -2
- modal/cli/programs/vscode.py +3 -3
- modal/cli/queues.py +1 -1
- modal/cli/run.py +6 -6
- modal/cli/secret.py +3 -3
- modal/cli/utils.py +2 -1
- modal/cli/volume.py +3 -3
- modal/client.py +6 -11
- modal/client.pyi +18 -27
- modal/cloud_bucket_mount.py +3 -3
- modal/cloud_bucket_mount.pyi +2 -2
- modal/cls.py +100 -47
- modal/cls.pyi +40 -40
- modal/config.py +3 -2
- modal/container_process.py +6 -2
- modal/dict.py +6 -3
- modal/dict.pyi +10 -9
- modal/environments.py +3 -3
- modal/environments.pyi +3 -3
- modal/exception.py +2 -3
- modal/functions.py +112 -104
- modal/functions.pyi +77 -58
- modal/image.py +59 -57
- modal/image.pyi +104 -103
- modal/io_streams.py +20 -12
- modal/io_streams.pyi +24 -14
- modal/mount.py +24 -24
- modal/mount.pyi +28 -29
- modal/network_file_system.py +14 -11
- modal/network_file_system.pyi +12 -11
- modal/object.py +9 -8
- modal/object.pyi +47 -34
- modal/output.py +2 -1
- modal/parallel_map.py +4 -4
- modal/partial_function.py +10 -14
- modal/partial_function.pyi +17 -18
- modal/queue.py +11 -8
- modal/queue.pyi +23 -22
- modal/retries.py +38 -0
- modal/runner.py +8 -7
- modal/runner.pyi +8 -14
- modal/running_app.py +3 -3
- modal/sandbox.py +20 -13
- modal/sandbox.pyi +73 -72
- modal/scheduler_placement.py +2 -1
- modal/secret.py +7 -7
- modal/secret.pyi +12 -12
- modal/serving.py +4 -3
- modal/serving.pyi +5 -4
- modal/token_flow.py +3 -2
- modal/token_flow.pyi +3 -3
- modal/volume.py +16 -23
- modal/volume.pyi +17 -16
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/METADATA +2 -2
- modal-0.67.33.dist-info/RECORD +168 -0
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/mounts/python_standalone.py +1 -1
- modal_proto/api.proto +15 -0
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +674 -654
- modal_proto/api_pb2.pyi +45 -1
- modal_proto/api_pb2_grpc.py +66 -0
- modal_proto/api_pb2_grpc.pyi +20 -0
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/_version_generated.py +1 -1
- modal-0.67.1.dist-info/RECORD +0 -168
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/LICENSE +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/WHEEL +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/entry_points.txt +0 -0
- {modal-0.67.1.dist-info → modal-0.67.33.dist-info}/top_level.txt +0 -0
modal/exception.py
CHANGED
@@ -4,7 +4,6 @@ import signal
|
|
4
4
|
import sys
|
5
5
|
import warnings
|
6
6
|
from datetime import date
|
7
|
-
from typing import Tuple
|
8
7
|
|
9
8
|
|
10
9
|
class Error(Exception):
|
@@ -132,12 +131,12 @@ def _is_internal_frame(frame):
|
|
132
131
|
return module in _INTERNAL_MODULES
|
133
132
|
|
134
133
|
|
135
|
-
def deprecation_error(deprecated_on:
|
134
|
+
def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
|
136
135
|
raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
|
137
136
|
|
138
137
|
|
139
138
|
def deprecation_warning(
|
140
|
-
deprecated_on:
|
139
|
+
deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
|
141
140
|
) -> None:
|
142
141
|
"""Utility for getting the proper stack entry.
|
143
142
|
|
modal/functions.py
CHANGED
@@ -1,24 +1,18 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
|
+
import dataclasses
|
2
3
|
import inspect
|
3
4
|
import textwrap
|
4
5
|
import time
|
5
6
|
import typing
|
6
7
|
import warnings
|
8
|
+
from collections.abc import AsyncGenerator, Collection, Sequence, Sized
|
7
9
|
from dataclasses import dataclass
|
8
10
|
from pathlib import PurePosixPath
|
9
11
|
from typing import (
|
10
12
|
TYPE_CHECKING,
|
11
13
|
Any,
|
12
|
-
AsyncGenerator,
|
13
14
|
Callable,
|
14
|
-
Collection,
|
15
|
-
Dict,
|
16
|
-
List,
|
17
15
|
Optional,
|
18
|
-
Sequence,
|
19
|
-
Sized,
|
20
|
-
Tuple,
|
21
|
-
Type,
|
22
16
|
Union,
|
23
17
|
)
|
24
18
|
|
@@ -26,6 +20,7 @@ import typing_extensions
|
|
26
20
|
from google.protobuf.message import Message
|
27
21
|
from grpclib import GRPCError, Status
|
28
22
|
from synchronicity.combined_types import MethodWithAio
|
23
|
+
from synchronicity.exceptions import UserCodeException
|
29
24
|
|
30
25
|
from modal._utils.async_utils import aclosing
|
31
26
|
from modal_proto import api_pb2
|
@@ -64,6 +59,7 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
64
59
|
from .config import config
|
65
60
|
from .exception import (
|
66
61
|
ExecutionError,
|
62
|
+
FunctionTimeoutError,
|
67
63
|
InvalidError,
|
68
64
|
NotFoundError,
|
69
65
|
OutputExpiredError,
|
@@ -86,7 +82,7 @@ from .parallel_map import (
|
|
86
82
|
_SynchronizedQueue,
|
87
83
|
)
|
88
84
|
from .proxy import _Proxy
|
89
|
-
from .retries import Retries
|
85
|
+
from .retries import Retries, RetryManager
|
90
86
|
from .schedule import Schedule
|
91
87
|
from .scheduler_placement import SchedulerPlacement
|
92
88
|
from .secret import _Secret
|
@@ -98,15 +94,32 @@ if TYPE_CHECKING:
|
|
98
94
|
import modal.partial_function
|
99
95
|
|
100
96
|
|
97
|
+
@dataclasses.dataclass
|
98
|
+
class _RetryContext:
|
99
|
+
function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
|
100
|
+
retry_policy: api_pb2.FunctionRetryPolicy
|
101
|
+
function_call_jwt: str
|
102
|
+
input_jwt: str
|
103
|
+
input_id: str
|
104
|
+
item: api_pb2.FunctionPutInputsItem
|
105
|
+
|
106
|
+
|
101
107
|
class _Invocation:
|
102
108
|
"""Internal client representation of a single-input call to a Modal Function or Generator"""
|
103
109
|
|
104
110
|
stub: ModalClientModal
|
105
111
|
|
106
|
-
def __init__(
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
stub: ModalClientModal,
|
115
|
+
function_call_id: str,
|
116
|
+
client: _Client,
|
117
|
+
retry_context: Optional[_RetryContext] = None,
|
118
|
+
):
|
107
119
|
self.stub = stub
|
108
120
|
self.client = client # Used by the deserializer.
|
109
121
|
self.function_call_id = function_call_id # TODO: remove and use only input_id
|
122
|
+
self._retry_context = retry_context
|
110
123
|
|
111
124
|
@staticmethod
|
112
125
|
async def create(
|
@@ -132,7 +145,17 @@ class _Invocation:
|
|
132
145
|
function_call_id = response.function_call_id
|
133
146
|
|
134
147
|
if response.pipelined_inputs:
|
135
|
-
|
148
|
+
assert len(response.pipelined_inputs) == 1
|
149
|
+
input = response.pipelined_inputs[0]
|
150
|
+
retry_context = _RetryContext(
|
151
|
+
function_call_invocation_type=function_call_invocation_type,
|
152
|
+
retry_policy=response.retry_policy,
|
153
|
+
function_call_jwt=response.function_call_jwt,
|
154
|
+
input_jwt=input.input_jwt,
|
155
|
+
input_id=input.input_id,
|
156
|
+
item=item,
|
157
|
+
)
|
158
|
+
return _Invocation(client.stub, function_call_id, client, retry_context)
|
136
159
|
|
137
160
|
request_put = api_pb2.FunctionPutInputsRequest(
|
138
161
|
function_id=function_id, inputs=[item], function_call_id=function_call_id
|
@@ -144,7 +167,16 @@ class _Invocation:
|
|
144
167
|
processed_inputs = inputs_response.inputs
|
145
168
|
if not processed_inputs:
|
146
169
|
raise Exception("Could not create function call - the input queue seems to be full")
|
147
|
-
|
170
|
+
input = inputs_response.inputs[0]
|
171
|
+
retry_context = _RetryContext(
|
172
|
+
function_call_invocation_type=function_call_invocation_type,
|
173
|
+
retry_policy=response.retry_policy,
|
174
|
+
function_call_jwt=response.function_call_jwt,
|
175
|
+
input_jwt=input.input_jwt,
|
176
|
+
input_id=input.input_id,
|
177
|
+
item=item,
|
178
|
+
)
|
179
|
+
return _Invocation(client.stub, function_call_id, client, retry_context)
|
148
180
|
|
149
181
|
async def pop_function_call_outputs(
|
150
182
|
self, timeout: Optional[float], clear_on_success: bool
|
@@ -180,13 +212,46 @@ class _Invocation:
|
|
180
212
|
# return the last response to check for state of num_unfinished_inputs
|
181
213
|
return response
|
182
214
|
|
183
|
-
async def
|
215
|
+
async def _retry_input(self) -> None:
|
216
|
+
ctx = self._retry_context
|
217
|
+
if not ctx:
|
218
|
+
raise ValueError("Cannot retry input when _retry_context is empty.")
|
219
|
+
|
220
|
+
item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
|
221
|
+
request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
|
222
|
+
await retry_transient_errors(
|
223
|
+
self.client.stub.FunctionRetryInputs,
|
224
|
+
request,
|
225
|
+
)
|
226
|
+
|
227
|
+
async def _get_single_output(self) -> Any:
|
184
228
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
185
229
|
item: api_pb2.FunctionGetOutputsItem = (
|
186
230
|
await self.pop_function_call_outputs(timeout=None, clear_on_success=True)
|
187
231
|
).outputs[0]
|
188
232
|
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
189
233
|
|
234
|
+
async def run_function(self) -> Any:
|
235
|
+
# Use retry logic only if retry policy is specified and
|
236
|
+
ctx = self._retry_context
|
237
|
+
if (
|
238
|
+
not ctx
|
239
|
+
or not ctx.retry_policy
|
240
|
+
or ctx.retry_policy.retries == 0
|
241
|
+
or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
242
|
+
):
|
243
|
+
return await self._get_single_output()
|
244
|
+
|
245
|
+
# User errors including timeouts are managed by the user specified retry policy.
|
246
|
+
user_retry_manager = RetryManager(ctx.retry_policy)
|
247
|
+
|
248
|
+
while True:
|
249
|
+
try:
|
250
|
+
return await self._get_single_output()
|
251
|
+
except (UserCodeException, FunctionTimeoutError) as exc:
|
252
|
+
await user_retry_manager.raise_or_sleep(exc)
|
253
|
+
await self._retry_input()
|
254
|
+
|
190
255
|
async def poll_function(self, timeout: Optional[float] = None):
|
191
256
|
"""Waits up to timeout for a result from a function.
|
192
257
|
|
@@ -278,12 +343,12 @@ class _FunctionSpec:
|
|
278
343
|
image: Optional[_Image]
|
279
344
|
mounts: Sequence[_Mount]
|
280
345
|
secrets: Sequence[_Secret]
|
281
|
-
network_file_systems:
|
282
|
-
volumes:
|
283
|
-
gpus: Union[GPU_T,
|
346
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem]
|
347
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
348
|
+
gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
284
349
|
cloud: Optional[str]
|
285
350
|
cpu: Optional[float]
|
286
|
-
memory: Optional[Union[int,
|
351
|
+
memory: Optional[Union[int, tuple[int, int]]]
|
287
352
|
ephemeral_disk: Optional[int]
|
288
353
|
scheduler_placement: Optional[SchedulerPlacement]
|
289
354
|
|
@@ -304,7 +369,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
304
369
|
|
305
370
|
# TODO: more type annotations
|
306
371
|
_info: Optional[FunctionInfo]
|
307
|
-
_serve_mounts:
|
372
|
+
_serve_mounts: frozenset[_Mount] # set at load time, only by loader
|
308
373
|
_app: Optional["modal.app._App"] = None
|
309
374
|
_obj: Optional["modal.cls._Obj"] = None # only set for InstanceServiceFunctions and bound instance methods
|
310
375
|
_web_url: Optional[str]
|
@@ -323,7 +388,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
323
388
|
_use_method_name: str = ""
|
324
389
|
|
325
390
|
_class_parameter_info: Optional["api_pb2.ClassParameterInfo"] = None
|
326
|
-
_method_handle_metadata: Optional[
|
391
|
+
_method_handle_metadata: Optional[dict[str, "api_pb2.FunctionHandleMetadata"]] = None
|
327
392
|
|
328
393
|
def _bind_method(
|
329
394
|
self,
|
@@ -359,68 +424,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
359
424
|
fun._is_method = True
|
360
425
|
return fun
|
361
426
|
|
362
|
-
def _bind_instance_method(self, class_bound_method: "_Function"):
|
363
|
-
"""mdmd:hidden
|
364
|
-
|
365
|
-
Binds an "instance service function" to a specific method.
|
366
|
-
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
367
|
-
it does it forward invocations to the underlying instance_service_function with the specified method,
|
368
|
-
and we don't support web_config for parameterized methods at the moment.
|
369
|
-
"""
|
370
|
-
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
371
|
-
# underlying instance_service_function. It's currently used in order to take advantage
|
372
|
-
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
373
|
-
# object itself doesn't need any "loading"
|
374
|
-
instance_service_function = self
|
375
|
-
assert instance_service_function._obj
|
376
|
-
method_name = class_bound_method._use_method_name
|
377
|
-
full_function_name = f"{class_bound_method._function_name}[parameterized]"
|
378
|
-
|
379
|
-
def hydrate_from_instance_service_function(method_placeholder_fun):
|
380
|
-
method_placeholder_fun._hydrate_from_other(instance_service_function)
|
381
|
-
method_placeholder_fun._obj = instance_service_function._obj
|
382
|
-
method_placeholder_fun._web_url = (
|
383
|
-
class_bound_method._web_url
|
384
|
-
) # TODO: this shouldn't be set when actual parameters are used
|
385
|
-
method_placeholder_fun._function_name = full_function_name
|
386
|
-
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
387
|
-
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
388
|
-
method_placeholder_fun._use_method_name = method_name
|
389
|
-
method_placeholder_fun._is_method = True
|
390
|
-
|
391
|
-
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
392
|
-
# there is currently no actual loading logic executed to create each method on
|
393
|
-
# the *parameterized* instance of a class - it uses the parameter-bound service-function
|
394
|
-
# for the instance. This load method just makes sure to set all attributes after the
|
395
|
-
# `instance_service_function` has been loaded (it's in the `_deps`)
|
396
|
-
hydrate_from_instance_service_function(fun)
|
397
|
-
|
398
|
-
def _deps():
|
399
|
-
if instance_service_function.is_hydrated:
|
400
|
-
# without this check, the common instance_service_function will be reloaded by all methods
|
401
|
-
# TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
|
402
|
-
return []
|
403
|
-
return [instance_service_function]
|
404
|
-
|
405
|
-
rep = f"Method({full_function_name})"
|
406
|
-
|
407
|
-
fun = _Function._from_loader(
|
408
|
-
_load,
|
409
|
-
rep,
|
410
|
-
deps=_deps,
|
411
|
-
hydrate_lazily=True,
|
412
|
-
)
|
413
|
-
if instance_service_function.is_hydrated:
|
414
|
-
# Eager hydration (skip load) if the instance service function is already loaded
|
415
|
-
hydrate_from_instance_service_function(fun)
|
416
|
-
|
417
|
-
fun._info = class_bound_method._info
|
418
|
-
fun._obj = instance_service_function._obj
|
419
|
-
fun._is_method = True
|
420
|
-
fun._app = class_bound_method._app
|
421
|
-
fun._spec = class_bound_method._spec
|
422
|
-
return fun
|
423
|
-
|
424
427
|
@staticmethod
|
425
428
|
def from_args(
|
426
429
|
info: FunctionInfo,
|
@@ -429,14 +432,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
429
432
|
secrets: Sequence[_Secret] = (),
|
430
433
|
schedule: Optional[Schedule] = None,
|
431
434
|
is_generator=False,
|
432
|
-
gpu: Union[GPU_T,
|
435
|
+
gpu: Union[GPU_T, list[GPU_T]] = None,
|
433
436
|
# TODO: maybe break this out into a separate decorator for notebooks.
|
434
437
|
mounts: Collection[_Mount] = (),
|
435
|
-
network_file_systems:
|
438
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
436
439
|
allow_cross_region_volumes: bool = False,
|
437
|
-
volumes:
|
440
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
438
441
|
webhook_config: Optional[api_pb2.WebhookConfig] = None,
|
439
|
-
memory: Optional[Union[int,
|
442
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
440
443
|
proxy: Optional[_Proxy] = None,
|
441
444
|
retries: Optional[Union[int, Retries]] = None,
|
442
445
|
timeout: Optional[int] = None,
|
@@ -623,8 +626,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
623
626
|
if image is not None and not isinstance(image, _Image):
|
624
627
|
raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
|
625
628
|
|
626
|
-
method_definitions: Optional[
|
627
|
-
partial_functions:
|
629
|
+
method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
|
630
|
+
partial_functions: dict[str, "modal.partial_function._PartialFunction"] = {}
|
628
631
|
if info.user_cls:
|
629
632
|
method_definitions = {}
|
630
633
|
partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
|
@@ -640,8 +643,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
640
643
|
|
641
644
|
function_type = get_function_type(is_generator)
|
642
645
|
|
643
|
-
def _deps(only_explicit_mounts=False) ->
|
644
|
-
deps:
|
646
|
+
def _deps(only_explicit_mounts=False) -> list[_Object]:
|
647
|
+
deps: list[_Object] = list(secrets)
|
645
648
|
if only_explicit_mounts:
|
646
649
|
# TODO: this is a bit hacky, but all_mounts may differ in the container vs locally
|
647
650
|
# We don't want the function dependencies to change, so we have this way to force it to
|
@@ -821,6 +824,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
821
824
|
is_method=function_definition.is_method,
|
822
825
|
use_function_id=function_definition.use_function_id,
|
823
826
|
use_method_name=function_definition.use_method_name,
|
827
|
+
method_definitions=function_definition.method_definitions,
|
828
|
+
method_definitions_set=function_definition.method_definitions_set,
|
824
829
|
_experimental_group_size=function_definition._experimental_group_size,
|
825
830
|
_experimental_buffer_containers=function_definition._experimental_buffer_containers,
|
826
831
|
_experimental_custom_scaling=function_definition._experimental_custom_scaling,
|
@@ -847,9 +852,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
847
852
|
function_data.ranked_functions.extend(ranked_functions)
|
848
853
|
function_definition = None # function_definition is not used in this case
|
849
854
|
else:
|
850
|
-
# TODO(irfansharif): Assert on this specific type once
|
851
|
-
#
|
852
|
-
# assert isinstance(gpu, GPU_T) # includes the case where gpu==None case
|
855
|
+
# TODO(irfansharif): Assert on this specific type once we get rid of python 3.9.
|
856
|
+
# assert isinstance(gpu, GPU_T) # includes the case where gpu==None case
|
853
857
|
function_definition.resources.CopyFrom(
|
854
858
|
convert_fn_config_to_resources_config(
|
855
859
|
cpu=cpu, memory=memory, gpu=gpu, ephemeral_disk=ephemeral_disk
|
@@ -878,7 +882,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
878
882
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
879
883
|
raise
|
880
884
|
function_creation_status.set_response(response)
|
881
|
-
serve_mounts =
|
885
|
+
serve_mounts = {m for m in all_mounts if m.is_local()} # needed for modal.serve file watching
|
882
886
|
serve_mounts |= image._serve_mounts
|
883
887
|
obj._serve_mounts = frozenset(serve_mounts)
|
884
888
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
@@ -897,7 +901,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
897
901
|
obj._spec = function_spec # needed for modal shell
|
898
902
|
|
899
903
|
# Used to check whether we should rebuild a modal.Image which uses `run_function`.
|
900
|
-
gpus:
|
904
|
+
gpus: list[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
|
901
905
|
obj._build_args = dict( # See get_build_def
|
902
906
|
secrets=repr(secrets),
|
903
907
|
gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
|
@@ -916,10 +920,9 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
916
920
|
def _bind_parameters(
|
917
921
|
self,
|
918
922
|
obj: "modal.cls._Obj",
|
919
|
-
from_other_workspace: bool,
|
920
923
|
options: Optional[api_pb2.FunctionOptions],
|
921
924
|
args: Sized,
|
922
|
-
kwargs:
|
925
|
+
kwargs: dict[str, Any],
|
923
926
|
) -> "_Function":
|
924
927
|
"""mdmd:hidden
|
925
928
|
|
@@ -927,7 +930,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
927
930
|
"""
|
928
931
|
|
929
932
|
# In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
|
930
|
-
can_use_parent = len(args) + len(kwargs) == 0 and
|
933
|
+
can_use_parent = len(args) + len(kwargs) == 0 and options is None
|
931
934
|
parent = self
|
932
935
|
|
933
936
|
async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
|
@@ -997,7 +1000,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
997
1000
|
Please exercise care when using this advanced feature!
|
998
1001
|
Setting and forgetting a warm pool on functions can lead to increased costs.
|
999
1002
|
|
1000
|
-
```python
|
1003
|
+
```python notest
|
1001
1004
|
# Usage on a regular function.
|
1002
1005
|
f = modal.Function.lookup("my-app", "function")
|
1003
1006
|
f.keep_warm(2)
|
@@ -1025,7 +1028,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1025
1028
|
|
1026
1029
|
@classmethod
|
1027
1030
|
def from_name(
|
1028
|
-
cls:
|
1031
|
+
cls: type["_Function"],
|
1029
1032
|
app_name: str,
|
1030
1033
|
tag: str,
|
1031
1034
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
@@ -1076,7 +1079,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1076
1079
|
In contrast to `modal.Function.from_name`, this is an eager method
|
1077
1080
|
that will hydrate the local object with metadata from Modal servers.
|
1078
1081
|
|
1079
|
-
```python
|
1082
|
+
```python notest
|
1080
1083
|
f = modal.Function.lookup("other-app", "function")
|
1081
1084
|
```
|
1082
1085
|
"""
|
@@ -1232,13 +1235,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1232
1235
|
yield item
|
1233
1236
|
|
1234
1237
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
1238
|
+
if config.get("client_retries"):
|
1239
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
1240
|
+
else:
|
1241
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
|
1235
1242
|
invocation = await _Invocation.create(
|
1236
1243
|
self,
|
1237
1244
|
args,
|
1238
1245
|
kwargs,
|
1239
1246
|
client=self._client,
|
1240
|
-
function_call_invocation_type=
|
1247
|
+
function_call_invocation_type=function_call_invocation_type,
|
1241
1248
|
)
|
1249
|
+
|
1242
1250
|
return await invocation.run_function()
|
1243
1251
|
|
1244
1252
|
async def _call_function_nowait(
|
@@ -1355,12 +1363,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1355
1363
|
if is_async(info.raw_f):
|
1356
1364
|
# We want to run __aenter__ and fun in the same coroutine
|
1357
1365
|
async def coro():
|
1358
|
-
await obj.
|
1366
|
+
await obj._aenter()
|
1359
1367
|
return await fun(*args, **kwargs)
|
1360
1368
|
|
1361
1369
|
return coro() # type: ignore
|
1362
1370
|
else:
|
1363
|
-
obj.
|
1371
|
+
obj._enter()
|
1364
1372
|
return fun(*args, **kwargs)
|
1365
1373
|
|
1366
1374
|
@synchronizer.no_input_translation
|
@@ -1476,7 +1484,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1476
1484
|
async for res in self._invocation().run_generator():
|
1477
1485
|
yield res
|
1478
1486
|
|
1479
|
-
async def get_call_graph(self) ->
|
1487
|
+
async def get_call_graph(self) -> list[InputInfo]:
|
1480
1488
|
"""Returns a structure representing the call graph from a given root
|
1481
1489
|
call ID, along with the status of execution for each node.
|
1482
1490
|
|