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.

Files changed (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/dict.py CHANGED
@@ -1,28 +1,227 @@
1
1
  # Copyright Modal Labs 2022
2
2
  from collections.abc import AsyncIterator, Mapping
3
- from typing import Any, Optional
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Any, Optional, Union
4
6
 
5
- from grpclib import GRPCError
7
+ from google.protobuf.message import Message
8
+ from grpclib import GRPCError, Status
9
+ from synchronicity import classproperty
6
10
  from synchronicity.async_wrap import asynccontextmanager
7
11
 
12
+ from modal._utils.grpc_utils import Retry
8
13
  from modal_proto import api_pb2
9
14
 
10
- from ._object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
15
+ from ._load_context import LoadContext
16
+ from ._object import (
17
+ EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
18
+ _get_environment_name,
19
+ _Object,
20
+ live_method,
21
+ live_method_gen,
22
+ )
11
23
  from ._resolver import Resolver
12
24
  from ._serialization import deserialize, serialize
13
25
  from ._utils.async_utils import TaskContext, synchronize_api
14
26
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
15
- from ._utils.grpc_utils import retry_transient_errors
16
27
  from ._utils.name_utils import check_object_name
28
+ from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
17
29
  from .client import _Client
18
30
  from .config import logger
19
- from .exception import RequestSizeError
31
+ from .exception import AlreadyExistsError, InvalidError, NotFoundError, RequestSizeError
32
+
33
+
34
+ class _NoDefaultSentinel:
35
+ def __repr__(self) -> str:
36
+ return "..."
37
+
38
+
39
+ _NO_DEFAULT = _NoDefaultSentinel()
20
40
 
21
41
 
22
42
  def _serialize_dict(data):
23
43
  return [api_pb2.DictEntry(key=serialize(k), value=serialize(v)) for k, v in data.items()]
24
44
 
25
45
 
46
+ @dataclass
47
+ class DictInfo:
48
+ """Information about a Dict object."""
49
+
50
+ # This dataclass should be limited to information that is unchanging over the lifetime of the Dict,
51
+ # since it is transmitted from the server when the object is hydrated and could be stale when accessed.
52
+
53
+ name: Optional[str]
54
+ created_at: datetime
55
+ created_by: Optional[str]
56
+
57
+
58
+ class _DictManager:
59
+ """Namespace with methods for managing named Dict objects."""
60
+
61
+ @staticmethod
62
+ async def create(
63
+ name: str, # Name to use for the new Dict
64
+ *,
65
+ allow_existing: bool = False, # If True, no-op when the Dict already exists
66
+ environment_name: Optional[str] = None, # Uses active environment if not specified
67
+ client: Optional[_Client] = None, # Optional client with Modal credentials
68
+ ) -> None:
69
+ """Create a new Dict object.
70
+
71
+ **Examples:**
72
+
73
+ ```python notest
74
+ modal.Dict.objects.create("my-dict")
75
+ ```
76
+
77
+ Dicts will be created in the active environment, or another one can be specified:
78
+
79
+ ```python notest
80
+ modal.Dict.objects.create("my-dict", environment_name="dev")
81
+ ```
82
+
83
+ By default, an error will be raised if the Dict already exists, but passing
84
+ `allow_existing=True` will make the creation attempt a no-op in this case.
85
+
86
+ ```python notest
87
+ modal.Dict.objects.create("my-dict", allow_existing=True)
88
+ ```
89
+
90
+ Note that this method does not return a local instance of the Dict. You can use
91
+ `modal.Dict.from_name` to perform a lookup after creation.
92
+
93
+ Added in v1.1.2.
94
+
95
+ """
96
+ check_object_name(name, "Dict")
97
+ client = await _Client.from_env() if client is None else client
98
+ object_creation_type = (
99
+ api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
100
+ if allow_existing
101
+ else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
102
+ )
103
+ req = api_pb2.DictGetOrCreateRequest(
104
+ deployment_name=name,
105
+ environment_name=_get_environment_name(environment_name),
106
+ object_creation_type=object_creation_type,
107
+ )
108
+ try:
109
+ await client.stub.DictGetOrCreate(req)
110
+ except GRPCError as exc:
111
+ if exc.status == Status.ALREADY_EXISTS and not allow_existing:
112
+ raise AlreadyExistsError(exc.message)
113
+ else:
114
+ raise
115
+
116
+ @staticmethod
117
+ async def list(
118
+ *,
119
+ max_objects: Optional[int] = None, # Limit results to this size
120
+ created_before: Optional[Union[datetime, str]] = None, # Limit based on creation date
121
+ environment_name: str = "", # Uses active environment if not specified
122
+ client: Optional[_Client] = None, # Optional client with Modal credentials
123
+ ) -> list["_Dict"]:
124
+ """Return a list of hydrated Dict objects.
125
+
126
+ **Examples:**
127
+
128
+ ```python
129
+ dicts = modal.Dict.objects.list()
130
+ print([d.name for d in dicts])
131
+ ```
132
+
133
+ Dicts will be retreived from the active environment, or another one can be specified:
134
+
135
+ ```python notest
136
+ dev_dicts = modal.Dict.objects.list(environment_name="dev")
137
+ ```
138
+
139
+ By default, all named Dict are returned, newest to oldest. It's also possible to limit the
140
+ number of results and to filter by creation date:
141
+
142
+ ```python
143
+ dicts = modal.Dict.objects.list(max_objects=10, created_before="2025-01-01")
144
+ ```
145
+
146
+ Added in v1.1.2.
147
+
148
+ """
149
+ client = await _Client.from_env() if client is None else client
150
+ if max_objects is not None and max_objects < 0:
151
+ raise InvalidError("max_objects cannot be negative")
152
+
153
+ items: list[api_pb2.DictListResponse.DictInfo] = []
154
+
155
+ async def retrieve_page(created_before: float) -> bool:
156
+ max_page_size = 100 if max_objects is None else min(100, max_objects - len(items))
157
+ pagination = api_pb2.ListPagination(max_objects=max_page_size, created_before=created_before)
158
+ req = api_pb2.DictListRequest(
159
+ environment_name=_get_environment_name(environment_name), pagination=pagination
160
+ )
161
+ resp = await client.stub.DictList(req)
162
+ items.extend(resp.dicts)
163
+ finished = (len(resp.dicts) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
164
+ return finished
165
+
166
+ finished = await retrieve_page(as_timestamp(created_before))
167
+ while True:
168
+ if finished:
169
+ break
170
+ finished = await retrieve_page(items[-1].metadata.creation_info.created_at)
171
+
172
+ dicts = [
173
+ _Dict._new_hydrated(
174
+ item.dict_id,
175
+ client,
176
+ item.metadata,
177
+ is_another_app=True,
178
+ rep=_Dict._repr(item.name, environment_name),
179
+ )
180
+ for item in items
181
+ ]
182
+ return dicts[:max_objects] if max_objects is not None else dicts
183
+
184
+ @staticmethod
185
+ async def delete(
186
+ name: str, # Name of the Dict to delete
187
+ *,
188
+ allow_missing: bool = False, # If True, don't raise an error if the Dict doesn't exist
189
+ environment_name: Optional[str] = None, # Uses active environment if not specified
190
+ client: Optional[_Client] = None, # Optional client with Modal credentials
191
+ ):
192
+ """Delete a named Dict.
193
+
194
+ Warning: This deletes an *entire Dict*, not just a specific key.
195
+ Deletion is irreversible and will affect any Apps currently using the Dict.
196
+
197
+ **Examples:**
198
+
199
+ ```python notest
200
+ await modal.Dict.objects.delete("my-dict")
201
+ ```
202
+
203
+ Dicts will be deleted from the active environment, or another one can be specified:
204
+
205
+ ```python notest
206
+ await modal.Dict.objects.delete("my-dict", environment_name="dev")
207
+ ```
208
+
209
+ Added in v1.1.2.
210
+
211
+ """
212
+ try:
213
+ obj = await _Dict.from_name(name, environment_name=environment_name).hydrate(client)
214
+ except NotFoundError:
215
+ if not allow_missing:
216
+ raise
217
+ else:
218
+ req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
219
+ await obj._client.stub.DictDelete(req)
220
+
221
+
222
+ DictManager = synchronize_api(_DictManager)
223
+
224
+
26
225
  class _Dict(_Object, type_prefix="di"):
27
226
  """Distributed dictionary for storage in Modal apps.
28
227
 
@@ -65,12 +264,33 @@ class _Dict(_Object, type_prefix="di"):
65
264
  For more examples, see the [guide](https://modal.com/docs/guide/dicts-and-queues#modal-dicts).
66
265
  """
67
266
 
267
+ _name: Optional[str] = None
268
+ _metadata: Optional[api_pb2.DictMetadata] = None
269
+
68
270
  def __init__(self, data={}):
69
271
  """mdmd:hidden"""
70
272
  raise RuntimeError(
71
273
  "`Dict(...)` constructor is not allowed. Please use `Dict.from_name` or `Dict.ephemeral` instead"
72
274
  )
73
275
 
276
+ @classproperty
277
+ def objects(cls) -> _DictManager:
278
+ return _DictManager
279
+
280
+ @property
281
+ def name(self) -> Optional[str]:
282
+ return self._name
283
+
284
+ def _hydrate_metadata(self, metadata: Optional[Message]):
285
+ if metadata:
286
+ assert isinstance(metadata, api_pb2.DictMetadata)
287
+ self._metadata = metadata
288
+ self._name = metadata.name
289
+
290
+ def _get_metadata(self) -> api_pb2.DictMetadata:
291
+ assert self._metadata
292
+ return self._metadata
293
+
74
294
  @classmethod
75
295
  @asynccontextmanager
76
296
  async def ephemeral(
@@ -78,7 +298,7 @@ class _Dict(_Object, type_prefix="di"):
78
298
  data: Optional[dict] = None, # DEPRECATED
79
299
  client: Optional[_Client] = None,
80
300
  environment_name: Optional[str] = None,
81
- _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
301
+ _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, # mdmd:line-hidden
82
302
  ) -> AsyncIterator["_Dict"]:
83
303
  """Creates a new ephemeral Dict within a context manager:
84
304
 
@@ -108,11 +328,17 @@ class _Dict(_Object, type_prefix="di"):
108
328
  environment_name=_get_environment_name(environment_name),
109
329
  data=serialized,
110
330
  )
111
- response = await retry_transient_errors(client.stub.DictGetOrCreate, request, total_timeout=10.0)
331
+ response = await client.stub.DictGetOrCreate(request, retry=Retry(total_timeout=10.0))
112
332
  async with TaskContext() as tc:
113
333
  request = api_pb2.DictHeartbeatRequest(dict_id=response.dict_id)
114
334
  tc.infinite_loop(lambda: client.stub.DictHeartbeat(request), sleep=_heartbeat_sleep)
115
- yield cls._new_hydrated(response.dict_id, client, None, is_another_app=True)
335
+ yield cls._new_hydrated(
336
+ response.dict_id,
337
+ client,
338
+ response.metadata,
339
+ is_another_app=True,
340
+ rep="modal.Dict.ephemeral()",
341
+ )
116
342
 
117
343
  @staticmethod
118
344
  def from_name(
@@ -122,6 +348,7 @@ class _Dict(_Object, type_prefix="di"):
122
348
  namespace=None, # mdmd:line-hidden
123
349
  environment_name: Optional[str] = None,
124
350
  create_if_missing: bool = False,
351
+ client: Optional[_Client] = None,
125
352
  ) -> "_Dict":
126
353
  """Reference a named Dict, creating if necessary.
127
354
 
@@ -143,77 +370,64 @@ class _Dict(_Object, type_prefix="di"):
143
370
  "Passing data to `modal.Dict.from_name` is deprecated and will stop working in a future release.",
144
371
  )
145
372
 
146
- async def _load(self: _Dict, resolver: Resolver, existing_object_id: Optional[str]):
373
+ async def _load(self: _Dict, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
147
374
  serialized = _serialize_dict(data if data is not None else {})
148
375
  req = api_pb2.DictGetOrCreateRequest(
149
376
  deployment_name=name,
150
- environment_name=_get_environment_name(environment_name, resolver),
377
+ environment_name=load_context.environment_name,
151
378
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
152
379
  data=serialized,
153
380
  )
154
- response = await resolver.client.stub.DictGetOrCreate(req)
381
+ response = await load_context.client.stub.DictGetOrCreate(req)
155
382
  logger.debug(f"Created dict with id {response.dict_id}")
156
- self._hydrate(response.dict_id, resolver.client, None)
157
-
158
- return _Dict._from_loader(_load, "Dict()", is_another_app=True, hydrate_lazily=True)
383
+ self._hydrate(response.dict_id, load_context.client, response.metadata)
384
+
385
+ rep = _Dict._repr(name, environment_name)
386
+ return _Dict._from_loader(
387
+ _load,
388
+ rep,
389
+ is_another_app=True,
390
+ hydrate_lazily=True,
391
+ name=name,
392
+ load_context_overrides=LoadContext(environment_name=environment_name, client=client),
393
+ )
159
394
 
160
395
  @staticmethod
161
- async def lookup(
396
+ async def delete(
162
397
  name: str,
163
- data: Optional[dict] = None,
164
- namespace=None, # mdmd:line-hidden
398
+ *,
165
399
  client: Optional[_Client] = None,
166
400
  environment_name: Optional[str] = None,
167
- create_if_missing: bool = False,
168
- ) -> "_Dict":
401
+ ):
169
402
  """mdmd:hidden
170
- Lookup a named Dict.
171
-
172
- DEPRECATED: This method is deprecated in favor of `modal.Dict.from_name`.
403
+ Delete a named Dict object.
173
404
 
174
- In contrast to `modal.Dict.from_name`, this is an eager method
175
- that will hydrate the local object with metadata from Modal servers.
405
+ Warning: This deletes an *entire Dict*, not just a specific key.
406
+ Deletion is irreversible and will affect any Apps currently using the Dict.
176
407
 
177
- ```python
178
- d = modal.Dict.from_name("my-dict")
179
- d["xyz"] = 123
180
- ```
408
+ DEPRECATED: This method is deprecated; we recommend using `modal.Dict.objects.delete` instead.
181
409
  """
182
410
  deprecation_warning(
183
- (2025, 1, 27),
184
- "`modal.Dict.lookup` is deprecated and will be removed in a future release."
185
- " It can be replaced with `modal.Dict.from_name`."
186
- "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
187
- )
188
- warn_if_passing_namespace(namespace, "modal.Dict.lookup")
189
- obj = _Dict.from_name(
190
- name,
191
- data=data,
192
- environment_name=environment_name,
193
- create_if_missing=create_if_missing,
411
+ (2025, 8, 6), "`modal.Dict.delete` is deprecated; we recommend using `modal.Dict.objects.delete` instead."
194
412
  )
195
- if client is None:
196
- client = await _Client.from_env()
197
- resolver = Resolver(client=client)
198
- await resolver.load(obj)
199
- return obj
413
+ await _Dict.objects.delete(name, environment_name=environment_name, client=client)
200
414
 
201
- @staticmethod
202
- async def delete(
203
- name: str,
204
- *,
205
- client: Optional[_Client] = None,
206
- environment_name: Optional[str] = None,
207
- ):
208
- obj = await _Dict.from_name(name, environment_name=environment_name).hydrate(client)
209
- req = api_pb2.DictDeleteRequest(dict_id=obj.object_id)
210
- await retry_transient_errors(obj._client.stub.DictDelete, req)
415
+ @live_method
416
+ async def info(self) -> DictInfo:
417
+ """Return information about the Dict object."""
418
+ metadata = self._get_metadata()
419
+ creation_info = metadata.creation_info
420
+ return DictInfo(
421
+ name=metadata.name or None,
422
+ created_at=timestamp_to_localized_dt(creation_info.created_at),
423
+ created_by=creation_info.created_by or None,
424
+ )
211
425
 
212
426
  @live_method
213
427
  async def clear(self) -> None:
214
428
  """Remove all items from the Dict."""
215
429
  req = api_pb2.DictClearRequest(dict_id=self.object_id)
216
- await retry_transient_errors(self._client.stub.DictClear, req)
430
+ await self._client.stub.DictClear(req)
217
431
 
218
432
  @live_method
219
433
  async def get(self, key: Any, default: Optional[Any] = None) -> Any:
@@ -222,7 +436,7 @@ class _Dict(_Object, type_prefix="di"):
222
436
  Returns `default` if key does not exist.
223
437
  """
224
438
  req = api_pb2.DictGetRequest(dict_id=self.object_id, key=serialize(key))
225
- resp = await retry_transient_errors(self._client.stub.DictGet, req)
439
+ resp = await self._client.stub.DictGet(req)
226
440
  if not resp.found:
227
441
  return default
228
442
  return deserialize(resp.value, self._client)
@@ -231,7 +445,7 @@ class _Dict(_Object, type_prefix="di"):
231
445
  async def contains(self, key: Any) -> bool:
232
446
  """Return if a key is present."""
233
447
  req = api_pb2.DictContainsRequest(dict_id=self.object_id, key=serialize(key))
234
- resp = await retry_transient_errors(self._client.stub.DictContains, req)
448
+ resp = await self._client.stub.DictContains(req)
235
449
  return resp.found
236
450
 
237
451
  @live_method
@@ -241,7 +455,7 @@ class _Dict(_Object, type_prefix="di"):
241
455
  Note: This is an expensive operation and will return at most 100,000.
242
456
  """
243
457
  req = api_pb2.DictLenRequest(dict_id=self.object_id)
244
- resp = await retry_transient_errors(self._client.stub.DictLen, req)
458
+ resp = await self._client.stub.DictLen(req)
245
459
  return resp.len
246
460
 
247
461
  @live_method
@@ -270,7 +484,7 @@ class _Dict(_Object, type_prefix="di"):
270
484
  serialized = _serialize_dict(contents)
271
485
  req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized)
272
486
  try:
273
- await retry_transient_errors(self._client.stub.DictUpdate, req)
487
+ await self._client.stub.DictUpdate(req)
274
488
  except GRPCError as exc:
275
489
  if "status = '413'" in exc.message:
276
490
  raise RequestSizeError("Dict.update request is too large") from exc
@@ -288,7 +502,7 @@ class _Dict(_Object, type_prefix="di"):
288
502
  serialized = _serialize_dict(updates)
289
503
  req = api_pb2.DictUpdateRequest(dict_id=self.object_id, updates=serialized, if_not_exists=skip_if_exists)
290
504
  try:
291
- resp = await retry_transient_errors(self._client.stub.DictUpdate, req)
505
+ resp = await self._client.stub.DictUpdate(req)
292
506
  return resp.created
293
507
  except GRPCError as exc:
294
508
  if "status = '413'" in exc.message:
@@ -305,11 +519,16 @@ class _Dict(_Object, type_prefix="di"):
305
519
  return await self.put(key, value)
306
520
 
307
521
  @live_method
308
- async def pop(self, key: Any) -> Any:
309
- """Remove a key from the Dict, returning the value if it exists."""
522
+ async def pop(self, key: Any, default: Any = _NO_DEFAULT) -> Any:
523
+ """Remove a key from the Dict, returning the value if it exists.
524
+
525
+ If key is not found, return default if provided, otherwise raise KeyError.
526
+ """
310
527
  req = api_pb2.DictPopRequest(dict_id=self.object_id, key=serialize(key))
311
- resp = await retry_transient_errors(self._client.stub.DictPop, req)
528
+ resp = await self._client.stub.DictPop(req)
312
529
  if not resp.found:
530
+ if default is not _NO_DEFAULT:
531
+ return default
313
532
  raise KeyError(f"{key} not in dict {self.object_id}")
314
533
  return deserialize(resp.value, self._client)
315
534