modal 0.72.5__py3-none-any.whl → 0.72.48__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/_container_entrypoint.py +5 -10
- modal/_object.py +297 -0
- modal/_resolver.py +7 -5
- modal/_runtime/container_io_manager.py +0 -11
- modal/_runtime/user_code_imports.py +7 -7
- modal/_serialization.py +4 -3
- modal/_tunnel.py +1 -1
- modal/app.py +14 -61
- modal/app.pyi +25 -25
- modal/cli/app.py +3 -2
- modal/cli/container.py +1 -1
- modal/cli/import_refs.py +185 -113
- modal/cli/launch.py +10 -5
- modal/cli/programs/run_jupyter.py +2 -2
- modal/cli/programs/vscode.py +3 -3
- modal/cli/run.py +134 -68
- modal/client.py +1 -0
- modal/client.pyi +18 -14
- modal/cloud_bucket_mount.py +4 -0
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +33 -5
- modal/cls.pyi +20 -5
- modal/container_process.pyi +8 -6
- modal/dict.py +1 -1
- modal/dict.pyi +32 -29
- modal/environments.py +1 -1
- modal/environments.pyi +2 -1
- modal/experimental.py +47 -11
- modal/experimental.pyi +29 -0
- modal/file_io.pyi +30 -28
- modal/file_pattern_matcher.py +3 -4
- modal/functions.py +31 -23
- modal/functions.pyi +57 -50
- modal/gpu.py +19 -26
- modal/image.py +47 -19
- modal/image.pyi +28 -21
- modal/io_streams.pyi +14 -12
- modal/mount.py +14 -5
- modal/mount.pyi +28 -25
- modal/network_file_system.py +7 -7
- modal/network_file_system.pyi +27 -24
- modal/object.py +2 -265
- modal/object.pyi +46 -130
- modal/parallel_map.py +2 -2
- modal/parallel_map.pyi +10 -7
- modal/partial_function.py +22 -3
- modal/partial_function.pyi +45 -27
- modal/proxy.py +1 -1
- modal/proxy.pyi +2 -1
- modal/queue.py +1 -1
- modal/queue.pyi +26 -23
- modal/runner.py +14 -3
- modal/sandbox.py +11 -7
- modal/sandbox.pyi +30 -27
- modal/secret.py +1 -1
- modal/secret.pyi +2 -1
- modal/token_flow.pyi +6 -4
- modal/volume.py +1 -1
- modal/volume.pyi +36 -33
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/METADATA +2 -2
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/RECORD +73 -71
- modal_proto/api.proto +151 -4
- modal_proto/api_grpc.py +113 -0
- modal_proto/api_pb2.py +998 -795
- modal_proto/api_pb2.pyi +430 -11
- modal_proto/api_pb2_grpc.py +233 -1
- modal_proto/api_pb2_grpc.pyi +75 -3
- modal_proto/modal_api_grpc.py +7 -0
- modal_version/_version_generated.py +1 -1
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/LICENSE +0 -0
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/WHEEL +0 -0
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/entry_points.txt +0 -0
- {modal-0.72.5.dist-info → modal-0.72.48.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -38,7 +38,7 @@ from modal.partial_function import (
|
|
38
38
|
_find_callables_for_obj,
|
39
39
|
_PartialFunctionFlags,
|
40
40
|
)
|
41
|
-
from modal.running_app import RunningApp
|
41
|
+
from modal.running_app import RunningApp, running_app_from_layout
|
42
42
|
from modal_proto import api_pb2
|
43
43
|
|
44
44
|
from ._runtime.container_io_manager import (
|
@@ -50,8 +50,8 @@ from ._runtime.container_io_manager import (
|
|
50
50
|
from ._runtime.execution_context import _set_current_context_ids
|
51
51
|
|
52
52
|
if TYPE_CHECKING:
|
53
|
+
import modal._object
|
53
54
|
import modal._runtime.container_io_manager
|
54
|
-
import modal.object
|
55
55
|
|
56
56
|
|
57
57
|
class DaemonizedThreadPool:
|
@@ -468,7 +468,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
468
468
|
batch_wait_ms = function_def.batch_linger_ms or 0
|
469
469
|
|
470
470
|
# Get ids and metadata for objects (primarily functions and classes) on the app
|
471
|
-
container_app: RunningApp =
|
471
|
+
container_app: RunningApp = running_app_from_layout(container_args.app_id, container_args.app_layout)
|
472
472
|
|
473
473
|
# Initialize objects on the app.
|
474
474
|
# This is basically only functions and classes - anything else is deprecated and will be unsupported soon
|
@@ -591,15 +591,10 @@ if __name__ == "__main__":
|
|
591
591
|
logger.debug("Container: starting")
|
592
592
|
|
593
593
|
container_args = api_pb2.ContainerArguments()
|
594
|
-
|
595
594
|
container_arguments_path: Optional[str] = os.environ.get("MODAL_CONTAINER_ARGUMENTS_PATH")
|
596
595
|
if container_arguments_path is None:
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
container_args.ParseFromString(base64.b64decode(sys.argv[1]))
|
601
|
-
else:
|
602
|
-
container_args.ParseFromString(open(container_arguments_path, "rb").read())
|
596
|
+
raise RuntimeError("No path to the container arguments file provided!")
|
597
|
+
container_args.ParseFromString(open(container_arguments_path, "rb").read())
|
603
598
|
|
604
599
|
# Note that we're creating the client in a synchronous context, but it will be running in a separate thread.
|
605
600
|
# This is good because if the function is long running then we the client can still send heartbeats
|
modal/_object.py
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
# Copyright Modal Labs 2022
|
2
|
+
import typing
|
3
|
+
import uuid
|
4
|
+
from collections.abc import Awaitable, Hashable, Sequence
|
5
|
+
from functools import wraps
|
6
|
+
from typing import Callable, ClassVar, Optional
|
7
|
+
|
8
|
+
from google.protobuf.message import Message
|
9
|
+
from typing_extensions import Self
|
10
|
+
|
11
|
+
from ._resolver import Resolver
|
12
|
+
from ._utils.async_utils import aclosing
|
13
|
+
from ._utils.deprecation import deprecation_warning
|
14
|
+
from .client import _Client
|
15
|
+
from .config import config, logger
|
16
|
+
from .exception import ExecutionError, InvalidError
|
17
|
+
|
18
|
+
EPHEMERAL_OBJECT_HEARTBEAT_SLEEP: int = 300
|
19
|
+
|
20
|
+
|
21
|
+
def _get_environment_name(environment_name: Optional[str] = None, resolver: Optional[Resolver] = None) -> Optional[str]:
|
22
|
+
if environment_name:
|
23
|
+
return environment_name
|
24
|
+
elif resolver and resolver.environment_name:
|
25
|
+
return resolver.environment_name
|
26
|
+
else:
|
27
|
+
return config.get("environment")
|
28
|
+
|
29
|
+
|
30
|
+
class _Object:
|
31
|
+
_type_prefix: ClassVar[Optional[str]] = None
|
32
|
+
_prefix_to_type: ClassVar[dict[str, type]] = {}
|
33
|
+
|
34
|
+
# For constructors
|
35
|
+
_load: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]]
|
36
|
+
_preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]]
|
37
|
+
_rep: str
|
38
|
+
_is_another_app: bool
|
39
|
+
_hydrate_lazily: bool
|
40
|
+
_deps: Optional[Callable[..., Sequence["_Object"]]]
|
41
|
+
_deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None
|
42
|
+
|
43
|
+
# For hydrated objects
|
44
|
+
_object_id: Optional[str]
|
45
|
+
_client: Optional[_Client]
|
46
|
+
_is_hydrated: bool
|
47
|
+
_is_rehydrated: bool
|
48
|
+
|
49
|
+
@classmethod
|
50
|
+
def __init_subclass__(cls, type_prefix: Optional[str] = None):
|
51
|
+
super().__init_subclass__()
|
52
|
+
if type_prefix is not None:
|
53
|
+
cls._type_prefix = type_prefix
|
54
|
+
cls._prefix_to_type[type_prefix] = cls
|
55
|
+
|
56
|
+
def __init__(self, *args, **kwargs):
|
57
|
+
raise InvalidError(f"Class {type(self).__name__} has no constructor. Use class constructor methods instead.")
|
58
|
+
|
59
|
+
def _init(
|
60
|
+
self,
|
61
|
+
rep: str,
|
62
|
+
load: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
|
63
|
+
is_another_app: bool = False,
|
64
|
+
preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
|
65
|
+
hydrate_lazily: bool = False,
|
66
|
+
deps: Optional[Callable[..., Sequence["_Object"]]] = None,
|
67
|
+
deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
|
68
|
+
):
|
69
|
+
self._local_uuid = str(uuid.uuid4())
|
70
|
+
self._load = load
|
71
|
+
self._preload = preload
|
72
|
+
self._rep = rep
|
73
|
+
self._is_another_app = is_another_app
|
74
|
+
self._hydrate_lazily = hydrate_lazily
|
75
|
+
self._deps = deps
|
76
|
+
self._deduplication_key = deduplication_key
|
77
|
+
|
78
|
+
self._object_id = None
|
79
|
+
self._client = None
|
80
|
+
self._is_hydrated = False
|
81
|
+
self._is_rehydrated = False
|
82
|
+
|
83
|
+
self._initialize_from_empty()
|
84
|
+
|
85
|
+
def _unhydrate(self):
|
86
|
+
self._object_id = None
|
87
|
+
self._client = None
|
88
|
+
self._is_hydrated = False
|
89
|
+
|
90
|
+
def _initialize_from_empty(self):
|
91
|
+
# default implementation, can be overriden in subclasses
|
92
|
+
pass
|
93
|
+
|
94
|
+
def _initialize_from_other(self, other):
|
95
|
+
# default implementation, can be overriden in subclasses
|
96
|
+
self._object_id = other._object_id
|
97
|
+
self._is_hydrated = other._is_hydrated
|
98
|
+
self._client = other._client
|
99
|
+
|
100
|
+
def _hydrate(self, object_id: str, client: _Client, metadata: Optional[Message]):
|
101
|
+
assert isinstance(object_id, str) and self._type_prefix is not None
|
102
|
+
if not object_id.startswith(self._type_prefix):
|
103
|
+
raise ExecutionError(
|
104
|
+
f"Can not hydrate {type(self)}:"
|
105
|
+
f" it has type prefix {self._type_prefix}"
|
106
|
+
f" but the object_id starts with {object_id[:3]}"
|
107
|
+
)
|
108
|
+
self._object_id = object_id
|
109
|
+
self._client = client
|
110
|
+
self._hydrate_metadata(metadata)
|
111
|
+
self._is_hydrated = True
|
112
|
+
|
113
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
114
|
+
# override this is subclasses that need additional data (other than an object_id) for a functioning Handle
|
115
|
+
pass
|
116
|
+
|
117
|
+
def _get_metadata(self) -> Optional[Message]:
|
118
|
+
# return the necessary metadata from this handle to be able to re-hydrate in another context if one is needed
|
119
|
+
# used to provide a handle's handle_metadata for serializing/pickling a live handle
|
120
|
+
# the object_id is already provided by other means
|
121
|
+
return None
|
122
|
+
|
123
|
+
def _validate_is_hydrated(self):
|
124
|
+
if not self._is_hydrated:
|
125
|
+
object_type = self.__class__.__name__.strip("_")
|
126
|
+
if hasattr(self, "_app") and getattr(self._app, "_running_app", "") is None: # type: ignore
|
127
|
+
# The most common cause of this error: e.g., user called a Function without using App.run()
|
128
|
+
reason = ", because the App it is defined on is not running"
|
129
|
+
else:
|
130
|
+
# Technically possible, but with an ambiguous cause.
|
131
|
+
reason = ""
|
132
|
+
raise ExecutionError(
|
133
|
+
f"{object_type} has not been hydrated with the metadata it needs to run on Modal{reason}."
|
134
|
+
)
|
135
|
+
|
136
|
+
def clone(self) -> Self:
|
137
|
+
"""mdmd:hidden Clone a given hydrated object."""
|
138
|
+
|
139
|
+
# Object to clone must already be hydrated, otherwise from_loader is more suitable.
|
140
|
+
self._validate_is_hydrated()
|
141
|
+
obj = type(self).__new__(type(self))
|
142
|
+
obj._initialize_from_other(self)
|
143
|
+
return obj
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def _from_loader(
|
147
|
+
cls,
|
148
|
+
load: Callable[[Self, Resolver, Optional[str]], Awaitable[None]],
|
149
|
+
rep: str,
|
150
|
+
is_another_app: bool = False,
|
151
|
+
preload: Optional[Callable[[Self, Resolver, Optional[str]], Awaitable[None]]] = None,
|
152
|
+
hydrate_lazily: bool = False,
|
153
|
+
deps: Optional[Callable[..., Sequence["_Object"]]] = None,
|
154
|
+
deduplication_key: Optional[Callable[[], Awaitable[Hashable]]] = None,
|
155
|
+
):
|
156
|
+
# TODO(erikbern): flip the order of the two first arguments
|
157
|
+
obj = _Object.__new__(cls)
|
158
|
+
obj._init(rep, load, is_another_app, preload, hydrate_lazily, deps, deduplication_key)
|
159
|
+
return obj
|
160
|
+
|
161
|
+
@staticmethod
|
162
|
+
def _get_type_from_id(object_id: str) -> type["_Object"]:
|
163
|
+
parts = object_id.split("-")
|
164
|
+
if len(parts) != 2:
|
165
|
+
raise InvalidError(f"Object id {object_id} has no dash in it")
|
166
|
+
prefix = parts[0]
|
167
|
+
if prefix not in _Object._prefix_to_type:
|
168
|
+
raise InvalidError(f"Object prefix {prefix} does not correspond to a type")
|
169
|
+
return _Object._prefix_to_type[prefix]
|
170
|
+
|
171
|
+
@classmethod
|
172
|
+
def _is_id_type(cls, object_id) -> bool:
|
173
|
+
return cls._get_type_from_id(object_id) == cls
|
174
|
+
|
175
|
+
@classmethod
|
176
|
+
def _new_hydrated(
|
177
|
+
cls, object_id: str, client: _Client, handle_metadata: Optional[Message], is_another_app: bool = False
|
178
|
+
) -> Self:
|
179
|
+
obj_cls: type[Self]
|
180
|
+
if cls._type_prefix is not None:
|
181
|
+
# This is called directly on a subclass, e.g. Secret.from_id
|
182
|
+
# validate the id matching the expected id type of the Object subclass
|
183
|
+
if not object_id.startswith(cls._type_prefix + "-"):
|
184
|
+
raise InvalidError(f"Object {object_id} does not start with {cls._type_prefix}")
|
185
|
+
|
186
|
+
obj_cls = cls
|
187
|
+
else:
|
188
|
+
# this means the method is used directly on _Object
|
189
|
+
# typically during deserialization of objects
|
190
|
+
obj_cls = typing.cast(type[Self], cls._get_type_from_id(object_id))
|
191
|
+
|
192
|
+
# Instantiate provider
|
193
|
+
obj = _Object.__new__(obj_cls)
|
194
|
+
rep = f"Object({object_id})" # TODO(erikbern): dumb
|
195
|
+
obj._init(rep, is_another_app=is_another_app)
|
196
|
+
obj._hydrate(object_id, client, handle_metadata)
|
197
|
+
|
198
|
+
return obj
|
199
|
+
|
200
|
+
def _hydrate_from_other(self, other: Self):
|
201
|
+
self._hydrate(other.object_id, other.client, other._get_metadata())
|
202
|
+
|
203
|
+
def __repr__(self):
|
204
|
+
return self._rep
|
205
|
+
|
206
|
+
@property
|
207
|
+
def local_uuid(self):
|
208
|
+
"""mdmd:hidden"""
|
209
|
+
return self._local_uuid
|
210
|
+
|
211
|
+
@property
|
212
|
+
def object_id(self) -> str:
|
213
|
+
"""mdmd:hidden"""
|
214
|
+
if self._object_id is None:
|
215
|
+
raise AttributeError(f"Attempting to get object_id of unhydrated {self}")
|
216
|
+
return self._object_id
|
217
|
+
|
218
|
+
@property
|
219
|
+
def client(self) -> _Client:
|
220
|
+
"""mdmd:hidden"""
|
221
|
+
if self._client is None:
|
222
|
+
raise AttributeError(f"Attempting to get client of unhydrated {self}")
|
223
|
+
return self._client
|
224
|
+
|
225
|
+
@property
|
226
|
+
def is_hydrated(self) -> bool:
|
227
|
+
"""mdmd:hidden"""
|
228
|
+
return self._is_hydrated
|
229
|
+
|
230
|
+
@property
|
231
|
+
def deps(self) -> Callable[..., Sequence["_Object"]]:
|
232
|
+
"""mdmd:hidden"""
|
233
|
+
|
234
|
+
def default_deps(*args, **kwargs) -> Sequence["_Object"]:
|
235
|
+
return []
|
236
|
+
|
237
|
+
return self._deps if self._deps is not None else default_deps
|
238
|
+
|
239
|
+
async def resolve(self, client: Optional[_Client] = None):
|
240
|
+
"""mdmd:hidden"""
|
241
|
+
obj = self.__class__.__name__.strip("_")
|
242
|
+
deprecation_warning(
|
243
|
+
(2025, 1, 16),
|
244
|
+
f"The `{obj}.resolve` method is deprecated and will be removed in a future release."
|
245
|
+
f" Please use `{obj}.hydrate()` or `await {obj}.hydrate.aio()` instead."
|
246
|
+
"\n\nNote that it is rarely necessary to explicitly hydrate objects, as most methods"
|
247
|
+
" will lazily hydrate when needed.",
|
248
|
+
show_source=False, # synchronicity interferes with attributing source correctly
|
249
|
+
)
|
250
|
+
await self.hydrate(client)
|
251
|
+
|
252
|
+
async def hydrate(self, client: Optional[_Client] = None) -> Self:
|
253
|
+
"""Synchronize the local object with its identity on the Modal server.
|
254
|
+
|
255
|
+
It is rarely necessary to call this method explicitly, as most operations
|
256
|
+
will lazily hydrate when needed. The main use case is when you need to
|
257
|
+
access object metadata, such as its ID.
|
258
|
+
"""
|
259
|
+
if self._is_hydrated:
|
260
|
+
if self.client._snapshotted and not self._is_rehydrated:
|
261
|
+
# memory snapshots capture references which must be rehydrated
|
262
|
+
# on restore to handle staleness.
|
263
|
+
logger.debug(f"rehydrating {self} after snapshot")
|
264
|
+
self._is_hydrated = False # un-hydrate and re-resolve
|
265
|
+
c = client if client is not None else await _Client.from_env()
|
266
|
+
resolver = Resolver(c)
|
267
|
+
await resolver.load(typing.cast(_Object, self))
|
268
|
+
self._is_rehydrated = True
|
269
|
+
logger.debug(f"rehydrated {self} with client {id(c)}")
|
270
|
+
elif not self._hydrate_lazily:
|
271
|
+
# TODO(michael) can remove _hydrate lazily? I think all objects support it now?
|
272
|
+
self._validate_is_hydrated()
|
273
|
+
else:
|
274
|
+
c = client if client is not None else await _Client.from_env()
|
275
|
+
resolver = Resolver(c)
|
276
|
+
await resolver.load(self)
|
277
|
+
return self
|
278
|
+
|
279
|
+
|
280
|
+
def live_method(method):
|
281
|
+
@wraps(method)
|
282
|
+
async def wrapped(self, *args, **kwargs):
|
283
|
+
await self.hydrate()
|
284
|
+
return await method(self, *args, **kwargs)
|
285
|
+
|
286
|
+
return wrapped
|
287
|
+
|
288
|
+
|
289
|
+
def live_method_gen(method):
|
290
|
+
@wraps(method)
|
291
|
+
async def wrapped(self, *args, **kwargs):
|
292
|
+
await self.hydrate()
|
293
|
+
async with aclosing(method(self, *args, **kwargs)) as stream:
|
294
|
+
async for item in stream:
|
295
|
+
yield item
|
296
|
+
|
297
|
+
return wrapped
|
modal/_resolver.py
CHANGED
@@ -15,7 +15,7 @@ from .exception import NotFoundError
|
|
15
15
|
if TYPE_CHECKING:
|
16
16
|
from rich.tree import Tree
|
17
17
|
|
18
|
-
|
18
|
+
import modal._object
|
19
19
|
|
20
20
|
|
21
21
|
class StatusRow:
|
@@ -33,7 +33,7 @@ class StatusRow:
|
|
33
33
|
self._spinner.update(text=message)
|
34
34
|
|
35
35
|
def finish(self, message):
|
36
|
-
if self._step_node is not None:
|
36
|
+
if self._step_node is not None and self._spinner is not None:
|
37
37
|
from ._output import OutputManager
|
38
38
|
|
39
39
|
self._spinner.update(text=message)
|
@@ -89,7 +89,7 @@ class Resolver:
|
|
89
89
|
if obj._preload is not None:
|
90
90
|
await obj._preload(obj, self, existing_object_id)
|
91
91
|
|
92
|
-
async def load(self, obj: "_Object", existing_object_id: Optional[str] = None):
|
92
|
+
async def load(self, obj: "modal._object._Object", existing_object_id: Optional[str] = None):
|
93
93
|
if obj._is_hydrated and obj._is_another_app:
|
94
94
|
# No need to reload this, it won't typically change
|
95
95
|
if obj.local_uuid not in self._local_uuid_to_future:
|
@@ -124,6 +124,8 @@ class Resolver:
|
|
124
124
|
await TaskContext.gather(*[self.load(dep) for dep in obj.deps()])
|
125
125
|
|
126
126
|
# Load the object itself
|
127
|
+
if not obj._load:
|
128
|
+
raise Exception(f"Object {obj} has no loader function")
|
127
129
|
try:
|
128
130
|
await obj._load(obj, self, existing_object_id)
|
129
131
|
except GRPCError as exc:
|
@@ -154,8 +156,8 @@ class Resolver:
|
|
154
156
|
# TODO(elias): print original exception/trace rather than the Resolver-internal trace
|
155
157
|
return await cached_future
|
156
158
|
|
157
|
-
def objects(self) -> list["_Object"]:
|
158
|
-
unique_objects: dict[str, "_Object"] = {}
|
159
|
+
def objects(self) -> list["modal._object._Object"]:
|
160
|
+
unique_objects: dict[str, "modal._object._Object"] = {}
|
159
161
|
for fut in self._local_uuid_to_future.values():
|
160
162
|
if not fut.done():
|
161
163
|
# this will raise an exception if not all loads have been awaited, but that *should* never happen
|
@@ -35,7 +35,6 @@ from modal._utils.package_utils import parse_major_minor_version
|
|
35
35
|
from modal.client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
36
36
|
from modal.config import config, logger
|
37
37
|
from modal.exception import ClientClosed, InputCancellation, InvalidError, SerializationError
|
38
|
-
from modal.running_app import RunningApp, running_app_from_layout
|
39
38
|
from modal_proto import api_pb2
|
40
39
|
|
41
40
|
if TYPE_CHECKING:
|
@@ -449,16 +448,6 @@ class _ContainerIOManager:
|
|
449
448
|
|
450
449
|
await asyncio.sleep(DYNAMIC_CONCURRENCY_INTERVAL_SECS)
|
451
450
|
|
452
|
-
async def get_app_objects(self, app_layout: api_pb2.AppLayout) -> RunningApp:
|
453
|
-
if len(app_layout.objects) == 0:
|
454
|
-
# TODO(erikbern): this should never happen! let's keep it in here for a short second
|
455
|
-
# until we've sanity checked that this is, in fact, dead code.
|
456
|
-
req = api_pb2.AppGetLayoutRequest(app_id=self.app_id)
|
457
|
-
resp = await retry_transient_errors(self._client.stub.AppGetLayout, req)
|
458
|
-
app_layout = resp.app_layout
|
459
|
-
|
460
|
-
return running_app_from_layout(self.app_id, app_layout)
|
461
|
-
|
462
451
|
async def get_serialized_function(self) -> tuple[Optional[Any], Optional[Callable[..., Any]]]:
|
463
452
|
# Fetch the serialized function definition
|
464
453
|
request = api_pb2.FunctionGetSerializedRequest(function_id=self.function_id)
|
@@ -3,11 +3,11 @@ import importlib
|
|
3
3
|
import typing
|
4
4
|
from abc import ABCMeta, abstractmethod
|
5
5
|
from dataclasses import dataclass
|
6
|
-
from typing import Any, Callable, Optional
|
6
|
+
from typing import Any, Callable, Optional, Sequence
|
7
7
|
|
8
|
+
import modal._object
|
8
9
|
import modal._runtime.container_io_manager
|
9
10
|
import modal.cls
|
10
|
-
import modal.object
|
11
11
|
from modal import Function
|
12
12
|
from modal._utils.async_utils import synchronizer
|
13
13
|
from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
|
@@ -41,7 +41,7 @@ class Service(metaclass=ABCMeta):
|
|
41
41
|
|
42
42
|
user_cls_instance: Any
|
43
43
|
app: Optional["modal.app._App"]
|
44
|
-
code_deps: Optional[
|
44
|
+
code_deps: Optional[Sequence["modal._object._Object"]]
|
45
45
|
|
46
46
|
@abstractmethod
|
47
47
|
def get_finalized_functions(
|
@@ -94,7 +94,7 @@ def construct_webhook_callable(
|
|
94
94
|
class ImportedFunction(Service):
|
95
95
|
user_cls_instance: Any
|
96
96
|
app: Optional["modal.app._App"]
|
97
|
-
code_deps: Optional[
|
97
|
+
code_deps: Optional[Sequence["modal._object._Object"]]
|
98
98
|
|
99
99
|
_user_defined_callable: Callable[..., Any]
|
100
100
|
|
@@ -137,7 +137,7 @@ class ImportedFunction(Service):
|
|
137
137
|
class ImportedClass(Service):
|
138
138
|
user_cls_instance: Any
|
139
139
|
app: Optional["modal.app._App"]
|
140
|
-
code_deps: Optional[
|
140
|
+
code_deps: Optional[Sequence["modal._object._Object"]]
|
141
141
|
|
142
142
|
_partial_functions: dict[str, "modal.partial_function._PartialFunction"]
|
143
143
|
|
@@ -229,7 +229,7 @@ def import_single_function_service(
|
|
229
229
|
"""
|
230
230
|
user_defined_callable: Callable
|
231
231
|
function: Optional[_Function] = None
|
232
|
-
code_deps: Optional[
|
232
|
+
code_deps: Optional[Sequence["modal._object._Object"]] = None
|
233
233
|
active_app: Optional[modal.app._App] = None
|
234
234
|
|
235
235
|
if ser_fun is not None:
|
@@ -306,7 +306,7 @@ def import_class_service(
|
|
306
306
|
See import_function.
|
307
307
|
"""
|
308
308
|
active_app: Optional["modal.app._App"]
|
309
|
-
code_deps: Optional[
|
309
|
+
code_deps: Optional[Sequence["modal._object._Object"]]
|
310
310
|
cls: typing.Union[type, modal.cls.Cls]
|
311
311
|
|
312
312
|
if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
|
modal/_serialization.py
CHANGED
@@ -8,10 +8,11 @@ from typing import Any
|
|
8
8
|
from modal._utils.async_utils import synchronizer
|
9
9
|
from modal_proto import api_pb2
|
10
10
|
|
11
|
+
from ._object import _Object
|
11
12
|
from ._vendor import cloudpickle
|
12
13
|
from .config import logger
|
13
14
|
from .exception import DeserializationError, ExecutionError, InvalidError
|
14
|
-
from .object import Object
|
15
|
+
from .object import Object
|
15
16
|
|
16
17
|
PICKLE_PROTOCOL = 4 # Support older Python versions.
|
17
18
|
|
@@ -48,8 +49,8 @@ class Pickler(cloudpickle.Pickler):
|
|
48
49
|
return ("sync", (impl_object.__class__, attributes))
|
49
50
|
else:
|
50
51
|
return
|
51
|
-
if not obj.
|
52
|
-
raise InvalidError(f"Can't serialize object {obj} which hasn't been
|
52
|
+
if not obj.is_hydrated:
|
53
|
+
raise InvalidError(f"Can't serialize object {obj} which hasn't been hydrated.")
|
53
54
|
return (obj.object_id, flag, obj._get_metadata())
|
54
55
|
|
55
56
|
|
modal/_tunnel.py
CHANGED
@@ -145,7 +145,7 @@ async def _forward(port: int, *, unencrypted: bool = False, client: Optional[_Cl
|
|
145
145
|
modal.Image.debian_slim()
|
146
146
|
.apt_install("openssh-server")
|
147
147
|
.run_commands("mkdir /run/sshd")
|
148
|
-
.
|
148
|
+
.add_local_file("~/.ssh/id_rsa.pub", "/root/.ssh/authorized_keys", copy=True)
|
149
149
|
)
|
150
150
|
|
151
151
|
|
modal/app.py
CHANGED
@@ -21,6 +21,7 @@ from synchronicity.async_wrap import asynccontextmanager
|
|
21
21
|
from modal_proto import api_pb2
|
22
22
|
|
23
23
|
from ._ipython import is_notebook
|
24
|
+
from ._object import _get_environment_name, _Object
|
24
25
|
from ._utils.async_utils import synchronize_api
|
25
26
|
from ._utils.deprecation import deprecation_error, deprecation_warning, renamed_parameter
|
26
27
|
from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
|
@@ -36,7 +37,6 @@ from .gpu import GPU_T
|
|
36
37
|
from .image import _Image
|
37
38
|
from .mount import _Mount
|
38
39
|
from .network_file_system import _NetworkFileSystem
|
39
|
-
from .object import _get_environment_name, _Object
|
40
40
|
from .partial_function import (
|
41
41
|
PartialFunction,
|
42
42
|
_find_partial_methods_for_user_cls,
|
@@ -118,23 +118,6 @@ class _FunctionDecoratorType:
|
|
118
118
|
...
|
119
119
|
|
120
120
|
|
121
|
-
_app_attr_error = """\
|
122
|
-
App assignments of the form `app.x` or `app["x"]` are deprecated!
|
123
|
-
|
124
|
-
The only use cases for these assignments is in conjunction with `.new()`, which is now
|
125
|
-
in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
|
126
|
-
need to assign those objects to the app. Example:
|
127
|
-
|
128
|
-
```python
|
129
|
-
d = modal.Dict.from_name("my-dict", create_if_missing=True)
|
130
|
-
|
131
|
-
@app.function()
|
132
|
-
def f(x, y):
|
133
|
-
d[x] = y # Refer to d in global scope
|
134
|
-
```
|
135
|
-
"""
|
136
|
-
|
137
|
-
|
138
121
|
class _App:
|
139
122
|
"""A Modal App is a group of functions and classes that are deployed together.
|
140
123
|
|
@@ -211,12 +194,12 @@ class _App:
|
|
211
194
|
self._name = name
|
212
195
|
self._description = name
|
213
196
|
|
214
|
-
check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of Mount objects")
|
215
|
-
check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of Secret objects")
|
197
|
+
check_sequence(mounts, _Mount, "`mounts=` has to be a list or tuple of `modal.Mount` objects")
|
198
|
+
check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
|
216
199
|
validate_volumes(volumes)
|
217
200
|
|
218
201
|
if image is not None and not isinstance(image, _Image):
|
219
|
-
raise InvalidError("image has to be a modal
|
202
|
+
raise InvalidError("`image=` has to be a `modal.Image` object")
|
220
203
|
|
221
204
|
self._functions = {}
|
222
205
|
self._classes = {}
|
@@ -303,34 +286,6 @@ class _App:
|
|
303
286
|
if not isinstance(value, _Object):
|
304
287
|
raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
|
305
288
|
|
306
|
-
def __getitem__(self, tag: str):
|
307
|
-
deprecation_error((2024, 3, 25), _app_attr_error)
|
308
|
-
|
309
|
-
def __setitem__(self, tag: str, obj: _Object):
|
310
|
-
deprecation_error((2024, 3, 25), _app_attr_error)
|
311
|
-
|
312
|
-
def __getattr__(self, tag: str):
|
313
|
-
# TODO(erikbern): remove this method later
|
314
|
-
assert isinstance(tag, str)
|
315
|
-
if tag.startswith("__"):
|
316
|
-
# Hacky way to avoid certain issues, e.g. pickle will try to look this up
|
317
|
-
raise AttributeError(f"App has no member {tag}")
|
318
|
-
if tag not in self._functions or tag not in self._classes:
|
319
|
-
# Primarily to make hasattr work
|
320
|
-
raise AttributeError(f"App has no member {tag}")
|
321
|
-
deprecation_error((2024, 3, 25), _app_attr_error)
|
322
|
-
|
323
|
-
def __setattr__(self, tag: str, obj: _Object):
|
324
|
-
# TODO(erikbern): remove this method later
|
325
|
-
# Note that only attributes defined in __annotations__ are set on the object itself,
|
326
|
-
# everything else is registered on the indexed_objects
|
327
|
-
if tag in self.__annotations__:
|
328
|
-
object.__setattr__(self, tag, obj)
|
329
|
-
elif tag == "image":
|
330
|
-
self._image = obj
|
331
|
-
else:
|
332
|
-
deprecation_error((2024, 3, 25), _app_attr_error)
|
333
|
-
|
334
289
|
@property
|
335
290
|
def image(self) -> _Image:
|
336
291
|
return self._image
|
@@ -365,6 +320,7 @@ class _App:
|
|
365
320
|
show_progress: Optional[bool] = None,
|
366
321
|
detach: bool = False,
|
367
322
|
interactive: bool = False,
|
323
|
+
environment_name: Optional[str] = None,
|
368
324
|
) -> AsyncGenerator["_App", None]:
|
369
325
|
"""Context manager that runs an app on Modal.
|
370
326
|
|
@@ -420,7 +376,9 @@ class _App:
|
|
420
376
|
elif show_progress is False:
|
421
377
|
deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
|
422
378
|
|
423
|
-
async with _run_app(
|
379
|
+
async with _run_app(
|
380
|
+
self, client=client, detach=detach, interactive=interactive, environment_name=environment_name
|
381
|
+
):
|
424
382
|
yield self
|
425
383
|
|
426
384
|
def _get_default_image(self):
|
@@ -442,11 +400,13 @@ class _App:
|
|
442
400
|
return [m for m in all_mounts if m.is_local()]
|
443
401
|
|
444
402
|
def _add_function(self, function: _Function, is_web_endpoint: bool):
|
445
|
-
if
|
403
|
+
if old_function := self._functions.get(function.tag, None):
|
404
|
+
if old_function is function:
|
405
|
+
return # already added the same exact instance, ignore
|
406
|
+
|
446
407
|
if not is_notebook():
|
447
|
-
old_function: _Function = self._functions[function.tag]
|
448
408
|
logger.warning(
|
449
|
-
f"Warning:
|
409
|
+
f"Warning: function name '{function.tag}' collision!"
|
450
410
|
" Overriding existing function "
|
451
411
|
f"[{old_function._info.module_name}].{old_function._info.function_name}"
|
452
412
|
f" with new function [{function._info.module_name}].{function._info.function_name}"
|
@@ -504,7 +464,7 @@ class _App:
|
|
504
464
|
return self._functions
|
505
465
|
|
506
466
|
@property
|
507
|
-
def registered_classes(self) -> dict[str,
|
467
|
+
def registered_classes(self) -> dict[str, _Cls]:
|
508
468
|
"""All modal.Cls objects registered on the app."""
|
509
469
|
return self._classes
|
510
470
|
|
@@ -995,13 +955,6 @@ class _App:
|
|
995
955
|
```
|
996
956
|
"""
|
997
957
|
for tag, function in other_app._functions.items():
|
998
|
-
existing_function = self._functions.get(tag)
|
999
|
-
if existing_function and existing_function != function:
|
1000
|
-
logger.warning(
|
1001
|
-
f"Named app function {tag} with existing value {existing_function} is being "
|
1002
|
-
f"overwritten by a different function {function}"
|
1003
|
-
)
|
1004
|
-
|
1005
958
|
self._add_function(function, False) # TODO(erikbern): webhook config?
|
1006
959
|
|
1007
960
|
for tag, cls in other_app._classes.items():
|