scrapely-python 0.1.22__py3-none-any.whl → 0.1.23__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 +143 -48
- scrapely/actor.py +86 -42
- scrapely/patch_browser_pool.py +96 -3
- scrapely/storage/__init__.py +84 -19
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/METADATA +1 -1
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/RECORD +11 -11
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/WHEEL +0 -0
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/licenses/LICENSE-APACHE +0 -0
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/licenses/NOTICE +0 -0
- {scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/top_level.txt +0 -0
scrapely/__init__.py
CHANGED
|
@@ -10,22 +10,20 @@ Usage:
|
|
|
10
10
|
await Actor.push_data({'url': actor_input['url']})
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
# Side-effect: patch browser launches (Crawlee BrowserPool, PlaywrightCrawler,
|
|
16
|
+
# raw Playwright, Camoufox, pyppeteer) to use the remote CDP browser (CDP_URL).
|
|
14
17
|
# Must run on package import so `from scrapely import Actor` enables the patch
|
|
15
|
-
# before
|
|
18
|
+
# before any browser is launched (Node SDK parity). It does not import Crawlee:
|
|
19
|
+
# the Crawlee patches are applied when Crawlee's browser modules load.
|
|
16
20
|
from .patch_browser_pool import set_cdp_url # noqa: F401
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
RequestManagerTandem,
|
|
24
|
-
SitemapRequestLoader,
|
|
25
|
-
)
|
|
26
|
-
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
27
|
-
from crawlee.storage_clients.models import DatasetItemsListPage
|
|
28
|
-
|
|
22
|
+
# Crawlee is NOT imported here: it costs ~1 s per run under gVisor and many
|
|
23
|
+
# actors never touch it. Everything that needs Crawlee (the re-exported Crawlee
|
|
24
|
+
# classes, storage clients, events, request loaders) is resolved on first
|
|
25
|
+
# attribute access by ``__getattr__`` below; ``Actor`` imports what it needs on
|
|
26
|
+
# first use. Every ``from scrapely import X`` that worked before still works.
|
|
29
27
|
from .actor import Actor
|
|
30
28
|
from .configuration import Configuration
|
|
31
29
|
from .charging_client import (
|
|
@@ -33,40 +31,6 @@ from .charging_client import (
|
|
|
33
31
|
create_charging_client,
|
|
34
32
|
push_data_charging_context,
|
|
35
33
|
)
|
|
36
|
-
from .storage import (
|
|
37
|
-
ApiStorageClient,
|
|
38
|
-
FileSystemStorageClient,
|
|
39
|
-
MemoryStorageClient,
|
|
40
|
-
ScrapelyStorageClient,
|
|
41
|
-
SmartScrapelyStorageClient,
|
|
42
|
-
StorageClient,
|
|
43
|
-
set_api_storage,
|
|
44
|
-
)
|
|
45
|
-
from .request_loaders import ScrapelyRequestList
|
|
46
|
-
from crawlee.events import EventListener
|
|
47
|
-
from .events import (
|
|
48
|
-
AbortingEvent,
|
|
49
|
-
ActorEventTypes,
|
|
50
|
-
CrawleeEventManager,
|
|
51
|
-
DeprecatedEvent,
|
|
52
|
-
Event,
|
|
53
|
-
EventAbortingData,
|
|
54
|
-
EventExitData,
|
|
55
|
-
EventManager,
|
|
56
|
-
EventMessage,
|
|
57
|
-
EventMigratingData,
|
|
58
|
-
EventPersistStateData,
|
|
59
|
-
EventSystemInfoData,
|
|
60
|
-
EventWithoutData,
|
|
61
|
-
ExitEvent,
|
|
62
|
-
LocalEventManager,
|
|
63
|
-
MigratingEvent,
|
|
64
|
-
PersistStateEvent,
|
|
65
|
-
ScrapelyEventManager,
|
|
66
|
-
SystemInfoEvent,
|
|
67
|
-
SystemInfoEventData,
|
|
68
|
-
UnknownEvent,
|
|
69
|
-
)
|
|
70
34
|
from .proxy_configuration import ProxyConfiguration
|
|
71
35
|
from .browser_configuration import BrowserConfiguration
|
|
72
36
|
from .charging import (
|
|
@@ -132,7 +96,138 @@ from .errors import (
|
|
|
132
96
|
raise_for_status,
|
|
133
97
|
)
|
|
134
98
|
|
|
135
|
-
|
|
99
|
+
# Public name -> (module, attribute) for names that need Crawlee. Loaded on
|
|
100
|
+
# first access and cached in the module namespace.
|
|
101
|
+
_LAZY_ATTRS = {
|
|
102
|
+
# Crawlee re-exports
|
|
103
|
+
'Request': ('crawlee', 'Request'),
|
|
104
|
+
'RequestLoader': ('crawlee.request_loaders', 'RequestLoader'),
|
|
105
|
+
'RequestManager': ('crawlee.request_loaders', 'RequestManager'),
|
|
106
|
+
'RequestManagerTandem': ('crawlee.request_loaders', 'RequestManagerTandem'),
|
|
107
|
+
'SitemapRequestLoader': ('crawlee.request_loaders', 'SitemapRequestLoader'),
|
|
108
|
+
'Dataset': ('crawlee.storages', 'Dataset'),
|
|
109
|
+
'KeyValueStore': ('crawlee.storages', 'KeyValueStore'),
|
|
110
|
+
'RequestQueue': ('crawlee.storages', 'RequestQueue'),
|
|
111
|
+
'DatasetItemsListPage': ('crawlee.storage_clients.models', 'DatasetItemsListPage'),
|
|
112
|
+
'EventListener': ('crawlee.events', 'EventListener'),
|
|
113
|
+
# Storage clients
|
|
114
|
+
'ApiStorageClient': ('scrapely.storage', 'ApiStorageClient'),
|
|
115
|
+
'FileSystemStorageClient': ('scrapely.storage', 'FileSystemStorageClient'),
|
|
116
|
+
'MemoryStorageClient': ('scrapely.storage', 'MemoryStorageClient'),
|
|
117
|
+
'ScrapelyStorageClient': ('scrapely.storage', 'ScrapelyStorageClient'),
|
|
118
|
+
'SmartScrapelyStorageClient': ('scrapely.storage', 'SmartScrapelyStorageClient'),
|
|
119
|
+
'StorageClient': ('scrapely.storage', 'StorageClient'),
|
|
120
|
+
'set_api_storage': ('scrapely.storage', 'set_api_storage'),
|
|
121
|
+
# Request loaders
|
|
122
|
+
'ScrapelyRequestList': ('scrapely.request_loaders', 'ScrapelyRequestList'),
|
|
123
|
+
# Events
|
|
124
|
+
**{
|
|
125
|
+
name: ('scrapely.events', name)
|
|
126
|
+
for name in (
|
|
127
|
+
'AbortingEvent',
|
|
128
|
+
'ActorEventTypes',
|
|
129
|
+
'CrawleeEventManager',
|
|
130
|
+
'DeprecatedEvent',
|
|
131
|
+
'Event',
|
|
132
|
+
'EventAbortingData',
|
|
133
|
+
'EventExitData',
|
|
134
|
+
'EventManager',
|
|
135
|
+
'EventMessage',
|
|
136
|
+
'EventMigratingData',
|
|
137
|
+
'EventPersistStateData',
|
|
138
|
+
'EventSystemInfoData',
|
|
139
|
+
'EventWithoutData',
|
|
140
|
+
'ExitEvent',
|
|
141
|
+
'LocalEventManager',
|
|
142
|
+
'MigratingEvent',
|
|
143
|
+
'PersistStateEvent',
|
|
144
|
+
'ScrapelyEventManager',
|
|
145
|
+
'SystemInfoEvent',
|
|
146
|
+
'SystemInfoEventData',
|
|
147
|
+
'UnknownEvent',
|
|
148
|
+
)
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Submodules that used to be loaded (and so bound as attributes) by this
|
|
153
|
+
# package's eager imports; ``scrapely.events`` etc. keep working without an
|
|
154
|
+
# explicit ``import scrapely.events``.
|
|
155
|
+
_LAZY_SUBMODULES = frozenset({
|
|
156
|
+
'events',
|
|
157
|
+
'log',
|
|
158
|
+
'request_loaders',
|
|
159
|
+
'storage',
|
|
160
|
+
'storage_clients',
|
|
161
|
+
'storages',
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def __getattr__(name: str) -> Any:
|
|
166
|
+
target = _LAZY_ATTRS.get(name)
|
|
167
|
+
if target is not None:
|
|
168
|
+
import importlib
|
|
169
|
+
|
|
170
|
+
value = getattr(importlib.import_module(target[0]), target[1])
|
|
171
|
+
globals()[name] = value
|
|
172
|
+
return value
|
|
173
|
+
if name in _LAZY_SUBMODULES:
|
|
174
|
+
import importlib
|
|
175
|
+
|
|
176
|
+
return importlib.import_module(f'{__name__}.{name}')
|
|
177
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def __dir__() -> list:
|
|
181
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS) | _LAZY_SUBMODULES)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if TYPE_CHECKING: # Static analysers and IDEs see the lazy names as plain imports.
|
|
185
|
+
from crawlee import Request
|
|
186
|
+
from crawlee.events import EventListener
|
|
187
|
+
from crawlee.request_loaders import (
|
|
188
|
+
RequestLoader,
|
|
189
|
+
RequestManager,
|
|
190
|
+
RequestManagerTandem,
|
|
191
|
+
SitemapRequestLoader,
|
|
192
|
+
)
|
|
193
|
+
from crawlee.storage_clients.models import DatasetItemsListPage
|
|
194
|
+
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
195
|
+
|
|
196
|
+
from .events import (
|
|
197
|
+
AbortingEvent,
|
|
198
|
+
ActorEventTypes,
|
|
199
|
+
CrawleeEventManager,
|
|
200
|
+
DeprecatedEvent,
|
|
201
|
+
Event,
|
|
202
|
+
EventAbortingData,
|
|
203
|
+
EventExitData,
|
|
204
|
+
EventManager,
|
|
205
|
+
EventMessage,
|
|
206
|
+
EventMigratingData,
|
|
207
|
+
EventPersistStateData,
|
|
208
|
+
EventSystemInfoData,
|
|
209
|
+
EventWithoutData,
|
|
210
|
+
ExitEvent,
|
|
211
|
+
LocalEventManager,
|
|
212
|
+
MigratingEvent,
|
|
213
|
+
PersistStateEvent,
|
|
214
|
+
ScrapelyEventManager,
|
|
215
|
+
SystemInfoEvent,
|
|
216
|
+
SystemInfoEventData,
|
|
217
|
+
UnknownEvent,
|
|
218
|
+
)
|
|
219
|
+
from .request_loaders import ScrapelyRequestList
|
|
220
|
+
from .storage import (
|
|
221
|
+
ApiStorageClient,
|
|
222
|
+
FileSystemStorageClient,
|
|
223
|
+
MemoryStorageClient,
|
|
224
|
+
ScrapelyStorageClient,
|
|
225
|
+
SmartScrapelyStorageClient,
|
|
226
|
+
StorageClient,
|
|
227
|
+
set_api_storage,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
__version__ = '0.1.23'
|
|
136
231
|
__all__ = [
|
|
137
232
|
# Core
|
|
138
233
|
'Actor',
|
scrapely/actor.py
CHANGED
|
@@ -20,10 +20,6 @@ from datetime import datetime, timedelta
|
|
|
20
20
|
from decimal import Decimal
|
|
21
21
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, cast
|
|
22
22
|
|
|
23
|
-
from crawlee.configuration import Configuration as CrawleeConfiguration
|
|
24
|
-
from crawlee.storage_clients._base import StorageClient
|
|
25
|
-
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
26
|
-
|
|
27
23
|
from .charging import (
|
|
28
24
|
ChargingManager,
|
|
29
25
|
is_default_dataset_item_event,
|
|
@@ -34,23 +30,10 @@ from .charging import (
|
|
|
34
30
|
from .configuration import Configuration
|
|
35
31
|
from .crypto import decrypt_secret_inputs
|
|
36
32
|
from .proxy_configuration import with_proxy_key_aliases
|
|
37
|
-
from crawlee.events import EventManager
|
|
38
|
-
|
|
39
|
-
from .events import Event, LocalEventManager, ScrapelyEventManager
|
|
40
33
|
from .charging_client import create_charging_client
|
|
41
34
|
from .errors import ForbiddenError
|
|
42
35
|
from .storage.api_client import ApiClient, ApiClientError
|
|
43
36
|
from .storage.client import is_storage_uuid
|
|
44
|
-
from .storage._metadata import StorageType, resolve_alias_from_storages_json
|
|
45
|
-
from .storage.patch_storage import (
|
|
46
|
-
disconnect_storage,
|
|
47
|
-
get_api_storage_factory,
|
|
48
|
-
get_smart_storage_client,
|
|
49
|
-
set_api_storage,
|
|
50
|
-
set_storage_client,
|
|
51
|
-
)
|
|
52
|
-
from .storage.scrapely_storage_client import ScrapelyStorageClient
|
|
53
|
-
from .storage.smart_scrapely_storage_client import SmartScrapelyStorageClient
|
|
54
37
|
from .types import (
|
|
55
38
|
AbortOptions,
|
|
56
39
|
ActorRun,
|
|
@@ -66,17 +49,74 @@ from .types import (
|
|
|
66
49
|
StartOptions,
|
|
67
50
|
WebhookOptions,
|
|
68
51
|
)
|
|
69
|
-
from .log import _configure_logging
|
|
70
52
|
from .webhook import Webhook
|
|
71
53
|
|
|
72
54
|
if TYPE_CHECKING:
|
|
55
|
+
from crawlee.events import EventManager
|
|
56
|
+
from crawlee.storage_clients._base import StorageClient
|
|
57
|
+
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
73
58
|
from scrapely_client import ApifyClientAsync
|
|
74
59
|
|
|
60
|
+
from .events import Event
|
|
61
|
+
from .storage._metadata import StorageType
|
|
62
|
+
from .storage.scrapely_storage_client import ScrapelyStorageClient
|
|
63
|
+
from .storage.smart_scrapely_storage_client import SmartScrapelyStorageClient
|
|
64
|
+
|
|
75
65
|
from .browser_configuration import BrowserConfiguration
|
|
76
66
|
from .proxy_configuration import ProxyConfiguration
|
|
77
67
|
|
|
78
68
|
logger = logging.getLogger(__name__)
|
|
79
69
|
|
|
70
|
+
# Names that need Crawlee, resolved on first use instead of at import: importing
|
|
71
|
+
# Crawlee costs ~1 s per run under gVisor. They stay module attributes
|
|
72
|
+
# (``scrapely.actor.Dataset`` works and can be monkeypatched): ``_lazy`` returns
|
|
73
|
+
# the module-global value when one is set, else imports and caches it.
|
|
74
|
+
_LAZY_ATTRS: Dict[str, Tuple[str, str]] = {
|
|
75
|
+
'CrawleeConfiguration': ('crawlee.configuration', 'Configuration'),
|
|
76
|
+
'StorageClient': ('crawlee.storage_clients._base', 'StorageClient'),
|
|
77
|
+
'Dataset': ('crawlee.storages', 'Dataset'),
|
|
78
|
+
'KeyValueStore': ('crawlee.storages', 'KeyValueStore'),
|
|
79
|
+
'RequestQueue': ('crawlee.storages', 'RequestQueue'),
|
|
80
|
+
'EventManager': ('crawlee.events', 'EventManager'),
|
|
81
|
+
'Event': ('scrapely.events', 'Event'),
|
|
82
|
+
'LocalEventManager': ('scrapely.events', 'LocalEventManager'),
|
|
83
|
+
'ScrapelyEventManager': ('scrapely.events', 'ScrapelyEventManager'),
|
|
84
|
+
'StorageType': ('scrapely.storage._metadata', 'StorageType'),
|
|
85
|
+
'resolve_alias_from_storages_json': ('scrapely.storage._metadata', 'resolve_alias_from_storages_json'),
|
|
86
|
+
'disconnect_storage': ('scrapely.storage.patch_storage', 'disconnect_storage'),
|
|
87
|
+
'get_api_storage_factory': ('scrapely.storage.patch_storage', 'get_api_storage_factory'),
|
|
88
|
+
'get_smart_storage_client': ('scrapely.storage.patch_storage', 'get_smart_storage_client'),
|
|
89
|
+
'set_api_storage': ('scrapely.storage.patch_storage', 'set_api_storage'),
|
|
90
|
+
'set_storage_client': ('scrapely.storage.patch_storage', 'set_storage_client'),
|
|
91
|
+
'ScrapelyStorageClient': ('scrapely.storage.scrapely_storage_client', 'ScrapelyStorageClient'),
|
|
92
|
+
'SmartScrapelyStorageClient': (
|
|
93
|
+
'scrapely.storage.smart_scrapely_storage_client',
|
|
94
|
+
'SmartScrapelyStorageClient',
|
|
95
|
+
),
|
|
96
|
+
'_configure_logging': ('scrapely.log', '_configure_logging'),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _lazy(name: str) -> Any:
|
|
101
|
+
"""Return a Crawlee-dependent module attribute, importing it on first use."""
|
|
102
|
+
value = globals().get(name, _MISSING)
|
|
103
|
+
if value is _MISSING:
|
|
104
|
+
import importlib
|
|
105
|
+
|
|
106
|
+
module_name, attr = _LAZY_ATTRS[name]
|
|
107
|
+
value = getattr(importlib.import_module(module_name), attr)
|
|
108
|
+
globals()[name] = value
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_MISSING = object()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def __getattr__(name: str) -> Any:
|
|
116
|
+
if name in _LAZY_ATTRS:
|
|
117
|
+
return _lazy(name)
|
|
118
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
119
|
+
|
|
80
120
|
# Terminal run statuses used by the run-control polling loops (call/call_task).
|
|
81
121
|
_RUN_TERMINAL_STATUSES = frozenset({'SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'})
|
|
82
122
|
|
|
@@ -324,7 +364,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
324
364
|
return
|
|
325
365
|
|
|
326
366
|
if options is None or options.configure_logging:
|
|
327
|
-
_configure_logging()
|
|
367
|
+
_lazy('_configure_logging')()
|
|
328
368
|
|
|
329
369
|
# Do NOT set _initialized until everything succeeds (failed init must be retryable).
|
|
330
370
|
cls._config = Configuration.get_global_config()
|
|
@@ -338,7 +378,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
338
378
|
for key, value in options.configuration.items():
|
|
339
379
|
cls._config.set(key, value)
|
|
340
380
|
|
|
341
|
-
crawlee_config = CrawleeConfiguration.get_global_configuration()
|
|
381
|
+
crawlee_config = _lazy('CrawleeConfiguration').get_global_configuration()
|
|
342
382
|
if cls._config.get('is_at_home'):
|
|
343
383
|
crawlee_config.purge_on_start = False
|
|
344
384
|
|
|
@@ -361,9 +401,9 @@ class Actor(metaclass=_ActorMeta):
|
|
|
361
401
|
'[Scrapely] Initializing API storage for run %s',
|
|
362
402
|
storage_opt['run_id'],
|
|
363
403
|
)
|
|
364
|
-
smart = set_api_storage(storage_opt)
|
|
365
|
-
elif isinstance(storage_opt, StorageClient):
|
|
366
|
-
smart = set_storage_client(storage_opt)
|
|
404
|
+
smart = _lazy('set_api_storage')(storage_opt)
|
|
405
|
+
elif isinstance(storage_opt, _lazy('StorageClient')):
|
|
406
|
+
smart = _lazy('set_storage_client')(storage_opt)
|
|
367
407
|
else:
|
|
368
408
|
raise TypeError(
|
|
369
409
|
'Actor.init() storage option must be ApiStorageConfig or StorageClient'
|
|
@@ -375,7 +415,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
375
415
|
|
|
376
416
|
if api_url and token and run_id:
|
|
377
417
|
logger.info('[Scrapely] Initializing API storage for run %s', run_id)
|
|
378
|
-
smart = set_api_storage({
|
|
418
|
+
smart = _lazy('set_api_storage')({
|
|
379
419
|
'api_url': api_url,
|
|
380
420
|
'token': token,
|
|
381
421
|
'run_id': run_id,
|
|
@@ -383,7 +423,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
383
423
|
elif not cls._config.get('is_at_home'):
|
|
384
424
|
# Local development without credentials: Smart → FS (Apify docs shape)
|
|
385
425
|
logger.info('[Scrapely] Initializing local file-system storage')
|
|
386
|
-
smart = set_api_storage()
|
|
426
|
+
smart = _lazy('set_api_storage')()
|
|
387
427
|
else:
|
|
388
428
|
raise RuntimeError(
|
|
389
429
|
'Missing required configuration for secure storage. '
|
|
@@ -392,8 +432,12 @@ class Actor(metaclass=_ActorMeta):
|
|
|
392
432
|
'or pass storage in Actor.init().'
|
|
393
433
|
)
|
|
394
434
|
|
|
395
|
-
cls._smart_storage =
|
|
396
|
-
|
|
435
|
+
cls._smart_storage = (
|
|
436
|
+
smart
|
|
437
|
+
if isinstance(smart, _lazy('SmartScrapelyStorageClient'))
|
|
438
|
+
else _lazy('get_smart_storage_client')()
|
|
439
|
+
)
|
|
440
|
+
factory = _lazy('get_api_storage_factory')()
|
|
397
441
|
cls._scrapely_storage = factory
|
|
398
442
|
if factory is not None and factory.has_credentials():
|
|
399
443
|
cls._client = factory.api_client
|
|
@@ -408,12 +452,12 @@ class Actor(metaclass=_ActorMeta):
|
|
|
408
452
|
|
|
409
453
|
event_manager: EventManager
|
|
410
454
|
if ws_url:
|
|
411
|
-
event_manager = ScrapelyEventManager(
|
|
455
|
+
event_manager = _lazy('ScrapelyEventManager')(
|
|
412
456
|
configuration=cls._config,
|
|
413
457
|
persist_state_interval=persist_interval,
|
|
414
458
|
)
|
|
415
459
|
else:
|
|
416
|
-
event_manager = LocalEventManager(
|
|
460
|
+
event_manager = _lazy('LocalEventManager')(
|
|
417
461
|
system_info_interval=timedelta(seconds=1),
|
|
418
462
|
persist_state_interval=persist_interval,
|
|
419
463
|
)
|
|
@@ -544,7 +588,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
544
588
|
cls._default_rq = None
|
|
545
589
|
cls._cached_input = None
|
|
546
590
|
try:
|
|
547
|
-
disconnect_storage()
|
|
591
|
+
_lazy('disconnect_storage')()
|
|
548
592
|
except BaseException as e:
|
|
549
593
|
cleanup_error = cleanup_error or e
|
|
550
594
|
logger.warning('disconnect_storage failed during shutdown: %s', e)
|
|
@@ -836,7 +880,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
836
880
|
|
|
837
881
|
target = cls._default_dataset
|
|
838
882
|
if dataset is not None:
|
|
839
|
-
target = dataset if isinstance(dataset, Dataset) else await cls.open_dataset(name=dataset)
|
|
883
|
+
target = dataset if isinstance(dataset, _lazy('Dataset')) else await cls.open_dataset(name=dataset)
|
|
840
884
|
is_default = target is cls._default_dataset or (
|
|
841
885
|
getattr(target, 'id', None) is not None and target.id == getattr(cls._default_dataset, 'id', None)
|
|
842
886
|
)
|
|
@@ -902,7 +946,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
902
946
|
|
|
903
947
|
if alias is not None:
|
|
904
948
|
storages_json = cls._env_config().get('actor_storages_json')
|
|
905
|
-
resolved = resolve_alias_from_storages_json(alias, storage_type, storages_json)
|
|
949
|
+
resolved = _lazy('resolve_alias_from_storages_json')(alias, storage_type, storages_json)
|
|
906
950
|
if resolved is not None:
|
|
907
951
|
id = resolved
|
|
908
952
|
alias = None
|
|
@@ -915,12 +959,12 @@ class Actor(metaclass=_ActorMeta):
|
|
|
915
959
|
|
|
916
960
|
@classmethod
|
|
917
961
|
def _storage_client_for_open(cls, *, force_cloud: bool = False) -> Optional[StorageClient]:
|
|
918
|
-
smart = cls._smart_storage or get_smart_storage_client()
|
|
962
|
+
smart = cls._smart_storage or _lazy('get_smart_storage_client')()
|
|
919
963
|
if smart is not None:
|
|
920
964
|
return smart.get_suitable_storage_client(force_cloud=force_cloud)
|
|
921
965
|
|
|
922
966
|
if force_cloud:
|
|
923
|
-
cloud = get_api_storage_factory()
|
|
967
|
+
cloud = _lazy('get_api_storage_factory')()
|
|
924
968
|
if cloud is None:
|
|
925
969
|
# Build from config like TS _buildStorageOpenOptions
|
|
926
970
|
config = cls.get_config()
|
|
@@ -928,7 +972,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
928
972
|
token = config.get('token')
|
|
929
973
|
run_id = config.get('actor_run_id')
|
|
930
974
|
if api_url and token and run_id:
|
|
931
|
-
return ScrapelyStorageClient(api_url=api_url, token=token, run_id=run_id)
|
|
975
|
+
return _lazy('ScrapelyStorageClient')(api_url=api_url, token=token, run_id=run_id)
|
|
932
976
|
raise RuntimeError('Cloud storage client is not configured')
|
|
933
977
|
return cloud
|
|
934
978
|
return None
|
|
@@ -958,10 +1002,10 @@ class Actor(metaclass=_ActorMeta):
|
|
|
958
1002
|
|
|
959
1003
|
if open_args['id'] is None and open_args['name'] is None and open_args['alias'] is None:
|
|
960
1004
|
if cls._default_dataset is None:
|
|
961
|
-
cls._default_dataset = await Dataset.open(storage_client=storage_client)
|
|
1005
|
+
cls._default_dataset = await _lazy('Dataset').open(storage_client=storage_client)
|
|
962
1006
|
return cls._default_dataset
|
|
963
1007
|
|
|
964
|
-
return await Dataset.open(
|
|
1008
|
+
return await _lazy('Dataset').open(
|
|
965
1009
|
id=open_args['id'],
|
|
966
1010
|
name=open_args['name'],
|
|
967
1011
|
alias=open_args['alias'],
|
|
@@ -993,10 +1037,10 @@ class Actor(metaclass=_ActorMeta):
|
|
|
993
1037
|
|
|
994
1038
|
if open_args['id'] is None and open_args['name'] is None and open_args['alias'] is None:
|
|
995
1039
|
if cls._default_kvs is None:
|
|
996
|
-
cls._default_kvs = await KeyValueStore.open(storage_client=storage_client)
|
|
1040
|
+
cls._default_kvs = await _lazy('KeyValueStore').open(storage_client=storage_client)
|
|
997
1041
|
return cls._default_kvs
|
|
998
1042
|
|
|
999
|
-
return await KeyValueStore.open(
|
|
1043
|
+
return await _lazy('KeyValueStore').open(
|
|
1000
1044
|
id=open_args['id'],
|
|
1001
1045
|
name=open_args['name'],
|
|
1002
1046
|
alias=open_args['alias'],
|
|
@@ -1028,10 +1072,10 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1028
1072
|
|
|
1029
1073
|
if open_args['id'] is None and open_args['name'] is None and open_args['alias'] is None:
|
|
1030
1074
|
if cls._default_rq is None:
|
|
1031
|
-
cls._default_rq = await RequestQueue.open(storage_client=storage_client)
|
|
1075
|
+
cls._default_rq = await _lazy('RequestQueue').open(storage_client=storage_client)
|
|
1032
1076
|
return cls._default_rq
|
|
1033
1077
|
|
|
1034
|
-
return await RequestQueue.open(
|
|
1078
|
+
return await _lazy('RequestQueue').open(
|
|
1035
1079
|
id=open_args['id'],
|
|
1036
1080
|
name=open_args['name'],
|
|
1037
1081
|
alias=open_args['alias'],
|
|
@@ -1710,7 +1754,7 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1710
1754
|
if cls._event_manager:
|
|
1711
1755
|
from crawlee.events import EventPersistStateData
|
|
1712
1756
|
cls._event_manager.emit(
|
|
1713
|
-
event=Event.PERSIST_STATE,
|
|
1757
|
+
event=_lazy('Event').PERSIST_STATE,
|
|
1714
1758
|
event_data=EventPersistStateData(is_migrating=True),
|
|
1715
1759
|
)
|
|
1716
1760
|
|
scrapely/patch_browser_pool.py
CHANGED
|
@@ -30,6 +30,11 @@ Mirrors the Node SDK ``patch-browser-pool.ts``:
|
|
|
30
30
|
gVisor-safe Chromium args under ``SCRAPELY_CONTAINER_RUNTIME=runsc``.
|
|
31
31
|
- Soft no-op when Playwright/Crawlee browsers are not installed (e.g. HTTP-only
|
|
32
32
|
actors).
|
|
33
|
+
- Crawlee is never imported by this module (it costs ~1 s under gVisor). The
|
|
34
|
+
Crawlee patches are applied at once when Crawlee's browser / Playwright
|
|
35
|
+
crawler modules are already loaded, otherwise by a post-import hook the
|
|
36
|
+
moment those modules finish loading — before actor code can use the classes.
|
|
37
|
+
The Playwright, Camoufox and pyppeteer patches are applied eagerly on import.
|
|
33
38
|
|
|
34
39
|
Location and proxy handling (the remote browser never takes a proxy):
|
|
35
40
|
|
|
@@ -63,9 +68,11 @@ from __future__ import annotations
|
|
|
63
68
|
|
|
64
69
|
import contextvars
|
|
65
70
|
import functools
|
|
71
|
+
import importlib.abc
|
|
66
72
|
import logging
|
|
67
73
|
import os
|
|
68
74
|
import re
|
|
75
|
+
import sys
|
|
69
76
|
from typing import Any, List, Optional, Tuple
|
|
70
77
|
from urllib.parse import SplitResult, parse_qsl, unquote, urlencode, urlsplit, urlunsplit
|
|
71
78
|
|
|
@@ -459,7 +466,9 @@ def _apply_crawler_patch() -> None:
|
|
|
459
466
|
``new_browser`` by the patched ``BrowserPool._get_new_page``.
|
|
460
467
|
"""
|
|
461
468
|
try:
|
|
462
|
-
|
|
469
|
+
# The defining module, not the ``crawlee.crawlers`` package: the
|
|
470
|
+
# post-import hook runs this while that package is still initialising.
|
|
471
|
+
from crawlee.crawlers._playwright._playwright_crawler import PlaywrightCrawler
|
|
463
472
|
except ImportError:
|
|
464
473
|
return
|
|
465
474
|
|
|
@@ -865,8 +874,92 @@ def _apply_pyppeteer_patch() -> None:
|
|
|
865
874
|
pyppeteer_mod._scrapely_cdp_patched = True
|
|
866
875
|
|
|
867
876
|
|
|
868
|
-
|
|
869
|
-
|
|
877
|
+
# ─── Crawlee patches without importing Crawlee ───────────────────────────────
|
|
878
|
+
#
|
|
879
|
+
# Importing Crawlee costs ~1 s per run under gVisor, so this module never
|
|
880
|
+
# imports it. The Crawlee patches mutate classes in place, so they only have to
|
|
881
|
+
# run before actor code uses those classes: they are applied immediately when
|
|
882
|
+
# the defining modules are already loaded, otherwise by a post-import hook the
|
|
883
|
+
# moment they finish executing (``import crawlee.browsers``,
|
|
884
|
+
# ``from crawlee.crawlers import PlaywrightCrawler``, a PlaywrightCrawler built
|
|
885
|
+
# by any template...). Behaviour is the same as applying them eagerly.
|
|
886
|
+
|
|
887
|
+
# Module name -> patch functions to run once that module has finished loading.
|
|
888
|
+
# ``crawlee.browsers`` imports BrowserPool, PlaywrightBrowserController and
|
|
889
|
+
# PlaywrightBrowserPlugin in its ``__init__``; any import of one of those
|
|
890
|
+
# submodules runs that ``__init__`` to completion first.
|
|
891
|
+
_CRAWLEE_POST_IMPORT_PATCHES: dict = {
|
|
892
|
+
'crawlee.browsers': (_apply_patch,),
|
|
893
|
+
'crawlee.crawlers._playwright._playwright_crawler': (_apply_crawler_patch,),
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
class _PostImportLoader(importlib.abc.Loader):
|
|
898
|
+
"""Delegating loader that runs the Scrapely patches after the module executes."""
|
|
899
|
+
|
|
900
|
+
def __init__(self, loader: Any, patches: tuple) -> None:
|
|
901
|
+
self._loader = loader
|
|
902
|
+
self._patches = patches
|
|
903
|
+
|
|
904
|
+
def create_module(self, spec: Any) -> Any:
|
|
905
|
+
return self._loader.create_module(spec)
|
|
906
|
+
|
|
907
|
+
def exec_module(self, module: Any) -> None:
|
|
908
|
+
# Leave the module looking untouched (resource readers, reload, ...).
|
|
909
|
+
spec = getattr(module, '__spec__', None)
|
|
910
|
+
if spec is not None:
|
|
911
|
+
spec.loader = self._loader
|
|
912
|
+
try:
|
|
913
|
+
module.__loader__ = self._loader
|
|
914
|
+
except Exception:
|
|
915
|
+
pass
|
|
916
|
+
self._loader.exec_module(module)
|
|
917
|
+
for patch in self._patches:
|
|
918
|
+
patch()
|
|
919
|
+
|
|
920
|
+
def __getattr__(self, name: str) -> Any:
|
|
921
|
+
return getattr(self._loader, name)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
class _CrawleePostImportFinder(importlib.abc.MetaPathFinder):
|
|
925
|
+
"""Meta path finder that hooks the Crawlee modules in ``_CRAWLEE_POST_IMPORT_PATCHES``."""
|
|
926
|
+
|
|
927
|
+
def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any:
|
|
928
|
+
patches = _CRAWLEE_POST_IMPORT_PATCHES.get(fullname)
|
|
929
|
+
if patches is None:
|
|
930
|
+
return None
|
|
931
|
+
for finder in sys.meta_path:
|
|
932
|
+
if finder is self or isinstance(finder, _CrawleePostImportFinder):
|
|
933
|
+
continue
|
|
934
|
+
find_spec = getattr(finder, 'find_spec', None)
|
|
935
|
+
if find_spec is None:
|
|
936
|
+
continue
|
|
937
|
+
spec = find_spec(fullname, path, target)
|
|
938
|
+
if spec is not None:
|
|
939
|
+
break
|
|
940
|
+
else:
|
|
941
|
+
return None
|
|
942
|
+
if spec.loader is None or not hasattr(spec.loader, 'exec_module'):
|
|
943
|
+
return spec
|
|
944
|
+
# One-shot: the patches are idempotent, but a module loads only once.
|
|
945
|
+
_CRAWLEE_POST_IMPORT_PATCHES.pop(fullname, None)
|
|
946
|
+
spec.loader = _PostImportLoader(spec.loader, patches)
|
|
947
|
+
return spec
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _install_crawlee_patches() -> None:
|
|
951
|
+
"""Apply the Crawlee patches now if Crawlee is loaded, else when it loads."""
|
|
952
|
+
for module_name in list(_CRAWLEE_POST_IMPORT_PATCHES):
|
|
953
|
+
if module_name in sys.modules:
|
|
954
|
+
for patch in _CRAWLEE_POST_IMPORT_PATCHES.pop(module_name):
|
|
955
|
+
patch()
|
|
956
|
+
if _CRAWLEE_POST_IMPORT_PATCHES and not any(
|
|
957
|
+
isinstance(finder, _CrawleePostImportFinder) for finder in sys.meta_path
|
|
958
|
+
):
|
|
959
|
+
sys.meta_path.insert(0, _CrawleePostImportFinder())
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
_install_crawlee_patches()
|
|
870
963
|
_apply_camoufox_patch()
|
|
871
964
|
_apply_raw_playwright_patch()
|
|
872
965
|
_apply_pyppeteer_patch()
|
scrapely/storage/__init__.py
CHANGED
|
@@ -1,27 +1,92 @@
|
|
|
1
1
|
"""Scrapely storage: Crawlee StorageClient backends + HTTP helpers."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from crawlee.storage_clients import MemoryStorageClient, StorageClient
|
|
5
|
-
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
6
4
|
|
|
5
|
+
# Only the Crawlee-free HTTP helpers load eagerly, so ``Actor`` can use
|
|
6
|
+
# ``scrapely.storage.api_client`` / ``.client`` without importing Crawlee.
|
|
7
|
+
# The Crawlee StorageClient backends load on first attribute access.
|
|
7
8
|
from .api_client import ApiClient, ApiClientError
|
|
8
9
|
from .client import ApiStorageClient
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
|
|
11
|
+
_LAZY_ATTRS = {
|
|
12
|
+
'Request': ('crawlee', 'Request'),
|
|
13
|
+
'MemoryStorageClient': ('crawlee.storage_clients', 'MemoryStorageClient'),
|
|
14
|
+
'StorageClient': ('crawlee.storage_clients', 'StorageClient'),
|
|
15
|
+
'Dataset': ('crawlee.storages', 'Dataset'),
|
|
16
|
+
'KeyValueStore': ('crawlee.storages', 'KeyValueStore'),
|
|
17
|
+
'RequestQueue': ('crawlee.storages', 'RequestQueue'),
|
|
18
|
+
'ScrapelyFileSystemStorageClient': ('scrapely.storage._file_system', 'ScrapelyFileSystemStorageClient'),
|
|
19
|
+
# Apify-style public alias
|
|
20
|
+
'FileSystemStorageClient': ('scrapely.storage._file_system', 'ScrapelyFileSystemStorageClient'),
|
|
21
|
+
'ScrapelyStorageClient': ('scrapely.storage.scrapely_storage_client', 'ScrapelyStorageClient'),
|
|
22
|
+
'SmartScrapelyStorageClient': ('scrapely.storage.smart_scrapely_storage_client', 'SmartScrapelyStorageClient'),
|
|
23
|
+
**{
|
|
24
|
+
name: ('scrapely.storage.patch_storage', name)
|
|
25
|
+
for name in (
|
|
26
|
+
'disconnect_storage',
|
|
27
|
+
'get_api_storage_factory',
|
|
28
|
+
'get_auto_saved_value',
|
|
29
|
+
'get_smart_storage_client',
|
|
30
|
+
'is_api_storage_configured',
|
|
31
|
+
'persist_autosaved_values',
|
|
32
|
+
'set_api_storage',
|
|
33
|
+
'set_storage_client',
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Submodules that used to be bound as attributes by this package's eager imports.
|
|
39
|
+
_LAZY_SUBMODULES = frozenset({
|
|
40
|
+
'_file_system',
|
|
41
|
+
'_metadata',
|
|
42
|
+
'patch_storage',
|
|
43
|
+
'scrapely_dataset_client',
|
|
44
|
+
'scrapely_kvs_client',
|
|
45
|
+
'scrapely_rq_client',
|
|
46
|
+
'scrapely_storage_client',
|
|
47
|
+
'smart_scrapely_storage_client',
|
|
48
|
+
'utils',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __getattr__(name: str) -> Any:
|
|
53
|
+
target = _LAZY_ATTRS.get(name)
|
|
54
|
+
if target is not None:
|
|
55
|
+
import importlib
|
|
56
|
+
|
|
57
|
+
value = getattr(importlib.import_module(target[0]), target[1])
|
|
58
|
+
globals()[name] = value
|
|
59
|
+
return value
|
|
60
|
+
if name in _LAZY_SUBMODULES:
|
|
61
|
+
import importlib
|
|
62
|
+
|
|
63
|
+
return importlib.import_module(f'{__name__}.{name}')
|
|
64
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def __dir__() -> list:
|
|
68
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS) | _LAZY_SUBMODULES)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
from crawlee import Request
|
|
73
|
+
from crawlee.storage_clients import MemoryStorageClient, StorageClient
|
|
74
|
+
from crawlee.storages import Dataset, KeyValueStore, RequestQueue
|
|
75
|
+
|
|
76
|
+
from ._file_system import ScrapelyFileSystemStorageClient
|
|
77
|
+
from ._file_system import ScrapelyFileSystemStorageClient as FileSystemStorageClient
|
|
78
|
+
from .patch_storage import (
|
|
79
|
+
disconnect_storage,
|
|
80
|
+
get_api_storage_factory,
|
|
81
|
+
get_auto_saved_value,
|
|
82
|
+
get_smart_storage_client,
|
|
83
|
+
is_api_storage_configured,
|
|
84
|
+
persist_autosaved_values,
|
|
85
|
+
set_api_storage,
|
|
86
|
+
set_storage_client,
|
|
87
|
+
)
|
|
88
|
+
from .scrapely_storage_client import ScrapelyStorageClient
|
|
89
|
+
from .smart_scrapely_storage_client import SmartScrapelyStorageClient
|
|
25
90
|
|
|
26
91
|
__all__ = [
|
|
27
92
|
'ApiClient',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
scrapely/__init__.py,sha256=
|
|
1
|
+
scrapely/__init__.py,sha256=urKlJYfwafwGuUNCaDEPM0wK5H_krI8jYNKys8dsh9U,9661
|
|
2
2
|
scrapely/_browser_locations.py,sha256=8QVVnkms2bDn3_LDYgYDR59MvFx8757l_JYnZ7xb3oI,145269
|
|
3
|
-
scrapely/actor.py,sha256=
|
|
3
|
+
scrapely/actor.py,sha256=fNxlB2KxattbBh6aMo1p_ALrDQ3GdbJa03w_6DgIkjs,85310
|
|
4
4
|
scrapely/browser_configuration.py,sha256=jAZapNSHLFW05B96df4wNBtcyiiU9DA3ml0yTL1FwfA,5770
|
|
5
5
|
scrapely/cdp_retry.py,sha256=hdTJEAIKhApM7IOk1DrQEl8NoflSJSmqd4RbNJ2IsgI,8540
|
|
6
6
|
scrapely/charging.py,sha256=ns-kuUJtl8hBI4-3iBisC62BggJG67yYOa1yVVItoUI,83800
|
|
@@ -8,7 +8,7 @@ scrapely/charging_client.py,sha256=-4q-Ww7gu9EZXpM9QIn1Vd4vwAXmgL_WEKqaQIx3ViA,3
|
|
|
8
8
|
scrapely/configuration.py,sha256=Of-lKm7hoPGUQO8d08kV7fAy2co9QTfb8Of_8ds5PSA,17028
|
|
9
9
|
scrapely/errors.py,sha256=DCAlVvE3jToZuPEG2HVDL7i8D8hLboomQ5JpSOcsXoQ,4107
|
|
10
10
|
scrapely/log.py,sha256=iVKoQdMLQXEWz1STzbSXrNUxsS_FNwiIdqoEg7CzeqU,1440
|
|
11
|
-
scrapely/patch_browser_pool.py,sha256=
|
|
11
|
+
scrapely/patch_browser_pool.py,sha256=R-1QDKa-ZvfdQ9wgyA8JCtmQqD9v7dOsaCZnSUoyf7U,42214
|
|
12
12
|
scrapely/pricing_types.py,sha256=J6wCddZd-U1MYeOtaaEdeHSsp1HBgiQLfvSInBOvhIo,5305
|
|
13
13
|
scrapely/proxy_configuration.py,sha256=4passyiUBfBsuzaBDyf5003rY0ayLYwwSnev0FaxCs0,38125
|
|
14
14
|
scrapely/py.typed,sha256=XtWlywqUjc6x_M2gAXlDtYXbAsYkCjKZEjFyJvaHvm4,105
|
|
@@ -22,7 +22,7 @@ scrapely/events/_types.py,sha256=XslMIEAQOe7othqevnxO86c7i7AysbvDLHH0lep64QU,599
|
|
|
22
22
|
scrapely/events/event_manager.py,sha256=9x3VqNy1gBi5tNQRv_1KptCRJQ9QnPwCDLT7JdlvjIM,12774
|
|
23
23
|
scrapely/request_loaders/__init__.py,sha256=aNgr4pRt3MziUbU6UQ_bjTT3moMNvvyUsPeCJyMQcwU,196
|
|
24
24
|
scrapely/request_loaders/_scrapely_request_list.py,sha256=vguj-EvBjvrGY5_vf2fGXnJBfnhlPeRjdkjPFaxUA7g,7628
|
|
25
|
-
scrapely/storage/__init__.py,sha256=
|
|
25
|
+
scrapely/storage/__init__.py,sha256=GWVzHJPlcsZp0bDCZwvYFwQD-JXSkoEYGLOFC1siLYw,3852
|
|
26
26
|
scrapely/storage/_metadata.py,sha256=WWawBnVN0b1iK2K-SlxZXUc6czb5wXWf8iC_0tzkgAQ,4764
|
|
27
27
|
scrapely/storage/api_client.py,sha256=uhIU5LnOmW0x6MOMt1H7iWlvs-b55Sn4zVCgAnynTug,22900
|
|
28
28
|
scrapely/storage/client.py,sha256=dfTD3424kb0AAFdFU5F6HMcS2XaKcJ1ACiViYniKDXg,22562
|
|
@@ -39,10 +39,10 @@ scrapely/storage/_file_system/_key_value_store_client.py,sha256=4Ipgbh4qEqAkwQJ9
|
|
|
39
39
|
scrapely/storage/_file_system/_storage_client.py,sha256=aaGJ4P0QAtIgEMl6yHpuobeWFD5M4rMC5HwhOOvuGuo,1794
|
|
40
40
|
scrapely/storage_clients/__init__.py,sha256=cs1YX7JV2_GKBXeiiJgkQFali08b5_y_jH6Yw7iROAA,656
|
|
41
41
|
scrapely/storages/__init__.py,sha256=4PAELhJNZ3exJ7Uouicn-bCQ58NafKjhLBixQ8ATQc4,173
|
|
42
|
-
scrapely_python-0.1.
|
|
43
|
-
scrapely_python-0.1.
|
|
44
|
-
scrapely_python-0.1.
|
|
45
|
-
scrapely_python-0.1.
|
|
46
|
-
scrapely_python-0.1.
|
|
47
|
-
scrapely_python-0.1.
|
|
48
|
-
scrapely_python-0.1.
|
|
42
|
+
scrapely_python-0.1.23.dist-info/licenses/LICENSE,sha256=pQGY0XwjIYh-a2HfawxHq1en53bXkFkDj2Q70jKWHr4,1088
|
|
43
|
+
scrapely_python-0.1.23.dist-info/licenses/LICENSE-APACHE,sha256=N8m0FzMq8i4UbTgXZvKEZJSD-27OF8pWZYS_atz4kTU,11355
|
|
44
|
+
scrapely_python-0.1.23.dist-info/licenses/NOTICE,sha256=DEsNrw6UdSIqdYYqMWVFgJnyt6jUivTD7iFU6YM_e1A,1122
|
|
45
|
+
scrapely_python-0.1.23.dist-info/METADATA,sha256=AY2FmyNKFInqRuBqZ2HnLSUVjz-UZ4bKeRTV5OsgPTU,3487
|
|
46
|
+
scrapely_python-0.1.23.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
47
|
+
scrapely_python-0.1.23.dist-info/top_level.txt,sha256=T4B9UMrs4q99QSAFyD1zdXhl40KnLhBKqrDkPhOTsmY,9
|
|
48
|
+
scrapely_python-0.1.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.22.dist-info → scrapely_python-0.1.23.dist-info}/licenses/LICENSE-APACHE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|