modal 1.1.0__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__main__.py +2 -2
- modal/_clustered_functions.py +3 -0
- modal/_clustered_functions.pyi +3 -2
- modal/_functions.py +78 -26
- modal/_object.py +9 -1
- modal/_output.py +14 -25
- modal/_runtime/gpu_memory_snapshot.py +158 -54
- modal/_utils/async_utils.py +6 -4
- modal/_utils/auth_token_manager.py +1 -1
- modal/_utils/blob_utils.py +16 -21
- modal/_utils/function_utils.py +16 -4
- modal/_utils/time_utils.py +8 -4
- modal/app.py +0 -4
- modal/app.pyi +0 -4
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +4 -4
- modal/cli/config.py +2 -2
- modal/cli/container.py +2 -2
- modal/cli/dict.py +4 -4
- modal/cli/entry_point.py +2 -2
- modal/cli/import_refs.py +3 -3
- modal/cli/network_file_system.py +8 -9
- modal/cli/profile.py +2 -2
- modal/cli/queues.py +5 -5
- modal/cli/secret.py +5 -5
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +8 -9
- modal/client.py +8 -1
- modal/client.pyi +9 -2
- modal/container_process.py +2 -2
- modal/dict.py +47 -3
- modal/dict.pyi +55 -0
- modal/exception.py +4 -0
- modal/experimental/__init__.py +1 -1
- modal/experimental/flash.py +18 -2
- modal/experimental/flash.pyi +19 -0
- modal/functions.pyi +0 -1
- modal/image.py +26 -10
- modal/image.pyi +12 -4
- modal/mount.py +1 -1
- modal/object.pyi +4 -0
- modal/parallel_map.py +432 -4
- modal/parallel_map.pyi +28 -0
- modal/queue.py +46 -3
- modal/queue.pyi +53 -0
- modal/sandbox.py +105 -25
- modal/sandbox.pyi +108 -18
- modal/secret.py +48 -5
- modal/secret.pyi +55 -0
- modal/token_flow.py +3 -3
- modal/volume.py +49 -18
- modal/volume.pyi +50 -8
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/METADATA +2 -2
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/RECORD +75 -75
- modal_proto/api.proto +140 -14
- modal_proto/api_grpc.py +80 -0
- modal_proto/api_pb2.py +927 -756
- modal_proto/api_pb2.pyi +488 -34
- modal_proto/api_pb2_grpc.py +166 -0
- modal_proto/api_pb2_grpc.pyi +52 -0
- modal_proto/modal_api_grpc.py +5 -0
- modal_version/__init__.py +1 -1
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/2025.06.txt +0 -0
- /modal/{requirements → builder}/PREVIEW.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/WHEEL +0 -0
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/entry_points.txt +0 -0
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.0.dist-info → modal-1.1.1.dist-info}/top_level.txt +0 -0
modal/cli/cluster.py
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
from typing import Optional, Union
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
5
|
from rich.text import Text
|
|
7
6
|
|
|
8
7
|
from modal._object import _get_environment_name
|
|
8
|
+
from modal._output import make_console
|
|
9
9
|
from modal._pty import get_pty_info
|
|
10
10
|
from modal._utils.async_utils import synchronizer
|
|
11
|
-
from modal._utils.time_utils import
|
|
11
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
12
|
from modal.cli.utils import ENV_OPTION, display_table, is_tty
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.config import config
|
|
@@ -42,7 +42,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
42
42
|
[
|
|
43
43
|
c.cluster_id,
|
|
44
44
|
c.app_id,
|
|
45
|
-
|
|
45
|
+
timestamp_to_localized_str(c.started_at, json) if c.started_at else "Pending",
|
|
46
46
|
str(len(c.task_ids)),
|
|
47
47
|
]
|
|
48
48
|
)
|
|
@@ -62,7 +62,7 @@ async def shell(
|
|
|
62
62
|
if len(res.cluster.task_ids) <= rank:
|
|
63
63
|
raise typer.Abort(f"No node with rank {rank} in cluster {cluster_id}")
|
|
64
64
|
task_id = res.cluster.task_ids[rank]
|
|
65
|
-
console =
|
|
65
|
+
console = make_console()
|
|
66
66
|
is_main = "(main)" if rank == 0 else ""
|
|
67
67
|
console.print(
|
|
68
68
|
f"Opening shell to node {rank} {is_main} of cluster {cluster_id} (container {task_id})", style="green"
|
modal/cli/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import typer
|
|
3
|
-
from rich.console import Console
|
|
4
3
|
|
|
4
|
+
from modal._output import make_console
|
|
5
5
|
from modal.config import _profile, _store_user_config, config
|
|
6
6
|
from modal.environments import Environment
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ def show(redact: bool = typer.Option(True, help="Redact the `token_secret` value
|
|
|
24
24
|
if redact and config_dict.get("token_secret"):
|
|
25
25
|
config_dict["token_secret"] = "***"
|
|
26
26
|
|
|
27
|
-
console =
|
|
27
|
+
console = make_console()
|
|
28
28
|
console.print(config_dict)
|
|
29
29
|
|
|
30
30
|
|
modal/cli/container.py
CHANGED
|
@@ -8,7 +8,7 @@ from modal._object import _get_environment_name
|
|
|
8
8
|
from modal._pty import get_pty_info
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
11
|
-
from modal._utils.time_utils import
|
|
11
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
12
|
from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.config import config
|
|
@@ -40,7 +40,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
40
40
|
task_stats.task_id,
|
|
41
41
|
task_stats.app_id,
|
|
42
42
|
task_stats.app_description,
|
|
43
|
-
|
|
43
|
+
timestamp_to_localized_str(task_stats.started_at, json) if task_stats.started_at else "Pending",
|
|
44
44
|
]
|
|
45
45
|
)
|
|
46
46
|
|
modal/cli/dict.py
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
5
|
from typer import Argument, Option, Typer
|
|
7
6
|
|
|
7
|
+
from modal._output import make_console
|
|
8
8
|
from modal._resolver import Resolver
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
11
|
-
from modal._utils.time_utils import
|
|
11
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
12
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.dict import _Dict
|
|
@@ -44,7 +44,7 @@ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
|
|
44
44
|
request = api_pb2.DictListRequest(environment_name=env)
|
|
45
45
|
response = await retry_transient_errors(client.stub.DictList, request)
|
|
46
46
|
|
|
47
|
-
rows = [(d.name,
|
|
47
|
+
rows = [(d.name, timestamp_to_localized_str(d.created_at, json)) for d in response.dicts]
|
|
48
48
|
display_table(["Name", "Created at"], rows, json)
|
|
49
49
|
|
|
50
50
|
|
|
@@ -85,7 +85,7 @@ async def get(name: str, key: str, *, env: Optional[str] = ENV_OPTION):
|
|
|
85
85
|
Note: When using the CLI, keys are always interpreted as having a string type.
|
|
86
86
|
"""
|
|
87
87
|
d = _Dict.from_name(name, environment_name=env)
|
|
88
|
-
console =
|
|
88
|
+
console = make_console()
|
|
89
89
|
val = await d.get(key)
|
|
90
90
|
console.print(val)
|
|
91
91
|
|
modal/cli/entry_point.py
CHANGED
|
@@ -3,9 +3,9 @@ import subprocess
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
6
|
from rich.rule import Rule
|
|
8
7
|
|
|
8
|
+
from modal._output import make_console
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
|
|
11
11
|
from . import run
|
|
@@ -71,7 +71,7 @@ def check_path():
|
|
|
71
71
|
"You may need to give it permissions or use `[white]python -m modal[/white]` as a workaround.[/red]\n"
|
|
72
72
|
)
|
|
73
73
|
text += f"See more information here:\n\n[link={url}]{url}[/link]\n"
|
|
74
|
-
console =
|
|
74
|
+
console = make_console()
|
|
75
75
|
console.print(text)
|
|
76
76
|
console.print(Rule(style="white"))
|
|
77
77
|
|
modal/cli/import_refs.py
CHANGED
|
@@ -19,9 +19,9 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Optional, Union, cast
|
|
20
20
|
|
|
21
21
|
import click
|
|
22
|
-
from rich.console import Console
|
|
23
22
|
from rich.markdown import Markdown
|
|
24
23
|
|
|
24
|
+
from modal._output import make_console
|
|
25
25
|
from modal._utils.deprecation import deprecation_warning
|
|
26
26
|
from modal.app import App, LocalEntrypoint
|
|
27
27
|
from modal.cls import Cls
|
|
@@ -258,7 +258,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
|
|
|
258
258
|
app = getattr(module, object_path)
|
|
259
259
|
|
|
260
260
|
if app is None:
|
|
261
|
-
error_console =
|
|
261
|
+
error_console = make_console(stderr=True)
|
|
262
262
|
error_console.print(f"[bold red]Could not find Modal app '{object_path}' in {import_path}.[/bold red]")
|
|
263
263
|
|
|
264
264
|
if not object_path:
|
|
@@ -282,7 +282,7 @@ def import_app_from_ref(import_ref: ImportRef, base_cmd: str = "") -> App:
|
|
|
282
282
|
def _show_function_ref_help(app_ref: ImportRef, base_cmd: str) -> None:
|
|
283
283
|
object_path = app_ref.object_path
|
|
284
284
|
import_path = app_ref.file_or_module
|
|
285
|
-
error_console =
|
|
285
|
+
error_console = make_console(stderr=True)
|
|
286
286
|
if object_path:
|
|
287
287
|
error_console.print(
|
|
288
288
|
f"[bold red]Could not find Modal function or local entrypoint"
|
modal/cli/network_file_system.py
CHANGED
|
@@ -7,17 +7,16 @@ from typing import Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from click import UsageError
|
|
9
9
|
from grpclib import GRPCError, Status
|
|
10
|
-
from rich.console import Console
|
|
11
10
|
from rich.syntax import Syntax
|
|
12
11
|
from rich.table import Table
|
|
13
12
|
from typer import Argument, Typer
|
|
14
13
|
|
|
15
14
|
import modal
|
|
16
15
|
from modal._location import display_location
|
|
17
|
-
from modal._output import OutputManager, ProgressHandler
|
|
16
|
+
from modal._output import OutputManager, ProgressHandler, make_console
|
|
18
17
|
from modal._utils.async_utils import synchronizer
|
|
19
18
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
20
|
-
from modal._utils.time_utils import
|
|
19
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
21
20
|
from modal.cli._download import _volume_download
|
|
22
21
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
23
22
|
from modal.client import _Client
|
|
@@ -45,7 +44,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
|
45
44
|
[
|
|
46
45
|
item.label,
|
|
47
46
|
display_location(item.cloud_provider),
|
|
48
|
-
|
|
47
|
+
timestamp_to_localized_str(item.created_at, json),
|
|
49
48
|
]
|
|
50
49
|
)
|
|
51
50
|
display_table(column_names, rows, json, title=f"Shared Volumes{env_part}")
|
|
@@ -66,7 +65,7 @@ def create(
|
|
|
66
65
|
):
|
|
67
66
|
ensure_env(env)
|
|
68
67
|
modal.NetworkFileSystem.create_deployed(name, environment_name=env)
|
|
69
|
-
console =
|
|
68
|
+
console = make_console()
|
|
70
69
|
console.print(f"Created volume '{name}'. \n\nCode example:\n")
|
|
71
70
|
usage = Syntax(gen_usage_code(name), "python")
|
|
72
71
|
console.print(usage)
|
|
@@ -93,7 +92,7 @@ async def ls(
|
|
|
93
92
|
raise
|
|
94
93
|
|
|
95
94
|
if sys.stdout.isatty():
|
|
96
|
-
console =
|
|
95
|
+
console = make_console()
|
|
97
96
|
console.print(f"Directory listing of '{path}' in '{volume_name}'")
|
|
98
97
|
table = Table()
|
|
99
98
|
|
|
@@ -131,7 +130,7 @@ async def put(
|
|
|
131
130
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
132
131
|
if remote_path.endswith("/"):
|
|
133
132
|
remote_path = remote_path + os.path.basename(local_path)
|
|
134
|
-
console =
|
|
133
|
+
console = make_console()
|
|
135
134
|
|
|
136
135
|
if Path(local_path).is_dir():
|
|
137
136
|
progress_handler = ProgressHandler(type="upload", console=console)
|
|
@@ -184,7 +183,7 @@ async def get(
|
|
|
184
183
|
ensure_env(env)
|
|
185
184
|
destination = Path(local_destination)
|
|
186
185
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
187
|
-
console =
|
|
186
|
+
console = make_console()
|
|
188
187
|
progress_handler = ProgressHandler(type="download", console=console)
|
|
189
188
|
with progress_handler.live:
|
|
190
189
|
await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
|
|
@@ -203,7 +202,7 @@ async def rm(
|
|
|
203
202
|
):
|
|
204
203
|
ensure_env(env)
|
|
205
204
|
volume = _NetworkFileSystem.from_name(volume_name)
|
|
206
|
-
console =
|
|
205
|
+
console = make_console()
|
|
207
206
|
try:
|
|
208
207
|
await volume.remove_file(remote_path, recursive=recursive)
|
|
209
208
|
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
modal/cli/profile.py
CHANGED
|
@@ -5,10 +5,10 @@ import os
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
-
from rich.console import Console
|
|
9
8
|
from rich.json import JSON
|
|
10
9
|
from rich.table import Table
|
|
11
10
|
|
|
11
|
+
from modal._output import make_console
|
|
12
12
|
from modal._utils.async_utils import synchronizer
|
|
13
13
|
from modal.config import Config, _lookup_workspace, _profile, config_profiles, config_set_active_profile
|
|
14
14
|
from modal.exception import AuthError
|
|
@@ -69,7 +69,7 @@ async def list_(json: Optional[bool] = False):
|
|
|
69
69
|
except AuthError:
|
|
70
70
|
env_based_workspace = "Unknown (authentication failure)"
|
|
71
71
|
|
|
72
|
-
console =
|
|
72
|
+
console = make_console()
|
|
73
73
|
highlight = "bold green" if env_based_workspace is None else "yellow"
|
|
74
74
|
if json:
|
|
75
75
|
json_data = []
|
modal/cli/queues.py
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from rich.console import Console
|
|
6
5
|
from typer import Argument, Option, Typer
|
|
7
6
|
|
|
7
|
+
from modal._output import make_console
|
|
8
8
|
from modal._resolver import Resolver
|
|
9
9
|
from modal._utils.async_utils import synchronizer
|
|
10
10
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
11
|
-
from modal._utils.time_utils import
|
|
11
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
12
12
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
13
13
|
from modal.client import _Client
|
|
14
14
|
from modal.environments import ensure_env
|
|
@@ -71,7 +71,7 @@ async def list_(*, json: bool = False, env: Optional[str] = ENV_OPTION):
|
|
|
71
71
|
rows = [
|
|
72
72
|
(
|
|
73
73
|
q.name,
|
|
74
|
-
|
|
74
|
+
timestamp_to_localized_str(q.created_at, json),
|
|
75
75
|
str(q.num_partitions),
|
|
76
76
|
str(q.total_size) if q.total_size <= max_total_size else f">{max_total_size}",
|
|
77
77
|
)
|
|
@@ -108,7 +108,7 @@ async def peek(
|
|
|
108
108
|
):
|
|
109
109
|
"""Print the next N items in the queue or queue partition (without removal)."""
|
|
110
110
|
q = _Queue.from_name(name, environment_name=env)
|
|
111
|
-
console =
|
|
111
|
+
console = make_console()
|
|
112
112
|
i = 0
|
|
113
113
|
async for item in q.iterate(partition=partition):
|
|
114
114
|
console.print(item)
|
|
@@ -128,5 +128,5 @@ async def len(
|
|
|
128
128
|
):
|
|
129
129
|
"""Print the length of a queue partition or the total length of all partitions."""
|
|
130
130
|
q = _Queue.from_name(name, environment_name=env)
|
|
131
|
-
console =
|
|
131
|
+
console = make_console()
|
|
132
132
|
console.print(await q.len(partition=partition, total=total))
|
modal/cli/secret.py
CHANGED
|
@@ -9,13 +9,13 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
import typer
|
|
12
|
-
from rich.console import Console
|
|
13
12
|
from rich.syntax import Syntax
|
|
14
13
|
from typer import Argument
|
|
15
14
|
|
|
15
|
+
from modal._output import make_console
|
|
16
16
|
from modal._utils.async_utils import synchronizer
|
|
17
17
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
18
|
-
from modal._utils.time_utils import
|
|
18
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
19
19
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
20
20
|
from modal.client import _Client
|
|
21
21
|
from modal.environments import ensure_env
|
|
@@ -38,8 +38,8 @@ async def list_(env: Optional[str] = ENV_OPTION, json: bool = False):
|
|
|
38
38
|
rows.append(
|
|
39
39
|
[
|
|
40
40
|
item.label,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
timestamp_to_localized_str(item.created_at, json),
|
|
42
|
+
timestamp_to_localized_str(item.last_used_at, json) if item.last_used_at else "-",
|
|
43
43
|
]
|
|
44
44
|
)
|
|
45
45
|
|
|
@@ -117,7 +117,7 @@ modal secret create my-credentials username=john password="$PASSWORD"
|
|
|
117
117
|
await _Secret.create_deployed(secret_name, env_dict, overwrite=force)
|
|
118
118
|
|
|
119
119
|
# Print code sample
|
|
120
|
-
console =
|
|
120
|
+
console = make_console()
|
|
121
121
|
env_var_code = "\n ".join(f'os.getenv("{name}")' for name in env_dict.keys()) if env_dict else "..."
|
|
122
122
|
example_code = f"""
|
|
123
123
|
@app.function(secrets=[modal.Secret.from_name("{secret_name}")])
|
modal/cli/utils.py
CHANGED
|
@@ -7,13 +7,12 @@ from typing import Optional, Union
|
|
|
7
7
|
import typer
|
|
8
8
|
from click import UsageError
|
|
9
9
|
from grpclib import GRPCError, Status
|
|
10
|
-
from rich.console import Console
|
|
11
10
|
from rich.table import Column, Table
|
|
12
11
|
from rich.text import Text
|
|
13
12
|
|
|
14
13
|
from modal_proto import api_pb2
|
|
15
14
|
|
|
16
|
-
from .._output import OutputManager, get_app_logs_loop
|
|
15
|
+
from .._output import OutputManager, get_app_logs_loop, make_console
|
|
17
16
|
from .._utils.async_utils import synchronizer
|
|
18
17
|
from ..client import _Client
|
|
19
18
|
from ..environments import ensure_env
|
|
@@ -66,7 +65,7 @@ def _plain(text: Union[Text, str]) -> str:
|
|
|
66
65
|
|
|
67
66
|
|
|
68
67
|
def is_tty() -> bool:
|
|
69
|
-
return
|
|
68
|
+
return make_console().is_terminal
|
|
70
69
|
|
|
71
70
|
|
|
72
71
|
def display_table(
|
|
@@ -78,7 +77,7 @@ def display_table(
|
|
|
78
77
|
def col_to_str(col: Union[Column, str]) -> str:
|
|
79
78
|
return str(col.header) if isinstance(col, Column) else col
|
|
80
79
|
|
|
81
|
-
console =
|
|
80
|
+
console = make_console()
|
|
82
81
|
if json:
|
|
83
82
|
json_data = [{col_to_str(col): _plain(row[i]) for i, col in enumerate(columns)} for row in rows]
|
|
84
83
|
console.print_json(dumps(json_data))
|
modal/cli/volume.py
CHANGED
|
@@ -7,15 +7,14 @@ from typing import Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from click import UsageError
|
|
9
9
|
from grpclib import GRPCError, Status
|
|
10
|
-
from rich.console import Console
|
|
11
10
|
from rich.syntax import Syntax
|
|
12
11
|
from typer import Argument, Option, Typer
|
|
13
12
|
|
|
14
13
|
import modal
|
|
15
|
-
from modal._output import OutputManager, ProgressHandler
|
|
14
|
+
from modal._output import OutputManager, ProgressHandler, make_console
|
|
16
15
|
from modal._utils.async_utils import synchronizer
|
|
17
16
|
from modal._utils.grpc_utils import retry_transient_errors
|
|
18
|
-
from modal._utils.time_utils import
|
|
17
|
+
from modal._utils.time_utils import timestamp_to_localized_str
|
|
19
18
|
from modal.cli._download import _volume_download
|
|
20
19
|
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table
|
|
21
20
|
from modal.client import _Client
|
|
@@ -64,7 +63,7 @@ def some_func():
|
|
|
64
63
|
os.listdir("/my_vol")
|
|
65
64
|
"""
|
|
66
65
|
|
|
67
|
-
console =
|
|
66
|
+
console = make_console()
|
|
68
67
|
console.print(f"Created Volume '{name}' in environment '{env_name}'. \n\nCode example:\n")
|
|
69
68
|
usage = Syntax(usage_code, "python")
|
|
70
69
|
console.print(usage)
|
|
@@ -96,7 +95,7 @@ async def get(
|
|
|
96
95
|
ensure_env(env)
|
|
97
96
|
destination = Path(local_destination)
|
|
98
97
|
volume = _Volume.from_name(volume_name, environment_name=env)
|
|
99
|
-
console =
|
|
98
|
+
console = make_console()
|
|
100
99
|
progress_handler = ProgressHandler(type="download", console=console)
|
|
101
100
|
with progress_handler.live:
|
|
102
101
|
await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
|
|
@@ -117,7 +116,7 @@ async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
|
117
116
|
column_names = ["Name", "Created at"]
|
|
118
117
|
rows = []
|
|
119
118
|
for item in response.items:
|
|
120
|
-
rows.append([item.label,
|
|
119
|
+
rows.append([item.label, timestamp_to_localized_str(item.created_at, json)])
|
|
121
120
|
display_table(column_names, rows, json, title=f"Volumes{env_part}")
|
|
122
121
|
|
|
123
122
|
|
|
@@ -164,7 +163,7 @@ async def ls(
|
|
|
164
163
|
(
|
|
165
164
|
entry.path.encode("unicode_escape").decode("utf-8"),
|
|
166
165
|
filetype,
|
|
167
|
-
|
|
166
|
+
timestamp_to_localized_str(entry.mtime, False),
|
|
168
167
|
humanize_filesize(entry.size),
|
|
169
168
|
)
|
|
170
169
|
)
|
|
@@ -197,7 +196,7 @@ async def put(
|
|
|
197
196
|
|
|
198
197
|
if remote_path.endswith("/"):
|
|
199
198
|
remote_path = remote_path + os.path.basename(local_path)
|
|
200
|
-
console =
|
|
199
|
+
console = make_console()
|
|
201
200
|
progress_handler = ProgressHandler(type="upload", console=console)
|
|
202
201
|
|
|
203
202
|
if Path(local_path).is_dir():
|
|
@@ -245,7 +244,7 @@ async def rm(
|
|
|
245
244
|
):
|
|
246
245
|
ensure_env(env)
|
|
247
246
|
volume = _Volume.from_name(volume_name, environment_name=env)
|
|
248
|
-
console =
|
|
247
|
+
console = make_console()
|
|
249
248
|
try:
|
|
250
249
|
await volume.remove_file(remote_path, recursive=recursive)
|
|
251
250
|
console.print(OutputManager.step_completed(f"{remote_path} was deleted successfully!"))
|
modal/client.py
CHANGED
|
@@ -268,6 +268,14 @@ class _Client:
|
|
|
268
268
|
# Just used from tests.
|
|
269
269
|
cls._client_from_env = client
|
|
270
270
|
|
|
271
|
+
async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]:
|
|
272
|
+
assert self._auth_token_manager, "Client must have an instance of auth token manager."
|
|
273
|
+
token = await self._auth_token_manager.get_token()
|
|
274
|
+
return [
|
|
275
|
+
("x-modal-input-plane-region", input_plane_region),
|
|
276
|
+
("x-modal-auth-token", token),
|
|
277
|
+
]
|
|
278
|
+
|
|
271
279
|
async def _call_safely(self, coro, readable_method: str):
|
|
272
280
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
|
273
281
|
|
|
@@ -456,4 +464,3 @@ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
|
|
|
456
464
|
self.wrapped_method.channel = await self.client._get_channel(self.server_url)
|
|
457
465
|
async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
|
|
458
466
|
yield response
|
|
459
|
-
|
modal/client.pyi
CHANGED
|
@@ -29,7 +29,7 @@ class _Client:
|
|
|
29
29
|
_snapshotted: bool
|
|
30
30
|
|
|
31
31
|
def __init__(
|
|
32
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.
|
|
32
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.1"
|
|
33
33
|
):
|
|
34
34
|
"""mdmd:hidden
|
|
35
35
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -108,6 +108,7 @@ class _Client:
|
|
|
108
108
|
"""mdmd:hidden"""
|
|
109
109
|
...
|
|
110
110
|
|
|
111
|
+
async def get_input_plane_metadata(self, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
111
112
|
async def _call_safely(self, coro, readable_method: str):
|
|
112
113
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
|
113
114
|
|
|
@@ -155,7 +156,7 @@ class Client:
|
|
|
155
156
|
_snapshotted: bool
|
|
156
157
|
|
|
157
158
|
def __init__(
|
|
158
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.
|
|
159
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "1.1.1"
|
|
159
160
|
):
|
|
160
161
|
"""mdmd:hidden
|
|
161
162
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -267,6 +268,12 @@ class Client:
|
|
|
267
268
|
"""mdmd:hidden"""
|
|
268
269
|
...
|
|
269
270
|
|
|
271
|
+
class __get_input_plane_metadata_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
272
|
+
def __call__(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
273
|
+
async def aio(self, /, input_plane_region: str) -> list[tuple[str, str]]: ...
|
|
274
|
+
|
|
275
|
+
get_input_plane_metadata: __get_input_plane_metadata_spec[typing_extensions.Self]
|
|
276
|
+
|
|
270
277
|
class ___call_safely_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
271
278
|
def __call__(self, /, coro, readable_method: str):
|
|
272
279
|
"""Runs coroutine wrapped in a task that's part of the client's task context
|
modal/container_process.py
CHANGED
|
@@ -144,9 +144,9 @@ class _ContainerProcess(Generic[T]):
|
|
|
144
144
|
print("interactive exec is not currently supported on Windows.")
|
|
145
145
|
return
|
|
146
146
|
|
|
147
|
-
from
|
|
147
|
+
from ._output import make_console
|
|
148
148
|
|
|
149
|
-
console =
|
|
149
|
+
console = make_console()
|
|
150
150
|
|
|
151
151
|
connecting_status = console.status("Connecting...")
|
|
152
152
|
connecting_status.start()
|
modal/dict.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
from collections.abc import AsyncIterator, Mapping
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from typing import Any, Optional
|
|
4
6
|
|
|
7
|
+
from google.protobuf.message import Message
|
|
5
8
|
from grpclib import GRPCError
|
|
6
9
|
from synchronicity.async_wrap import asynccontextmanager
|
|
7
10
|
|
|
@@ -14,6 +17,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
|
|
|
14
17
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
15
18
|
from ._utils.grpc_utils import retry_transient_errors
|
|
16
19
|
from ._utils.name_utils import check_object_name
|
|
20
|
+
from ._utils.time_utils import timestamp_to_localized_dt
|
|
17
21
|
from .client import _Client
|
|
18
22
|
from .config import logger
|
|
19
23
|
from .exception import RequestSizeError
|
|
@@ -23,6 +27,18 @@ def _serialize_dict(data):
|
|
|
23
27
|
return [api_pb2.DictEntry(key=serialize(k), value=serialize(v)) for k, v in data.items()]
|
|
24
28
|
|
|
25
29
|
|
|
30
|
+
@dataclass
|
|
31
|
+
class DictInfo:
|
|
32
|
+
"""Information about the Dict object."""
|
|
33
|
+
|
|
34
|
+
# This dataclass should be limited to information that is unchanging over the lifetime of the Dict,
|
|
35
|
+
# since it is transmitted from the server when the object is hydrated and could be stale when accessed.
|
|
36
|
+
|
|
37
|
+
name: Optional[str]
|
|
38
|
+
created_at: datetime
|
|
39
|
+
created_by: Optional[str]
|
|
40
|
+
|
|
41
|
+
|
|
26
42
|
class _Dict(_Object, type_prefix="di"):
|
|
27
43
|
"""Distributed dictionary for storage in Modal apps.
|
|
28
44
|
|
|
@@ -65,12 +81,29 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
65
81
|
For more examples, see the [guide](https://modal.com/docs/guide/dicts-and-queues#modal-dicts).
|
|
66
82
|
"""
|
|
67
83
|
|
|
84
|
+
_name: Optional[str] = None
|
|
85
|
+
_metadata: Optional[api_pb2.DictMetadata] = None
|
|
86
|
+
|
|
68
87
|
def __init__(self, data={}):
|
|
69
88
|
"""mdmd:hidden"""
|
|
70
89
|
raise RuntimeError(
|
|
71
90
|
"`Dict(...)` constructor is not allowed. Please use `Dict.from_name` or `Dict.ephemeral` instead"
|
|
72
91
|
)
|
|
73
92
|
|
|
93
|
+
@property
|
|
94
|
+
def name(self) -> Optional[str]:
|
|
95
|
+
return self._name
|
|
96
|
+
|
|
97
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
98
|
+
if metadata:
|
|
99
|
+
assert isinstance(metadata, api_pb2.DictMetadata)
|
|
100
|
+
self._metadata = metadata
|
|
101
|
+
self._name = metadata.name
|
|
102
|
+
|
|
103
|
+
def _get_metadata(self) -> api_pb2.DictMetadata:
|
|
104
|
+
assert self._metadata
|
|
105
|
+
return self._metadata
|
|
106
|
+
|
|
74
107
|
@classmethod
|
|
75
108
|
@asynccontextmanager
|
|
76
109
|
async def ephemeral(
|
|
@@ -112,7 +145,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
112
145
|
async with TaskContext() as tc:
|
|
113
146
|
request = api_pb2.DictHeartbeatRequest(dict_id=response.dict_id)
|
|
114
147
|
tc.infinite_loop(lambda: client.stub.DictHeartbeat(request), sleep=_heartbeat_sleep)
|
|
115
|
-
yield cls._new_hydrated(response.dict_id, client,
|
|
148
|
+
yield cls._new_hydrated(response.dict_id, client, response.metadata, is_another_app=True)
|
|
116
149
|
|
|
117
150
|
@staticmethod
|
|
118
151
|
def from_name(
|
|
@@ -153,9 +186,9 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
153
186
|
)
|
|
154
187
|
response = await resolver.client.stub.DictGetOrCreate(req)
|
|
155
188
|
logger.debug(f"Created dict with id {response.dict_id}")
|
|
156
|
-
self._hydrate(response.dict_id, resolver.client,
|
|
189
|
+
self._hydrate(response.dict_id, resolver.client, response.metadata)
|
|
157
190
|
|
|
158
|
-
return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
|
|
191
|
+
return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True, name=name)
|
|
159
192
|
|
|
160
193
|
@staticmethod
|
|
161
194
|
async def lookup(
|
|
@@ -209,6 +242,17 @@ class _Dict(_Object, type_prefix="di"):
|
|
|
209
242
|
req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
|
|
210
243
|
await retry_transient_errors(obj._client.stub.DictDelete, req)
|
|
211
244
|
|
|
245
|
+
@live_method
|
|
246
|
+
async def info(self) -> DictInfo:
|
|
247
|
+
"""Return information about the Dict object."""
|
|
248
|
+
metadata = self._get_metadata()
|
|
249
|
+
creation_info = metadata.creation_info
|
|
250
|
+
return DictInfo(
|
|
251
|
+
name=metadata.name or None,
|
|
252
|
+
created_at=timestamp_to_localized_dt(creation_info.created_at),
|
|
253
|
+
created_by=creation_info.created_by or None,
|
|
254
|
+
)
|
|
255
|
+
|
|
212
256
|
@live_method
|
|
213
257
|
async def clear(self) -> None:
|
|
214
258
|
"""Remove all items from the Dict."""
|