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/cli/run.py
CHANGED
@@ -7,15 +7,14 @@ import re
|
|
7
7
|
import shlex
|
8
8
|
import sys
|
9
9
|
import time
|
10
|
-
import typing
|
11
10
|
from functools import partial
|
12
11
|
from typing import Any, Callable, Optional, get_type_hints
|
13
12
|
|
14
13
|
import click
|
15
14
|
import typer
|
15
|
+
from click import ClickException
|
16
16
|
from typing_extensions import TypedDict
|
17
17
|
|
18
|
-
from .. import Cls
|
19
18
|
from ..app import App, LocalEntrypoint
|
20
19
|
from ..config import config
|
21
20
|
from ..environments import ensure_env
|
@@ -26,7 +25,7 @@ from ..output import enable_output
|
|
26
25
|
from ..runner import deploy_app, interactive_shell, run_app
|
27
26
|
from ..serving import serve_app
|
28
27
|
from ..volume import Volume
|
29
|
-
from .import_refs import import_app,
|
28
|
+
from .import_refs import CLICommand, MethodReference, _get_runnable_app, import_and_filter, import_app, parse_import_ref
|
30
29
|
from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
|
31
30
|
|
32
31
|
|
@@ -145,39 +144,7 @@ def _write_local_result(result_path: str, res: Any):
|
|
145
144
|
fid.write(res)
|
146
145
|
|
147
146
|
|
148
|
-
def
|
149
|
-
function = app.registered_functions.get(function_tag)
|
150
|
-
if not function or (isinstance(function, Function) and function.info.user_cls is not None):
|
151
|
-
# This is either a function_tag for a class method function (e.g MyClass.foo) or a function tag for a
|
152
|
-
# class service function (MyClass.*)
|
153
|
-
class_name, method_name = function_tag.rsplit(".", 1)
|
154
|
-
if not function:
|
155
|
-
function = app.registered_functions.get(f"{class_name}.*")
|
156
|
-
assert isinstance(function, Function)
|
157
|
-
function = typing.cast(Function, function)
|
158
|
-
if function.is_generator:
|
159
|
-
raise InvalidError("`modal run` is not supported for generator functions")
|
160
|
-
|
161
|
-
signature: dict[str, ParameterMetadata]
|
162
|
-
cls: Optional[Cls] = None
|
163
|
-
if function.info.user_cls is not None:
|
164
|
-
cls = typing.cast(Cls, app.registered_classes[class_name])
|
165
|
-
cls_signature = _get_signature(function.info.user_cls)
|
166
|
-
if method_name == "*":
|
167
|
-
method_names = list(cls._get_partial_functions().keys())
|
168
|
-
if len(method_names) == 1:
|
169
|
-
method_name = method_names[0]
|
170
|
-
else:
|
171
|
-
class_name = function.info.user_cls.__name__
|
172
|
-
raise click.UsageError(
|
173
|
-
f"Please specify a specific method of {class_name} to run, e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
|
174
|
-
)
|
175
|
-
fun_signature = _get_signature(getattr(cls, method_name).info.raw_f, is_method=True)
|
176
|
-
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
177
|
-
# TODO(erikbern): assert there's no overlap?
|
178
|
-
else:
|
179
|
-
signature = _get_signature(function.info.raw_f)
|
180
|
-
|
147
|
+
def _make_click_function(app, inner: Callable[[dict[str, Any]], Any]):
|
181
148
|
@click.pass_context
|
182
149
|
def f(ctx, **kwargs):
|
183
150
|
show_progress: bool = ctx.obj["show_progress"]
|
@@ -188,21 +155,65 @@ def _get_click_command_for_function(app: App, function_tag):
|
|
188
155
|
environment_name=ctx.obj["env"],
|
189
156
|
interactive=ctx.obj["interactive"],
|
190
157
|
):
|
191
|
-
|
192
|
-
res = function.remote(**kwargs)
|
193
|
-
else:
|
194
|
-
# unpool class and method arguments
|
195
|
-
# TODO(erikbern): this code is a bit hacky
|
196
|
-
cls_kwargs = {k: kwargs[k] for k in cls_signature}
|
197
|
-
fun_kwargs = {k: kwargs[k] for k in fun_signature}
|
198
|
-
|
199
|
-
instance = cls(**cls_kwargs)
|
200
|
-
method: Function = getattr(instance, method_name)
|
201
|
-
res = method.remote(**fun_kwargs)
|
158
|
+
res = inner(kwargs)
|
202
159
|
|
203
160
|
if result_path := ctx.obj["result_path"]:
|
204
161
|
_write_local_result(result_path, res)
|
205
162
|
|
163
|
+
return f
|
164
|
+
|
165
|
+
|
166
|
+
def _get_click_command_for_function(app: App, function: Function):
|
167
|
+
if function.is_generator:
|
168
|
+
raise InvalidError("`modal run` is not supported for generator functions")
|
169
|
+
|
170
|
+
signature: dict[str, ParameterMetadata] = _get_signature(function.info.raw_f)
|
171
|
+
|
172
|
+
def _inner(click_kwargs):
|
173
|
+
return function.remote(**click_kwargs)
|
174
|
+
|
175
|
+
f = _make_click_function(app, _inner)
|
176
|
+
|
177
|
+
with_click_options = _add_click_options(f, signature)
|
178
|
+
return click.command(with_click_options)
|
179
|
+
|
180
|
+
|
181
|
+
def _get_click_command_for_cls(app: App, method_ref: MethodReference):
|
182
|
+
signature: dict[str, ParameterMetadata]
|
183
|
+
cls = method_ref.cls
|
184
|
+
method_name = method_ref.method_name
|
185
|
+
|
186
|
+
cls_signature = _get_signature(cls._get_user_cls())
|
187
|
+
partial_functions = cls._get_partial_functions()
|
188
|
+
|
189
|
+
if method_name in ("*", ""):
|
190
|
+
# auto infer method name - not sure if we have to support this...
|
191
|
+
method_names = list(partial_functions.keys())
|
192
|
+
if len(method_names) == 1:
|
193
|
+
method_name = method_names[0]
|
194
|
+
else:
|
195
|
+
raise click.UsageError(
|
196
|
+
f"Please specify a specific method of {cls._get_name()} to run, "
|
197
|
+
f"e.g. `modal run foo.py::MyClass.bar`" # noqa: E501
|
198
|
+
)
|
199
|
+
|
200
|
+
partial_function = partial_functions[method_name]
|
201
|
+
fun_signature = _get_signature(partial_function._get_raw_f(), is_method=True)
|
202
|
+
|
203
|
+
# TODO(erikbern): assert there's no overlap?
|
204
|
+
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
205
|
+
|
206
|
+
def _inner(click_kwargs):
|
207
|
+
# unpool class and method arguments
|
208
|
+
# TODO(erikbern): this code is a bit hacky
|
209
|
+
cls_kwargs = {k: click_kwargs[k] for k in cls_signature}
|
210
|
+
fun_kwargs = {k: click_kwargs[k] for k in fun_signature}
|
211
|
+
|
212
|
+
instance = cls(**cls_kwargs)
|
213
|
+
method: Function = getattr(instance, method_name)
|
214
|
+
return method.remote(**fun_kwargs)
|
215
|
+
|
216
|
+
f = _make_click_function(app, _inner)
|
206
217
|
with_click_options = _add_click_options(f, signature)
|
207
218
|
return click.command(with_click_options)
|
208
219
|
|
@@ -242,6 +253,15 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
|
|
242
253
|
return click.command(with_click_options)
|
243
254
|
|
244
255
|
|
256
|
+
def _get_runnable_list(all_usable_commands: list[CLICommand]) -> str:
|
257
|
+
usable_command_lines = []
|
258
|
+
for cmd in all_usable_commands:
|
259
|
+
cmd_names = " / ".join(cmd.names)
|
260
|
+
usable_command_lines.append(cmd_names)
|
261
|
+
|
262
|
+
return "\n".join(usable_command_lines)
|
263
|
+
|
264
|
+
|
245
265
|
class RunGroup(click.Group):
|
246
266
|
def get_command(self, ctx, func_ref):
|
247
267
|
# note: get_command here is run before the "group logic" in the `run` logic below
|
@@ -249,16 +269,39 @@ class RunGroup(click.Group):
|
|
249
269
|
# needs to be handled here, and not in the `run` logic below
|
250
270
|
ctx.ensure_object(dict)
|
251
271
|
ctx.obj["env"] = ensure_env(ctx.params["env"])
|
252
|
-
|
253
|
-
|
272
|
+
|
273
|
+
import_ref = parse_import_ref(func_ref)
|
274
|
+
runnable, all_usable_commands = import_and_filter(
|
275
|
+
import_ref, accept_local_entrypoint=True, accept_webhook=False
|
276
|
+
)
|
277
|
+
if not runnable:
|
278
|
+
help_header = (
|
279
|
+
"Specify a Modal Function or local entrypoint to run. E.g.\n"
|
280
|
+
f"> modal run {import_ref.file_or_module}::my_function [..args]"
|
281
|
+
)
|
282
|
+
|
283
|
+
if all_usable_commands:
|
284
|
+
help_footer = f"'{import_ref.file_or_module}' has the following functions and local entrypoints:\n"
|
285
|
+
help_footer += _get_runnable_list(all_usable_commands)
|
286
|
+
else:
|
287
|
+
help_footer = f"'{import_ref.file_or_module}' has no functions or local entrypoints."
|
288
|
+
|
289
|
+
raise ClickException(f"{help_header}\n\n{help_footer}")
|
290
|
+
|
291
|
+
app = _get_runnable_app(runnable)
|
292
|
+
|
254
293
|
if app.description is None:
|
255
294
|
app.set_description(_get_clean_app_description(func_ref))
|
256
|
-
if isinstance(function_or_entrypoint, LocalEntrypoint):
|
257
|
-
click_command = _get_click_command_for_local_entrypoint(app, function_or_entrypoint)
|
258
|
-
else:
|
259
|
-
tag = function_or_entrypoint.info.get_tag()
|
260
|
-
click_command = _get_click_command_for_function(app, tag)
|
261
295
|
|
296
|
+
if isinstance(runnable, LocalEntrypoint):
|
297
|
+
click_command = _get_click_command_for_local_entrypoint(app, runnable)
|
298
|
+
elif isinstance(runnable, Function):
|
299
|
+
click_command = _get_click_command_for_function(app, runnable)
|
300
|
+
elif isinstance(runnable, MethodReference):
|
301
|
+
click_command = _get_click_command_for_cls(app, runnable)
|
302
|
+
else:
|
303
|
+
# This should be unreachable...
|
304
|
+
raise ValueError(f"{runnable} is neither function, local entrypoint or class/method")
|
262
305
|
return click_command
|
263
306
|
|
264
307
|
|
@@ -409,36 +452,36 @@ def shell(
|
|
409
452
|
):
|
410
453
|
"""Run a command or interactive shell inside a Modal container.
|
411
454
|
|
412
|
-
|
455
|
+
**Examples:**
|
413
456
|
|
414
|
-
|
457
|
+
Start an interactive shell inside the default Debian-based image:
|
415
458
|
|
416
|
-
|
459
|
+
```
|
417
460
|
modal shell
|
418
461
|
```
|
419
462
|
|
420
|
-
|
463
|
+
Start an interactive shell with the spec for `my_function` in your App
|
421
464
|
(uses the same image, volumes, mounts, etc.):
|
422
465
|
|
423
|
-
|
466
|
+
```
|
424
467
|
modal shell hello_world.py::my_function
|
425
468
|
```
|
426
469
|
|
427
|
-
|
470
|
+
Or, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
|
428
471
|
|
429
|
-
|
472
|
+
```
|
430
473
|
modal shell hello_world.py::MyClass.my_method
|
431
474
|
```
|
432
475
|
|
433
476
|
Start a `python` shell:
|
434
477
|
|
435
|
-
|
478
|
+
```
|
436
479
|
modal shell hello_world.py --cmd=python
|
437
480
|
```
|
438
481
|
|
439
|
-
|
482
|
+
Run a command with your function's spec and pipe the output to a file:
|
440
483
|
|
441
|
-
|
484
|
+
```
|
442
485
|
modal shell hello_world.py -c 'uv pip list' > env.txt
|
443
486
|
```
|
444
487
|
"""
|
@@ -464,11 +507,34 @@ def shell(
|
|
464
507
|
exec(container_id=container_or_function, command=shlex.split(cmd), pty=pty)
|
465
508
|
return
|
466
509
|
|
467
|
-
|
468
|
-
|
510
|
+
import_ref = parse_import_ref(container_or_function)
|
511
|
+
runnable, all_usable_commands = import_and_filter(
|
512
|
+
import_ref, accept_local_entrypoint=False, accept_webhook=True
|
469
513
|
)
|
470
|
-
|
471
|
-
|
514
|
+
if not runnable:
|
515
|
+
help_header = (
|
516
|
+
"Specify a Modal function to start a shell session for. E.g.\n"
|
517
|
+
f"> modal shell {import_ref.file_or_module}::my_function"
|
518
|
+
)
|
519
|
+
|
520
|
+
if all_usable_commands:
|
521
|
+
help_footer = f"The selected module '{import_ref.file_or_module}' has the following choices:\n\n"
|
522
|
+
help_footer += _get_runnable_list(all_usable_commands)
|
523
|
+
else:
|
524
|
+
help_footer = f"The selected module '{import_ref.file_or_module}' has no Modal functions or classes."
|
525
|
+
|
526
|
+
raise ClickException(f"{help_header}\n\n{help_footer}")
|
527
|
+
|
528
|
+
function_spec: _FunctionSpec
|
529
|
+
if isinstance(runnable, MethodReference):
|
530
|
+
# TODO: let users specify a class instead of a method, since they use the same environment
|
531
|
+
class_service_function = runnable.cls._get_class_service_function()
|
532
|
+
function_spec = class_service_function.spec
|
533
|
+
elif isinstance(runnable, Function):
|
534
|
+
function_spec = runnable.spec
|
535
|
+
else:
|
536
|
+
raise ValueError("Referenced entity is not a Modal function or class")
|
537
|
+
|
472
538
|
start_shell = partial(
|
473
539
|
interactive_shell,
|
474
540
|
image=function_spec.image,
|
modal/client.py
CHANGED
modal/client.pyi
CHANGED
@@ -24,9 +24,10 @@ class _Client:
|
|
24
24
|
_cancellation_context: modal._utils.async_utils.TaskContext
|
25
25
|
_cancellation_context_event_loop: asyncio.events.AbstractEventLoop
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
|
+
_snapshotted: bool
|
27
28
|
|
28
29
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.
|
30
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.48"
|
30
31
|
): ...
|
31
32
|
def is_closed(self) -> bool: ...
|
32
33
|
@property
|
@@ -73,37 +74,40 @@ class _Client:
|
|
73
74
|
],
|
74
75
|
) -> collections.abc.AsyncGenerator[typing.Any, None]: ...
|
75
76
|
|
77
|
+
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
78
|
+
|
76
79
|
class Client:
|
77
80
|
_client_from_env: typing.ClassVar[typing.Optional[Client]]
|
78
81
|
_client_from_env_lock: typing.ClassVar[typing.Optional[asyncio.locks.Lock]]
|
79
82
|
_cancellation_context: modal._utils.async_utils.TaskContext
|
80
83
|
_cancellation_context_event_loop: asyncio.events.AbstractEventLoop
|
81
84
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
85
|
+
_snapshotted: bool
|
82
86
|
|
83
87
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.
|
88
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.48"
|
85
89
|
): ...
|
86
90
|
def is_closed(self) -> bool: ...
|
87
91
|
@property
|
88
92
|
def stub(self) -> modal_proto.modal_api_grpc.ModalClientModal: ...
|
89
93
|
|
90
|
-
class ___open_spec(typing_extensions.Protocol):
|
94
|
+
class ___open_spec(typing_extensions.Protocol[SUPERSELF]):
|
91
95
|
def __call__(self): ...
|
92
96
|
async def aio(self): ...
|
93
97
|
|
94
|
-
_open: ___open_spec
|
98
|
+
_open: ___open_spec[typing_extensions.Self]
|
95
99
|
|
96
|
-
class ___close_spec(typing_extensions.Protocol):
|
100
|
+
class ___close_spec(typing_extensions.Protocol[SUPERSELF]):
|
97
101
|
def __call__(self, prep_for_restore: bool = False): ...
|
98
102
|
async def aio(self, prep_for_restore: bool = False): ...
|
99
103
|
|
100
|
-
_close: ___close_spec
|
104
|
+
_close: ___close_spec[typing_extensions.Self]
|
101
105
|
|
102
|
-
class __hello_spec(typing_extensions.Protocol):
|
106
|
+
class __hello_spec(typing_extensions.Protocol[SUPERSELF]):
|
103
107
|
def __call__(self): ...
|
104
108
|
async def aio(self): ...
|
105
109
|
|
106
|
-
hello: __hello_spec
|
110
|
+
hello: __hello_spec[typing_extensions.Self]
|
107
111
|
|
108
112
|
def __enter__(self): ...
|
109
113
|
async def __aenter__(self): ...
|
@@ -120,23 +124,23 @@ class Client:
|
|
120
124
|
@classmethod
|
121
125
|
def set_env_client(cls, client: typing.Optional[Client]): ...
|
122
126
|
|
123
|
-
class ___call_safely_spec(typing_extensions.Protocol):
|
127
|
+
class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
|
124
128
|
def __call__(self, coro, readable_method: str): ...
|
125
129
|
async def aio(self, coro, readable_method: str): ...
|
126
130
|
|
127
|
-
_call_safely: ___call_safely_spec
|
131
|
+
_call_safely: ___call_safely_spec[typing_extensions.Self]
|
128
132
|
|
129
|
-
class ___reset_on_pid_change_spec(typing_extensions.Protocol):
|
133
|
+
class ___reset_on_pid_change_spec(typing_extensions.Protocol[SUPERSELF]):
|
130
134
|
def __call__(self): ...
|
131
135
|
async def aio(self): ...
|
132
136
|
|
133
|
-
_reset_on_pid_change: ___reset_on_pid_change_spec
|
137
|
+
_reset_on_pid_change: ___reset_on_pid_change_spec[typing_extensions.Self]
|
134
138
|
|
135
|
-
class ___get_grpclib_method_spec(typing_extensions.Protocol):
|
139
|
+
class ___get_grpclib_method_spec(typing_extensions.Protocol[SUPERSELF]):
|
136
140
|
def __call__(self, method_name: str) -> typing.Any: ...
|
137
141
|
async def aio(self, method_name: str) -> typing.Any: ...
|
138
142
|
|
139
|
-
_get_grpclib_method: ___get_grpclib_method_spec
|
143
|
+
_get_grpclib_method: ___get_grpclib_method_spec[typing_extensions.Self]
|
140
144
|
|
141
145
|
async def _call_unary(
|
142
146
|
self,
|
modal/cloud_bucket_mount.py
CHANGED
@@ -112,6 +112,9 @@ class _CloudBucketMount:
|
|
112
112
|
# If the bucket is publicly accessible, the secret is unnecessary and can be omitted.
|
113
113
|
secret: Optional[_Secret] = None
|
114
114
|
|
115
|
+
# Role ARN used for using OIDC authentication to access a cloud bucket.
|
116
|
+
oidc_auth_role_arn: Optional[str] = None
|
117
|
+
|
115
118
|
read_only: bool = False
|
116
119
|
requester_pays: bool = False
|
117
120
|
|
@@ -155,6 +158,7 @@ def cloud_bucket_mounts_to_proto(mounts: list[tuple[str, _CloudBucketMount]]) ->
|
|
155
158
|
bucket_type=bucket_type,
|
156
159
|
requester_pays=mount.requester_pays,
|
157
160
|
key_prefix=key_prefix,
|
161
|
+
oidc_auth_role_arn=mount.oidc_auth_role_arn,
|
158
162
|
)
|
159
163
|
cloud_bucket_mounts.append(cloud_bucket_mount)
|
160
164
|
|
modal/cloud_bucket_mount.pyi
CHANGED
@@ -7,6 +7,7 @@ class _CloudBucketMount:
|
|
7
7
|
bucket_endpoint_url: typing.Optional[str]
|
8
8
|
key_prefix: typing.Optional[str]
|
9
9
|
secret: typing.Optional[modal.secret._Secret]
|
10
|
+
oidc_auth_role_arn: typing.Optional[str]
|
10
11
|
read_only: bool
|
11
12
|
requester_pays: bool
|
12
13
|
|
@@ -16,6 +17,7 @@ class _CloudBucketMount:
|
|
16
17
|
bucket_endpoint_url: typing.Optional[str] = None,
|
17
18
|
key_prefix: typing.Optional[str] = None,
|
18
19
|
secret: typing.Optional[modal.secret._Secret] = None,
|
20
|
+
oidc_auth_role_arn: typing.Optional[str] = None,
|
19
21
|
read_only: bool = False,
|
20
22
|
requester_pays: bool = False,
|
21
23
|
) -> None: ...
|
@@ -31,6 +33,7 @@ class CloudBucketMount:
|
|
31
33
|
bucket_endpoint_url: typing.Optional[str]
|
32
34
|
key_prefix: typing.Optional[str]
|
33
35
|
secret: typing.Optional[modal.secret.Secret]
|
36
|
+
oidc_auth_role_arn: typing.Optional[str]
|
34
37
|
read_only: bool
|
35
38
|
requester_pays: bool
|
36
39
|
|
@@ -40,6 +43,7 @@ class CloudBucketMount:
|
|
40
43
|
bucket_endpoint_url: typing.Optional[str] = None,
|
41
44
|
key_prefix: typing.Optional[str] = None,
|
42
45
|
secret: typing.Optional[modal.secret.Secret] = None,
|
46
|
+
oidc_auth_role_arn: typing.Optional[str] = None,
|
43
47
|
read_only: bool = False,
|
44
48
|
requester_pays: bool = False,
|
45
49
|
) -> None: ...
|
modal/cls.py
CHANGED
@@ -11,19 +11,19 @@ from grpclib import GRPCError, Status
|
|
11
11
|
from modal._utils.function_utils import CLASS_PARAM_TYPE_MAP
|
12
12
|
from modal_proto import api_pb2
|
13
13
|
|
14
|
+
from ._object import _get_environment_name, _Object
|
14
15
|
from ._resolver import Resolver
|
15
16
|
from ._resources import convert_fn_config_to_resources_config
|
16
17
|
from ._serialization import check_valid_cls_constructor_arg
|
17
18
|
from ._traceback import print_server_warnings
|
18
19
|
from ._utils.async_utils import synchronize_api, synchronizer
|
19
|
-
from ._utils.deprecation import renamed_parameter
|
20
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
20
21
|
from ._utils.grpc_utils import retry_transient_errors
|
21
22
|
from ._utils.mount_utils import validate_volumes
|
22
23
|
from .client import _Client
|
23
24
|
from .exception import ExecutionError, InvalidError, NotFoundError, VersionError
|
24
25
|
from .functions import _Function, _parse_retries
|
25
26
|
from .gpu import GPU_T
|
26
|
-
from .object import _get_environment_name, _Object
|
27
27
|
from .partial_function import (
|
28
28
|
_find_callables_for_obj,
|
29
29
|
_find_partial_methods_for_user_cls,
|
@@ -72,9 +72,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature:
|
|
72
72
|
|
73
73
|
|
74
74
|
def _bind_instance_method(service_function: _Function, class_bound_method: _Function):
|
75
|
-
"""
|
76
|
-
|
77
|
-
Binds an "instance service function" to a specific method name.
|
75
|
+
"""Binds an "instance service function" to a specific method name.
|
78
76
|
This "dummy" _Function gets no unique object_id and isn't backend-backed at the moment, since all
|
79
77
|
it does it forward invocations to the underlying instance_service_function with the specified method,
|
80
78
|
and we don't support web_config for parameterized methods at the moment.
|
@@ -129,6 +127,7 @@ def _bind_instance_method(service_function: _Function, class_bound_method: _Func
|
|
129
127
|
fun._is_method = True
|
130
128
|
fun._app = class_bound_method._app
|
131
129
|
fun._spec = class_bound_method._spec
|
130
|
+
fun._is_web_endpoint = class_bound_method._is_web_endpoint
|
132
131
|
return fun
|
133
132
|
|
134
133
|
|
@@ -374,12 +373,14 @@ class _Cls(_Object, type_prefix="cs"):
|
|
374
373
|
_options: Optional[api_pb2.FunctionOptions]
|
375
374
|
_callables: dict[str, Callable[..., Any]]
|
376
375
|
_app: Optional["modal.app._App"] = None # not set for lookups
|
376
|
+
_name: Optional[str]
|
377
377
|
|
378
378
|
def _initialize_from_empty(self):
|
379
379
|
self._user_cls = None
|
380
380
|
self._class_service_function = None
|
381
381
|
self._options = None
|
382
382
|
self._callables = {}
|
383
|
+
self._name = None
|
383
384
|
|
384
385
|
def _initialize_from_other(self, other: "_Cls"):
|
385
386
|
super()._initialize_from_other(other)
|
@@ -388,12 +389,29 @@ class _Cls(_Object, type_prefix="cs"):
|
|
388
389
|
self._method_functions = other._method_functions
|
389
390
|
self._options = other._options
|
390
391
|
self._callables = other._callables
|
392
|
+
self._name = other._name
|
391
393
|
|
392
394
|
def _get_partial_functions(self) -> dict[str, _PartialFunction]:
|
393
395
|
if not self._user_cls:
|
394
396
|
raise AttributeError("You can only get the partial functions of a local Cls instance")
|
395
397
|
return _find_partial_methods_for_user_cls(self._user_cls, _PartialFunctionFlags.all())
|
396
398
|
|
399
|
+
def _get_app(self) -> "modal.app._App":
|
400
|
+
return self._app
|
401
|
+
|
402
|
+
def _get_user_cls(self) -> type:
|
403
|
+
return self._user_cls
|
404
|
+
|
405
|
+
def _get_name(self) -> str:
|
406
|
+
return self._name
|
407
|
+
|
408
|
+
def _get_class_service_function(self) -> "modal.functions._Function":
|
409
|
+
return self._class_service_function
|
410
|
+
|
411
|
+
def _get_method_names(self) -> Collection[str]:
|
412
|
+
# returns method names for a *local* class only for now (used by cli)
|
413
|
+
return self._method_functions.keys()
|
414
|
+
|
397
415
|
def _hydrate_metadata(self, metadata: Message):
|
398
416
|
assert isinstance(metadata, api_pb2.ClassHandleMetadata)
|
399
417
|
if (
|
@@ -412,6 +430,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
412
430
|
self._method_functions[method_name]._hydrate(
|
413
431
|
self._class_service_function.object_id, self._client, method_handle_metadata
|
414
432
|
)
|
433
|
+
|
415
434
|
else:
|
416
435
|
# We're here when the function is loaded remotely (e.g. _Cls.from_name)
|
417
436
|
self._method_functions = {}
|
@@ -506,6 +525,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
506
525
|
cls._class_service_function = class_service_function
|
507
526
|
cls._method_functions = method_functions
|
508
527
|
cls._callables = callables
|
528
|
+
cls._name = user_cls.__name__
|
509
529
|
return cls
|
510
530
|
|
511
531
|
def _uses_common_service_function(self):
|
@@ -576,6 +596,7 @@ class _Cls(_Object, type_prefix="cs"):
|
|
576
596
|
rep = f"Ref({app_name})"
|
577
597
|
cls = cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
|
578
598
|
# TODO: when pre 0.63 is phased out, we can set class_service_function here instead
|
599
|
+
cls._name = name
|
579
600
|
return cls
|
580
601
|
|
581
602
|
def with_options(
|
@@ -681,6 +702,13 @@ class _Cls(_Object, type_prefix="cs"):
|
|
681
702
|
# Used by CLI and container entrypoint
|
682
703
|
# TODO: remove this method - access to attributes on classes should be discouraged
|
683
704
|
if k in self._method_functions:
|
705
|
+
deprecation_warning(
|
706
|
+
(2025, 1, 13),
|
707
|
+
"Usage of methods directly on the class will soon be deprecated, "
|
708
|
+
"instantiate classes before using methods, e.g.:\n"
|
709
|
+
f"{self._name}().{k} instead of {self._name}.{k}",
|
710
|
+
pending=True,
|
711
|
+
)
|
684
712
|
return self._method_functions[k]
|
685
713
|
return getattr(self._user_cls, k)
|
686
714
|
|
modal/cls.pyi
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import collections.abc
|
2
2
|
import google.protobuf.message
|
3
3
|
import inspect
|
4
|
+
import modal._object
|
4
5
|
import modal.app
|
5
6
|
import modal.client
|
6
7
|
import modal.functions
|
@@ -54,6 +55,8 @@ class _Obj:
|
|
54
55
|
async def _aenter(self): ...
|
55
56
|
def __getattr__(self, k): ...
|
56
57
|
|
58
|
+
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
59
|
+
|
57
60
|
class Obj:
|
58
61
|
_cls: Cls
|
59
62
|
_functions: dict[str, modal.functions.Function]
|
@@ -76,11 +79,11 @@ class Obj:
|
|
76
79
|
def _get_parameter_values(self) -> dict[str, typing.Any]: ...
|
77
80
|
def _new_user_cls_instance(self): ...
|
78
81
|
|
79
|
-
class __keep_warm_spec(typing_extensions.Protocol):
|
82
|
+
class __keep_warm_spec(typing_extensions.Protocol[SUPERSELF]):
|
80
83
|
def __call__(self, warm_pool_size: int) -> None: ...
|
81
84
|
async def aio(self, warm_pool_size: int) -> None: ...
|
82
85
|
|
83
|
-
keep_warm: __keep_warm_spec
|
86
|
+
keep_warm: __keep_warm_spec[typing_extensions.Self]
|
84
87
|
|
85
88
|
def _cached_user_cls_instance(self): ...
|
86
89
|
def _enter(self): ...
|
@@ -91,17 +94,23 @@ class Obj:
|
|
91
94
|
async def _aenter(self): ...
|
92
95
|
def __getattr__(self, k): ...
|
93
96
|
|
94
|
-
class _Cls(modal.
|
97
|
+
class _Cls(modal._object._Object):
|
95
98
|
_user_cls: typing.Optional[type]
|
96
99
|
_class_service_function: typing.Optional[modal.functions._Function]
|
97
100
|
_method_functions: typing.Optional[dict[str, modal.functions._Function]]
|
98
101
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
99
|
-
_callables: dict[str,
|
102
|
+
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
100
103
|
_app: typing.Optional[modal.app._App]
|
104
|
+
_name: typing.Optional[str]
|
101
105
|
|
102
106
|
def _initialize_from_empty(self): ...
|
103
107
|
def _initialize_from_other(self, other: _Cls): ...
|
104
108
|
def _get_partial_functions(self) -> dict[str, modal.partial_function._PartialFunction]: ...
|
109
|
+
def _get_app(self) -> modal.app._App: ...
|
110
|
+
def _get_user_cls(self) -> type: ...
|
111
|
+
def _get_name(self) -> str: ...
|
112
|
+
def _get_class_service_function(self) -> modal.functions._Function: ...
|
113
|
+
def _get_method_names(self) -> collections.abc.Collection[str]: ...
|
105
114
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
106
115
|
@staticmethod
|
107
116
|
def validate_construction_mechanism(user_cls): ...
|
@@ -147,13 +156,19 @@ class Cls(modal.object.Object):
|
|
147
156
|
_class_service_function: typing.Optional[modal.functions.Function]
|
148
157
|
_method_functions: typing.Optional[dict[str, modal.functions.Function]]
|
149
158
|
_options: typing.Optional[modal_proto.api_pb2.FunctionOptions]
|
150
|
-
_callables: dict[str,
|
159
|
+
_callables: dict[str, collections.abc.Callable[..., typing.Any]]
|
151
160
|
_app: typing.Optional[modal.app.App]
|
161
|
+
_name: typing.Optional[str]
|
152
162
|
|
153
163
|
def __init__(self, *args, **kwargs): ...
|
154
164
|
def _initialize_from_empty(self): ...
|
155
165
|
def _initialize_from_other(self, other: Cls): ...
|
156
166
|
def _get_partial_functions(self) -> dict[str, modal.partial_function.PartialFunction]: ...
|
167
|
+
def _get_app(self) -> modal.app.App: ...
|
168
|
+
def _get_user_cls(self) -> type: ...
|
169
|
+
def _get_name(self) -> str: ...
|
170
|
+
def _get_class_service_function(self) -> modal.functions.Function: ...
|
171
|
+
def _get_method_names(self) -> collections.abc.Collection[str]: ...
|
157
172
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
158
173
|
@staticmethod
|
159
174
|
def validate_construction_mechanism(user_cls): ...
|
modal/container_process.pyi
CHANGED
@@ -36,6 +36,8 @@ class _ContainerProcess(typing.Generic[T]):
|
|
36
36
|
async def wait(self) -> int: ...
|
37
37
|
async def attach(self, *, pty: typing.Optional[bool] = None): ...
|
38
38
|
|
39
|
+
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
40
|
+
|
39
41
|
class ContainerProcess(typing.Generic[T]):
|
40
42
|
_process_id: typing.Optional[str]
|
41
43
|
_stdout: modal.io_streams.StreamReader[T]
|
@@ -63,20 +65,20 @@ class ContainerProcess(typing.Generic[T]):
|
|
63
65
|
@property
|
64
66
|
def returncode(self) -> int: ...
|
65
67
|
|
66
|
-
class __poll_spec(typing_extensions.Protocol):
|
68
|
+
class __poll_spec(typing_extensions.Protocol[SUPERSELF]):
|
67
69
|
def __call__(self) -> typing.Optional[int]: ...
|
68
70
|
async def aio(self) -> typing.Optional[int]: ...
|
69
71
|
|
70
|
-
poll: __poll_spec
|
72
|
+
poll: __poll_spec[typing_extensions.Self]
|
71
73
|
|
72
|
-
class __wait_spec(typing_extensions.Protocol):
|
74
|
+
class __wait_spec(typing_extensions.Protocol[SUPERSELF]):
|
73
75
|
def __call__(self) -> int: ...
|
74
76
|
async def aio(self) -> int: ...
|
75
77
|
|
76
|
-
wait: __wait_spec
|
78
|
+
wait: __wait_spec[typing_extensions.Self]
|
77
79
|
|
78
|
-
class __attach_spec(typing_extensions.Protocol):
|
80
|
+
class __attach_spec(typing_extensions.Protocol[SUPERSELF]):
|
79
81
|
def __call__(self, *, pty: typing.Optional[bool] = None): ...
|
80
82
|
async def aio(self, *, pty: typing.Optional[bool] = None): ...
|
81
83
|
|
82
|
-
attach: __attach_spec
|
84
|
+
attach: __attach_spec[typing_extensions.Self]
|