modal 0.62.115__py3-none-any.whl → 0.72.11__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 +407 -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 +1036 -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 +197 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +946 -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.11.dist-info}/METADATA +5 -5
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.11.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.11.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/token_flow.pyi
CHANGED
@@ -5,46 +5,51 @@ import typing
|
|
5
5
|
import typing_extensions
|
6
6
|
|
7
7
|
class _TokenFlow:
|
8
|
-
def __init__(self, client: modal.client._Client):
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
...
|
16
|
-
|
8
|
+
def __init__(self, client: modal.client._Client): ...
|
9
|
+
def start(
|
10
|
+
self, utm_source: typing.Optional[str] = None, next_url: typing.Optional[str] = None
|
11
|
+
) -> typing.AsyncContextManager[tuple[str, str, str]]: ...
|
12
|
+
async def finish(
|
13
|
+
self, timeout: float = 40.0, grpc_extra_timeout: float = 5.0
|
14
|
+
) -> typing.Optional[modal_proto.api_pb2.TokenFlowWaitResponse]: ...
|
17
15
|
|
18
16
|
class TokenFlow:
|
19
|
-
def __init__(self, client: modal.client.Client):
|
20
|
-
...
|
17
|
+
def __init__(self, client: modal.client.Client): ...
|
21
18
|
|
22
19
|
class __start_spec(typing_extensions.Protocol):
|
23
|
-
def __call__(
|
24
|
-
|
25
|
-
|
26
|
-
def aio(
|
27
|
-
|
20
|
+
def __call__(
|
21
|
+
self, utm_source: typing.Optional[str] = None, next_url: typing.Optional[str] = None
|
22
|
+
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[tuple[str, str, str]]: ...
|
23
|
+
def aio(
|
24
|
+
self, utm_source: typing.Optional[str] = None, next_url: typing.Optional[str] = None
|
25
|
+
) -> typing.AsyncContextManager[tuple[str, str, str]]: ...
|
28
26
|
|
29
27
|
start: __start_spec
|
30
28
|
|
31
29
|
class __finish_spec(typing_extensions.Protocol):
|
32
|
-
def __call__(
|
33
|
-
|
34
|
-
|
35
|
-
async def aio(
|
36
|
-
|
30
|
+
def __call__(
|
31
|
+
self, timeout: float = 40.0, grpc_extra_timeout: float = 5.0
|
32
|
+
) -> typing.Optional[modal_proto.api_pb2.TokenFlowWaitResponse]: ...
|
33
|
+
async def aio(
|
34
|
+
self, timeout: float = 40.0, grpc_extra_timeout: float = 5.0
|
35
|
+
) -> typing.Optional[modal_proto.api_pb2.TokenFlowWaitResponse]: ...
|
37
36
|
|
38
37
|
finish: __finish_spec
|
39
38
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
39
|
+
async def _new_token(
|
40
|
+
*,
|
41
|
+
profile: typing.Optional[str] = None,
|
42
|
+
activate: bool = True,
|
43
|
+
verify: bool = True,
|
44
|
+
source: typing.Optional[str] = None,
|
45
|
+
next_url: typing.Optional[str] = None,
|
46
|
+
): ...
|
47
|
+
async def _set_token(
|
48
|
+
token_id: str,
|
49
|
+
token_secret: str,
|
50
|
+
*,
|
51
|
+
profile: typing.Optional[str] = None,
|
52
|
+
activate: bool = True,
|
53
|
+
verify: bool = True,
|
54
|
+
): ...
|
55
|
+
def _open_url(url: str) -> bool: ...
|
modal/volume.py
CHANGED
@@ -2,35 +2,33 @@
|
|
2
2
|
import asyncio
|
3
3
|
import concurrent.futures
|
4
4
|
import enum
|
5
|
+
import functools
|
5
6
|
import os
|
6
7
|
import platform
|
7
8
|
import re
|
8
9
|
import time
|
10
|
+
import typing
|
11
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Generator, Sequence
|
9
12
|
from dataclasses import dataclass
|
10
13
|
from pathlib import Path, PurePosixPath
|
11
14
|
from typing import (
|
12
15
|
IO,
|
13
|
-
|
14
|
-
AsyncIterator,
|
16
|
+
Any,
|
15
17
|
BinaryIO,
|
16
18
|
Callable,
|
17
|
-
Generator,
|
18
|
-
List,
|
19
19
|
Optional,
|
20
|
-
Sequence,
|
21
|
-
Type,
|
22
20
|
Union,
|
23
21
|
)
|
24
22
|
|
25
|
-
import aiostream
|
26
23
|
from grpclib import GRPCError, Status
|
27
24
|
from synchronicity.async_wrap import asynccontextmanager
|
28
25
|
|
29
|
-
|
26
|
+
import modal_proto.api_pb2
|
27
|
+
from modal.exception import VolumeUploadTimeoutError
|
30
28
|
from modal_proto import api_pb2
|
31
29
|
|
32
30
|
from ._resolver import Resolver
|
33
|
-
from ._utils.async_utils import TaskContext, asyncnullcontext, synchronize_api
|
31
|
+
from ._utils.async_utils import TaskContext, aclosing, async_map, asyncnullcontext, synchronize_api
|
34
32
|
from ._utils.blob_utils import (
|
35
33
|
FileUploadSpec,
|
36
34
|
blob_iter,
|
@@ -38,15 +36,16 @@ from ._utils.blob_utils import (
|
|
38
36
|
get_file_upload_spec_from_fileobj,
|
39
37
|
get_file_upload_spec_from_path,
|
40
38
|
)
|
41
|
-
from ._utils.
|
39
|
+
from ._utils.deprecation import deprecation_error, deprecation_warning, renamed_parameter
|
40
|
+
from ._utils.grpc_utils import retry_transient_errors
|
41
|
+
from ._utils.name_utils import check_object_name
|
42
42
|
from .client import _Client
|
43
43
|
from .config import logger
|
44
|
-
from .exception import deprecation_error
|
45
44
|
from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
|
46
45
|
|
47
46
|
# Max duration for uploading to volumes files
|
48
47
|
# As a guide, files >40GiB will take >10 minutes to upload.
|
49
|
-
VOLUME_PUT_FILE_CLIENT_TIMEOUT =
|
48
|
+
VOLUME_PUT_FILE_CLIENT_TIMEOUT = 60 * 60
|
50
49
|
|
51
50
|
|
52
51
|
class FileEntryType(enum.IntEnum):
|
@@ -76,12 +75,6 @@ class FileEntry:
|
|
76
75
|
size=proto.size,
|
77
76
|
)
|
78
77
|
|
79
|
-
def __getattr__(self, name: str):
|
80
|
-
deprecation_error(
|
81
|
-
(2024, 4, 15),
|
82
|
-
f"The FileEntry dataclass was introduced to replace a private Protobuf message. This dataclass does not have the {name} attribute.",
|
83
|
-
)
|
84
|
-
|
85
78
|
|
86
79
|
class _Volume(_Object, type_prefix="vo"):
|
87
80
|
"""A writeable volume that can be used to share files between one or more Modal functions.
|
@@ -108,7 +101,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
108
101
|
```python
|
109
102
|
import modal
|
110
103
|
|
111
|
-
app = modal.App()
|
104
|
+
app = modal.App()
|
112
105
|
volume = modal.Volume.from_name("my-persisted-volume", create_if_missing=True)
|
113
106
|
|
114
107
|
@app.function(volumes={"/root/foo": volume})
|
@@ -125,69 +118,57 @@ class _Volume(_Object, type_prefix="vo"):
|
|
125
118
|
```
|
126
119
|
"""
|
127
120
|
|
128
|
-
_lock: asyncio.Lock
|
121
|
+
_lock: Optional[asyncio.Lock] = None
|
129
122
|
|
130
|
-
def
|
123
|
+
async def _get_lock(self):
|
131
124
|
# To (mostly*) prevent multiple concurrent operations on the same volume, which can cause problems under
|
132
125
|
# some unlikely circumstances.
|
133
126
|
# *: You can bypass this by creating multiple handles to the same volume, e.g. via lookup. But this
|
134
127
|
# covers the typical case = good enough.
|
135
|
-
self._lock = asyncio.Lock()
|
136
|
-
|
137
|
-
@staticmethod
|
138
|
-
def new() -> "_Volume":
|
139
|
-
"""`Volume.new` is deprecated.
|
140
128
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
# Volume already exists; do nothing.
|
149
|
-
self._hydrate(existing_object_id, resolver.client, None)
|
150
|
-
return
|
151
|
-
|
152
|
-
status_row.message("Creating volume...")
|
153
|
-
req = api_pb2.VolumeCreateRequest(app_id=resolver.app_id)
|
154
|
-
resp = await retry_transient_errors(resolver.client.stub.VolumeCreate, req)
|
155
|
-
status_row.finish("Created volume.")
|
156
|
-
self._hydrate(resp.volume_id, resolver.client, None)
|
157
|
-
|
158
|
-
return _Volume._from_loader(_load, "Volume()")
|
129
|
+
# Note: this function runs no async code but is marked as async to ensure it's
|
130
|
+
# being run inside the synchronicity event loop and binds the lock to the
|
131
|
+
# correct event loop on Python 3.9 which eagerly assigns event loops on
|
132
|
+
# constructions of locks
|
133
|
+
if self._lock is None:
|
134
|
+
self._lock = asyncio.Lock()
|
135
|
+
return self._lock
|
159
136
|
|
160
137
|
@staticmethod
|
138
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
161
139
|
def from_name(
|
162
|
-
|
140
|
+
name: str,
|
163
141
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
164
142
|
environment_name: Optional[str] = None,
|
165
143
|
create_if_missing: bool = False,
|
144
|
+
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
166
145
|
) -> "_Volume":
|
167
|
-
"""
|
146
|
+
"""Reference a Volume by name, creating if necessary.
|
168
147
|
|
169
|
-
|
148
|
+
In contrast to `modal.Volume.lookup`, this is a lazy method
|
149
|
+
that defers hydrating the local object with metadata from
|
150
|
+
Modal servers until the first time is is actually used.
|
170
151
|
|
171
152
|
```python
|
172
|
-
|
173
|
-
|
174
|
-
volume = modal.Volume.from_name("my-volume", create_if_missing=True)
|
153
|
+
vol = modal.Volume.from_name("my-volume", create_if_missing=True)
|
175
154
|
|
176
|
-
app = modal.App()
|
155
|
+
app = modal.App()
|
177
156
|
|
178
157
|
# Volume refers to the same object, even across instances of `app`.
|
179
|
-
@app.function(volumes={"/
|
158
|
+
@app.function(volumes={"/data": vol})
|
180
159
|
def f():
|
181
160
|
pass
|
182
161
|
```
|
183
162
|
"""
|
163
|
+
check_object_name(name, "Volume")
|
184
164
|
|
185
165
|
async def _load(self: _Volume, resolver: Resolver, existing_object_id: Optional[str]):
|
186
166
|
req = api_pb2.VolumeGetOrCreateRequest(
|
187
|
-
deployment_name=
|
167
|
+
deployment_name=name,
|
188
168
|
namespace=namespace,
|
189
169
|
environment_name=_get_environment_name(environment_name, resolver),
|
190
170
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
171
|
+
version=version,
|
191
172
|
)
|
192
173
|
response = await resolver.client.stub.VolumeGetOrCreate(req)
|
193
174
|
self._hydrate(response.volume_id, resolver.client, None)
|
@@ -197,20 +178,24 @@ class _Volume(_Object, type_prefix="vo"):
|
|
197
178
|
@classmethod
|
198
179
|
@asynccontextmanager
|
199
180
|
async def ephemeral(
|
200
|
-
cls:
|
181
|
+
cls: type["_Volume"],
|
201
182
|
client: Optional[_Client] = None,
|
202
183
|
environment_name: Optional[str] = None,
|
184
|
+
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
203
185
|
_heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
|
204
|
-
) ->
|
186
|
+
) -> AsyncGenerator["_Volume", None]:
|
205
187
|
"""Creates a new ephemeral volume within a context manager:
|
206
188
|
|
207
189
|
Usage:
|
208
190
|
```python
|
209
|
-
|
210
|
-
|
191
|
+
import modal
|
192
|
+
with modal.Volume.ephemeral() as vol:
|
193
|
+
assert vol.listdir("/") == []
|
194
|
+
```
|
211
195
|
|
212
|
-
|
213
|
-
|
196
|
+
```python notest
|
197
|
+
async with modal.Volume.ephemeral() as vol:
|
198
|
+
assert await vol.listdir("/") == []
|
214
199
|
```
|
215
200
|
"""
|
216
201
|
if client is None:
|
@@ -218,6 +203,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
218
203
|
request = api_pb2.VolumeGetOrCreateRequest(
|
219
204
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
|
220
205
|
environment_name=_get_environment_name(environment_name),
|
206
|
+
version=version,
|
221
207
|
)
|
222
208
|
response = await client.stub.VolumeGetOrCreate(request)
|
223
209
|
async with TaskContext() as tc:
|
@@ -226,33 +212,31 @@ class _Volume(_Object, type_prefix="vo"):
|
|
226
212
|
yield cls._new_hydrated(response.volume_id, client, None, is_another_app=True)
|
227
213
|
|
228
214
|
@staticmethod
|
229
|
-
|
230
|
-
label: str,
|
231
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
232
|
-
environment_name: Optional[str] = None,
|
233
|
-
cloud: Optional[str] = None,
|
234
|
-
) -> "_Volume":
|
235
|
-
"""Deprecated! Use `Volume.from_name(name, create_if_missing=True)`."""
|
236
|
-
deprecation_warning((2024, 3, 1), _Volume.persisted.__doc__)
|
237
|
-
return _Volume.from_name(label, namespace, environment_name, create_if_missing=True)
|
238
|
-
|
239
|
-
@staticmethod
|
215
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
240
216
|
async def lookup(
|
241
|
-
|
217
|
+
name: str,
|
242
218
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
243
219
|
client: Optional[_Client] = None,
|
244
220
|
environment_name: Optional[str] = None,
|
245
221
|
create_if_missing: bool = False,
|
222
|
+
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
246
223
|
) -> "_Volume":
|
247
|
-
"""Lookup a
|
224
|
+
"""Lookup a named Volume.
|
248
225
|
|
249
|
-
|
250
|
-
|
251
|
-
|
226
|
+
In contrast to `modal.Volume.from_name`, this is an eager method
|
227
|
+
that will hydrate the local object with metadata from Modal servers.
|
228
|
+
|
229
|
+
```python notest
|
230
|
+
vol = modal.Volume.lookup("my-volume")
|
231
|
+
print(vol.listdir("/"))
|
252
232
|
```
|
253
233
|
"""
|
254
234
|
obj = _Volume.from_name(
|
255
|
-
|
235
|
+
name,
|
236
|
+
namespace=namespace,
|
237
|
+
environment_name=environment_name,
|
238
|
+
create_if_missing=create_if_missing,
|
239
|
+
version=version,
|
256
240
|
)
|
257
241
|
if client is None:
|
258
242
|
client = await _Client.from_env()
|
@@ -266,8 +250,10 @@ class _Volume(_Object, type_prefix="vo"):
|
|
266
250
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
267
251
|
client: Optional[_Client] = None,
|
268
252
|
environment_name: Optional[str] = None,
|
253
|
+
version: "typing.Optional[modal_proto.api_pb2.VolumeFsVersion.ValueType]" = None,
|
269
254
|
) -> str:
|
270
255
|
"""mdmd:hidden"""
|
256
|
+
check_object_name(deployment_name, "Volume")
|
271
257
|
if client is None:
|
272
258
|
client = await _Client.from_env()
|
273
259
|
request = api_pb2.VolumeGetOrCreateRequest(
|
@@ -275,13 +261,14 @@ class _Volume(_Object, type_prefix="vo"):
|
|
275
261
|
namespace=namespace,
|
276
262
|
environment_name=_get_environment_name(environment_name),
|
277
263
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
264
|
+
version=version,
|
278
265
|
)
|
279
266
|
resp = await retry_transient_errors(client.stub.VolumeGetOrCreate, request)
|
280
267
|
return resp.volume_id
|
281
268
|
|
282
269
|
@live_method
|
283
270
|
async def _do_reload(self, lock=True):
|
284
|
-
async with self.
|
271
|
+
async with (await self._get_lock()) if lock else asyncnullcontext():
|
285
272
|
req = api_pb2.VolumeReloadRequest(volume_id=self.object_id)
|
286
273
|
_ = await retry_transient_errors(self._client.stub.VolumeReload, req)
|
287
274
|
|
@@ -292,7 +279,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
292
279
|
If successful, the changes made are now persisted in durable storage and available to other containers accessing
|
293
280
|
the volume.
|
294
281
|
"""
|
295
|
-
async with self.
|
282
|
+
async with await self._get_lock():
|
296
283
|
req = api_pb2.VolumeCommitRequest(volume_id=self.object_id)
|
297
284
|
try:
|
298
285
|
# TODO(gongy): only apply indefinite retries on 504 status.
|
@@ -343,23 +330,30 @@ class _Volume(_Object, type_prefix="vo"):
|
|
343
330
|
# This allows us to remove the server shim after 0.62 is no longer supported.
|
344
331
|
deprecation = deprecation_warning if (major_number, minor_number) <= (0, 62) else deprecation_error
|
345
332
|
if path.endswith("**"):
|
333
|
+
msg = (
|
334
|
+
"Glob patterns in `volume get` and `Volume.listdir()` are deprecated. "
|
335
|
+
"Please pass recursive=True instead. For the CLI, just remove the glob suffix."
|
336
|
+
)
|
346
337
|
deprecation(
|
347
338
|
(2024, 4, 23),
|
348
|
-
|
339
|
+
msg,
|
349
340
|
)
|
350
341
|
elif path.endswith("*"):
|
351
342
|
deprecation(
|
352
343
|
(2024, 4, 23),
|
353
|
-
|
344
|
+
(
|
345
|
+
"Glob patterns in `volume get` and `Volume.listdir()` are deprecated. "
|
346
|
+
"Please remove the glob `*` suffix."
|
347
|
+
),
|
354
348
|
)
|
355
349
|
|
356
350
|
req = api_pb2.VolumeListFilesRequest(volume_id=self.object_id, path=path, recursive=recursive)
|
357
|
-
async for batch in
|
351
|
+
async for batch in self._client.stub.VolumeListFiles.unary_stream(req):
|
358
352
|
for entry in batch.entries:
|
359
353
|
yield FileEntry._from_proto(entry)
|
360
354
|
|
361
355
|
@live_method
|
362
|
-
async def listdir(self, path: str, *, recursive: bool = False) ->
|
356
|
+
async def listdir(self, path: str, *, recursive: bool = False) -> list[FileEntry]:
|
363
357
|
"""List all files under a path prefix in the modal.Volume.
|
364
358
|
|
365
359
|
Passing a directory path lists all files in the directory. For a file path, return only that
|
@@ -369,7 +363,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
369
363
|
return [entry async for entry in self.iterdir(path, recursive=recursive)]
|
370
364
|
|
371
365
|
@live_method_gen
|
372
|
-
async def read_file(self, path:
|
366
|
+
async def read_file(self, path: str) -> AsyncIterator[bytes]:
|
373
367
|
"""
|
374
368
|
Read a file from the modal.Volume.
|
375
369
|
|
@@ -383,8 +377,6 @@ class _Volume(_Object, type_prefix="vo"):
|
|
383
377
|
print(len(data)) # == 1024 * 1024
|
384
378
|
```
|
385
379
|
"""
|
386
|
-
if isinstance(path, str):
|
387
|
-
path = path.encode("utf-8")
|
388
380
|
req = api_pb2.VolumeGetFileRequest(volume_id=self.object_id, path=path)
|
389
381
|
try:
|
390
382
|
response = await retry_transient_errors(self._client.stub.VolumeGetFile, req)
|
@@ -399,14 +391,12 @@ class _Volume(_Object, type_prefix="vo"):
|
|
399
391
|
yield data
|
400
392
|
|
401
393
|
@live_method
|
402
|
-
async def read_file_into_fileobj(self, path:
|
394
|
+
async def read_file_into_fileobj(self, path: str, fileobj: IO[bytes]) -> int:
|
403
395
|
"""mdmd:hidden
|
404
396
|
|
405
397
|
Read volume file into file-like IO object.
|
406
398
|
In the future, this will replace the current generator implementation of the `read_file` method.
|
407
399
|
"""
|
408
|
-
if isinstance(path, str):
|
409
|
-
path = path.encode("utf-8")
|
410
400
|
|
411
401
|
chunk_size_bytes = 8 * 1024 * 1024
|
412
402
|
start = 0
|
@@ -420,7 +410,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
420
410
|
|
421
411
|
n = fileobj.write(response.data)
|
422
412
|
if n != len(response.data):
|
423
|
-
raise
|
413
|
+
raise OSError(f"failed to write {len(response.data)} bytes to output. Wrote {n}.")
|
424
414
|
elif n == response.size:
|
425
415
|
return response.size
|
426
416
|
elif n > response.size:
|
@@ -441,7 +431,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
441
431
|
|
442
432
|
n = fileobj.write(response.data)
|
443
433
|
if n != len(response.data):
|
444
|
-
raise
|
434
|
+
raise OSError(f"failed to write {len(response.data)} bytes to output. Wrote {n}.")
|
445
435
|
written += n
|
446
436
|
if written == file_size:
|
447
437
|
break
|
@@ -449,15 +439,13 @@ class _Volume(_Object, type_prefix="vo"):
|
|
449
439
|
return written
|
450
440
|
|
451
441
|
@live_method
|
452
|
-
async def remove_file(self, path:
|
442
|
+
async def remove_file(self, path: str, recursive: bool = False) -> None:
|
453
443
|
"""Remove a file or directory from a volume."""
|
454
|
-
if isinstance(path, str):
|
455
|
-
path = path.encode("utf-8")
|
456
444
|
req = api_pb2.VolumeRemoveFileRequest(volume_id=self.object_id, path=path, recursive=recursive)
|
457
445
|
await retry_transient_errors(self._client.stub.VolumeRemoveFile, req)
|
458
446
|
|
459
447
|
@live_method
|
460
|
-
async def copy_files(self, src_paths: Sequence[
|
448
|
+
async def copy_files(self, src_paths: Sequence[str], dst_path: str) -> None:
|
461
449
|
"""
|
462
450
|
Copy files within the volume from src_paths to dst_path.
|
463
451
|
The semantics of the copy operation follow those of the UNIX cp command.
|
@@ -481,10 +469,6 @@ class _Volume(_Object, type_prefix="vo"):
|
|
481
469
|
like `os.rename()` and then `commit()` the volume. The `copy_files()` method is useful when you don't have
|
482
470
|
the volume mounted as a filesystem, e.g. when running a script on your local computer.
|
483
471
|
"""
|
484
|
-
src_paths = [path.encode("utf-8") for path in src_paths if isinstance(path, str)]
|
485
|
-
if isinstance(dst_path, str):
|
486
|
-
dst_path = dst_path.encode("utf-8")
|
487
|
-
|
488
472
|
request = api_pb2.VolumeCopyFilesRequest(volume_id=self.object_id, src_paths=src_paths, dst_path=dst_path)
|
489
473
|
await retry_transient_errors(self._client.stub.VolumeCopyFiles, request, base_delay=1)
|
490
474
|
|
@@ -515,32 +499,25 @@ class _Volume(_Object, type_prefix="vo"):
|
|
515
499
|
self._client.stub.VolumeDelete, api_pb2.VolumeDeleteRequest(volume_id=self.object_id)
|
516
500
|
)
|
517
501
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
if isinstance(self := args[0], _Volume):
|
523
|
-
msg = (
|
524
|
-
"Calling Volume.delete as an instance method is deprecated."
|
525
|
-
" Please update your code to call it as a static method, passing"
|
526
|
-
" the name of the volume to delete, e.g. `modal.Volume.delete('my-volume')`."
|
527
|
-
)
|
528
|
-
deprecation_warning((2024, 4, 23), msg)
|
529
|
-
await self._instance_delete()
|
530
|
-
return
|
531
|
-
elif isinstance(args[0], type):
|
532
|
-
args = args[1:]
|
533
|
-
|
534
|
-
if args and isinstance(args[0], str):
|
535
|
-
if label:
|
536
|
-
raise InvalidError("`label` specified as both positional and keyword argument")
|
537
|
-
label = args[0]
|
538
|
-
# -- Backwards-compatibility code ends here
|
539
|
-
|
540
|
-
obj = await _Volume.lookup(label, client=client, environment_name=environment_name)
|
502
|
+
@staticmethod
|
503
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
504
|
+
async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
505
|
+
obj = await _Volume.lookup(name, client=client, environment_name=environment_name)
|
541
506
|
req = api_pb2.VolumeDeleteRequest(volume_id=obj.object_id)
|
542
507
|
await retry_transient_errors(obj._client.stub.VolumeDelete, req)
|
543
508
|
|
509
|
+
@staticmethod
|
510
|
+
async def rename(
|
511
|
+
old_name: str,
|
512
|
+
new_name: str,
|
513
|
+
*,
|
514
|
+
client: Optional[_Client] = None,
|
515
|
+
environment_name: Optional[str] = None,
|
516
|
+
):
|
517
|
+
obj = await _Volume.lookup(old_name, client=client, environment_name=environment_name)
|
518
|
+
req = api_pb2.VolumeRenameRequest(volume_id=obj.object_id, name=new_name)
|
519
|
+
await retry_transient_errors(obj._client.stub.VolumeRename, req)
|
520
|
+
|
544
521
|
|
545
522
|
class _VolumeUploadContextManager:
|
546
523
|
"""Context manager for batch-uploading files to a Volume."""
|
@@ -548,13 +525,17 @@ class _VolumeUploadContextManager:
|
|
548
525
|
_volume_id: str
|
549
526
|
_client: _Client
|
550
527
|
_force: bool
|
551
|
-
|
528
|
+
progress_cb: Callable[..., Any]
|
529
|
+
_upload_generators: list[Generator[Callable[[], FileUploadSpec], None, None]]
|
552
530
|
|
553
|
-
def __init__(
|
531
|
+
def __init__(
|
532
|
+
self, volume_id: str, client: _Client, progress_cb: Optional[Callable[..., Any]] = None, force: bool = False
|
533
|
+
):
|
554
534
|
"""mdmd:hidden"""
|
555
535
|
self._volume_id = volume_id
|
556
536
|
self._client = client
|
557
537
|
self._upload_generators = []
|
538
|
+
self._progress_cb = progress_cb or (lambda *_, **__: None)
|
558
539
|
self._force = force
|
559
540
|
|
560
541
|
async def __aenter__(self):
|
@@ -576,11 +557,13 @@ class _VolumeUploadContextManager:
|
|
576
557
|
for fut in asyncio.as_completed(futs):
|
577
558
|
yield await fut
|
578
559
|
|
579
|
-
# Compute checksums
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
560
|
+
# Compute checksums & Upload files
|
561
|
+
files: list[api_pb2.MountFile] = []
|
562
|
+
async with aclosing(async_map(gen_file_upload_specs(), self._upload_file, concurrency=20)) as stream:
|
563
|
+
async for item in stream:
|
564
|
+
files.append(item)
|
565
|
+
|
566
|
+
self._progress_cb(complete=True)
|
584
567
|
|
585
568
|
request = api_pb2.VolumePutFilesRequest(
|
586
569
|
volume_id=self._volume_id,
|
@@ -646,7 +629,7 @@ class _VolumeUploadContextManager:
|
|
646
629
|
|
647
630
|
async def _upload_file(self, file_spec: FileUploadSpec) -> api_pb2.MountFile:
|
648
631
|
remote_filename = file_spec.mount_filename
|
649
|
-
|
632
|
+
progress_task_id = self._progress_cb(name=remote_filename, size=file_spec.size)
|
650
633
|
request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
|
651
634
|
response = await retry_transient_errors(self._client.stub.MountPutFile, request, base_delay=1)
|
652
635
|
|
@@ -655,7 +638,13 @@ class _VolumeUploadContextManager:
|
|
655
638
|
if file_spec.use_blob:
|
656
639
|
logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
|
657
640
|
with file_spec.source() as fp:
|
658
|
-
blob_id = await blob_upload_file(
|
641
|
+
blob_id = await blob_upload_file(
|
642
|
+
fp,
|
643
|
+
self._client.stub,
|
644
|
+
functools.partial(self._progress_cb, progress_task_id),
|
645
|
+
sha256_hex=file_spec.sha256_hex,
|
646
|
+
md5_hex=file_spec.md5_hex,
|
647
|
+
)
|
659
648
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
660
649
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
661
650
|
else:
|
@@ -663,6 +652,7 @@ class _VolumeUploadContextManager:
|
|
663
652
|
f"Uploading file {file_spec.source_description} to {remote_filename} ({file_spec.size} bytes)"
|
664
653
|
)
|
665
654
|
request2 = api_pb2.MountPutFileRequest(data=file_spec.content, sha256_hex=file_spec.sha256_hex)
|
655
|
+
self._progress_cb(task_id=progress_task_id, complete=True)
|
666
656
|
|
667
657
|
while (time.monotonic() - start_time) < VOLUME_PUT_FILE_CLIENT_TIMEOUT:
|
668
658
|
response = await retry_transient_errors(self._client.stub.MountPutFile, request2, base_delay=1)
|
@@ -671,7 +661,8 @@ class _VolumeUploadContextManager:
|
|
671
661
|
|
672
662
|
if not response.exists:
|
673
663
|
raise VolumeUploadTimeoutError(f"Uploading of {file_spec.source_description} timed out")
|
674
|
-
|
664
|
+
else:
|
665
|
+
self._progress_cb(task_id=progress_task_id, complete=True)
|
675
666
|
return api_pb2.MountFile(
|
676
667
|
filename=remote_filename,
|
677
668
|
sha256_hex=file_spec.sha256_hex,
|
@@ -697,16 +688,11 @@ def _open_files_error_annotation(mount_path: str) -> Optional[str]:
|
|
697
688
|
cmdline = " ".join([part.decode() for part in parts]).rstrip(" ")
|
698
689
|
|
699
690
|
cwd = PurePosixPath(os.readlink(f"/proc/{pid}/cwd"))
|
700
|
-
|
701
|
-
# we drop Python 3.8 support.
|
702
|
-
try:
|
703
|
-
_rel_cwd = cwd.relative_to(mount_path)
|
691
|
+
if cwd.is_relative_to(mount_path):
|
704
692
|
if pid == self_pid:
|
705
693
|
return "cwd is inside volume"
|
706
694
|
else:
|
707
695
|
return f"cwd of '{cmdline}' is inside volume"
|
708
|
-
except ValueError:
|
709
|
-
pass
|
710
696
|
|
711
697
|
for fd in os.listdir(f"/proc/{pid}/fd"):
|
712
698
|
try:
|