modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/cli/network_file_system.py
CHANGED
@@ -8,18 +8,17 @@ import typer
|
|
8
8
|
from click import UsageError
|
9
9
|
from grpclib import GRPCError, Status
|
10
10
|
from rich.console import Console
|
11
|
-
from rich.live import Live
|
12
11
|
from rich.syntax import Syntax
|
13
12
|
from rich.table import Table
|
14
|
-
from typer import Typer
|
13
|
+
from typer import Argument, Typer
|
15
14
|
|
16
15
|
import modal
|
17
16
|
from modal._location import display_location
|
18
|
-
from modal._output import
|
17
|
+
from modal._output import OutputManager, ProgressHandler
|
19
18
|
from modal._utils.async_utils import synchronizer
|
20
19
|
from modal._utils.grpc_utils import retry_transient_errors
|
21
20
|
from modal.cli._download import _volume_download
|
22
|
-
from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
|
21
|
+
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
|
23
22
|
from modal.client import _Client
|
24
23
|
from modal.environments import ensure_env
|
25
24
|
from modal.network_file_system import _NetworkFileSystem
|
@@ -28,9 +27,9 @@ from modal_proto import api_pb2
|
|
28
27
|
nfs_cli = Typer(name="nfs", help="Read and edit `modal.NetworkFileSystem` file systems.", no_args_is_help=True)
|
29
28
|
|
30
29
|
|
31
|
-
@nfs_cli.command(name="list", help="List the names of all network file systems.")
|
30
|
+
@nfs_cli.command(name="list", help="List the names of all network file systems.", rich_help_panel="Management")
|
32
31
|
@synchronizer.create_blocking
|
33
|
-
async def
|
32
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
34
33
|
env = ensure_env(env)
|
35
34
|
|
36
35
|
client = await _Client.from_env()
|
@@ -59,7 +58,7 @@ def some_func():
|
|
59
58
|
"""
|
60
59
|
|
61
60
|
|
62
|
-
@nfs_cli.command(name="create", help="Create a named network file system.")
|
61
|
+
@nfs_cli.command(name="create", help="Create a named network file system.", rich_help_panel="Management")
|
63
62
|
def create(
|
64
63
|
name: str,
|
65
64
|
env: Optional[str] = ENV_OPTION,
|
@@ -81,7 +80,11 @@ async def _volume_from_name(deployment_name: str) -> _NetworkFileSystem:
|
|
81
80
|
return network_file_system
|
82
81
|
|
83
82
|
|
84
|
-
@nfs_cli.command(
|
83
|
+
@nfs_cli.command(
|
84
|
+
name="ls",
|
85
|
+
help="List files and directories in a network file system.",
|
86
|
+
rich_help_panel="File operations",
|
87
|
+
)
|
85
88
|
@synchronizer.create_blocking
|
86
89
|
async def ls(
|
87
90
|
volume_name: str,
|
@@ -120,8 +123,10 @@ async def ls(
|
|
120
123
|
|
121
124
|
Remote parent directories will be created as needed.
|
122
125
|
|
123
|
-
Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory and the file
|
126
|
+
Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory and the file
|
127
|
+
will be uploaded with its current name under that directory.
|
124
128
|
""",
|
129
|
+
rich_help_panel="File operations",
|
125
130
|
)
|
126
131
|
@synchronizer.create_blocking
|
127
132
|
async def put(
|
@@ -137,19 +142,23 @@ async def put(
|
|
137
142
|
console = Console()
|
138
143
|
|
139
144
|
if Path(local_path).is_dir():
|
140
|
-
|
141
|
-
with
|
142
|
-
await volume.add_local_dir(local_path, remote_path)
|
143
|
-
|
145
|
+
progress_handler = ProgressHandler(type="upload", console=console)
|
146
|
+
with progress_handler.live:
|
147
|
+
await volume.add_local_dir(local_path, remote_path, progress_cb=progress_handler.progress)
|
148
|
+
progress_handler.progress(complete=True)
|
149
|
+
console.print(OutputManager.step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
|
144
150
|
|
145
151
|
elif "*" in local_path:
|
146
152
|
raise UsageError("Glob uploads are currently not supported")
|
147
153
|
else:
|
148
|
-
|
149
|
-
with
|
150
|
-
written_bytes = await volume.add_local_file(local_path, remote_path)
|
154
|
+
progress_handler = ProgressHandler(type="upload", console=console)
|
155
|
+
with progress_handler.live:
|
156
|
+
written_bytes = await volume.add_local_file(local_path, remote_path, progress_cb=progress_handler.progress)
|
157
|
+
progress_handler.progress(complete=True)
|
151
158
|
console.print(
|
152
|
-
step_completed(
|
159
|
+
OutputManager.step_completed(
|
160
|
+
f"Uploaded file '{local_path}' to '{remote_path}' ({written_bytes} bytes written)"
|
161
|
+
)
|
153
162
|
)
|
154
163
|
|
155
164
|
|
@@ -158,7 +167,7 @@ class CliError(Exception):
|
|
158
167
|
self.message = message
|
159
168
|
|
160
169
|
|
161
|
-
@nfs_cli.command(name="get")
|
170
|
+
@nfs_cli.command(name="get", rich_help_panel="File operations")
|
162
171
|
@synchronizer.create_blocking
|
163
172
|
async def get(
|
164
173
|
volume_name: str,
|
@@ -174,7 +183,7 @@ async def get(
|
|
174
183
|
|
175
184
|
For example, to download an entire network file system into `dump_volume`:
|
176
185
|
|
177
|
-
```
|
186
|
+
```
|
178
187
|
modal nfs get <volume-name> "**" dump_volume
|
179
188
|
```
|
180
189
|
|
@@ -183,10 +192,16 @@ async def get(
|
|
183
192
|
ensure_env(env)
|
184
193
|
destination = Path(local_destination)
|
185
194
|
volume = await _volume_from_name(volume_name)
|
186
|
-
|
195
|
+
console = Console()
|
196
|
+
progress_handler = ProgressHandler(type="download", console=console)
|
197
|
+
with progress_handler.live:
|
198
|
+
await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
|
199
|
+
console.print(OutputManager.step_completed("Finished downloading files to local!"))
|
187
200
|
|
188
201
|
|
189
|
-
@nfs_cli.command(
|
202
|
+
@nfs_cli.command(
|
203
|
+
name="rm", help="Delete a file or directory from a network file system.", rich_help_panel="File operations"
|
204
|
+
)
|
190
205
|
@synchronizer.create_blocking
|
191
206
|
async def rm(
|
192
207
|
volume_name: str,
|
@@ -202,3 +217,24 @@ async def rm(
|
|
202
217
|
if exc.status in (Status.NOT_FOUND, Status.INVALID_ARGUMENT):
|
203
218
|
raise UsageError(exc.message)
|
204
219
|
raise
|
220
|
+
|
221
|
+
|
222
|
+
@nfs_cli.command(
|
223
|
+
name="delete",
|
224
|
+
help="Delete a named, persistent modal.NetworkFileSystem.",
|
225
|
+
rich_help_panel="Management",
|
226
|
+
)
|
227
|
+
@synchronizer.create_blocking
|
228
|
+
async def delete(
|
229
|
+
nfs_name: str = Argument(help="Name of the modal.NetworkFileSystem to be deleted. Case sensitive"),
|
230
|
+
yes: bool = YES_OPTION,
|
231
|
+
env: Optional[str] = ENV_OPTION,
|
232
|
+
):
|
233
|
+
if not yes:
|
234
|
+
typer.confirm(
|
235
|
+
f"Are you sure you want to irrevocably delete the modal.NetworkFileSystem '{nfs_name}'?",
|
236
|
+
default=False,
|
237
|
+
abort=True,
|
238
|
+
)
|
239
|
+
|
240
|
+
await _NetworkFileSystem.delete(nfs_name, environment_name=env)
|
modal/cli/profile.py
CHANGED
@@ -28,7 +28,7 @@ def current():
|
|
28
28
|
|
29
29
|
@profile_cli.command(name="list", help="Show all Modal profiles and highlight the active one.")
|
30
30
|
@synchronizer.create_blocking
|
31
|
-
async def
|
31
|
+
async def list_(json: Optional[bool] = False):
|
32
32
|
config = Config()
|
33
33
|
profiles = config_profiles()
|
34
34
|
lookup_coros = [
|
@@ -8,16 +8,32 @@ import subprocess
|
|
8
8
|
import threading
|
9
9
|
import time
|
10
10
|
import webbrowser
|
11
|
-
from typing import Any
|
11
|
+
from typing import Any
|
12
12
|
|
13
|
-
from modal import App, Image, Queue, forward
|
14
|
-
|
15
|
-
# Passed by `modal launch` locally via CLI, empty on remote runner.
|
16
|
-
args: Dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_LOCAL_ARGS", "{}"))
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
17
14
|
|
15
|
+
# Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
18
17
|
|
19
18
|
app = App()
|
20
|
-
|
19
|
+
|
20
|
+
image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
|
21
|
+
|
22
|
+
if args.get("mount"):
|
23
|
+
image = image.add_local_dir(
|
24
|
+
args.get("mount"),
|
25
|
+
remote_path="/root/lab/mount",
|
26
|
+
)
|
27
|
+
|
28
|
+
volume = (
|
29
|
+
Volume.from_name(
|
30
|
+
args.get("volume"),
|
31
|
+
create_if_missing=True,
|
32
|
+
)
|
33
|
+
if args.get("volume")
|
34
|
+
else None
|
35
|
+
)
|
36
|
+
volumes = {"/root/lab/volume": volume} if volume else {}
|
21
37
|
|
22
38
|
|
23
39
|
def wait_for_port(url: str, q: Queue):
|
@@ -33,9 +49,18 @@ def wait_for_port(url: str, q: Queue):
|
|
33
49
|
q.put(url)
|
34
50
|
|
35
51
|
|
36
|
-
@app.function(
|
52
|
+
@app.function(
|
53
|
+
image=image,
|
54
|
+
cpu=args.get("cpu"),
|
55
|
+
memory=args.get("memory"),
|
56
|
+
gpu=args.get("gpu"),
|
57
|
+
timeout=args.get("timeout"),
|
58
|
+
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
59
|
+
volumes=volumes,
|
60
|
+
concurrency_limit=1 if volume else None,
|
61
|
+
)
|
37
62
|
def run_jupyter(q: Queue):
|
38
|
-
os.
|
63
|
+
os.makedirs("/root/lab", exist_ok=True)
|
39
64
|
token = secrets.token_urlsafe(13)
|
40
65
|
with forward(8888) as tunnel:
|
41
66
|
url = tunnel.url + "/?token=" + token
|
@@ -48,7 +73,7 @@ def run_jupyter(q: Queue):
|
|
48
73
|
"--allow-root",
|
49
74
|
"--ip=0.0.0.0",
|
50
75
|
"--port=8888",
|
51
|
-
"--notebook-dir=/lab",
|
76
|
+
"--notebook-dir=/root/lab",
|
52
77
|
"--LabApp.allow_origin='*'",
|
53
78
|
"--LabApp.allow_remote_access=1",
|
54
79
|
],
|
modal/cli/programs/vscode.py
CHANGED
@@ -8,19 +8,60 @@ import subprocess
|
|
8
8
|
import threading
|
9
9
|
import time
|
10
10
|
import webbrowser
|
11
|
-
from typing import Any
|
11
|
+
from typing import Any
|
12
12
|
|
13
|
-
from modal import App, Image, Queue, forward
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
14
14
|
|
15
|
-
# Passed by `modal launch` locally via CLI,
|
16
|
-
args:
|
15
|
+
# Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
|
16
|
+
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
17
|
+
|
18
|
+
CODE_SERVER_INSTALLER = "https://code-server.dev/install.sh"
|
19
|
+
CODE_SERVER_ENTRYPOINT = (
|
20
|
+
"https://raw.githubusercontent.com/coder/code-server/refs/tags/v4.96.1/ci/release-image/entrypoint.sh"
|
21
|
+
)
|
22
|
+
FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fixuid-0.6.0-linux-$ARCH.tar.gz"
|
17
23
|
|
18
24
|
|
19
25
|
app = App()
|
20
|
-
|
26
|
+
image = (
|
27
|
+
Image.from_registry(args.get("image"), add_python="3.11")
|
28
|
+
.apt_install("curl", "dumb-init", "git", "git-lfs")
|
29
|
+
.run_commands(
|
30
|
+
f"curl -fsSL {CODE_SERVER_INSTALLER} | sh",
|
31
|
+
f"curl -fsSL {CODE_SERVER_ENTRYPOINT} > /code-server.sh",
|
32
|
+
"chmod u+x /code-server.sh",
|
33
|
+
)
|
34
|
+
.run_commands(
|
35
|
+
'ARCH="$(dpkg --print-architecture)"'
|
36
|
+
f' && curl -fsSL "{FIXUD_INSTALLER}" | tar -C /usr/local/bin -xzf - '
|
37
|
+
" && chown root:root /usr/local/bin/fixuid"
|
38
|
+
" && chmod 4755 /usr/local/bin/fixuid"
|
39
|
+
" && mkdir -p /etc/fixuid"
|
40
|
+
' && echo "user: root" >> /etc/fixuid/config.yml'
|
41
|
+
' && echo "group: root" >> /etc/fixuid/config.yml'
|
42
|
+
)
|
43
|
+
.run_commands("mkdir /home/coder")
|
44
|
+
.env({"ENTRYPOINTD": ""})
|
45
|
+
)
|
46
|
+
|
47
|
+
if args.get("mount"):
|
48
|
+
image = image.add_local_dir(
|
49
|
+
args.get("mount"),
|
50
|
+
remote_path="/home/coder/mount",
|
51
|
+
)
|
52
|
+
|
53
|
+
volume = (
|
54
|
+
Volume.from_name(
|
55
|
+
args.get("volume"),
|
56
|
+
create_if_missing=True,
|
57
|
+
)
|
58
|
+
if args.get("volume")
|
59
|
+
else None
|
60
|
+
)
|
61
|
+
volumes = {"/home/coder/volume": volume} if volume else {}
|
21
62
|
|
22
63
|
|
23
|
-
def wait_for_port(data:
|
64
|
+
def wait_for_port(data: tuple[str, str], q: Queue):
|
24
65
|
start_time = time.monotonic()
|
25
66
|
while True:
|
26
67
|
try:
|
@@ -33,7 +74,16 @@ def wait_for_port(data: Tuple[str, str], q: Queue):
|
|
33
74
|
q.put(data)
|
34
75
|
|
35
76
|
|
36
|
-
@app.function(
|
77
|
+
@app.function(
|
78
|
+
image=image,
|
79
|
+
cpu=args.get("cpu"),
|
80
|
+
memory=args.get("memory"),
|
81
|
+
gpu=args.get("gpu"),
|
82
|
+
timeout=args.get("timeout"),
|
83
|
+
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
84
|
+
volumes=volumes,
|
85
|
+
concurrency_limit=1 if volume else None,
|
86
|
+
)
|
37
87
|
def run_vscode(q: Queue):
|
38
88
|
os.chdir("/home/coder")
|
39
89
|
token = secrets.token_urlsafe(13)
|
@@ -41,7 +91,7 @@ def run_vscode(q: Queue):
|
|
41
91
|
url = tunnel.url
|
42
92
|
threading.Thread(target=wait_for_port, args=((url, token), q)).start()
|
43
93
|
subprocess.run(
|
44
|
-
["/
|
94
|
+
["/code-server.sh", "--bind-addr", "0.0.0.0:8080", "."],
|
45
95
|
env={**os.environ, "SHELL": "/bin/bash", "PASSWORD": token},
|
46
96
|
)
|
47
97
|
q.put("done")
|
modal/cli/queues.py
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from rich.console import Console
|
6
|
+
from typer import Argument, Option, Typer
|
7
|
+
|
8
|
+
from modal._resolver import Resolver
|
9
|
+
from modal._utils.async_utils import synchronizer
|
10
|
+
from modal._utils.grpc_utils import retry_transient_errors
|
11
|
+
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
|
12
|
+
from modal.client import _Client
|
13
|
+
from modal.environments import ensure_env
|
14
|
+
from modal.queue import _Queue
|
15
|
+
from modal_proto import api_pb2
|
16
|
+
|
17
|
+
queue_cli = Typer(
|
18
|
+
name="queue",
|
19
|
+
no_args_is_help=True,
|
20
|
+
help="Manage `modal.Queue` objects and inspect their contents.",
|
21
|
+
)
|
22
|
+
|
23
|
+
PARTITION_OPTION = Option(
|
24
|
+
None,
|
25
|
+
"-p",
|
26
|
+
"--partition",
|
27
|
+
help="Name of the partition to use, otherwise use the default (anonymous) partition.",
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
@queue_cli.command(name="create", rich_help_panel="Management")
|
32
|
+
@synchronizer.create_blocking
|
33
|
+
async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
34
|
+
"""Create a named Queue.
|
35
|
+
|
36
|
+
Note: This is a no-op when the Queue already exists.
|
37
|
+
"""
|
38
|
+
q = _Queue.from_name(name, environment_name=env, create_if_missing=True)
|
39
|
+
client = await _Client.from_env()
|
40
|
+
resolver = Resolver(client=client)
|
41
|
+
await resolver.load(q)
|
42
|
+
|
43
|
+
|
44
|
+
@queue_cli.command(name="delete", rich_help_panel="Management")
|
45
|
+
@synchronizer.create_blocking
|
46
|
+
async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
|
47
|
+
"""Delete a named Queue and all of its data."""
|
48
|
+
# Lookup first to validate the name, even though delete is a staticmethod
|
49
|
+
await _Queue.lookup(name, environment_name=env)
|
50
|
+
if not yes:
|
51
|
+
typer.confirm(
|
52
|
+
f"Are you sure you want to irrevocably delete the modal.Queue '{name}'?",
|
53
|
+
default=False,
|
54
|
+
abort=True,
|
55
|
+
)
|
56
|
+
await _Queue.delete(name, environment_name=env)
|
57
|
+
|
58
|
+
|
59
|
+
@queue_cli.command(name="list", rich_help_panel="Management")
|
60
|
+
@synchronizer.create_blocking
|
61
|
+
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
62
|
+
"""List all named Queues."""
|
63
|
+
env = ensure_env(env)
|
64
|
+
|
65
|
+
max_total_size = 100_000
|
66
|
+
client = await _Client.from_env()
|
67
|
+
request = api_pb2.QueueListRequest(environment_name=env, total_size_limit=max_total_size + 1)
|
68
|
+
response = await retry_transient_errors(client.stub.QueueList, request)
|
69
|
+
|
70
|
+
rows = [
|
71
|
+
(
|
72
|
+
q.name,
|
73
|
+
timestamp_to_local(q.created_at, json),
|
74
|
+
str(q.num_partitions),
|
75
|
+
str(q.total_size) if q.total_size <= max_total_size else f">{max_total_size}",
|
76
|
+
)
|
77
|
+
for q in response.queues
|
78
|
+
]
|
79
|
+
display_table(["Name", "Created at", "Partitions", "Total size"], rows, json)
|
80
|
+
|
81
|
+
|
82
|
+
@queue_cli.command(name="clear", rich_help_panel="Management")
|
83
|
+
@synchronizer.create_blocking
|
84
|
+
async def clear(
|
85
|
+
name: str,
|
86
|
+
partition: Optional[str] = PARTITION_OPTION,
|
87
|
+
all: bool = Option(False, "-a", "--all", help="Clear the contents of all partitions."),
|
88
|
+
yes: bool = YES_OPTION,
|
89
|
+
*,
|
90
|
+
env: Optional[str] = ENV_OPTION,
|
91
|
+
):
|
92
|
+
"""Clear the contents of a queue by removing all of its data."""
|
93
|
+
q = await _Queue.lookup(name, environment_name=env)
|
94
|
+
if not yes:
|
95
|
+
typer.confirm(
|
96
|
+
f"Are you sure you want to irrevocably delete the contents of modal.Queue '{name}'?",
|
97
|
+
default=False,
|
98
|
+
abort=True,
|
99
|
+
)
|
100
|
+
await q.clear(partition=partition, all=all)
|
101
|
+
|
102
|
+
|
103
|
+
@queue_cli.command(name="peek", rich_help_panel="Inspection")
|
104
|
+
@synchronizer.create_blocking
|
105
|
+
async def peek(
|
106
|
+
name: str, n: int = Argument(1), partition: Optional[str] = PARTITION_OPTION, *, env: Optional[str] = ENV_OPTION
|
107
|
+
):
|
108
|
+
"""Print the next N items in the queue or queue partition (without removal)."""
|
109
|
+
q = await _Queue.lookup(name, environment_name=env)
|
110
|
+
console = Console()
|
111
|
+
i = 0
|
112
|
+
async for item in q.iterate(partition=partition):
|
113
|
+
console.print(item)
|
114
|
+
i += 1
|
115
|
+
if i >= n:
|
116
|
+
break
|
117
|
+
|
118
|
+
|
119
|
+
@queue_cli.command(name="len", rich_help_panel="Inspection")
|
120
|
+
@synchronizer.create_blocking
|
121
|
+
async def len(
|
122
|
+
name: str,
|
123
|
+
partition: Optional[str] = PARTITION_OPTION,
|
124
|
+
total: bool = Option(False, "-t", "--total", help="Compute the sum of the queue lengths across all partitions"),
|
125
|
+
*,
|
126
|
+
env: Optional[str] = ENV_OPTION,
|
127
|
+
):
|
128
|
+
"""Print the length of a queue partition or the total length of all partitions."""
|
129
|
+
q = await _Queue.lookup(name, environment_name=env)
|
130
|
+
console = Console()
|
131
|
+
console.print(await q.len(partition=partition, total=total))
|