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/container.py
CHANGED
@@ -1,28 +1,37 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
|
3
|
-
from typing import List, Union
|
2
|
+
from typing import Optional, Union
|
4
3
|
|
5
4
|
import typer
|
6
5
|
from rich.text import Text
|
7
6
|
|
8
|
-
from modal.
|
7
|
+
from modal._pty import get_pty_info
|
9
8
|
from modal._utils.async_utils import synchronizer
|
10
|
-
from modal.
|
9
|
+
from modal._utils.grpc_utils import retry_transient_errors
|
10
|
+
from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs, timestamp_to_local
|
11
11
|
from modal.client import _Client
|
12
|
+
from modal.config import config
|
13
|
+
from modal.container_process import _ContainerProcess
|
14
|
+
from modal.environments import ensure_env
|
15
|
+
from modal.object import _get_environment_name
|
16
|
+
from modal.stream_type import StreamType
|
12
17
|
from modal_proto import api_pb2
|
13
18
|
|
14
|
-
container_cli = typer.Typer(name="container", help="Manage running containers.", no_args_is_help=True)
|
19
|
+
container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
|
15
20
|
|
16
21
|
|
17
22
|
@container_cli.command("list")
|
18
23
|
@synchronizer.create_blocking
|
19
|
-
async def
|
24
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
20
25
|
"""List all containers that are currently running."""
|
26
|
+
env = ensure_env(env)
|
21
27
|
client = await _Client.from_env()
|
22
|
-
|
28
|
+
environment_name = _get_environment_name(env)
|
29
|
+
res: api_pb2.TaskListResponse = await client.stub.TaskList(
|
30
|
+
api_pb2.TaskListRequest(environment_name=environment_name)
|
31
|
+
)
|
23
32
|
|
24
33
|
column_names = ["Container ID", "App ID", "App Name", "Start Time"]
|
25
|
-
rows:
|
34
|
+
rows: list[list[Union[Text, str]]] = []
|
26
35
|
res.tasks.sort(key=lambda task: task.started_at, reverse=True)
|
27
36
|
for task_stats in res.tasks:
|
28
37
|
rows.append(
|
@@ -34,16 +43,55 @@ async def list(json: bool = False):
|
|
34
43
|
]
|
35
44
|
)
|
36
45
|
|
37
|
-
display_table(column_names, rows, json=json, title="Active Containers")
|
46
|
+
display_table(column_names, rows, json=json, title=f"Active Containers in environment: {environment_name}")
|
47
|
+
|
48
|
+
|
49
|
+
@container_cli.command("logs")
|
50
|
+
def logs(container_id: str = typer.Argument(help="Container ID")):
|
51
|
+
"""Show logs for a specific container, streaming while active."""
|
52
|
+
stream_app_logs(task_id=container_id)
|
38
53
|
|
39
54
|
|
40
55
|
@container_cli.command("exec")
|
41
56
|
@synchronizer.create_blocking
|
42
57
|
async def exec(
|
43
|
-
|
44
|
-
|
45
|
-
|
58
|
+
pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
|
59
|
+
container_id: str = typer.Argument(help="Container ID"),
|
60
|
+
command: list[str] = typer.Argument(
|
61
|
+
help="A command to run inside the container.\n\n"
|
62
|
+
"To pass command-line flags or options, add `--` before the start of your commands. "
|
63
|
+
"For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
|
64
|
+
),
|
46
65
|
):
|
47
66
|
"""Execute a command in a container."""
|
67
|
+
|
68
|
+
if pty is None:
|
69
|
+
pty = is_tty()
|
70
|
+
|
71
|
+
client = await _Client.from_env()
|
72
|
+
|
73
|
+
req = api_pb2.ContainerExecRequest(
|
74
|
+
task_id=container_id,
|
75
|
+
command=command,
|
76
|
+
pty_info=get_pty_info(shell=True) if pty else None,
|
77
|
+
runtime_debug=config.get("function_runtime_debug"),
|
78
|
+
)
|
79
|
+
res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
80
|
+
|
81
|
+
if pty:
|
82
|
+
await _ContainerProcess(res.exec_id, client).attach()
|
83
|
+
else:
|
84
|
+
# TODO: redirect stderr to its own stream?
|
85
|
+
await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
|
86
|
+
|
87
|
+
|
88
|
+
@container_cli.command("stop")
|
89
|
+
@synchronizer.create_blocking
|
90
|
+
async def stop(container_id: str = typer.Argument(help="Container ID")):
|
91
|
+
"""Stop a currently-running container and reassign its in-progress inputs.
|
92
|
+
|
93
|
+
This will send the container a SIGINT signal that Modal will handle.
|
94
|
+
"""
|
48
95
|
client = await _Client.from_env()
|
49
|
-
|
96
|
+
request = api_pb2.ContainerStopRequest(task_id=container_id)
|
97
|
+
await retry_transient_errors(client.stub.ContainerStop, request)
|
modal/cli/dict.py
ADDED
@@ -0,0 +1,128 @@
|
|
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.dict import _Dict
|
14
|
+
from modal.environments import ensure_env
|
15
|
+
from modal_proto import api_pb2
|
16
|
+
|
17
|
+
dict_cli = Typer(
|
18
|
+
name="dict",
|
19
|
+
no_args_is_help=True,
|
20
|
+
help="Manage `modal.Dict` objects and inspect their contents.",
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
@dict_cli.command(name="create", rich_help_panel="Management")
|
25
|
+
@synchronizer.create_blocking
|
26
|
+
async def create(name: str, *, env: Optional[str] = ENV_OPTION):
|
27
|
+
"""Create a named Dict object.
|
28
|
+
|
29
|
+
Note: This is a no-op when the Dict already exists.
|
30
|
+
"""
|
31
|
+
d = _Dict.from_name(name, environment_name=env, create_if_missing=True)
|
32
|
+
client = await _Client.from_env()
|
33
|
+
resolver = Resolver(client=client)
|
34
|
+
await resolver.load(d)
|
35
|
+
|
36
|
+
|
37
|
+
@dict_cli.command(name="list", rich_help_panel="Management")
|
38
|
+
@synchronizer.create_blocking
|
39
|
+
async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
40
|
+
"""List all named Dicts."""
|
41
|
+
env = ensure_env(env)
|
42
|
+
client = await _Client.from_env()
|
43
|
+
request = api_pb2.DictListRequest(environment_name=env)
|
44
|
+
response = await retry_transient_errors(client.stub.DictList, request)
|
45
|
+
|
46
|
+
rows = [(d.name, timestamp_to_local(d.created_at, json)) for d in response.dicts]
|
47
|
+
display_table(["Name", "Created at"], rows, json)
|
48
|
+
|
49
|
+
|
50
|
+
@dict_cli.command("clear", rich_help_panel="Management")
|
51
|
+
@synchronizer.create_blocking
|
52
|
+
async def clear(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
|
53
|
+
"""Clear the contents of a named Dict by deleting all of its data."""
|
54
|
+
d = await _Dict.lookup(name, environment_name=env)
|
55
|
+
if not yes:
|
56
|
+
typer.confirm(
|
57
|
+
f"Are you sure you want to irrevocably delete the contents of modal.Dict '{name}'?",
|
58
|
+
default=False,
|
59
|
+
abort=True,
|
60
|
+
)
|
61
|
+
await d.clear()
|
62
|
+
|
63
|
+
|
64
|
+
@dict_cli.command(name="delete", rich_help_panel="Management")
|
65
|
+
@synchronizer.create_blocking
|
66
|
+
async def delete(name: str, *, yes: bool = YES_OPTION, env: Optional[str] = ENV_OPTION):
|
67
|
+
"""Delete a named Dict and all of its data."""
|
68
|
+
# Lookup first to validate the name, even though delete is a staticmethod
|
69
|
+
await _Dict.lookup(name, environment_name=env)
|
70
|
+
if not yes:
|
71
|
+
typer.confirm(
|
72
|
+
f"Are you sure you want to irrevocably delete the modal.Dict '{name}'?",
|
73
|
+
default=False,
|
74
|
+
abort=True,
|
75
|
+
)
|
76
|
+
await _Dict.delete(name, environment_name=env)
|
77
|
+
|
78
|
+
|
79
|
+
@dict_cli.command(name="get", rich_help_panel="Inspection")
|
80
|
+
@synchronizer.create_blocking
|
81
|
+
async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
|
82
|
+
"""Print the value for a specific key.
|
83
|
+
|
84
|
+
Note: When using the CLI, keys are always interpreted as having a string type.
|
85
|
+
"""
|
86
|
+
d = await _Dict.lookup(name, environment_name=env)
|
87
|
+
console = Console()
|
88
|
+
val = await d.get(key)
|
89
|
+
console.print(val)
|
90
|
+
|
91
|
+
|
92
|
+
def _display(input: str, use_repr: bool) -> str:
|
93
|
+
val = repr(input) if use_repr else str(input)
|
94
|
+
return val[:80] + "..." if len(val) > 80 else val
|
95
|
+
|
96
|
+
|
97
|
+
@dict_cli.command(name="items", rich_help_panel="Inspection")
|
98
|
+
@synchronizer.create_blocking
|
99
|
+
async def items(
|
100
|
+
name: str,
|
101
|
+
n: int = Argument(default=20, help="Limit the number of entries shown"),
|
102
|
+
*,
|
103
|
+
all: bool = Option(False, "-a", "--all", help="Ignore N and print all entries in the Dict (may be slow)"),
|
104
|
+
use_repr: bool = Option(False, "-r", "--repr", help="Display items using `repr()` to see more details"),
|
105
|
+
json: bool = False,
|
106
|
+
env: Optional[str] = ENV_OPTION,
|
107
|
+
):
|
108
|
+
"""Print the contents of a Dict.
|
109
|
+
|
110
|
+
Note: By default, this command truncates the contents. Use the `N` argument to control the
|
111
|
+
amount of data shown or the `--all` option to retrieve the entire Dict, which may be slow.
|
112
|
+
"""
|
113
|
+
d = await _Dict.lookup(name, environment_name=env)
|
114
|
+
|
115
|
+
i, items = 0, []
|
116
|
+
async for key, val in d.items():
|
117
|
+
i += 1
|
118
|
+
if not json and not all and i > n:
|
119
|
+
items.append(("...", "..."))
|
120
|
+
break
|
121
|
+
else:
|
122
|
+
if json:
|
123
|
+
display_item = key, val
|
124
|
+
else:
|
125
|
+
display_item = _display(key, use_repr), _display(val, use_repr) # type: ignore # mypy/issue/12056
|
126
|
+
items.append(display_item)
|
127
|
+
|
128
|
+
display_table(["Key", "Value"], items, json)
|
modal/cli/entry_point.py
CHANGED
@@ -12,10 +12,12 @@ from . import run
|
|
12
12
|
from .app import app_cli
|
13
13
|
from .config import config_cli
|
14
14
|
from .container import container_cli
|
15
|
+
from .dict import dict_cli
|
15
16
|
from .environment import environment_cli
|
16
17
|
from .launch import launch_cli
|
17
18
|
from .network_file_system import nfs_cli
|
18
19
|
from .profile import profile_cli
|
20
|
+
from .queues import queue_cli
|
19
21
|
from .secret import secret_cli
|
20
22
|
from .token import _new_token, token_cli
|
21
23
|
from .volume import volume_cli
|
@@ -81,22 +83,33 @@ async def setup(profile: Optional[str] = None):
|
|
81
83
|
await _new_token(profile=profile, next_url="/home")
|
82
84
|
|
83
85
|
|
84
|
-
|
85
|
-
entrypoint_cli_typer.
|
86
|
-
entrypoint_cli_typer.add_typer(container_cli)
|
87
|
-
entrypoint_cli_typer.add_typer(environment_cli)
|
88
|
-
entrypoint_cli_typer.add_typer(launch_cli)
|
89
|
-
entrypoint_cli_typer.add_typer(nfs_cli)
|
90
|
-
entrypoint_cli_typer.add_typer(profile_cli)
|
91
|
-
entrypoint_cli_typer.add_typer(secret_cli)
|
92
|
-
entrypoint_cli_typer.add_typer(token_cli)
|
93
|
-
entrypoint_cli_typer.add_typer(volume_cli)
|
94
|
-
|
95
|
-
entrypoint_cli_typer.command("deploy", help="Deploy a Modal stub as an application.", no_args_is_help=True)(run.deploy)
|
86
|
+
# Commands
|
87
|
+
entrypoint_cli_typer.command("deploy", help="Deploy a Modal application.", no_args_is_help=True)(run.deploy)
|
96
88
|
entrypoint_cli_typer.command("serve", no_args_is_help=True)(run.serve)
|
97
|
-
entrypoint_cli_typer.command("setup", help="Bootstrap Modal's configuration.")(setup)
|
98
89
|
entrypoint_cli_typer.command("shell")(run.shell)
|
90
|
+
entrypoint_cli_typer.add_typer(launch_cli)
|
91
|
+
|
92
|
+
# Deployments
|
93
|
+
entrypoint_cli_typer.add_typer(app_cli, rich_help_panel="Deployments")
|
94
|
+
entrypoint_cli_typer.add_typer(container_cli, rich_help_panel="Deployments")
|
95
|
+
|
96
|
+
# Storage
|
97
|
+
entrypoint_cli_typer.add_typer(dict_cli, rich_help_panel="Storage")
|
98
|
+
entrypoint_cli_typer.add_typer(nfs_cli, rich_help_panel="Storage")
|
99
|
+
entrypoint_cli_typer.add_typer(secret_cli, rich_help_panel="Storage")
|
100
|
+
entrypoint_cli_typer.add_typer(queue_cli, rich_help_panel="Storage")
|
101
|
+
entrypoint_cli_typer.add_typer(volume_cli, rich_help_panel="Storage")
|
102
|
+
|
103
|
+
# Configuration
|
104
|
+
entrypoint_cli_typer.add_typer(config_cli, rich_help_panel="Configuration")
|
105
|
+
entrypoint_cli_typer.add_typer(environment_cli, rich_help_panel="Configuration")
|
106
|
+
entrypoint_cli_typer.add_typer(profile_cli, rich_help_panel="Configuration")
|
107
|
+
entrypoint_cli_typer.add_typer(token_cli, rich_help_panel="Configuration")
|
108
|
+
|
109
|
+
# Hide setup from help as it's redundant with modal token new, but nicer for onboarding
|
110
|
+
entrypoint_cli_typer.command("setup", help="Bootstrap Modal's configuration.", rich_help_panel="Onboarding")(setup)
|
99
111
|
|
112
|
+
# Special handling for modal run, which is more complicated
|
100
113
|
entrypoint_cli = typer.main.get_command(entrypoint_cli_typer)
|
101
114
|
entrypoint_cli.add_command(run.run, name="run") # type: ignore
|
102
115
|
entrypoint_cli.list_commands(None) # type: ignore
|
modal/cli/environment.py
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
|
-
from typing import Optional
|
2
|
+
from typing import Annotated, Optional, Union
|
3
3
|
|
4
4
|
import typer
|
5
5
|
from click import UsageError
|
6
|
-
from
|
6
|
+
from grpclib import GRPCError, Status
|
7
|
+
from rich.text import Text
|
7
8
|
|
8
9
|
from modal import environments
|
10
|
+
from modal._utils.name_utils import check_environment_name
|
9
11
|
from modal.cli.utils import display_table
|
10
12
|
from modal.config import config
|
13
|
+
from modal.exception import InvalidError
|
11
14
|
|
12
15
|
ENVIRONMENT_HELP_TEXT = """Create and interact with Environments
|
13
16
|
|
@@ -24,7 +27,7 @@ while still being able to deploy changes to a live environment.
|
|
24
27
|
environment_cli = typer.Typer(name="environment", help=ENVIRONMENT_HELP_TEXT, no_args_is_help=True)
|
25
28
|
|
26
29
|
|
27
|
-
class RenderableBool:
|
30
|
+
class RenderableBool(Text):
|
28
31
|
def __init__(self, value: bool):
|
29
32
|
self.value = value
|
30
33
|
|
@@ -33,12 +36,21 @@ class RenderableBool:
|
|
33
36
|
|
34
37
|
|
35
38
|
@environment_cli.command(name="list", help="List all environments in the current workspace")
|
36
|
-
def
|
39
|
+
def list_(json: Optional[bool] = False):
|
37
40
|
envs = environments.list_environments()
|
41
|
+
|
42
|
+
# determine which environment is currently active, prioritizing the local default
|
43
|
+
# over the server default
|
44
|
+
active_env = config.get("environment")
|
45
|
+
for env in envs:
|
46
|
+
if env.default is True and active_env is None:
|
47
|
+
active_env = env.name
|
48
|
+
|
38
49
|
table_data = []
|
39
50
|
for item in envs:
|
40
|
-
is_active = item.name ==
|
41
|
-
|
51
|
+
is_active = item.name == active_env
|
52
|
+
is_active_display: Union[Text, str] = str(is_active) if json else RenderableBool(is_active)
|
53
|
+
row = [item.name, item.webhook_suffix, is_active_display]
|
42
54
|
table_data.append(row)
|
43
55
|
display_table(["name", "web suffix", "active"], table_data, json=json)
|
44
56
|
|
@@ -48,7 +60,14 @@ ENVIRONMENT_CREATE_HELP = """Create a new environment in the current workspace""
|
|
48
60
|
|
49
61
|
@environment_cli.command(name="create", help=ENVIRONMENT_CREATE_HELP)
|
50
62
|
def create(name: Annotated[str, typer.Argument(help="Name of the new environment. Must be unique. Case sensitive")]):
|
51
|
-
|
63
|
+
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
|
52
71
|
typer.echo(f"Environment created: {name}")
|
53
72
|
|
54
73
|
|
@@ -65,7 +84,10 @@ def delete(
|
|
65
84
|
):
|
66
85
|
if not confirm:
|
67
86
|
typer.confirm(
|
68
|
-
|
87
|
+
(
|
88
|
+
f"Are you sure you want to irrevocably delete the environment '{name}' and"
|
89
|
+
" all its associated apps and secrets?"
|
90
|
+
),
|
69
91
|
default=False,
|
70
92
|
abort=True,
|
71
93
|
)
|
@@ -88,5 +110,14 @@ def update(
|
|
88
110
|
if set_name is None and set_web_suffix is None:
|
89
111
|
raise UsageError("You need to at least one new property (using --set-name or --set-web-suffix)")
|
90
112
|
|
91
|
-
|
113
|
+
if set_name:
|
114
|
+
check_environment_name(set_name)
|
115
|
+
|
116
|
+
try:
|
117
|
+
environments.update_environment(current_name, new_name=set_name, new_web_suffix=set_web_suffix)
|
118
|
+
except GRPCError as exc:
|
119
|
+
if exc.status == Status.INVALID_ARGUMENT:
|
120
|
+
raise InvalidError(exc.message)
|
121
|
+
raise
|
122
|
+
|
92
123
|
typer.echo("Environment updated")
|
modal/cli/import_refs.py
CHANGED
@@ -18,9 +18,8 @@ import click
|
|
18
18
|
from rich.console import Console
|
19
19
|
from rich.markdown import Markdown
|
20
20
|
|
21
|
-
import modal
|
22
21
|
from modal.app import App, LocalEntrypoint
|
23
|
-
from modal.exception import
|
22
|
+
from modal.exception import InvalidError, _CliUserExecutionError
|
24
23
|
from modal.functions import Function
|
25
24
|
|
26
25
|
|
@@ -34,7 +33,7 @@ def parse_import_ref(object_ref: str) -> ImportRef:
|
|
34
33
|
if object_ref.find("::") > 1:
|
35
34
|
file_or_module, object_path = object_ref.split("::", 1)
|
36
35
|
elif object_ref.find(":") > 1:
|
37
|
-
raise
|
36
|
+
raise InvalidError(f"Invalid object reference: {object_ref}. Did you mean '::' instead of ':'?")
|
38
37
|
else:
|
39
38
|
file_or_module, object_path = object_ref, None
|
40
39
|
|
@@ -52,8 +51,14 @@ def import_file_or_module(file_or_module: str):
|
|
52
51
|
sys.path.insert(0, "") # "" means the current working directory
|
53
52
|
|
54
53
|
if file_or_module.endswith(".py"):
|
55
|
-
# when using a script path, that scripts directory should also be on the path as it is
|
54
|
+
# when using a script path, that scripts directory should also be on the path as it is
|
55
|
+
# with `python some/script.py`
|
56
56
|
full_path = Path(file_or_module).resolve()
|
57
|
+
if "." in full_path.name.removesuffix(".py"):
|
58
|
+
raise InvalidError(
|
59
|
+
f"Invalid Modal source filename: {full_path.name!r}."
|
60
|
+
"\n\nSource filename cannot contain additional period characters."
|
61
|
+
)
|
57
62
|
sys.path.insert(0, str(full_path.parent))
|
58
63
|
|
59
64
|
module_name = inspect.getmodulename(file_or_module)
|
@@ -74,7 +79,7 @@ def import_file_or_module(file_or_module: str):
|
|
74
79
|
return module
|
75
80
|
|
76
81
|
|
77
|
-
def get_by_object_path(obj: Any, obj_path:
|
82
|
+
def get_by_object_path(obj: Any, obj_path: str) -> Optional[Any]:
|
78
83
|
# Try to evaluate a `.`-delimited object path in a Modal context
|
79
84
|
# With the caveat that some object names can actually have `.` in their name (lifecycled methods' tags)
|
80
85
|
|
@@ -102,40 +107,10 @@ def get_by_object_path(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
|
102
107
|
return obj
|
103
108
|
|
104
109
|
|
105
|
-
def get_by_object_path_try_possible_app_names(obj: Any, obj_path: Optional[str]) -> Optional[Any]:
|
106
|
-
"""This just exists as a dumb workaround to support both "stub" and "app" """
|
107
|
-
|
108
|
-
if obj_path:
|
109
|
-
return get_by_object_path(obj, obj_path)
|
110
|
-
else:
|
111
|
-
app = get_by_object_path(obj, DEFAULT_APP_NAME)
|
112
|
-
stub = get_by_object_path(obj, "stub")
|
113
|
-
if isinstance(app, App):
|
114
|
-
return app
|
115
|
-
elif app is not None and isinstance(stub, App):
|
116
|
-
deprecation_warning(
|
117
|
-
(2024, 4, 20),
|
118
|
-
"The symbol `app` is present at the module level but it's not a Modal app."
|
119
|
-
" We will use `stub` instead, but this will not work in future Modal versions."
|
120
|
-
" Suggestion: change the name of `app` to something else.",
|
121
|
-
)
|
122
|
-
return stub
|
123
|
-
elif isinstance(stub, App):
|
124
|
-
# TODO(erikbern): enable this deprecation warning shortly
|
125
|
-
# deprecation_warning(
|
126
|
-
# (2024, 4, 20),
|
127
|
-
# "The symbol `app` is not present but `stub` is. This will not work in future"
|
128
|
-
# " Modal versions. Suggestion: change the name of `stub` to `app`."
|
129
|
-
# )
|
130
|
-
return stub
|
131
|
-
else:
|
132
|
-
return None
|
133
|
-
|
134
|
-
|
135
110
|
def _infer_function_or_help(
|
136
111
|
app: App, module, accept_local_entrypoint: bool, accept_webhook: bool
|
137
112
|
) -> Union[Function, LocalEntrypoint]:
|
138
|
-
function_choices = set(app.registered_functions
|
113
|
+
function_choices = set(app.registered_functions)
|
139
114
|
if not accept_webhook:
|
140
115
|
function_choices -= set(app.registered_web_endpoints)
|
141
116
|
if accept_local_entrypoint:
|
@@ -179,7 +154,7 @@ Registered functions and local entrypoints on the selected app are:
|
|
179
154
|
# entrypoint is in entrypoint registry, for now
|
180
155
|
return app.registered_entrypoints[function_name]
|
181
156
|
|
182
|
-
function = app.
|
157
|
+
function = app.registered_functions[function_name]
|
183
158
|
assert isinstance(function, Function)
|
184
159
|
return function
|
185
160
|
|
@@ -192,7 +167,8 @@ def _show_no_auto_detectable_app(app_ref: ImportRef) -> None:
|
|
192
167
|
|
193
168
|
if object_path is None:
|
194
169
|
guidance_msg = (
|
195
|
-
f"Expected to find an app variable named **`{DEFAULT_APP_NAME}`** (the default app name).
|
170
|
+
f"Expected to find an app variable named **`{DEFAULT_APP_NAME}`** (the default app name). "
|
171
|
+
"If your `modal.App` is named differently, "
|
196
172
|
"you must specify it in the app ref argument. "
|
197
173
|
f"For example an App variable `app_2 = modal.App()` in `{import_path}` would "
|
198
174
|
f"be specified as `{import_path}::app_2`."
|
@@ -205,7 +181,7 @@ def import_app(app_ref: str) -> App:
|
|
205
181
|
import_ref = parse_import_ref(app_ref)
|
206
182
|
|
207
183
|
module = import_file_or_module(import_ref.file_or_module)
|
208
|
-
app =
|
184
|
+
app = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
209
185
|
|
210
186
|
if app is None:
|
211
187
|
_show_no_auto_detectable_app(import_ref)
|
@@ -223,11 +199,13 @@ def _show_function_ref_help(app_ref: ImportRef, base_cmd: str) -> None:
|
|
223
199
|
error_console = Console(stderr=True)
|
224
200
|
if object_path:
|
225
201
|
error_console.print(
|
226
|
-
f"[bold red]Could not find Modal function or local entrypoint
|
202
|
+
f"[bold red]Could not find Modal function or local entrypoint"
|
203
|
+
f" '{object_path}' in '{import_path}'.[/bold red]"
|
227
204
|
)
|
228
205
|
else:
|
229
206
|
error_console.print(
|
230
|
-
f"[bold red]No function was specified, and no [green]`app`[/green] variable
|
207
|
+
f"[bold red]No function was specified, and no [green]`app`[/green] variable "
|
208
|
+
f"could be found in '{import_path}'.[/bold red]"
|
231
209
|
)
|
232
210
|
guidance_msg = f"""
|
233
211
|
Usage:
|
@@ -235,7 +213,7 @@ Usage:
|
|
235
213
|
|
236
214
|
Given the following example `app.py`:
|
237
215
|
```
|
238
|
-
app = modal.App()
|
216
|
+
app = modal.App()
|
239
217
|
|
240
218
|
@app.function()
|
241
219
|
def foo():
|
@@ -251,7 +229,7 @@ def import_function(
|
|
251
229
|
import_ref = parse_import_ref(func_ref)
|
252
230
|
|
253
231
|
module = import_file_or_module(import_ref.file_or_module)
|
254
|
-
app_or_function =
|
232
|
+
app_or_function = get_by_object_path(module, import_ref.object_path or DEFAULT_APP_NAME)
|
255
233
|
|
256
234
|
if app_or_function is None:
|
257
235
|
_show_function_ref_help(import_ref, base_cmd)
|
@@ -272,8 +250,3 @@ def import_function(
|
|
272
250
|
return app_or_function
|
273
251
|
else:
|
274
252
|
raise click.UsageError(f"{app_or_function} is not a Modal entity (should be an App or Function)")
|
275
|
-
|
276
|
-
|
277
|
-
# For backwards compatibility - delete soon
|
278
|
-
# We use it in our internal intergration tests
|
279
|
-
import_stub = import_app
|
modal/cli/launch.py
CHANGED
@@ -4,12 +4,13 @@ import inspect
|
|
4
4
|
import json
|
5
5
|
import os
|
6
6
|
from pathlib import Path
|
7
|
-
from typing import Any,
|
7
|
+
from typing import Any, Optional
|
8
8
|
|
9
9
|
from typer import Typer
|
10
10
|
|
11
11
|
from ..app import App
|
12
12
|
from ..exception import _CliUserExecutionError
|
13
|
+
from ..output import enable_output
|
13
14
|
from ..runner import run_app
|
14
15
|
from .import_refs import import_function
|
15
16
|
|
@@ -17,15 +18,15 @@ launch_cli = Typer(
|
|
17
18
|
name="launch",
|
18
19
|
no_args_is_help=True,
|
19
20
|
help="""
|
20
|
-
|
21
|
+
Open a serverless app instance on Modal.
|
21
22
|
|
22
23
|
This command is in preview and may change in the future.
|
23
24
|
""",
|
24
25
|
)
|
25
26
|
|
26
27
|
|
27
|
-
def _launch_program(name: str, filename: str, args:
|
28
|
-
os.environ["
|
28
|
+
def _launch_program(name: str, filename: str, detach: bool, args: dict[str, Any]) -> None:
|
29
|
+
os.environ["MODAL_LAUNCH_ARGS"] = json.dumps(args)
|
29
30
|
|
30
31
|
program_path = str(Path(__file__).parent / "programs" / filename)
|
31
32
|
entrypoint = import_function(program_path, "modal launch")
|
@@ -35,14 +36,15 @@ def _launch_program(name: str, filename: str, args: Dict[str, Any]) -> None:
|
|
35
36
|
# `launch/` scripts must have a `local_entrypoint()` with no args, for simplicity here.
|
36
37
|
func = entrypoint.info.raw_f
|
37
38
|
isasync = inspect.iscoroutinefunction(func)
|
38
|
-
with
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
39
|
+
with enable_output():
|
40
|
+
with run_app(app, detach=detach):
|
41
|
+
try:
|
42
|
+
if isasync:
|
43
|
+
asyncio.run(func())
|
44
|
+
else:
|
45
|
+
func()
|
46
|
+
except Exception as exc:
|
47
|
+
raise _CliUserExecutionError(inspect.getsourcefile(func)) from exc
|
46
48
|
|
47
49
|
|
48
50
|
@launch_cli.command(name="jupyter", help="Start Jupyter Lab on Modal.")
|
@@ -53,6 +55,9 @@ def jupyter(
|
|
53
55
|
timeout: int = 3600,
|
54
56
|
image: str = "ubuntu:22.04",
|
55
57
|
add_python: Optional[str] = "3.11",
|
58
|
+
mount: Optional[str] = None, # Adds a local directory to the jupyter container
|
59
|
+
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
60
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
56
61
|
):
|
57
62
|
args = {
|
58
63
|
"cpu": cpu,
|
@@ -61,8 +66,10 @@ def jupyter(
|
|
61
66
|
"timeout": timeout,
|
62
67
|
"image": image,
|
63
68
|
"add_python": add_python,
|
69
|
+
"mount": mount,
|
70
|
+
"volume": volume,
|
64
71
|
}
|
65
|
-
_launch_program("jupyter", "run_jupyter.py", args)
|
72
|
+
_launch_program("jupyter", "run_jupyter.py", detach, args)
|
66
73
|
|
67
74
|
|
68
75
|
@launch_cli.command(name="vscode", help="Start Visual Studio Code on Modal.")
|
@@ -70,12 +77,19 @@ def vscode(
|
|
70
77
|
cpu: int = 8,
|
71
78
|
memory: int = 32768,
|
72
79
|
gpu: Optional[str] = None,
|
80
|
+
image: str = "debian:12",
|
73
81
|
timeout: int = 3600,
|
82
|
+
mount: Optional[str] = None, # Create a `modal.Mount` from a local directory.
|
83
|
+
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
84
|
+
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
74
85
|
):
|
75
86
|
args = {
|
76
87
|
"cpu": cpu,
|
77
88
|
"memory": memory,
|
78
89
|
"gpu": gpu,
|
90
|
+
"image": image,
|
79
91
|
"timeout": timeout,
|
92
|
+
"mount": mount,
|
93
|
+
"volume": volume,
|
80
94
|
}
|
81
|
-
_launch_program("vscode", "vscode.py", args)
|
95
|
+
_launch_program("vscode", "vscode.py", detach, args)
|