modal 0.67.6__py3-none-any.whl → 0.67.11__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 +5 -4
- modal/_output.py +29 -28
- modal/_pty.py +2 -2
- modal/_resolver.py +6 -5
- modal/_resources.py +3 -3
- modal/_runtime/asgi.py +7 -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 +4 -3
- modal/_tunnel.pyi +2 -2
- modal/_utils/async_utils.py +8 -15
- modal/_utils/blob_utils.py +4 -3
- modal/_utils/function_utils.py +14 -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 +29 -34
- modal/app.pyi +102 -97
- 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/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 +30 -30
- modal/cls.pyi +35 -34
- modal/config.py +3 -2
- modal/dict.py +4 -3
- modal/dict.pyi +10 -9
- modal/environments.py +3 -3
- modal/environments.pyi +3 -3
- modal/exception.py +2 -3
- modal/functions.py +105 -35
- modal/functions.pyi +71 -48
- modal/image.py +45 -48
- modal/image.pyi +102 -101
- modal/io_streams.py +4 -7
- modal/io_streams.pyi +14 -13
- modal/mount.py +23 -22
- modal/mount.pyi +28 -29
- modal/network_file_system.py +7 -6
- 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 +9 -13
- modal/partial_function.pyi +17 -18
- modal/queue.py +9 -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 +14 -13
- modal/sandbox.pyi +67 -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 +7 -12
- modal/volume.pyi +17 -16
- {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/METADATA +1 -1
- modal-0.67.11.dist-info/RECORD +168 -0
- modal_docs/mdmd/signatures.py +1 -2
- modal_version/_version_generated.py +1 -1
- modal-0.67.6.dist-info/RECORD +0 -168
- {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/LICENSE +0 -0
- {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/WHEEL +0 -0
- {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/entry_points.txt +0 -0
- {modal-0.67.6.dist-info → modal-0.67.11.dist-info}/top_level.txt +0 -0
modal/cls.pyi
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import collections.abc
|
1
2
|
import google.protobuf.message
|
2
3
|
import inspect
|
3
4
|
import modal.app
|
@@ -20,10 +21,10 @@ def _use_annotation_parameters(user_cls) -> bool: ...
|
|
20
21
|
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
21
22
|
|
22
23
|
class _Obj:
|
23
|
-
_functions:
|
24
|
-
|
24
|
+
_functions: dict[str, modal.functions._Function]
|
25
|
+
_has_entered: bool
|
25
26
|
_user_cls_instance: typing.Optional[typing.Any]
|
26
|
-
_construction_args:
|
27
|
+
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
27
28
|
_instance_service_function: typing.Optional[modal.functions._Function]
|
28
29
|
|
29
30
|
def _uses_common_service_function(self): ...
|
@@ -31,7 +32,7 @@ class _Obj:
|
|
31
32
|
self,
|
32
33
|
user_cls: type,
|
33
34
|
class_service_function: typing.Optional[modal.functions._Function],
|
34
|
-
classbound_methods:
|
35
|
+
classbound_methods: dict[str, modal.functions._Function],
|
35
36
|
from_other_workspace: bool,
|
36
37
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
37
38
|
args,
|
@@ -40,26 +41,26 @@ class _Obj:
|
|
40
41
|
def _new_user_cls_instance(self): ...
|
41
42
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
42
43
|
def _cached_user_cls_instance(self): ...
|
43
|
-
def
|
44
|
+
def _enter(self): ...
|
44
45
|
@property
|
45
|
-
def
|
46
|
-
@
|
47
|
-
def
|
48
|
-
async def
|
46
|
+
def _entered(self) -> bool: ...
|
47
|
+
@_entered.setter
|
48
|
+
def _entered(self, val: bool): ...
|
49
|
+
async def _aenter(self): ...
|
49
50
|
def __getattr__(self, k): ...
|
50
51
|
|
51
52
|
class Obj:
|
52
|
-
_functions:
|
53
|
-
|
53
|
+
_functions: dict[str, modal.functions.Function]
|
54
|
+
_has_entered: bool
|
54
55
|
_user_cls_instance: typing.Optional[typing.Any]
|
55
|
-
_construction_args:
|
56
|
+
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
56
57
|
_instance_service_function: typing.Optional[modal.functions.Function]
|
57
58
|
|
58
59
|
def __init__(
|
59
60
|
self,
|
60
61
|
user_cls: type,
|
61
62
|
class_service_function: typing.Optional[modal.functions.Function],
|
62
|
-
classbound_methods:
|
63
|
+
classbound_methods: dict[str, modal.functions.Function],
|
63
64
|
from_other_workspace: bool,
|
64
65
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
65
66
|
args,
|
@@ -75,26 +76,26 @@ class Obj:
|
|
75
76
|
keep_warm: __keep_warm_spec
|
76
77
|
|
77
78
|
def _cached_user_cls_instance(self): ...
|
78
|
-
def
|
79
|
+
def _enter(self): ...
|
79
80
|
@property
|
80
|
-
def
|
81
|
-
@
|
82
|
-
def
|
83
|
-
async def
|
81
|
+
def _entered(self) -> bool: ...
|
82
|
+
@_entered.setter
|
83
|
+
def _entered(self, val: bool): ...
|
84
|
+
async def _aenter(self): ...
|
84
85
|
def __getattr__(self, k): ...
|
85
86
|
|
86
87
|
class _Cls(modal.object._Object):
|
87
88
|
_user_cls: typing.Optional[type]
|
88
89
|
_class_service_function: typing.Optional[modal.functions._Function]
|
89
|
-
_method_functions: typing.Optional[
|
90
|
+
_method_functions: typing.Optional[dict[str, modal.functions._Function]]
|
90
91
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
91
|
-
_callables:
|
92
|
+
_callables: dict[str, typing.Callable[..., typing.Any]]
|
92
93
|
_from_other_workspace: typing.Optional[bool]
|
93
94
|
_app: typing.Optional[modal.app._App]
|
94
95
|
|
95
96
|
def _initialize_from_empty(self): ...
|
96
97
|
def _initialize_from_other(self, other: _Cls): ...
|
97
|
-
def _get_partial_functions(self) ->
|
98
|
+
def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
|
98
99
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
99
100
|
@staticmethod
|
100
101
|
def validate_construction_mechanism(user_cls): ...
|
@@ -103,7 +104,7 @@ class _Cls(modal.object._Object):
|
|
103
104
|
def _uses_common_service_function(self): ...
|
104
105
|
@classmethod
|
105
106
|
def from_name(
|
106
|
-
cls:
|
107
|
+
cls: type[_Cls],
|
107
108
|
app_name: str,
|
108
109
|
tag: str,
|
109
110
|
namespace=1,
|
@@ -112,11 +113,11 @@ class _Cls(modal.object._Object):
|
|
112
113
|
) -> _Cls: ...
|
113
114
|
def with_options(
|
114
115
|
self: _Cls,
|
115
|
-
cpu: typing.Union[float,
|
116
|
-
memory: typing.Union[int,
|
116
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
117
|
+
memory: typing.Union[int, tuple[int, int], None] = None,
|
117
118
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
118
|
-
secrets:
|
119
|
-
volumes:
|
119
|
+
secrets: collections.abc.Collection[modal.secret._Secret] = (),
|
120
|
+
volumes: dict[typing.Union[str, os.PathLike], modal.volume._Volume] = {},
|
120
121
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
121
122
|
timeout: typing.Optional[int] = None,
|
122
123
|
concurrency_limit: typing.Optional[int] = None,
|
@@ -138,16 +139,16 @@ class _Cls(modal.object._Object):
|
|
138
139
|
class Cls(modal.object.Object):
|
139
140
|
_user_cls: typing.Optional[type]
|
140
141
|
_class_service_function: typing.Optional[modal.functions.Function]
|
141
|
-
_method_functions: typing.Optional[
|
142
|
+
_method_functions: typing.Optional[dict[str, modal.functions.Function]]
|
142
143
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
143
|
-
_callables:
|
144
|
+
_callables: dict[str, typing.Callable[..., typing.Any]]
|
144
145
|
_from_other_workspace: typing.Optional[bool]
|
145
146
|
_app: typing.Optional[modal.app.App]
|
146
147
|
|
147
148
|
def __init__(self, *args, **kwargs): ...
|
148
149
|
def _initialize_from_empty(self): ...
|
149
150
|
def _initialize_from_other(self, other: Cls): ...
|
150
|
-
def _get_partial_functions(self) ->
|
151
|
+
def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
|
151
152
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
152
153
|
@staticmethod
|
153
154
|
def validate_construction_mechanism(user_cls): ...
|
@@ -156,7 +157,7 @@ class Cls(modal.object.Object):
|
|
156
157
|
def _uses_common_service_function(self): ...
|
157
158
|
@classmethod
|
158
159
|
def from_name(
|
159
|
-
cls:
|
160
|
+
cls: type[Cls],
|
160
161
|
app_name: str,
|
161
162
|
tag: str,
|
162
163
|
namespace=1,
|
@@ -165,11 +166,11 @@ class Cls(modal.object.Object):
|
|
165
166
|
) -> Cls: ...
|
166
167
|
def with_options(
|
167
168
|
self: Cls,
|
168
|
-
cpu: typing.Union[float,
|
169
|
-
memory: typing.Union[int,
|
169
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
170
|
+
memory: typing.Union[int, tuple[int, int], None] = None,
|
170
171
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
171
|
-
secrets:
|
172
|
-
volumes:
|
172
|
+
secrets: collections.abc.Collection[modal.secret.Secret] = (),
|
173
|
+
volumes: dict[typing.Union[str, os.PathLike], modal.volume.Volume] = {},
|
173
174
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
174
175
|
timeout: typing.Optional[int] = None,
|
175
176
|
concurrency_limit: typing.Optional[int] = None,
|
modal/config.py
CHANGED
@@ -80,7 +80,7 @@ import os
|
|
80
80
|
import typing
|
81
81
|
import warnings
|
82
82
|
from textwrap import dedent
|
83
|
-
from typing import Any,
|
83
|
+
from typing import Any, Optional
|
84
84
|
|
85
85
|
from google.protobuf.empty_pb2 import Empty
|
86
86
|
|
@@ -221,6 +221,7 @@ _SETTINGS = {
|
|
221
221
|
"image_builder_version": _Setting(),
|
222
222
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
223
223
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
224
|
+
"client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
|
224
225
|
}
|
225
226
|
|
226
227
|
|
@@ -282,7 +283,7 @@ configure_logger(logger, config["loglevel"], config["log_format"])
|
|
282
283
|
|
283
284
|
|
284
285
|
def _store_user_config(
|
285
|
-
new_settings:
|
286
|
+
new_settings: dict[str, Any], profile: Optional[str] = None, active_profile: Optional[str] = None
|
286
287
|
):
|
287
288
|
"""Internal method, used by the CLI to set tokens."""
|
288
289
|
if profile is None:
|
modal/dict.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
from
|
2
|
+
from collections.abc import AsyncIterator
|
3
|
+
from typing import Any, Optional
|
3
4
|
|
4
5
|
from grpclib import GRPCError
|
5
6
|
from synchronicity.async_wrap import asynccontextmanager
|
@@ -74,7 +75,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
74
75
|
@classmethod
|
75
76
|
@asynccontextmanager
|
76
77
|
async def ephemeral(
|
77
|
-
cls:
|
78
|
+
cls: type["_Dict"],
|
78
79
|
data: Optional[dict] = None,
|
79
80
|
client: Optional[_Client] = None,
|
80
81
|
environment_name: Optional[str] = None,
|
@@ -316,7 +317,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
316
317
|
yield deserialize(resp.value, self._client)
|
317
318
|
|
318
319
|
@live_method_gen
|
319
|
-
async def items(self) -> AsyncIterator[
|
320
|
+
async def items(self) -> AsyncIterator[tuple[Any, Any]]:
|
320
321
|
"""Return an iterator over the (key, value) tuples in this dictionary.
|
321
322
|
|
322
323
|
Note that (unlike with Python dicts) the return value is a simple iterator,
|
modal/dict.pyi
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import collections.abc
|
1
2
|
import modal.client
|
2
3
|
import modal.object
|
3
4
|
import synchronicity.combined_types
|
@@ -12,7 +13,7 @@ class _Dict(modal.object._Object):
|
|
12
13
|
def __init__(self, data={}): ...
|
13
14
|
@classmethod
|
14
15
|
def ephemeral(
|
15
|
-
cls:
|
16
|
+
cls: type[_Dict],
|
16
17
|
data: typing.Optional[dict] = None,
|
17
18
|
client: typing.Optional[modal.client._Client] = None,
|
18
19
|
environment_name: typing.Optional[str] = None,
|
@@ -53,9 +54,9 @@ class _Dict(modal.object._Object):
|
|
53
54
|
async def pop(self, key: typing.Any) -> typing.Any: ...
|
54
55
|
async def __delitem__(self, key: typing.Any) -> typing.Any: ...
|
55
56
|
async def __contains__(self, key: typing.Any) -> bool: ...
|
56
|
-
def keys(self) ->
|
57
|
-
def values(self) ->
|
58
|
-
def items(self) ->
|
57
|
+
def keys(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
58
|
+
def values(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
59
|
+
def items(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
|
59
60
|
|
60
61
|
class Dict(modal.object.Object):
|
61
62
|
def __init__(self, data={}): ...
|
@@ -63,7 +64,7 @@ class Dict(modal.object.Object):
|
|
63
64
|
def new(data: typing.Optional[dict] = None): ...
|
64
65
|
@classmethod
|
65
66
|
def ephemeral(
|
66
|
-
cls:
|
67
|
+
cls: type[Dict],
|
67
68
|
data: typing.Optional[dict] = None,
|
68
69
|
client: typing.Optional[modal.client.Client] = None,
|
69
70
|
environment_name: typing.Optional[str] = None,
|
@@ -186,18 +187,18 @@ class Dict(modal.object.Object):
|
|
186
187
|
|
187
188
|
class __keys_spec(typing_extensions.Protocol):
|
188
189
|
def __call__(self) -> typing.Iterator[typing.Any]: ...
|
189
|
-
def aio(self) ->
|
190
|
+
def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
190
191
|
|
191
192
|
keys: __keys_spec
|
192
193
|
|
193
194
|
class __values_spec(typing_extensions.Protocol):
|
194
195
|
def __call__(self) -> typing.Iterator[typing.Any]: ...
|
195
|
-
def aio(self) ->
|
196
|
+
def aio(self) -> collections.abc.AsyncIterator[typing.Any]: ...
|
196
197
|
|
197
198
|
values: __values_spec
|
198
199
|
|
199
200
|
class __items_spec(typing_extensions.Protocol):
|
200
|
-
def __call__(self) -> typing.Iterator[
|
201
|
-
def aio(self) ->
|
201
|
+
def __call__(self) -> typing.Iterator[tuple[typing.Any, typing.Any]]: ...
|
202
|
+
def aio(self) -> collections.abc.AsyncIterator[tuple[typing.Any, typing.Any]]: ...
|
202
203
|
|
203
204
|
items: __items_spec
|
modal/environments.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import
|
3
|
+
from typing import Optional
|
4
4
|
|
5
5
|
from google.protobuf.empty_pb2 import Empty
|
6
6
|
from google.protobuf.message import Message
|
@@ -98,7 +98,7 @@ Environment = synchronize_api(_Environment)
|
|
98
98
|
|
99
99
|
|
100
100
|
# Needs to be after definition; synchronicity interferes with forward references?
|
101
|
-
ENVIRONMENT_CACHE:
|
101
|
+
ENVIRONMENT_CACHE: dict[str, _Environment] = {}
|
102
102
|
|
103
103
|
|
104
104
|
async def _get_environment_cached(name: str, client: _Client) -> _Environment:
|
@@ -151,7 +151,7 @@ async def create_environment(name: str, client: Optional[_Client] = None):
|
|
151
151
|
|
152
152
|
|
153
153
|
@synchronizer.create_blocking
|
154
|
-
async def list_environments(client: Optional[_Client] = None) ->
|
154
|
+
async def list_environments(client: Optional[_Client] = None) -> list[api_pb2.EnvironmentListItem]:
|
155
155
|
if client is None:
|
156
156
|
client = await _Client.from_env()
|
157
157
|
resp = await client.stub.EnvironmentList(Empty())
|
modal/environments.pyi
CHANGED
@@ -87,13 +87,13 @@ create_environment: __create_environment_spec
|
|
87
87
|
class __list_environments_spec(typing_extensions.Protocol):
|
88
88
|
def __call__(
|
89
89
|
self, client: typing.Optional[modal.client.Client] = None
|
90
|
-
) ->
|
90
|
+
) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
|
91
91
|
async def aio(
|
92
92
|
self, client: typing.Optional[modal.client.Client] = None
|
93
|
-
) ->
|
93
|
+
) -> list[modal_proto.api_pb2.EnvironmentListItem]: ...
|
94
94
|
|
95
95
|
list_environments: __list_environments_spec
|
96
96
|
|
97
97
|
def ensure_env(environment_name: typing.Optional[str] = None) -> str: ...
|
98
98
|
|
99
|
-
ENVIRONMENT_CACHE:
|
99
|
+
ENVIRONMENT_CACHE: dict[str, _Environment]
|
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,
|
@@ -429,14 +494,14 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
429
494
|
secrets: Sequence[_Secret] = (),
|
430
495
|
schedule: Optional[Schedule] = None,
|
431
496
|
is_generator=False,
|
432
|
-
gpu: Union[GPU_T,
|
497
|
+
gpu: Union[GPU_T, list[GPU_T]] = None,
|
433
498
|
# TODO: maybe break this out into a separate decorator for notebooks.
|
434
499
|
mounts: Collection[_Mount] = (),
|
435
|
-
network_file_systems:
|
500
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
436
501
|
allow_cross_region_volumes: bool = False,
|
437
|
-
volumes:
|
502
|
+
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]] = {},
|
438
503
|
webhook_config: Optional[api_pb2.WebhookConfig] = None,
|
439
|
-
memory: Optional[Union[int,
|
504
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
440
505
|
proxy: Optional[_Proxy] = None,
|
441
506
|
retries: Optional[Union[int, Retries]] = None,
|
442
507
|
timeout: Optional[int] = None,
|
@@ -623,8 +688,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
623
688
|
if image is not None and not isinstance(image, _Image):
|
624
689
|
raise InvalidError(f"Expected modal.Image object. Got {type(image)}.")
|
625
690
|
|
626
|
-
method_definitions: Optional[
|
627
|
-
partial_functions:
|
691
|
+
method_definitions: Optional[dict[str, api_pb2.MethodDefinition]] = None
|
692
|
+
partial_functions: dict[str, "modal.partial_function._PartialFunction"] = {}
|
628
693
|
if info.user_cls:
|
629
694
|
method_definitions = {}
|
630
695
|
partial_functions = _find_partial_methods_for_user_cls(info.user_cls, _PartialFunctionFlags.FUNCTION)
|
@@ -640,8 +705,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
640
705
|
|
641
706
|
function_type = get_function_type(is_generator)
|
642
707
|
|
643
|
-
def _deps(only_explicit_mounts=False) ->
|
644
|
-
deps:
|
708
|
+
def _deps(only_explicit_mounts=False) -> list[_Object]:
|
709
|
+
deps: list[_Object] = list(secrets)
|
645
710
|
if only_explicit_mounts:
|
646
711
|
# TODO: this is a bit hacky, but all_mounts may differ in the container vs locally
|
647
712
|
# We don't want the function dependencies to change, so we have this way to force it to
|
@@ -878,7 +943,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
878
943
|
raise InvalidError(f"Function {info.function_name} is too large to deploy.")
|
879
944
|
raise
|
880
945
|
function_creation_status.set_response(response)
|
881
|
-
serve_mounts =
|
946
|
+
serve_mounts = {m for m in all_mounts if m.is_local()} # needed for modal.serve file watching
|
882
947
|
serve_mounts |= image._serve_mounts
|
883
948
|
obj._serve_mounts = frozenset(serve_mounts)
|
884
949
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
@@ -897,7 +962,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
897
962
|
obj._spec = function_spec # needed for modal shell
|
898
963
|
|
899
964
|
# Used to check whether we should rebuild a modal.Image which uses `run_function`.
|
900
|
-
gpus:
|
965
|
+
gpus: list[GPU_T] = gpu if isinstance(gpu, list) else [gpu]
|
901
966
|
obj._build_args = dict( # See get_build_def
|
902
967
|
secrets=repr(secrets),
|
903
968
|
gpu_config=repr([parse_gpu_config(_gpu) for _gpu in gpus]),
|
@@ -919,7 +984,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
919
984
|
from_other_workspace: bool,
|
920
985
|
options: Optional[api_pb2.FunctionOptions],
|
921
986
|
args: Sized,
|
922
|
-
kwargs:
|
987
|
+
kwargs: dict[str, Any],
|
923
988
|
) -> "_Function":
|
924
989
|
"""mdmd:hidden
|
925
990
|
|
@@ -1025,7 +1090,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1025
1090
|
|
1026
1091
|
@classmethod
|
1027
1092
|
def from_name(
|
1028
|
-
cls:
|
1093
|
+
cls: type["_Function"],
|
1029
1094
|
app_name: str,
|
1030
1095
|
tag: str,
|
1031
1096
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
@@ -1232,13 +1297,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1232
1297
|
yield item
|
1233
1298
|
|
1234
1299
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
1300
|
+
if config.get("client_retries"):
|
1301
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
1302
|
+
else:
|
1303
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
|
1235
1304
|
invocation = await _Invocation.create(
|
1236
1305
|
self,
|
1237
1306
|
args,
|
1238
1307
|
kwargs,
|
1239
1308
|
client=self._client,
|
1240
|
-
function_call_invocation_type=
|
1309
|
+
function_call_invocation_type=function_call_invocation_type,
|
1241
1310
|
)
|
1311
|
+
|
1242
1312
|
return await invocation.run_function()
|
1243
1313
|
|
1244
1314
|
async def _call_function_nowait(
|
@@ -1355,12 +1425,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1355
1425
|
if is_async(info.raw_f):
|
1356
1426
|
# We want to run __aenter__ and fun in the same coroutine
|
1357
1427
|
async def coro():
|
1358
|
-
await obj.
|
1428
|
+
await obj._aenter()
|
1359
1429
|
return await fun(*args, **kwargs)
|
1360
1430
|
|
1361
1431
|
return coro() # type: ignore
|
1362
1432
|
else:
|
1363
|
-
obj.
|
1433
|
+
obj._enter()
|
1364
1434
|
return fun(*args, **kwargs)
|
1365
1435
|
|
1366
1436
|
@synchronizer.no_input_translation
|
@@ -1476,7 +1546,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
1476
1546
|
async for res in self._invocation().run_generator():
|
1477
1547
|
yield res
|
1478
1548
|
|
1479
|
-
async def get_call_graph(self) ->
|
1549
|
+
async def get_call_graph(self) -> list[InputInfo]:
|
1480
1550
|
"""Returns a structure representing the call graph from a given root
|
1481
1551
|
call ID, along with the status of execution for each node.
|
1482
1552
|
|