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
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, 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
@@ -11,11 +12,13 @@ from modal_proto import api_pb2
11
12
 
12
13
  from ._resolver import Resolver
13
14
  from ._serialization import deserialize, serialize
14
- from ._utils.async_utils import TaskContext, synchronize_api
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
18
- from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method
20
+ from .exception import InvalidError, RequestSizeError
21
+ from .object import EPHEMERAL_OBJECT_HEARTBEAT_SLEEP, _get_environment_name, _Object, live_method, live_method_gen
19
22
 
20
23
 
21
24
  class _Queue(_Object, type_prefix="qu"):
@@ -23,46 +26,73 @@ class _Queue(_Object, type_prefix="qu"):
23
26
 
24
27
  The queue can contain any object serializable by `cloudpickle`, including Modal objects.
25
28
 
26
- **Lifetime of a queue and its contents**
27
-
28
- A `Queue`'s lifetime matches the lifetime of the app it's attached to, but the contents expire after 30 days.
29
- Because of this, `Queues`s are best used for communication between active functions and not relied on for
30
- persistent storage. On app completion or after stopping an app any associated `Queue` objects are cleaned up.
29
+ By default, the `Queue` object acts as a single FIFO queue which supports puts and gets (blocking and non-blocking).
31
30
 
32
31
  **Usage**
33
32
 
34
33
  ```python
35
- from modal import Queue, Stub
36
-
37
- stub = Stub()
38
- my_queue = Queue.from_name("my-persisted-queue", create_if_missing=True)
34
+ from modal import Queue
39
35
 
40
- @stub.local_entrypoint()
41
- def main():
36
+ # Create an ephemeral queue which is anonymous and garbage collected
37
+ with Queue.ephemeral() as my_queue:
38
+ # Putting values
42
39
  my_queue.put("some value")
43
40
  my_queue.put(123)
44
41
 
42
+ # Getting values
45
43
  assert my_queue.get() == "some value"
46
44
  assert my_queue.get() == 123
45
+
46
+ # Using partitions
47
+ my_queue.put(0)
48
+ my_queue.put(1, partition="foo")
49
+ my_queue.put(2, partition="bar")
50
+
51
+ # Default and "foo" partition are ignored by the get operation.
52
+ assert my_queue.get(partition="bar") == 2
53
+
54
+ # Set custom 10s expiration time on "foo" partition.
55
+ my_queue.put(3, partition="foo", partition_ttl=10)
56
+
57
+ # (beta feature) Iterate through items in place (read immutably)
58
+ my_queue.put(1)
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
47
65
  ```
48
66
 
