modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__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 +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +4 -2
- modal/_container_entrypoint.py +41 -49
- modal/_functions.py +424 -195
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +68 -20
- modal/_output.py +58 -45
- modal/_partial_function.py +36 -11
- modal/_pty.py +7 -3
- modal/_resolver.py +21 -35
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +301 -186
- modal/_runtime/container_io_manager.pyi +70 -61
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +170 -63
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +57 -1
- modal/_utils/async_utils.py +33 -12
- modal/_utils/auth_token_manager.py +2 -5
- modal/_utils/blob_utils.py +110 -53
- modal/_utils/function_utils.py +49 -42
- modal/_utils/grpc_utils.py +80 -50
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +219 -83
- modal/app.pyi +229 -56
- modal/billing.py +5 -0
- modal/{requirements → builder}/2025.06.txt +1 -0
- modal/{requirements → builder}/PREVIEW.txt +1 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +9 -13
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +58 -16
- modal/cli/secret.py +48 -22
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +28 -25
- modal/client.py +13 -116
- modal/client.pyi +9 -91
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +5 -1
- modal/cls.py +130 -102
- modal/cls.pyi +45 -85
- modal/config.py +29 -10
- modal/container_process.py +291 -13
- modal/container_process.pyi +95 -32
- modal/dict.py +282 -63
- modal/dict.pyi +423 -73
- modal/environments.py +15 -27
- modal/environments.pyi +5 -15
- modal/exception.py +8 -0
- modal/experimental/__init__.py +143 -38
- modal/experimental/flash.py +247 -78
- modal/experimental/flash.pyi +137 -9
- modal/file_io.py +14 -28
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +134 -61
- modal/image.py +255 -86
- modal/image.pyi +300 -62
- modal/io_streams.py +436 -126
- modal/io_streams.pyi +236 -171
- modal/mount.py +62 -157
- modal/mount.pyi +45 -172
- modal/network_file_system.py +30 -53
- modal/network_file_system.pyi +16 -76
- modal/object.pyi +42 -8
- modal/parallel_map.py +821 -113
- modal/parallel_map.pyi +134 -0
- modal/partial_function.pyi +4 -1
- modal/proxy.py +16 -7
- modal/proxy.pyi +10 -2
- modal/queue.py +263 -61
- modal/queue.pyi +409 -66
- modal/runner.py +112 -92
- modal/runner.pyi +45 -27
- modal/sandbox.py +451 -124
- modal/sandbox.pyi +513 -67
- modal/secret.py +291 -67
- modal/secret.pyi +425 -19
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/token_flow.py +4 -4
- modal/volume.py +344 -98
- modal/volume.pyi +464 -68
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +399 -67
- modal_proto/api_grpc.py +241 -1
- modal_proto/api_pb2.py +1395 -1000
- modal_proto/api_pb2.pyi +1239 -79
- modal_proto/api_pb2_grpc.py +499 -4
- modal_proto/api_pb2_grpc.pyi +162 -14
- modal_proto/modal_api_grpc.py +175 -160
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal-1.0.6.dev58.dist-info/RECORD +0 -183
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /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}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/_utils/grpc_utils.py
CHANGED
|
@@ -8,12 +8,8 @@ import typing
|
|
|
8
8
|
import urllib.parse
|
|
9
9
|
import uuid
|
|
10
10
|
from collections.abc import AsyncIterator
|
|
11
|
-
from dataclasses import dataclass
|
|
12
|
-
from typing import
|
|
13
|
-
Any,
|
|
14
|
-
Optional,
|
|
15
|
-
TypeVar,
|
|
16
|
-
)
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Optional, TypeVar
|
|
17
13
|
|
|
18
14
|
import grpclib.client
|
|
19
15
|
import grpclib.config
|
|
@@ -28,6 +24,7 @@ from grpclib.protocol import H2Protocol
|
|
|
28
24
|
from modal.exception import AuthError, ConnectionError
|
|
29
25
|
from modal_version import __version__
|
|
30
26
|
|
|
27
|
+
from .._traceback import suppress_tb_frames
|
|
31
28
|
from .async_utils import retry
|
|
32
29
|
from .logger import logger
|
|
33
30
|
|
|
@@ -35,6 +32,7 @@ RequestType = TypeVar("RequestType", bound=Message)
|
|
|
35
32
|
ResponseType = TypeVar("ResponseType", bound=Message)
|
|
36
33
|
|
|
37
34
|
if typing.TYPE_CHECKING:
|
|
35
|
+
import modal._grpc_client
|
|
38
36
|
import modal.client
|
|
39
37
|
|
|
40
38
|
# Monkey patches grpclib to have a Modal User Agent header.
|
|
@@ -165,7 +163,7 @@ if typing.TYPE_CHECKING:
|
|
|
165
163
|
|
|
166
164
|
|
|
167
165
|
async def unary_stream(
|
|
168
|
-
method: "modal.
|
|
166
|
+
method: "modal._grpc_client.UnaryStreamWrapper[RequestType, ResponseType]",
|
|
169
167
|
request: RequestType,
|
|
170
168
|
metadata: Optional[Any] = None,
|
|
171
169
|
) -> AsyncIterator[ResponseType]:
|
|
@@ -174,36 +172,66 @@ async def unary_stream(
|
|
|
174
172
|
yield item
|
|
175
173
|
|
|
176
174
|
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class Retry:
|
|
177
|
+
base_delay: float = 0.1
|
|
178
|
+
max_delay: float = 1
|
|
179
|
+
delay_factor: float = 2
|
|
180
|
+
max_retries: Optional[int] = 3
|
|
181
|
+
additional_status_codes: list = field(default_factory=list)
|
|
182
|
+
attempt_timeout: Optional[float] = None # timeout for each attempt
|
|
183
|
+
total_timeout: Optional[float] = None # timeout for the entire function call
|
|
184
|
+
attempt_timeout_floor: float = 2.0 # always have at least this much timeout (only for total_timeout)
|
|
185
|
+
warning_message: Optional[RetryWarningMessage] = None
|
|
186
|
+
|
|
187
|
+
|
|
177
188
|
async def retry_transient_errors(
|
|
178
|
-
fn: "
|
|
179
|
-
|
|
180
|
-
base_delay: float = 0.1,
|
|
181
|
-
max_delay: float = 1,
|
|
182
|
-
delay_factor: float = 2,
|
|
189
|
+
fn: "grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]",
|
|
190
|
+
req: RequestType,
|
|
183
191
|
max_retries: Optional[int] = 3,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
) -> ResponseType:
|
|
193
|
+
"""Minimum API version of _retry_transient_errors that works with grpclib.client.UnaryUnaryMethod.
|
|
194
|
+
|
|
195
|
+
Used by modal server.
|
|
196
|
+
"""
|
|
197
|
+
return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def _retry_transient_errors(
|
|
201
|
+
fn: typing.Union[
|
|
202
|
+
"modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
|
|
203
|
+
"grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]",
|
|
204
|
+
],
|
|
205
|
+
req: RequestType,
|
|
206
|
+
retry: Retry,
|
|
207
|
+
metadata: Optional[list[tuple[str, str]]] = None,
|
|
190
208
|
) -> ResponseType:
|
|
191
209
|
"""Retry on transient gRPC failures with back-off until max_retries is reached.
|
|
192
210
|
If max_retries is None, retry forever."""
|
|
211
|
+
import modal._grpc_client
|
|
212
|
+
|
|
213
|
+
if isinstance(fn, modal._grpc_client.UnaryUnaryWrapper):
|
|
214
|
+
fn_callable = fn.direct
|
|
215
|
+
elif isinstance(fn, grpclib.client.UnaryUnaryMethod):
|
|
216
|
+
fn_callable = fn # type: ignore
|
|
217
|
+
else:
|
|
218
|
+
raise ValueError("Only modal._grpc_client.UnaryUnaryWrapper and grpclib.client.UnaryUnaryMethod are supported")
|
|
193
219
|
|
|
194
|
-
delay = base_delay
|
|
220
|
+
delay = retry.base_delay
|
|
195
221
|
n_retries = 0
|
|
196
222
|
|
|
197
|
-
status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *additional_status_codes]
|
|
223
|
+
status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
|
|
198
224
|
|
|
199
225
|
idempotency_key = str(uuid.uuid4())
|
|
200
226
|
|
|
201
227
|
t0 = time.time()
|
|
202
|
-
if total_timeout is not None:
|
|
203
|
-
total_deadline = t0 + total_timeout
|
|
228
|
+
if retry.total_timeout is not None:
|
|
229
|
+
total_deadline = t0 + retry.total_timeout
|
|
204
230
|
else:
|
|
205
231
|
total_deadline = None
|
|
206
232
|
|
|
233
|
+
metadata = (metadata or []) + [("x-modal-timestamp", str(time.time()))]
|
|
234
|
+
|
|
207
235
|
while True:
|
|
208
236
|
attempt_metadata = [
|
|
209
237
|
("x-idempotency-key", idempotency_key),
|
|
@@ -213,16 +241,17 @@ async def retry_transient_errors(
|
|
|
213
241
|
if n_retries > 0:
|
|
214
242
|
attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
|
|
215
243
|
timeouts = []
|
|
216
|
-
if attempt_timeout is not None:
|
|
217
|
-
timeouts.append(attempt_timeout)
|
|
218
|
-
if total_timeout is not None:
|
|
219
|
-
timeouts.append(max(total_deadline - time.time(), attempt_timeout_floor))
|
|
244
|
+
if retry.attempt_timeout is not None:
|
|
245
|
+
timeouts.append(retry.attempt_timeout)
|
|
246
|
+
if retry.total_timeout is not None and total_deadline is not None:
|
|
247
|
+
timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
|
|
220
248
|
if timeouts:
|
|
221
249
|
timeout = min(timeouts) # In case the function provided both types of timeouts
|
|
222
250
|
else:
|
|
223
251
|
timeout = None
|
|
224
252
|
try:
|
|
225
|
-
|
|
253
|
+
with suppress_tb_frames(1):
|
|
254
|
+
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
226
255
|
except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
|
|
227
256
|
if isinstance(exc, GRPCError) and exc.status not in status_codes:
|
|
228
257
|
if exc.status == Status.UNAUTHENTICATED:
|
|
@@ -230,45 +259,46 @@ async def retry_transient_errors(
|
|
|
230
259
|
else:
|
|
231
260
|
raise exc
|
|
232
261
|
|
|
233
|
-
if max_retries is not None and n_retries >= max_retries:
|
|
262
|
+
if retry.max_retries is not None and n_retries >= retry.max_retries:
|
|
234
263
|
final_attempt = True
|
|
235
|
-
elif total_deadline is not None and time.time() + delay + attempt_timeout_floor >= total_deadline:
|
|
264
|
+
elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
|
|
236
265
|
final_attempt = True
|
|
237
266
|
else:
|
|
238
267
|
final_attempt = False
|
|
239
268
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
with suppress_tb_frames(1):
|
|
270
|
+
if final_attempt:
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
|
|
273
|
+
f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
|
|
274
|
+
)
|
|
275
|
+
if isinstance(exc, OSError):
|
|
276
|
+
raise ConnectionError(str(exc))
|
|
277
|
+
elif isinstance(exc, asyncio.TimeoutError):
|
|
278
|
+
raise ConnectionError(str(exc))
|
|
279
|
+
else:
|
|
280
|
+
raise exc
|
|
281
|
+
|
|
282
|
+
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
283
|
+
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
284
|
+
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
285
|
+
# TODO: update to newer version (>=0.4.8) once stable
|
|
250
286
|
raise exc
|
|
251
287
|
|
|
252
|
-
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
253
|
-
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
254
|
-
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
255
|
-
# TODO: update to newer version (>=0.4.8) once stable
|
|
256
|
-
raise exc
|
|
257
|
-
|
|
258
288
|
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
|
|
259
289
|
|
|
260
290
|
n_retries += 1
|
|
261
291
|
|
|
262
292
|
if (
|
|
263
|
-
|
|
264
|
-
and n_retries %
|
|
293
|
+
retry.warning_message
|
|
294
|
+
and n_retries % retry.warning_message.warning_interval == 0
|
|
265
295
|
and isinstance(exc, GRPCError)
|
|
266
|
-
and exc.status in
|
|
296
|
+
and exc.status in retry.warning_message.errors_to_warn_for
|
|
267
297
|
):
|
|
268
|
-
logger.warning(
|
|
298
|
+
logger.warning(retry.warning_message.message)
|
|
269
299
|
|
|
270
300
|
await asyncio.sleep(delay)
|
|
271
|
-
delay = min(delay * delay_factor, max_delay)
|
|
301
|
+
delay = min(delay * retry.delay_factor, retry.max_delay)
|
|
272
302
|
|
|
273
303
|
|
|
274
304
|
def find_free_port() -> int:
|
modal/_utils/mount_utils.py
CHANGED
|
@@ -3,7 +3,9 @@ import posixpath
|
|
|
3
3
|
import typing
|
|
4
4
|
from collections.abc import Mapping, Sequence
|
|
5
5
|
from pathlib import PurePath, PurePosixPath
|
|
6
|
-
from typing import Union
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from typing_extensions import TypeGuard
|
|
7
9
|
|
|
8
10
|
from ..cloud_bucket_mount import _CloudBucketMount
|
|
9
11
|
from ..exception import InvalidError
|
|
@@ -76,3 +78,26 @@ def validate_volumes(
|
|
|
76
78
|
)
|
|
77
79
|
|
|
78
80
|
return validated_volumes
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_only_modal_volumes(
|
|
84
|
+
volumes: Optional[Optional[dict[Union[str, PurePosixPath], _Volume]]],
|
|
85
|
+
caller_name: str,
|
|
86
|
+
) -> Sequence[tuple[str, _Volume]]:
|
|
87
|
+
"""Validate all volumes are `modal.Volume`."""
|
|
88
|
+
if volumes is None:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
validated_volumes = validate_volumes(volumes)
|
|
92
|
+
|
|
93
|
+
# Although the typing forbids `_CloudBucketMount` for type checking, one can still pass a `_CloudBucketMount`
|
|
94
|
+
# during runtime, so we'll check the type here.
|
|
95
|
+
def all_modal_volumes(
|
|
96
|
+
vols: Sequence[tuple[str, Union[_Volume, _CloudBucketMount]]],
|
|
97
|
+
) -> TypeGuard[Sequence[tuple[str, _Volume]]]:
|
|
98
|
+
return all(isinstance(v, _Volume) for _, v in vols)
|
|
99
|
+
|
|
100
|
+
if not all_modal_volumes(validated_volumes):
|
|
101
|
+
raise InvalidError(f"{caller_name} only supports volumes that are modal.Volume")
|
|
102
|
+
|
|
103
|
+
return validated_volumes
|
modal/_utils/name_utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import re
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
|
|
4
5
|
from ..exception import InvalidError
|
|
5
6
|
|
|
@@ -31,12 +32,25 @@ def is_valid_environment_name(name: str) -> bool:
|
|
|
31
32
|
return len(name) <= 64 and re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-_.]+$", name) is not None
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
def is_valid_tag(tag: str) -> bool:
|
|
35
|
-
"""Tags are alphanumeric, dashes, periods, and underscores, and
|
|
36
|
-
pattern =
|
|
35
|
+
def is_valid_tag(tag: str, max_length: int = 50) -> bool:
|
|
36
|
+
"""Tags are alphanumeric, dashes, periods, and underscores, and not longer than the max_length."""
|
|
37
|
+
pattern = rf"^[a-zA-Z0-9._-]{{1,{max_length}}}$"
|
|
37
38
|
return bool(re.match(pattern, tag))
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def check_tag_dict(tags: Mapping[str, str]) -> None:
|
|
42
|
+
rules = (
|
|
43
|
+
"\n\nTags may contain only alphanumeric characters, dashes, periods, or underscores, "
|
|
44
|
+
"and must be 63 characters or less."
|
|
45
|
+
)
|
|
46
|
+
max_length = 63
|
|
47
|
+
for key, value in tags.items():
|
|
48
|
+
if not is_valid_tag(key, max_length):
|
|
49
|
+
raise InvalidError(f"Invalid tag key: {key!r}.{rules}")
|
|
50
|
+
if not is_valid_tag(value, max_length):
|
|
51
|
+
raise InvalidError(f"Invalid tag value: {value!r}.{rules}")
|
|
52
|
+
|
|
53
|
+
|
|
40
54
|
def check_object_name(name: str, object_type: str) -> None:
|
|
41
55
|
message = (
|
|
42
56
|
f"Invalid {object_type} name: '{name}'."
|