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.
Files changed (220) hide show
  1. modal/__init__.py +17 -13
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +420 -937
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +5 -7
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {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 typing import Any, AsyncIterator, Optional, Type
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 deprecation_warning
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
- serialized by `cloudpickle`, including Modal objects.
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
- Because of this, `Dict`s are best not used for
32
- long-term storage. All data is deleted when the app is stopped.
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, Stub
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
- @stub.local_entrypoint()
43
- def main():
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
- assert my_dict["some key"] == "some value"
48
- assert my_dict[123] == 456
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: Type["_Dict"],
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(request)
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
- label: str,
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
- """Create a reference to a persisted Dict
122
-
123
- **Examples**
114
+ """Reference a named Dict, creating if necessary.
124
115
 
125
- ```python notest
126
- # In one app:
127
- stub.dict = Dict.persisted("my-dict")
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
- # Later, in another app or Python file:
130
- stub.dict = Dict.from_name("my-dict")
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=label,
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
- label: str,
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 dict with a given name and tag.
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
- label,
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 modal.Dict."""
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
- This function only works in a synchronous context.
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
- await retry_transient_errors(self._client.stub.DictUpdate, req)
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
- await retry_transient_errors(self._client.stub.DictUpdate, req)
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
- This function only works in a synchronous context.
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
- This function only works in a synchronous context.
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
- This function only works in a synchronous context.
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)