modal 0.62.115__py3-none-any.whl → 0.72.13__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 +13 -9
  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 +402 -398
  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 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  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 +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  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 +3 -3
  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 +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  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 +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/queue.py CHANGED
@@ -2,7 +2,8 @@
2
2
  import queue # The system library
3
3
  import time
4
4
  import warnings
5
- from typing import Any, AsyncGenerator, AsyncIterator, List, Optional, Type
5
+ from collections.abc import AsyncGenerator, AsyncIterator
6
+ from typing import Any, Optional
6
7
 
7
8
  from grpclib import GRPCError, Status
8
9
  from synchronicity.async_wrap import asynccontextmanager
@@ -12,9 +13,11 @@ from modal_proto import api_pb2
12
13
  from ._resolver import Resolver
13
14
  from ._serialization import deserialize, serialize
14
15
  from ._utils.async_utils import TaskContext, synchronize_api, warn_if_generator_is_not_consumed
16
+ from ._utils.deprecation import renamed_parameter
15
17
  from ._utils.grpc_utils import retry_transient_errors
18
+ from ._utils.name_utils import check_object_name
16
19
  from .client import _Client
17
- from .exception import InvalidError, deprecation_warning
20
+ from .exception import InvalidError, RequestSizeError
18
21
  from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
19
22
 
20
23
 
@@ -25,35 +28,12 @@ class _Queue(_Object, type_prefix="qu"):
25
28
 
26
29
  By default, the `Queue` object acts as a single FIFO queue which supports puts and gets (blocking and non-blocking).
27
30
 
28
- **Queue partitions (beta)**
29
-
30
- Specifying partition keys gives access to other independent FIFO partitions within the same `Queue` object.
31
- Across any two partitions, puts and gets are completely independent. For example, a put in one partition does not affect
32
- a get in any other partition.
33
-
34
- When no partition key is specified (by default), puts and gets will operate on a default partition. This default partition
35
- is also isolated from all other partitions. Please see the Usage section below for an example using partitions.
36
-
37
- **Lifetime of a queue and its partitions**
38
-
39
- By default, each partition is cleared 24 hours after the last `put` operation. A lower TTL can be specified by the `partition_ttl`
40
- argument in the `put` or `put_many` methods. Each partition's expiry is handled independently.
41
-
42
- As such, `Queue`s are best used for communication between active functions and not relied on for persistent storage.
43
-
44
- On app completion or after stopping an app any associated `Queue` objects are cleaned up. All its partitions will be cleared.
45
-
46
- **Limits**
47
-
48
- A single `Queue` can contain up to 100,000 partitions, each with up to 5,000 items. Each item can be up to 256 KiB.
49
-
50
- Partition keys must be non-empty and must not exceed 64 bytes.
51
-
52
31
  **Usage**
53
32
 
54
33
  ```python
55
34
  from modal import Queue
56
35
 
36
+ # Create an ephemeral queue which is anonymous and garbage collected
57
37
  with Queue.ephemeral() as my_queue:
58
38
  # Putting values
59
39
  my_queue.put("some value")
@@ -77,25 +57,42 @@ class _Queue(_Object, type_prefix="qu"):
77
57
  # (beta feature) Iterate through items in place (read immutably)
78
58
  my_queue.put(1)
79
59
  assert [v for v in my_queue.iterate()] == [0, 1]
60
+
61
+ # You can also create persistent queues that can be used across apps
62
+ queue = Queue.from_name("my-persisted-queue", create_if_missing=True)
63
+ queue.put(42)
64
+ assert queue.get() == 42
80
65
  ```
81
66
 
