scrapely-client-python 1.0.0__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 scrapely-client-python might be problematic. Click here for more details.
- scrapely_client/__init__.py +30 -0
- scrapely_client/_charging.py +186 -0
- scrapely_client/_client_registry.py +136 -0
- scrapely_client/_consts.py +96 -0
- scrapely_client/_docs.py +37 -0
- scrapely_client/_literals.py +539 -0
- scrapely_client/_logging.py +180 -0
- scrapely_client/_models.py +4172 -0
- scrapely_client/_pagination.py +220 -0
- scrapely_client/_resource_clients/__init__.py +84 -0
- scrapely_client/_resource_clients/_resource_client.py +567 -0
- scrapely_client/_resource_clients/actor.py +1042 -0
- scrapely_client/_resource_clients/actor_collection.py +377 -0
- scrapely_client/_resource_clients/actor_env_var.py +154 -0
- scrapely_client/_resource_clients/actor_env_var_collection.py +165 -0
- scrapely_client/_resource_clients/actor_version.py +254 -0
- scrapely_client/_resource_clients/actor_version_collection.py +232 -0
- scrapely_client/_resource_clients/build.py +244 -0
- scrapely_client/_resource_clients/build_collection.py +164 -0
- scrapely_client/_resource_clients/dataset.py +1151 -0
- scrapely_client/_resource_clients/dataset_collection.py +244 -0
- scrapely_client/_resource_clients/key_value_store.py +906 -0
- scrapely_client/_resource_clients/key_value_store_collection.py +243 -0
- scrapely_client/_resource_clients/log.py +225 -0
- scrapely_client/_resource_clients/request_queue.py +1167 -0
- scrapely_client/_resource_clients/request_queue_collection.py +240 -0
- scrapely_client/_resource_clients/run.py +875 -0
- scrapely_client/_resource_clients/run_collection.py +226 -0
- scrapely_client/_resource_clients/schedule.py +243 -0
- scrapely_client/_resource_clients/schedule_collection.py +260 -0
- scrapely_client/_resource_clients/store_collection.py +242 -0
- scrapely_client/_resource_clients/task.py +793 -0
- scrapely_client/_resource_clients/task_collection.py +322 -0
- scrapely_client/_resource_clients/user.py +262 -0
- scrapely_client/_resource_clients/webhook.py +292 -0
- scrapely_client/_resource_clients/webhook_collection.py +281 -0
- scrapely_client/_resource_clients/webhook_dispatch.py +84 -0
- scrapely_client/_resource_clients/webhook_dispatch_collection.py +158 -0
- scrapely_client/_scrapely_client.py +837 -0
- scrapely_client/_statistics.py +29 -0
- scrapely_client/_status_message_watcher.py +197 -0
- scrapely_client/_streamed_log.py +250 -0
- scrapely_client/_typeddicts.py +408 -0
- scrapely_client/_utils/__init__.py +0 -0
- scrapely_client/_utils/crypto.py +87 -0
- scrapely_client/_utils/encoding.py +133 -0
- scrapely_client/_utils/errors.py +33 -0
- scrapely_client/_utils/http.py +137 -0
- scrapely_client/_utils/time.py +32 -0
- scrapely_client/_utils/try_import.py +61 -0
- scrapely_client/errors.py +190 -0
- scrapely_client/http_clients/__init__.py +10 -0
- scrapely_client/http_clients/_base.py +430 -0
- scrapely_client/http_clients/_impit.py +598 -0
- scrapely_client/http_compressors/__init__.py +18 -0
- scrapely_client/http_compressors/_base.py +25 -0
- scrapely_client/http_compressors/_brotli.py +38 -0
- scrapely_client/http_compressors/_gzip.py +38 -0
- scrapely_client/http_compressors/_resolve.py +38 -0
- scrapely_client/py.typed +0 -0
- scrapely_client/types.py +52 -0
- scrapely_client_python-1.0.0.dist-info/METADATA +244 -0
- scrapely_client_python-1.0.0.dist-info/RECORD +65 -0
- scrapely_client_python-1.0.0.dist-info/WHEEL +4 -0
- scrapely_client_python-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# generated by datamodel-codegen
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, NotRequired, TypeAlias, TypedDict
|
|
6
|
+
|
|
7
|
+
from scrapely_client._docs import docs_group
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@docs_group('Typed dicts')
|
|
11
|
+
class RequestBaseDict(TypedDict):
|
|
12
|
+
unique_key: NotRequired[str]
|
|
13
|
+
"""
|
|
14
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
15
|
+
"""
|
|
16
|
+
url: NotRequired[str]
|
|
17
|
+
"""
|
|
18
|
+
The URL of the request.
|
|
19
|
+
"""
|
|
20
|
+
method: NotRequired[Literal['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']]
|
|
21
|
+
retry_count: NotRequired[int]
|
|
22
|
+
"""
|
|
23
|
+
The number of times this request has been retried.
|
|
24
|
+
"""
|
|
25
|
+
loaded_url: NotRequired[str | None]
|
|
26
|
+
"""
|
|
27
|
+
The final URL that was loaded, after redirects (if any).
|
|
28
|
+
"""
|
|
29
|
+
payload: NotRequired[str | dict[str, Any] | None]
|
|
30
|
+
"""
|
|
31
|
+
The request payload, typically used with POST or PUT requests.
|
|
32
|
+
"""
|
|
33
|
+
headers: NotRequired[dict[str, Any] | None]
|
|
34
|
+
"""
|
|
35
|
+
HTTP headers sent with the request.
|
|
36
|
+
"""
|
|
37
|
+
user_data: NotRequired[RequestUserDataDict]
|
|
38
|
+
no_retry: NotRequired[bool | None]
|
|
39
|
+
"""
|
|
40
|
+
Indicates whether the request should not be retried if processing fails.
|
|
41
|
+
"""
|
|
42
|
+
error_messages: NotRequired[list[str] | None]
|
|
43
|
+
"""
|
|
44
|
+
Error messages recorded from failed processing attempts.
|
|
45
|
+
"""
|
|
46
|
+
handled_at: NotRequired[str | None]
|
|
47
|
+
"""
|
|
48
|
+
The timestamp when the request was marked as handled, if applicable.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@docs_group('Typed dicts')
|
|
53
|
+
class RequestBaseCamelDict(TypedDict):
|
|
54
|
+
uniqueKey: NotRequired[str]
|
|
55
|
+
"""
|
|
56
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
57
|
+
"""
|
|
58
|
+
url: NotRequired[str]
|
|
59
|
+
"""
|
|
60
|
+
The URL of the request.
|
|
61
|
+
"""
|
|
62
|
+
method: NotRequired[Literal['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']]
|
|
63
|
+
retryCount: NotRequired[int]
|
|
64
|
+
"""
|
|
65
|
+
The number of times this request has been retried.
|
|
66
|
+
"""
|
|
67
|
+
loadedUrl: NotRequired[str | None]
|
|
68
|
+
"""
|
|
69
|
+
The final URL that was loaded, after redirects (if any).
|
|
70
|
+
"""
|
|
71
|
+
payload: NotRequired[str | dict[str, Any] | None]
|
|
72
|
+
"""
|
|
73
|
+
The request payload, typically used with POST or PUT requests.
|
|
74
|
+
"""
|
|
75
|
+
headers: NotRequired[dict[str, Any] | None]
|
|
76
|
+
"""
|
|
77
|
+
HTTP headers sent with the request.
|
|
78
|
+
"""
|
|
79
|
+
userData: NotRequired[RequestUserDataCamelDict]
|
|
80
|
+
noRetry: NotRequired[bool | None]
|
|
81
|
+
"""
|
|
82
|
+
Indicates whether the request should not be retried if processing fails.
|
|
83
|
+
"""
|
|
84
|
+
errorMessages: NotRequired[list[str] | None]
|
|
85
|
+
"""
|
|
86
|
+
Error messages recorded from failed processing attempts.
|
|
87
|
+
"""
|
|
88
|
+
handledAt: NotRequired[str | None]
|
|
89
|
+
"""
|
|
90
|
+
The timestamp when the request was marked as handled, if applicable.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@docs_group('Typed dicts')
|
|
95
|
+
class RequestDict(RequestBaseDict):
|
|
96
|
+
"""A request stored in the request queue, including its metadata and processing state."""
|
|
97
|
+
|
|
98
|
+
id: NotRequired[str]
|
|
99
|
+
"""
|
|
100
|
+
A unique identifier assigned to the request.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@docs_group('Typed dicts')
|
|
105
|
+
class RequestCamelDict(RequestBaseCamelDict):
|
|
106
|
+
"""A request stored in the request queue, including its metadata and processing state."""
|
|
107
|
+
|
|
108
|
+
id: NotRequired[str]
|
|
109
|
+
"""
|
|
110
|
+
A unique identifier assigned to the request.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@docs_group('Typed dicts')
|
|
115
|
+
class RequestDraftDict(TypedDict):
|
|
116
|
+
"""A request that failed to be processed during a request queue operation and can be retried."""
|
|
117
|
+
|
|
118
|
+
id: NotRequired[str]
|
|
119
|
+
"""
|
|
120
|
+
A unique identifier assigned to the request.
|
|
121
|
+
"""
|
|
122
|
+
unique_key: str
|
|
123
|
+
"""
|
|
124
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
125
|
+
"""
|
|
126
|
+
url: str
|
|
127
|
+
"""
|
|
128
|
+
The URL of the request.
|
|
129
|
+
"""
|
|
130
|
+
method: NotRequired[Literal['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@docs_group('Typed dicts')
|
|
134
|
+
class RequestDraftCamelDict(TypedDict):
|
|
135
|
+
"""A request that failed to be processed during a request queue operation and can be retried."""
|
|
136
|
+
|
|
137
|
+
id: NotRequired[str]
|
|
138
|
+
"""
|
|
139
|
+
A unique identifier assigned to the request.
|
|
140
|
+
"""
|
|
141
|
+
uniqueKey: str
|
|
142
|
+
"""
|
|
143
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
144
|
+
"""
|
|
145
|
+
url: str
|
|
146
|
+
"""
|
|
147
|
+
The URL of the request.
|
|
148
|
+
"""
|
|
149
|
+
method: NotRequired[Literal['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@docs_group('Typed dicts')
|
|
153
|
+
class RequestDraftDeleteByIdDict(TypedDict):
|
|
154
|
+
"""A request that should be deleted, identified by its ID."""
|
|
155
|
+
|
|
156
|
+
id: str
|
|
157
|
+
"""
|
|
158
|
+
A unique identifier assigned to the request.
|
|
159
|
+
"""
|
|
160
|
+
unique_key: NotRequired[str]
|
|
161
|
+
"""
|
|
162
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@docs_group('Typed dicts')
|
|
167
|
+
class RequestDraftDeleteByIdCamelDict(TypedDict):
|
|
168
|
+
"""A request that should be deleted, identified by its ID."""
|
|
169
|
+
|
|
170
|
+
id: str
|
|
171
|
+
"""
|
|
172
|
+
A unique identifier assigned to the request.
|
|
173
|
+
"""
|
|
174
|
+
uniqueKey: NotRequired[str]
|
|
175
|
+
"""
|
|
176
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@docs_group('Typed dicts')
|
|
181
|
+
class RequestDraftDeleteByUniqueKeyDict(TypedDict):
|
|
182
|
+
"""A request that should be deleted, identified by its unique key."""
|
|
183
|
+
|
|
184
|
+
id: NotRequired[str]
|
|
185
|
+
"""
|
|
186
|
+
A unique identifier assigned to the request.
|
|
187
|
+
"""
|
|
188
|
+
unique_key: str
|
|
189
|
+
"""
|
|
190
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@docs_group('Typed dicts')
|
|
195
|
+
class RequestDraftDeleteByUniqueKeyCamelDict(TypedDict):
|
|
196
|
+
"""A request that should be deleted, identified by its unique key."""
|
|
197
|
+
|
|
198
|
+
id: NotRequired[str]
|
|
199
|
+
"""
|
|
200
|
+
A unique identifier assigned to the request.
|
|
201
|
+
"""
|
|
202
|
+
uniqueKey: str
|
|
203
|
+
"""
|
|
204
|
+
A unique key used for request de-duplication. Requests with the same unique key are considered identical.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
RequestDraftDeleteDict: TypeAlias = RequestDraftDeleteByIdDict | RequestDraftDeleteByUniqueKeyDict
|
|
209
|
+
"""
|
|
210
|
+
A request that should be deleted.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
RequestDraftDeleteCamelDict: TypeAlias = RequestDraftDeleteByIdCamelDict | RequestDraftDeleteByUniqueKeyCamelDict
|
|
214
|
+
"""
|
|
215
|
+
A request that should be deleted.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
RequestUserDataDict: TypeAlias = dict[str, Any]
|
|
220
|
+
|
|
221
|
+
RequestUserDataCamelDict: TypeAlias = dict[str, Any]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
TaskInputDict: TypeAlias = dict[str, Any]
|
|
225
|
+
|
|
226
|
+
TaskInputCamelDict: TypeAlias = dict[str, Any]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@docs_group('Typed dicts')
|
|
230
|
+
class WebhookConditionDict(TypedDict):
|
|
231
|
+
actor_id: NotRequired[str | None]
|
|
232
|
+
actor_task_id: NotRequired[str | None]
|
|
233
|
+
actor_run_id: NotRequired[str | None]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@docs_group('Typed dicts')
|
|
237
|
+
class WebhookConditionCamelDict(TypedDict):
|
|
238
|
+
actorId: NotRequired[str | None]
|
|
239
|
+
actorTaskId: NotRequired[str | None]
|
|
240
|
+
actorRunId: NotRequired[str | None]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@docs_group('Typed dicts')
|
|
244
|
+
class WebhookCreateDict(TypedDict):
|
|
245
|
+
is_ad_hoc: NotRequired[bool | None]
|
|
246
|
+
event_types: list[
|
|
247
|
+
Literal[
|
|
248
|
+
'ACTOR.BUILD.ABORTED',
|
|
249
|
+
'ACTOR.BUILD.CREATED',
|
|
250
|
+
'ACTOR.BUILD.FAILED',
|
|
251
|
+
'ACTOR.BUILD.SUCCEEDED',
|
|
252
|
+
'ACTOR.BUILD.TIMED_OUT',
|
|
253
|
+
'ACTOR.RUN.ABORTED',
|
|
254
|
+
'ACTOR.RUN.CREATED',
|
|
255
|
+
'ACTOR.RUN.FAILED',
|
|
256
|
+
'ACTOR.RUN.RESURRECTED',
|
|
257
|
+
'ACTOR.RUN.SUCCEEDED',
|
|
258
|
+
'ACTOR.RUN.TIMED_OUT',
|
|
259
|
+
'TEST',
|
|
260
|
+
]
|
|
261
|
+
]
|
|
262
|
+
condition: WebhookConditionDict
|
|
263
|
+
idempotency_key: NotRequired[str | None]
|
|
264
|
+
ignore_ssl_errors: NotRequired[bool | None]
|
|
265
|
+
do_not_retry: NotRequired[bool | None]
|
|
266
|
+
request_url: str
|
|
267
|
+
payload_template: NotRequired[str | None]
|
|
268
|
+
headers_template: NotRequired[str | None]
|
|
269
|
+
description: NotRequired[str | None]
|
|
270
|
+
should_interpolate_strings: NotRequired[bool | None]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@docs_group('Typed dicts')
|
|
274
|
+
class WebhookCreateCamelDict(TypedDict):
|
|
275
|
+
isAdHoc: NotRequired[bool | None]
|
|
276
|
+
eventTypes: list[
|
|
277
|
+
Literal[
|
|
278
|
+
'ACTOR.BUILD.ABORTED',
|
|
279
|
+
'ACTOR.BUILD.CREATED',
|
|
280
|
+
'ACTOR.BUILD.FAILED',
|
|
281
|
+
'ACTOR.BUILD.SUCCEEDED',
|
|
282
|
+
'ACTOR.BUILD.TIMED_OUT',
|
|
283
|
+
'ACTOR.RUN.ABORTED',
|
|
284
|
+
'ACTOR.RUN.CREATED',
|
|
285
|
+
'ACTOR.RUN.FAILED',
|
|
286
|
+
'ACTOR.RUN.RESURRECTED',
|
|
287
|
+
'ACTOR.RUN.SUCCEEDED',
|
|
288
|
+
'ACTOR.RUN.TIMED_OUT',
|
|
289
|
+
'TEST',
|
|
290
|
+
]
|
|
291
|
+
]
|
|
292
|
+
condition: WebhookConditionCamelDict
|
|
293
|
+
idempotencyKey: NotRequired[str | None]
|
|
294
|
+
ignoreSslErrors: NotRequired[bool | None]
|
|
295
|
+
doNotRetry: NotRequired[bool | None]
|
|
296
|
+
requestUrl: str
|
|
297
|
+
payloadTemplate: NotRequired[str | None]
|
|
298
|
+
headersTemplate: NotRequired[str | None]
|
|
299
|
+
description: NotRequired[str | None]
|
|
300
|
+
shouldInterpolateStrings: NotRequired[bool | None]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@docs_group('Typed dicts')
|
|
304
|
+
class WebhookRepresentationDict(TypedDict):
|
|
305
|
+
"""Minimal representation of an ad-hoc webhook attached to a single Actor run or build via the
|
|
306
|
+
`webhooks` query parameter. The query parameter value is a Base64-encoded JSON array whose
|
|
307
|
+
items match this schema. Persistent webhook fields (e.g. `condition`) are not used here.
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
event_types: list[
|
|
312
|
+
Literal[
|
|
313
|
+
'ACTOR.BUILD.ABORTED',
|
|
314
|
+
'ACTOR.BUILD.CREATED',
|
|
315
|
+
'ACTOR.BUILD.FAILED',
|
|
316
|
+
'ACTOR.BUILD.SUCCEEDED',
|
|
317
|
+
'ACTOR.BUILD.TIMED_OUT',
|
|
318
|
+
'ACTOR.RUN.ABORTED',
|
|
319
|
+
'ACTOR.RUN.CREATED',
|
|
320
|
+
'ACTOR.RUN.FAILED',
|
|
321
|
+
'ACTOR.RUN.RESURRECTED',
|
|
322
|
+
'ACTOR.RUN.SUCCEEDED',
|
|
323
|
+
'ACTOR.RUN.TIMED_OUT',
|
|
324
|
+
'TEST',
|
|
325
|
+
]
|
|
326
|
+
]
|
|
327
|
+
request_url: str
|
|
328
|
+
"""
|
|
329
|
+
The URL to which the webhook sends its payload.
|
|
330
|
+
"""
|
|
331
|
+
payload_template: NotRequired[str | None]
|
|
332
|
+
"""
|
|
333
|
+
Optional template for the JSON payload sent by the webhook.
|
|
334
|
+
"""
|
|
335
|
+
headers_template: NotRequired[str | None]
|
|
336
|
+
"""
|
|
337
|
+
Optional template for the HTTP headers sent by the webhook.
|
|
338
|
+
"""
|
|
339
|
+
should_interpolate_strings: NotRequired[bool | None]
|
|
340
|
+
"""
|
|
341
|
+
Flag to also interpolate `{{...}}` variables inside string values of the payload and headers templates.
|
|
342
|
+
"""
|
|
343
|
+
idempotency_key: NotRequired[str | None]
|
|
344
|
+
"""
|
|
345
|
+
Key that prevents creating duplicate webhooks, e.g. when the run-starting request is retried.
|
|
346
|
+
"""
|
|
347
|
+
ignore_ssl_errors: NotRequired[bool | None]
|
|
348
|
+
"""
|
|
349
|
+
Flag to ignore SSL errors when the webhook sends the request.
|
|
350
|
+
"""
|
|
351
|
+
do_not_retry: NotRequired[bool | None]
|
|
352
|
+
"""
|
|
353
|
+
Flag to skip retrying the webhook request on failure.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@docs_group('Typed dicts')
|
|
358
|
+
class WebhookRepresentationCamelDict(TypedDict):
|
|
359
|
+
"""Minimal representation of an ad-hoc webhook attached to a single Actor run or build via the
|
|
360
|
+
`webhooks` query parameter. The query parameter value is a Base64-encoded JSON array whose
|
|
361
|
+
items match this schema. Persistent webhook fields (e.g. `condition`) are not used here.
|
|
362
|
+
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
eventTypes: list[
|
|
366
|
+
Literal[
|
|
367
|
+
'ACTOR.BUILD.ABORTED',
|
|
368
|
+
'ACTOR.BUILD.CREATED',
|
|
369
|
+
'ACTOR.BUILD.FAILED',
|
|
370
|
+
'ACTOR.BUILD.SUCCEEDED',
|
|
371
|
+
'ACTOR.BUILD.TIMED_OUT',
|
|
372
|
+
'ACTOR.RUN.ABORTED',
|
|
373
|
+
'ACTOR.RUN.CREATED',
|
|
374
|
+
'ACTOR.RUN.FAILED',
|
|
375
|
+
'ACTOR.RUN.RESURRECTED',
|
|
376
|
+
'ACTOR.RUN.SUCCEEDED',
|
|
377
|
+
'ACTOR.RUN.TIMED_OUT',
|
|
378
|
+
'TEST',
|
|
379
|
+
]
|
|
380
|
+
]
|
|
381
|
+
requestUrl: str
|
|
382
|
+
"""
|
|
383
|
+
The URL to which the webhook sends its payload.
|
|
384
|
+
"""
|
|
385
|
+
payloadTemplate: NotRequired[str | None]
|
|
386
|
+
"""
|
|
387
|
+
Optional template for the JSON payload sent by the webhook.
|
|
388
|
+
"""
|
|
389
|
+
headersTemplate: NotRequired[str | None]
|
|
390
|
+
"""
|
|
391
|
+
Optional template for the HTTP headers sent by the webhook.
|
|
392
|
+
"""
|
|
393
|
+
shouldInterpolateStrings: NotRequired[bool | None]
|
|
394
|
+
"""
|
|
395
|
+
Flag to also interpolate `{{...}}` variables inside string values of the payload and headers templates.
|
|
396
|
+
"""
|
|
397
|
+
idempotencyKey: NotRequired[str | None]
|
|
398
|
+
"""
|
|
399
|
+
Key that prevents creating duplicate webhooks, e.g. when the run-starting request is retried.
|
|
400
|
+
"""
|
|
401
|
+
ignoreSslErrors: NotRequired[bool | None]
|
|
402
|
+
"""
|
|
403
|
+
Flag to ignore SSL errors when the webhook sends the request.
|
|
404
|
+
"""
|
|
405
|
+
doNotRetry: NotRequired[bool | None]
|
|
406
|
+
"""
|
|
407
|
+
Flag to skip retrying the webhook request on failure.
|
|
408
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import string
|
|
6
|
+
import time
|
|
7
|
+
from base64 import urlsafe_b64encode
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from scrapely_client._utils.time import to_seconds
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
|
|
15
|
+
_BASE62_CHARSET = string.digits + string.ascii_letters
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def encode_base62(num: int) -> str:
|
|
19
|
+
"""Encode an integer to a base62 string.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
num: The number to encode.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The base62-encoded string.
|
|
26
|
+
"""
|
|
27
|
+
if num == 0:
|
|
28
|
+
return _BASE62_CHARSET[0]
|
|
29
|
+
|
|
30
|
+
# Use list to build result for O(n) complexity instead of O(n^2) string concatenation.
|
|
31
|
+
parts = []
|
|
32
|
+
while num > 0:
|
|
33
|
+
num, remainder = divmod(num, 62)
|
|
34
|
+
parts.append(_BASE62_CHARSET[remainder])
|
|
35
|
+
|
|
36
|
+
# Reverse and join once at the end.
|
|
37
|
+
return ''.join(reversed(parts))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_hmac_signature(secret_key: str, message: str) -> str:
|
|
41
|
+
"""Generate an HMAC-SHA256 signature and encode it using base62.
|
|
42
|
+
|
|
43
|
+
The HMAC signature is truncated to 30 characters and then encoded in base62 to reduce the signature length.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
secret_key: The secret key used for signing.
|
|
47
|
+
message: The message to be signed.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The base62-encoded signature.
|
|
51
|
+
"""
|
|
52
|
+
signature = hmac.new(secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()[:30]
|
|
53
|
+
|
|
54
|
+
decimal_signature = int(signature, 16)
|
|
55
|
+
|
|
56
|
+
return encode_base62(decimal_signature)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_storage_content_signature(
|
|
60
|
+
resource_id: str,
|
|
61
|
+
url_signing_secret_key: str,
|
|
62
|
+
*,
|
|
63
|
+
expires_in: timedelta | None = None,
|
|
64
|
+
version: int = 0,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Create a secure signature for a storage resource like a dataset or key-value store.
|
|
67
|
+
|
|
68
|
+
This signature is used to generate a signed URL for authenticated access, which can be expiring or permanent.
|
|
69
|
+
The signature is created using HMAC with the provided secret key and includes the resource ID, expiration time,
|
|
70
|
+
and version.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
resource_id: The unique identifier of the storage resource.
|
|
74
|
+
url_signing_secret_key: The secret key for signing the URL.
|
|
75
|
+
expires_in: Optional expiration duration; if None, the signature never expires.
|
|
76
|
+
version: The signature version number (default: 0).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The base64url-encoded signature string.
|
|
80
|
+
"""
|
|
81
|
+
expires_at = int(time.time() * 1000) + int(to_seconds(expires_in) * 1000) if expires_in is not None else 0
|
|
82
|
+
|
|
83
|
+
message_to_sign = f'{version}.{expires_at}.{resource_id}'
|
|
84
|
+
hmac_sig = create_hmac_signature(url_signing_secret_key, message_to_sign)
|
|
85
|
+
|
|
86
|
+
base64url_encoded_payload = urlsafe_b64encode(f'{version}.{expires_at}.{hmac_sig}'.encode())
|
|
87
|
+
return base64url_encoded_payload.decode('utf-8')
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from base64 import b64encode
|
|
5
|
+
from functools import cache
|
|
6
|
+
from inspect import isawaitable, iscoroutine
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from scrapely_client._models import WebhookCreate, WebhookRepresentation
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from scrapely_client.types import WebhooksList
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def encode_key_value_store_record_value(
|
|
16
|
+
value: Any, *, content_type: str | None = None, content_encoding: str | None = None
|
|
17
|
+
) -> tuple[bytes | bytearray | str, str]:
|
|
18
|
+
"""Encode a value for storage in a key-value store record.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: The value to encode. Anything exposing a callable `read` is treated as a file-like object: `read`
|
|
22
|
+
is called with no arguments, so the value is consumed from its current position and buffered in
|
|
23
|
+
memory whole - the object is neither rewound nor closed, and async file-like objects are rejected.
|
|
24
|
+
Any other value is JSON-serialized unless it is already bytes or a string.
|
|
25
|
+
content_type: The content type; if None, it's inferred from the value type.
|
|
26
|
+
content_encoding: The encoding the caller declares the value already carries, if any. Anything other than
|
|
27
|
+
`identity` means the value is compressed, which only a bytes-like payload can be, so any other value
|
|
28
|
+
is rejected. The check belongs here because a file-like value has to be read before its payload type
|
|
29
|
+
is known, and reading it a second time in the caller is not possible.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A tuple of (encoded_value, content_type).
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
TypeError: If the value cannot be encoded into a body the transport accepts, or if it cannot be carrying
|
|
36
|
+
the declared `content_encoding`.
|
|
37
|
+
"""
|
|
38
|
+
# Read file-like values into memory; the transport only accepts bytes-like bodies. Detect them by a
|
|
39
|
+
# callable `read` (not `io.IOBase`) so duck-typed file-likes are read, not JSON-serialized. Impit exposes
|
|
40
|
+
# no streaming `content=` API, so the value has to be buffered whole.
|
|
41
|
+
read = getattr(value, 'read', None)
|
|
42
|
+
if callable(read):
|
|
43
|
+
value = read()
|
|
44
|
+
|
|
45
|
+
if isawaitable(value):
|
|
46
|
+
if iscoroutine(value):
|
|
47
|
+
value.close() # Prevent a "coroutine was never awaited" warning.
|
|
48
|
+
raise TypeError(
|
|
49
|
+
'Async file-like objects are not supported. Await the read yourself and pass the resulting '
|
|
50
|
+
'bytes or string.'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not isinstance(value, (bytes, bytearray, str)):
|
|
54
|
+
raise TypeError(f'Reading the file-like value returned {type(value).__name__}, expected bytes or str.')
|
|
55
|
+
|
|
56
|
+
# A declared compression describes bytes the caller compressed. A string, a JSON-serializable object, or a
|
|
57
|
+
# text-mode file cannot be carrying one, and would otherwise be stored under a header that misdescribes it -
|
|
58
|
+
# the client forwards the header untouched and never inspects the body.
|
|
59
|
+
declared_encoding = (content_encoding or '').strip().lower()
|
|
60
|
+
if declared_encoding not in ('', 'identity') and not isinstance(value, (bytes, bytearray)):
|
|
61
|
+
raise TypeError(
|
|
62
|
+
f'Cannot upload a {type(value).__name__} value with `Content-Encoding: {content_encoding}`. An encoding '
|
|
63
|
+
'other than `identity` declares the value is already compressed, so pass the compressed bytes, or a '
|
|
64
|
+
'file-like object that reads them.'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not content_type:
|
|
68
|
+
if isinstance(value, (bytes, bytearray)):
|
|
69
|
+
content_type = 'application/octet-stream'
|
|
70
|
+
elif isinstance(value, str):
|
|
71
|
+
content_type = 'text/plain; charset=utf-8'
|
|
72
|
+
else:
|
|
73
|
+
content_type = 'application/json; charset=utf-8'
|
|
74
|
+
|
|
75
|
+
if 'application/json' in content_type and not isinstance(value, (bytes, bytearray, str)):
|
|
76
|
+
# Don't use indentation to reduce size.
|
|
77
|
+
value = json.dumps(
|
|
78
|
+
value,
|
|
79
|
+
ensure_ascii=False,
|
|
80
|
+
allow_nan=False,
|
|
81
|
+
default=str,
|
|
82
|
+
).encode('utf-8')
|
|
83
|
+
|
|
84
|
+
# A non-JSON content type skips the serialization above, so anything that is not bytes-like would reach the
|
|
85
|
+
# transport unencoded and fail there with an opaque error.
|
|
86
|
+
if not isinstance(value, (bytes, bytearray, str)):
|
|
87
|
+
raise TypeError(
|
|
88
|
+
f'Cannot encode a {type(value).__name__} value as {content_type!r}. Pass bytes, a string, or a '
|
|
89
|
+
'file-like object, or use a JSON content type.'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return (value, content_type)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@cache
|
|
96
|
+
def _webhook_representation_keys() -> frozenset[str]:
|
|
97
|
+
"""Return all field names and aliases declared on `WebhookRepresentation`."""
|
|
98
|
+
keys = set[str]()
|
|
99
|
+
for name, info in WebhookRepresentation.model_fields.items():
|
|
100
|
+
keys.add(name)
|
|
101
|
+
if info.alias is not None:
|
|
102
|
+
keys.add(info.alias)
|
|
103
|
+
return frozenset(keys)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def encode_webhooks_to_base64(webhooks: WebhooksList | None) -> str | None:
|
|
107
|
+
"""Encode a list of ad-hoc webhooks to a base64 string for the `webhooks` query parameter.
|
|
108
|
+
|
|
109
|
+
Returns `None` for `None` or an empty list, so the query parameter is omitted.
|
|
110
|
+
|
|
111
|
+
See `WebhooksList` for the accepted shapes. `WebhookRepresentation` instances are used as-is. `WebhookCreate`
|
|
112
|
+
instances and dict shapes are projected onto the fields `WebhookRepresentation` declares, dropping anything else
|
|
113
|
+
(e.g. persistent-only fields like `condition`). Filtering by the declared field names and aliases means new
|
|
114
|
+
ad-hoc fields added to `WebhookRepresentation` flow through automatically, without touching this function.
|
|
115
|
+
"""
|
|
116
|
+
if not webhooks:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
representations = list[WebhookRepresentation]()
|
|
120
|
+
allowed = _webhook_representation_keys()
|
|
121
|
+
|
|
122
|
+
for webhook in webhooks:
|
|
123
|
+
if isinstance(webhook, WebhookRepresentation):
|
|
124
|
+
representations.append(webhook)
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
data = webhook.model_dump(by_alias=True) if isinstance(webhook, WebhookCreate) else dict(webhook)
|
|
128
|
+
filtered = {key: value for key, value in data.items() if key in allowed}
|
|
129
|
+
representations.append(WebhookRepresentation.model_validate(filtered))
|
|
130
|
+
|
|
131
|
+
data = [r.model_dump(by_alias=True, exclude_none=True) for r in representations]
|
|
132
|
+
json_string = json.dumps(data).encode(encoding='utf-8')
|
|
133
|
+
return b64encode(json_string).decode(encoding='ascii')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from scrapely_client.errors import NotFoundError
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from scrapely_client.errors import ApifyApiError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def catch_not_found_or_throw(exc: ApifyApiError) -> None:
|
|
12
|
+
"""Suppress 404 Not Found errors and re-raise all other API errors.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
exc: The API error to check.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ApifyApiError: If the error is not a 404 Not Found error.
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(exc, NotFoundError):
|
|
21
|
+
raise exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def catch_not_found_for_resource_or_throw(exc: ApifyApiError, resource_id: str | None) -> None:
|
|
25
|
+
"""Like `catch_not_found_or_throw`, but only suppress 404s when the client targets a specific resource by ID.
|
|
26
|
+
|
|
27
|
+
For chained clients without a `resource_id` (e.g. `run.dataset()`, `run.log()`), a 404 could mean either the
|
|
28
|
+
parent or the default sub-resource is missing — the API body cannot disambiguate — so the error propagates
|
|
29
|
+
rather than being swallowed.
|
|
30
|
+
"""
|
|
31
|
+
if resource_id is None:
|
|
32
|
+
raise exc
|
|
33
|
+
catch_not_found_or_throw(exc)
|