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/volume.py
CHANGED
@@ -2,26 +2,23 @@
|
|
2
2
|
import os
|
3
3
|
import sys
|
4
4
|
from pathlib import Path
|
5
|
-
from typing import
|
5
|
+
from typing import Optional
|
6
6
|
|
7
7
|
import typer
|
8
8
|
from click import UsageError
|
9
9
|
from grpclib import GRPCError, Status
|
10
10
|
from rich.console import Console
|
11
|
-
from rich.live import Live
|
12
11
|
from rich.syntax import Syntax
|
13
|
-
from rich.table import Table
|
14
12
|
from typer import Argument, Option, Typer
|
15
13
|
|
16
14
|
import modal
|
17
|
-
from modal._output import
|
15
|
+
from modal._output import OutputManager, ProgressHandler
|
18
16
|
from modal._utils.async_utils import synchronizer
|
19
17
|
from modal._utils.grpc_utils import retry_transient_errors
|
20
18
|
from modal.cli._download import _volume_download
|
21
|
-
from modal.cli.utils import ENV_OPTION, display_table, timestamp_to_local
|
19
|
+
from modal.cli.utils import ENV_OPTION, YES_OPTION, display_table, timestamp_to_local
|
22
20
|
from modal.client import _Client
|
23
21
|
from modal.environments import ensure_env
|
24
|
-
from modal.exception import deprecation_warning
|
25
22
|
from modal.volume import _Volume, _VolumeUploadContextManager
|
26
23
|
from modal_proto import api_pb2
|
27
24
|
|
@@ -52,13 +49,14 @@ def humanize_filesize(value: int) -> str:
|
|
52
49
|
return format % (base * bytes_ / unit) + s
|
53
50
|
|
54
51
|
|
55
|
-
@volume_cli.command(name="create", help="Create a named, persistent modal.Volume.")
|
52
|
+
@volume_cli.command(name="create", help="Create a named, persistent modal.Volume.", rich_help_panel="Management")
|
56
53
|
def create(
|
57
54
|
name: str,
|
58
55
|
env: Optional[str] = ENV_OPTION,
|
56
|
+
version: Optional[int] = Option(default=None, help="VolumeFS version. (Experimental)"),
|
59
57
|
):
|
60
58
|
env_name = ensure_env(env)
|
61
|
-
modal.Volume.create_deployed(name, environment_name=env)
|
59
|
+
modal.Volume.create_deployed(name, environment_name=env, version=version)
|
62
60
|
usage_code = f"""
|
63
61
|
@app.function(volumes={{"/my_vol": modal.Volume.from_name("{name}")}})
|
64
62
|
def some_func():
|
@@ -66,12 +64,12 @@ def some_func():
|
|
66
64
|
"""
|
67
65
|
|
68
66
|
console = Console()
|
69
|
-
console.print(f"Created
|
67
|
+
console.print(f"Created Volume '{name}' in environment '{env_name}'. \n\nCode example:\n")
|
70
68
|
usage = Syntax(usage_code, "python")
|
71
69
|
console.print(usage)
|
72
70
|
|
73
71
|
|
74
|
-
@volume_cli.command(name="get")
|
72
|
+
@volume_cli.command(name="get", rich_help_panel="File operations")
|
75
73
|
@synchronizer.create_blocking
|
76
74
|
async def get(
|
77
75
|
volume_name: str,
|
@@ -80,14 +78,14 @@ async def get(
|
|
80
78
|
force: bool = False,
|
81
79
|
env: Optional[str] = ENV_OPTION,
|
82
80
|
):
|
83
|
-
"""Download files from a Volume object.
|
81
|
+
"""Download files from a modal.Volume object.
|
84
82
|
|
85
83
|
If a folder is passed for REMOTE_PATH, the contents of the folder will be downloaded
|
86
84
|
recursively, including all subdirectories.
|
87
85
|
|
88
86
|
**Example**
|
89
87
|
|
90
|
-
```
|
88
|
+
```
|
91
89
|
modal volume get <volume_name> logs/april-12-1.txt
|
92
90
|
modal volume get <volume_name> / volume_data_dump
|
93
91
|
```
|
@@ -97,12 +95,20 @@ async def get(
|
|
97
95
|
ensure_env(env)
|
98
96
|
destination = Path(local_destination)
|
99
97
|
volume = await _Volume.lookup(volume_name, environment_name=env)
|
100
|
-
|
98
|
+
console = Console()
|
99
|
+
progress_handler = ProgressHandler(type="download", console=console)
|
100
|
+
with progress_handler.live:
|
101
|
+
await _volume_download(volume, remote_path, destination, force, progress_cb=progress_handler.progress)
|
102
|
+
console.print(OutputManager.step_completed("Finished downloading files to local!"))
|
101
103
|
|
102
104
|
|
103
|
-
@volume_cli.command(
|
105
|
+
@volume_cli.command(
|
106
|
+
name="list",
|
107
|
+
help="List the details of all modal.Volume volumes in an Environment.",
|
108
|
+
rich_help_panel="Management",
|
109
|
+
)
|
104
110
|
@synchronizer.create_blocking
|
105
|
-
async def
|
111
|
+
async def list_(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
106
112
|
env = ensure_env(env)
|
107
113
|
client = await _Client.from_env()
|
108
114
|
response = await retry_transient_errors(client.stub.VolumeList, api_pb2.VolumeListRequest(environment_name=env))
|
@@ -114,7 +120,11 @@ async def list(env: Optional[str] = ENV_OPTION, json: Optional[bool] = False):
|
|
114
120
|
display_table(column_names, rows, json, title=f"Volumes{env_part}")
|
115
121
|
|
116
122
|
|
117
|
-
@volume_cli.command(
|
123
|
+
@volume_cli.command(
|
124
|
+
name="ls",
|
125
|
+
help="List files and directories in a modal.Volume volume.",
|
126
|
+
rich_help_panel="File operations",
|
127
|
+
)
|
118
128
|
@synchronizer.create_blocking
|
119
129
|
async def ls(
|
120
130
|
volume_name: str,
|
@@ -134,13 +144,12 @@ async def ls(
|
|
134
144
|
raise UsageError(exc.message)
|
135
145
|
raise
|
136
146
|
|
137
|
-
if sys.stdout.isatty():
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
147
|
+
if not json and not sys.stdout.isatty():
|
148
|
+
# Legacy behavior -- I am not sure why exactly we did this originally but I don't want to break it
|
149
|
+
for entry in entries:
|
150
|
+
print(entry.path)
|
151
|
+
else:
|
152
|
+
rows = []
|
144
153
|
for entry in entries:
|
145
154
|
if entry.type == api_pb2.FileEntry.FileType.DIRECTORY:
|
146
155
|
filetype = "dir"
|
@@ -148,26 +157,29 @@ async def ls(
|
|
148
157
|
filetype = "link"
|
149
158
|
else:
|
150
159
|
filetype = "file"
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
160
|
+
rows.append(
|
161
|
+
(
|
162
|
+
entry.path.encode("unicode_escape").decode("utf-8"),
|
163
|
+
filetype,
|
164
|
+
timestamp_to_local(entry.mtime, False),
|
165
|
+
humanize_filesize(entry.size),
|
166
|
+
)
|
156
167
|
)
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
print(entry.path)
|
168
|
+
columns = ["Filename", "Type", "Created/Modified", "Size"]
|
169
|
+
title = f"Directory listing of '{path}' in '{volume_name}'"
|
170
|
+
display_table(columns, rows, json, title)
|
161
171
|
|
162
172
|
|
163
173
|
@volume_cli.command(
|
164
174
|
name="put",
|
165
|
-
help="""Upload a file or directory to a
|
175
|
+
help="""Upload a file or directory to a modal.Volume.
|
166
176
|
|
167
177
|
Remote parent directories will be created as needed.
|
168
178
|
|
169
|
-
Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory
|
179
|
+
Ending the REMOTE_PATH with a forward slash (/), it's assumed to be a directory
|
180
|
+
and the file will be uploaded with its current name under that directory.
|
170
181
|
""",
|
182
|
+
rich_help_panel="File operations",
|
171
183
|
)
|
172
184
|
@synchronizer.create_blocking
|
173
185
|
async def put(
|
@@ -185,30 +197,36 @@ async def put(
|
|
185
197
|
if remote_path.endswith("/"):
|
186
198
|
remote_path = remote_path + os.path.basename(local_path)
|
187
199
|
console = Console()
|
200
|
+
progress_handler = ProgressHandler(type="upload", console=console)
|
188
201
|
|
189
202
|
if Path(local_path).is_dir():
|
190
|
-
|
191
|
-
with Live(spinner, console=console):
|
203
|
+
with progress_handler.live:
|
192
204
|
try:
|
193
|
-
async with _VolumeUploadContextManager(
|
205
|
+
async with _VolumeUploadContextManager(
|
206
|
+
vol.object_id, vol._client, progress_cb=progress_handler.progress, force=force
|
207
|
+
) as batch:
|
194
208
|
batch.put_directory(local_path, remote_path)
|
195
209
|
except FileExistsError as exc:
|
196
210
|
raise UsageError(str(exc))
|
197
|
-
console.print(step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
|
211
|
+
console.print(OutputManager.step_completed(f"Uploaded directory '{local_path}' to '{remote_path}'"))
|
198
212
|
elif "*" in local_path:
|
199
213
|
raise UsageError("Glob uploads are currently not supported")
|
200
214
|
else:
|
201
|
-
|
202
|
-
with Live(spinner, console=console):
|
215
|
+
with progress_handler.live:
|
203
216
|
try:
|
204
|
-
async with _VolumeUploadContextManager(
|
217
|
+
async with _VolumeUploadContextManager(
|
218
|
+
vol.object_id, vol._client, progress_cb=progress_handler.progress, force=force
|
219
|
+
) as batch:
|
205
220
|
batch.put_file(local_path, remote_path)
|
221
|
+
|
206
222
|
except FileExistsError as exc:
|
207
223
|
raise UsageError(str(exc))
|
208
|
-
console.print(step_completed(f"Uploaded file '{local_path}' to '{remote_path}'"))
|
224
|
+
console.print(OutputManager.step_completed(f"Uploaded file '{local_path}' to '{remote_path}'"))
|
209
225
|
|
210
226
|
|
211
|
-
@volume_cli.command(
|
227
|
+
@volume_cli.command(
|
228
|
+
name="rm", help="Delete a file or directory from a modal.Volume.", rich_help_panel="File operations"
|
229
|
+
)
|
212
230
|
@synchronizer.create_blocking
|
213
231
|
async def rm(
|
214
232
|
volume_name: str,
|
@@ -229,12 +247,17 @@ async def rm(
|
|
229
247
|
|
230
248
|
|
231
249
|
@volume_cli.command(
|
232
|
-
name="cp",
|
250
|
+
name="cp",
|
251
|
+
help=(
|
252
|
+
"Copy within a modal.Volume. "
|
253
|
+
"Copy source file to destination file or multiple source files to destination directory."
|
254
|
+
),
|
255
|
+
rich_help_panel="File operations",
|
233
256
|
)
|
234
257
|
@synchronizer.create_blocking
|
235
258
|
async def cp(
|
236
259
|
volume_name: str,
|
237
|
-
paths:
|
260
|
+
paths: list[str], # accepts multiple paths, last path is treated as destination path
|
238
261
|
env: Optional[str] = ENV_OPTION,
|
239
262
|
):
|
240
263
|
ensure_env(env)
|
@@ -245,27 +268,45 @@ async def cp(
|
|
245
268
|
await volume.copy_files(src_paths, dst_path)
|
246
269
|
|
247
270
|
|
248
|
-
@volume_cli.command(
|
271
|
+
@volume_cli.command(
|
272
|
+
name="delete",
|
273
|
+
help="Delete a named, persistent modal.Volume.",
|
274
|
+
rich_help_panel="Management",
|
275
|
+
)
|
249
276
|
@synchronizer.create_blocking
|
250
277
|
async def delete(
|
251
278
|
volume_name: str = Argument(help="Name of the modal.Volume to be deleted. Case sensitive"),
|
252
|
-
yes: bool =
|
253
|
-
confirm: bool = Option(default=False, help="DEPRECATED: See `--yes` option"),
|
279
|
+
yes: bool = YES_OPTION,
|
254
280
|
env: Optional[str] = ENV_OPTION,
|
255
281
|
):
|
256
|
-
if
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
282
|
+
if not yes:
|
283
|
+
typer.confirm(
|
284
|
+
f"Are you sure you want to irrevocably delete the modal.Volume '{volume_name}'?",
|
285
|
+
default=False,
|
286
|
+
abort=True,
|
261
287
|
)
|
262
|
-
yes = True
|
263
288
|
|
289
|
+
await _Volume.delete(volume_name, environment_name=env)
|
290
|
+
|
291
|
+
|
292
|
+
@volume_cli.command(
|
293
|
+
name="rename",
|
294
|
+
help="Rename a modal.Volume.",
|
295
|
+
rich_help_panel="Management",
|
296
|
+
)
|
297
|
+
@synchronizer.create_blocking
|
298
|
+
async def rename(
|
299
|
+
old_name: str,
|
300
|
+
new_name: str,
|
301
|
+
yes: bool = YES_OPTION,
|
302
|
+
env: Optional[str] = ENV_OPTION,
|
303
|
+
):
|
264
304
|
if not yes:
|
265
305
|
typer.confirm(
|
266
|
-
f"Are you sure you want
|
306
|
+
f"Are you sure you want rename the modal.Volume '{old_name}'?"
|
307
|
+
" This may break any Apps currently using it.",
|
267
308
|
default=False,
|
268
309
|
abort=True,
|
269
310
|
)
|
270
311
|
|
271
|
-
await _Volume.
|
312
|
+
await _Volume.rename(old_name, new_name, environment_name=env)
|