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.
Files changed (43) hide show
  1. scrapely/__init__.py +246 -0
  2. scrapely/_browser_locations.py +5470 -0
  3. scrapely/actor.py +1836 -0
  4. scrapely/browser_configuration.py +146 -0
  5. scrapely/charging.py +1143 -0
  6. scrapely/charging_client.py +107 -0
  7. scrapely/configuration.py +375 -0
  8. scrapely/crypto/__init__.py +21 -0
  9. scrapely/crypto/_secrets.py +210 -0
  10. scrapely/errors.py +153 -0
  11. scrapely/events/__init__.py +57 -0
  12. scrapely/events/_types.py +194 -0
  13. scrapely/events/event_manager.py +292 -0
  14. scrapely/log.py +39 -0
  15. scrapely/patch_browser_pool.py +861 -0
  16. scrapely/pricing_types.py +172 -0
  17. scrapely/proxy_configuration.py +741 -0
  18. scrapely/py.typed +2 -0
  19. scrapely/request_loaders/__init__.py +5 -0
  20. scrapely/request_loaders/_scrapely_request_list.py +169 -0
  21. scrapely/storage/__init__.py +48 -0
  22. scrapely/storage/_file_system/__init__.py +9 -0
  23. scrapely/storage/_file_system/_key_value_store_client.py +155 -0
  24. scrapely/storage/_file_system/_storage_client.py +50 -0
  25. scrapely/storage/_metadata.py +138 -0
  26. scrapely/storage/api_client.py +350 -0
  27. scrapely/storage/client.py +618 -0
  28. scrapely/storage/patch_storage.py +257 -0
  29. scrapely/storage/scrapely_dataset_client.py +305 -0
  30. scrapely/storage/scrapely_kvs_client.py +220 -0
  31. scrapely/storage/scrapely_rq_client.py +496 -0
  32. scrapely/storage/scrapely_storage_client.py +192 -0
  33. scrapely/storage/smart_scrapely_storage_client.py +109 -0
  34. scrapely/storage/types.py +601 -0
  35. scrapely/storage/utils.py +38 -0
  36. scrapely/storage_clients/__init__.py +25 -0
  37. scrapely/storages/__init__.py +5 -0
  38. scrapely/types.py +269 -0
  39. scrapely/webhook.py +82 -0
  40. scrapely_python-0.1.0.dist-info/METADATA +83 -0
  41. scrapely_python-0.1.0.dist-info/RECORD +43 -0
  42. scrapely_python-0.1.0.dist-info/WHEEL +5 -0
  43. 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