apify 3.0.0rc1__py3-none-any.whl → 3.0.1b2__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 apify might be problematic. Click here for more details.
- apify/_actor.py +150 -117
- apify/_charging.py +19 -0
- apify/_configuration.py +51 -11
- apify/events/__init__.py +2 -2
- apify/storage_clients/__init__.py +2 -0
- apify/storage_clients/_apify/_dataset_client.py +47 -23
- apify/storage_clients/_apify/_key_value_store_client.py +46 -22
- apify/storage_clients/_apify/_models.py +25 -1
- apify/storage_clients/_apify/_request_queue_client.py +188 -648
- apify/storage_clients/_apify/_request_queue_shared_client.py +527 -0
- apify/storage_clients/_apify/_request_queue_single_client.py +399 -0
- apify/storage_clients/_apify/_storage_client.py +55 -29
- apify/storage_clients/_apify/_utils.py +194 -0
- apify/storage_clients/_file_system/_key_value_store_client.py +70 -3
- apify/storage_clients/_file_system/_storage_client.py +7 -1
- apify/storage_clients/_smart_apify/__init__.py +1 -0
- apify/storage_clients/_smart_apify/_storage_client.py +117 -0
- {apify-3.0.0rc1.dist-info → apify-3.0.1b2.dist-info}/METADATA +20 -5
- {apify-3.0.0rc1.dist-info → apify-3.0.1b2.dist-info}/RECORD +21 -16
- {apify-3.0.0rc1.dist-info → apify-3.0.1b2.dist-info}/WHEEL +0 -0
- {apify-3.0.0rc1.dist-info → apify-3.0.1b2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,62 +1,34 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
import re
|
|
5
|
-
from base64 import b64encode
|
|
6
|
-
from collections import deque
|
|
7
|
-
from datetime import datetime, timedelta, timezone
|
|
8
|
-
from hashlib import sha256
|
|
9
3
|
from logging import getLogger
|
|
10
|
-
from typing import TYPE_CHECKING, Final
|
|
4
|
+
from typing import TYPE_CHECKING, Final, Literal
|
|
11
5
|
|
|
12
|
-
from cachetools import LRUCache
|
|
13
6
|
from typing_extensions import override
|
|
14
7
|
|
|
15
8
|
from apify_client import ApifyClientAsync
|
|
9
|
+
from crawlee._utils.crypto import crypto_random_object_id
|
|
16
10
|
from crawlee.storage_clients._base import RequestQueueClient
|
|
17
11
|
from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata
|
|
12
|
+
from crawlee.storages import RequestQueue
|
|
18
13
|
|
|
19
|
-
from ._models import
|
|
20
|
-
from
|
|
14
|
+
from ._models import ApifyRequestQueueMetadata, RequestQueueStats
|
|
15
|
+
from ._request_queue_shared_client import _ApifyRequestQueueSharedClient
|
|
16
|
+
from ._request_queue_single_client import _ApifyRequestQueueSingleClient
|
|
17
|
+
from ._utils import AliasResolver
|
|
21
18
|
|
|
22
19
|
if TYPE_CHECKING:
|
|
23
20
|
from collections.abc import Sequence
|
|
24
21
|
|
|
25
22
|
from apify_client.clients import RequestQueueClientAsync
|
|
23
|
+
from crawlee import Request
|
|
26
24
|
|
|
27
25
|
from apify import Configuration
|
|
28
26
|
|
|
29
27
|
logger = getLogger(__name__)
|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
def unique_key_to_request_id(unique_key: str, *, request_id_length: int = 15) -> str:
|
|
33
|
-
"""Generate a deterministic request ID based on a unique key.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
unique_key: The unique key to convert into a request ID.
|
|
37
|
-
request_id_length: The length of the request ID.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
A URL-safe, truncated request ID based on the unique key.
|
|
41
|
-
"""
|
|
42
|
-
# Encode the unique key and compute its SHA-256 hash
|
|
43
|
-
hashed_key = sha256(unique_key.encode('utf-8')).digest()
|
|
44
|
-
|
|
45
|
-
# Encode the hash in base64 and decode it to get a string
|
|
46
|
-
base64_encoded = b64encode(hashed_key).decode('utf-8')
|
|
47
|
-
|
|
48
|
-
# Remove characters that are not URL-safe ('+', '/', or '=')
|
|
49
|
-
url_safe_key = re.sub(r'(\+|\/|=)', '', base64_encoded)
|
|
50
|
-
|
|
51
|
-
# Truncate the key to the desired length
|
|
52
|
-
return url_safe_key[:request_id_length]
|
|
53
|
-
|
|
54
|
-
|
|
55
30
|
class ApifyRequestQueueClient(RequestQueueClient):
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
_DEFAULT_LOCK_TIME: Final[timedelta] = timedelta(minutes=3)
|
|
59
|
-
"""The default lock time for requests in the queue."""
|
|
31
|
+
"""Base class for Apify platform implementations of the request queue client."""
|
|
60
32
|
|
|
61
33
|
_MAX_CACHED_REQUESTS: Final[int] = 1_000_000
|
|
62
34
|
"""Maximum number of requests that can be cached."""
|
|
@@ -65,10 +37,8 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
65
37
|
self,
|
|
66
38
|
*,
|
|
67
39
|
api_client: RequestQueueClientAsync,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
total_request_count: int,
|
|
71
|
-
handled_request_count: int,
|
|
40
|
+
metadata: RequestQueueMetadata,
|
|
41
|
+
access: Literal['single', 'shared'] = 'single',
|
|
72
42
|
) -> None:
|
|
73
43
|
"""Initialize a new instance.
|
|
74
44
|
|
|
@@ -77,179 +47,25 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
77
47
|
self._api_client = api_client
|
|
78
48
|
"""The Apify request queue client for API operations."""
|
|
79
49
|
|
|
80
|
-
self.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self._queue_head = deque[str]()
|
|
87
|
-
"""A deque to store request unique keys in the queue head."""
|
|
88
|
-
|
|
89
|
-
self._requests_cache: LRUCache[str, CachedRequest] = LRUCache(maxsize=self._MAX_CACHED_REQUESTS)
|
|
90
|
-
"""A cache to store request objects. Request unique key is used as the cache key."""
|
|
91
|
-
|
|
92
|
-
self._queue_has_locked_requests: bool | None = None
|
|
93
|
-
"""Whether the queue has requests locked by another client."""
|
|
94
|
-
|
|
95
|
-
self._should_check_for_forefront_requests = False
|
|
96
|
-
"""Whether to check for forefront requests in the next list_head call."""
|
|
97
|
-
|
|
98
|
-
self._had_multiple_clients = False
|
|
99
|
-
"""Whether the request queue has been accessed by multiple clients."""
|
|
100
|
-
|
|
101
|
-
self._initial_total_count = total_request_count
|
|
102
|
-
"""The initial total request count (from the API) when the queue was opened."""
|
|
103
|
-
|
|
104
|
-
self._initial_handled_count = handled_request_count
|
|
105
|
-
"""The initial handled request count (from the API) when the queue was opened."""
|
|
106
|
-
|
|
107
|
-
self._assumed_total_count = 0
|
|
108
|
-
"""The number of requests we assume are in the queue (tracked manually for this instance)."""
|
|
109
|
-
|
|
110
|
-
self._assumed_handled_count = 0
|
|
111
|
-
"""The number of requests we assume have been handled (tracked manually for this instance)."""
|
|
112
|
-
|
|
113
|
-
self._fetch_lock = asyncio.Lock()
|
|
114
|
-
"""Fetch lock to minimize race conditions when communicating with API."""
|
|
115
|
-
|
|
116
|
-
@override
|
|
117
|
-
async def get_metadata(self) -> RequestQueueMetadata:
|
|
118
|
-
total_count = self._initial_total_count + self._assumed_total_count
|
|
119
|
-
handled_count = self._initial_handled_count + self._assumed_handled_count
|
|
120
|
-
pending_count = total_count - handled_count
|
|
121
|
-
|
|
122
|
-
return RequestQueueMetadata(
|
|
123
|
-
id=self._id,
|
|
124
|
-
name=self._name,
|
|
125
|
-
total_request_count=total_count,
|
|
126
|
-
handled_request_count=handled_count,
|
|
127
|
-
pending_request_count=pending_count,
|
|
128
|
-
created_at=datetime.now(timezone.utc),
|
|
129
|
-
modified_at=datetime.now(timezone.utc),
|
|
130
|
-
accessed_at=datetime.now(timezone.utc),
|
|
131
|
-
had_multiple_clients=self._had_multiple_clients,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
@classmethod
|
|
135
|
-
async def open(
|
|
136
|
-
cls,
|
|
137
|
-
*,
|
|
138
|
-
id: str | None,
|
|
139
|
-
name: str | None,
|
|
140
|
-
configuration: Configuration,
|
|
141
|
-
) -> ApifyRequestQueueClient:
|
|
142
|
-
"""Open an Apify request queue client.
|
|
143
|
-
|
|
144
|
-
This method creates and initializes a new instance of the Apify request queue client. It handles
|
|
145
|
-
authentication, storage lookup/creation, and metadata retrieval, and sets up internal caching and queue
|
|
146
|
-
management structures.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
id: The ID of an existing request queue to open. If provided, the client will connect to this specific
|
|
150
|
-
storage. Cannot be used together with `name`.
|
|
151
|
-
name: The name of a request queue to get or create. If a storage with this name exists, it will be opened;
|
|
152
|
-
otherwise, a new one will be created. Cannot be used together with `id`.
|
|
153
|
-
configuration: The configuration object containing API credentials and settings. Must include a valid
|
|
154
|
-
`token` and `api_base_url`. May also contain a `default_request_queue_id` for fallback when neither
|
|
155
|
-
`id` nor `name` is provided.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
An instance for the opened or created storage client.
|
|
159
|
-
|
|
160
|
-
Raises:
|
|
161
|
-
ValueError: If the configuration is missing required fields (token, api_base_url), if both `id` and `name`
|
|
162
|
-
are provided, or if neither `id` nor `name` is provided and no default storage ID is available
|
|
163
|
-
in the configuration.
|
|
164
|
-
"""
|
|
165
|
-
token = configuration.token
|
|
166
|
-
if not token:
|
|
167
|
-
raise ValueError(f'Apify storage client requires a valid token in Configuration (token={token}).')
|
|
168
|
-
|
|
169
|
-
api_url = configuration.api_base_url
|
|
170
|
-
if not api_url:
|
|
171
|
-
raise ValueError(f'Apify storage client requires a valid API URL in Configuration (api_url={api_url}).')
|
|
172
|
-
|
|
173
|
-
api_public_base_url = configuration.api_public_base_url
|
|
174
|
-
if not api_public_base_url:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
'Apify storage client requires a valid API public base URL in Configuration '
|
|
177
|
-
f'(api_public_base_url={api_public_base_url}).'
|
|
50
|
+
self._implementation: _ApifyRequestQueueSingleClient | _ApifyRequestQueueSharedClient
|
|
51
|
+
"""Internal implementation used to communicate with the Apify platform based Request Queue."""
|
|
52
|
+
if access == 'single':
|
|
53
|
+
self._implementation = _ApifyRequestQueueSingleClient(
|
|
54
|
+
api_client=self._api_client, metadata=metadata, cache_size=self._MAX_CACHED_REQUESTS
|
|
178
55
|
)
|
|
56
|
+
elif access == 'shared':
|
|
57
|
+
self._implementation = _ApifyRequestQueueSharedClient(
|
|
58
|
+
api_client=self._api_client,
|
|
59
|
+
metadata=metadata,
|
|
60
|
+
cache_size=self._MAX_CACHED_REQUESTS,
|
|
61
|
+
metadata_getter=self.get_metadata,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
raise RuntimeError(f"Unsupported access type: {access}. Allowed values are 'single' or 'shared'.")
|
|
179
65
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
api_url=api_url,
|
|
184
|
-
max_retries=8,
|
|
185
|
-
min_delay_between_retries_millis=500,
|
|
186
|
-
timeout_secs=360,
|
|
187
|
-
)
|
|
188
|
-
apify_rqs_client = apify_client_async.request_queues()
|
|
189
|
-
|
|
190
|
-
# If both id and name are provided, raise an error.
|
|
191
|
-
if id and name:
|
|
192
|
-
raise ValueError('Only one of "id" or "name" can be specified, not both.')
|
|
193
|
-
|
|
194
|
-
# If id is provided, get the storage by ID.
|
|
195
|
-
if id and name is None:
|
|
196
|
-
apify_rq_client = apify_client_async.request_queue(request_queue_id=id)
|
|
197
|
-
|
|
198
|
-
# If name is provided, get or create the storage by name.
|
|
199
|
-
if name and id is None:
|
|
200
|
-
id = RequestQueueMetadata.model_validate(
|
|
201
|
-
await apify_rqs_client.get_or_create(name=name),
|
|
202
|
-
).id
|
|
203
|
-
apify_rq_client = apify_client_async.request_queue(request_queue_id=id)
|
|
204
|
-
|
|
205
|
-
# If both id and name are None, try to get the default storage ID from environment variables.
|
|
206
|
-
# The default storage ID environment variable is set by the Apify platform. It also contains
|
|
207
|
-
# a new storage ID after Actor's reboot or migration.
|
|
208
|
-
if id is None and name is None:
|
|
209
|
-
id = configuration.default_request_queue_id
|
|
210
|
-
apify_rq_client = apify_client_async.request_queue(request_queue_id=id)
|
|
211
|
-
|
|
212
|
-
# Fetch its metadata.
|
|
213
|
-
metadata = await apify_rq_client.get()
|
|
214
|
-
|
|
215
|
-
# If metadata is None, it means the storage does not exist, so we create it.
|
|
216
|
-
if metadata is None:
|
|
217
|
-
id = RequestQueueMetadata.model_validate(
|
|
218
|
-
await apify_rqs_client.get_or_create(),
|
|
219
|
-
).id
|
|
220
|
-
apify_rq_client = apify_client_async.request_queue(request_queue_id=id)
|
|
221
|
-
|
|
222
|
-
# Verify that the storage exists by fetching its metadata again.
|
|
223
|
-
metadata = await apify_rq_client.get()
|
|
224
|
-
if metadata is None:
|
|
225
|
-
raise ValueError(f'Opening request queue with id={id} and name={name} failed.')
|
|
226
|
-
|
|
227
|
-
metadata_model = RequestQueueMetadata.model_validate(
|
|
228
|
-
await apify_rqs_client.get_or_create(),
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
# Ensure we have a valid ID.
|
|
232
|
-
if id is None:
|
|
233
|
-
raise ValueError('Request queue ID cannot be None.')
|
|
234
|
-
|
|
235
|
-
return cls(
|
|
236
|
-
api_client=apify_rq_client,
|
|
237
|
-
id=id,
|
|
238
|
-
name=name,
|
|
239
|
-
total_request_count=metadata_model.total_request_count,
|
|
240
|
-
handled_request_count=metadata_model.handled_request_count,
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
@override
|
|
244
|
-
async def purge(self) -> None:
|
|
245
|
-
raise NotImplementedError(
|
|
246
|
-
'Purging the request queue is not supported in the Apify platform. '
|
|
247
|
-
'Use the `drop` method to delete the request queue instead.'
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
@override
|
|
251
|
-
async def drop(self) -> None:
|
|
252
|
-
await self._api_client.delete()
|
|
66
|
+
@property
|
|
67
|
+
def _metadata(self) -> RequestQueueMetadata:
|
|
68
|
+
return self._implementation.metadata
|
|
253
69
|
|
|
254
70
|
@override
|
|
255
71
|
async def add_batch_of_requests(
|
|
@@ -267,100 +83,7 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
267
83
|
Returns:
|
|
268
84
|
Response containing information about the added requests.
|
|
269
85
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
new_requests: list[Request] = []
|
|
273
|
-
already_present_requests: list[ProcessedRequest] = []
|
|
274
|
-
|
|
275
|
-
for request in requests:
|
|
276
|
-
if self._requests_cache.get(request.unique_key):
|
|
277
|
-
# We are not sure if it was already handled at this point, and it is not worth calling API for it.
|
|
278
|
-
# It could have been handled by another client in the meantime, so cached information about
|
|
279
|
-
# `request.was_already_handled` is not reliable.
|
|
280
|
-
already_present_requests.append(
|
|
281
|
-
ProcessedRequest.model_validate(
|
|
282
|
-
{
|
|
283
|
-
'uniqueKey': request.unique_key,
|
|
284
|
-
'wasAlreadyPresent': True,
|
|
285
|
-
'wasAlreadyHandled': request.was_already_handled,
|
|
286
|
-
}
|
|
287
|
-
)
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
else:
|
|
291
|
-
# Add new request to the cache.
|
|
292
|
-
processed_request = ProcessedRequest.model_validate(
|
|
293
|
-
{
|
|
294
|
-
'uniqueKey': request.unique_key,
|
|
295
|
-
'wasAlreadyPresent': True,
|
|
296
|
-
'wasAlreadyHandled': request.was_already_handled,
|
|
297
|
-
}
|
|
298
|
-
)
|
|
299
|
-
self._cache_request(
|
|
300
|
-
request.unique_key,
|
|
301
|
-
processed_request,
|
|
302
|
-
)
|
|
303
|
-
new_requests.append(request)
|
|
304
|
-
|
|
305
|
-
if new_requests:
|
|
306
|
-
# Prepare requests for API by converting to dictionaries.
|
|
307
|
-
requests_dict = [
|
|
308
|
-
request.model_dump(
|
|
309
|
-
by_alias=True,
|
|
310
|
-
exclude={'id'}, # Exclude ID fields from requests since the API doesn't accept them.
|
|
311
|
-
)
|
|
312
|
-
for request in new_requests
|
|
313
|
-
]
|
|
314
|
-
|
|
315
|
-
# Send requests to API.
|
|
316
|
-
api_response = AddRequestsResponse.model_validate(
|
|
317
|
-
await self._api_client.batch_add_requests(requests=requests_dict, forefront=forefront)
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Add the locally known already present processed requests based on the local cache.
|
|
321
|
-
api_response.processed_requests.extend(already_present_requests)
|
|
322
|
-
|
|
323
|
-
# Remove unprocessed requests from the cache
|
|
324
|
-
for unprocessed_request in api_response.unprocessed_requests:
|
|
325
|
-
self._requests_cache.pop(unprocessed_request.unique_key, None)
|
|
326
|
-
|
|
327
|
-
else:
|
|
328
|
-
api_response = AddRequestsResponse.model_validate(
|
|
329
|
-
{'unprocessedRequests': [], 'processedRequests': already_present_requests}
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
logger.debug(
|
|
333
|
-
f'Tried to add new requests: {len(new_requests)}, '
|
|
334
|
-
f'succeeded to add new requests: {len(api_response.processed_requests) - len(already_present_requests)}, '
|
|
335
|
-
f'skipped already present requests: {len(already_present_requests)}'
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
# Update assumed total count for newly added requests.
|
|
339
|
-
new_request_count = 0
|
|
340
|
-
for processed_request in api_response.processed_requests:
|
|
341
|
-
if not processed_request.was_already_present and not processed_request.was_already_handled:
|
|
342
|
-
new_request_count += 1
|
|
343
|
-
|
|
344
|
-
self._assumed_total_count += new_request_count
|
|
345
|
-
|
|
346
|
-
return api_response
|
|
347
|
-
|
|
348
|
-
@override
|
|
349
|
-
async def get_request(self, unique_key: str) -> Request | None:
|
|
350
|
-
"""Get a request by unique key.
|
|
351
|
-
|
|
352
|
-
Args:
|
|
353
|
-
unique_key: Unique key of the request to get.
|
|
354
|
-
|
|
355
|
-
Returns:
|
|
356
|
-
The request or None if not found.
|
|
357
|
-
"""
|
|
358
|
-
response = await self._api_client.get_request(unique_key_to_request_id(unique_key))
|
|
359
|
-
|
|
360
|
-
if response is None:
|
|
361
|
-
return None
|
|
362
|
-
|
|
363
|
-
return Request.model_validate(response)
|
|
86
|
+
return await self._implementation.add_batch_of_requests(requests, forefront=forefront)
|
|
364
87
|
|
|
365
88
|
@override
|
|
366
89
|
async def fetch_next_request(self) -> Request | None:
|
|
@@ -374,45 +97,7 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
374
97
|
Returns:
|
|
375
98
|
The request or `None` if there are no more pending requests.
|
|
376
99
|
"""
|
|
377
|
-
|
|
378
|
-
async with self._fetch_lock:
|
|
379
|
-
await self._ensure_head_is_non_empty()
|
|
380
|
-
|
|
381
|
-
# If queue head is empty after ensuring, there are no requests
|
|
382
|
-
if not self._queue_head:
|
|
383
|
-
return None
|
|
384
|
-
|
|
385
|
-
# Get the next request ID from the queue head
|
|
386
|
-
next_unique_key = self._queue_head.popleft()
|
|
387
|
-
|
|
388
|
-
request = await self._get_or_hydrate_request(next_unique_key)
|
|
389
|
-
|
|
390
|
-
# Handle potential inconsistency where request might not be in the main table yet
|
|
391
|
-
if request is None:
|
|
392
|
-
logger.debug(
|
|
393
|
-
'Cannot find a request from the beginning of queue, will be retried later',
|
|
394
|
-
extra={'nextRequestUniqueKey': next_unique_key},
|
|
395
|
-
)
|
|
396
|
-
return None
|
|
397
|
-
|
|
398
|
-
# If the request was already handled, skip it
|
|
399
|
-
if request.handled_at is not None:
|
|
400
|
-
logger.debug(
|
|
401
|
-
'Request fetched from the beginning of queue was already handled',
|
|
402
|
-
extra={'nextRequestUniqueKey': next_unique_key},
|
|
403
|
-
)
|
|
404
|
-
return None
|
|
405
|
-
|
|
406
|
-
# Use get request to ensure we have the full request object.
|
|
407
|
-
request = await self.get_request(request.unique_key)
|
|
408
|
-
if request is None:
|
|
409
|
-
logger.debug(
|
|
410
|
-
'Request fetched from the beginning of queue was not found in the RQ',
|
|
411
|
-
extra={'nextRequestUniqueKey': next_unique_key},
|
|
412
|
-
)
|
|
413
|
-
return None
|
|
414
|
-
|
|
415
|
-
return request
|
|
100
|
+
return await self._implementation.fetch_next_request()
|
|
416
101
|
|
|
417
102
|
@override
|
|
418
103
|
async def mark_request_as_handled(self, request: Request) -> ProcessedRequest | None:
|
|
@@ -426,33 +111,19 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
426
111
|
Returns:
|
|
427
112
|
Information about the queue operation. `None` if the given request was not in progress.
|
|
428
113
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
self._assumed_handled_count += 1
|
|
443
|
-
|
|
444
|
-
# Update the cache with the handled request
|
|
445
|
-
cache_key = request.unique_key
|
|
446
|
-
self._cache_request(
|
|
447
|
-
cache_key,
|
|
448
|
-
processed_request,
|
|
449
|
-
hydrated_request=request,
|
|
450
|
-
)
|
|
451
|
-
except Exception as exc:
|
|
452
|
-
logger.debug(f'Error marking request {request.unique_key} as handled: {exc!s}')
|
|
453
|
-
return None
|
|
454
|
-
else:
|
|
455
|
-
return processed_request
|
|
114
|
+
return await self._implementation.mark_request_as_handled(request)
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
async def get_request(self, unique_key: str) -> Request | None:
|
|
118
|
+
"""Get a request by unique key.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
unique_key: Unique key of the request to get.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The request or None if not found.
|
|
125
|
+
"""
|
|
126
|
+
return await self._implementation.get_request(unique_key)
|
|
456
127
|
|
|
457
128
|
@override
|
|
458
129
|
async def reclaim_request(
|
|
@@ -472,46 +143,7 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
472
143
|
Returns:
|
|
473
144
|
Information about the queue operation. `None` if the given request was not in progress.
|
|
474
145
|
"""
|
|
475
|
-
|
|
476
|
-
# we want to put the request back for processing.
|
|
477
|
-
if request.was_already_handled:
|
|
478
|
-
request.handled_at = None
|
|
479
|
-
|
|
480
|
-
# Reclaim with lock to prevent race conditions that could lead to double processing of the same request.
|
|
481
|
-
async with self._fetch_lock:
|
|
482
|
-
try:
|
|
483
|
-
# Update the request in the API.
|
|
484
|
-
processed_request = await self._update_request(request, forefront=forefront)
|
|
485
|
-
processed_request.unique_key = request.unique_key
|
|
486
|
-
|
|
487
|
-
# If the request was previously handled, decrement our handled count since
|
|
488
|
-
# we're putting it back for processing.
|
|
489
|
-
if request.was_already_handled and not processed_request.was_already_handled:
|
|
490
|
-
self._assumed_handled_count -= 1
|
|
491
|
-
|
|
492
|
-
# Update the cache
|
|
493
|
-
cache_key = request.unique_key
|
|
494
|
-
self._cache_request(
|
|
495
|
-
cache_key,
|
|
496
|
-
processed_request,
|
|
497
|
-
hydrated_request=request,
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
# If we're adding to the forefront, we need to check for forefront requests
|
|
501
|
-
# in the next list_head call
|
|
502
|
-
if forefront:
|
|
503
|
-
self._should_check_for_forefront_requests = True
|
|
504
|
-
|
|
505
|
-
# Try to release the lock on the request
|
|
506
|
-
try:
|
|
507
|
-
await self._delete_request_lock(request.unique_key, forefront=forefront)
|
|
508
|
-
except Exception as err:
|
|
509
|
-
logger.debug(f'Failed to delete request lock for request {request.unique_key}', exc_info=err)
|
|
510
|
-
except Exception as exc:
|
|
511
|
-
logger.debug(f'Error reclaiming request {request.unique_key}: {exc!s}')
|
|
512
|
-
return None
|
|
513
|
-
else:
|
|
514
|
-
return processed_request
|
|
146
|
+
return await self._implementation.reclaim_request(request, forefront=forefront)
|
|
515
147
|
|
|
516
148
|
@override
|
|
517
149
|
async def is_empty(self) -> bool:
|
|
@@ -520,268 +152,176 @@ class ApifyRequestQueueClient(RequestQueueClient):
|
|
|
520
152
|
Returns:
|
|
521
153
|
True if the queue is empty, False otherwise.
|
|
522
154
|
"""
|
|
523
|
-
|
|
524
|
-
# Without the lock the `is_empty` is prone to falsely report True with some low probability race condition.
|
|
525
|
-
async with self._fetch_lock:
|
|
526
|
-
head = await self._list_head(limit=1, lock_time=None)
|
|
527
|
-
return len(head.items) == 0 and not self._queue_has_locked_requests
|
|
528
|
-
|
|
529
|
-
async def _ensure_head_is_non_empty(self) -> None:
|
|
530
|
-
"""Ensure that the queue head has requests if they are available in the queue."""
|
|
531
|
-
# If queue head has adequate requests, skip fetching more
|
|
532
|
-
if len(self._queue_head) > 1 and not self._should_check_for_forefront_requests:
|
|
533
|
-
return
|
|
534
|
-
|
|
535
|
-
# Fetch requests from the API and populate the queue head
|
|
536
|
-
await self._list_head(lock_time=self._DEFAULT_LOCK_TIME)
|
|
537
|
-
|
|
538
|
-
async def _get_or_hydrate_request(self, unique_key: str) -> Request | None:
|
|
539
|
-
"""Get a request by unique key, either from cache or by fetching from API.
|
|
540
|
-
|
|
541
|
-
Args:
|
|
542
|
-
unique_key: Unique keu of the request to get.
|
|
543
|
-
|
|
544
|
-
Returns:
|
|
545
|
-
The request if found and valid, otherwise None.
|
|
546
|
-
"""
|
|
547
|
-
# First check if the request is in our cache
|
|
548
|
-
cached_entry = self._requests_cache.get(unique_key)
|
|
549
|
-
|
|
550
|
-
if cached_entry and cached_entry.hydrated:
|
|
551
|
-
# If we have the request hydrated in cache, check if lock is expired
|
|
552
|
-
if cached_entry.lock_expires_at and cached_entry.lock_expires_at < datetime.now(tz=timezone.utc):
|
|
553
|
-
# Try to prolong the lock if it's expired
|
|
554
|
-
try:
|
|
555
|
-
lock_secs = int(self._DEFAULT_LOCK_TIME.total_seconds())
|
|
556
|
-
response = await self._prolong_request_lock(unique_key, lock_secs=lock_secs)
|
|
557
|
-
cached_entry.lock_expires_at = response.lock_expires_at
|
|
558
|
-
except Exception:
|
|
559
|
-
# If prolonging the lock fails, we lost the request
|
|
560
|
-
logger.debug(f'Failed to prolong lock for request {unique_key}, returning None')
|
|
561
|
-
return None
|
|
562
|
-
|
|
563
|
-
return cached_entry.hydrated
|
|
564
|
-
|
|
565
|
-
# If not in cache or not hydrated, fetch the request
|
|
566
|
-
try:
|
|
567
|
-
# Try to acquire or prolong the lock
|
|
568
|
-
lock_secs = int(self._DEFAULT_LOCK_TIME.total_seconds())
|
|
569
|
-
await self._prolong_request_lock(unique_key, lock_secs=lock_secs)
|
|
570
|
-
|
|
571
|
-
# Fetch the request data
|
|
572
|
-
request = await self.get_request(unique_key)
|
|
573
|
-
|
|
574
|
-
# If request is not found, release lock and return None
|
|
575
|
-
if not request:
|
|
576
|
-
await self._delete_request_lock(unique_key)
|
|
577
|
-
return None
|
|
578
|
-
|
|
579
|
-
# Update cache with hydrated request
|
|
580
|
-
cache_key = request.unique_key
|
|
581
|
-
self._cache_request(
|
|
582
|
-
cache_key,
|
|
583
|
-
ProcessedRequest(
|
|
584
|
-
unique_key=request.unique_key,
|
|
585
|
-
was_already_present=True,
|
|
586
|
-
was_already_handled=request.handled_at is not None,
|
|
587
|
-
),
|
|
588
|
-
hydrated_request=request,
|
|
589
|
-
)
|
|
590
|
-
except Exception as exc:
|
|
591
|
-
logger.debug(f'Error fetching or locking request {unique_key}: {exc!s}')
|
|
592
|
-
return None
|
|
593
|
-
else:
|
|
594
|
-
return request
|
|
155
|
+
return await self._implementation.is_empty()
|
|
595
156
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
request
|
|
599
|
-
*,
|
|
600
|
-
forefront: bool = False,
|
|
601
|
-
) -> ProcessedRequest:
|
|
602
|
-
"""Update a request in the queue.
|
|
603
|
-
|
|
604
|
-
Args:
|
|
605
|
-
request: The updated request.
|
|
606
|
-
forefront: Whether to put the updated request in the beginning or the end of the queue.
|
|
157
|
+
@override
|
|
158
|
+
async def get_metadata(self) -> ApifyRequestQueueMetadata:
|
|
159
|
+
"""Get metadata about the request queue.
|
|
607
160
|
|
|
608
161
|
Returns:
|
|
609
|
-
|
|
162
|
+
Metadata from the API, merged with local estimation, because in some cases, the data from the API can
|
|
163
|
+
be delayed.
|
|
610
164
|
"""
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
165
|
+
response = await self._api_client.get()
|
|
166
|
+
if response is None:
|
|
167
|
+
raise ValueError('Failed to fetch request queue metadata from the API.')
|
|
168
|
+
# Enhance API response by local estimations (API can be delayed few seconds, while local estimation not.)
|
|
169
|
+
return ApifyRequestQueueMetadata(
|
|
170
|
+
id=response['id'],
|
|
171
|
+
name=response['name'],
|
|
172
|
+
total_request_count=max(response['totalRequestCount'], self._metadata.total_request_count),
|
|
173
|
+
handled_request_count=max(response['handledRequestCount'], self._metadata.handled_request_count),
|
|
174
|
+
pending_request_count=response['pendingRequestCount'],
|
|
175
|
+
created_at=min(response['createdAt'], self._metadata.created_at),
|
|
176
|
+
modified_at=max(response['modifiedAt'], self._metadata.modified_at),
|
|
177
|
+
accessed_at=max(response['accessedAt'], self._metadata.accessed_at),
|
|
178
|
+
had_multiple_clients=response['hadMultipleClients'] or self._metadata.had_multiple_clients,
|
|
179
|
+
stats=RequestQueueStats.model_validate(response['stats'], by_alias=True),
|
|
620
180
|
)
|
|
621
181
|
|
|
622
|
-
|
|
623
|
-
|
|
182
|
+
@classmethod
|
|
183
|
+
async def open(
|
|
184
|
+
cls,
|
|
624
185
|
*,
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
186
|
+
id: str | None,
|
|
187
|
+
name: str | None,
|
|
188
|
+
alias: str | None,
|
|
189
|
+
configuration: Configuration,
|
|
190
|
+
access: Literal['single', 'shared'] = 'single',
|
|
191
|
+
) -> ApifyRequestQueueClient:
|
|
192
|
+
"""Open an Apify request queue client.
|
|
193
|
+
|
|
194
|
+
This method creates and initializes a new instance of the Apify request queue client. It handles
|
|
195
|
+
authentication, storage lookup/creation, and metadata retrieval, and sets up internal caching and queue
|
|
196
|
+
management structures.
|
|
629
197
|
|
|
630
198
|
Args:
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
199
|
+
id: The ID of the RQ to open. If provided, searches for existing RQ by ID.
|
|
200
|
+
Mutually exclusive with name and alias.
|
|
201
|
+
name: The name of the RQ to open (global scope, persists across runs).
|
|
202
|
+
Mutually exclusive with id and alias.
|
|
203
|
+
alias: The alias of the RQ to open (run scope, creates unnamed storage).
|
|
204
|
+
Mutually exclusive with id and name.
|
|
205
|
+
configuration: The configuration object containing API credentials and settings. Must include a valid
|
|
206
|
+
`token` and `api_base_url`. May also contain a `default_request_queue_id` for fallback when neither
|
|
207
|
+
`id`, `name`, nor `alias` is provided.
|
|
208
|
+
access: Controls the implementation of the request queue client based on expected scenario:
|
|
209
|
+
- 'single' is suitable for single consumer scenarios. It makes less API calls, is cheaper and faster.
|
|
210
|
+
- 'shared' is suitable for multiple consumers scenarios at the cost of higher API usage.
|
|
211
|
+
Detailed constraints for the 'single' access type:
|
|
212
|
+
- Only one client is consuming the request queue at the time.
|
|
213
|
+
- Multiple producers can put requests to the queue, but their forefront requests are not guaranteed to
|
|
214
|
+
be handled so quickly as this client does not aggressively fetch the forefront and relies on local
|
|
215
|
+
head estimation.
|
|
216
|
+
- Requests are only added to the queue, never deleted by other clients. (Marking as handled is ok.)
|
|
217
|
+
- Other producers can add new requests, but not modify existing ones.
|
|
218
|
+
(Modifications would not be included in local cache)
|
|
634
219
|
|
|
635
220
|
Returns:
|
|
636
|
-
|
|
221
|
+
An instance for the opened or created storage client.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValueError: If the configuration is missing required fields (token, api_base_url), if more than one of
|
|
225
|
+
`id`, `name`, or `alias` is provided, or if none are provided and no default storage ID is available
|
|
226
|
+
in the configuration.
|
|
637
227
|
"""
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
logger.debug(f'Using cached queue head with {len(self._queue_head)} requests')
|
|
641
|
-
# Create a list of requests from the cached queue head
|
|
642
|
-
items = []
|
|
643
|
-
for unique_key in list(self._queue_head)[:limit]:
|
|
644
|
-
cached_request = self._requests_cache.get(unique_key)
|
|
645
|
-
if cached_request and cached_request.hydrated:
|
|
646
|
-
items.append(cached_request.hydrated)
|
|
647
|
-
|
|
648
|
-
metadata = await self.get_metadata()
|
|
649
|
-
|
|
650
|
-
return RequestQueueHead(
|
|
651
|
-
limit=limit,
|
|
652
|
-
had_multiple_clients=metadata.had_multiple_clients,
|
|
653
|
-
queue_modified_at=metadata.modified_at,
|
|
654
|
-
items=items,
|
|
655
|
-
queue_has_locked_requests=self._queue_has_locked_requests,
|
|
656
|
-
lock_time=lock_time,
|
|
657
|
-
)
|
|
658
|
-
leftover_buffer = list[str]()
|
|
659
|
-
if self._should_check_for_forefront_requests:
|
|
660
|
-
leftover_buffer = list(self._queue_head)
|
|
661
|
-
self._queue_head.clear()
|
|
662
|
-
self._should_check_for_forefront_requests = False
|
|
663
|
-
|
|
664
|
-
# Otherwise fetch from API
|
|
665
|
-
lock_time = lock_time or self._DEFAULT_LOCK_TIME
|
|
666
|
-
lock_secs = int(lock_time.total_seconds())
|
|
667
|
-
|
|
668
|
-
response = await self._api_client.list_and_lock_head(
|
|
669
|
-
lock_secs=lock_secs,
|
|
670
|
-
limit=limit,
|
|
671
|
-
)
|
|
228
|
+
if sum(1 for param in [id, name, alias] if param is not None) > 1:
|
|
229
|
+
raise ValueError('Only one of "id", "name", or "alias" can be specified, not multiple.')
|
|
672
230
|
|
|
673
|
-
|
|
674
|
-
|
|
231
|
+
token = configuration.token
|
|
232
|
+
if not token:
|
|
233
|
+
raise ValueError(f'Apify storage client requires a valid token in Configuration (token={token}).')
|
|
675
234
|
|
|
676
|
-
|
|
677
|
-
|
|
235
|
+
api_url = configuration.api_base_url
|
|
236
|
+
if not api_url:
|
|
237
|
+
raise ValueError(f'Apify storage client requires a valid API URL in Configuration (api_url={api_url}).')
|
|
678
238
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
'unique_key': request.unique_key,
|
|
685
|
-
},
|
|
686
|
-
)
|
|
687
|
-
continue
|
|
688
|
-
|
|
689
|
-
# Cache the request
|
|
690
|
-
self._cache_request(
|
|
691
|
-
request.unique_key,
|
|
692
|
-
ProcessedRequest(
|
|
693
|
-
unique_key=request.unique_key,
|
|
694
|
-
was_already_present=True,
|
|
695
|
-
was_already_handled=False,
|
|
696
|
-
),
|
|
697
|
-
hydrated_request=request,
|
|
239
|
+
api_public_base_url = configuration.api_public_base_url
|
|
240
|
+
if not api_public_base_url:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
'Apify storage client requires a valid API public base URL in Configuration '
|
|
243
|
+
f'(api_public_base_url={api_public_base_url}).'
|
|
698
244
|
)
|
|
699
|
-
self._queue_head.append(request.unique_key)
|
|
700
245
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
246
|
+
# Create Apify client with the provided token and API URL.
|
|
247
|
+
apify_client_async = ApifyClientAsync(
|
|
248
|
+
token=token,
|
|
249
|
+
api_url=api_url,
|
|
250
|
+
max_retries=8,
|
|
251
|
+
min_delay_between_retries_millis=500,
|
|
252
|
+
timeout_secs=360,
|
|
253
|
+
)
|
|
254
|
+
apify_rqs_client = apify_client_async.request_queues()
|
|
705
255
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
256
|
+
# Normalize unnamed default storage in cases where not defined in `configuration.default_request_queue_id` to
|
|
257
|
+
# unnamed storage aliased as `__default__`
|
|
258
|
+
if not any([alias, name, id, configuration.default_request_queue_id]):
|
|
259
|
+
alias = '__default__'
|
|
260
|
+
|
|
261
|
+
if alias:
|
|
262
|
+
# Check if there is pre-existing alias mapping in the default KVS.
|
|
263
|
+
async with AliasResolver(storage_type=RequestQueue, alias=alias, configuration=configuration) as _alias:
|
|
264
|
+
id = await _alias.resolve_id()
|
|
265
|
+
|
|
266
|
+
# There was no pre-existing alias in the mapping.
|
|
267
|
+
# Create a new unnamed storage and store the mapping.
|
|
268
|
+
if id is None:
|
|
269
|
+
new_storage_metadata = RequestQueueMetadata.model_validate(
|
|
270
|
+
await apify_rqs_client.get_or_create(),
|
|
271
|
+
)
|
|
272
|
+
id = new_storage_metadata.id
|
|
273
|
+
await _alias.store_mapping(storage_id=id)
|
|
713
274
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
275
|
+
# If name is provided, get or create the storage by name.
|
|
276
|
+
elif name:
|
|
277
|
+
id = RequestQueueMetadata.model_validate(
|
|
278
|
+
await apify_rqs_client.get_or_create(name=name),
|
|
279
|
+
).id
|
|
717
280
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
lock_secs=lock_secs,
|
|
727
|
-
)
|
|
281
|
+
# If none are provided, try to get the default storage ID from environment variables.
|
|
282
|
+
elif id is None:
|
|
283
|
+
id = configuration.default_request_queue_id
|
|
284
|
+
if not id:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
'RequestQueue "id", "name", or "alias" must be specified, '
|
|
287
|
+
'or a default default_request_queue_id ID must be set in the configuration.'
|
|
288
|
+
)
|
|
728
289
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
290
|
+
# Use suitable client_key to make `hadMultipleClients` response of Apify API useful.
|
|
291
|
+
# It should persist across migrated or resurrected Actor runs on the Apify platform.
|
|
292
|
+
_api_max_client_key_length = 32
|
|
293
|
+
client_key = (configuration.actor_run_id or crypto_random_object_id(length=_api_max_client_key_length))[
|
|
294
|
+
:_api_max_client_key_length
|
|
295
|
+
]
|
|
732
296
|
|
|
733
|
-
|
|
734
|
-
for cached_request in self._requests_cache.values():
|
|
735
|
-
if cached_request.unique_key == unique_key:
|
|
736
|
-
cached_request.lock_expires_at = result.lock_expires_at
|
|
737
|
-
break
|
|
297
|
+
apify_rq_client = apify_client_async.request_queue(request_queue_id=id, client_key=client_key)
|
|
738
298
|
|
|
739
|
-
|
|
299
|
+
# Fetch its metadata.
|
|
300
|
+
metadata = await apify_rq_client.get()
|
|
740
301
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
"""Delete the lock on a specific request in the queue.
|
|
302
|
+
# If metadata is None, it means the storage does not exist, so we create it.
|
|
303
|
+
if metadata is None:
|
|
304
|
+
id = RequestQueueMetadata.model_validate(
|
|
305
|
+
await apify_rqs_client.get_or_create(),
|
|
306
|
+
).id
|
|
307
|
+
apify_rq_client = apify_client_async.request_queue(request_queue_id=id, client_key=client_key)
|
|
748
308
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
try:
|
|
754
|
-
await self._api_client.delete_request_lock(
|
|
755
|
-
request_id=unique_key_to_request_id(unique_key),
|
|
756
|
-
forefront=forefront,
|
|
757
|
-
)
|
|
309
|
+
# Verify that the storage exists by fetching its metadata again.
|
|
310
|
+
metadata = await apify_rq_client.get()
|
|
311
|
+
if metadata is None:
|
|
312
|
+
raise ValueError(f'Opening request queue with id={id}, name={name}, and alias={alias} failed.')
|
|
758
313
|
|
|
759
|
-
|
|
760
|
-
for cached_request in self._requests_cache.values():
|
|
761
|
-
if cached_request.unique_key == unique_key:
|
|
762
|
-
cached_request.lock_expires_at = None
|
|
763
|
-
break
|
|
764
|
-
except Exception as err:
|
|
765
|
-
logger.debug(f'Failed to delete request lock for request {unique_key}', exc_info=err)
|
|
314
|
+
metadata_model = RequestQueueMetadata.model_validate(metadata)
|
|
766
315
|
|
|
767
|
-
|
|
768
|
-
self,
|
|
769
|
-
cache_key: str,
|
|
770
|
-
processed_request: ProcessedRequest,
|
|
771
|
-
*,
|
|
772
|
-
hydrated_request: Request | None = None,
|
|
773
|
-
) -> None:
|
|
774
|
-
"""Cache a request for future use.
|
|
316
|
+
return cls(api_client=apify_rq_client, metadata=metadata_model, access=access)
|
|
775
317
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
"""
|
|
782
|
-
self._requests_cache[cache_key] = CachedRequest(
|
|
783
|
-
unique_key=processed_request.unique_key,
|
|
784
|
-
was_already_handled=processed_request.was_already_handled,
|
|
785
|
-
hydrated=hydrated_request,
|
|
786
|
-
lock_expires_at=None,
|
|
318
|
+
@override
|
|
319
|
+
async def purge(self) -> None:
|
|
320
|
+
raise NotImplementedError(
|
|
321
|
+
'Purging the request queue is not supported in the Apify platform. '
|
|
322
|
+
'Use the `drop` method to delete the request queue instead.'
|
|
787
323
|
)
|
|
324
|
+
|
|
325
|
+
@override
|
|
326
|
+
async def drop(self) -> None:
|
|
327
|
+
await self._api_client.delete()
|