82
67
  For more examples, see the [guide](/docs/guide/dicts-and-queues#modal-queues).
83
- """
84
68
 
85
- @staticmethod
86
- def new():
87
- """`Queue.new` is deprecated.
69
+ **Queue partitions (beta)**
88
70
 
89
- Please use `Queue.from_name` (for persisted) or `Queue.ephemeral` (for ephemeral) queues.
90
- """
91
- deprecation_warning((2024, 3, 19), Queue.new.__doc__)
71
+ Specifying partition keys gives access to other independent FIFO partitions within the same `Queue` object.
72
+ Across any two partitions, puts and gets are completely independent.
73
+ For example, a put in one partition does not affect a get in any other partition.
92
74
 
93
- async def _load(self: _Queue, resolver: Resolver, existing_object_id: Optional[str]):
94
- request = api_pb2.QueueCreateRequest(app_id=resolver.app_id, existing_queue_id=existing_object_id)
95
- response = await resolver.client.stub.QueueCreate(request)
96
- self._hydrate(response.queue_id, resolver.client, None)
75
+ When no partition key is specified (by default), puts and gets will operate on a default partition.
76
+ This default partition is also isolated from all other partitions.
77
+ Please see the Usage section below for an example using partitions.
78
+
79
+ **Lifetime of a queue and its partitions**
97
80
 
98
- return _Queue._from_loader(_load, "Queue()")
81
+ By default, each partition is cleared 24 hours after the last `put` operation.
82
+ A lower TTL can be specified by the `partition_ttl` argument in the `put` or `put_many` methods.
83
+ Each partition's expiry is handled independently.
84
+
85
+ As such, `Queue`s are best used for communication between active functions and not relied on for persistent storage.
86
+
87
+ On app completion or after stopping an app any associated `Queue` objects are cleaned up.
88
+ All its partitions will be cleared.
89
+
90
+ **Limits**
91
+
92
+ A single `Queue` can contain up to 100,000 partitions, each with up to 5,000 items. Each item can be up to 256 KiB.
93
+
94
+ Partition keys must be non-empty and must not exceed 64 bytes.
95
+ """
99
96
 
100
97
  def __init__(self):
101
98
  """mdmd:hidden"""
@@ -115,7 +112,7 @@ class _Queue(_Object, type_prefix="qu"):
115
112
  @classmethod
116
113
  @asynccontextmanager
117
114
  async def ephemeral(
118
- cls: Type["_Queue"],
115
+ cls: type["_Queue"],
119
116
  client: Optional[_Client] = None,
120
117
  environment_name: Optional[str] = None,
121
118
  _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
@@ -128,7 +125,9 @@ class _Queue(_Object, type_prefix="qu"):
128
125
 
129
126
  with Queue.ephemeral() as q:
130
127
  q.put(123)
128
+ ```
131
129
 
130
+ ```python notest
132
131
  async with Queue.ephemeral() as q:
133
132
  await q.put.aio(123)
134
133
  ```
@@ -146,27 +145,29 @@ class _Queue(_Object, type_prefix="qu"):
146
145
  yield cls._new_hydrated(response.queue_id, client, None, is_another_app=True)
147
146
 
148
147
  @staticmethod
148
+ @renamed_parameter((2024, 12, 18), "label", "name")
149
149
  def from_name(
150
- label: str,
150
+ name: str,
151
151
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
152
152
  environment_name: Optional[str] = None,
153
153
  create_if_missing: bool = False,
154
154
  ) -> "_Queue":
155
- """Create a reference to a persisted Queue
155
+ """Reference a named Queue, creating if necessary.
156
156
 
157
- **Examples**
157
+ In contrast to `modal.Queue.lookup`, this is a lazy method
158
+ the defers hydrating the local object with metadata from
159
+ Modal servers until the first time it is actually used.
158
160
 
159
161
  ```python
160
- from modal import Queue
161
-
162
- queue = Queue.from_name("my-queue", create_if_missing=True)
163
- queue.put(123)
162
+ q = modal.Queue.from_name("my-queue", create_if_missing=True)
163
+ q.put(123)
164
164
  ```
165
165
  """
166
+ check_object_name(name, "Queue")
166
167
 
167
168
  async def _load(self: _Queue, resolver: Resolver, existing_object_id: Optional[str]):
168
169
  req = api_pb2.QueueGetOrCreateRequest(
169
- deployment_name=label,
170
+ deployment_name=name,
170
171
  namespace=namespace,
171
172
  environment_name=_get_environment_name(environment_name, resolver),
172
173
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
@@ -177,32 +178,26 @@ class _Queue(_Object, type_prefix="qu"):
177
178
  return _Queue._from_loader(_load, "Queue()", is_another_app=True, hydrate_lazily=True)
178
179
 
179
180
  @staticmethod
180
- def persisted(
181
- label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None
182
- ) -> "_Queue":
183
- """Deprecated! Use `Queue.from_name(name, create_if_missing=True)`."""
184
- deprecation_warning((2024, 3, 1), _Queue.persisted.__doc__)
185
- return _Queue.from_name(label, namespace, environment_name, create_if_missing=True)
186
-
187
- @staticmethod
181
+ @renamed_parameter((2024, 12, 18), "label", "name")
188
182
  async def lookup(
189
- label: str,
183
+ name: str,
190
184
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
191
185
  client: Optional[_Client] = None,
192
186
  environment_name: Optional[str] = None,
193
187
  create_if_missing: bool = False,
194
188
  ) -> "_Queue":
195
- """Lookup a queue with a given name and tag.
189
+ """Lookup a named Queue.
196
190
 
197
- ```python
198
- from modal import Queue
191
+ In contrast to `modal.Queue.from_name`, this is an eager method
192
+ that will hydrate the local object with metadata from Modal servers.
199
193
 
194
+ ```python
200
195
  q = modal.Queue.lookup("my-queue")
201
196
  q.put(123)
202
197
  ```
203
198
  """
204
199
  obj = _Queue.from_name(
205
- label, namespace=namespace, environment_name=environment_name, create_if_missing=create_if_missing
200
+ name, namespace=namespace, environment_name=environment_name, create_if_missing=create_if_missing
206
201
  )
207
202
  if client is None:
208
203
  client = await _Client.from_env()
@@ -211,12 +206,13 @@ class _Queue(_Object, type_prefix="qu"):
211
206
  return obj
212
207
 
213
208
  @staticmethod
214
- async def delete(label: str, *, client: Optional[_Client] = None, environment_name: Optional[str] = None):
215
- obj = await _Queue.lookup(label, client=client, environment_name=environment_name)
209
+ @renamed_parameter((2024, 12, 18), "label", "name")
210
+ async def delete(name: str, *, client: Optional[_Client] = None, environment_name: Optional[str] = None):
211
+ obj = await _Queue.lookup(name, client=client, environment_name=environment_name)
216
212
  req = api_pb2.QueueDeleteRequest(queue_id=obj.object_id)
217
213
  await retry_transient_errors(obj._client.stub.QueueDelete, req)
218
214
 
219
- async def _get_nonblocking(self, partition: Optional[str], n_values: int) -> List[Any]:
215
+ async def _get_nonblocking(self, partition: Optional[str], n_values: int) -> list[Any]:
220
216
  request = api_pb2.QueueGetRequest(
221
217
  queue_id=self.object_id,
222
218
  partition_key=self.validate_partition_key(partition),
@@ -230,7 +226,7 @@ class _Queue(_Object, type_prefix="qu"):
230
226
  else:
231
227
  return []
232
228
 
233
- async def _get_blocking(self, partition: Optional[str], timeout: Optional[float], n_values: int) -> List[Any]:
229
+ async def _get_blocking(self, partition: Optional[str], timeout: Optional[float], n_values: int) -> list[Any]:
234
230
  if timeout is not None:
235
231
  deadline = time.time() + timeout
236
232
  else:
@@ -259,6 +255,18 @@ class _Queue(_Object, type_prefix="qu"):
259
255
 
260
256
  raise queue.Empty()
261
257
 
258
+ @live_method
259
+ async def clear(self, *, partition: Optional[str] = None, all: bool = False) -> None:
260
+ """Clear the contents of a single partition or all partitions."""
261
+ if partition and all:
262
+ raise InvalidError("Partition must be null when requesting to clear all.")
263
+ request = api_pb2.QueueClearRequest(
264
+ queue_id=self.object_id,
265
+ partition_key=self.validate_partition_key(partition),
266
+ all_partitions=all,
267
+ )
268
+ await retry_transient_errors(self._client.stub.QueueClear, request)
269
+
262
270
  @live_method
263
271
  async def get(
264
272
  self, block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
@@ -288,7 +296,7 @@ class _Queue(_Object, type_prefix="qu"):
288
296
  @live_method
289
297
  async def get_many(
290
298
  self, n_values: int, block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
291
- ) -> List[Any]:
299
+ ) -> list[Any]:
292
300
  """Remove and return up to `n_values` objects from the queue.
293
301
 
294
302
  If there are fewer than `n_values` items in the queue, return all of them.
@@ -331,7 +339,7 @@ class _Queue(_Object, type_prefix="qu"):
331
339
  @live_method
332
340
  async def put_many(
333
341
  self,
334
- vs: List[Any],
342
+ vs: list[Any],
335
343
  block: bool = True,
336
344
  timeout: Optional[float] = None,
337
345
  *,
@@ -355,7 +363,7 @@ class _Queue(_Object, type_prefix="qu"):
355
363
  await self._put_many_nonblocking(partition, partition_ttl, vs)
356
364
 
357
365
  async def _put_many_blocking(
358
- self, partition: Optional[str], partition_ttl: int, vs: List[Any], timeout: Optional[float] = None
366
+ self, partition: Optional[str], partition_ttl: int, vs: list[Any], timeout: Optional[float] = None
359
367
  ):
360
368
  vs_encoded = [serialize(v) for v in vs]
361
369
 
@@ -372,12 +380,19 @@ class _Queue(_Object, type_prefix="qu"):
372
380
  # A full queue will return this status.
373
381
  additional_status_codes=[Status.RESOURCE_EXHAUSTED],
374
382
  max_delay=30.0,
383
+ max_retries=None,
375
384
  total_timeout=timeout,
376
385
  )
377
386
  except GRPCError as exc:
378
- raise queue.Full(str(exc)) if exc.status == Status.RESOURCE_EXHAUSTED else exc
379
-
380
- async def _put_many_nonblocking(self, partition: Optional[str], partition_ttl: int, vs: List[Any]):
387
+ if exc.status == Status.RESOURCE_EXHAUSTED:
388
+ raise queue.Full(str(exc))
389
+ elif "status = '413'" in exc.message:
390
+ method = "put_many" if len(vs) > 1 else "put"
391
+ raise RequestSizeError(f"Queue.{method} request is too large") from exc
392
+ else:
393
+ raise exc
394
+
395
+ async def _put_many_nonblocking(self, partition: Optional[str], partition_ttl: int, vs: list[Any]):
381
396
  vs_encoded = [serialize(v) for v in vs]
382
397
  request = api_pb2.QueuePutRequest(
383
398
  queue_id=self.object_id,
@@ -388,14 +403,23 @@ class _Queue(_Object, type_prefix="qu"):
388
403
  try:
389
404
  await retry_transient_errors(self._client.stub.QueuePut, request)
390
405
  except GRPCError as exc:
391
- raise queue.Full(exc.message) if exc.status == Status.RESOURCE_EXHAUSTED else exc
406
+ if exc.status == Status.RESOURCE_EXHAUSTED:
407
+ raise queue.Full(exc.message)
408
+ elif "status = '413'" in exc.message:
409
+ method = "put_many" if len(vs) > 1 else "put"
410
+ raise RequestSizeError(f"Queue.{method} request is too large") from exc
411
+ else:
412
+ raise exc
392
413
 
393
414
  @live_method
394
- async def len(self, *, partition: Optional[str] = None) -> int:
415
+ async def len(self, *, partition: Optional[str] = None, total: bool = False) -> int:
395
416
  """Return the number of objects in the queue partition."""
417
+ if partition and total:
418
+ raise InvalidError("Partition must be null when requesting total length.")
396
419
  request = api_pb2.QueueLenRequest(
397
420
  queue_id=self.object_id,
398
421
  partition_key=self.validate_partition_key(partition),
422
+ total=total,
399
423
  )
400
424
  response = await retry_transient_errors(self._client.stub.QueueLen, request)
401
425
  return response.len