modal 0.67.22__py3-none-any.whl → 0.67.42__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 +3 -1
- modal/_runtime/container_io_manager.py +2 -2
- modal/_utils/function_utils.py +6 -0
- modal/cli/_traceback.py +8 -3
- modal/cli/app.py +1 -1
- modal/cli/network_file_system.py +23 -2
- modal/client.py +1 -1
- modal/client.pyi +2 -2
- modal/cls.py +68 -15
- modal/cls.pyi +5 -6
- modal/functions.py +3 -66
- modal/functions.pyi +4 -8
- modal/image.py +13 -8
- modal/image.pyi +2 -2
- modal/io_streams.py +1 -1
- modal/network_file_system.py +6 -0
- modal/network_file_system.pyi +20 -0
- modal/queue.py +33 -27
- modal/sandbox.py +37 -12
- modal/sandbox.pyi +10 -3
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/METADATA +1 -1
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/RECORD +34 -34
- modal_proto/api.proto +28 -1
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +743 -710
- modal_proto/api_pb2.pyi +92 -5
- modal_proto/api_pb2_grpc.py +67 -1
- modal_proto/api_pb2_grpc.pyi +23 -3
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/_version_generated.py +1 -1
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/LICENSE +0 -0
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/WHEEL +0 -0
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/entry_points.txt +0 -0
- {modal-0.67.22.dist-info → modal-0.67.42.dist-info}/top_level.txt +0 -0
modal/_container_entrypoint.py
CHANGED
@@ -478,7 +478,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
|
|
478
478
|
if len(service.code_deps) != len(dep_object_ids):
|
479
479
|
raise ExecutionError(
|
480
480
|
f"Function has {len(service.code_deps)} dependencies"
|
481
|
-
f" but container got {len(dep_object_ids)} object ids
|
481
|
+
f" but container got {len(dep_object_ids)} object ids.\n"
|
482
|
+
f"Code deps: {service.code_deps}\n"
|
483
|
+
f"Object ids: {dep_object_ids}"
|
482
484
|
)
|
483
485
|
for object_id, obj in zip(dep_object_ids, service.code_deps):
|
484
486
|
metadata: Message = container_app.object_handle_metadata[object_id]
|
@@ -908,7 +908,7 @@ class _ContainerIOManager:
|
|
908
908
|
if self.checkpoint_id:
|
909
909
|
logger.debug(f"Checkpoint ID: {self.checkpoint_id} (Memory Snapshot ID)")
|
910
910
|
else:
|
911
|
-
|
911
|
+
raise ValueError("No checkpoint ID provided for memory snapshot")
|
912
912
|
|
913
913
|
# Pause heartbeats since they keep the client connection open which causes the snapshotter to crash
|
914
914
|
async with self.heartbeat_condition:
|
@@ -918,7 +918,7 @@ class _ContainerIOManager:
|
|
918
918
|
self.heartbeat_condition.notify_all()
|
919
919
|
|
920
920
|
await self._client.stub.ContainerCheckpoint(
|
921
|
-
api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id
|
921
|
+
api_pb2.ContainerCheckpointRequest(checkpoint_id=self.checkpoint_id)
|
922
922
|
)
|
923
923
|
|
924
924
|
await self._client._close(prep_for_restore=True)
|
modal/_utils/function_utils.py
CHANGED
@@ -248,7 +248,13 @@ class FunctionInfo:
|
|
248
248
|
def get_globals(self) -> dict[str, Any]:
|
249
249
|
from .._vendor.cloudpickle import _extract_code_globals
|
250
250
|
|
251
|
+
if self.raw_f is None:
|
252
|
+
return {}
|
253
|
+
|
251
254
|
func = self.raw_f
|
255
|
+
while hasattr(func, "__wrapped__") and func is not func.__wrapped__:
|
256
|
+
# Unwrap functions decorated using functools.wrapped (potentially multiple times)
|
257
|
+
func = func.__wrapped__
|
252
258
|
f_globals_ref = _extract_code_globals(func.__code__)
|
253
259
|
f_globals = {k: func.__globals__[k] for k in f_globals_ref if k in func.__globals__}
|
254
260
|
return f_globals
|
modal/cli/_traceback.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
2
|
"""Helper functions related to displaying tracebacks in the CLI."""
|
3
3
|
import functools
|
4
|
+
import re
|
4
5
|
import warnings
|
5
6
|
from typing import Optional
|
6
7
|
|
@@ -166,8 +167,12 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
166
167
|
def showwarning(warning, category, filename, lineno, file=None, line=None):
|
167
168
|
if issubclass(category, (DeprecationError, PendingDeprecationError)):
|
168
169
|
content = str(warning)
|
169
|
-
|
170
|
-
|
170
|
+
if re.match(r"^\d{4}-\d{2}-\d{2}", content):
|
171
|
+
date = content[:10]
|
172
|
+
message = content[11:].strip()
|
173
|
+
else:
|
174
|
+
date = ""
|
175
|
+
message = content
|
171
176
|
try:
|
172
177
|
with open(filename, encoding="utf-8", errors="replace") as code_file:
|
173
178
|
source = code_file.readlines()[lineno - 1].strip()
|
@@ -178,7 +183,7 @@ def highlight_modal_deprecation_warnings() -> None:
|
|
178
183
|
panel = Panel(
|
179
184
|
message,
|
180
185
|
style="yellow",
|
181
|
-
title=f"Modal Deprecation Warning ({date})",
|
186
|
+
title=f"Modal Deprecation Warning ({date})" if date else "Modal Deprecation Warning",
|
182
187
|
title_align="left",
|
183
188
|
)
|
184
189
|
Console().print(panel)
|
modal/cli/app.py
CHANGED
modal/cli/network_file_system.py
CHANGED
@@ -10,7 +10,7 @@ from grpclib import GRPCError, Status
|
|
10
10
|
from rich.console import Console
|
11
11
|
from rich.syntax import Syntax
|
12
12
|
from rich.table import Table
|
13
|
-
from typer import Typer
|
13
|
+
from typer import Argument, Typer
|
14
14
|
|
15
15
|
import modal
|
16
16
|
from modal._location import display_location
|
@@ -18,7 +18,7 @@ from modal._output import OutputManager, ProgressHandler
|
|
18
18
|
from modal._utils.async_utils import synchronizer
|
19
19
|
from modal._utils.grpc_utils import retry_transient_errors
|
20
20
|
from modal.cli._download import _volume_download
|
21
|
-
from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
|
21
|
+
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
|
22
22
|
from modal.client import _Client
|
23
23
|
from modal.environments import ensure_env
|
24
24
|
from modal.network_file_system import _NetworkFileSystem
|
@@ -217,3 +217,24 @@ async def rm(
|
|
217
217
|
if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
|
218
218
|
raise UsageError(exc.message)
|
219
219
|
raise
|
220
|
+
|
221
|
+
|
222
|
+
@nfs_cli.command(
|
223
|
+
name="delete",
|
224
|
+
help="Delete a named, persistent modal.NetworkFileSystem.",
|
225
|
+
rich_help_panel="Management",
|
226
|
+
)
|
227
|
+
@synchronizer.create_blocking
|
228
|
+
async def delete(
|
229
|
+
nfs_name: str = Argument(help="Name of the modal.NetworkFileSystem to be deleted. Case sensitive"),
|
230
|
+
yes: bool = YES_OPTION,
|
231
|
+
env: Optional[str] = ENV_OPTION,
|
232
|
+
):
|
233
|
+
if not yes:
|
234
|
+
typer.confirm(
|
235
|
+
f"Are you sure you want to irrevocably delete the modal.NetworkFileSystem '{nfs_name}'?",
|
236
|
+
default=False,
|
237
|
+
abort=True,
|
238
|
+
)
|
239
|
+
|
240
|
+
await _NetworkFileSystem.delete(label=nfs_name, environment_name=env)
|
modal/client.py
CHANGED
@@ -147,7 +147,7 @@ class _Client:
|
|
147
147
|
)
|
148
148
|
if resp.warning:
|
149
149
|
ALARM_EMOJI = chr(0x1F6A8)
|
150
|
-
warnings.
|
150
|
+
warnings.warn_explicit(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError, "<unknown>", 0)
|
151
151
|
except GRPCError as exc:
|
152
152
|
if exc.status == Status.FAILED_PRECONDITION:
|
153
153
|
raise VersionError(
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/cls.py
CHANGED
@@ -72,6 +72,68 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
72
72
|
return inspect.Signature(constructor_parameters)
|
73
73
|
|
74
74
|
|
75
|
+
def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
|
76
|
+
"""mdmd:hidden
|
77
|
+
|
78
|
+
Binds an "instance service function" to a specific method.
|
79
|
+
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
80
|
+
it does it forward invocations to the underlying instance_service_function with the specified method,
|
81
|
+
and we don't support web_config for parameterized methods at the moment.
|
82
|
+
"""
|
83
|
+
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
84
|
+
# underlying instance_service_function. It's currently used in order to take advantage
|
85
|
+
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
86
|
+
# object itself doesn't need any "loading"
|
87
|
+
assert service_function._obj
|
88
|
+
method_name = class_bound_method._use_method_name
|
89
|
+
full_function_name = f"{class_bound_method._function_name}[parameterized]"
|
90
|
+
|
91
|
+
def hydrate_from_instance_service_function(method_placeholder_fun):
|
92
|
+
method_placeholder_fun._hydrate_from_other(service_function)
|
93
|
+
method_placeholder_fun._obj = service_function._obj
|
94
|
+
method_placeholder_fun._web_url = (
|
95
|
+
class_bound_method._web_url
|
96
|
+
) # TODO: this shouldn't be set when actual parameters are used
|
97
|
+
method_placeholder_fun._function_name = full_function_name
|
98
|
+
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
99
|
+
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
100
|
+
method_placeholder_fun._use_method_name = method_name
|
101
|
+
method_placeholder_fun._is_method = True
|
102
|
+
|
103
|
+
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
104
|
+
# there is currently no actual loading logic executed to create each method on
|
105
|
+
# the *parameterized* instance of a class - it uses the parameter-bound service-function
|
106
|
+
# for the instance. This load method just makes sure to set all attributes after the
|
107
|
+
# `service_function` has been loaded (it's in the `_deps`)
|
108
|
+
hydrate_from_instance_service_function(fun)
|
109
|
+
|
110
|
+
def _deps():
|
111
|
+
if service_function.is_hydrated:
|
112
|
+
# without this check, the common service_function will be reloaded by all methods
|
113
|
+
# TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
|
114
|
+
return []
|
115
|
+
return [service_function]
|
116
|
+
|
117
|
+
rep = f"Method({full_function_name})"
|
118
|
+
|
119
|
+
fun = _Function._from_loader(
|
120
|
+
_load,
|
121
|
+
rep,
|
122
|
+
deps=_deps,
|
123
|
+
hydrate_lazily=True,
|
124
|
+
)
|
125
|
+
if service_function.is_hydrated:
|
126
|
+
# Eager hydration (skip load) if the instance service function is already loaded
|
127
|
+
hydrate_from_instance_service_function(fun)
|
128
|
+
|
129
|
+
fun._info = class_bound_method._info
|
130
|
+
fun._obj = service_function._obj
|
131
|
+
fun._is_method = True
|
132
|
+
fun._app = class_bound_method._app
|
133
|
+
fun._spec = class_bound_method._spec
|
134
|
+
return fun
|
135
|
+
|
136
|
+
|
75
137
|
class _Obj:
|
76
138
|
"""An instance of a `Cls`, i.e. `Cls("foo", 42)` returns an `Obj`.
|
77
139
|
|
@@ -90,10 +152,9 @@ class _Obj:
|
|
90
152
|
|
91
153
|
def __init__(
|
92
154
|
self,
|
93
|
-
user_cls: type,
|
155
|
+
user_cls: Optional[type], # this would be None in case of lookups
|
94
156
|
class_service_function: Optional[_Function], # only None for <v0.63 classes
|
95
157
|
classbound_methods: dict[str, _Function],
|
96
|
-
from_other_workspace: bool,
|
97
158
|
options: Optional[api_pb2.FunctionOptions],
|
98
159
|
args,
|
99
160
|
kwargs,
|
@@ -107,17 +168,15 @@ class _Obj:
|
|
107
168
|
if class_service_function:
|
108
169
|
# >= v0.63 classes
|
109
170
|
# first create the singular object function used by all methods on this parameterization
|
110
|
-
self._instance_service_function = class_service_function._bind_parameters(
|
111
|
-
self, from_other_workspace, options, args, kwargs
|
112
|
-
)
|
171
|
+
self._instance_service_function = class_service_function._bind_parameters(self, options, args, kwargs)
|
113
172
|
for method_name, class_bound_method in classbound_methods.items():
|
114
|
-
method = self._instance_service_function
|
173
|
+
method = _bind_instance_method(self._instance_service_function, class_bound_method)
|
115
174
|
self._method_functions[method_name] = method
|
116
175
|
else:
|
117
176
|
# looked up <v0.63 classes - bind each individual method to the new parameters
|
118
177
|
self._instance_service_function = None
|
119
178
|
for method_name, class_bound_method in classbound_methods.items():
|
120
|
-
method = class_bound_method._bind_parameters(self,
|
179
|
+
method = class_bound_method._bind_parameters(self, options, args, kwargs)
|
121
180
|
self._method_functions[method_name] = method
|
122
181
|
|
123
182
|
# Used for construction local object lazily
|
@@ -247,7 +306,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
247
306
|
_method_functions: Optional[dict[str, _Function]] = None # Placeholder _Functions for each method
|
248
307
|
_options: Optional[api_pb2.FunctionOptions]
|
249
308
|
_callables: dict[str, Callable[..., Any]]
|
250
|
-
_from_other_workspace: Optional[bool] # Functions require FunctionBindParams before invocation.
|
251
309
|
_app: Optional["modal.app._App"] = None # not set for lookups
|
252
310
|
|
253
311
|
def _initialize_from_empty(self):
|
@@ -255,7 +313,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
255
313
|
self._class_service_function = None
|
256
314
|
self._options = None
|
257
315
|
self._callables = {}
|
258
|
-
self._from_other_workspace = None
|
259
316
|
|
260
317
|
def _initialize_from_other(self, other: "_Cls"):
|
261
318
|
self._user_cls = other._user_cls
|
@@ -263,7 +320,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
263
320
|
self._method_functions = other._method_functions
|
264
321
|
self._options = other._options
|
265
322
|
self._callables = other._callables
|
266
|
-
self._from_other_workspace = other._from_other_workspace
|
267
323
|
|
268
324
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
269
325
|
if not self._user_cls:
|
@@ -277,7 +333,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
277
333
|
and self._class_service_function._method_handle_metadata
|
278
334
|
and len(self._class_service_function._method_handle_metadata)
|
279
335
|
):
|
280
|
-
# The class only has a class service
|
336
|
+
# The class only has a class service function and no method placeholders (v0.67+)
|
281
337
|
if self._method_functions:
|
282
338
|
# We're here when the Cls is loaded locally (e.g. _Cls.from_local) so the _method_functions mapping is
|
283
339
|
# populated with (un-hydrated) _Function objects
|
@@ -298,7 +354,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
298
354
|
self._method_functions[method_name] = _Function._new_hydrated(
|
299
355
|
self._class_service_function.object_id, self._client, method_handle_metadata
|
300
356
|
)
|
301
|
-
elif self._class_service_function:
|
357
|
+
elif self._class_service_function and self._class_service_function.object_id:
|
302
358
|
# A class with a class service function and method placeholder functions
|
303
359
|
self._method_functions = {}
|
304
360
|
for method in metadata.methods:
|
@@ -382,7 +438,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
382
438
|
cls._class_service_function = class_service_function
|
383
439
|
cls._method_functions = method_functions
|
384
440
|
cls._callables = callables
|
385
|
-
cls._from_other_workspace = False
|
386
441
|
return cls
|
387
442
|
|
388
443
|
def _uses_common_service_function(self):
|
@@ -449,7 +504,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
449
504
|
|
450
505
|
rep = f"Ref({app_name})"
|
451
506
|
cls = cls._from_loader(_load_remote, rep, is_another_app=True)
|
452
|
-
cls._from_other_workspace = bool(workspace is not None)
|
453
507
|
return cls
|
454
508
|
|
455
509
|
def with_options(
|
@@ -543,7 +597,6 @@ class _Cls(_Object, type_prefix="cs"):
|
|
543
597
|
self._user_cls,
|
544
598
|
self._class_service_function,
|
545
599
|
self._method_functions,
|
546
|
-
self._from_other_workspace,
|
547
600
|
self._options,
|
548
601
|
args,
|
549
602
|
kwargs,
|
modal/cls.pyi
CHANGED
@@ -19,6 +19,9 @@ T = typing.TypeVar("T")
|
|
19
19
|
|
20
20
|
def _use_annotation_parameters(user_cls) -> bool: ...
|
21
21
|
def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
22
|
+
def _bind_instance_method(
|
23
|
+
service_function: modal.functions._Function, class_bound_method: modal.functions._Function
|
24
|
+
): ...
|
22
25
|
|
23
26
|
class _Obj:
|
24
27
|
_functions: dict[str, modal.functions._Function]
|
@@ -30,10 +33,9 @@ class _Obj:
|
|
30
33
|
def _uses_common_service_function(self): ...
|
31
34
|
def __init__(
|
32
35
|
self,
|
33
|
-
user_cls: type,
|
36
|
+
user_cls: typing.Optional[type],
|
34
37
|
class_service_function: typing.Optional[modal.functions._Function],
|
35
38
|
classbound_methods: dict[str, modal.functions._Function],
|
36
|
-
from_other_workspace: bool,
|
37
39
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
38
40
|
args,
|
39
41
|
kwargs,
|
@@ -58,10 +60,9 @@ class Obj:
|
|
58
60
|
|
59
61
|
def __init__(
|
60
62
|
self,
|
61
|
-
user_cls: type,
|
63
|
+
user_cls: typing.Optional[type],
|
62
64
|
class_service_function: typing.Optional[modal.functions.Function],
|
63
65
|
classbound_methods: dict[str, modal.functions.Function],
|
64
|
-
from_other_workspace: bool,
|
65
66
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
66
67
|
args,
|
67
68
|
kwargs,
|
@@ -90,7 +91,6 @@ class _Cls(modal.object._Object):
|
|
90
91
|
_method_functions: typing.Optional[dict[str, modal.functions._Function]]
|
91
92
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
92
93
|
_callables: dict[str, typing.Callable[..., typing.Any]]
|
93
|
-
_from_other_workspace: typing.Optional[bool]
|
94
94
|
_app: typing.Optional[modal.app._App]
|
95
95
|
|
96
96
|
def _initialize_from_empty(self): ...
|
@@ -142,7 +142,6 @@ class Cls(modal.object.Object):
|
|
142
142
|
_method_functions: typing.Optional[dict[str, modal.functions.Function]]
|
143
143
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
144
144
|
_callables: dict[str, typing.Callable[..., typing.Any]]
|
145
|
-
_from_other_workspace: typing.Optional[bool]
|
146
145
|
_app: typing.Optional[modal.app.App]
|
147
146
|
|
148
147
|
def __init__(self, *args, **kwargs): ...
|
modal/functions.py
CHANGED
@@ -347,7 +347,7 @@ class _FunctionSpec:
|
|
347
347
|
volumes: dict[Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]]
|
348
348
|
gpus: Union[GPU_T, list[GPU_T]] # TODO(irfansharif): Somehow assert that it's the first kind, in sandboxes
|
349
349
|
cloud: Optional[str]
|
350
|
-
cpu: Optional[float]
|
350
|
+
cpu: Optional[Union[float, tuple[float, float]]]
|
351
351
|
memory: Optional[Union[int, tuple[int, int]]]
|
352
352
|
ephemeral_disk: Optional[int]
|
353
353
|
scheduler_placement: Optional[SchedulerPlacement]
|
@@ -424,68 +424,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
424
424
|
fun._is_method = True
|
425
425
|
return fun
|
426
426
|
|
427
|
-
def _bind_instance_method(self, class_bound_method: "_Function"):
|
428
|
-
"""mdmd:hidden
|
429
|
-
|
430
|
-
Binds an "instance service function" to a specific method.
|
431
|
-
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
432
|
-
it does it forward invocations to the underlying instance_service_function with the specified method,
|
433
|
-
and we don't support web_config for parameterized methods at the moment.
|
434
|
-
"""
|
435
|
-
# TODO(elias): refactor to not use `_from_loader()` as a crutch for lazy-loading the
|
436
|
-
# underlying instance_service_function. It's currently used in order to take advantage
|
437
|
-
# of resolver logic and get "chained" resolution of lazy loads, even though this thin
|
438
|
-
# object itself doesn't need any "loading"
|
439
|
-
instance_service_function = self
|
440
|
-
assert instance_service_function._obj
|
441
|
-
method_name = class_bound_method._use_method_name
|
442
|
-
full_function_name = f"{class_bound_method._function_name}[parameterized]"
|
443
|
-
|
444
|
-
def hydrate_from_instance_service_function(method_placeholder_fun):
|
445
|
-
method_placeholder_fun._hydrate_from_other(instance_service_function)
|
446
|
-
method_placeholder_fun._obj = instance_service_function._obj
|
447
|
-
method_placeholder_fun._web_url = (
|
448
|
-
class_bound_method._web_url
|
449
|
-
) # TODO: this shouldn't be set when actual parameters are used
|
450
|
-
method_placeholder_fun._function_name = full_function_name
|
451
|
-
method_placeholder_fun._is_generator = class_bound_method._is_generator
|
452
|
-
method_placeholder_fun._cluster_size = class_bound_method._cluster_size
|
453
|
-
method_placeholder_fun._use_method_name = method_name
|
454
|
-
method_placeholder_fun._is_method = True
|
455
|
-
|
456
|
-
async def _load(fun: "_Function", resolver: Resolver, existing_object_id: Optional[str]):
|
457
|
-
# there is currently no actual loading logic executed to create each method on
|
458
|
-
# the *parameterized* instance of a class - it uses the parameter-bound service-function
|
459
|
-
# for the instance. This load method just makes sure to set all attributes after the
|
460
|
-
# `instance_service_function` has been loaded (it's in the `_deps`)
|
461
|
-
hydrate_from_instance_service_function(fun)
|
462
|
-
|
463
|
-
def _deps():
|
464
|
-
if instance_service_function.is_hydrated:
|
465
|
-
# without this check, the common instance_service_function will be reloaded by all methods
|
466
|
-
# TODO(elias): Investigate if we can fix this multi-loader in the resolver - feels like a bug?
|
467
|
-
return []
|
468
|
-
return [instance_service_function]
|
469
|
-
|
470
|
-
rep = f"Method({full_function_name})"
|
471
|
-
|
472
|
-
fun = _Function._from_loader(
|
473
|
-
_load,
|
474
|
-
rep,
|
475
|
-
deps=_deps,
|
476
|
-
hydrate_lazily=True,
|
477
|
-
)
|
478
|
-
if instance_service_function.is_hydrated:
|
479
|
-
# Eager hydration (skip load) if the instance service function is already loaded
|
480
|
-
hydrate_from_instance_service_function(fun)
|
481
|
-
|
482
|
-
fun._info = class_bound_method._info
|
483
|
-
fun._obj = instance_service_function._obj
|
484
|
-
fun._is_method = True
|
485
|
-
fun._app = class_bound_method._app
|
486
|
-
fun._spec = class_bound_method._spec
|
487
|
-
return fun
|
488
|
-
|
489
427
|
@staticmethod
|
490
428
|
def from_args(
|
491
429
|
info: FunctionInfo,
|
@@ -510,7 +448,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
510
448
|
batch_max_size: Optional[int] = None,
|
511
449
|
batch_wait_ms: Optional[int] = None,
|
512
450
|
container_idle_timeout: Optional[int] = None,
|
513
|
-
cpu: Optional[float] = None,
|
451
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
514
452
|
keep_warm: Optional[int] = None, # keep_warm=True is equivalent to keep_warm=1
|
515
453
|
cloud: Optional[str] = None,
|
516
454
|
scheduler_placement: Optional[SchedulerPlacement] = None,
|
@@ -982,7 +920,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
982
920
|
def _bind_parameters(
|
983
921
|
self,
|
984
922
|
obj: "modal.cls._Obj",
|
985
|
-
from_other_workspace: bool,
|
986
923
|
options: Optional[api_pb2.FunctionOptions],
|
987
924
|
args: Sized,
|
988
925
|
kwargs: dict[str, Any],
|
@@ -993,7 +930,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
993
930
|
"""
|
994
931
|
|
995
932
|
# In some cases, reuse the base function, i.e. not create new clones of each method or the "service function"
|
996
|
-
can_use_parent = len(args) + len(kwargs) == 0 and
|
933
|
+
can_use_parent = len(args) + len(kwargs) == 0 and options is None
|
997
934
|
parent = self
|
998
935
|
|
999
936
|
async def _load(param_bound_func: _Function, resolver: Resolver, existing_object_id: Optional[str]):
|
modal/functions.pyi
CHANGED
@@ -96,7 +96,7 @@ class _FunctionSpec:
|
|
96
96
|
]
|
97
97
|
gpus: typing.Union[None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]]
|
98
98
|
cloud: typing.Optional[str]
|
99
|
-
cpu: typing.
|
99
|
+
cpu: typing.Union[float, tuple[float, float], None]
|
100
100
|
memory: typing.Union[int, tuple[int, int], None]
|
101
101
|
ephemeral_disk: typing.Optional[int]
|
102
102
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement]
|
@@ -117,7 +117,7 @@ class _FunctionSpec:
|
|
117
117
|
None, bool, str, modal.gpu._GPUConfig, list[typing.Union[None, bool, str, modal.gpu._GPUConfig]]
|
118
118
|
],
|
119
119
|
cloud: typing.Optional[str],
|
120
|
-
cpu: typing.
|
120
|
+
cpu: typing.Union[float, tuple[float, float], None],
|
121
121
|
memory: typing.Union[int, tuple[int, int], None],
|
122
122
|
ephemeral_disk: typing.Optional[int],
|
123
123
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement],
|
@@ -150,7 +150,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
150
150
|
_method_handle_metadata: typing.Optional[dict[str, modal_proto.api_pb2.FunctionHandleMetadata]]
|
151
151
|
|
152
152
|
def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function._PartialFunction): ...
|
153
|
-
def _bind_instance_method(self, class_bound_method: _Function): ...
|
154
153
|
@staticmethod
|
155
154
|
def from_args(
|
156
155
|
info: modal._utils.function_utils.FunctionInfo,
|
@@ -181,7 +180,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
181
180
|
batch_max_size: typing.Optional[int] = None,
|
182
181
|
batch_wait_ms: typing.Optional[int] = None,
|
183
182
|
container_idle_timeout: typing.Optional[int] = None,
|
184
|
-
cpu: typing.
|
183
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
185
184
|
keep_warm: typing.Optional[int] = None,
|
186
185
|
cloud: typing.Optional[str] = None,
|
187
186
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
@@ -200,7 +199,6 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
200
199
|
def _bind_parameters(
|
201
200
|
self,
|
202
201
|
obj: modal.cls._Obj,
|
203
|
-
from_other_workspace: bool,
|
204
202
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
205
203
|
args: collections.abc.Sized,
|
206
204
|
kwargs: dict[str, typing.Any],
|
@@ -320,7 +318,6 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
320
318
|
|
321
319
|
def __init__(self, *args, **kwargs): ...
|
322
320
|
def _bind_method(self, user_cls, method_name: str, partial_function: modal.partial_function.PartialFunction): ...
|
323
|
-
def _bind_instance_method(self, class_bound_method: Function): ...
|
324
321
|
@staticmethod
|
325
322
|
def from_args(
|
326
323
|
info: modal._utils.function_utils.FunctionInfo,
|
@@ -351,7 +348,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
351
348
|
batch_max_size: typing.Optional[int] = None,
|
352
349
|
batch_wait_ms: typing.Optional[int] = None,
|
353
350
|
container_idle_timeout: typing.Optional[int] = None,
|
354
|
-
cpu: typing.
|
351
|
+
cpu: typing.Union[float, tuple[float, float], None] = None,
|
355
352
|
keep_warm: typing.Optional[int] = None,
|
356
353
|
cloud: typing.Optional[str] = None,
|
357
354
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement] = None,
|
@@ -370,7 +367,6 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
370
367
|
def _bind_parameters(
|
371
368
|
self,
|
372
369
|
obj: modal.cls.Obj,
|
373
|
-
from_other_workspace: bool,
|
374
370
|
options: typing.Optional[modal_proto.api_pb2.FunctionOptions],
|
375
371
|
args: collections.abc.Sized,
|
376
372
|
kwargs: dict[str, typing.Any],
|
modal/image.py
CHANGED
@@ -677,13 +677,13 @@ class _Image(_Object, type_prefix="im"):
|
|
677
677
|
context_mount=mount,
|
678
678
|
)
|
679
679
|
|
680
|
-
def
|
681
|
-
"""Adds Python
|
680
|
+
def add_local_python_source(self, *modules: str, copy: bool = False) -> "_Image":
|
681
|
+
"""Adds locally available Python packages/modules to containers
|
682
682
|
|
683
|
-
Adds all files from the specified Python
|
683
|
+
Adds all files from the specified Python package or module to containers running the Image.
|
684
684
|
|
685
685
|
Packages are added to the `/root` directory of containers, which is on the `PYTHONPATH`
|
686
|
-
of any executed Modal Functions.
|
686
|
+
of any executed Modal Functions, enabling import of the module by that name.
|
687
687
|
|
688
688
|
By default (`copy=False`), the files are added to containers on startup and are not built into the actual Image,
|
689
689
|
which speeds up deployment.
|
@@ -693,9 +693,14 @@ class _Image(_Object, type_prefix="im"):
|
|
693
693
|
required if you want to run additional build steps after this one.
|
694
694
|
|
695
695
|
**Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
|
696
|
-
To add full directories with finer control, use `.add_local_dir()` instead
|
696
|
+
To add full directories with finer control, use `.add_local_dir()` instead and specify `/root` as
|
697
|
+
the destination directory.
|
697
698
|
"""
|
698
|
-
|
699
|
+
|
700
|
+
def only_py_files(filename):
|
701
|
+
return filename.endswith(".py")
|
702
|
+
|
703
|
+
mount = _Mount.from_local_python_packages(*modules, condition=only_py_files)
|
699
704
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
700
705
|
|
701
706
|
def copy_local_dir(self, local_path: Union[str, Path], remote_path: Union[str, Path] = ".") -> "_Image":
|
@@ -1005,8 +1010,8 @@ class _Image(_Object, type_prefix="im"):
|
|
1005
1010
|
If not provided as argument the path to the lockfile is inferred. However, the
|
1006
1011
|
file has to exist, unless `ignore_lockfile` is set to `True`.
|
1007
1012
|
|
1008
|
-
Note that the root project of the poetry project is not installed,
|
1009
|
-
|
1013
|
+
Note that the root project of the poetry project is not installed, only the dependencies.
|
1014
|
+
For including local python source files see `add_local_python_source`
|
1010
1015
|
"""
|
1011
1016
|
|
1012
1017
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
modal/image.pyi
CHANGED
@@ -115,7 +115,7 @@ class _Image(modal.object._Object):
|
|
115
115
|
def copy_local_file(
|
116
116
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
117
117
|
) -> _Image: ...
|
118
|
-
def
|
118
|
+
def add_local_python_source(self, *module_names: str, copy: bool = False) -> _Image: ...
|
119
119
|
def copy_local_dir(
|
120
120
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
121
121
|
) -> _Image: ...
|
@@ -372,7 +372,7 @@ class Image(modal.object.Object):
|
|
372
372
|
def copy_local_file(
|
373
373
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "./"
|
374
374
|
) -> Image: ...
|
375
|
-
def
|
375
|
+
def add_local_python_source(self, *module_names: str, copy: bool = False) -> Image: ...
|
376
376
|
def copy_local_dir(
|
377
377
|
self, local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.Path] = "."
|
378
378
|
) -> Image: ...
|
modal/io_streams.py
CHANGED
@@ -184,7 +184,7 @@ class _StreamReader(Generic[T]):
|
|
184
184
|
|
185
185
|
async for message in iterator:
|
186
186
|
if self._stream_type == StreamType.STDOUT and message:
|
187
|
-
print(message, end="")
|
187
|
+
print(message.decode("utf-8"), end="")
|
188
188
|
elif self._stream_type == StreamType.PIPE:
|
189
189
|
self._container_process_buffer.append(message)
|
190
190
|
if message is None:
|
modal/network_file_system.py
CHANGED
@@ -221,6 +221,12 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
221
221
|
resp = await retry_transient_errors(client.stub.SharedVolumeGetOrCreate, request)
|
222
222
|
return resp.shared_volume_id
|
223
223
|
|
224
|
+
@staticmethod
|
225
|
+
async def delete(label: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
226
|
+
obj = await _NetworkFileSystem.lookup(label, client=client, environment_name=environment_name)
|
227
|
+
req = api_pb2.SharedVolumeDeleteRequest(shared_volume_id=obj.object_id)
|
228
|
+
await retry_transient_errors(obj._client.stub.SharedVolumeDelete, req)
|
229
|
+
|
224
230
|
@live_method
|
225
231
|
async def write_file(self, remote_path: str, fp: BinaryIO, progress_cb: Optional[Callable[..., Any]] = None) -> int:
|
226
232
|
"""Write from a file object to a path on the network file system, atomically.
|