modal 0.62.16__py3-none-any.whl → 0.72.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/cli/run.py
CHANGED
@@ -2,27 +2,32 @@
|
|
2
2
|
import asyncio
|
3
3
|
import functools
|
4
4
|
import inspect
|
5
|
+
import platform
|
5
6
|
import re
|
7
|
+
import shlex
|
6
8
|
import sys
|
7
9
|
import time
|
10
|
+
import typing
|
8
11
|
from functools import partial
|
9
|
-
from typing import Any, Callable,
|
12
|
+
from typing import Any, Callable, Optional, get_type_hints
|
10
13
|
|
11
14
|
import click
|
12
15
|
import typer
|
13
|
-
from rich.console import Console
|
14
16
|
from typing_extensions import TypedDict
|
15
17
|
|
18
|
+
from .. import Cls
|
19
|
+
from ..app import App, LocalEntrypoint
|
16
20
|
from ..config import config
|
17
21
|
from ..environments import ensure_env
|
18
22
|
from ..exception import ExecutionError, InvalidError, _CliUserExecutionError
|
19
|
-
from ..functions import Function,
|
23
|
+
from ..functions import Function, _FunctionSpec
|
20
24
|
from ..image import Image
|
21
|
-
from ..
|
22
|
-
from ..
|
23
|
-
from ..
|
24
|
-
from
|
25
|
-
from .
|
25
|
+
from ..output import enable_output
|
26
|
+
from ..runner import deploy_app, interactive_shell, run_app
|
27
|
+
from ..serving import serve_app
|
28
|
+
from ..volume import Volume
|
29
|
+
from .import_refs import import_app, import_function
|
30
|
+
from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
|
26
31
|
|
27
32
|
|
28
33
|
class ParameterMetadata(TypedDict):
|
@@ -53,7 +58,7 @@ class NoParserAvailable(InvalidError):
|
|
53
58
|
pass
|
54
59
|
|
55
60
|
|
56
|
-
def _get_signature(f: Callable, is_method: bool = False) ->
|
61
|
+
def _get_signature(f: Callable[..., Any], is_method: bool = False) -> dict[str, ParameterMetadata]:
|
57
62
|
try:
|
58
63
|
type_hints = get_type_hints(f)
|
59
64
|
except Exception as exc:
|
@@ -64,7 +69,7 @@ def _get_signature(f: Callable, is_method: bool = False) -> Dict[str, ParameterM
|
|
64
69
|
if is_method:
|
65
70
|
self = None # Dummy, doesn't matter
|
66
71
|
f = functools.partial(f, self)
|
67
|
-
signature:
|
72
|
+
signature: dict[str, ParameterMetadata] = {}
|
68
73
|
for param in inspect.signature(f).parameters.values():
|
69
74
|
signature[param.name] = {
|
70
75
|
"name": param.name,
|
@@ -91,7 +96,7 @@ def _get_param_type_as_str(annot: Any) -> str:
|
|
91
96
|
return annot_str
|
92
97
|
|
93
98
|
|
94
|
-
def _add_click_options(func, signature:
|
99
|
+
def _add_click_options(func, signature: dict[str, ParameterMetadata]):
|
95
100
|
"""Adds @click.option based on function signature
|
96
101
|
|
97
102
|
Kind of like typer, but using options instead of positional arguments
|
@@ -118,7 +123,7 @@ def _add_click_options(func, signature: Dict[str, ParameterMetadata]):
|
|
118
123
|
return func
|
119
124
|
|
120
125
|
|
121
|
-
def
|
126
|
+
def _get_clean_app_description(func_ref: str) -> str:
|
122
127
|
# If possible, consider the 'ref' argument the start of the app's args. Everything
|
123
128
|
# before it Modal CLI cruft (eg. `modal run --detach`).
|
124
129
|
try:
|
@@ -128,17 +133,46 @@ def _get_clean_stub_description(func_ref: str) -> str:
|
|
128
133
|
return " ".join(sys.argv)
|
129
134
|
|
130
135
|
|
131
|
-
def
|
132
|
-
|
136
|
+
def _write_local_result(result_path: str, res: Any):
|
137
|
+
if isinstance(res, str):
|
138
|
+
mode = "wt"
|
139
|
+
elif isinstance(res, bytes):
|
140
|
+
mode = "wb"
|
141
|
+
else:
|
142
|
+
res_type = type(res).__name__
|
143
|
+
raise InvalidError(f"Function must return str or bytes when using `--write-result`; got {res_type}.")
|
144
|
+
with open(result_path, mode) as fid:
|
145
|
+
fid.write(res)
|
146
|
+
|
147
|
+
|
148
|
+
def _get_click_command_for_function(app: App, function_tag):
|
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}.*")
|
133
156
|
assert isinstance(function, Function)
|
134
|
-
|
157
|
+
function = typing.cast(Function, function)
|
135
158
|
if function.is_generator:
|
136
159
|
raise InvalidError("`modal run` is not supported for generator functions")
|
137
160
|
|
138
|
-
signature:
|
139
|
-
|
140
|
-
|
141
|
-
|
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)
|
142
176
|
signature = dict(**cls_signature, **fun_signature) # Pool all arguments
|
143
177
|
# TODO(erikbern): assert there's no overlap?
|
144
178
|
else:
|
@@ -146,28 +180,34 @@ def _get_click_command_for_function(stub: Stub, function_tag):
|
|
146
180
|
|
147
181
|
@click.pass_context
|
148
182
|
def f(ctx, **kwargs):
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
183
|
+
show_progress: bool = ctx.obj["show_progress"]
|
184
|
+
with enable_output(show_progress):
|
185
|
+
with run_app(
|
186
|
+
app,
|
187
|
+
detach=ctx.obj["detach"],
|
188
|
+
environment_name=ctx.obj["env"],
|
189
|
+
interactive=ctx.obj["interactive"],
|
190
|
+
):
|
191
|
+
if cls is None:
|
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)
|
202
|
+
|
203
|
+
if result_path := ctx.obj["result_path"]:
|
204
|
+
_write_local_result(result_path, res)
|
165
205
|
|
166
206
|
with_click_options = _add_click_options(f, signature)
|
167
207
|
return click.command(with_click_options)
|
168
208
|
|
169
209
|
|
170
|
-
def _get_click_command_for_local_entrypoint(
|
210
|
+
def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoint):
|
171
211
|
func = entrypoint.info.raw_f
|
172
212
|
isasync = inspect.iscoroutinefunction(func)
|
173
213
|
|
@@ -175,23 +215,28 @@ def _get_click_command_for_local_entrypoint(stub: Stub, entrypoint: LocalEntrypo
|
|
175
215
|
def f(ctx, *args, **kwargs):
|
176
216
|
if ctx.obj["detach"]:
|
177
217
|
print(
|
178
|
-
"Note that running a local entrypoint in detached mode only keeps the last
|
218
|
+
"Note that running a local entrypoint in detached mode only keeps the last "
|
219
|
+
"triggered Modal function alive after the parent process has been killed or disconnected."
|
179
220
|
)
|
180
221
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
222
|
+
show_progress: bool = ctx.obj["show_progress"]
|
223
|
+
with enable_output(show_progress):
|
224
|
+
with run_app(
|
225
|
+
app,
|
226
|
+
detach=ctx.obj["detach"],
|
227
|
+
environment_name=ctx.obj["env"],
|
228
|
+
interactive=ctx.obj["interactive"],
|
229
|
+
):
|
230
|
+
try:
|
231
|
+
if isasync:
|
232
|
+
res = asyncio.run(func(*args, **kwargs))
|
233
|
+
else:
|
234
|
+
res = func(*args, **kwargs)
|
235
|
+
except Exception as exc:
|
236
|
+
raise _CliUserExecutionError(inspect.getsourcefile(func)) from exc
|
237
|
+
|
238
|
+
if result_path := ctx.obj["result_path"]:
|
239
|
+
_write_local_result(result_path, res)
|
195
240
|
|
196
241
|
with_click_options = _add_click_options(f, _get_signature(func))
|
197
242
|
return click.command(with_click_options)
|
@@ -205,14 +250,14 @@ class RunGroup(click.Group):
|
|
205
250
|
ctx.ensure_object(dict)
|
206
251
|
ctx.obj["env"] = ensure_env(ctx.params["env"])
|
207
252
|
function_or_entrypoint = import_function(func_ref, accept_local_entrypoint=True, base_cmd="modal run")
|
208
|
-
|
209
|
-
if
|
210
|
-
|
253
|
+
app: App = function_or_entrypoint.app
|
254
|
+
if app.description is None:
|
255
|
+
app.set_description(_get_clean_app_description(func_ref))
|
211
256
|
if isinstance(function_or_entrypoint, LocalEntrypoint):
|
212
|
-
click_command = _get_click_command_for_local_entrypoint(
|
257
|
+
click_command = _get_click_command_for_local_entrypoint(app, function_or_entrypoint)
|
213
258
|
else:
|
214
259
|
tag = function_or_entrypoint.info.get_tag()
|
215
|
-
click_command = _get_click_command_for_function(
|
260
|
+
click_command = _get_click_command_for_function(app, tag)
|
216
261
|
|
217
262
|
return click_command
|
218
263
|
|
@@ -221,181 +266,239 @@ class RunGroup(click.Group):
|
|
221
266
|
cls=RunGroup,
|
222
267
|
subcommand_metavar="FUNC_REF",
|
223
268
|
)
|
269
|
+
@click.option("-w", "--write-result", help="Write return value (which must be str or bytes) to this local path.")
|
224
270
|
@click.option("-q", "--quiet", is_flag=True, help="Don't show Modal progress indicators.")
|
225
271
|
@click.option("-d", "--detach", is_flag=True, help="Don't stop the app if the local process dies or disconnects.")
|
226
272
|
@click.option("-i", "--interactive", is_flag=True, help="Run the app in interactive mode.")
|
227
273
|
@click.option("-e", "--env", help=ENV_OPTION_HELP, default=None)
|
228
274
|
@click.pass_context
|
229
|
-
def run(ctx, detach, quiet, interactive, env):
|
275
|
+
def run(ctx, write_result, detach, quiet, interactive, env):
|
230
276
|
"""Run a Modal function or local entrypoint.
|
231
277
|
|
232
278
|
`FUNC_REF` should be of the format `{file or module}::{function name}`.
|
233
|
-
Alternatively, you can refer to the function via the
|
279
|
+
Alternatively, you can refer to the function via the app:
|
234
280
|
|
235
|
-
`{file or module}::{
|
281
|
+
`{file or module}::{app variable name}.{function name}`
|
236
282
|
|
237
283
|
**Examples:**
|
238
284
|
|
239
285
|
To run the hello_world function (or local entrypoint) in my_app.py:
|
240
286
|
|
241
|
-
```
|
287
|
+
```
|
242
288
|
modal run my_app.py::hello_world
|
243
289
|
```
|
244
290
|
|
245
|
-
If your module only has a single
|
246
|
-
single local entrypoint (or single function), you can omit the
|
291
|
+
If your module only has a single app called `app` and your app has a
|
292
|
+
single local entrypoint (or single function), you can omit the app and
|
247
293
|
function parts:
|
248
294
|
|
249
|
-
```
|
295
|
+
```
|
250
296
|
modal run my_app.py
|
251
297
|
```
|
252
298
|
|
253
299
|
Instead of pointing to a file, you can also use the Python module path:
|
254
300
|
|
255
|
-
```
|
301
|
+
```
|
256
302
|
modal run my_project.my_app
|
257
303
|
```
|
258
304
|
"""
|
259
305
|
ctx.ensure_object(dict)
|
306
|
+
ctx.obj["result_path"] = write_result
|
260
307
|
ctx.obj["detach"] = detach # if subcommand would be a click command...
|
261
308
|
ctx.obj["show_progress"] = False if quiet else True
|
262
309
|
ctx.obj["interactive"] = interactive
|
263
310
|
|
264
311
|
|
265
312
|
def deploy(
|
266
|
-
|
267
|
-
name: str = typer.Option(
|
313
|
+
app_ref: str = typer.Argument(..., help="Path to a Python file with an app."),
|
314
|
+
name: str = typer.Option("", help="Name of the deployment."),
|
268
315
|
env: str = ENV_OPTION,
|
269
|
-
|
270
|
-
|
271
|
-
),
|
272
|
-
skip_confirm: bool = typer.Option(False, help="Skip public app confirmation dialog."),
|
316
|
+
stream_logs: bool = typer.Option(False, help="Stream logs from the app upon deployment."),
|
317
|
+
tag: str = typer.Option("", help="Tag the deployment with a version."),
|
273
318
|
):
|
274
319
|
# this ensures that `modal.lookup()` without environment specification uses the same env as specified
|
275
320
|
env = ensure_env(env)
|
276
321
|
|
277
|
-
|
322
|
+
app = import_app(app_ref)
|
278
323
|
|
279
324
|
if name is None:
|
280
|
-
name =
|
325
|
+
name = app.name
|
281
326
|
|
282
|
-
|
283
|
-
|
284
|
-
"⚠️ Public apps are a beta feature. ⚠️\n"
|
285
|
-
"Making an app public will allow any user (including from outside your workspace) to look up and use your functions.\n"
|
286
|
-
"Are you sure you want your app to be public?"
|
287
|
-
):
|
288
|
-
return
|
327
|
+
with enable_output():
|
328
|
+
res = deploy_app(app, name=name, environment_name=env or "", tag=tag)
|
289
329
|
|
290
|
-
|
330
|
+
if stream_logs:
|
331
|
+
stream_app_logs(app_id=res.app_id, app_logs_url=res.app_logs_url)
|
291
332
|
|
292
333
|
|
293
334
|
def serve(
|
294
|
-
|
335
|
+
app_ref: str = typer.Argument(..., help="Path to a Python file with an app."),
|
295
336
|
timeout: Optional[float] = None,
|
296
337
|
env: str = ENV_OPTION,
|
297
338
|
):
|
298
|
-
"""Run a web endpoint(s) associated with a Modal
|
339
|
+
"""Run a web endpoint(s) associated with a Modal app and hot-reload code.
|
299
340
|
|
300
341
|
**Examples:**
|
301
342
|
|
302
|
-
```
|
343
|
+
```
|
303
344
|
modal serve hello_world.py
|
304
345
|
```
|
305
346
|
"""
|
306
347
|
env = ensure_env(env)
|
307
348
|
|
308
|
-
|
309
|
-
if
|
310
|
-
|
349
|
+
app = import_app(app_ref)
|
350
|
+
if app.description is None:
|
351
|
+
app.set_description(_get_clean_app_description(app_ref))
|
311
352
|
|
312
|
-
with
|
313
|
-
|
314
|
-
timeout
|
315
|
-
|
316
|
-
timeout
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
353
|
+
with enable_output():
|
354
|
+
with serve_app(app, app_ref, environment_name=env):
|
355
|
+
if timeout is None:
|
356
|
+
timeout = config["serve_timeout"]
|
357
|
+
if timeout is None:
|
358
|
+
timeout = float("inf")
|
359
|
+
while timeout > 0:
|
360
|
+
t = min(timeout, 3600)
|
361
|
+
time.sleep(t)
|
362
|
+
timeout -= t
|
321
363
|
|
322
364
|
|
323
365
|
def shell(
|
324
|
-
|
366
|
+
container_or_function: Optional[str] = typer.Argument(
|
325
367
|
default=None,
|
326
|
-
help=
|
327
|
-
|
368
|
+
help=(
|
369
|
+
"ID of running container, or path to a Python file containing a Modal App."
|
370
|
+
" Can also include a function specifier, like `module.py::func`, if the file defines multiple functions."
|
371
|
+
),
|
372
|
+
metavar="REF",
|
328
373
|
),
|
329
|
-
cmd: str = typer.Option(
|
374
|
+
cmd: str = typer.Option("/bin/bash", "-c", "--cmd", help="Command to run inside the Modal image."),
|
330
375
|
env: str = ENV_OPTION,
|
331
376
|
image: Optional[str] = typer.Option(
|
332
|
-
default=None, help="Container image tag for inside the shell (if not using
|
377
|
+
default=None, help="Container image tag for inside the shell (if not using REF)."
|
333
378
|
),
|
334
|
-
add_python: Optional[str] = typer.Option(default=None, help="Add Python to the image (if not using
|
335
|
-
|
336
|
-
default=None,
|
379
|
+
add_python: Optional[str] = typer.Option(default=None, help="Add Python to the image (if not using REF)."),
|
380
|
+
volume: Optional[list[str]] = typer.Option(
|
381
|
+
default=None,
|
382
|
+
help=(
|
383
|
+
"Name of a `modal.Volume` to mount inside the shell at `/mnt/{name}` (if not using REF)."
|
384
|
+
" Can be used multiple times."
|
385
|
+
),
|
337
386
|
),
|
387
|
+
cpu: Optional[int] = typer.Option(default=None, help="Number of CPUs to allocate to the shell (if not using REF)."),
|
338
388
|
memory: Optional[int] = typer.Option(
|
339
|
-
default=None, help="Memory to allocate for the shell, in MiB (if not using
|
389
|
+
default=None, help="Memory to allocate for the shell, in MiB (if not using REF)."
|
340
390
|
),
|
341
391
|
gpu: Optional[str] = typer.Option(
|
342
392
|
default=None,
|
343
|
-
help="GPUs to request for the shell, if any. Examples are `any`, `a10g`, `a100:4` (if not using
|
393
|
+
help="GPUs to request for the shell, if any. Examples are `any`, `a10g`, `a100:4` (if not using REF).",
|
344
394
|
),
|
345
395
|
cloud: Optional[str] = typer.Option(
|
346
396
|
default=None,
|
347
|
-
help=
|
397
|
+
help=(
|
398
|
+
"Cloud provider to run the shell on. " "Possible values are `aws`, `gcp`, `oci`, `auto` (if not using REF)."
|
399
|
+
),
|
400
|
+
),
|
401
|
+
region: Optional[str] = typer.Option(
|
402
|
+
default=None,
|
403
|
+
help=(
|
404
|
+
"Region(s) to run the container on. "
|
405
|
+
"Can be a single region or a comma-separated list to choose from (if not using REF)."
|
406
|
+
),
|
348
407
|
),
|
408
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
349
409
|
):
|
350
|
-
"""Run
|
410
|
+
"""Run a command or interactive shell inside a Modal container.
|
351
411
|
|
352
412
|
**Examples:**
|
353
413
|
|
354
|
-
Start
|
414
|
+
Start an interactive shell inside the default Debian-based image:
|
355
415
|
|
356
|
-
```
|
416
|
+
```
|
357
417
|
modal shell
|
358
418
|
```
|
359
419
|
|
360
|
-
Start
|
420
|
+
Start an interactive shell with the spec for `my_function` in your App
|
421
|
+
(uses the same image, volumes, mounts, etc.):
|
361
422
|
|
362
|
-
```
|
423
|
+
```
|
363
424
|
modal shell hello_world.py::my_function
|
364
425
|
```
|
365
426
|
|
427
|
+
Or, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
|
428
|
+
|
429
|
+
```
|
430
|
+
modal shell hello_world.py::MyClass.my_method
|
431
|
+
```
|
432
|
+
|
366
433
|
Start a `python` shell:
|
367
434
|
|
368
|
-
```
|
435
|
+
```
|
369
436
|
modal shell hello_world.py --cmd=python
|
370
437
|
```
|
438
|
+
|
439
|
+
Run a command with your function's spec and pipe the output to a file:
|
440
|
+
|
441
|
+
```
|
442
|
+
modal shell hello_world.py -c 'uv pip list' > env.txt
|
443
|
+
```
|
371
444
|
"""
|
372
445
|
env = ensure_env(env)
|
373
446
|
|
374
|
-
|
375
|
-
|
376
|
-
|
447
|
+
if pty is None:
|
448
|
+
pty = is_tty()
|
449
|
+
|
450
|
+
if platform.system() == "Windows":
|
451
|
+
raise InvalidError("`modal shell` is currently not supported on Windows")
|
452
|
+
|
453
|
+
app = App("modal shell")
|
454
|
+
|
455
|
+
if container_or_function is not None:
|
456
|
+
# `modal shell` with a container ID is a special case, alias for `modal container exec`.
|
457
|
+
if (
|
458
|
+
container_or_function.startswith("ta-")
|
459
|
+
and len(container_or_function[3:]) > 0
|
460
|
+
and container_or_function[3:].isalnum()
|
461
|
+
):
|
462
|
+
from .container import exec
|
377
463
|
|
378
|
-
|
464
|
+
exec(container_id=container_or_function, command=shlex.split(cmd), pty=pty)
|
465
|
+
return
|
379
466
|
|
380
|
-
|
381
|
-
|
467
|
+
function = import_function(
|
468
|
+
container_or_function, accept_local_entrypoint=False, accept_webhook=True, base_cmd="modal shell"
|
469
|
+
)
|
382
470
|
assert isinstance(function, Function)
|
383
|
-
|
471
|
+
function_spec: _FunctionSpec = function.spec
|
384
472
|
start_shell = partial(
|
385
473
|
interactive_shell,
|
386
|
-
image=
|
387
|
-
mounts=
|
388
|
-
secrets=
|
389
|
-
network_file_systems=
|
390
|
-
gpu=
|
391
|
-
cloud=
|
392
|
-
cpu=
|
393
|
-
memory=
|
394
|
-
volumes=
|
395
|
-
|
474
|
+
image=function_spec.image,
|
475
|
+
mounts=function_spec.mounts,
|
476
|
+
secrets=function_spec.secrets,
|
477
|
+
network_file_systems=function_spec.network_file_systems,
|
478
|
+
gpu=function_spec.gpus,
|
479
|
+
cloud=function_spec.cloud,
|
480
|
+
cpu=function_spec.cpu,
|
481
|
+
memory=function_spec.memory,
|
482
|
+
volumes=function_spec.volumes,
|
483
|
+
region=function_spec.scheduler_placement.proto.regions if function_spec.scheduler_placement else None,
|
484
|
+
pty=pty,
|
485
|
+
proxy=function_spec.proxy,
|
396
486
|
)
|
397
487
|
else:
|
398
488
|
modal_image = Image.from_registry(image, add_python=add_python) if image else None
|
399
|
-
|
489
|
+
volumes = {} if volume is None else {f"/mnt/{vol}": Volume.from_name(vol) for vol in volume}
|
490
|
+
start_shell = partial(
|
491
|
+
interactive_shell,
|
492
|
+
image=modal_image,
|
493
|
+
cpu=cpu,
|
494
|
+
memory=memory,
|
495
|
+
gpu=gpu,
|
496
|
+
cloud=cloud,
|
497
|
+
volumes=volumes,
|
498
|
+
region=region.split(",") if region else [],
|
499
|
+
pty=pty,
|
500
|
+
)
|
400
501
|
|
401
|
-
|
502
|
+
# NB: invoking under bash makes --cmd a lot more flexible.
|
503
|
+
cmds = shlex.split(f'/bin/bash -c "{cmd}"')
|
504
|
+
start_shell(app, cmds=cmds, environment_name=env, timeout=3600)
|
modal/cli/secret.py
CHANGED
@@ -3,7 +3,7 @@ import os
|
|
3
3
|
import platform
|
4
4
|
import subprocess
|
5
5
|
from tempfile import NamedTemporaryFile
|
6
|
-
from typing import
|
6
|
+
from typing import Optional
|
7
7
|
|
8
8
|
import click
|
9
9
|
import typer
|
@@ -23,7 +23,7 @@ secret_cli = typer.Typer(name="secret", help="Manage secrets.", no_args_is_help=
|
|
23
23
|
|
24
24
|
@secret_cli.command("list", help="List your published secrets.")
|
25
25
|
@synchronizer.create_blocking
|
26
|
-
async def
|
26
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
27
27
|
env = ensure_env(env)
|
28
28
|
client = await _Client.from_env()
|
29
29
|
response = await retry_transient_errors(client.stub.SecretList, api_pb2.SecretListRequest(environment_name=env))
|
@@ -34,8 +34,8 @@ async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
34
34
|
rows.append(
|
35
35
|
[
|
36
36
|
item.label,
|
37
|
-
timestamp_to_local(item.created_at),
|
38
|
-
timestamp_to_local(item.last_used_at) if item.last_used_at else "-",
|
37
|
+
timestamp_to_local(item.created_at, json),
|
38
|
+
timestamp_to_local(item.last_used_at, json) if item.last_used_at else "-",
|
39
39
|
]
|
40
40
|
)
|
41
41
|
|
@@ -47,7 +47,7 @@ async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
47
47
|
@synchronizer.create_blocking
|
48
48
|
async def create(
|
49
49
|
secret_name,
|
50
|
-
keyvalues:
|
50
|
+
keyvalues: list[str] = typer.Argument(..., help="Space-separated KEY=VALUE items"),
|
51
51
|
env: Optional[str] = ENV_OPTION,
|
52
52
|
force: bool = typer.Option(False, "--force", help="Overwrite the secret if it already exists."),
|
53
53
|
):
|
@@ -79,7 +79,7 @@ modal secret create my-credentials username=john password=-
|
|
79
79
|
console = Console()
|
80
80
|
env_var_code = "\n ".join(f'os.getenv("{name}")' for name in env_dict.keys()) if env_dict else "..."
|
81
81
|
example_code = f"""
|
82
|
-
@
|
82
|
+
@app.function(secrets=[modal.Secret.from_name("{secret_name}")])
|
83
83
|
def some_function():
|
84
84
|
{env_var_code}
|
85
85
|
"""
|
@@ -105,7 +105,8 @@ def get_text_from_editor(key) -> str:
|
|
105
105
|
|
106
106
|
if status_code != 0:
|
107
107
|
raise ValueError(
|
108
|
-
"Something went wrong with the external editor.
|
108
|
+
"Something went wrong with the external editor. "
|
109
|
+
"Try again, or use '--' as the value to pass input through stdin instead"
|
109
110
|
)
|
110
111
|
|
111
112
|
bufferfile.seek(0)
|
modal/cli/token.py
CHANGED
@@ -12,7 +12,9 @@ token_cli = typer.Typer(name="token", help="Manage tokens.", no_args_is_help=Tru
|
|
12
12
|
profile_option = typer.Option(
|
13
13
|
None,
|
14
14
|
help=(
|
15
|
-
"Modal profile to set credentials for. If unspecified
|
15
|
+
"Modal profile to set credentials for. If unspecified "
|
16
|
+
"(and MODAL_PROFILE environment variable is not set), "
|
17
|
+
"uses the workspace name associated with the credentials."
|
16
18
|
),
|
17
19
|
)
|
18
20
|
activate_option = typer.Option(
|
@@ -28,7 +30,10 @@ verify_option = typer.Option(
|
|
28
30
|
|
29
31
|
@token_cli.command(
|
30
32
|
name="set",
|
31
|
-
help=
|
33
|
+
help=(
|
34
|
+
"Set account credentials for connecting to Modal. "
|
35
|
+
"If not provided with the command, you will be prompted to enter your credentials."
|
36
|
+
),
|
32
37
|
)
|
33
38
|
@synchronizer.create_blocking
|
34
39
|
async def set(
|