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,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})'