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/call_graph.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
from dataclasses import dataclass
|
3
3
|
from enum import IntEnum
|
4
|
-
from typing import
|
4
|
+
from typing import Optional
|
5
5
|
|
6
6
|
from modal_proto import api_pb2
|
7
7
|
|
@@ -12,6 +12,7 @@ class InputStatus(IntEnum):
|
|
12
12
|
PENDING = 0
|
13
13
|
SUCCESS = api_pb2.GenericResult.GENERIC_STATUS_SUCCESS
|
14
14
|
FAILURE = api_pb2.GenericResult.GENERIC_STATUS_FAILURE
|
15
|
+
INIT_FAILURE = api_pb2.GenericResult.GENERIC_STATUS_INIT_FAILURE
|
15
16
|
TERMINATED = api_pb2.GenericResult.GENERIC_STATUS_TERMINATED
|
16
17
|
TIMEOUT = api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT
|
17
18
|
|
@@ -30,12 +31,12 @@ class InputInfo:
|
|
30
31
|
status: InputStatus
|
31
32
|
function_name: str
|
32
33
|
module_name: str
|
33
|
-
children:
|
34
|
+
children: list["InputInfo"]
|
34
35
|
|
35
36
|
|
36
|
-
def _reconstruct_call_graph(ser_graph: api_pb2.FunctionGetCallGraphResponse) ->
|
37
|
-
function_calls_by_id:
|
38
|
-
inputs_by_id:
|
37
|
+
def _reconstruct_call_graph(ser_graph: api_pb2.FunctionGetCallGraphResponse) -> list[InputInfo]:
|
38
|
+
function_calls_by_id: dict[str, api_pb2.FunctionCallCallGraphInfo] = {}
|
39
|
+
inputs_by_id: dict[str, api_pb2.InputCallGraphInfo] = {}
|
39
40
|
|
40
41
|
for function_call in ser_graph.function_calls:
|
41
42
|
function_calls_by_id[function_call.function_call_id] = function_call
|
@@ -43,7 +44,7 @@ def _reconstruct_call_graph(ser_graph: api_pb2.FunctionGetCallGraphResponse) ->
|
|
43
44
|
for input in ser_graph.inputs:
|
44
45
|
inputs_by_id[input.input_id] = input
|
45
46
|
|
46
|
-
input_info_by_id:
|
47
|
+
input_info_by_id: dict[str, InputInfo] = {}
|
47
48
|
result = []
|
48
49
|
|
49
50
|
def _reconstruct(input_id: str) -> Optional[InputInfo]:
|
modal/cli/_download.py
CHANGED
@@ -3,11 +3,14 @@ import asyncio
|
|
3
3
|
import os
|
4
4
|
import shutil
|
5
5
|
import sys
|
6
|
-
from
|
7
|
-
from
|
6
|
+
from collections.abc import AsyncIterator
|
7
|
+
from pathlib import Path, PurePosixPath
|
8
|
+
from typing import Callable, Optional, Union
|
8
9
|
|
9
10
|
from click import UsageError
|
10
11
|
|
12
|
+
from modal._utils.async_utils import TaskContext
|
13
|
+
from modal.config import logger
|
11
14
|
from modal.network_file_system import _NetworkFileSystem
|
12
15
|
from modal.volume import FileEntry, FileEntryType, _Volume
|
13
16
|
|
@@ -19,10 +22,11 @@ async def _volume_download(
|
|
19
22
|
remote_path: str,
|
20
23
|
local_destination: Path,
|
21
24
|
overwrite: bool,
|
25
|
+
progress_cb: Callable,
|
22
26
|
):
|
23
27
|
is_pipe = local_destination == PIPE_PATH
|
24
28
|
|
25
|
-
q: asyncio.Queue[
|
29
|
+
q: asyncio.Queue[tuple[Optional[Path], Optional[FileEntry]]] = asyncio.Queue()
|
26
30
|
num_consumers = 1 if is_pipe else 10 # concurrency limit for downloading files
|
27
31
|
|
28
32
|
async def producer():
|
@@ -36,7 +40,12 @@ async def _volume_download(
|
|
36
40
|
if is_pipe:
|
37
41
|
await q.put((None, entry))
|
38
42
|
else:
|
39
|
-
|
43
|
+
start_path = Path(remote_path).parent.as_posix().split("*")[0]
|
44
|
+
rel_path = PurePosixPath(entry.path).relative_to(start_path.lstrip("/"))
|
45
|
+
if local_destination.is_dir():
|
46
|
+
output_path = local_destination / rel_path
|
47
|
+
else:
|
48
|
+
output_path = local_destination
|
40
49
|
if output_path.exists():
|
41
50
|
if overwrite:
|
42
51
|
if output_path.is_file():
|
@@ -60,21 +69,28 @@ async def _volume_download(
|
|
60
69
|
try:
|
61
70
|
if is_pipe:
|
62
71
|
if entry.type == FileEntryType.FILE:
|
72
|
+
progress_task_id = progress_cb(name=entry.path, size=entry.size)
|
63
73
|
async for chunk in volume.read_file(entry.path):
|
64
74
|
sys.stdout.buffer.write(chunk)
|
75
|
+
progress_cb(task_id=progress_task_id, advance=len(chunk))
|
76
|
+
progress_cb(task_id=progress_task_id, complete=True)
|
65
77
|
else:
|
66
78
|
if entry.type == FileEntryType.FILE:
|
79
|
+
progress_task_id = progress_cb(name=entry.path, size=entry.size)
|
67
80
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
68
81
|
with output_path.open("wb") as fp:
|
69
82
|
b = 0
|
70
83
|
async for chunk in volume.read_file(entry.path):
|
71
84
|
b += fp.write(chunk)
|
72
|
-
|
85
|
+
progress_cb(task_id=progress_task_id, advance=len(chunk))
|
86
|
+
logger.debug(f"Wrote {b} bytes to {output_path}")
|
87
|
+
progress_cb(task_id=progress_task_id, complete=True)
|
73
88
|
elif entry.type == FileEntryType.DIRECTORY:
|
74
89
|
output_path.mkdir(parents=True, exist_ok=True)
|
75
90
|
finally:
|
76
91
|
q.task_done()
|
77
92
|
|
78
93
|
consumers = [consumer() for _ in range(num_consumers)]
|
79
|
-
await
|
94
|
+
await TaskContext.gather(producer(), *consumers)
|
95
|
+
progress_cb(complete=True)
|
80
96
|
sys.stdout.flush()
|
modal/cli/_traceback.py
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
"""Helper functions related to displaying tracebacks in the CLI."""
|
3
|
+
|
4
|
+
import functools
|
5
|
+
import re
|
6
|
+
import warnings
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
from rich.console import Console, RenderResult, group
|
10
|
+
from rich.panel import Panel
|
11
|
+
from rich.syntax import Syntax
|
12
|
+
from rich.text import Text
|
13
|
+
from rich.traceback import PathHighlighter, Stack, Traceback, install
|
14
|
+
|
15
|
+
from ..exception import DeprecationError, PendingDeprecationError, ServerWarning
|
16
|
+
|
17
|
+
|
18
|
+
@group()
|
19
|
+
def _render_stack(self, stack: Stack) -> RenderResult:
|
20
|
+
"""Patched variant of rich.Traceback._render_stack that uses the line from the modal StackSummary,
|
21
|
+
when the file isn't available to be read locally."""
|
22
|
+
|
23
|
+
path_highlighter = PathHighlighter()
|
24
|
+
theme = self.theme
|
25
|
+
code_cache: dict[str, str] = {}
|
26
|
+
line_cache = getattr(stack, "line_cache", {})
|
27
|
+
task_id = None
|
28
|
+
|
29
|
+
def read_code(filename: str) -> str:
|
30
|
+
code = code_cache.get(filename)
|
31
|
+
if code is None:
|
32
|
+
with open(filename, encoding="utf-8", errors="replace") as code_file:
|
33
|
+
code = code_file.read()
|
34
|
+
code_cache[filename] = code
|
35
|
+
return code
|
36
|
+
|
37
|
+
exclude_frames: Optional[range] = None
|
38
|
+
if self.max_frames != 0:
|
39
|
+
exclude_frames = range(
|
40
|
+
self.max_frames // 2,
|
41
|
+
len(stack.frames) - self.max_frames // 2,
|
42
|
+
)
|
43
|
+
|
44
|
+
excluded = False
|
45
|
+
for frame_index, frame in enumerate(stack.frames):
|
46
|
+
if exclude_frames and frame_index in exclude_frames:
|
47
|
+
excluded = True
|
48
|
+
continue
|
49
|
+
|
50
|
+
if excluded:
|
51
|
+
assert exclude_frames is not None
|
52
|
+
yield Text(
|
53
|
+
f"\n... {len(exclude_frames)} frames hidden ...",
|
54
|
+
justify="center",
|
55
|
+
style="traceback.error",
|
56
|
+
)
|
57
|
+
excluded = False
|
58
|
+
|
59
|
+
first = frame_index == 0
|
60
|
+
# Patched Modal-specific code.
|
61
|
+
if frame.filename.startswith("<") and ":" in frame.filename:
|
62
|
+
next_task_id, frame_filename = frame.filename.split(":", 1)
|
63
|
+
next_task_id = next_task_id.strip("<>")
|
64
|
+
else:
|
65
|
+
frame_filename = frame.filename
|
66
|
+
next_task_id = None
|
67
|
+
suppressed = any(frame_filename.startswith(path) for path in self.suppress)
|
68
|
+
|
69
|
+
if next_task_id != task_id:
|
70
|
+
task_id = next_task_id
|
71
|
+
yield ""
|
72
|
+
yield Text(
|
73
|
+
f"...Remote call to Modal Function ({task_id})...",
|
74
|
+
justify="center",
|
75
|
+
style="green",
|
76
|
+
)
|
77
|
+
|
78
|
+
text = Text.assemble(
|
79
|
+
path_highlighter(Text(frame_filename, style="pygments.string")),
|
80
|
+
(":", "pygments.text"),
|
81
|
+
(str(frame.lineno), "pygments.number"),
|
82
|
+
" in ",
|
83
|
+
(frame.name, "pygments.function"),
|
84
|
+
style="pygments.text",
|
85
|
+
)
|
86
|
+
if not frame_filename.startswith("<") and not first:
|
87
|
+
yield ""
|
88
|
+
|
89
|
+
yield text
|
90
|
+
if not suppressed:
|
91
|
+
try:
|
92
|
+
code = read_code(frame_filename)
|
93
|
+
lexer_name = self._guess_lexer(frame_filename, code)
|
94
|
+
syntax = Syntax(
|
95
|
+
code,
|
96
|
+
lexer_name,
|
97
|
+
theme=theme,
|
98
|
+
line_numbers=True,
|
99
|
+
line_range=(
|
100
|
+
frame.lineno - self.extra_lines,
|
101
|
+
frame.lineno + self.extra_lines,
|
102
|
+
),
|
103
|
+
highlight_lines={frame.lineno},
|
104
|
+
word_wrap=self.word_wrap,
|
105
|
+
code_width=88,
|
106
|
+
indent_guides=self.indent_guides,
|
107
|
+
dedent=False,
|
108
|
+
)
|
109
|
+
yield ""
|
110
|
+
except Exception as error:
|
111
|
+
# Patched Modal-specific code.
|
112
|
+
line = line_cache.get((frame_filename, frame.lineno))
|
113
|
+
if line:
|
114
|
+
try:
|
115
|
+
lexer_name = self._guess_lexer(frame_filename, line)
|
116
|
+
yield ""
|
117
|
+
yield Syntax(
|
118
|
+
line,
|
119
|
+
lexer_name,
|
120
|
+
theme=theme,
|
121
|
+
line_numbers=True,
|
122
|
+
line_range=(0, 1),
|
123
|
+
highlight_lines={frame.lineno},
|
124
|
+
word_wrap=self.word_wrap,
|
125
|
+
code_width=88,
|
126
|
+
indent_guides=self.indent_guides,
|
127
|
+
dedent=False,
|
128
|
+
start_line=frame.lineno,
|
129
|
+
)
|
130
|
+
except Exception:
|
131
|
+
yield Text.assemble(
|
132
|
+
(f"\n{error}", "traceback.error"),
|
133
|
+
)
|
134
|
+
yield ""
|
135
|
+
else:
|
136
|
+
yield syntax
|
137
|
+
|
138
|
+
|
139
|
+
def setup_rich_traceback() -> None:
|
140
|
+
from_exception = Traceback.from_exception
|
141
|
+
|
142
|
+
@functools.wraps(Traceback.from_exception)
|
143
|
+
def _from_exception(exc_type, exc_value, *args, **kwargs):
|
144
|
+
"""Patch from_exception to grab the Modal line_cache and store it with the
|
145
|
+
Stack object, so it's available to render_stack at display time."""
|
146
|
+
|
147
|
+
line_cache = getattr(exc_value, "__line_cache__", {})
|
148
|
+
tb = from_exception(exc_type, exc_value, *args, **kwargs)
|
149
|
+
for stack in tb.trace.stacks:
|
150
|
+
stack.line_cache = line_cache # type: ignore
|
151
|
+
return tb
|
152
|
+
|
153
|
+
Traceback._render_stack = _render_stack # type: ignore
|
154
|
+
Traceback.from_exception = _from_exception # type: ignore
|
155
|
+
|
156
|
+
import click
|
157
|
+
import grpclib
|
158
|
+
import synchronicity
|
159
|
+
import typer
|
160
|
+
|
161
|
+
install(suppress=[synchronicity, grpclib, click, typer], extra_lines=1)
|
162
|
+
|
163
|
+
|
164
|
+
def highlight_modal_deprecation_warnings() -> None:
|
165
|
+
"""Patch the warnings module to make client deprecation warnings more salient in the CLI."""
|
166
|
+
base_showwarning = warnings.showwarning
|
167
|
+
|
168
|
+
def showwarning(warning, category, filename, lineno, file=None, line=None):
|
169
|
+
if issubclass(category, (DeprecationError, PendingDeprecationError, ServerWarning)):
|
170
|
+
content = str(warning)
|
171
|
+
if re.match(r"^\d{4}-\d{2}-\d{2}", content):
|
172
|
+
date = content[:10]
|
173
|
+
message = content[11:].strip()
|
174
|
+
else:
|
175
|
+
date = ""
|
176
|
+
message = content
|
177
|
+
try:
|
178
|
+
with open(filename, encoding="utf-8", errors="replace") as code_file:
|
179
|
+
source = code_file.readlines()[lineno - 1].strip()
|
180
|
+
message = f"{message}\n\nSource: {filename}:{lineno}\n {source}"
|
181
|
+
except OSError:
|
182
|
+
# e.g., when filename is "<unknown>"; raises FileNotFoundError on posix but OSError on windows
|
183
|
+
pass
|
184
|
+
if issubclass(category, ServerWarning):
|
185
|
+
title = "Modal Warning"
|
186
|
+
else:
|
187
|
+
title = "Modal Deprecation Warning"
|
188
|
+
if date:
|
189
|
+
title += f" ({date})"
|
190
|
+
panel = Panel(
|
191
|
+
message,
|
192
|
+
border_style="yellow",
|
193
|
+
title=title,
|
194
|
+
title_align="left",
|
195
|
+
)
|
196
|
+
Console().print(panel)
|
197
|
+
else:
|
198
|
+
base_showwarning(warning, category, filename, lineno, file=None, line=None)
|
199
|
+
|
200
|
+
warnings.showwarning = showwarning
|
modal/cli/app.py
CHANGED
@@ -1,88 +1,249 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
import
|
3
|
-
from typing import
|
2
|
+
import re
|
3
|
+
from typing import Optional, Union
|
4
4
|
|
5
|
+
import rich
|
5
6
|
import typer
|
6
7
|
from click import UsageError
|
7
|
-
from
|
8
|
+
from rich.table import Column
|
8
9
|
from rich.text import Text
|
10
|
+
from typer import Argument
|
9
11
|
|
10
|
-
from modal._output import OutputManager, get_app_logs_loop
|
11
12
|
from modal._utils.async_utils import synchronizer
|
12
|
-
from modal.
|
13
|
-
from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
|
13
|
+
from modal._utils.deprecation import deprecation_warning
|
14
14
|
from modal.client import _Client
|
15
15
|
from modal.environments import ensure_env
|
16
|
+
from modal.object import _get_environment_name
|
16
17
|
from modal_proto import api_pb2
|
17
18
|
|
19
|
+
from .utils import ENV_OPTION, display_table, get_app_id_from_name, stream_app_logs, timestamp_to_local
|
20
|
+
|
21
|
+
APP_IDENTIFIER = Argument("", help="App name or ID")
|
22
|
+
NAME_OPTION = typer.Option("", "-n", "--name", help="Deprecated: Pass App name as a positional argument")
|
23
|
+
|
18
24
|
app_cli = typer.Typer(name="app", help="Manage deployed and running apps.", no_args_is_help=True)
|
19
25
|
|
20
26
|
APP_STATE_TO_MESSAGE = {
|
21
27
|
api_pb2.APP_STATE_DEPLOYED: Text("deployed", style="green"),
|
22
|
-
api_pb2.APP_STATE_DETACHED: Text("
|
28
|
+
api_pb2.APP_STATE_DETACHED: Text("ephemeral (detached)", style="green"),
|
23
29
|
api_pb2.APP_STATE_DISABLED: Text("disabled", style="dim"),
|
24
|
-
api_pb2.APP_STATE_EPHEMERAL: Text("
|
30
|
+
api_pb2.APP_STATE_EPHEMERAL: Text("ephemeral", style="green"),
|
25
31
|
api_pb2.APP_STATE_INITIALIZING: Text("initializing...", style="green"),
|
26
32
|
api_pb2.APP_STATE_STOPPED: Text("stopped", style="blue"),
|
27
33
|
api_pb2.APP_STATE_STOPPING: Text("stopping...", style="blue"),
|
28
34
|
}
|
29
35
|
|
30
36
|
|
37
|
+
@synchronizer.create_blocking
|
38
|
+
async def get_app_id(app_identifier: str, env: Optional[str], client: Optional[_Client] = None) -> str:
|
39
|
+
"""Resolve an app_identifier that may be a name or an ID into an ID."""
|
40
|
+
if re.match(r"^ap-[a-zA-Z0-9]{22}$", app_identifier):
|
41
|
+
return app_identifier
|
42
|
+
return await get_app_id_from_name.aio(app_identifier, env, client)
|
43
|
+
|
44
|
+
|
45
|
+
def warn_on_name_option(command: str, app_identifier: str, name: str) -> str:
|
46
|
+
if name:
|
47
|
+
message = (
|
48
|
+
"Passing an App name using --name is deprecated;"
|
49
|
+
" App names can now be passed directly as positional arguments:"
|
50
|
+
f"\n\n modal app {command} {name} ..."
|
51
|
+
)
|
52
|
+
deprecation_warning((2024, 8, 15), message, show_source=False)
|
53
|
+
return name
|
54
|
+
return app_identifier
|
55
|
+
|
56
|
+
|
31
57
|
@app_cli.command("list")
|
32
58
|
@synchronizer.create_blocking
|
33
|
-
async def
|
34
|
-
"""List
|
59
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
60
|
+
"""List Modal apps that are currently deployed/running or recently stopped."""
|
35
61
|
env = ensure_env(env)
|
62
|
+
client = await _Client.from_env()
|
36
63
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
for app_stats in apps:
|
41
|
-
state = APP_STATE_TO_MESSAGE.get(app_stats.state, Text("unknown", style="gray"))
|
64
|
+
resp: api_pb2.AppListResponse = await client.stub.AppList(
|
65
|
+
api_pb2.AppListRequest(environment_name=_get_environment_name(env))
|
66
|
+
)
|
42
67
|
|
68
|
+
columns: list[Union[Column, str]] = [
|
69
|
+
Column("App ID", min_width=25), # Ensure that App ID is not truncated in slim terminals
|
70
|
+
"Description",
|
71
|
+
"State",
|
72
|
+
"Tasks",
|
73
|
+
"Created at",
|
74
|
+
"Stopped at",
|
75
|
+
]
|
76
|
+
rows: list[list[Union[Text, str]]] = []
|
77
|
+
for app_stats in resp.apps:
|
78
|
+
state = APP_STATE_TO_MESSAGE.get(app_stats.state, Text("unknown", style="gray"))
|
43
79
|
rows.append(
|
44
80
|
[
|
45
81
|
app_stats.app_id,
|
46
82
|
app_stats.description,
|
47
83
|
state,
|
84
|
+
str(app_stats.n_running_tasks),
|
48
85
|
timestamp_to_local(app_stats.created_at, json),
|
49
86
|
timestamp_to_local(app_stats.stopped_at, json),
|
50
87
|
]
|
51
88
|
)
|
52
89
|
|
53
90
|
env_part = f" in environment '{env}'" if env else ""
|
54
|
-
display_table(
|
55
|
-
|
56
|
-
|
57
|
-
@app_cli.command("logs")
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
91
|
+
display_table(columns, rows, json, title=f"Apps{env_part}")
|
92
|
+
|
93
|
+
|
94
|
+
@app_cli.command("logs", no_args_is_help=True)
|
95
|
+
def logs(
|
96
|
+
app_identifier: str = APP_IDENTIFIER,
|
97
|
+
*,
|
98
|
+
name: str = NAME_OPTION,
|
99
|
+
env: Optional[str] = ENV_OPTION,
|
100
|
+
):
|
101
|
+
"""Show App logs, streaming while active.
|
102
|
+
|
103
|
+
**Examples:**
|
104
|
+
|
105
|
+
Get the logs based on an app ID:
|
106
|
+
|
107
|
+
```
|
108
|
+
modal app logs ap-123456
|
109
|
+
```
|
110
|
+
|
111
|
+
Get the logs for a currently deployed App based on its name:
|
112
|
+
|
113
|
+
```
|
114
|
+
modal app logs my-app
|
115
|
+
```
|
116
|
+
|
117
|
+
"""
|
118
|
+
app_identifier = warn_on_name_option("logs", app_identifier, name)
|
119
|
+
app_id = get_app_id(app_identifier, env)
|
120
|
+
stream_app_logs(app_id)
|
121
|
+
|
122
|
+
|
123
|
+
@app_cli.command("rollback", no_args_is_help=True, context_settings={"ignore_unknown_options": True})
|
124
|
+
@synchronizer.create_blocking
|
125
|
+
async def rollback(
|
126
|
+
app_identifier: str = APP_IDENTIFIER,
|
127
|
+
version: str = typer.Argument("", help="Target version for rollback."),
|
128
|
+
*,
|
129
|
+
env: Optional[str] = ENV_OPTION,
|
130
|
+
):
|
131
|
+
"""Redeploy a previous version of an App.
|
132
|
+
|
133
|
+
Note that the App must currently be in a "deployed" state.
|
134
|
+
Rollbacks will appear as a new deployment in the App history, although
|
135
|
+
the App state will be reset to the state at the time of the previous deployment.
|
136
|
+
|
137
|
+
**Examples:**
|
138
|
+
|
139
|
+
Rollback an App to its previous version:
|
140
|
+
|
141
|
+
```
|
142
|
+
modal app rollback my-app
|
143
|
+
```
|
144
|
+
|
145
|
+
Rollback an App to a specific version:
|
146
|
+
|
147
|
+
```
|
148
|
+
modal app rollback my-app v3
|
149
|
+
```
|
150
|
+
|
151
|
+
Rollback an App using its App ID instead of its name:
|
152
|
+
|
153
|
+
```
|
154
|
+
modal app rollback ap-abcdefghABCDEFGH123456
|
155
|
+
```
|
156
|
+
|
157
|
+
"""
|
158
|
+
env = ensure_env(env)
|
159
|
+
client = await _Client.from_env()
|
160
|
+
app_id = await get_app_id.aio(app_identifier, env, client)
|
161
|
+
if not version:
|
162
|
+
version_number = -1
|
163
|
+
else:
|
164
|
+
if m := re.match(r"v(\d+)", version):
|
165
|
+
version_number = int(m.group(1))
|
76
166
|
else:
|
77
|
-
raise
|
78
|
-
|
79
|
-
|
167
|
+
raise UsageError(f"Invalid version specifer: {version}")
|
168
|
+
req = api_pb2.AppRollbackRequest(app_id=app_id, version=version_number)
|
169
|
+
await client.stub.AppRollback(req)
|
170
|
+
rich.print("[green]✓[/green] Deployment rollback successful!")
|
80
171
|
|
81
172
|
|
82
|
-
@app_cli.command("stop")
|
173
|
+
@app_cli.command("stop", no_args_is_help=True)
|
83
174
|
@synchronizer.create_blocking
|
84
|
-
async def stop(
|
175
|
+
async def stop(
|
176
|
+
app_identifier: str = APP_IDENTIFIER,
|
177
|
+
*,
|
178
|
+
name: str = NAME_OPTION,
|
179
|
+
env: Optional[str] = ENV_OPTION,
|
180
|
+
):
|
85
181
|
"""Stop an app."""
|
182
|
+
app_identifier = warn_on_name_option("stop", app_identifier, name)
|
86
183
|
client = await _Client.from_env()
|
184
|
+
app_id = await get_app_id.aio(app_identifier, env)
|
87
185
|
req = api_pb2.AppStopRequest(app_id=app_id, source=api_pb2.APP_STOP_SOURCE_CLI)
|
88
186
|
await client.stub.AppStop(req)
|
187
|
+
|
188
|
+
|
189
|
+
@app_cli.command("history", no_args_is_help=True)
|
190
|
+
@synchronizer.create_blocking
|
191
|
+
async def history(
|
192
|
+
app_identifier: str = APP_IDENTIFIER,
|
193
|
+
*,
|
194
|
+
env: Optional[str] = ENV_OPTION,
|
195
|
+
name: str = NAME_OPTION,
|
196
|
+
json: bool = False,
|
197
|
+
):
|
198
|
+
"""Show App deployment history, for a currently deployed app
|
199
|
+
|
200
|
+
**Examples:**
|
201
|
+
|
202
|
+
Get the history based on an app ID:
|
203
|
+
|
204
|
+
```
|
205
|
+
modal app history ap-123456
|
206
|
+
```
|
207
|
+
|
208
|
+
Get the history for a currently deployed App based on its name:
|
209
|
+
|
210
|
+
```
|
211
|
+
modal app history my-app
|
212
|
+
```
|
213
|
+
|
214
|
+
"""
|
215
|
+
app_identifier = warn_on_name_option("history", app_identifier, name)
|
216
|
+
env = ensure_env(env)
|
217
|
+
client = await _Client.from_env()
|
218
|
+
app_id = await get_app_id.aio(app_identifier, env, client)
|
219
|
+
resp = await client.stub.AppDeploymentHistory(api_pb2.AppDeploymentHistoryRequest(app_id=app_id))
|
220
|
+
|
221
|
+
columns = [
|
222
|
+
"Version",
|
223
|
+
"Time deployed",
|
224
|
+
"Client",
|
225
|
+
"Deployed by",
|
226
|
+
]
|
227
|
+
rows = []
|
228
|
+
deployments_with_tags = False
|
229
|
+
for idx, app_stats in enumerate(resp.app_deployment_histories):
|
230
|
+
style = "bold green" if idx == 0 else ""
|
231
|
+
|
232
|
+
row = [
|
233
|
+
Text(f"v{app_stats.version}", style=style),
|
234
|
+
Text(timestamp_to_local(app_stats.deployed_at, json), style=style),
|
235
|
+
Text(app_stats.client_version, style=style),
|
236
|
+
Text(app_stats.deployed_by, style=style),
|
237
|
+
]
|
238
|
+
|
239
|
+
if app_stats.tag:
|
240
|
+
deployments_with_tags = True
|
241
|
+
row.append(Text(app_stats.tag, style=style))
|
242
|
+
|
243
|
+
rows.append(row)
|
244
|
+
|
245
|
+
if deployments_with_tags:
|
246
|
+
columns.append("Tag")
|
247
|
+
|
248
|
+
rows = sorted(rows, key=lambda x: int(str(x[0])[1:]), reverse=True)
|
249
|
+
display_table(columns, rows, json)
|
modal/cli/config.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
import pprint
|
3
|
-
|
4
2
|
import typer
|
3
|
+
from rich.console import Console
|
5
4
|
|
6
5
|
from modal.config import _profile, _store_user_config, config
|
6
|
+
from modal.environments import Environment
|
7
7
|
|
8
8
|
config_cli = typer.Typer(
|
9
9
|
name="config",
|
@@ -17,10 +17,15 @@ config_cli = typer.Typer(
|
|
17
17
|
)
|
18
18
|
|
19
19
|
|
20
|
-
@config_cli.command(help="Show configuration values
|
21
|
-
def show():
|
20
|
+
@config_cli.command(help="Show current configuration values (debugging command).")
|
21
|
+
def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value.")):
|
22
22
|
# This is just a test command
|
23
|
-
|
23
|
+
config_dict = config.to_dict()
|
24
|
+
if redact and config_dict.get("token_secret"):
|
25
|
+
config_dict["token_secret"] = "***"
|
26
|
+
|
27
|
+
console = Console()
|
28
|
+
console.print(config_dict)
|
24
29
|
|
25
30
|
|
26
31
|
SET_DEFAULT_ENV_HELP = """Set the default Modal environment for the active profile
|
@@ -34,6 +39,8 @@ when running a command that requires an environment.
|
|
34
39
|
|
35
40
|
@config_cli.command(help=SET_DEFAULT_ENV_HELP)
|
36
41
|
def set_environment(environment_name: str):
|
42
|
+
# Confirm that the environment exists by looking it up
|
43
|
+
Environment.lookup(environment_name)
|
37
44
|
_store_user_config({"environment": environment_name})
|
38
45
|
typer.echo(f"New default environment for profile {_profile}: {environment_name}")
|
39
46
|
|