modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +4 -2
- modal/_container_entrypoint.py +41 -49
- modal/_functions.py +424 -195
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +68 -20
- modal/_output.py +58 -45
- modal/_partial_function.py +36 -11
- modal/_pty.py +7 -3
- modal/_resolver.py +21 -35
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +301 -186
- modal/_runtime/container_io_manager.pyi +70 -61
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +170 -63
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +57 -1
- modal/_utils/async_utils.py +33 -12
- modal/_utils/auth_token_manager.py +2 -5
- modal/_utils/blob_utils.py +110 -53
- modal/_utils/function_utils.py +49 -42
- modal/_utils/grpc_utils.py +80 -50
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +219 -83
- modal/app.pyi +229 -56
- modal/billing.py +5 -0
- modal/{requirements → builder}/2025.06.txt +1 -0
- modal/{requirements → builder}/PREVIEW.txt +1 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +9 -13
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +58 -16
- modal/cli/secret.py +48 -22
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +28 -25
- modal/client.py +13 -116
- modal/client.pyi +9 -91
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +5 -1
- modal/cls.py +130 -102
- modal/cls.pyi +45 -85
- modal/config.py +29 -10
- modal/container_process.py +291 -13
- modal/container_process.pyi +95 -32
- modal/dict.py +282 -63
- modal/dict.pyi +423 -73
- modal/environments.py +15 -27
- modal/environments.pyi +5 -15
- modal/exception.py +8 -0
- modal/experimental/__init__.py +143 -38
- modal/experimental/flash.py +247 -78
- modal/experimental/flash.pyi +137 -9
- modal/file_io.py +14 -28
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +134 -61
- modal/image.py +255 -86
- modal/image.pyi +300 -62
- modal/io_streams.py +436 -126
- modal/io_streams.pyi +236 -171
- modal/mount.py +62 -157
- modal/mount.pyi +45 -172
- modal/network_file_system.py +30 -53
- modal/network_file_system.pyi +16 -76
- modal/object.pyi +42 -8
- modal/parallel_map.py +821 -113
- modal/parallel_map.pyi +134 -0
- modal/partial_function.pyi +4 -1
- modal/proxy.py +16 -7
- modal/proxy.pyi +10 -2
- modal/queue.py +263 -61
- modal/queue.pyi +409 -66
- modal/runner.py +112 -92
- modal/runner.pyi +45 -27
- modal/sandbox.py +451 -124
- modal/sandbox.pyi +513 -67
- modal/secret.py +291 -67
- modal/secret.pyi +425 -19
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/token_flow.py +4 -4
- modal/volume.py +344 -98
- modal/volume.pyi +464 -68
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +399 -67
- modal_proto/api_grpc.py +241 -1
- modal_proto/api_pb2.py +1395 -1000
- modal_proto/api_pb2.pyi +1239 -79
- modal_proto/api_pb2_grpc.py +499 -4
- modal_proto/api_pb2_grpc.pyi +162 -14
- modal_proto/modal_api_grpc.py +175 -160
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal-1.0.6.dev58.dist-info/RECORD +0 -183
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/cli/app.py
CHANGED
|
@@ -15,7 +15,7 @@ from modal.client import _Client
|
|
|
15
15
|
from modal.environments import ensure_env
|
|
16
16
|
from modal_proto import api_pb2
|
|
17
17
|
|
|
18
|
-
from .._utils.time_utils import
|
|
18
|
+
from .._utils.time_utils import timestamp_to_localized_str
|
|
19
19
|
from .utils import ENV_OPTION, display_table, get_app_id_from_name, stream_app_logs
|
|
20
20
|
|
|
21
21
|
APP_IDENTIFIER = Argument("", help="App name or ID")
|
|
@@ -71,8 +71,8 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
71
71
|
app_stats.description,
|
|
72
72
|
state,
|
|
73
73
|
str(app_stats.n_running_tasks),
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
timestamp_to_localized_str(app_stats.created_at, json),
|
|
75
|
+
timestamp_to_localized_str(app_stats.stopped_at, json),
|
|
76
76
|
]
|
|
77
77
|
)
|
|
78
78
|
|
|
@@ -217,7 +217,7 @@ async def history(
|
|
|
217
217
|
|
|
218
218
|
row = [
|
|
219
219
|
Text(f"v{app_stats.version}", style=style),
|
|
220
|
-
Text(
|
|
220
|
+
Text(timestamp_to_localized_str(app_stats.deployed_at, json), style=style),
|
|
221
221
|
Text(app_stats.client_version, style=style),
|
|
222
222
|
Text(app_stats.deployed_by, style=style),
|
|
223
223
|
]
|
modal/cli/cluster.py
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
from typing import Optional, Union
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.
|
|
5
|
+
from rich.table import Column
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
8
8
|
from modal._object import _get_environment_name
|
|
9
|
+
from modal._output import make_console
|
|
9
10
|
from modal._pty import get_pty_info
|
|
10
11
|
from modal._utils.async_utils import synchronizer
|
|
11
|
-
from modal._utils.time_utils import
|
|
12
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
13
|
from modal.cli.utils import ENV_OPTION, display_table, is_tty
|
|
13
14
|
from modal.client import _Client
|
|
14
15
|
from modal.config import config
|
|
@@ -33,7 +34,12 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
33
34
|
api_pb2.ClusterListRequest(environment_name=environment_name)
|
|
34
35
|
)
|
|
35
36
|
|
|
36
|
-
column_names
|
|
37
|
+
column_names: list[Union[Column, str]] = [
|
|
38
|
+
Column("Cluster ID", min_width=25),
|
|
39
|
+
Column("App ID", min_width=25),
|
|
40
|
+
"Start Time",
|
|
41
|
+
"Nodes",
|
|
42
|
+
]
|
|
37
43
|
rows: list[list[Union[Text, str]]] = []
|
|
38
44
|
res.clusters.sort(key=lambda c: c.started_at, reverse=True)
|
|
39
45
|
|
|
@@ -42,7 +48,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
42
48
|
[
|
|
43
49
|
c.cluster_id,
|
|
44
50
|
c.app_id,
|
|
45
|
-
|
|
51
|
+
timestamp_to_localized_str(c.started_at, json) if c.started_at else "Pending",
|
|
46
52
|
str(len(c.task_ids)),
|
|
47
53
|
]
|
|
48
54
|
)
|
|
@@ -62,7 +68,7 @@ async def shell(
|
|
|
62
68
|
if len(res.cluster.task_ids) <= rank:
|
|
63
69
|
raise typer.Abort(f"No node with rank {rank} in cluster {cluster_id}")
|
|
64
70
|
task_id = res.cluster.task_ids[rank]
|
|
65
|
-
console =
|
|
71
|
+
console = make_console()
|
|
66
72
|
is_main = "(main)" if rank == 0 else ""
|
|
67
73
|
console.print(
|
|
68
74
|
f"Opening shell to node {rank} {is_main} of cluster {cluster_id} (container {task_id})", style="green"
|
|
@@ -77,7 +83,9 @@ async def shell(
|
|
|
77
83
|
)
|
|
78
84
|
exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
|
79
85
|
if pty:
|
|
80
|
-
await _ContainerProcess(exec_res.exec_id, client).attach()
|
|
86
|
+
await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
|
|
81
87
|
else:
|
|
82
88
|
# TODO: redirect stderr to its own stream?
|
|
83
|
-
await _ContainerProcess(
|
|
89
|
+
await _ContainerProcess(
|
|
90
|
+
exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
91
|
+
).wait()
|
modal/cli/config.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
|
+
import json
|
|
3
|
+
|
|
2
4
|
import typer
|
|
3
|
-
from rich.console import Console
|
|
4
5
|
|
|
6
|
+
from modal._output import make_console
|
|
5
7
|
from modal.config import _profile, _store_user_config, config
|
|
6
8
|
from modal.environments import Environment
|
|
7
9
|
|
|
@@ -24,8 +26,8 @@ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value
|
|
|
24
26
|
if redact and config_dict.get("token_secret"):
|
|
25
27
|
config_dict["token_secret"] = "***"
|
|
26
28
|
|
|
27
|
-
console =
|
|
28
|
-
console.
|
|
29
|
+
console = make_console()
|
|
30
|
+
console.print_json(json.dumps(config_dict))
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
|
modal/cli/container.py
CHANGED
|
@@ -7,8 +7,7 @@ from rich.text import Text
|
|
|
7
7
|
from modal._object import _get_environment_name
|
|
8
8
|
from modal._pty import get_pty_info
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
|
-
from modal._utils.
|
|
11
|
-
from modal._utils.time_utils import timestamp_to_local
|
|
10
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
11
|
from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs
|
|
13
12
|
from modal.client import _Client
|
|
14
13
|
from modal.config import config
|
|
@@ -40,7 +39,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
40
39
|
task_stats.task_id,
|
|
41
40
|
task_stats.app_id,
|
|
42
41
|
task_stats.app_description,
|
|
43
|
-
|
|
42
|
+
timestamp_to_localized_str(task_stats.started_at, json) if task_stats.started_at else "Pending",
|
|
44
43
|
]
|
|
45
44
|
)
|
|
46
45
|
|
|
@@ -80,10 +79,12 @@ async def exec(
|
|
|
80
79
|
res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
|
81
80
|
|
|
82
81
|
if pty:
|
|
83
|
-
await _ContainerProcess(res.exec_id, client).attach()
|
|
82
|
+
await _ContainerProcess(res.exec_id, container_id, client).attach()
|
|
84
83
|
else:
|
|
85
84
|
# TODO: redirect stderr to its own stream?
|
|
86
|
-
await _ContainerProcess(
|
|
85
|
+
await _ContainerProcess(
|
|
86
|
+
res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
87
|
+
).wait()
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
@container_cli.command("stop")
|
|
@@ -95,4 +96,4 @@ async def stop(container_id: str = typer.Argument(help="Container ID")):
|
|
|
95
96
|
"""
|
|
96
97
|
client = await _Client.from_env()
|
|
97
98
|
request = api_pb2.ContainerStopRequest(task_id=container_id)
|
|
98
|
-
await
|
|
99
|
+
await client.stub.ContainerStop(request)
|
modal/cli/dict.py
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
5
|
from typer import Argument, Option, Typer
|
|
7
6
|
|
|
7
|
+
from modal._load_context import LoadContext
|
|
8
|
+
from modal._output import make_console
|
|
8
9
|
from modal._resolver import Resolver
|
|
9
10
|
from modal._utils.async_utils import synchronizer
|
|
10
|
-
from modal._utils.
|
|
11
|
-
from modal._utils.time_utils import timestamp_to_local
|
|
11
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
12
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.dict import _Dict
|
|
15
15
|
from modal.environments import ensure_env
|
|
16
|
-
from modal_proto import api_pb2
|
|
17
16
|
|
|
18
17
|
dict_cli = Typer(
|
|
19
18
|
name="dict",
|
|
@@ -31,8 +30,10 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
31
30
|
"""
|
|
32
31
|
d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
|
|
33
32
|
client = await _Client.from_env()
|
|
34
|
-
resolver = Resolver(
|
|
35
|
-
|
|
33
|
+
resolver = Resolver()
|
|
34
|
+
|
|
35
|
+
load_context = LoadContext(client=client, environment_name=env)
|
|
36
|
+
await resolver.load(d, load_context)
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dict_cli.command(name="list", rich_help_panel="Management")
|
|
@@ -40,12 +41,13 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
40
41
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
|
41
42
|
"""List all named Dicts."""
|
|
42
43
|
env = ensure_env(env)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
dicts = await _Dict.objects.list(environment_name=env)
|
|
45
|
+
rows = []
|
|
46
|
+
for obj in dicts:
|
|
47
|
+
info = await obj.info()
|
|
48
|
+
rows.append((info.name, timestamp_to_localized_str(info.created_at.timestamp(), json), info.created_by))
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
display_table(["Name", "Created at"], rows, json)
|
|
50
|
+
display_table(["Name", "Created at", "Created by"], rows, json)
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
@dict_cli.command("clear", rich_help_panel="Management")
|
|
@@ -64,17 +66,21 @@ async def clear(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_O
|
|
|
64
66
|
|
|
65
67
|
@dict_cli.command(name="delete", rich_help_panel="Management")
|
|
66
68
|
@synchronizer.create_blocking
|
|
67
|
-
async def delete(
|
|
69
|
+
async def delete(
|
|
70
|
+
name: str,
|
|
71
|
+
*,
|
|
72
|
+
allow_missing: bool = Option(False, "--allow-missing", help="Don't error if the Dict doesn't exist."),
|
|
73
|
+
yes: bool = YES_OPTION,
|
|
74
|
+
env: Optional[str] = ENV_OPTION,
|
|
75
|
+
):
|
|
68
76
|
"""Delete a named Dict and all of its data."""
|
|
69
|
-
# Lookup first to validate the name, even though delete is a staticmethod
|
|
70
|
-
await _Dict.from_name(name, environment_name=env).hydrate()
|
|
71
77
|
if not yes:
|
|
72
78
|
typer.confirm(
|
|
73
79
|
f"Are you sure you want to irrevocably delete the modal.Dict '{name}'?",
|
|
74
80
|
default=False,
|
|
75
81
|
abort=True,
|
|
76
82
|
)
|
|
77
|
-
await _Dict.delete(name, environment_name=env)
|
|
83
|
+
await _Dict.objects.delete(name, environment_name=env, allow_missing=allow_missing)
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
@dict_cli.command(name="get", rich_help_panel="Inspection")
|
|
@@ -85,7 +91,7 @@ async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
85
91
|
Note: When using the CLI, keys are always interpreted as having a string type.
|
|
86
92
|
"""
|
|
87
93
|
d = _Dict.from_name(name, environment_name=env)
|
|
88
|
-
console =
|
|
94
|
+
console = make_console()
|
|
89
95
|
val = await d.get(key)
|
|
90
96
|
console.print(val)
|
|
91
97
|
|
modal/cli/entry_point.py
CHANGED
|
@@ -3,9 +3,9 @@ import subprocess
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
6
|
from rich.rule import Rule
|
|
8
7
|
|
|
8
|
+
from modal._output import make_console
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
|
|
11
11
|
from . import run
|
|
@@ -33,9 +33,10 @@ def version_callback(value: bool):
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
entrypoint_cli_typer = typer.Typer(
|
|
36
|
-
no_args_is_help=
|
|
36
|
+
no_args_is_help=False,
|
|
37
37
|
add_completion=False,
|
|
38
38
|
rich_markup_mode="markdown",
|
|
39
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
39
40
|
help="""
|
|
40
41
|
Modal is the fastest way to run code in the cloud.
|
|
41
42
|
|
|
@@ -45,12 +46,18 @@ entrypoint_cli_typer = typer.Typer(
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
@entrypoint_cli_typer.callback()
|
|
49
|
+
@entrypoint_cli_typer.callback(invoke_without_command=True)
|
|
49
50
|
def modal(
|
|
50
51
|
ctx: typer.Context,
|
|
51
52
|
version: bool = typer.Option(None, "--version", callback=version_callback),
|
|
52
53
|
):
|
|
53
|
-
|
|
54
|
+
# TODO: When https://github.com/fastapi/typer/pull/1240 gets shipped, then
|
|
55
|
+
# - set invoke_without_command=False in the callback decorator
|
|
56
|
+
# - set no_args_is_help=True in entrypoint_cli_typer
|
|
57
|
+
if ctx.invoked_subcommand is None:
|
|
58
|
+
console = make_console()
|
|
59
|
+
console.print(ctx.get_help())
|
|
60
|
+
raise typer.Exit()
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
def check_path():
|
|
@@ -71,7 +78,7 @@ def check_path():
|
|
|
71
78
|
"You may need to give it permissions or use `[white]python -m modal[/white]` as a workaround.[/red]\n"
|
|
72
79
|
)
|
|
73
80
|
text += f"See more information here:\n\n[link={url}]{url}[/link]\n"
|
|
74
|
-
console =
|
|
81
|
+
console = make_console()
|
|
75
82
|
console.print(text)
|
|
76
83
|
console.print(Rule(style="white"))
|
|
77
84
|
|
modal/cli/environment.py
CHANGED
|
@@ -8,7 +8,7 @@ from rich.text import Text
|
|
|
8
8
|
|
|
9
9
|
from modal import environments
|
|
10
10
|
from modal._utils.name_utils import check_environment_name
|
|
11
|
-
from modal.cli.utils import display_table
|
|
11
|
+
from modal.cli.utils import YES_OPTION, display_table
|
|
12
12
|
from modal.config import config
|
|
13
13
|
from modal.exception import InvalidError
|
|
14
14
|
|
|
@@ -80,13 +80,14 @@ Deletes all apps in the selected environment and deletes the environment irrevoc
|
|
|
80
80
|
@environment_cli.command(name="delete", help=ENVIRONMENT_DELETE_HELP)
|
|
81
81
|
def delete(
|
|
82
82
|
name: str = typer.Argument(help="Name of the environment to be deleted. Case sensitive"),
|
|
83
|
-
|
|
83
|
+
*,
|
|
84
|
+
yes: bool = YES_OPTION,
|
|
84
85
|
):
|
|
85
|
-
if not
|
|
86
|
+
if not yes:
|
|
86
87
|
typer.confirm(
|
|
87
88
|
(
|
|
88
89
|
f"Are you sure you want to irrevocably delete the environment '{name}' and"
|
|
89
|
-
" all its associated
|
|
90
|
+
" all its associated Apps, Secrets, Volumes, Dicts and Queues?"
|
|
90
91
|
),
|
|
91
92
|
default=False,
|
|
92
93
|
abort=True,
|
modal/cli/import_refs.py
CHANGED
|
@@ -19,9 +19,9 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Optional, Union, cast
|
|
20
20
|
|
|
21
21
|
import click
|
|
22
|
-
from rich.console import Console
|
|
23
22
|
from rich.markdown import Markdown
|
|
24
23
|
|
|
24
|
+
from modal._output import make_console
|
|
25
25
|
from modal._utils.deprecation import deprecation_warning
|
|
26
26
|
from modal.app import App, LocalEntrypoint
|
|
27
27
|
from modal.cls import Cls
|
|
@@ -258,7 +258,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
|
|
|
258
258
|
app = getattr(module, object_path)
|
|
259
259
|
|
|
260
260
|
if app is None:
|
|
261
|
-
error_console =
|
|
261
|
+
error_console = make_console(stderr=True)
|
|
262
262
|
error_console.print(f"[bold red]Could not find Modal app '{object_path}' in {import_path}.[/bold red]")
|
|
263
263
|
|
|
264
264
|
if not object_path:
|
|
@@ -282,7 +282,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
|
|
|
282
282
|
def _show_function_ref_help(app_ref: ImportRef, base_cmd: str) -> None:
|
|
283
283
|
object_path = app_ref.object_path
|
|
284
284
|
import_path = app_ref.file_or_module
|
|
285
|
-
error_console =
|
|
285
|
+
error_console = make_console(stderr=True)
|
|
286
286
|
if object_path:
|
|
287
287
|
error_console.print(
|
|
288
288
|
f"[bold red]Could not find Modal function or local entrypoint"
|
modal/cli/launch.py
CHANGED
|
@@ -3,11 +3,16 @@ import asyncio
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Any, Optional
|
|
8
10
|
|
|
11
|
+
import rich.panel
|
|
12
|
+
from rich.markdown import Markdown
|
|
9
13
|
from typer import Typer
|
|
10
14
|
|
|
15
|
+
from .._output import make_console
|
|
11
16
|
from ..exception import _CliUserExecutionError
|
|
12
17
|
from ..output import enable_output
|
|
13
18
|
from ..runner import run_app
|
|
@@ -16,15 +21,24 @@ from .import_refs import ImportRef, _get_runnable_app, import_file_or_module
|
|
|
16
21
|
launch_cli = Typer(
|
|
17
22
|
name="launch",
|
|
18
23
|
no_args_is_help=True,
|
|
24
|
+
rich_markup_mode="markdown",
|
|
19
25
|
help="""
|
|
20
|
-
Open a serverless app instance on Modal.
|
|
21
|
-
|
|
22
|
-
This command is in preview and may change in the future.
|
|
26
|
+
[Experimental] Open a serverless app instance on Modal.
|
|
23
27
|
""",
|
|
24
28
|
)
|
|
25
29
|
|
|
26
30
|
|
|
27
|
-
def _launch_program(
|
|
31
|
+
def _launch_program(
|
|
32
|
+
name: str, filename: str, detach: bool, args: dict[str, Any], *, description: Optional[str] = None
|
|
33
|
+
) -> None:
|
|
34
|
+
console = make_console()
|
|
35
|
+
console.print(
|
|
36
|
+
rich.panel.Panel(
|
|
37
|
+
Markdown(f"⚠️ `modal launch {name}` is **experimental** and may change in the future."),
|
|
38
|
+
border_style="yellow",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
28
42
|
os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
|
|
29
43
|
|
|
30
44
|
program_path = str(Path(__file__).parent / "programs" / filename)
|
|
@@ -33,7 +47,7 @@ def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]
|
|
|
33
47
|
entrypoint = module.main
|
|
34
48
|
|
|
35
49
|
app = _get_runnable_app(entrypoint)
|
|
36
|
-
app.set_description(base_cmd)
|
|
50
|
+
app.set_description(description if description else base_cmd)
|
|
37
51
|
|
|
38
52
|
# `launch/` scripts must have a `local_entrypoint()` with no args, for simplicity here.
|
|
39
53
|
func = entrypoint.info.raw_f
|
|
@@ -61,6 +75,17 @@ def jupyter(
|
|
|
61
75
|
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
|
62
76
|
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
|
63
77
|
):
|
|
78
|
+
console = make_console()
|
|
79
|
+
console.print(
|
|
80
|
+
rich.panel.Panel(
|
|
81
|
+
(
|
|
82
|
+
"[link=https://modal.com/notebooks]Try Modal Notebooks! "
|
|
83
|
+
"modal.com/notebooks[/link]\n"
|
|
84
|
+
"Notebooks have a new UI, saved content, real-time collaboration and more."
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
style="bold cyan",
|
|
88
|
+
)
|
|
64
89
|
args = {
|
|
65
90
|
"cpu": cpu,
|
|
66
91
|
"memory": memory,
|
|
@@ -95,3 +120,75 @@ def vscode(
|
|
|
95
120
|
"volume": volume,
|
|
96
121
|
}
|
|
97
122
|
_launch_program("vscode", "vscode.py", detach, args)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@launch_cli.command(name="machine", help="Start an instance on Modal, with direct SSH access.", hidden=True)
|
|
126
|
+
def machine(
|
|
127
|
+
name: str, # Name of the machine App.
|
|
128
|
+
cpu: int = 8, # Reservation of CPU cores (can burst above this value).
|
|
129
|
+
memory: int = 32768, # Reservation of memory in MiB (can burst above this value).
|
|
130
|
+
gpu: Optional[str] = None, # GPU type and count, e.g. "t4" or "h100:2".
|
|
131
|
+
image: Optional[str] = None, # Image tag to use from registry. Defaults to the notebook base image.
|
|
132
|
+
timeout: int = 3600 * 24, # Timeout in seconds for the instance.
|
|
133
|
+
volume: str = "machine-vol", # Attach a persisted `modal.Volume` at /workspace (created if missing).
|
|
134
|
+
):
|
|
135
|
+
tempdir = Path(tempfile.gettempdir())
|
|
136
|
+
key_path = tempdir / "modal-machine-keyfile.pem"
|
|
137
|
+
# Generate a new SSH key pair for this machine instance.
|
|
138
|
+
if not key_path.exists():
|
|
139
|
+
subprocess.run(
|
|
140
|
+
["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", ""],
|
|
141
|
+
check=True,
|
|
142
|
+
stdout=subprocess.DEVNULL,
|
|
143
|
+
)
|
|
144
|
+
# Add the key with expiry 1d to ssh agent.
|
|
145
|
+
subprocess.run(
|
|
146
|
+
["ssh-add", "-t", "1d", str(key_path)],
|
|
147
|
+
check=True,
|
|
148
|
+
stdout=subprocess.DEVNULL,
|
|
149
|
+
stderr=subprocess.DEVNULL,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
os.environ["SSH_PUBLIC_KEY"] = Path(str(key_path) + ".pub").read_text()
|
|
153
|
+
os.environ["MODAL_LOGS_TIMEOUT"] = "0" # hack to work with --detach
|
|
154
|
+
|
|
155
|
+
args = {
|
|
156
|
+
"cpu": cpu,
|
|
157
|
+
"memory": memory,
|
|
158
|
+
"gpu": gpu,
|
|
159
|
+
"image": image,
|
|
160
|
+
"timeout": timeout,
|
|
161
|
+
"volume": volume,
|
|
162
|
+
}
|
|
163
|
+
_launch_program(
|
|
164
|
+
"machine",
|
|
165
|
+
"launch_instance_ssh.py",
|
|
166
|
+
True,
|
|
167
|
+
args,
|
|
168
|
+
description=name,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@launch_cli.command(name="marimo", help="Start a remote Marimo notebook on Modal.", hidden=True)
|
|
173
|
+
def marimo(
|
|
174
|
+
cpu: int = 8,
|
|
175
|
+
memory: int = 32768,
|
|
176
|
+
gpu: Optional[str] = None,
|
|
177
|
+
image: str = "debian:12",
|
|
178
|
+
timeout: int = 3600,
|
|
179
|
+
add_python: Optional[str] = "3.12",
|
|
180
|
+
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
|
181
|
+
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
|
182
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
|
183
|
+
):
|
|
184
|
+
args = {
|
|
185
|
+
"cpu": cpu,
|
|
186
|
+
"memory": memory,
|
|
187
|
+
"gpu": gpu,
|
|
188
|
+
"timeout": timeout,
|
|
189
|
+
"image": image,
|
|
190
|
+
"add_python": add_python,
|
|
191
|
+
"mount": mount,
|
|
192
|
+
"volume": volume,
|
|
193
|
+
}
|
|
194
|
+
_launch_program("marimo", "run_marimo.py", detach, args)
|
modal/cli/network_file_system.py
CHANGED
|
@@ -7,17 +7,15 @@ from typing import Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from click import UsageError
|
|
9
9
|
from grpclib import GRPCError, Status
|
|
10
|
-
from rich.console import Console
|
|
11
10
|
from rich.syntax import Syntax
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
from typer import Argument, Typer
|
|
14
13
|
|
|
15
14
|
import modal
|
|
16
15
|
from modal._location import display_location
|
|
17
|
-
from modal._output import OutputManager, ProgressHandler
|
|
16
|
+
from modal._output import OutputManager, ProgressHandler, make_console
|
|
18
17
|
from modal._utils.async_utils import synchronizer
|
|
19
|
-
from modal._utils.
|
|
20
|
-
from modal._utils.time_utils import timestamp_to_local
|
|
18
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
21
19
|
from modal.cli._download import _volume_download
|
|
22
20
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
23
21
|
from modal.client import _Client
|
|
@@ -34,9 +32,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
|
34
32
|
env = ensure_env(env)
|
|
35
33
|
|
|
36
34
|
client = await _Client.from_env()
|
|
37
|
-
response = await
|
|
38
|
-
client.stub.SharedVolumeList, api_pb2.SharedVolumeListRequest(environment_name=env)
|
|
39
|
-
)
|
|
35
|
+
response = await client.stub.SharedVolumeList(api_pb2.SharedVolumeListRequest(environment_name=env))
|
|
40
36
|
env_part = f" in environment '{env}'" if env else ""
|
|
41
37
|
column_names = ["Name", "Location", "Created at"]
|
|
42
38
|
rows = []
|
|
@@ -45,7 +41,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
|
45
41
|
[
|
|
46
42
|
item.label,
|
|
47
43
|
display_location(item.cloud_provider),
|
|
48
|
-
|
|
44
|
+
timestamp_to_localized_str(item.created_at, json),
|
|
49
45
|
]
|
|
50
46
|
)
|
|
51
47
|
display_table(column_names, rows, json, title=f"Shared Volumes{env_part}")
|
|
@@ -66,7 +62,7 @@ def create(
|
|
|
66
62
|
):
|
|
67
63
|
ensure_env(env)
|
|
68
64
|
modal.NetworkFileSystem.create_deployed(name, environment_name=env)
|
|
69
|
-
console =
|
|
65
|
+
console = make_console()
|
|
70
66
|
console.print(f"Created volume '{name}'. \n\nCode example:\n")
|
|
71
67
|
usage = Syntax(gen_usage_code(name), "python")
|
|
72
68
|
console.print(usage)
|
|
@@ -93,7 +89,7 @@ async def ls(
|
|
|
93
89
|
raise
|
|
94
90
|
|
|
95
91
|
if sys.stdout.isatty():
|
|
96
|
-
console =
|
|
92
|
+
console = make_console()
|
|
97
93
|
console.print(f"Directory listing of '{path}' in '{volume_name}'")
|
|
98
94
|
table = Table()
|
|
99
95
|
|
|
@@ -131,7 +127,7 @@ async def put(
|
|
|
131
127
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
132
128
|
if remote_path.endswith("/"):
|
|
133
129
|
remote_path = remote_path + os.path.basename(local_path)
|
|
134
|
-
console =
|
|
130
|
+
console = make_console()
|
|
135
131
|
|
|
136
132
|
if Path(local_path).is_dir():
|
|
137
133
|
progress_handler = ProgressHandler(type="upload", console=console)
|
|
@@ -184,7 +180,7 @@ async def get(
|
|
|
184
180
|
ensure_env(env)
|
|
185
181
|
destination = Path(local_destination)
|
|
186
182
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
187
|
-
console =
|
|
183
|
+
console = make_console()
|
|
188
184
|
progress_handler = ProgressHandler(type="download", console=console)
|
|
189
185
|
with progress_handler.live:
|
|
190
186
|
await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
|
|
@@ -203,7 +199,7 @@ async def rm(
|
|
|
203
199
|
):
|
|
204
200
|
ensure_env(env)
|
|
205
201
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
206
|
-
console =
|
|
202
|
+
console = make_console()
|
|
207
203
|
try:
|
|
208
204
|
await volume.remove_file(remote_path, recursive=recursive)
|
|
209
205
|
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
modal/cli/profile.py
CHANGED
|
@@ -5,10 +5,10 @@ import os
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from rich.console import Console
|
|
9
8
|
from rich.json import JSON
|
|
10
9
|
from rich.table import Table
|
|
11
10
|
|
|
11
|
+
from modal._output import make_console
|
|
12
12
|
from modal._utils.async_utils import synchronizer
|
|
13
13
|
from modal.config import Config, _lookup_workspace, _profile, config_profiles, config_set_active_profile
|
|
14
14
|
from modal.exception import AuthError
|
|
@@ -19,6 +19,7 @@ profile_cli = typer.Typer(name="profile", help="Switch between Modal profiles.",
|
|
|
19
19
|
@profile_cli.command(help="Change the active Modal profile.")
|
|
20
20
|
def activate(profile: str = typer.Argument(..., help="Modal profile to activate.")):
|
|
21
21
|
config_set_active_profile(profile)
|
|
22
|
+
typer.echo(f"Active profile: {profile}")
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@profile_cli.command(help="Print the currently active Modal profile.")
|
|
@@ -69,7 +70,7 @@ async def list_(json: Optional[bool] = False):
|
|
|
69
70
|
except AuthError:
|
|
70
71
|
env_based_workspace = "Unknown (authentication failure)"
|
|
71
72
|
|
|
72
|
-
console =
|
|
73
|
+
console = make_console()
|
|
73
74
|
highlight = "bold green" if env_based_workspace is None else "yellow"
|
|
74
75
|
if json:
|
|
75
76
|
json_data = []
|