modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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 +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +171 -138
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +30 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +2 -3
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +285 -105
- modal/app.pyi +216 -53
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +24 -53
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +82 -47
- modal/image.pyi +51 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -46
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -107
- modal/sandbox.pyi +226 -60
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +141 -70
- modal_proto/api_grpc.py +42 -26
- modal_proto/api_pb2.py +1123 -1103
- modal_proto/api_pb2.pyi +331 -83
- modal_proto/api_pb2_grpc.py +80 -48
- modal_proto/api_pb2_grpc.pyi +26 -18
- modal_proto/modal_api_grpc.py +175 -174
- modal_proto/task_command_router.proto +164 -0
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev66.dist-info/RECORD +0 -191
- 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_proto/sandbox_router.proto +0 -125
- modal_proto/sandbox_router_grpc.py +0 -89
- modal_proto/sandbox_router_pb2.py +0 -128
- modal_proto/sandbox_router_pb2_grpc.py +0 -169
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/cli/container.py
CHANGED
|
@@ -7,7 +7,6 @@ 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.grpc_utils import retry_transient_errors
|
|
11
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
|
|
@@ -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
|
@@ -4,6 +4,7 @@ from typing import Optional
|
|
|
4
4
|
import typer
|
|
5
5
|
from typer import Argument, Option, Typer
|
|
6
6
|
|
|
7
|
+
from modal._load_context import LoadContext
|
|
7
8
|
from modal._output import make_console
|
|
8
9
|
from modal._resolver import Resolver
|
|
9
10
|
from modal._utils.async_utils import synchronizer
|
|
@@ -29,8 +30,10 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
29
30
|
"""
|
|
30
31
|
d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
|
|
31
32
|
client = await _Client.from_env()
|
|
32
|
-
resolver = Resolver(
|
|
33
|
-
|
|
33
|
+
resolver = Resolver()
|
|
34
|
+
|
|
35
|
+
load_context = LoadContext(client=client, environment_name=env)
|
|
36
|
+
await resolver.load(d, load_context)
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
@dict_cli.command(name="list", rich_help_panel="Management")
|
modal/cli/entry_point.py
CHANGED
|
@@ -8,7 +8,7 @@ from rich.rule import Rule
|
|
|
8
8
|
from modal._output import make_console
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
|
|
11
|
-
from . import run
|
|
11
|
+
from . import run, shell as shell_module
|
|
12
12
|
from .app import app_cli
|
|
13
13
|
from .cluster import cluster_cli
|
|
14
14
|
from .config import config_cli
|
|
@@ -36,6 +36,7 @@ entrypoint_cli_typer = typer.Typer(
|
|
|
36
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
|
|
|
@@ -86,6 +87,29 @@ def check_path():
|
|
|
86
87
|
async def setup(profile: Optional[str] = None):
|
|
87
88
|
check_path()
|
|
88
89
|
|
|
90
|
+
art = """
|
|
91
|
+
############# #############
|
|
92
|
+
#### ## #### ##
|
|
93
|
+
## ## ## ## ## ##
|
|
94
|
+
## ## ## ## ## ##
|
|
95
|
+
## ## #### ## ##
|
|
96
|
+
## ############# ## ##
|
|
97
|
+
## ## #### ## ##
|
|
98
|
+
## ## ## ## ## ##
|
|
99
|
+
## ## ## ## ## ##
|
|
100
|
+
## ## ## ## ## ##
|
|
101
|
+
## ## ## ## ## ##
|
|
102
|
+
## ## ## ## #############
|
|
103
|
+
## ## ## ## ## ##
|
|
104
|
+
## ## ## ## ## ##
|
|
105
|
+
## ## ## ## ## ##
|
|
106
|
+
#### ## #### ##
|
|
107
|
+
############# #############
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
console = make_console()
|
|
111
|
+
console.print(art, style="green")
|
|
112
|
+
|
|
89
113
|
# Fetch a new token (same as `modal token new` but redirect to /home once finishes)
|
|
90
114
|
await _new_token(profile=profile, next_url="/home")
|
|
91
115
|
|
|
@@ -93,7 +117,7 @@ async def setup(profile: Optional[str] = None):
|
|
|
93
117
|
# Commands
|
|
94
118
|
entrypoint_cli_typer.command("deploy", no_args_is_help=True)(run.deploy)
|
|
95
119
|
entrypoint_cli_typer.command("serve", no_args_is_help=True)(run.serve)
|
|
96
|
-
entrypoint_cli_typer.command("shell")(
|
|
120
|
+
entrypoint_cli_typer.command("shell")(shell_module.shell)
|
|
97
121
|
entrypoint_cli_typer.add_typer(launch_cli)
|
|
98
122
|
|
|
99
123
|
# Deployments
|
modal/cli/environment.py
CHANGED
|
@@ -3,14 +3,12 @@ from typing import Annotated, Optional, Union
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
from click import UsageError
|
|
6
|
-
from grpclib import GRPCError, Status
|
|
7
6
|
from rich.text import Text
|
|
8
7
|
|
|
9
8
|
from modal import environments
|
|
10
9
|
from modal._utils.name_utils import check_environment_name
|
|
11
10
|
from modal.cli.utils import YES_OPTION, display_table
|
|
12
11
|
from modal.config import config
|
|
13
|
-
from modal.exception import InvalidError
|
|
14
12
|
|
|
15
13
|
ENVIRONMENT_HELP_TEXT = """Create and interact with Environments
|
|
16
14
|
|
|
@@ -61,13 +59,7 @@ ENVIRONMENT_CREATE_HELP = """Create a new environment in the current workspace""
|
|
|
61
59
|
@environment_cli.command(name="create", help=ENVIRONMENT_CREATE_HELP)
|
|
62
60
|
def create(name: Annotated[str, typer.Argument(help="Name of the new environment. Must be unique. Case sensitive")]):
|
|
63
61
|
check_environment_name(name)
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
environments.create_environment(name)
|
|
67
|
-
except GRPCError as exc:
|
|
68
|
-
if exc.status == Status.INVALID_ARGUMENT:
|
|
69
|
-
raise InvalidError(exc.message)
|
|
70
|
-
raise
|
|
62
|
+
environments.create_environment(name)
|
|
71
63
|
typer.echo(f"Environment created: {name}")
|
|
72
64
|
|
|
73
65
|
|
|
@@ -114,11 +106,5 @@ def update(
|
|
|
114
106
|
if set_name:
|
|
115
107
|
check_environment_name(set_name)
|
|
116
108
|
|
|
117
|
-
|
|
118
|
-
environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
|
|
119
|
-
except GRPCError as exc:
|
|
120
|
-
if exc.status == Status.INVALID_ARGUMENT:
|
|
121
|
-
raise InvalidError(exc.message)
|
|
122
|
-
raise
|
|
123
|
-
|
|
109
|
+
environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
|
|
124
110
|
typer.echo("Environment updated")
|
modal/cli/launch.py
CHANGED
|
@@ -3,8 +3,6 @@ import asyncio
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import tempfile
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
from typing import Any, Optional
|
|
10
8
|
|
|
@@ -23,8 +21,7 @@ launch_cli = Typer(
|
|
|
23
21
|
no_args_is_help=True,
|
|
24
22
|
rich_markup_mode="markdown",
|
|
25
23
|
help="""
|
|
26
|
-
Open a serverless app instance on Modal.
|
|
27
|
-
>⚠️ `modal launch` is **experimental** and may change in the future.
|
|
24
|
+
[Experimental] Open a serverless app instance on Modal.
|
|
28
25
|
""",
|
|
29
26
|
)
|
|
30
27
|
|
|
@@ -121,75 +118,3 @@ def vscode(
|
|
|
121
118
|
"volume": volume,
|
|
122
119
|
}
|
|
123
120
|
_launch_program("vscode", "vscode.py", detach, args)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@launch_cli.command(name="machine", help="Start an instance on Modal, with direct SSH access.", hidden=True)
|
|
127
|
-
def machine(
|
|
128
|
-
name: str, # Name of the machine App.
|
|
129
|
-
cpu: int = 8, # Reservation of CPU cores (can burst above this value).
|
|
130
|
-
memory: int = 32768, # Reservation of memory in MiB (can burst above this value).
|
|
131
|
-
gpu: Optional[str] = None, # GPU type and count, e.g. "t4" or "h100:2".
|
|
132
|
-
image: Optional[str] = None, # Image tag to use from registry. Defaults to the notebook base image.
|
|
133
|
-
timeout: int = 3600 * 24, # Timeout in seconds for the instance.
|
|
134
|
-
volume: str = "machine-vol", # Attach a persisted `modal.Volume` at /workspace (created if missing).
|
|
135
|
-
):
|
|
136
|
-
tempdir = Path(tempfile.gettempdir())
|
|
137
|
-
key_path = tempdir / "modal-machine-keyfile.pem"
|
|
138
|
-
# Generate a new SSH key pair for this machine instance.
|
|
139
|
-
if not key_path.exists():
|
|
140
|
-
subprocess.run(
|
|
141
|
-
["ssh-keygen", "-t", "ed25519", "-f", str(key_path), "-N", ""],
|
|
142
|
-
check=True,
|
|
143
|
-
stdout=subprocess.DEVNULL,
|
|
144
|
-
)
|
|
145
|
-
# Add the key with expiry 1d to ssh agent.
|
|
146
|
-
subprocess.run(
|
|
147
|
-
["ssh-add", "-t", "1d", str(key_path)],
|
|
148
|
-
check=True,
|
|
149
|
-
stdout=subprocess.DEVNULL,
|
|
150
|
-
stderr=subprocess.DEVNULL,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
os.environ["SSH_PUBLIC_KEY"] = Path(str(key_path) + ".pub").read_text()
|
|
154
|
-
os.environ["MODAL_LOGS_TIMEOUT"] = "0" # hack to work with --detach
|
|
155
|
-
|
|
156
|
-
args = {
|
|
157
|
-
"cpu": cpu,
|
|
158
|
-
"memory": memory,
|
|
159
|
-
"gpu": gpu,
|
|
160
|
-
"image": image,
|
|
161
|
-
"timeout": timeout,
|
|
162
|
-
"volume": volume,
|
|
163
|
-
}
|
|
164
|
-
_launch_program(
|
|
165
|
-
"machine",
|
|
166
|
-
"launch_instance_ssh.py",
|
|
167
|
-
True,
|
|
168
|
-
args,
|
|
169
|
-
description=name,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@launch_cli.command(name="marimo", help="Start a remote Marimo notebook on Modal.", hidden=True)
|
|
174
|
-
def marimo(
|
|
175
|
-
cpu: int = 8,
|
|
176
|
-
memory: int = 32768,
|
|
177
|
-
gpu: Optional[str] = None,
|
|
178
|
-
image: str = "debian:12",
|
|
179
|
-
timeout: int = 3600,
|
|
180
|
-
add_python: Optional[str] = "3.12",
|
|
181
|
-
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
|
182
|
-
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
|
183
|
-
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
|
184
|
-
):
|
|
185
|
-
args = {
|
|
186
|
-
"cpu": cpu,
|
|
187
|
-
"memory": memory,
|
|
188
|
-
"gpu": gpu,
|
|
189
|
-
"timeout": timeout,
|
|
190
|
-
"image": image,
|
|
191
|
-
"add_python": add_python,
|
|
192
|
-
"mount": mount,
|
|
193
|
-
"volume": volume,
|
|
194
|
-
}
|
|
195
|
-
_launch_program("marimo", "run_marimo.py", detach, args)
|
modal/cli/network_file_system.py
CHANGED
|
@@ -6,7 +6,6 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from click import UsageError
|
|
9
|
-
from grpclib import GRPCError, Status
|
|
10
9
|
from rich.syntax import Syntax
|
|
11
10
|
from rich.table import Table
|
|
12
11
|
from typer import Argument, Typer
|
|
@@ -15,7 +14,6 @@ import modal
|
|
|
15
14
|
from modal._location import display_location
|
|
16
15
|
from modal._output import OutputManager, ProgressHandler, make_console
|
|
17
16
|
from modal._utils.async_utils import synchronizer
|
|
18
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
19
17
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
20
18
|
from modal.cli._download import _volume_download
|
|
21
19
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
@@ -33,9 +31,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
|
33
31
|
env = ensure_env(env)
|
|
34
32
|
|
|
35
33
|
client = await _Client.from_env()
|
|
36
|
-
response = await
|
|
37
|
-
client.stub.SharedVolumeList, api_pb2.SharedVolumeListRequest(environment_name=env)
|
|
38
|
-
)
|
|
34
|
+
response = await client.stub.SharedVolumeList(api_pb2.SharedVolumeListRequest(environment_name=env))
|
|
39
35
|
env_part = f" in environment '{env}'" if env else ""
|
|
40
36
|
column_names = ["Name", "Location", "Created at"]
|
|
41
37
|
rows = []
|
|
@@ -84,12 +80,7 @@ async def ls(
|
|
|
84
80
|
):
|
|
85
81
|
ensure_env(env)
|
|
86
82
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
87
|
-
|
|
88
|
-
entries = await volume.listdir(path)
|
|
89
|
-
except GRPCError as exc:
|
|
90
|
-
if exc.status in (Status.INVALID_ARGUMENT, Status.NOT_FOUND):
|
|
91
|
-
raise UsageError(exc.message)
|
|
92
|
-
raise
|
|
83
|
+
entries = await volume.listdir(path)
|
|
93
84
|
|
|
94
85
|
if sys.stdout.isatty():
|
|
95
86
|
console = make_console()
|
|
@@ -105,7 +96,7 @@ async def ls(
|
|
|
105
96
|
console.print(table)
|
|
106
97
|
else:
|
|
107
98
|
for entry in entries:
|
|
108
|
-
print(entry.path)
|
|
99
|
+
print(entry.path) # noqa: T201
|
|
109
100
|
|
|
110
101
|
|
|
111
102
|
@nfs_cli.command(
|
|
@@ -203,14 +194,8 @@ async def rm(
|
|
|
203
194
|
ensure_env(env)
|
|
204
195
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
205
196
|
console = make_console()
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
|
209
|
-
|
|
210
|
-
except GRPCError as exc:
|
|
211
|
-
if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
|
|
212
|
-
raise UsageError(exc.message)
|
|
213
|
-
raise
|
|
197
|
+
await volume.remove_file(remote_path, recursive=recursive)
|
|
198
|
+
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
|
214
199
|
|
|
215
200
|
|
|
216
201
|
@nfs_cli.command(
|
|
@@ -54,7 +54,7 @@ def wait_for_port(url: str, q: Queue):
|
|
|
54
54
|
cpu=args.get("cpu"),
|
|
55
55
|
memory=args.get("memory"),
|
|
56
56
|
gpu=args.get("gpu"),
|
|
57
|
-
timeout=args.get("timeout"),
|
|
57
|
+
timeout=args.get("timeout", 3600),
|
|
58
58
|
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
|
59
59
|
volumes=volumes,
|
|
60
60
|
max_containers=1 if volume else None,
|
modal/cli/programs/vscode.py
CHANGED
|
@@ -79,7 +79,7 @@ def wait_for_port(data: tuple[str, str], q: Queue):
|
|
|
79
79
|
cpu=args.get("cpu"),
|
|
80
80
|
memory=args.get("memory"),
|
|
81
81
|
gpu=args.get("gpu"),
|
|
82
|
-
timeout=args.get("timeout"),
|
|
82
|
+
timeout=args.get("timeout", 3600),
|
|
83
83
|
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
|
84
84
|
volumes=volumes,
|
|
85
85
|
max_containers=1 if volume else None,
|
modal/cli/queues.py
CHANGED
|
@@ -5,10 +5,10 @@ from typing import Optional
|
|
|
5
5
|
import typer
|
|
6
6
|
from typer import Argument, Option, Typer
|
|
7
7
|
|
|
8
|
+
from modal._load_context import LoadContext
|
|
8
9
|
from modal._output import make_console
|
|
9
10
|
from modal._resolver import Resolver
|
|
10
11
|
from modal._utils.async_utils import synchronizer
|
|
11
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
12
12
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
13
13
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
14
14
|
from modal.client import _Client
|
|
@@ -39,8 +39,9 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
39
39
|
"""
|
|
40
40
|
q = _Queue.from_name(name, environment_name=env, create_if_missing=True)
|
|
41
41
|
client = await _Client.from_env()
|
|
42
|
-
resolver = Resolver(
|
|
43
|
-
|
|
42
|
+
resolver = Resolver()
|
|
43
|
+
load_context = LoadContext(client=client, environment_name=env)
|
|
44
|
+
await resolver.load(q, load_context)
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
@queue_cli.command(name="delete", rich_help_panel="Management")
|
|
@@ -83,7 +84,7 @@ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
|
|
83
84
|
max_page_size = 100
|
|
84
85
|
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
85
86
|
req = api_pb2.QueueListRequest(environment_name=env, pagination=pagination, total_size_limit=max_total_size)
|
|
86
|
-
resp = await
|
|
87
|
+
resp = await client.stub.QueueList(req)
|
|
87
88
|
items.extend(resp.queues)
|
|
88
89
|
return len(resp.queues) < max_page_size
|
|
89
90
|
|
modal/cli/run.py
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import functools
|
|
4
4
|
import inspect
|
|
5
|
-
import platform
|
|
6
5
|
import re
|
|
7
|
-
import shlex
|
|
8
6
|
import sys
|
|
9
7
|
import time
|
|
10
8
|
import typing
|
|
11
9
|
from dataclasses import dataclass
|
|
12
|
-
from functools import partial
|
|
13
10
|
from typing import Any, Callable, Optional
|
|
14
11
|
|
|
15
12
|
import click
|
|
@@ -17,19 +14,15 @@ import typer
|
|
|
17
14
|
from click import ClickException
|
|
18
15
|
from typing_extensions import TypedDict
|
|
19
16
|
|
|
20
|
-
from .._functions import _FunctionSpec
|
|
21
17
|
from ..app import App, LocalEntrypoint
|
|
22
18
|
from ..cls import _get_class_constructor_signature
|
|
23
19
|
from ..config import config
|
|
24
20
|
from ..environments import ensure_env
|
|
25
|
-
from ..exception import ExecutionError, InvalidError,
|
|
21
|
+
from ..exception import ExecutionError, InvalidError, _CliUserExecutionError
|
|
26
22
|
from ..functions import Function
|
|
27
|
-
from ..image import Image
|
|
28
23
|
from ..output import enable_output
|
|
29
|
-
from ..runner import deploy_app,
|
|
30
|
-
from ..secret import Secret
|
|
24
|
+
from ..runner import deploy_app, run_app
|
|
31
25
|
from ..serving import serve_app
|
|
32
|
-
from ..volume import Volume
|
|
33
26
|
from .import_refs import (
|
|
34
27
|
CLICommand,
|
|
35
28
|
MethodReference,
|
|
@@ -38,7 +31,7 @@ from .import_refs import (
|
|
|
38
31
|
import_app_from_ref,
|
|
39
32
|
parse_import_ref,
|
|
40
33
|
)
|
|
41
|
-
from .utils import ENV_OPTION, ENV_OPTION_HELP,
|
|
34
|
+
from .utils import ENV_OPTION, ENV_OPTION_HELP, stream_app_logs
|
|
42
35
|
|
|
43
36
|
|
|
44
37
|
class ParameterMetadata(TypedDict):
|
|
@@ -136,7 +129,7 @@ def _add_click_options(func, parameters: dict[str, ParameterMetadata]):
|
|
|
136
129
|
|
|
137
130
|
parser = option_parsers.get(param_type_str)
|
|
138
131
|
if parser is None:
|
|
139
|
-
msg = f"Parameter `{param_name}` has unparseable annotation: {param['annotation']
|
|
132
|
+
msg = f"Parameter `{param_name}` has unparseable annotation: {param['annotation']}"
|
|
140
133
|
raise NoParserAvailable(msg)
|
|
141
134
|
kwargs: Any = {
|
|
142
135
|
"type": parser,
|
|
@@ -207,11 +200,21 @@ def _make_click_function(app, signature: CliRunnableSignature, inner: Callable[[
|
|
|
207
200
|
return f
|
|
208
201
|
|
|
209
202
|
|
|
203
|
+
def _get_signature(func: typing.Any) -> inspect.Signature:
|
|
204
|
+
"""Returns signature with the original source annotations."""
|
|
205
|
+
kwargs: dict[str, typing.Any] = {}
|
|
206
|
+
if sys.version_info[:2] >= (3, 14):
|
|
207
|
+
import annotationlib
|
|
208
|
+
|
|
209
|
+
kwargs["annotation_format"] = annotationlib.Format.STRING
|
|
210
|
+
return inspect.signature(func, **kwargs)
|
|
211
|
+
|
|
212
|
+
|
|
210
213
|
def _get_click_command_for_function(app: App, function: Function, ctx: click.Context):
|
|
211
214
|
if function.is_generator:
|
|
212
215
|
raise InvalidError("`modal run` is not supported for generator functions")
|
|
213
216
|
|
|
214
|
-
sig: inspect.Signature =
|
|
217
|
+
sig: inspect.Signature = _get_signature(function.info.raw_f)
|
|
215
218
|
type_hints = safe_get_type_hints(function.info.raw_f)
|
|
216
219
|
signature: CliRunnableSignature = _get_cli_runnable_signature(sig, type_hints)
|
|
217
220
|
|
|
@@ -260,7 +263,7 @@ def _get_click_command_for_cls(app: App, method_ref: MethodReference, ctx: click
|
|
|
260
263
|
|
|
261
264
|
partial_function = partial_functions[method_name]
|
|
262
265
|
raw_f = partial_function._get_raw_f()
|
|
263
|
-
sig_without_self =
|
|
266
|
+
sig_without_self = _get_signature(functools.partial(raw_f, None))
|
|
264
267
|
fun_signature = _get_cli_runnable_signature(sig_without_self, safe_get_type_hints(raw_f))
|
|
265
268
|
|
|
266
269
|
# TODO(erikbern): assert there's no overlap?
|
|
@@ -294,12 +297,12 @@ def _get_click_command_for_local_entrypoint(app: App, entrypoint: LocalEntrypoin
|
|
|
294
297
|
func = entrypoint.info.raw_f
|
|
295
298
|
isasync = inspect.iscoroutinefunction(func)
|
|
296
299
|
|
|
297
|
-
signature = _get_cli_runnable_signature(
|
|
300
|
+
signature = _get_cli_runnable_signature(_get_signature(func), safe_get_type_hints(func))
|
|
298
301
|
|
|
299
302
|
@click.pass_context
|
|
300
303
|
def f(ctx, *args, **kwargs):
|
|
301
304
|
if ctx.obj["detach"]:
|
|
302
|
-
print(
|
|
305
|
+
print( # noqa: T201
|
|
303
306
|
"Note that running a local entrypoint in detached mode only keeps the last "
|
|
304
307
|
"triggered Modal function alive after the parent process has been killed or disconnected."
|
|
305
308
|
)
|
|
@@ -496,6 +499,12 @@ def serve(
|
|
|
496
499
|
```
|
|
497
500
|
modal serve hello_world.py
|
|
498
501
|
```
|
|
502
|
+
|
|
503
|
+
Modal-generated URLs will have a `-dev` suffix appended to them when running with `modal serve`.
|
|
504
|
+
To customize this suffix (i.e., to avoid collisions with other users in your workspace who are
|
|
505
|
+
concurrently serving the App), you can set the `dev_suffix` in your `.modal.toml` file or the
|
|
506
|
+
`MODAL_DEV_SUFFIX` environment variable.
|
|
507
|
+
|
|
499
508
|
"""
|
|
500
509
|
env = ensure_env(env)
|
|
501
510
|
import_ref = parse_import_ref(app_ref, use_module_mode=use_module_mode)
|
|
@@ -513,192 +522,3 @@ def serve(
|
|
|
513
522
|
t = min(timeout, 3600)
|
|
514
523
|
time.sleep(t)
|
|
515
524
|
timeout -= t
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def shell(
|
|
519
|
-
ref: Optional[str] = typer.Argument(
|
|
520
|
-
default=None,
|
|
521
|
-
help=(
|
|
522
|
-
"ID of running container or Sandbox, or path to a Python file containing an App."
|
|
523
|
-
" Can also include a Function specifier, like `module.py::func`, if the file defines multiple Functions."
|
|
524
|
-
),
|
|
525
|
-
),
|
|
526
|
-
cmd: str = typer.Option("/bin/bash", "-c", "--cmd", help="Command to run inside the Modal image."),
|
|
527
|
-
env: str = ENV_OPTION,
|
|
528
|
-
image: Optional[str] = typer.Option(
|
|
529
|
-
default=None, help="Container image tag for inside the shell (if not using REF)."
|
|
530
|
-
),
|
|
531
|
-
add_python: Optional[str] = typer.Option(default=None, help="Add Python to the image (if not using REF)."),
|
|
532
|
-
volume: Optional[list[str]] = typer.Option(
|
|
533
|
-
default=None,
|
|
534
|
-
help=(
|
|
535
|
-
"Name of a `modal.Volume` to mount inside the shell at `/mnt/{name}` (if not using REF)."
|
|
536
|
-
" Can be used multiple times."
|
|
537
|
-
),
|
|
538
|
-
),
|
|
539
|
-
secret: Optional[list[str]] = typer.Option(
|
|
540
|
-
default=None,
|
|
541
|
-
help=("Name of a `modal.Secret` to mount inside the shell (if not using REF). Can be used multiple times."),
|
|
542
|
-
),
|
|
543
|
-
cpu: Optional[int] = typer.Option(default=None, help="Number of CPUs to allocate to the shell (if not using REF)."),
|
|
544
|
-
memory: Optional[int] = typer.Option(
|
|
545
|
-
default=None, help="Memory to allocate for the shell, in MiB (if not using REF)."
|
|
546
|
-
),
|
|
547
|
-
gpu: Optional[str] = typer.Option(
|
|
548
|
-
default=None,
|
|
549
|
-
help="GPUs to request for the shell, if any. Examples are `any`, `a10g`, `a100:4` (if not using REF).",
|
|
550
|
-
),
|
|
551
|
-
cloud: Optional[str] = typer.Option(
|
|
552
|
-
default=None,
|
|
553
|
-
help=(
|
|
554
|
-
"Cloud provider to run the shell on. Possible values are `aws`, `gcp`, `oci`, `auto` (if not using REF)."
|
|
555
|
-
),
|
|
556
|
-
),
|
|
557
|
-
region: Optional[str] = typer.Option(
|
|
558
|
-
default=None,
|
|
559
|
-
help=(
|
|
560
|
-
"Region(s) to run the container on. "
|
|
561
|
-
"Can be a single region or a comma-separated list to choose from (if not using REF)."
|
|
562
|
-
),
|
|
563
|
-
),
|
|
564
|
-
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
|
565
|
-
use_module_mode: bool = typer.Option(
|
|
566
|
-
False, "-m", help="Interpret argument as a Python module path instead of a file/script path"
|
|
567
|
-
),
|
|
568
|
-
):
|
|
569
|
-
"""Run a command or interactive shell inside a Modal container.
|
|
570
|
-
|
|
571
|
-
**Examples:**
|
|
572
|
-
|
|
573
|
-
Start an interactive shell inside the default Debian-based image:
|
|
574
|
-
|
|
575
|
-
```
|
|
576
|
-
modal shell
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
Start an interactive shell with the spec for `my_function` in your App
|
|
580
|
-
(uses the same image, volumes, mounts, etc.):
|
|
581
|
-
|
|
582
|
-
```
|
|
583
|
-
modal shell hello_world.py::my_function
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
Or, if you're using a [modal.Cls](https://modal.com/docs/reference/modal.Cls)
|
|
587
|
-
you can refer to a `@modal.method` directly:
|
|
588
|
-
|
|
589
|
-
```
|
|
590
|
-
modal shell hello_world.py::MyClass.my_method
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
Start a `python` shell:
|
|
594
|
-
|
|
595
|
-
```
|
|
596
|
-
modal shell hello_world.py --cmd=python
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
Run a command with your function's spec and pipe the output to a file:
|
|
600
|
-
|
|
601
|
-
```
|
|
602
|
-
modal shell hello_world.py -c 'uv pip list' > env.txt
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
Connect to a running Sandbox by ID:
|
|
606
|
-
|
|
607
|
-
```
|
|
608
|
-
modal shell sb-abc123xyz
|
|
609
|
-
```
|
|
610
|
-
"""
|
|
611
|
-
env = ensure_env(env)
|
|
612
|
-
|
|
613
|
-
if pty is None:
|
|
614
|
-
pty = is_tty()
|
|
615
|
-
|
|
616
|
-
if platform.system() == "Windows":
|
|
617
|
-
raise InvalidError("`modal shell` is currently not supported on Windows")
|
|
618
|
-
|
|
619
|
-
app = App("modal shell")
|
|
620
|
-
|
|
621
|
-
if ref is not None:
|
|
622
|
-
# `modal shell` with a sandbox ID gets the task_id, that's then handled by the `ta-*` flow below.
|
|
623
|
-
if ref.startswith("sb-") and len(ref[3:]) > 0 and ref[3:].isalnum():
|
|
624
|
-
from ..sandbox import Sandbox
|
|
625
|
-
|
|
626
|
-
try:
|
|
627
|
-
sandbox = Sandbox.from_id(ref)
|
|
628
|
-
task_id = sandbox._get_task_id()
|
|
629
|
-
ref = task_id
|
|
630
|
-
except NotFoundError as e:
|
|
631
|
-
raise ClickException(f"Sandbox '{ref}' not found")
|
|
632
|
-
except Exception as e:
|
|
633
|
-
raise ClickException(f"Error connecting to sandbox '{ref}': {str(e)}")
|
|
634
|
-
|
|
635
|
-
# `modal shell` with a container ID is a special case, alias for `modal container exec`.
|
|
636
|
-
if ref.startswith("ta-") and len(ref[3:]) > 0 and ref[3:].isalnum():
|
|
637
|
-
from .container import exec
|
|
638
|
-
|
|
639
|
-
exec(container_id=ref, command=shlex.split(cmd), pty=pty)
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
import_ref = parse_import_ref(ref, use_module_mode=use_module_mode)
|
|
643
|
-
runnable, all_usable_commands = import_and_filter(
|
|
644
|
-
import_ref, base_cmd="modal shell", accept_local_entrypoint=False, accept_webhook=True
|
|
645
|
-
)
|
|
646
|
-
if not runnable:
|
|
647
|
-
help_header = (
|
|
648
|
-
"Specify a Modal function to start a shell session for. E.g.\n"
|
|
649
|
-
f"> modal shell {import_ref.file_or_module}::my_function"
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
if all_usable_commands:
|
|
653
|
-
help_footer = f"The selected module '{import_ref.file_or_module}' has the following choices:\n\n"
|
|
654
|
-
help_footer += _get_runnable_list(all_usable_commands)
|
|
655
|
-
else:
|
|
656
|
-
help_footer = f"The selected module '{import_ref.file_or_module}' has no Modal functions or classes."
|
|
657
|
-
|
|
658
|
-
raise ClickException(f"{help_header}\n\n{help_footer}")
|
|
659
|
-
|
|
660
|
-
function_spec: _FunctionSpec
|
|
661
|
-
if isinstance(runnable, MethodReference):
|
|
662
|
-
# TODO: let users specify a class instead of a method, since they use the same environment
|
|
663
|
-
class_service_function = runnable.cls._get_class_service_function()
|
|
664
|
-
function_spec = class_service_function.spec
|
|
665
|
-
elif isinstance(runnable, Function):
|
|
666
|
-
function_spec = runnable.spec
|
|
667
|
-
else:
|
|
668
|
-
raise ValueError("Referenced entity is not a Modal function or class")
|
|
669
|
-
|
|
670
|
-
start_shell = partial(
|
|
671
|
-
interactive_shell,
|
|
672
|
-
image=function_spec.image,
|
|
673
|
-
mounts=function_spec.mounts,
|
|
674
|
-
secrets=function_spec.secrets,
|
|
675
|
-
network_file_systems=function_spec.network_file_systems,
|
|
676
|
-
gpu=function_spec.gpus,
|
|
677
|
-
cloud=function_spec.cloud,
|
|
678
|
-
cpu=function_spec.cpu,
|
|
679
|
-
memory=function_spec.memory,
|
|
680
|
-
volumes=function_spec.volumes,
|
|
681
|
-
region=function_spec.scheduler_placement.proto.regions if function_spec.scheduler_placement else None,
|
|
682
|
-
pty=pty,
|
|
683
|
-
proxy=function_spec.proxy,
|
|
684
|
-
)
|
|
685
|
-
else:
|
|
686
|
-
modal_image = Image.from_registry(image, add_python=add_python) if image else None
|
|
687
|
-
volumes = {} if volume is None else {f"/mnt/{vol}": Volume.from_name(vol) for vol in volume}
|
|
688
|
-
secrets = [] if secret is None else [Secret.from_name(s) for s in secret]
|
|
689
|
-
start_shell = partial(
|
|
690
|
-
interactive_shell,
|
|
691
|
-
image=modal_image,
|
|
692
|
-
cpu=cpu,
|
|
693
|
-
memory=memory,
|
|
694
|
-
gpu=gpu,
|
|
695
|
-
cloud=cloud,
|
|
696
|
-
volumes=volumes,
|
|
697
|
-
secrets=secrets,
|
|
698
|
-
region=region.split(",") if region else [],
|
|
699
|
-
pty=pty,
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
# NB: invoking under bash makes --cmd a lot more flexible.
|
|
703
|
-
cmds = shlex.split(f'/bin/bash -c "{cmd}"')
|
|
704
|
-
start_shell(app, cmds=cmds, environment_name=env, timeout=3600)
|
modal/cli/secret.py
CHANGED
|
@@ -15,7 +15,6 @@ from typer import Argument, Option
|
|
|
15
15
|
|
|
16
16
|
from modal._output import make_console
|
|
17
17
|
from modal._utils.async_utils import synchronizer
|
|
18
|
-
from modal._utils.grpc_utils import retry_transient_errors
|
|
19
18
|
from modal._utils.time_utils import timestamp_to_localized_str
|
|
20
19
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
21
20
|
from modal.client import _Client
|
|
@@ -44,7 +43,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
44
43
|
max_page_size = 100
|
|
45
44
|
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
46
45
|
req = api_pb2.SecretListRequest(environment_name=env, pagination=pagination)
|
|
47
|
-
resp = await
|
|
46
|
+
resp = await client.stub.SecretList(req)
|
|
48
47
|
items.extend(resp.items)
|
|
49
48
|
return len(resp.items) < max_page_size
|
|
50
49
|
|