modal 0.62.16__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 +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- 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 +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- 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 +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.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 +5 -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 +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- 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_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- 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 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- 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 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- 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 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- 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 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
# Copyright Modal Labs 2024
|
2
|
+
import asyncio
|
3
|
+
import platform
|
4
|
+
from typing import Generic, Optional, TypeVar
|
5
|
+
|
6
|
+
from modal_proto import api_pb2
|
7
|
+
|
8
|
+
from ._utils.async_utils import TaskContext, synchronize_api
|
9
|
+
from ._utils.deprecation import deprecation_error
|
10
|
+
from ._utils.grpc_utils import retry_transient_errors
|
11
|
+
from ._utils.shell_utils import stream_from_stdin, write_to_fd
|
12
|
+
from .client import _Client
|
13
|
+
from .exception import InteractiveTimeoutError, InvalidError
|
14
|
+
from .io_streams import _StreamReader, _StreamWriter
|
15
|
+
from .stream_type import StreamType
|
16
|
+
|
17
|
+
T = TypeVar("T", str, bytes)
|
18
|
+
|
19
|
+
|
20
|
+
class _ContainerProcess(Generic[T]):
|
21
|
+
_process_id: Optional[str] = None
|
22
|
+
_stdout: _StreamReader[T]
|
23
|
+
_stderr: _StreamReader[T]
|
24
|
+
_stdin: _StreamWriter
|
25
|
+
_text: bool
|
26
|
+
_by_line: bool
|
27
|
+
_returncode: Optional[int] = None
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
process_id: str,
|
32
|
+
client: _Client,
|
33
|
+
stdout: StreamType = StreamType.PIPE,
|
34
|
+
stderr: StreamType = StreamType.PIPE,
|
35
|
+
text: bool = True,
|
36
|
+
by_line: bool = False,
|
37
|
+
) -> None:
|
38
|
+
self._process_id = process_id
|
39
|
+
self._client = client
|
40
|
+
self._text = text
|
41
|
+
self._by_line = by_line
|
42
|
+
self._stdout = _StreamReader[T](
|
43
|
+
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
44
|
+
process_id,
|
45
|
+
"container_process",
|
46
|
+
self._client,
|
47
|
+
stream_type=stdout,
|
48
|
+
text=text,
|
49
|
+
by_line=by_line,
|
50
|
+
)
|
51
|
+
self._stderr = _StreamReader[T](
|
52
|
+
api_pb2.FILE_DESCRIPTOR_STDERR,
|
53
|
+
process_id,
|
54
|
+
"container_process",
|
55
|
+
self._client,
|
56
|
+
stream_type=stderr,
|
57
|
+
text=text,
|
58
|
+
by_line=by_line,
|
59
|
+
)
|
60
|
+
self._stdin = _StreamWriter(process_id, "container_process", self._client)
|
61
|
+
|
62
|
+
@property
|
63
|
+
def stdout(self) -> _StreamReader[T]:
|
64
|
+
"""StreamReader for the container process's stdout stream."""
|
65
|
+
return self._stdout
|
66
|
+
|
67
|
+
@property
|
68
|
+
def stderr(self) -> _StreamReader[T]:
|
69
|
+
"""StreamReader for the container process's stderr stream."""
|
70
|
+
return self._stderr
|
71
|
+
|
72
|
+
@property
|
73
|
+
def stdin(self) -> _StreamWriter:
|
74
|
+
"""StreamWriter for the container process's stdin stream."""
|
75
|
+
return self._stdin
|
76
|
+
|
77
|
+
@property
|
78
|
+
def returncode(self) -> int:
|
79
|
+
if self._returncode is None:
|
80
|
+
raise InvalidError(
|
81
|
+
"You must call wait() before accessing the returncode. "
|
82
|
+
"To poll for the status of a running process, use poll() instead."
|
83
|
+
)
|
84
|
+
return self._returncode
|
85
|
+
|
86
|
+
async def poll(self) -> Optional[int]:
|
87
|
+
"""Check if the container process has finished running.
|
88
|
+
|
89
|
+
Returns `None` if the process is still running, else returns the exit code.
|
90
|
+
"""
|
91
|
+
if self._returncode is not None:
|
92
|
+
return self._returncode
|
93
|
+
|
94
|
+
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
|
95
|
+
resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(self._client.stub.ContainerExecWait, req)
|
96
|
+
|
97
|
+
if resp.completed:
|
98
|
+
self._returncode = resp.exit_code
|
99
|
+
return self._returncode
|
100
|
+
|
101
|
+
return None
|
102
|
+
|
103
|
+
async def wait(self) -> int:
|
104
|
+
"""Wait for the container process to finish running. Returns the exit code."""
|
105
|
+
|
106
|
+
if self._returncode is not None:
|
107
|
+
return self._returncode
|
108
|
+
|
109
|
+
while True:
|
110
|
+
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=50)
|
111
|
+
resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
|
112
|
+
self._client.stub.ContainerExecWait, req
|
113
|
+
)
|
114
|
+
if resp.completed:
|
115
|
+
self._returncode = resp.exit_code
|
116
|
+
return self._returncode
|
117
|
+
|
118
|
+
async def attach(self, *, pty: Optional[bool] = None):
|
119
|
+
if platform.system() == "Windows":
|
120
|
+
print("interactive exec is not currently supported on Windows.")
|
121
|
+
return
|
122
|
+
|
123
|
+
if pty is not None:
|
124
|
+
deprecation_error(
|
125
|
+
(2024, 12, 9),
|
126
|
+
"The `pty` argument to `modal.container_process.attach(pty=...)` is deprecated, "
|
127
|
+
"as only PTY mode is supported. Please remove the argument.",
|
128
|
+
)
|
129
|
+
|
130
|
+
from rich.console import Console
|
131
|
+
|
132
|
+
console = Console()
|
133
|
+
|
134
|
+
connecting_status = console.status("Connecting...")
|
135
|
+
connecting_status.start()
|
136
|
+
on_connect = asyncio.Event()
|
137
|
+
|
138
|
+
async def _write_to_fd_loop(stream: _StreamReader):
|
139
|
+
# Don't skip empty messages so we can detect when the process has booted.
|
140
|
+
async for chunk in stream._get_logs(skip_empty_messages=False):
|
141
|
+
if chunk is None:
|
142
|
+
break
|
143
|
+
|
144
|
+
if not on_connect.is_set():
|
145
|
+
connecting_status.stop()
|
146
|
+
on_connect.set()
|
147
|
+
|
148
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
149
|
+
|
150
|
+
async def _handle_input(data: bytes, message_index: int):
|
151
|
+
self.stdin.write(data)
|
152
|
+
await self.stdin.drain()
|
153
|
+
|
154
|
+
async with TaskContext() as tc:
|
155
|
+
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
156
|
+
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
157
|
+
|
158
|
+
try:
|
159
|
+
# time out if we can't connect to the server fast enough
|
160
|
+
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
161
|
+
|
162
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
163
|
+
await stdout_task
|
164
|
+
await stderr_task
|
165
|
+
|
166
|
+
# TODO: this doesn't work right now.
|
167
|
+
# if exit_status != 0:
|
168
|
+
# raise ExecutionError(f"Process exited with status code {exit_status}")
|
169
|
+
|
170
|
+
except (asyncio.TimeoutError, TimeoutError):
|
171
|
+
connecting_status.stop()
|
172
|
+
stdout_task.cancel()
|
173
|
+
stderr_task.cancel()
|
174
|
+
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
175
|
+
|
176
|
+
|
177
|
+
ContainerProcess = synchronize_api(_ContainerProcess)
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import modal.client
|
2
|
+
import modal.io_streams
|
3
|
+
import modal.stream_type
|
4
|
+
import typing
|
5
|
+
import typing_extensions
|
6
|
+
|
7
|
+
T = typing.TypeVar("T")
|
8
|
+
|
9
|
+
class _ContainerProcess(typing.Generic[T]):
|
10
|
+
_process_id: typing.Optional[str]
|
11
|
+
_stdout: modal.io_streams._StreamReader[T]
|
12
|
+
_stderr: modal.io_streams._StreamReader[T]
|
13
|
+
_stdin: modal.io_streams._StreamWriter
|
14
|
+
_text: bool
|
15
|
+
_by_line: bool
|
16
|
+
_returncode: typing.Optional[int]
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
process_id: str,
|
21
|
+
client: modal.client._Client,
|
22
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
23
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
24
|
+
text: bool = True,
|
25
|
+
by_line: bool = False,
|
26
|
+
) -> None: ...
|
27
|
+
@property
|
28
|
+
def stdout(self) -> modal.io_streams._StreamReader[T]: ...
|
29
|
+
@property
|
30
|
+
def stderr(self) -> modal.io_streams._StreamReader[T]: ...
|
31
|
+
@property
|
32
|
+
def stdin(self) -> modal.io_streams._StreamWriter: ...
|
33
|
+
@property
|
34
|
+
def returncode(self) -> int: ...
|
35
|
+
async def poll(self) -> typing.Optional[int]: ...
|
36
|
+
async def wait(self) -> int: ...
|
37
|
+
async def attach(self, *, pty: typing.Optional[bool] = None): ...
|
38
|
+
|
39
|
+
class ContainerProcess(typing.Generic[T]):
|
40
|
+
_process_id: typing.Optional[str]
|
41
|
+
_stdout: modal.io_streams.StreamReader[T]
|
42
|
+
_stderr: modal.io_streams.StreamReader[T]
|
43
|
+
_stdin: modal.io_streams.StreamWriter
|
44
|
+
_text: bool
|
45
|
+
_by_line: bool
|
46
|
+
_returncode: typing.Optional[int]
|
47
|
+
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
process_id: str,
|
51
|
+
client: modal.client.Client,
|
52
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
53
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
54
|
+
text: bool = True,
|
55
|
+
by_line: bool = False,
|
56
|
+
) -> None: ...
|
57
|
+
@property
|
58
|
+
def stdout(self) -> modal.io_streams.StreamReader[T]: ...
|
59
|
+
@property
|
60
|
+
def stderr(self) -> modal.io_streams.StreamReader[T]: ...
|
61
|
+
@property
|
62
|
+
def stdin(self) -> modal.io_streams.StreamWriter: ...
|
63
|
+
@property
|
64
|
+
def returncode(self) -> int: ...
|
65
|
+
|
66
|
+
class __poll_spec(typing_extensions.Protocol):
|
67
|
+
def __call__(self) -> typing.Optional[int]: ...
|
68
|
+
async def aio(self) -> typing.Optional[int]: ...
|
69
|
+
|
70
|
+
poll: __poll_spec
|
71
|
+
|
72
|
+
class __wait_spec(typing_extensions.Protocol):
|
73
|
+
def __call__(self) -> int: ...
|
74
|
+
async def aio(self) -> int: ...
|
75
|
+
|
76
|
+
wait: __wait_spec
|
77
|
+
|
78
|
+
class __attach_spec(typing_extensions.Protocol):
|
79
|
+
def __call__(self, *, pty: typing.Optional[bool] = None): ...
|
80
|
+
async def aio(self, *, pty: typing.Optional[bool] = None): ...
|
81
|
+
|
82
|
+
attach: __attach_spec
|
modal/dict.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
-
from
|
2
|
+
from collections.abc import AsyncIterator
|
3
|
+
from typing import Any, Optional
|
3
4
|
|
5
|
+
from grpclib import GRPCError
|
4
6
|
from synchronicity.async_wrap import asynccontextmanager
|
5
7
|
|
6
8
|
from modal_proto import api_pb2
|
@@ -8,11 +10,13 @@ from modal_proto import api_pb2
|
|
8
10
|
from ._resolver import Resolver
|
9
11
|
from ._serialization import deserialize, serialize
|
10
12
|
from ._utils.async_utils import TaskContext, synchronize_api
|
13
|
+
from ._utils.deprecation import renamed_parameter
|
11
14
|
from ._utils.grpc_utils import retry_transient_errors
|
15
|
+
from ._utils.name_utils import check_object_name
|
12
16
|
from .client import _Client
|
13
17
|
from .config import logger
|
14
|
-
from .exception import
|
15
|
-
from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method
|
18
|
+
from .exception import RequestSizeError
|
19
|
+
from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
|
16
20
|
|
17
21
|
|
18
22
|
def _serialize_dict(data):
|
@@ -22,54 +26,38 @@ def _serialize_dict(data):
|
|
22
26
|
class _Dict(_Object, type_prefix="di"):
|
23
27
|
"""Distributed dictionary for storage in Modal apps.
|
24
28
|
|
25
|
-
Keys and values can be essentially any object, so long as they can be
|
26
|
-
|
29
|
+
Keys and values can be essentially any object, so long as they can be serialized by
|
30
|
+
`cloudpickle`, which includes other Modal objects.
|
27
31
|
|
28
32
|
**Lifetime of a Dict and its items**
|
29
33
|
|
30
34
|
An individual dict entry will expire 30 days after it was last added to its Dict object.
|
31
|
-
|
32
|
-
|
35
|
+
Additionally, data are stored in memory on the Modal server and could be lost due to
|
36
|
+
unexpected server restarts. Because of this, `Dict` is best suited for storing short-term
|
37
|
+
state and is not recommended for durable storage.
|
33
38
|
|
34
39
|
**Usage**
|
35
40
|
|
36
41
|
```python
|
37
|
-
from modal import Dict
|
42
|
+
from modal import Dict
|
38
43
|
|
39
|
-
stub = Stub()
|
40
44
|
my_dict = Dict.from_name("my-persisted_dict", create_if_missing=True)
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
my_dict["some key"] = "some value"
|
45
|
-
my_dict[123] = 456
|
46
|
+
my_dict["some key"] = "some value"
|
47
|
+
my_dict[123] = 456
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
+
assert my_dict["some key"] == "some value"
|
50
|
+
assert my_dict[123] == 456
|
49
51
|
```
|
50
52
|
|
53
|
+
The `Dict` class offers a few methods for operations that are usually accomplished
|
54
|
+
in Python with operators, such as `Dict.put` and `Dict.contains`. The advantage of
|
55
|
+
these methods is that they can be safely called in an asynchronous context, whereas
|
56
|
+
their operator-based analogues will block the event loop.
|
57
|
+
|
51
58
|
For more examples, see the [guide](/docs/guide/dicts-and-queues#modal-dicts).
|
52
59
|
"""
|
53
60
|
|
54
|
-
@staticmethod
|
55
|
-
def new(data: Optional[dict] = None) -> "_Dict":
|
56
|
-
"""`Dict.new` is deprecated.
|
57
|
-
|
58
|
-
Please use `Dict.from_name` (for persisted) or `Dict.ephemeral` (for ephemeral) dicts.
|
59
|
-
"""
|
60
|
-
deprecation_warning((2024, 3, 19), Dict.new.__doc__)
|
61
|
-
|
62
|
-
async def _load(self: _Dict, resolver: Resolver, existing_object_id: Optional[str]):
|
63
|
-
serialized = _serialize_dict(data if data is not None else {})
|
64
|
-
req = api_pb2.DictCreateRequest(
|
65
|
-
app_id=resolver.app_id, data=serialized, existing_dict_id=existing_object_id
|
66
|
-
)
|
67
|
-
response = await resolver.client.stub.DictCreate(req)
|
68
|
-
logger.debug(f"Created dict with id {response.dict_id}")
|
69
|
-
self._hydrate(response.dict_id, resolver.client, None)
|
70
|
-
|
71
|
-
return _Dict._from_loader(_load, "Dict()")
|
72
|
-
|
73
61
|
def __init__(self, data={}):
|
74
62
|
"""mdmd:hidden"""
|
75
63
|
raise RuntimeError(
|
@@ -79,7 +67,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
79
67
|
@classmethod
|
80
68
|
@asynccontextmanager
|
81
69
|
async def ephemeral(
|
82
|
-
cls:
|
70
|
+
cls: type["_Dict"],
|
83
71
|
data: Optional[dict] = None,
|
84
72
|
client: Optional[_Client] = None,
|
85
73
|
environment_name: Optional[str] = None,
|
@@ -89,9 +77,13 @@ class _Dict(_Object, type_prefix="di"):
|
|
89
77
|
|
90
78
|
Usage:
|
91
79
|
```python
|
80
|
+
from modal import Dict
|
81
|
+
|
92
82
|
with Dict.ephemeral() as d:
|
93
83
|
d["foo"] = "bar"
|
84
|
+
```
|
94
85
|
|
86
|
+
```python notest
|
95
87
|
async with Dict.ephemeral() as d:
|
96
88
|
await d.put.aio("foo", "bar")
|
97
89
|
```
|
@@ -104,37 +96,38 @@ class _Dict(_Object, type_prefix="di"):
|
|
104
96
|
environment_name=_get_environment_name(environment_name),
|
105
97
|
data=serialized,
|
106
98
|
)
|
107
|
-
response = await client.stub.DictGetOrCreate
|
99
|
+
response = await retry_transient_errors(client.stub.DictGetOrCreate, request, total_timeout=10.0)
|
108
100
|
async with TaskContext() as tc:
|
109
101
|
request = api_pb2.DictHeartbeatRequest(dict_id=response.dict_id)
|
110
102
|
tc.infinite_loop(lambda: client.stub.DictHeartbeat(request), sleep=_heartbeat_sleep)
|
111
103
|
yield cls._new_hydrated(response.dict_id, client, None, is_another_app=True)
|
112
104
|
|
113
105
|
@staticmethod
|
106
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
114
107
|
def from_name(
|
115
|
-
|
108
|
+
name: str,
|
116
109
|
data: Optional[dict] = None,
|
117
110
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
118
111
|
environment_name: Optional[str] = None,
|
119
112
|
create_if_missing: bool = False,
|
120
113
|
) -> "_Dict":
|
121
|
-
"""
|
122
|
-
|
123
|
-
**Examples**
|
114
|
+
"""Reference a named Dict, creating if necessary.
|
124
115
|
|
125
|
-
|
126
|
-
|
127
|
-
|
116
|
+
In contrast to `modal.Dict.lookup`, this is a lazy method
|
117
|
+
that defers hydrating the local object with metadata from
|
118
|
+
Modal servers until the first time it is actually used.
|
128
119
|
|
129
|
-
|
130
|
-
|
120
|
+
```python
|
121
|
+
d = modal.Dict.from_name("my-dict", create_if_missing=True)
|
122
|
+
d[123] = 456
|
131
123
|
```
|
132
124
|
"""
|
125
|
+
check_object_name(name, "Dict")
|
133
126
|
|
134
127
|
async def _load(self: _Dict, resolver: Resolver, existing_object_id: Optional[str]):
|
135
128
|
serialized = _serialize_dict(data if data is not None else {})
|
136
129
|
req = api_pb2.DictGetOrCreateRequest(
|
137
|
-
deployment_name=
|
130
|
+
deployment_name=name,
|
138
131
|
namespace=namespace,
|
139
132
|
environment_name=_get_environment_name(environment_name, resolver),
|
140
133
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
@@ -144,26 +137,22 @@ class _Dict(_Object, type_prefix="di"):
|
|
144
137
|
logger.debug(f"Created dict with id {response.dict_id}")
|
145
138
|
self._hydrate(response.dict_id, resolver.client, None)
|
146
139
|
|
147
|
-
return _Dict._from_loader(_load, "Dict()", is_another_app=True)
|
148
|
-
|
149
|
-
@staticmethod
|
150
|
-
def persisted(
|
151
|
-
label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None
|
152
|
-
) -> "_Dict":
|
153
|
-
"""Deprecated! Use `Dict.from_name(name, create_if_missing=True)`."""
|
154
|
-
deprecation_warning((2024, 3, 1), _Dict.persisted.__doc__)
|
155
|
-
return _Dict.from_name(label, namespace, environment_name, create_if_missing=True)
|
140
|
+
return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
|
156
141
|
|
157
142
|
@staticmethod
|
143
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
158
144
|
async def lookup(
|
159
|
-
|
145
|
+
name: str,
|
160
146
|
data: Optional[dict] = None,
|
161
147
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
162
148
|
client: Optional[_Client] = None,
|
163
149
|
environment_name: Optional[str] = None,
|
164
150
|
create_if_missing: bool = False,
|
165
151
|
) -> "_Dict":
|
166
|
-
"""Lookup a
|
152
|
+
"""Lookup a named Dict.
|
153
|
+
|
154
|
+
In contrast to `modal.Dict.from_name`, this is an eager method
|
155
|
+
that will hydrate the local object with metadata from Modal servers.
|
167
156
|
|
168
157
|
```python
|
169
158
|
d = modal.Dict.lookup("my-dict")
|
@@ -171,7 +160,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
171
160
|
```
|
172
161
|
"""
|
173
162
|
obj = _Dict.from_name(
|
174
|
-
|
163
|
+
name,
|
175
164
|
data=data,
|
176
165
|
namespace=namespace,
|
177
166
|
environment_name=environment_name,
|
@@ -183,9 +172,21 @@ class _Dict(_Object, type_prefix="di"):
|
|
183
172
|
await resolver.load(obj)
|
184
173
|
return obj
|
185
174
|
|
175
|
+
@staticmethod
|
176
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
177
|
+
async def delete(
|
178
|
+
name: str,
|
179
|
+
*,
|
180
|
+
client: Optional[_Client] = None,
|
181
|
+
environment_name: Optional[str] = None,
|
182
|
+
):
|
183
|
+
obj = await _Dict.lookup(name, client=client, environment_name=environment_name)
|
184
|
+
req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
|
185
|
+
await retry_transient_errors(obj._client.stub.DictDelete, req)
|
186
|
+
|
186
187
|
@live_method
|
187
188
|
async def clear(self) -> None:
|
188
|
-
"""Remove all items from the
|
189
|
+
"""Remove all items from the Dict."""
|
189
190
|
req = api_pb2.DictClearRequest(dict_id=self.object_id)
|
190
191
|
await retry_transient_errors(self._client.stub.DictClear, req)
|
191
192
|
|
@@ -219,7 +220,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
219
220
|
async def __getitem__(self, key: Any) -> Any:
|
220
221
|
"""Get the value associated with a key.
|
221
222
|
|
222
|
-
|
223
|
+
Note: this function will block the event loop when called in an async context.
|
223
224
|
"""
|
224
225
|
NOT_FOUND = object()
|
225
226
|
value = await self.get(key, NOT_FOUND)
|
@@ -233,7 +234,13 @@ class _Dict(_Object, type_prefix="di"):
|
|
233
234
|
"""Update the dictionary with additional items."""
|
234
235
|
serialized = _serialize_dict(kwargs)
|
235
236
|
req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized)
|
236
|
-
|
237
|
+
try:
|
238
|
+
await retry_transient_errors(self._client.stub.DictUpdate, req)
|
239
|
+
except GRPCError as exc:
|
240
|
+
if "status = '413'" in exc.message:
|
241
|
+
raise RequestSizeError("Dict.update request is too large") from exc
|
242
|
+
else:
|
243
|
+
raise exc
|
237
244
|
|
238
245
|
@live_method
|
239
246
|
async def put(self, key: Any, value: Any) -> None:
|
@@ -241,13 +248,19 @@ class _Dict(_Object, type_prefix="di"):
|
|
241
248
|
updates = {key: value}
|
242
249
|
serialized = _serialize_dict(updates)
|
243
250
|
req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized)
|
244
|
-
|
251
|
+
try:
|
252
|
+
await retry_transient_errors(self._client.stub.DictUpdate, req)
|
253
|
+
except GRPCError as exc:
|
254
|
+
if "status = '413'" in exc.message:
|
255
|
+
raise RequestSizeError("Dict.put request is too large") from exc
|
256
|
+
else:
|
257
|
+
raise exc
|
245
258
|
|
246
259
|
@live_method
|
247
260
|
async def __setitem__(self, key: Any, value: Any) -> None:
|
248
261
|
"""Set a specific key-value pair to the dictionary.
|
249
262
|
|
250
|
-
|
263
|
+
Note: this function will block the event loop when called in an async context.
|
251
264
|
"""
|
252
265
|
return await self.put(key, value)
|
253
266
|
|
@@ -264,7 +277,7 @@ class _Dict(_Object, type_prefix="di"):
|
|
264
277
|
async def __delitem__(self, key: Any) -> Any:
|
265
278
|
"""Delete a key from the dictionary.
|
266
279
|
|
267
|
-
|
280
|
+
Note: this function will block the event loop when called in an async context.
|
268
281
|
"""
|
269
282
|
return await self.pop(key)
|
270
283
|
|
@@ -272,9 +285,42 @@ class _Dict(_Object, type_prefix="di"):
|
|
272
285
|
async def __contains__(self, key: Any) -> bool:
|
273
286
|
"""Return if a key is present.
|
274
287
|
|
275
|
-
|
288
|
+
Note: this function will block the event loop when called in an async context.
|
276
289
|
"""
|
277
290
|
return await self.contains(key)
|
278
291
|
|
292
|
+
@live_method_gen
|
293
|
+
async def keys(self) -> AsyncIterator[Any]:
|
294
|
+
"""Return an iterator over the keys in this dictionary.
|
295
|
+
|
296
|
+
Note that (unlike with Python dicts) the return value is a simple iterator,
|
297
|
+
and results are unordered.
|
298
|
+
"""
|
299
|
+
req = api_pb2.DictContentsRequest(dict_id=self.object_id, keys=True)
|
300
|
+
async for resp in self._client.stub.DictContents.unary_stream(req):
|
301
|
+
yield deserialize(resp.key, self._client)
|
302
|
+
|
303
|
+
@live_method_gen
|
304
|
+
async def values(self) -> AsyncIterator[Any]:
|
305
|
+
"""Return an iterator over the values in this dictionary.
|
306
|
+
|
307
|
+
Note that (unlike with Python dicts) the return value is a simple iterator,
|
308
|
+
and results are unordered.
|
309
|
+
"""
|
310
|
+
req = api_pb2.DictContentsRequest(dict_id=self.object_id, values=True)
|
311
|
+
async for resp in self._client.stub.DictContents.unary_stream(req):
|
312
|
+
yield deserialize(resp.value, self._client)
|
313
|
+
|
314
|
+
@live_method_gen
|
315
|
+
async def items(self) -> AsyncIterator[tuple[Any, Any]]:
|
316
|
+
"""Return an iterator over the (key, value) tuples in this dictionary.
|
317
|
+
|
318
|
+
Note that (unlike with Python dicts) the return value is a simple iterator,
|
319
|
+
and results are unordered.
|
320
|
+
"""
|
321
|
+
req = api_pb2.DictContentsRequest(dict_id=self.object_id, keys=True, values=True)
|
322
|
+
async for resp in self._client.stub.DictContents.unary_stream(req):
|
323
|
+
yield (deserialize(resp.key, self._client), deserialize(resp.value, self._client))
|
324
|
+
|
279
325
|
|
280
326
|
Dict = synchronize_api(_Dict)
|