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,146 @@
|
|
|
1
|
+
"""Scrapely remote browser configuration (Evomi websocket parameters).
|
|
2
|
+
|
|
3
|
+
Passed to browser crawlers as ``browser_configuration``, next to
|
|
4
|
+
``proxy_configuration``, or to a raw ``chromium.launch(browser_configuration=...)``.
|
|
5
|
+
Outside the Scrapely remote browser (local runs, unpatched builds) the option
|
|
6
|
+
is accepted and ignored, so the same code runs everywhere. Mirrors the Node
|
|
7
|
+
SDK ``browser-configuration.ts``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any, Dict, Mapping, Optional
|
|
15
|
+
|
|
16
|
+
_BROWSER_OS_VALUES = ('windows', 'macos', 'android')
|
|
17
|
+
_MAX_TIMEZONE_LENGTH = 64
|
|
18
|
+
_MAX_LANGUAGE_LENGTH = 128
|
|
19
|
+
_COUNTRY_CODE_RE = re.compile(r'^[A-Z]{2}$')
|
|
20
|
+
_Q_VALUE_RE = re.compile(r';\s*q=', re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fail(field: str, message: str) -> None:
|
|
24
|
+
raise ValueError(f'BrowserConfiguration: "{field}" {message}')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _require_str(field: str, value: Any) -> str:
|
|
28
|
+
if not isinstance(value, str):
|
|
29
|
+
_fail(field, f'must be a string (got {type(value).__name__}).')
|
|
30
|
+
stripped = value.strip()
|
|
31
|
+
if not stripped:
|
|
32
|
+
_fail(field, 'must not be empty.')
|
|
33
|
+
return stripped
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalize_location_name(field: str, value: Any) -> str:
|
|
37
|
+
"""Evomi location format: lowercase, dots for spaces (``New York`` → ``new.york``)."""
|
|
38
|
+
return re.sub(r'\s+', '.', _require_str(field, value).lower())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _require_bool(field: str, value: Any) -> bool:
|
|
42
|
+
if not isinstance(value, bool):
|
|
43
|
+
_fail(field, f'must be a boolean (got {type(value).__name__}).')
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BrowserConfiguration:
|
|
48
|
+
"""Location and fingerprint options of the Scrapely remote browser.
|
|
49
|
+
|
|
50
|
+
The remote browser brings its own proxy — custom proxies are not supported
|
|
51
|
+
for browsers.
|
|
52
|
+
|
|
53
|
+
Example::
|
|
54
|
+
|
|
55
|
+
browser_configuration = await Actor.create_browser_configuration(country_code='US', state='California')
|
|
56
|
+
crawler = PlaywrightCrawler(browser_configuration=browser_configuration, request_handler=handler)
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
country_code: Exit country, ISO 3166-1 alpha-2 (``US``). Sent as ``proxy_country``.
|
|
60
|
+
state: Region name, not an ISO code (``California``). Sent as ``proxy_state``.
|
|
61
|
+
city: City name (``San Jose``). Sent as ``proxy_city``.
|
|
62
|
+
os: Fingerprint operating system: ``windows``, ``macos`` or ``android``.
|
|
63
|
+
timezone: IANA timezone (``Asia/Tokyo``). Defaults to the exit country's primary zone.
|
|
64
|
+
language: Comma-separated language tags without q-values (``de-CH,de``).
|
|
65
|
+
adblock: Built-in ad/tracker blocking. Browser default: off.
|
|
66
|
+
solve_cloudflare: Automatic Cloudflare Turnstile solving. Browser default: on.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
country_code: Optional[str] = None,
|
|
73
|
+
state: Optional[str] = None,
|
|
74
|
+
city: Optional[str] = None,
|
|
75
|
+
os: Optional[str] = None,
|
|
76
|
+
timezone: Optional[str] = None,
|
|
77
|
+
language: Optional[str] = None,
|
|
78
|
+
adblock: Optional[bool] = None,
|
|
79
|
+
solve_cloudflare: Optional[bool] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
options: Dict[str, Any] = {}
|
|
82
|
+
|
|
83
|
+
if country_code is not None:
|
|
84
|
+
code = _require_str('country_code', country_code).upper()
|
|
85
|
+
if not _COUNTRY_CODE_RE.match(code):
|
|
86
|
+
_fail('country_code', f'must be a two-letter ISO 3166-1 country code (got "{country_code}").')
|
|
87
|
+
options['country_code'] = code
|
|
88
|
+
|
|
89
|
+
if state is not None:
|
|
90
|
+
options['state'] = _normalize_location_name('state', state)
|
|
91
|
+
if city is not None:
|
|
92
|
+
options['city'] = _normalize_location_name('city', city)
|
|
93
|
+
|
|
94
|
+
if os is not None:
|
|
95
|
+
os_value = _require_str('os', os).lower()
|
|
96
|
+
if os_value not in _BROWSER_OS_VALUES:
|
|
97
|
+
_fail('os', f'must be one of {", ".join(_BROWSER_OS_VALUES)} (got "{os}").')
|
|
98
|
+
options['os'] = os_value
|
|
99
|
+
|
|
100
|
+
if timezone is not None:
|
|
101
|
+
tz = _require_str('timezone', timezone)
|
|
102
|
+
if len(tz) > _MAX_TIMEZONE_LENGTH:
|
|
103
|
+
_fail('timezone', f'must be at most {_MAX_TIMEZONE_LENGTH} characters.')
|
|
104
|
+
options['timezone'] = tz
|
|
105
|
+
|
|
106
|
+
if language is not None:
|
|
107
|
+
lang = _require_str('language', language)
|
|
108
|
+
if len(lang) > _MAX_LANGUAGE_LENGTH:
|
|
109
|
+
_fail('language', f'must be at most {_MAX_LANGUAGE_LENGTH} characters.')
|
|
110
|
+
if _Q_VALUE_RE.search(lang):
|
|
111
|
+
_fail('language', 'must be a plain tag list without q-values (e.g. "de-CH,de").')
|
|
112
|
+
options['language'] = lang
|
|
113
|
+
|
|
114
|
+
if adblock is not None:
|
|
115
|
+
options['adblock'] = _require_bool('adblock', adblock)
|
|
116
|
+
if solve_cloudflare is not None:
|
|
117
|
+
options['solve_cloudflare'] = _require_bool('solve_cloudflare', solve_cloudflare)
|
|
118
|
+
|
|
119
|
+
self._options: Mapping[str, Any] = MappingProxyType(options)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def options(self) -> Mapping[str, Any]:
|
|
123
|
+
"""Normalized options (read-only)."""
|
|
124
|
+
return self._options
|
|
125
|
+
|
|
126
|
+
def to_cdp_params(self) -> Dict[str, str]:
|
|
127
|
+
"""Remote browser websocket parameters set by this configuration."""
|
|
128
|
+
o = self._options
|
|
129
|
+
params: Dict[str, str] = {}
|
|
130
|
+
for option, param in (
|
|
131
|
+
('country_code', 'proxy_country'),
|
|
132
|
+
('state', 'proxy_state'),
|
|
133
|
+
('city', 'proxy_city'),
|
|
134
|
+
('os', 'os'),
|
|
135
|
+
('timezone', 'timezone'),
|
|
136
|
+
('language', 'language'),
|
|
137
|
+
):
|
|
138
|
+
if o.get(option):
|
|
139
|
+
params[param] = o[option]
|
|
140
|
+
for option, param in (('adblock', 'adblock'), ('solve_cloudflare', 'solve_cloudflare')):
|
|
141
|
+
if option in o:
|
|
142
|
+
params[param] = 'true' if o[option] else 'false'
|
|
143
|
+
return params
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
return f'BrowserConfiguration({dict(self._options)!r})'
|