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/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,8 +199,11 @@ async def rm(
|
|
|
203
199
|
):
|
|
204
200
|
ensure_env(env)
|
|
205
201
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
202
|
+
console = make_console()
|
|
206
203
|
try:
|
|
207
204
|
await volume.remove_file(remote_path, recursive=recursive)
|
|
205
|
+
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
|
206
|
+
|
|
208
207
|
except GRPCError as exc:
|
|
209
208
|
if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
|
|
210
209
|
raise UsageError(exc.message)
|
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 = []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Copyright Modal Labs 2023
|
|
2
|
+
# type: ignore
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import rich
|
|
9
|
+
import rich.panel
|
|
10
|
+
import rich.rule
|
|
11
|
+
|
|
12
|
+
import modal
|
|
13
|
+
import modal.experimental
|
|
14
|
+
|
|
15
|
+
# Passed by `modal launch` locally via CLI.
|
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
|
17
|
+
|
|
18
|
+
app = modal.App()
|
|
19
|
+
|
|
20
|
+
image: modal.Image
|
|
21
|
+
if args.get("image"):
|
|
22
|
+
image = modal.Image.from_registry(args.get("image"))
|
|
23
|
+
else:
|
|
24
|
+
# Must be set to the same image builder version as the notebook base image.
|
|
25
|
+
os.environ["MODAL_IMAGE_BUILDER_VERSION"] = "2024.10"
|
|
26
|
+
image = modal.experimental.notebook_base_image(python_version="3.12")
|
|
27
|
+
|
|
28
|
+
volume = (
|
|
29
|
+
modal.Volume.from_name(
|
|
30
|
+
args.get("volume"),
|
|
31
|
+
create_if_missing=True,
|
|
32
|
+
)
|
|
33
|
+
if args.get("volume")
|
|
34
|
+
else None
|
|
35
|
+
)
|
|
36
|
+
volumes = {"/workspace": volume} if volume else {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
startup_script = """
|
|
40
|
+
set -eu
|
|
41
|
+
mkdir -p /run/sshd
|
|
42
|
+
|
|
43
|
+
# Check if sshd is installed, install if not
|
|
44
|
+
test -x /usr/sbin/sshd || (apt-get update && apt-get install -y openssh-server)
|
|
45
|
+
|
|
46
|
+
# Change default working directory to /workspace
|
|
47
|
+
echo "cd /workspace" >> /root/.profile
|
|
48
|
+
|
|
49
|
+
mkdir -p /root/.ssh
|
|
50
|
+
echo "$SSH_PUBLIC_KEY" >> /root/.ssh/authorized_keys
|
|
51
|
+
/usr/sbin/sshd -D -e
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.local_entrypoint()
|
|
56
|
+
def main():
|
|
57
|
+
if not os.environ.get("SSH_PUBLIC_KEY"):
|
|
58
|
+
raise ValueError("SSH_PUBLIC_KEY environment variable is not set")
|
|
59
|
+
|
|
60
|
+
sb = modal.Sandbox.create(
|
|
61
|
+
*("sh", "-c", startup_script),
|
|
62
|
+
app=app,
|
|
63
|
+
image=image,
|
|
64
|
+
cpu=args.get("cpu"),
|
|
65
|
+
memory=args.get("memory"),
|
|
66
|
+
gpu=args.get("gpu"),
|
|
67
|
+
timeout=args.get("timeout"),
|
|
68
|
+
volumes=volumes,
|
|
69
|
+
unencrypted_ports=[22], # Forward SSH port
|
|
70
|
+
secrets=[modal.Secret.from_dict({"SSH_PUBLIC_KEY": os.environ.get("SSH_PUBLIC_KEY")})],
|
|
71
|
+
)
|
|
72
|
+
hostname, port = sb.tunnels()[22].tcp_socket
|
|
73
|
+
connection_cmd = f"ssh -A -p {port} root@{hostname}"
|
|
74
|
+
|
|
75
|
+
rich.print(
|
|
76
|
+
rich.rule.Rule(style="yellow"),
|
|
77
|
+
rich.panel.Panel(
|
|
78
|
+
f"""Your instance is ready! You can SSH into it using the following command:
|
|
79
|
+
|
|
80
|
+
[dim gray]>[/dim gray] [bold cyan]{connection_cmd}[/bold cyan]
|
|
81
|
+
|
|
82
|
+
[italic]Details:[/italic]
|
|
83
|
+
• Name: [magenta]{app.description}[/magenta]
|
|
84
|
+
• CPU: [yellow]{args.get("cpu")} cores[/yellow]
|
|
85
|
+
• Memory: [yellow]{args.get("memory")} MiB[/yellow]
|
|
86
|
+
• Timeout: [yellow]{args.get("timeout")} seconds[/yellow]
|
|
87
|
+
• GPU: [green]{(args.get("gpu") or "N/A").upper()}[/green]""",
|
|
88
|
+
title="SSH Connection",
|
|
89
|
+
expand=False,
|
|
90
|
+
),
|
|
91
|
+
rich.rule.Rule(style="yellow"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
sys.exit(0) # Exit immediately to prevent "Timed out waiting for final apps log."
|
|
@@ -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,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright Modal Labs 2025
|
|
2
|
+
# type: ignore
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import webbrowser
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
|
14
|
+
|
|
15
|
+
# Args injected by `modal launch` CLI.
|
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
|
17
|
+
|
|
18
|
+
app = App()
|
|
19
|
+
|
|
20
|
+
image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).uv_pip_install("marimo")
|
|
21
|
+
|
|
22
|
+
# Optional host-filesystem mount (read-only snapshot of your project, useful for editing)
|
|
23
|
+
if args.get("mount"):
|
|
24
|
+
image = image.add_local_dir(args["mount"], remote_path="/root/marimo/mount")
|
|
25
|
+
|
|
26
|
+
# Optional persistent Modal volume
|
|
27
|
+
volume = Volume.from_name(args["volume"], create_if_missing=True) if args.get("volume") else None
|
|
28
|
+
volumes = {"/root/marimo/volume": volume} if volume else {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _wait_for_port(url: str, q: Queue) -> None:
|
|
32
|
+
start = time.monotonic()
|
|
33
|
+
while True:
|
|
34
|
+
try:
|
|
35
|
+
with socket.create_connection(("localhost", 8888), timeout=30):
|
|
36
|
+
break
|
|
37
|
+
except OSError as exc:
|
|
38
|
+
if time.monotonic() - start > 30:
|
|
39
|
+
raise TimeoutError("marimo server did not start within 30 s") from exc
|
|
40
|
+
time.sleep(0.05)
|
|
41
|
+
q.put(url)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.function(
|
|
45
|
+
image=image,
|
|
46
|
+
cpu=args.get("cpu"),
|
|
47
|
+
memory=args.get("memory"),
|
|
48
|
+
gpu=args.get("gpu"),
|
|
49
|
+
timeout=args.get("timeout", 3600),
|
|
50
|
+
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
|
51
|
+
volumes=volumes,
|
|
52
|
+
max_containers=1 if volume else None,
|
|
53
|
+
)
|
|
54
|
+
def run_marimo(q: Queue):
|
|
55
|
+
os.makedirs("/root/marimo", exist_ok=True)
|
|
56
|
+
|
|
57
|
+
# marimo supports token-based auth; generate one so only you can connect
|
|
58
|
+
token = secrets.token_urlsafe(12)
|
|
59
|
+
|
|
60
|
+
with forward(8888) as tunnel:
|
|
61
|
+
url = f"{tunnel.url}/?access_token={token}"
|
|
62
|
+
threading.Thread(target=_wait_for_port, args=(url, q), daemon=True).start()
|
|
63
|
+
|
|
64
|
+
print("\nmarimo on Modal, opening in browser …")
|
|
65
|
+
print(f" -> {url}\n")
|
|
66
|
+
|
|
67
|
+
# Launch the headless edit server
|
|
68
|
+
subprocess.run(
|
|
69
|
+
[
|
|
70
|
+
"marimo",
|
|
71
|
+
"edit",
|
|
72
|
+
"--headless", # don't open browser in container
|
|
73
|
+
"--host",
|
|
74
|
+
"0.0.0.0", # bind all interfaces
|
|
75
|
+
"--port",
|
|
76
|
+
"8888",
|
|
77
|
+
"--token-password",
|
|
78
|
+
token, # enable session-based auth
|
|
79
|
+
"--skip-update-check",
|
|
80
|
+
"/root/marimo", # workspace directory
|
|
81
|
+
],
|
|
82
|
+
env={**os.environ, "SHELL": "/bin/bash"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
q.put("done")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.local_entrypoint()
|
|
89
|
+
def main():
|
|
90
|
+
with Queue.ephemeral() as q:
|
|
91
|
+
run_marimo.spawn(q)
|
|
92
|
+
url = q.get() # first message = connect URL
|
|
93
|
+
time.sleep(1) # give server a heartbeat
|
|
94
|
+
webbrowser.open(url)
|
|
95
|
+
assert q.get() == "done"
|
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
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Copyright Modal Labs 2024
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
6
|
from typer import Argument, Option, Typer
|
|
7
7
|
|
|
8
|
+
from modal._load_context import LoadContext
|
|
9
|
+
from modal._output import make_console
|
|
8
10
|
from modal._resolver import Resolver
|
|
9
11
|
from modal._utils.async_utils import synchronizer
|
|
10
|
-
from modal._utils.
|
|
11
|
-
from modal._utils.time_utils import timestamp_to_local
|
|
12
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
13
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
13
14
|
from modal.client import _Client
|
|
14
15
|
from modal.environments import ensure_env
|
|
@@ -38,23 +39,29 @@ async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
38
39
|
"""
|
|
39
40
|
q = _Queue.from_name(name, environment_name=env, create_if_missing=True)
|
|
40
41
|
client = await _Client.from_env()
|
|
41
|
-
resolver = Resolver(
|
|
42
|
-
|
|
42
|
+
resolver = Resolver()
|
|
43
|
+
load_context = LoadContext(client=client, environment_name=env)
|
|
44
|
+
await resolver.load(q, load_context)
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
@queue_cli.command(name="delete", rich_help_panel="Management")
|
|
46
48
|
@synchronizer.create_blocking
|
|
47
|
-
async def delete(
|
|
49
|
+
async def delete(
|
|
50
|
+
name: str,
|
|
51
|
+
*,
|
|
52
|
+
allow_missing: bool = Option(False, "--allow-missing", help="Don't error if the Queue doesn't exist."),
|
|
53
|
+
yes: bool = YES_OPTION,
|
|
54
|
+
env: Optional[str] = ENV_OPTION,
|
|
55
|
+
):
|
|
48
56
|
"""Delete a named Queue and all of its data."""
|
|
49
|
-
|
|
50
|
-
await _Queue.from_name(name, environment_name=env).hydrate()
|
|
57
|
+
env = ensure_env(env)
|
|
51
58
|
if not yes:
|
|
52
59
|
typer.confirm(
|
|
53
60
|
f"Are you sure you want to irrevocably delete the modal.Queue '{name}'?",
|
|
54
61
|
default=False,
|
|
55
62
|
abort=True,
|
|
56
63
|
)
|
|
57
|
-
await _Queue.delete(name, environment_name=env)
|
|
64
|
+
await _Queue.objects.delete(name, environment_name=env, allow_missing=allow_missing)
|
|
58
65
|
|
|
59
66
|
|
|
60
67
|
@queue_cli.command(name="list", rich_help_panel="Management")
|
|
@@ -62,22 +69,46 @@ async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_
|
|
|
62
69
|
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
|
63
70
|
"""List all named Queues."""
|
|
64
71
|
env = ensure_env(env)
|
|
65
|
-
|
|
66
|
-
max_total_size = 100_000
|
|
67
72
|
client = await _Client.from_env()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
max_total_size = 100_000 # Limit on the *Queue size* that we report
|
|
74
|
+
|
|
75
|
+
items: list[api_pb2.QueueListResponse.QueueInfo] = []
|
|
76
|
+
|
|
77
|
+
# Note that we need to continue using the gRPC API directly here rather than using Queue.objects.list.
|
|
78
|
+
# There is some metadata that historically appears in the CLI output (num_partitions, total_size) that
|
|
79
|
+
# doesn't make sense to transmit as hydration metadata, because the values can change over time and
|
|
80
|
+
# the metadata retrieved at hydration time could get stale. Alternatively, we could rewrite this using
|
|
81
|
+
# only public API by sequentially retrieving the queues and then querying their dynamic metadata, but
|
|
82
|
+
# that would require multiple round trips and would add lag to the CLI.
|
|
83
|
+
async def retrieve_page(created_before: float) -> bool:
|
|
84
|
+
max_page_size = 100
|
|
85
|
+
pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
|
|
86
|
+
req = api_pb2.QueueListRequest(environment_name=env, pagination=pagination, total_size_limit=max_total_size)
|
|
87
|
+
resp = await client.stub.QueueList(req)
|
|
88
|
+
items.extend(resp.queues)
|
|
89
|
+
return len(resp.queues) < max_page_size
|
|
90
|
+
|
|
91
|
+
finished = await retrieve_page(datetime.now().timestamp())
|
|
92
|
+
while True:
|
|
93
|
+
if finished:
|
|
94
|
+
break
|
|
95
|
+
finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
|
|
96
|
+
|
|
97
|
+
queues = [_Queue._new_hydrated(item.queue_id, client, item.metadata, is_another_app=True) for item in items]
|
|
98
|
+
|
|
99
|
+
rows = []
|
|
100
|
+
for obj, resp_data in zip(queues, items):
|
|
101
|
+
info = await obj.info()
|
|
102
|
+
rows.append(
|
|
103
|
+
(
|
|
104
|
+
obj.name,
|
|
105
|
+
timestamp_to_localized_str(info.created_at.timestamp(), json),
|
|
106
|
+
info.created_by,
|
|
107
|
+
str(resp_data.num_partitions),
|
|
108
|
+
str(resp_data.total_size) if resp_data.total_size <= max_total_size else f">{max_total_size}",
|
|
109
|
+
)
|
|
77
110
|
)
|
|
78
|
-
|
|
79
|
-
]
|
|
80
|
-
display_table(["Name", "Created at", "Partitions", "Total size"], rows, json)
|
|
111
|
+
display_table(["Name", "Created at", "Created by", "Partitions", "Total size"], rows, json)
|
|
81
112
|
|
|
82
113
|
|
|
83
114
|
@queue_cli.command(name="clear", rich_help_panel="Management")
|
|
@@ -108,7 +139,7 @@ async def peek(
|
|
|
108
139
|
):
|
|
109
140
|
"""Print the next N items in the queue or queue partition (without removal)."""
|
|
110
141
|
q = _Queue.from_name(name, environment_name=env)
|
|
111
|
-
console =
|
|
142
|
+
console = make_console()
|
|
112
143
|
i = 0
|
|
113
144
|
async for item in q.iterate(partition=partition):
|
|
114
145
|
console.print(item)
|
|
@@ -119,7 +150,7 @@ async def peek(
|
|
|
119
150
|
|
|
120
151
|
@queue_cli.command(name="len", rich_help_panel="Inspection")
|
|
121
152
|
@synchronizer.create_blocking
|
|
122
|
-
async def
|
|
153
|
+
async def len_(
|
|
123
154
|
name: str,
|
|
124
155
|
partition: Optional[str] = PARTITION_OPTION,
|
|
125
156
|
total: bool = Option(False, "-t", "--total", help="Compute the sum of the queue lengths across all partitions"),
|
|
@@ -128,5 +159,5 @@ async def len(
|
|
|
128
159
|
):
|
|
129
160
|
"""Print the length of a queue partition or the total length of all partitions."""
|
|
130
161
|
q = _Queue.from_name(name, environment_name=env)
|
|
131
|
-
console =
|
|
162
|
+
console = make_console()
|
|
132
163
|
console.print(await q.len(partition=partition, total=total))
|