scrapely-client-python 3.1.3__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-3.1.3.dist-info/METADATA +269 -0
- scrapely_client_python-3.1.3.dist-info/RECORD +65 -0
- scrapely_client_python-3.1.3.dist-info/WHEEL +4 -0
- scrapely_client_python-3.1.3.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
from ._charging import (
|
|
4
|
+
USES_PUSH_DATA_INTERCEPTION,
|
|
5
|
+
ChargeResult,
|
|
6
|
+
ChargingHooks,
|
|
7
|
+
PpeAwarePushDataContext,
|
|
8
|
+
normalize_items,
|
|
9
|
+
push_data_charging_context,
|
|
10
|
+
)
|
|
11
|
+
from ._scrapely_client import ApifyClient, ApifyClientAsync, ScrapelyClient, ScrapelyClientAsync
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = metadata.version('scrapely-client-python')
|
|
15
|
+
except metadata.PackageNotFoundError:
|
|
16
|
+
__version__ = '0.0.0'
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'USES_PUSH_DATA_INTERCEPTION',
|
|
20
|
+
'ApifyClient',
|
|
21
|
+
'ApifyClientAsync',
|
|
22
|
+
'ChargeResult',
|
|
23
|
+
'ChargingHooks',
|
|
24
|
+
'PpeAwarePushDataContext',
|
|
25
|
+
'ScrapelyClient',
|
|
26
|
+
'ScrapelyClientAsync',
|
|
27
|
+
'__version__',
|
|
28
|
+
'normalize_items',
|
|
29
|
+
'push_data_charging_context',
|
|
30
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Opt-in pay-per-event charging interception for dataset pushes.
|
|
2
|
+
|
|
3
|
+
Port of ``scrapely-python``'s ``patched_apify_client.py`` semantics into the client
|
|
4
|
+
itself, with the charging logic supplied by the host SDK via the
|
|
5
|
+
:class:`ChargingHooks` protocol (no SDK import — the SDK adapts its
|
|
6
|
+
``ChargingManager`` to these hooks later).
|
|
7
|
+
|
|
8
|
+
Without hooks (the default), the client behaves exactly like stock
|
|
9
|
+
``apify-client``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
import json
|
|
16
|
+
from contextvars import ContextVar
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
22
|
+
|
|
23
|
+
USES_PUSH_DATA_INTERCEPTION = '__scrapely_uses_push_data_interception__'
|
|
24
|
+
"""Attribute marking dataset clients whose ``push_items`` charges pay-per-event items.
|
|
25
|
+
|
|
26
|
+
The host SDK checks this attribute to detect that push-data interception is active."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ChargeResult:
|
|
31
|
+
"""Result of a charged push-data operation.
|
|
32
|
+
|
|
33
|
+
Mirrors the SDK-side ``ChargeResult`` contract; the client only stores and merges
|
|
34
|
+
these values, it never computes them.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
event_charge_limit_reached: bool
|
|
38
|
+
"""Whether an event charge limit was reached during the operation."""
|
|
39
|
+
|
|
40
|
+
charged_count: int
|
|
41
|
+
"""Total number of charged items."""
|
|
42
|
+
|
|
43
|
+
chargeable_within_limit: dict[str, int | None] = field(default_factory=dict)
|
|
44
|
+
"""How many more items could be charged per event within the current limits."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class PpeAwarePushDataContext:
|
|
49
|
+
"""Context carried through ``push_data_charging_context``.
|
|
50
|
+
|
|
51
|
+
Wrapping a push in ``push_data_charging_context.run({...}, fn)`` makes every
|
|
52
|
+
intercepted ``push_items()`` inside ``fn`` charge ``event_name`` and merge its
|
|
53
|
+
result into the context.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
event_name: str | None = None
|
|
57
|
+
charge_result: ChargeResult | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
push_data_charging_context: ContextVar[PpeAwarePushDataContext | None] = ContextVar(
|
|
61
|
+
'scrapely_push_data_charging_context', default=None
|
|
62
|
+
)
|
|
63
|
+
"""ContextVar for the current :class:`PpeAwarePushDataContext` (async-local in asyncio)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@runtime_checkable
|
|
67
|
+
class ChargingHooks(Protocol):
|
|
68
|
+
"""Charging hooks supplied by the host SDK (e.g. ``scrapely``'s ``ChargingManager``).
|
|
69
|
+
|
|
70
|
+
The client knows nothing about pricing, budgets or ledgers — it only routes
|
|
71
|
+
``push_items()`` through these hooks when they are configured and the dataset
|
|
72
|
+
is the actor run's default dataset.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def should_charge_dataset_pushes(self, dataset_id: str) -> bool:
|
|
76
|
+
"""Whether pushes to this dataset should be intercepted and charged."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def push_and_charge(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
items: Sequence[dict[str, Any]],
|
|
83
|
+
event_name: str | None,
|
|
84
|
+
push_fn: Callable[[Sequence[dict[str, Any]]], Any],
|
|
85
|
+
) -> ChargeResult | Awaitable[ChargeResult]:
|
|
86
|
+
"""Charge for (and persist) the given items.
|
|
87
|
+
|
|
88
|
+
``push_fn`` persists the budget-limited subset of items; implementations must
|
|
89
|
+
call it with only the items that were actually accepted for charging. Both
|
|
90
|
+
sync and async implementations are supported (async clients await awaitables).
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
def merge_charge_results(self, a: ChargeResult, b: ChargeResult) -> ChargeResult:
|
|
95
|
+
"""Merge two charge results (e.g. multiple pushes within one context)."""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_items(items: Any) -> list[dict[str, Any]]:
|
|
100
|
+
"""Normalize ``push_items`` input (str, dict, or list of either) to a list of dicts."""
|
|
101
|
+
if isinstance(items, str):
|
|
102
|
+
parsed = json.loads(items)
|
|
103
|
+
return parsed if isinstance(parsed, list) else [parsed]
|
|
104
|
+
if isinstance(items, list):
|
|
105
|
+
result: list[dict[str, Any]] = []
|
|
106
|
+
for item in items:
|
|
107
|
+
if isinstance(item, str):
|
|
108
|
+
parsed = json.loads(item)
|
|
109
|
+
if isinstance(parsed, list):
|
|
110
|
+
result.extend(parsed)
|
|
111
|
+
else:
|
|
112
|
+
result.append(parsed)
|
|
113
|
+
else:
|
|
114
|
+
result.append(item)
|
|
115
|
+
return result
|
|
116
|
+
return [items]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _merge_context_charge_result(context: PpeAwarePushDataContext, hooks: ChargingHooks, result: ChargeResult) -> None:
|
|
120
|
+
if context.charge_result is None:
|
|
121
|
+
context.charge_result = result
|
|
122
|
+
else:
|
|
123
|
+
context.charge_result = hooks.merge_charge_results(context.charge_result, result)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def wrap_dataset_client_with_charging(client: Any, hooks: ChargingHooks, *, is_async: bool) -> Any:
|
|
127
|
+
"""Wrap a dataset client's ``push_items`` with charging interception (method swap by design).
|
|
128
|
+
|
|
129
|
+
Semantics match ``scrapely``'s ``patched_apify_client.py``: normalize items →
|
|
130
|
+
``push_and_charge`` with a ``push_fn`` that pushes the accepted subset via the
|
|
131
|
+
original ``push_items``, and accumulate the charge result into the ambient
|
|
132
|
+
``push_data_charging_context`` when present.
|
|
133
|
+
"""
|
|
134
|
+
original_push_items = client.push_items
|
|
135
|
+
|
|
136
|
+
if is_async:
|
|
137
|
+
|
|
138
|
+
async def patched_push_items(items: Any, **kwargs: Any) -> None:
|
|
139
|
+
context = push_data_charging_context.get()
|
|
140
|
+
normalized = normalize_items(items)
|
|
141
|
+
|
|
142
|
+
async def push_fn(limited_items: Sequence[dict[str, Any]]) -> None:
|
|
143
|
+
await original_push_items(limited_items, **kwargs)
|
|
144
|
+
|
|
145
|
+
outcome = hooks.push_and_charge(
|
|
146
|
+
items=normalized, event_name=context.event_name if context else None, push_fn=push_fn
|
|
147
|
+
)
|
|
148
|
+
if inspect.isawaitable(outcome):
|
|
149
|
+
outcome = await outcome
|
|
150
|
+
result = cast('ChargeResult', outcome)
|
|
151
|
+
if context is not None:
|
|
152
|
+
_merge_context_charge_result(context, hooks, result)
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
|
|
156
|
+
def patched_push_items(items: Any, **kwargs: Any) -> None:
|
|
157
|
+
context = push_data_charging_context.get()
|
|
158
|
+
normalized = normalize_items(items)
|
|
159
|
+
|
|
160
|
+
def push_fn(limited_items: Sequence[dict[str, Any]]) -> None:
|
|
161
|
+
original_push_items(limited_items, **kwargs)
|
|
162
|
+
|
|
163
|
+
outcome = hooks.push_and_charge(
|
|
164
|
+
items=normalized, event_name=context.event_name if context else None, push_fn=push_fn
|
|
165
|
+
)
|
|
166
|
+
result = cast('ChargeResult', outcome)
|
|
167
|
+
if inspect.isawaitable(outcome):
|
|
168
|
+
msg = 'Sync clients require synchronous ChargingHooks.push_and_charge'
|
|
169
|
+
raise TypeError(msg)
|
|
170
|
+
if context is not None:
|
|
171
|
+
_merge_context_charge_result(context, hooks, result)
|
|
172
|
+
|
|
173
|
+
client.push_items = patched_push_items # type: ignore[method-assign]
|
|
174
|
+
setattr(client, USES_PUSH_DATA_INTERCEPTION, True)
|
|
175
|
+
return client
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = [
|
|
179
|
+
'USES_PUSH_DATA_INTERCEPTION',
|
|
180
|
+
'ChargeResult',
|
|
181
|
+
'ChargingHooks',
|
|
182
|
+
'PpeAwarePushDataContext',
|
|
183
|
+
'normalize_items',
|
|
184
|
+
'push_data_charging_context',
|
|
185
|
+
'wrap_dataset_client_with_charging',
|
|
186
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from scrapely_client._resource_clients import (
|
|
8
|
+
ActorClient,
|
|
9
|
+
ActorClientAsync,
|
|
10
|
+
ActorCollectionClient,
|
|
11
|
+
ActorCollectionClientAsync,
|
|
12
|
+
ActorEnvVarClient,
|
|
13
|
+
ActorEnvVarClientAsync,
|
|
14
|
+
ActorEnvVarCollectionClient,
|
|
15
|
+
ActorEnvVarCollectionClientAsync,
|
|
16
|
+
ActorVersionClient,
|
|
17
|
+
ActorVersionClientAsync,
|
|
18
|
+
ActorVersionCollectionClient,
|
|
19
|
+
ActorVersionCollectionClientAsync,
|
|
20
|
+
BuildClient,
|
|
21
|
+
BuildClientAsync,
|
|
22
|
+
BuildCollectionClient,
|
|
23
|
+
BuildCollectionClientAsync,
|
|
24
|
+
DatasetClient,
|
|
25
|
+
DatasetClientAsync,
|
|
26
|
+
DatasetCollectionClient,
|
|
27
|
+
DatasetCollectionClientAsync,
|
|
28
|
+
KeyValueStoreClient,
|
|
29
|
+
KeyValueStoreClientAsync,
|
|
30
|
+
KeyValueStoreCollectionClient,
|
|
31
|
+
KeyValueStoreCollectionClientAsync,
|
|
32
|
+
LogClient,
|
|
33
|
+
LogClientAsync,
|
|
34
|
+
RequestQueueClient,
|
|
35
|
+
RequestQueueClientAsync,
|
|
36
|
+
RequestQueueCollectionClient,
|
|
37
|
+
RequestQueueCollectionClientAsync,
|
|
38
|
+
RunClient,
|
|
39
|
+
RunClientAsync,
|
|
40
|
+
RunCollectionClient,
|
|
41
|
+
RunCollectionClientAsync,
|
|
42
|
+
ScheduleClient,
|
|
43
|
+
ScheduleClientAsync,
|
|
44
|
+
ScheduleCollectionClient,
|
|
45
|
+
ScheduleCollectionClientAsync,
|
|
46
|
+
StoreCollectionClient,
|
|
47
|
+
StoreCollectionClientAsync,
|
|
48
|
+
TaskClient,
|
|
49
|
+
TaskClientAsync,
|
|
50
|
+
TaskCollectionClient,
|
|
51
|
+
TaskCollectionClientAsync,
|
|
52
|
+
UserClient,
|
|
53
|
+
UserClientAsync,
|
|
54
|
+
WebhookClient,
|
|
55
|
+
WebhookClientAsync,
|
|
56
|
+
WebhookCollectionClient,
|
|
57
|
+
WebhookCollectionClientAsync,
|
|
58
|
+
WebhookDispatchClient,
|
|
59
|
+
WebhookDispatchClientAsync,
|
|
60
|
+
WebhookDispatchCollectionClient,
|
|
61
|
+
WebhookDispatchCollectionClientAsync,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ClientRegistry:
|
|
67
|
+
"""Bundle of all sync client classes for dependency injection.
|
|
68
|
+
|
|
69
|
+
This config object is passed to the resource clients to avoid circular dependencies. Each resource client
|
|
70
|
+
receives this config and can instantiate other clients as needed.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
actor_client: type[ActorClient]
|
|
74
|
+
actor_collection_client: type[ActorCollectionClient]
|
|
75
|
+
actor_env_var_client: type[ActorEnvVarClient]
|
|
76
|
+
actor_env_var_collection_client: type[ActorEnvVarCollectionClient]
|
|
77
|
+
actor_version_client: type[ActorVersionClient]
|
|
78
|
+
actor_version_collection_client: type[ActorVersionCollectionClient]
|
|
79
|
+
build_client: type[BuildClient]
|
|
80
|
+
build_collection_client: type[BuildCollectionClient]
|
|
81
|
+
dataset_client: type[DatasetClient]
|
|
82
|
+
dataset_collection_client: type[DatasetCollectionClient]
|
|
83
|
+
key_value_store_client: type[KeyValueStoreClient]
|
|
84
|
+
key_value_store_collection_client: type[KeyValueStoreCollectionClient]
|
|
85
|
+
log_client: type[LogClient]
|
|
86
|
+
request_queue_client: type[RequestQueueClient]
|
|
87
|
+
request_queue_collection_client: type[RequestQueueCollectionClient]
|
|
88
|
+
run_client: type[RunClient]
|
|
89
|
+
run_collection_client: type[RunCollectionClient]
|
|
90
|
+
schedule_client: type[ScheduleClient]
|
|
91
|
+
schedule_collection_client: type[ScheduleCollectionClient]
|
|
92
|
+
store_collection_client: type[StoreCollectionClient]
|
|
93
|
+
task_client: type[TaskClient]
|
|
94
|
+
task_collection_client: type[TaskCollectionClient]
|
|
95
|
+
user_client: type[UserClient]
|
|
96
|
+
webhook_client: type[WebhookClient]
|
|
97
|
+
webhook_collection_client: type[WebhookCollectionClient]
|
|
98
|
+
webhook_dispatch_client: type[WebhookDispatchClient]
|
|
99
|
+
webhook_dispatch_collection_client: type[WebhookDispatchCollectionClient]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ClientRegistryAsync:
|
|
104
|
+
"""Bundle of all async client classes for dependency injection.
|
|
105
|
+
|
|
106
|
+
This config object is passed to the resource clients to avoid circular dependencies. Each resource client
|
|
107
|
+
receives this config and can instantiate other clients as needed.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
actor_client: type[ActorClientAsync]
|
|
111
|
+
actor_collection_client: type[ActorCollectionClientAsync]
|
|
112
|
+
actor_env_var_client: type[ActorEnvVarClientAsync]
|
|
113
|
+
actor_env_var_collection_client: type[ActorEnvVarCollectionClientAsync]
|
|
114
|
+
actor_version_client: type[ActorVersionClientAsync]
|
|
115
|
+
actor_version_collection_client: type[ActorVersionCollectionClientAsync]
|
|
116
|
+
build_client: type[BuildClientAsync]
|
|
117
|
+
build_collection_client: type[BuildCollectionClientAsync]
|
|
118
|
+
dataset_client: type[DatasetClientAsync]
|
|
119
|
+
dataset_collection_client: type[DatasetCollectionClientAsync]
|
|
120
|
+
key_value_store_client: type[KeyValueStoreClientAsync]
|
|
121
|
+
key_value_store_collection_client: type[KeyValueStoreCollectionClientAsync]
|
|
122
|
+
log_client: type[LogClientAsync]
|
|
123
|
+
request_queue_client: type[RequestQueueClientAsync]
|
|
124
|
+
request_queue_collection_client: type[RequestQueueCollectionClientAsync]
|
|
125
|
+
run_client: type[RunClientAsync]
|
|
126
|
+
run_collection_client: type[RunCollectionClientAsync]
|
|
127
|
+
schedule_client: type[ScheduleClientAsync]
|
|
128
|
+
schedule_collection_client: type[ScheduleCollectionClientAsync]
|
|
129
|
+
store_collection_client: type[StoreCollectionClientAsync]
|
|
130
|
+
task_client: type[TaskClientAsync]
|
|
131
|
+
task_collection_client: type[TaskCollectionClientAsync]
|
|
132
|
+
user_client: type[UserClientAsync]
|
|
133
|
+
webhook_client: type[WebhookClientAsync]
|
|
134
|
+
webhook_collection_client: type[WebhookCollectionClientAsync]
|
|
135
|
+
webhook_dispatch_client: type[WebhookDispatchClientAsync]
|
|
136
|
+
webhook_dispatch_collection_client: type[WebhookDispatchCollectionClientAsync]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
DEFAULT_API_URL = 'https://api-staging.scrape.ly'
|
|
6
|
+
"""Default base URL for the Scrapely API."""
|
|
7
|
+
|
|
8
|
+
API_VERSION = 'v2'
|
|
9
|
+
"""Current Scrapely API version."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_TIMEOUT_SHORT = timedelta(seconds=5)
|
|
12
|
+
"""Default timeout for fast CRUD operations (e.g., get, update, delete)."""
|
|
13
|
+
|
|
14
|
+
DEFAULT_TIMEOUT_MEDIUM = timedelta(seconds=30)
|
|
15
|
+
"""Default timeout for batch, list, and data transfer operations."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_TIMEOUT_LONG = timedelta(seconds=360)
|
|
18
|
+
"""Default timeout for long-polling, streaming, and other heavy operations."""
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIMEOUT_MAX = timedelta(seconds=360)
|
|
21
|
+
"""Default maximum timeout cap for individual API requests (limits exponential growth)."""
|
|
22
|
+
|
|
23
|
+
DEFAULT_MAX_RETRIES = 4
|
|
24
|
+
"""Default maximum number of retries for failed requests."""
|
|
25
|
+
|
|
26
|
+
DEFAULT_MIN_DELAY_BETWEEN_RETRIES = timedelta(milliseconds=500)
|
|
27
|
+
"""Default minimum delay between retries."""
|
|
28
|
+
|
|
29
|
+
DEFAULT_WAIT_FOR_FINISH = timedelta(seconds=999999)
|
|
30
|
+
"""Default maximum wait time for job completion (effectively infinite)."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_WAIT_WHEN_JOB_NOT_EXIST = timedelta(seconds=3)
|
|
33
|
+
"""How long to wait for a job to exist before giving up."""
|
|
34
|
+
|
|
35
|
+
OVERRIDABLE_DEFAULT_HEADERS = {'Accept', 'Authorization', 'Accept-Encoding', 'User-Agent'}
|
|
36
|
+
"""Headers that can be overridden by users, but will trigger a warning if they do so, as it may lead to API errors."""
|
|
37
|
+
|
|
38
|
+
MIN_COMPRESSION_SIZE = 1024
|
|
39
|
+
"""Smallest request body, in bytes, that is worth compressing.
|
|
40
|
+
|
|
41
|
+
A smaller body already fits in a single network packet, so compressing it costs CPU time without
|
|
42
|
+
saving a round trip.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
ALREADY_COMPRESSED_MEDIA_TYPE_PREFIXES = ('audio/', 'image/', 'video/')
|
|
46
|
+
"""Media type prefixes whose payloads carry their own compression, so compressing the request body is wasted work."""
|
|
47
|
+
|
|
48
|
+
ALREADY_COMPRESSED_MEDIA_TYPES = frozenset(
|
|
49
|
+
{
|
|
50
|
+
'application/epub+zip',
|
|
51
|
+
'application/gzip',
|
|
52
|
+
'application/java-archive',
|
|
53
|
+
'application/vnd.android.package-archive',
|
|
54
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
55
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
56
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
57
|
+
'application/vnd.rar',
|
|
58
|
+
'application/x-7z-compressed',
|
|
59
|
+
'application/x-bzip',
|
|
60
|
+
'application/x-bzip2',
|
|
61
|
+
'application/x-gzip',
|
|
62
|
+
'application/x-rar-compressed',
|
|
63
|
+
'application/x-xz',
|
|
64
|
+
'application/x-zip-compressed',
|
|
65
|
+
'application/zip',
|
|
66
|
+
'application/zstd',
|
|
67
|
+
'font/woff',
|
|
68
|
+
'font/woff2',
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
"""Exact media types whose payloads carry their own compression."""
|
|
72
|
+
|
|
73
|
+
COMPRESSIBLE_MEDIA_TYPES = frozenset(
|
|
74
|
+
{
|
|
75
|
+
'audio/aiff',
|
|
76
|
+
'audio/basic',
|
|
77
|
+
'audio/l16',
|
|
78
|
+
'audio/l24',
|
|
79
|
+
'audio/midi',
|
|
80
|
+
'audio/vnd.wave',
|
|
81
|
+
'audio/wav',
|
|
82
|
+
'audio/wave',
|
|
83
|
+
'audio/x-aiff',
|
|
84
|
+
'audio/x-wav',
|
|
85
|
+
'image/bmp',
|
|
86
|
+
'image/tiff',
|
|
87
|
+
'image/vnd.adobe.photoshop',
|
|
88
|
+
'image/vnd.microsoft.icon',
|
|
89
|
+
'image/x-icon',
|
|
90
|
+
'image/x-ms-bmp',
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
"""Uncompressed media types that sit under an already-compressed prefix, so compressing them still pays off."""
|
|
94
|
+
|
|
95
|
+
COMPRESSIBLE_MEDIA_TYPE_SUFFIXES = ('+json', '+xml')
|
|
96
|
+
"""Structured syntax suffixes marking a media type as text even under an already-compressed prefix (`image/svg+xml`)."""
|
scrapely_client/_docs.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, Literal, TypeVar
|
|
5
|
+
|
|
6
|
+
# The order of the rendered API groups is defined by GROUP_ORDER in website/transformDocs.js
|
|
7
|
+
# and applied via groupSort in website/docusaurus.config.js.
|
|
8
|
+
GroupName = Literal[
|
|
9
|
+
'Scrapely API clients',
|
|
10
|
+
'HTTP clients',
|
|
11
|
+
'Resource clients',
|
|
12
|
+
'Errors',
|
|
13
|
+
'Models',
|
|
14
|
+
'Typed dicts',
|
|
15
|
+
'Other',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
T = TypeVar('T', bound=Callable[..., Any])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def docs_group(group_name: GroupName) -> Callable[[T], T]: # noqa: ARG001
|
|
22
|
+
"""Mark a symbol for rendering and grouping in documentation.
|
|
23
|
+
|
|
24
|
+
This decorator is used solely for documentation purposes and does not modify the behavior
|
|
25
|
+
of the decorated callable.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
group_name: The documentation group to which the symbol belongs.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The original callable without modification.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def wrapper(func: T) -> T:
|
|
35
|
+
return func
|
|
36
|
+
|
|
37
|
+
return wrapper
|