modal 0.62.16__py3-none-any.whl → 0.72.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/__init__.py +17 -13
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +420 -937
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -59
- modal/_resources.py +51 -0
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1036 -0
- modal/_runtime/execution_context.py +89 -0
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +134 -9
- modal/_traceback.py +47 -187
- modal/_tunnel.py +52 -16
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +479 -100
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +460 -171
- modal/_utils/grpc_testing.py +47 -31
- modal/_utils/grpc_utils.py +62 -109
- modal/_utils/hash_utils.py +61 -19
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +5 -7
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +14 -12
- modal/app.py +1003 -314
- modal/app.pyi +540 -264
- modal/call_graph.py +7 -6
- modal/cli/_download.py +63 -53
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +205 -45
- modal/cli/config.py +12 -5
- modal/cli/container.py +62 -14
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +64 -58
- modal/cli/launch.py +32 -18
- modal/cli/network_file_system.py +64 -83
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +35 -10
- modal/cli/programs/vscode.py +60 -10
- modal/cli/queues.py +131 -0
- modal/cli/run.py +234 -131
- modal/cli/secret.py +8 -7
- modal/cli/token.py +7 -2
- modal/cli/utils.py +79 -10
- modal/cli/volume.py +110 -109
- modal/client.py +250 -144
- modal/client.pyi +157 -118
- modal/cloud_bucket_mount.py +108 -34
- modal/cloud_bucket_mount.pyi +32 -38
- modal/cls.py +535 -148
- modal/cls.pyi +190 -146
- modal/config.py +41 -19
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +111 -65
- modal/dict.pyi +136 -131
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +34 -43
- modal/experimental.py +61 -2
- modal/extensions/ipython.py +5 -5
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +197 -0
- modal/functions.py +906 -911
- modal/functions.pyi +466 -430
- modal/gpu.py +57 -44
- modal/image.py +1089 -479
- modal/image.pyi +584 -228
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +314 -101
- modal/mount.pyi +241 -235
- modal/network_file_system.py +92 -92
- modal/network_file_system.pyi +152 -110
- modal/object.py +67 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +434 -0
- modal/parallel_map.pyi +75 -0
- modal/partial_function.py +282 -117
- modal/partial_function.pyi +222 -129
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +182 -65
- modal/queue.pyi +218 -118
- modal/requirements/2024.04.txt +29 -0
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +48 -7
- modal/runner.py +459 -156
- modal/runner.pyi +135 -71
- modal/running_app.py +38 -0
- modal/sandbox.py +514 -236
- modal/sandbox.pyi +397 -169
- modal/schedule.py +4 -4
- modal/scheduler_placement.py +20 -3
- modal/secret.py +56 -31
- modal/secret.pyi +62 -42
- modal/serving.py +51 -56
- modal/serving.pyi +44 -36
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +285 -157
- modal/volume.pyi +249 -184
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
- modal-0.72.11.dist-info/RECORD +174 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +5 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1288 -533
- modal_proto/api_grpc.py +856 -456
- modal_proto/api_pb2.py +2165 -1157
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1674 -855
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_entrypoint.pyi +0 -378
- modal/_container_exec.py +0 -128
- modal/_sandbox_shell.py +0 -49
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal/stub.py +0 -783
- modal/stub.pyi +0 -332
- modal-0.62.16.dist-info/RECORD +0 -198
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -262
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -659
- test/client_test.py +0 -194
- test/cls_test.py +0 -630
- test/config_test.py +0 -137
- test/conftest.py +0 -1420
- test/container_app_test.py +0 -32
- test/container_test.py +0 -1389
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -33
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -653
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -141
- test/helpers.py +0 -42
- test/image_test.py +0 -669
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -329
- test/network_file_system_test.py +0 -181
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -97
- test/resolver_test.py +0 -58
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -29
- test/secret_test.py +0 -78
- test/serialization_test.py +0 -42
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -360
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -341
- test/watcher_test.py +0 -30
- test/webhook_test.py +0 -146
- /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
- /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
- {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
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
|
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,
|
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
|
-
|
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
|
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
|
-
|
41
|
-
|
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
|
-
|
53
|
-
def new():
|
54
|
-
"""`Queue.new` is deprecated.
|
69
|
+
**Queue partitions (beta)**
|
55
70
|
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
"""
|
121
|
-
|
122
|
-
**Examples**
|
155
|
+
"""Reference a named Queue, creating if necessary.
|
123
156
|
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
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=
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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) ->
|
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
|
-
) ->
|
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,
|
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,
|
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(
|
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
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
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)
|