49
67
  For more examples, see the [guide](/docs/guide/dicts-and-queues#modal-queues).
50
- """
51
68
 
52
- @staticmethod
53
- def new():
54
- """`Queue.new` is deprecated.
69
+ **Queue partitions (beta)**
55
70
 
56
- Please use `Queue.from_name` (for persisted) or `Queue.ephemeral` (for ephemeral) queues.
57
- """
58
- 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.
59
74
 
60
- async def _load(self: _Queue, resolver: Resolver, existing_object_id: Optional[str]):
61
- request = api_pb2.QueueCreateRequest(app_id=resolver.app_id, existing_queue_id=existing_object_id)
62
- response = await resolver.client.stub.QueueCreate(request)
63
- 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**
64
80
 
65
- 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
+ """
66
96
 
67
97
  def __init__(self):
68
98
  """mdmd:hidden"""
@@ -82,7 +112,7 @@ class _Queue(_Object, type_prefix="qu"):
82
112
  @classmethod
83
113
  @asynccontextmanager
84
114
  async def ephemeral(
85
- cls: Type["_Queue"],
115
+ cls: type["_Queue"],
86
116
  client: Optional[_Client] = None,
87
117
  environment_name: Optional[str] = None,
88
118
  _heartbeat_sleep: float = EPHEMERAL_OBJECT_HEARTBEAT_SLEEP,
@@ -91,9 +121,13 @@ class _Queue(_Object, type_prefix="qu"):
91
121
 
92
122
  Usage:
93
123
  ```python
124
+ from modal import Queue
125
+
94
126
  with Queue.ephemeral() as q:
95
127
  q.put(123)
128
+ ```
96
129
 
130
+ ```python notest
97
131
  async with Queue.ephemeral() as q:
98
132
  await q.put.aio(123)
99
133
  ```
@@ -111,28 +145,29 @@ class _Queue(_Object, type_prefix="qu"):
111
145
  yield cls._new_hydrated(response.queue_id, client, None, is_another_app=True)
112
146
 
113
147
  @staticmethod
148
+ @renamed_parameter((2024, 12, 18), "label", "name")
114
149
  def from_name(
115
- label: str,
150
+ name: str,
116
151
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
117
152
  environment_name: Optional[str] = None,
118
153
  create_if_missing: bool = False,
119
154
  ) -> "_Queue":
120
- """Create a reference to a persisted Queue
121
-
122
- **Examples**
155
+ """Reference a named Queue, creating if necessary.
123
156
 
124
- ```python notest
125
- # In one app:
126
- stub.queue = Queue.persisted("my-queue")
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.
127
160
 
128
- # Later, in another app or Python file:
129
- stub.queue = Queue.from_name("my-queue")
161
+ ```python
162
+ q = modal.Queue.from_name("my-queue", create_if_missing=True)
163
+ q.put(123)
130
164
  ```
131
165
  """
166
+ check_object_name(name, "Queue")
132
167
 
133
168
  async def _load(self: _Queue, resolver: Resolver, existing_object_id: Optional[str]):
134
169
  req = api_pb2.QueueGetOrCreateRequest(
135
- deployment_name=label,
170
+ deployment_name=name,
136
171
  namespace=namespace,
137
172
  environment_name=_get_environment_name(environment_name, resolver),
138
173
  object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
@@ -140,25 +175,21 @@ class _Queue(_Object, type_prefix="qu"):
140
175
  response = await resolver.client.stub.QueueGetOrCreate(req)
141
176
  self._hydrate(response.queue_id, resolver.client, None)
142
177
 
143
- return _Queue._from_loader(_load, "Queue()", is_another_app=True)
144
-
145
- @staticmethod
146
- def persisted(
147
- label: str, namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE, environment_name: Optional[str] = None
148
- ) -> "_Queue":
149
- """Deprecated! Use `Queue.from_name(name, create_if_missing=True)`."""
150
- deprecation_warning((2024, 3, 1), _Queue.persisted.__doc__)
151
- return _Queue.from_name(label, namespace, environment_name, create_if_missing=True)
178
+ return _Queue._from_loader(_load, "Queue()", is_another_app=True, hydrate_lazily=True)
152
179
 
153
180
  @staticmethod
181
+ @renamed_parameter((2024, 12, 18), "label", "name")
154
182
  async def lookup(
155
- label: str,
183
+ name: str,
156
184
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
157
185
  client: Optional[_Client] = None,
158
186
  environment_name: Optional[str] = None,
159
187
  create_if_missing: bool = False,
160
188
  ) -> "_Queue":
161
- """Lookup a queue with a given name and tag.
189
+ """Lookup a named Queue.
190
+
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.
162
193
 
163
194
  ```python
164
195
  q = modal.Queue.lookup("my-queue")
@@ -166,7 +197,7 @@ class _Queue(_Object, type_prefix="qu"):
166
197
  ```
167
198
  """
168
199
  obj = _Queue.from_name(
169
- 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
170
201
  )
171
202
  if client is None:
172
203
  client = await _Client.from_env()
@@ -174,7 +205,14 @@ class _Queue(_Object, type_prefix="qu"):
174
205
  await resolver.load(obj)
175
206
  return obj
176
207
 
177
- async def _get_nonblocking(self, partition: Optional[str], n_values: int) -> List[Any]:
208
+ @staticmethod
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)
212
+ req = api_pb2.QueueDeleteRequest(queue_id=obj.object_id)
213
+ await retry_transient_errors(obj._client.stub.QueueDelete, req)
214
+
215
+ async def _get_nonblocking(self, partition: Optional[str], n_values: int) -> list[Any]:
178
216
  request = api_pb2.QueueGetRequest(
179
217
  queue_id=self.object_id,
180
218
  partition_key=self.validate_partition_key(partition),
@@ -188,7 +226,7 @@ class _Queue(_Object, type_prefix="qu"):
188
226
  else:
189
227
  return []
190
228
 
191
- 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]:
192
230
  if timeout is not None:
193
231
  deadline = time.time() + timeout
194
232
  else:
@@ -217,6 +255,18 @@ class _Queue(_Object, type_prefix="qu"):
217
255
 
218
256
  raise queue.Empty()
219
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
+
220
270
  @live_method
221
271
  async def get(
222
272
  self, block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
@@ -246,7 +296,7 @@ class _Queue(_Object, type_prefix="qu"):
246
296
  @live_method
247
297
  async def get_many(
248
298
  self, n_values: int, block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
249
- ) -> List[Any]:
299
+ ) -> list[Any]:
250
300
  """Remove and return up to `n_values` objects from the queue.
251
301
 
252
302
  If there are fewer than `n_values` items in the queue, return all of them.
@@ -268,7 +318,13 @@ class _Queue(_Object, type_prefix="qu"):
268
318
 
269
319
  @live_method
270
320
  async def put(
271
- self, v: Any, block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
321
+ self,
322
+ v: Any,
323
+ block: bool = True,
324
+ timeout: Optional[float] = None,
325
+ *,
326
+ partition: Optional[str] = None,
327
+ partition_ttl: int = 24 * 3600, # After 24 hours of no activity, this partition will be deletd.
272
328
  ) -> None:
273
329
  """Add an object to the end of the queue.
274
330
 
@@ -278,11 +334,17 @@ class _Queue(_Object, type_prefix="qu"):
278
334
 
279
335
  If `block` is `False`, this method raises `queue.Full` immediately if the queue is full. The `timeout` is
280
336
  ignored in this case."""
281
- await self.put_many([v], block, timeout, partition=partition)
337
+ await self.put_many([v], block, timeout, partition=partition, partition_ttl=partition_ttl)
282
338
 
283
339
  @live_method
284
340
  async def put_many(
285
- self, vs: List[Any], block: bool = True, timeout: Optional[float] = None, *, partition: Optional[str] = None
341
+ self,
342
+ vs: list[Any],
343
+ block: bool = True,
344
+ timeout: Optional[float] = None,
345
+ *,
346
+ partition: Optional[str] = None,
347
+ partition_ttl: int = 24 * 3600, # After 24 hours of no activity, this partition will be deletd.
286
348
  ) -> None:
287
349
  """Add several objects to the end of the queue.
288
350
 
@@ -291,21 +353,25 @@ class _Queue(_Object, type_prefix="qu"):
291
353
  If blocking it is not recommended to omit the `timeout`, as the operation could wait indefinitely.
292
354
 
293
355
  If `block` is `False`, this method raises `queue.Full` immediately if the queue is full. The `timeout` is
294
- ignored in this case."""
356
+ ignored in this case.
357
+ """
295
358
  if block:
296
- await self._put_many_blocking(partition, vs, timeout)
359
+ await self._put_many_blocking(partition, partition_ttl, vs, timeout)
297
360
  else:
298
361
  if timeout is not None:
299
362
  warnings.warn("`timeout` argument is ignored for non-blocking put.")
300
- await self._put_many_nonblocking(partition, vs)
363
+ await self._put_many_nonblocking(partition, partition_ttl, vs)
301
364
 
302
- async def _put_many_blocking(self, partition: Optional[str], vs: List[Any], timeout: Optional[float] = None):
365
+ async def _put_many_blocking(
366
+ self, partition: Optional[str], partition_ttl: int, vs: list[Any], timeout: Optional[float] = None
367
+ ):
303
368
  vs_encoded = [serialize(v) for v in vs]
304
369
 
305
370
  request = api_pb2.QueuePutRequest(
306
371
  queue_id=self.object_id,
307
372
  partition_key=self.validate_partition_key(partition),
308
373
  values=vs_encoded,
374
+ partition_ttl_seconds=partition_ttl,
309
375
  )
310
376
  try:
311
377
  await retry_transient_errors(
@@ -314,32 +380,83 @@ class _Queue(_Object, type_prefix="qu"):
314
380
  # A full queue will return this status.
315
381
  additional_status_codes=[Status.RESOURCE_EXHAUSTED],
316
382
  max_delay=30.0,
383
+ max_retries=None,
317
384
  total_timeout=timeout,
318
385
  )
319
386
  except GRPCError as exc:
320
- raise queue.Full(str(exc)) if exc.status == Status.RESOURCE_EXHAUSTED else exc
321
-
322
- async def _put_many_nonblocking(self, partition: Optional[str], 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]):
323
396
  vs_encoded = [serialize(v) for v in vs]
324
397
  request = api_pb2.QueuePutRequest(
325
398
  queue_id=self.object_id,
326
399
  partition_key=self.validate_partition_key(partition),
327
400
  values=vs_encoded,
401
+ partition_ttl_seconds=partition_ttl,
328
402
  )
329
403
  try:
330
404
  await retry_transient_errors(self._client.stub.QueuePut, request)
331
405
  except GRPCError as exc:
332
- 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
333
413
 
334
414
  @live_method
335
- async def len(self, *, partition: Optional[str] = None) -> int:
415
+ async def len(self, *, partition: Optional[str] = None, total: bool = False) -> int:
336
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.")
337
419
  request = api_pb2.QueueLenRequest(
338
420
  queue_id=self.object_id,
339
421
  partition_key=self.validate_partition_key(partition),
422
+ total=total,
340
423
  )
341
424
  response = await retry_transient_errors(self._client.stub.QueueLen, request)
342
425
  return response.len
343
426
 
427
+ @warn_if_generator_is_not_consumed()
428
+ @live_method_gen
429
+ async def iterate(
430
+ self, *, partition: Optional[str] = None, item_poll_timeout: float = 0.0
431
+ ) -> AsyncGenerator[Any, None]:
432
+ """(Beta feature) Iterate through items in the queue without mutation.
433
+
434
+ Specify `item_poll_timeout` to control how long the iterator should wait for the next time before giving up.
435
+ """
436
+ last_entry_id: Optional[str] = None
437
+ validated_partition_key = self.validate_partition_key(partition)
438
+ fetch_deadline = time.time() + item_poll_timeout
439
+
440
+ MAX_POLL_DURATION = 30.0
441
+ while True:
442
+ poll_duration = max(0.0, min(MAX_POLL_DURATION, fetch_deadline - time.time()))
443
+ request = api_pb2.QueueNextItemsRequest(
444
+ queue_id=self.object_id,
445
+ partition_key=validated_partition_key,
446
+ last_entry_id=last_entry_id,
447
+ item_poll_timeout=poll_duration,
448
+ )
449
+
450
+ response: api_pb2.QueueNextItemsResponse = await retry_transient_errors(
451
+ self._client.stub.QueueNextItems, request
452
+ )
453
+ if response.items:
454
+ for item in response.items:
455
+ yield deserialize(item.value, self._client)
456
+ last_entry_id = item.entry_id
457
+ fetch_deadline = time.time() + item_poll_timeout
458
+ elif time.time() > fetch_deadline:
459
+ break
460
+
344
461
 
345
462
  Queue = synchronize_api(_Queue)