modal 1.0.3.dev10__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/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- 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 +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -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 +11 -12
- 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 +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- 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/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- 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-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/billing.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
aiohappyeyeballs==2.6.1
|
|
2
|
+
aiohttp==3.12.7
|
|
3
|
+
aiosignal==1.3.2
|
|
4
|
+
async-timeout==5.0.1 ; python_version < "3.11"
|
|
5
|
+
attrs==25.3.0
|
|
6
|
+
cbor2==5.7.0
|
|
7
|
+
certifi==2025.4.26
|
|
8
|
+
frozenlist==1.6.0
|
|
9
|
+
grpclib==0.4.8
|
|
10
|
+
h2==4.2.0
|
|
11
|
+
hpack==4.1.0
|
|
12
|
+
hyperframe==6.1.0
|
|
13
|
+
idna==3.10
|
|
14
|
+
multidict==6.4.4
|
|
15
|
+
propcache==0.3.1
|
|
16
|
+
protobuf==6.31.1
|
|
17
|
+
typing_extensions==4.13.2
|
|
18
|
+
yarl==1.20.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
aiohappyeyeballs==2.6.1
|
|
2
|
+
aiohttp==3.12.7
|
|
3
|
+
aiosignal==1.3.2
|
|
4
|
+
async-timeout==5.0.1 ; python_version < "3.11"
|
|
5
|
+
attrs==25.3.0
|
|
6
|
+
cbor2==5.7.0
|
|
7
|
+
certifi==2025.4.26
|
|
8
|
+
frozenlist==1.6.0
|
|
9
|
+
grpclib==0.4.8
|
|
10
|
+
h2==4.2.0
|
|
11
|
+
hpack==4.1.0
|
|
12
|
+
hyperframe==6.1.0
|
|
13
|
+
idna==3.10
|
|
14
|
+
multidict==6.4.4
|
|
15
|
+
propcache==0.3.1
|
|
16
|
+
protobuf==6.31.1
|
|
17
|
+
typing_extensions==4.13.2
|
|
18
|
+
yarl==1.20.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"debian": {
|
|
3
|
+
"PREVIEW": "bookworm",
|
|
4
|
+
"2025.06": "bookworm",
|
|
5
|
+
"2024.10": "bookworm",
|
|
6
|
+
"2024.04": "bookworm",
|
|
7
|
+
"2023.12": "bullseye"
|
|
8
|
+
},
|
|
9
|
+
"python": {
|
|
10
|
+
"PREVIEW": [
|
|
11
|
+
"3.9.22",
|
|
12
|
+
"3.10.17",
|
|
13
|
+
"3.11.12",
|
|
14
|
+
"3.12.10",
|
|
15
|
+
"3.13.3"
|
|
16
|
+
],
|
|
17
|
+
"2025.06": [
|
|
18
|
+
"3.9.22",
|
|
19
|
+
"3.10.17",
|
|
20
|
+
"3.11.12",
|
|
21
|
+
"3.12.10",
|
|
22
|
+
"3.13.3"
|
|
23
|
+
],
|
|
24
|
+
"2024.10": [
|
|
25
|
+
"3.9.20",
|
|
26
|
+
"3.10.15",
|
|
27
|
+
"3.11.10",
|
|
28
|
+
"3.12.6",
|
|
29
|
+
"3.13.0"
|
|
30
|
+
],
|
|
31
|
+
"2024.04": [
|
|
32
|
+
"3.9.19",
|
|
33
|
+
"3.10.14",
|
|
34
|
+
"3.11.8",
|
|
35
|
+
"3.12.2"
|
|
36
|
+
],
|
|
37
|
+
"2023.12": [
|
|
38
|
+
"3.9.15",
|
|
39
|
+
"3.10.8",
|
|
40
|
+
"3.11.0",
|
|
41
|
+
"3.12.1"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"micromamba": {
|
|
45
|
+
"PREVIEW": "2.1.1-debian12-slim",
|
|
46
|
+
"2025.06": "2.1.1-debian12-slim",
|
|
47
|
+
"2024.10": "1.5.10-bookworm-slim",
|
|
48
|
+
"2024.04": "1.5.8-bookworm-slim",
|
|
49
|
+
"2023.12": "1.3.1-bullseye-slim"
|
|
50
|
+
},
|
|
51
|
+
"package_tools": {
|
|
52
|
+
"PREVIEW": "pip wheel uv",
|
|
53
|
+
"2025.06": "pip wheel uv",
|
|
54
|
+
"2024.10": "pip wheel uv",
|
|
55
|
+
"2024.04": "pip wheel uv",
|
|
56
|
+
"2023.12": "pip"
|
|
57
|
+
}
|
|
58
|
+
}
|
modal/cli/_download.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2023
|
|
2
2
|
import asyncio
|
|
3
3
|
import functools
|
|
4
|
+
import multiprocessing
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import sys
|
|
@@ -23,12 +24,22 @@ async def _volume_download(
|
|
|
23
24
|
remote_path: str,
|
|
24
25
|
local_destination: Path,
|
|
25
26
|
overwrite: bool,
|
|
26
|
-
|
|
27
|
+
concurrency: Optional[int] = None,
|
|
28
|
+
progress_cb: Optional[Callable] = None,
|
|
27
29
|
):
|
|
30
|
+
if progress_cb is None:
|
|
31
|
+
|
|
32
|
+
def progress_cb(*_, **__):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
if concurrency is None:
|
|
36
|
+
concurrency = max(128, 2 * multiprocessing.cpu_count())
|
|
37
|
+
|
|
28
38
|
is_pipe = local_destination == PIPE_PATH
|
|
29
39
|
|
|
30
40
|
q: asyncio.Queue[tuple[Optional[Path], Optional[FileEntry]]] = asyncio.Queue()
|
|
31
|
-
num_consumers = 1 if is_pipe else
|
|
41
|
+
num_consumers = 1 if is_pipe else concurrency # concurrency limit for downloading files
|
|
42
|
+
download_semaphore = asyncio.Semaphore(concurrency)
|
|
32
43
|
|
|
33
44
|
async def producer():
|
|
34
45
|
iterator: AsyncIterator[FileEntry]
|
|
@@ -86,7 +97,12 @@ async def _volume_download(
|
|
|
86
97
|
|
|
87
98
|
with output_path.open("wb") as fp:
|
|
88
99
|
if isinstance(volume, _Volume):
|
|
89
|
-
b = await volume.
|
|
100
|
+
b = await volume._read_file_into_fileobj(
|
|
101
|
+
path=entry.path,
|
|
102
|
+
fileobj=fp,
|
|
103
|
+
download_semaphore=download_semaphore,
|
|
104
|
+
progress_cb=file_progress_cb,
|
|
105
|
+
)
|
|
90
106
|
else:
|
|
91
107
|
b = 0
|
|
92
108
|
async for chunk in volume.read_file(entry.path):
|
modal/cli/_traceback.py
CHANGED
|
@@ -6,12 +6,13 @@ import re
|
|
|
6
6
|
import warnings
|
|
7
7
|
from typing import Optional
|
|
8
8
|
|
|
9
|
-
from rich.console import
|
|
9
|
+
from rich.console import RenderResult, group
|
|
10
10
|
from rich.panel import Panel
|
|
11
11
|
from rich.syntax import Syntax
|
|
12
12
|
from rich.text import Text
|
|
13
13
|
from rich.traceback import PathHighlighter, Stack, Traceback, install
|
|
14
14
|
|
|
15
|
+
from .._output import make_console
|
|
15
16
|
from ..exception import DeprecationError, PendingDeprecationError, ServerWarning
|
|
16
17
|
|
|
17
18
|
|
|
@@ -193,7 +194,7 @@ def highlight_modal_warnings() -> None:
|
|
|
193
194
|
title=title,
|
|
194
195
|
title_align="left",
|
|
195
196
|
)
|
|
196
|
-
|
|
197
|
+
make_console().print(panel)
|
|
197
198
|
else:
|
|
198
199
|
base_showwarning(warning, category, filename, lineno, file=None, line=None)
|
|
199
200
|
|
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)
|