scrapely-python 0.1.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.
- scrapely/__init__.py +246 -0
- scrapely/_browser_locations.py +5470 -0
- scrapely/actor.py +1836 -0
- scrapely/browser_configuration.py +146 -0
- scrapely/charging.py +1143 -0
- scrapely/charging_client.py +107 -0
- scrapely/configuration.py +375 -0
- scrapely/crypto/__init__.py +21 -0
- scrapely/crypto/_secrets.py +210 -0
- scrapely/errors.py +153 -0
- scrapely/events/__init__.py +57 -0
- scrapely/events/_types.py +194 -0
- scrapely/events/event_manager.py +292 -0
- scrapely/log.py +39 -0
- scrapely/patch_browser_pool.py +861 -0
- scrapely/pricing_types.py +172 -0
- scrapely/proxy_configuration.py +741 -0
- scrapely/py.typed +2 -0
- scrapely/request_loaders/__init__.py +5 -0
- scrapely/request_loaders/_scrapely_request_list.py +169 -0
- scrapely/storage/__init__.py +48 -0
- scrapely/storage/_file_system/__init__.py +9 -0
- scrapely/storage/_file_system/_key_value_store_client.py +155 -0
- scrapely/storage/_file_system/_storage_client.py +50 -0
- scrapely/storage/_metadata.py +138 -0
- scrapely/storage/api_client.py +350 -0
- scrapely/storage/client.py +618 -0
- scrapely/storage/patch_storage.py +257 -0
- scrapely/storage/scrapely_dataset_client.py +305 -0
- scrapely/storage/scrapely_kvs_client.py +220 -0
- scrapely/storage/scrapely_rq_client.py +496 -0
- scrapely/storage/scrapely_storage_client.py +192 -0
- scrapely/storage/smart_scrapely_storage_client.py +109 -0
- scrapely/storage/types.py +601 -0
- scrapely/storage/utils.py +38 -0
- scrapely/storage_clients/__init__.py +25 -0
- scrapely/storages/__init__.py +5 -0
- scrapely/types.py +269 -0
- scrapely/webhook.py +82 -0
- scrapely_python-0.1.0.dist-info/METADATA +83 -0
- scrapely_python-0.1.0.dist-info/RECORD +43 -0
- scrapely_python-0.1.0.dist-info/WHEEL +5 -0
- scrapely_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Scrapely cloud KeyValueStoreClient for Crawlee."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, MutableMapping, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from typing_extensions import Self, override
|
|
8
|
+
|
|
9
|
+
from crawlee.storage_clients._base import KeyValueStoreClient
|
|
10
|
+
from crawlee.storage_clients.models import (
|
|
11
|
+
KeyValueStoreMetadata,
|
|
12
|
+
KeyValueStoreRecord,
|
|
13
|
+
KeyValueStoreRecordMetadata,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .client import ApiStorageClient
|
|
17
|
+
from ._metadata import resolve_storage_id, to_kvs_metadata
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import AsyncIterator
|
|
21
|
+
|
|
22
|
+
from crawlee._types import JsonSerializable
|
|
23
|
+
|
|
24
|
+
T = TypeVar('T')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _old_kvs_to_crawlee(meta: Any) -> KeyValueStoreMetadata:
|
|
28
|
+
return to_kvs_metadata(
|
|
29
|
+
{
|
|
30
|
+
'id': meta.id,
|
|
31
|
+
'name': meta.name,
|
|
32
|
+
'createdAt': meta.created_at.isoformat() if getattr(meta, 'created_at', None) else None,
|
|
33
|
+
'accessedAt': meta.accessed_at.isoformat() if getattr(meta, 'accessed_at', None) else None,
|
|
34
|
+
'modifiedAt': meta.modified_at.isoformat() if getattr(meta, 'modified_at', None) else None,
|
|
35
|
+
},
|
|
36
|
+
meta.id,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ScrapelyKeyValueStoreClient(KeyValueStoreClient):
|
|
41
|
+
"""Key-value store client backed by Scrapely `/v2/key-value-stores`."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, *, api: ApiStorageClient, metadata: KeyValueStoreMetadata) -> None:
|
|
44
|
+
self._api = api
|
|
45
|
+
self._metadata = metadata
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def id(self) -> str:
|
|
49
|
+
"""Get the storage ID."""
|
|
50
|
+
return self._metadata.id
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self) -> Optional[str]:
|
|
54
|
+
"""Get the storage name."""
|
|
55
|
+
return self._metadata.name
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def open(
|
|
59
|
+
cls,
|
|
60
|
+
*,
|
|
61
|
+
api: ApiStorageClient,
|
|
62
|
+
id: Optional[str] = None,
|
|
63
|
+
name: Optional[str] = None,
|
|
64
|
+
alias: Optional[str] = None,
|
|
65
|
+
default_id: Optional[str] = None,
|
|
66
|
+
) -> Self:
|
|
67
|
+
open_id, open_name = resolve_storage_id(
|
|
68
|
+
id=id, name=name, alias=alias, default_id=default_id,
|
|
69
|
+
)
|
|
70
|
+
meta = await api.get_or_create_key_value_store(id=open_id, name=open_name)
|
|
71
|
+
return cls(api=api, metadata=_old_kvs_to_crawlee(meta))
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
async def get_metadata(self) -> KeyValueStoreMetadata:
|
|
75
|
+
meta = await self._api.kvs_get(self._metadata.id)
|
|
76
|
+
self._metadata = _old_kvs_to_crawlee(meta)
|
|
77
|
+
return self._metadata
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
async def drop(self) -> None:
|
|
81
|
+
await self._api.kvs_delete(self._metadata.id)
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
async def purge(self) -> None:
|
|
85
|
+
async for record in self.iterate_keys():
|
|
86
|
+
await self.delete_value(key=record.key)
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
async def get_value(self, *, key: str) -> Optional[KeyValueStoreRecord]:
|
|
90
|
+
value, content_type = await self._api.kvs_get_record(self._metadata.id, key)
|
|
91
|
+
if value is None and content_type is None:
|
|
92
|
+
return None
|
|
93
|
+
return KeyValueStoreRecord(
|
|
94
|
+
key=key,
|
|
95
|
+
value=value,
|
|
96
|
+
content_type=content_type or 'application/octet-stream',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
async def set_value(self, *, key: str, value: Any, content_type: Optional[str] = None) -> None:
|
|
101
|
+
if value is None:
|
|
102
|
+
await self.delete_value(key=key)
|
|
103
|
+
return
|
|
104
|
+
await self._api.kvs_set_record(self._metadata.id, key, value, content_type)
|
|
105
|
+
|
|
106
|
+
@override
|
|
107
|
+
async def delete_value(self, *, key: str) -> None:
|
|
108
|
+
await self._api.kvs_delete_record(self._metadata.id, key)
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
async def iterate_keys(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
exclusive_start_key: Optional[str] = None,
|
|
115
|
+
limit: Optional[int] = None,
|
|
116
|
+
) -> AsyncIterator[KeyValueStoreRecordMetadata]:
|
|
117
|
+
start = exclusive_start_key
|
|
118
|
+
yielded = 0
|
|
119
|
+
while True:
|
|
120
|
+
page_limit = 1000
|
|
121
|
+
if limit is not None:
|
|
122
|
+
remaining = limit - yielded
|
|
123
|
+
if remaining <= 0:
|
|
124
|
+
break
|
|
125
|
+
page_limit = min(1000, remaining)
|
|
126
|
+
|
|
127
|
+
data = await self._api.kvs_list_keys(
|
|
128
|
+
self._metadata.id,
|
|
129
|
+
exclusive_start_key=start,
|
|
130
|
+
limit=page_limit,
|
|
131
|
+
)
|
|
132
|
+
items = data.get('items') or []
|
|
133
|
+
if not items:
|
|
134
|
+
break
|
|
135
|
+
for item in items:
|
|
136
|
+
key = item.get('key')
|
|
137
|
+
if key is None:
|
|
138
|
+
continue
|
|
139
|
+
yield KeyValueStoreRecordMetadata(
|
|
140
|
+
key=str(key),
|
|
141
|
+
content_type=str(item.get('contentType') or 'application/octet-stream'),
|
|
142
|
+
size=int(item.get('size') or 0) if item.get('size') is not None else None,
|
|
143
|
+
)
|
|
144
|
+
yielded += 1
|
|
145
|
+
if limit is not None and yielded >= limit:
|
|
146
|
+
return
|
|
147
|
+
if not data.get('isTruncated'):
|
|
148
|
+
break
|
|
149
|
+
start = data.get('nextExclusiveStartKey')
|
|
150
|
+
if not start:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
async def get_public_url(self, *, key: str) -> str:
|
|
155
|
+
exists = await self.record_exists(key=key)
|
|
156
|
+
if not exists:
|
|
157
|
+
raise ValueError(f'Record with key "{key}" does not exist')
|
|
158
|
+
return self._api.kvs_public_url(self._metadata.id, key)
|
|
159
|
+
|
|
160
|
+
@override
|
|
161
|
+
async def record_exists(self, *, key: str) -> bool:
|
|
162
|
+
return await self._api.kvs_record_exists(self._metadata.id, key)
|
|
163
|
+
|
|
164
|
+
async def get_auto_saved_value(
|
|
165
|
+
self,
|
|
166
|
+
key: str,
|
|
167
|
+
default_value: Optional[MutableMapping[str, 'JsonSerializable']] = None,
|
|
168
|
+
) -> MutableMapping[str, 'JsonSerializable']:
|
|
169
|
+
"""Get a value that will be automatically saved on changes.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
key: Key of the record to store the value.
|
|
173
|
+
default_value: Default value if record doesn't exist.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The stored value or the default value.
|
|
177
|
+
"""
|
|
178
|
+
record = await self.get_value(key=key)
|
|
179
|
+
if record is None:
|
|
180
|
+
# Store and return the default value
|
|
181
|
+
value = default_value if default_value is not None else {}
|
|
182
|
+
await self.set_value(key=key, value=value)
|
|
183
|
+
return value
|
|
184
|
+
value = record.value
|
|
185
|
+
if not isinstance(value, dict):
|
|
186
|
+
# If stored value isn't a dict, return it wrapped or default
|
|
187
|
+
return default_value if default_value is not None else {}
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
async def persist_autosaved_values(self) -> None:
|
|
191
|
+
"""Force autosaved values to be saved.
|
|
192
|
+
|
|
193
|
+
Note: In this implementation, values are saved immediately on change,
|
|
194
|
+
so this method is a no-op. It exists for API compatibility.
|
|
195
|
+
"""
|
|
196
|
+
# Values are saved immediately in set_value, so nothing to do here
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
async def list_keys(
|
|
200
|
+
self,
|
|
201
|
+
*,
|
|
202
|
+
exclusive_start_key: Optional[str] = None,
|
|
203
|
+
limit: int = 1000,
|
|
204
|
+
) -> list[KeyValueStoreRecordMetadata]:
|
|
205
|
+
"""List all keys in the store.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
exclusive_start_key: Key to start iteration from.
|
|
209
|
+
limit: Maximum number of keys to return.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of KeyValueStoreRecordMetadata objects.
|
|
213
|
+
"""
|
|
214
|
+
result: list[KeyValueStoreRecordMetadata] = []
|
|
215
|
+
async for record in self.iterate_keys(
|
|
216
|
+
exclusive_start_key=exclusive_start_key,
|
|
217
|
+
limit=limit,
|
|
218
|
+
):
|
|
219
|
+
result.append(record)
|
|
220
|
+
return result
|