scrapely-python 0.1.0__tar.gz → 0.1.2__tar.gz
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_python-0.1.0 → scrapely_python-0.1.2}/PKG-INFO +4 -5
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/README.md +1 -1
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/pyproject.toml +4 -5
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/__init__.py +1 -1
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/actor.py +5 -4
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/proxy_configuration.py +29 -6
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/PKG-INFO +4 -5
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/requires.txt +1 -1
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_proxy_configuration.py +129 -2
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/setup.cfg +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/_browser_locations.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/browser_configuration.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/charging.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/charging_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/configuration.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/crypto/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/crypto/_secrets.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/errors.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/_types.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/event_manager.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/log.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/patch_browser_pool.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/pricing_types.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/py.typed +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/request_loaders/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_metadata.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/api_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/patch_storage.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_rq_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/types.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/utils.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage_clients/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storages/__init__.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/types.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/webhook.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/top_level.txt +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_apify_parity.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_browser_configuration.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_charging_parity.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_charging_pending.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_parity.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_patch_browser_pool.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_run_control.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_secrets.py +0 -0
- {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_storage.py +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapely-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform
|
|
5
5
|
Author-email: Scrapely <support@scrape.ly>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://scrape.ly
|
|
8
8
|
Project-URL: Documentation, https://docs.scrape.ly
|
|
9
9
|
Project-URL: Repository, https://github.com/SandeepDev1/scrapely
|
|
10
10
|
Keywords: scraping,actor,serverless,apify,scrapely
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -22,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
22
21
|
Requires-Dist: httpx>=0.25.0
|
|
23
22
|
Requires-Dist: crawlee<2.0.0,>=1.8.0
|
|
24
23
|
Requires-Dist: websockets>=12.0
|
|
25
|
-
Requires-Dist: scrapely-client-python<2,>=1.0.
|
|
24
|
+
Requires-Dist: scrapely-client-python<2,>=1.0.2
|
|
26
25
|
Requires-Dist: pydantic>=2.0.0
|
|
27
26
|
Requires-Dist: cryptography>=42.0.0
|
|
28
27
|
Provides-Extra: dev
|
|
@@ -43,7 +42,7 @@ Build Python Actors for the [Scrapely](https://scrape.ly) platform with lifecycl
|
|
|
43
42
|
Scrapely requires Python 3.9 or newer.
|
|
44
43
|
|
|
45
44
|
```bash
|
|
46
|
-
python -m pip install scrapely
|
|
45
|
+
python -m pip install scrapely-python
|
|
47
46
|
```
|
|
48
47
|
|
|
49
48
|
Install the appropriate Crawlee/Playwright browser dependencies separately when your Actor uses a browser.
|
|
@@ -7,7 +7,7 @@ Build Python Actors for the [Scrapely](https://scrape.ly) platform with lifecycl
|
|
|
7
7
|
Scrapely requires Python 3.9 or newer.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
python -m pip install scrapely
|
|
10
|
+
python -m pip install scrapely-python
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Install the appropriate Crawlee/Playwright browser dependencies separately when your Actor uses a browser.
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scrapely-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Scrapely", email = "support@scrape.ly"}
|
|
13
13
|
]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
|
-
"License :: OSI Approved :: MIT License",
|
|
18
17
|
"Operating System :: OS Independent",
|
|
19
18
|
"Programming Language :: Python :: 3",
|
|
20
19
|
"Programming Language :: Python :: 3.11",
|
|
@@ -28,7 +27,7 @@ dependencies = [
|
|
|
28
27
|
"httpx>=0.25.0",
|
|
29
28
|
"crawlee>=1.8.0,<2.0.0",
|
|
30
29
|
"websockets>=12.0",
|
|
31
|
-
"scrapely-client-python>=1.0.
|
|
30
|
+
"scrapely-client-python>=1.0.2,<2",
|
|
32
31
|
"pydantic>=2.0.0",
|
|
33
32
|
"cryptography>=42.0.0",
|
|
34
33
|
]
|
|
@@ -1776,19 +1776,20 @@ class Actor(metaclass=_ActorMeta):
|
|
|
1776
1776
|
"""
|
|
1777
1777
|
cls._ensure_initialized('create_proxy_configuration')
|
|
1778
1778
|
|
|
1779
|
-
from .proxy_configuration import ProxyConfiguration
|
|
1779
|
+
from .proxy_configuration import ProxyConfiguration, _input_use_platform_proxy
|
|
1780
1780
|
|
|
1781
1781
|
# Handle actor_proxy_input (Node SDK normalizeProxyInput/createProxyConfiguration
|
|
1782
|
-
# parity): `
|
|
1782
|
+
# parity): `useScrapelyProxy: false` (or the legacy `useApifyProxy: false`; a
|
|
1783
|
+
# boolean useScrapelyProxy wins) explicitly disables the PLATFORM proxy only —
|
|
1783
1784
|
# custom proxies (proxyUrls / tiered lists / new_url_function) from the same
|
|
1784
1785
|
# input must still be used. The standard "Custom proxy URLs" input shape is
|
|
1785
|
-
# {
|
|
1786
|
+
# {useScrapelyProxy: false, proxyUrls: [...]}; returning None for it would
|
|
1786
1787
|
# silently drop the user's own proxies and crawl on their real IPs. We only
|
|
1787
1788
|
# bail when no custom proxies are given either. A MISSING key falls back to
|
|
1788
1789
|
# platform mode when the server injected credentials — input like
|
|
1789
1790
|
# {apifyProxyGroups: [...]} must not silently disable proxying.
|
|
1790
1791
|
if actor_proxy_input is not None:
|
|
1791
|
-
if actor_proxy_input
|
|
1792
|
+
if _input_use_platform_proxy(actor_proxy_input) is False and not (
|
|
1792
1793
|
proxy_urls
|
|
1793
1794
|
or new_url_function
|
|
1794
1795
|
or tiered_proxy_urls
|
|
@@ -108,6 +108,19 @@ def _sanitize_fragment(value: str) -> str:
|
|
|
108
108
|
return re.sub(r'[:@/?#\s]', '', value)
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def _input_use_platform_proxy(actor_proxy_input: Dict[str, Any]) -> Optional[bool]:
|
|
112
|
+
"""The proxy input field's platform-proxy switch, or None when it has none.
|
|
113
|
+
|
|
114
|
+
`useScrapelyProxy` wins when it is a boolean; otherwise the legacy
|
|
115
|
+
`useApifyProxy` key of existing inputs and ported actors decides.
|
|
116
|
+
"""
|
|
117
|
+
for key in ('useScrapelyProxy', 'useApifyProxy'):
|
|
118
|
+
value = actor_proxy_input.get(key)
|
|
119
|
+
if isinstance(value, bool):
|
|
120
|
+
return value
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
111
124
|
def _platform_env() -> Optional[Dict[str, Any]]:
|
|
112
125
|
"""Read platform proxy credentials from the environment (or None)."""
|
|
113
126
|
hostname = os.environ.get('SCRAPELY_PROXY_HOSTNAME') or os.environ.get('APIFY_PROXY_HOSTNAME')
|
|
@@ -213,7 +226,7 @@ class ProxyConfiguration:
|
|
|
213
226
|
|
|
214
227
|
# If actor_proxy_input is provided, extract settings from it
|
|
215
228
|
if actor_proxy_input:
|
|
216
|
-
if actor_proxy_input
|
|
229
|
+
if _input_use_platform_proxy(actor_proxy_input):
|
|
217
230
|
self._groups = actor_proxy_input.get('apifyProxyGroups', []) or []
|
|
218
231
|
self._country_code = actor_proxy_input.get('apifyProxyCountry')
|
|
219
232
|
self._subdivision_code = actor_proxy_input.get('apifyProxySubdivision')
|
|
@@ -291,7 +304,7 @@ class ProxyConfiguration:
|
|
|
291
304
|
)
|
|
292
305
|
if has_real_custom and (self._groups or self._country_code or self._subdivision_code or password):
|
|
293
306
|
raise ValueError(
|
|
294
|
-
'Cannot combine custom proxies with
|
|
307
|
+
'Cannot combine custom proxies with Scrapely Proxy! It is not allowed to set '
|
|
295
308
|
'"proxy_urls" or "new_url_function" combined with "groups", "country_code" '
|
|
296
309
|
'or "subdivision_code".'
|
|
297
310
|
)
|
|
@@ -480,7 +493,8 @@ class ProxyConfiguration:
|
|
|
480
493
|
Sends ``GET {status_url}/?format=json`` *through the platform gateway``
|
|
481
494
|
so the proxy handshake itself authenticates the run's credentials.
|
|
482
495
|
Mirrors the JS SDK: 4s timeout, 2 attempts, returns ``None`` when the
|
|
483
|
-
endpoint stays unreachable. Never raises.
|
|
496
|
+
endpoint stays unreachable. Never raises. ``fromPayload`` is True when
|
|
497
|
+
the gateway answered with its own status JSON rather than an HTTP error.
|
|
484
498
|
"""
|
|
485
499
|
import urllib.error
|
|
486
500
|
import urllib.request
|
|
@@ -514,6 +528,7 @@ class ProxyConfiguration:
|
|
|
514
528
|
'connected': payload.get('connected') is True,
|
|
515
529
|
'connectionError': payload.get('connectionError') or '',
|
|
516
530
|
'isManInTheMiddle': payload.get('isManInTheMiddle') is True,
|
|
531
|
+
'fromPayload': True,
|
|
517
532
|
}
|
|
518
533
|
except urllib.error.HTTPError as e:
|
|
519
534
|
if e.code == 407:
|
|
@@ -524,6 +539,7 @@ class ProxyConfiguration:
|
|
|
524
539
|
'SCRAPELY_PROXY_*/APIFY_PROXY_* credentials.'
|
|
525
540
|
),
|
|
526
541
|
'isManInTheMiddle': False,
|
|
542
|
+
'fromPayload': False,
|
|
527
543
|
}
|
|
528
544
|
if 590 <= e.code <= 599:
|
|
529
545
|
description = PROXY_STATUS_CODE_DESCRIPTIONS.get(e.code, 'upstream error')
|
|
@@ -531,6 +547,7 @@ class ProxyConfiguration:
|
|
|
531
547
|
'connected': False,
|
|
532
548
|
'connectionError': f'Platform proxy upstream error (HTTP {e.code}: {description}).',
|
|
533
549
|
'isManInTheMiddle': False,
|
|
550
|
+
'fromPayload': False,
|
|
534
551
|
}
|
|
535
552
|
# Other HTTP errors behave like connection errors: retry.
|
|
536
553
|
except Exception: # noqa: BLE001 - retry connection errors (Apify behavior)
|
|
@@ -623,6 +640,9 @@ class ProxyConfiguration:
|
|
|
623
640
|
|
|
624
641
|
Raises:
|
|
625
642
|
ValueError: If proxy configuration is invalid
|
|
643
|
+
ConnectionError: If the platform proxy reports that this configuration
|
|
644
|
+
cannot be used (for example an unknown proxy group), with the
|
|
645
|
+
proxy's ``connectionError`` as the message (Apify parity)
|
|
626
646
|
"""
|
|
627
647
|
if self._initialized:
|
|
628
648
|
return
|
|
@@ -639,9 +659,10 @@ class ProxyConfiguration:
|
|
|
639
659
|
if self._proxy_urls and self._new_url_function:
|
|
640
660
|
raise ValueError('Cannot specify both proxy_urls and new_url_function')
|
|
641
661
|
|
|
642
|
-
# Apify-parity access check
|
|
643
|
-
#
|
|
644
|
-
#
|
|
662
|
+
# Apify-parity access check: GET {status_url}/?format=json *through the
|
|
663
|
+
# platform gateway* so the proxy handshake itself authenticates the run's
|
|
664
|
+
# credentials. A "connected": false answer raises like Apify's
|
|
665
|
+
# _check_access; timeouts and HTTP errors only warn. Parity with the
|
|
645
666
|
# Apify SDKs (usesApifyProxy / uses_apify_proxy) and the JS Scrapely
|
|
646
667
|
# port (platformEnv): the check only applies when this configuration
|
|
647
668
|
# actually uses the platform proxy — BYOP-only configurations skip it
|
|
@@ -662,6 +683,8 @@ class ProxyConfiguration:
|
|
|
662
683
|
)
|
|
663
684
|
else:
|
|
664
685
|
self._is_man_in_the_middle = status['isManInTheMiddle']
|
|
686
|
+
if not status['connected'] and status['fromPayload']:
|
|
687
|
+
raise ConnectionError(status['connectionError'])
|
|
665
688
|
if not status['connected']:
|
|
666
689
|
logger.warning('Platform proxy access check failed: %s', status['connectionError'])
|
|
667
690
|
else:
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapely-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Scrapely Python SDK - A toolkit for building Actors on the Scrapely platform
|
|
5
5
|
Author-email: Scrapely <support@scrape.ly>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://scrape.ly
|
|
8
8
|
Project-URL: Documentation, https://docs.scrape.ly
|
|
9
9
|
Project-URL: Repository, https://github.com/SandeepDev1/scrapely
|
|
10
10
|
Keywords: scraping,actor,serverless,apify,scrapely
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -22,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
22
21
|
Requires-Dist: httpx>=0.25.0
|
|
23
22
|
Requires-Dist: crawlee<2.0.0,>=1.8.0
|
|
24
23
|
Requires-Dist: websockets>=12.0
|
|
25
|
-
Requires-Dist: scrapely-client-python<2,>=1.0.
|
|
24
|
+
Requires-Dist: scrapely-client-python<2,>=1.0.2
|
|
26
25
|
Requires-Dist: pydantic>=2.0.0
|
|
27
26
|
Requires-Dist: cryptography>=42.0.0
|
|
28
27
|
Provides-Extra: dev
|
|
@@ -43,7 +42,7 @@ Build Python Actors for the [Scrapely](https://scrape.ly) platform with lifecycl
|
|
|
43
42
|
Scrapely requires Python 3.9 or newer.
|
|
44
43
|
|
|
45
44
|
```bash
|
|
46
|
-
python -m pip install scrapely
|
|
45
|
+
python -m pip install scrapely-python
|
|
47
46
|
```
|
|
48
47
|
|
|
49
48
|
Install the appropriate Crawlee/Playwright browser dependencies separately when your Actor uses a browser.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
|
|
5
7
|
import pytest
|
|
6
8
|
|
|
7
9
|
from scrapely.proxy_configuration import ProxyConfiguration
|
|
@@ -87,7 +89,7 @@ async def test_sentinel_without_env_raises(monkeypatch: pytest.MonkeyPatch) -> N
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
async def test_real_custom_plus_groups_rejected() -> None:
|
|
90
|
-
with pytest.raises(ValueError, match='Cannot combine custom proxies with
|
|
92
|
+
with pytest.raises(ValueError, match='Cannot combine custom proxies with Scrapely Proxy'):
|
|
91
93
|
ProxyConfiguration(proxy_urls=['http://user:pass@byok.example.com:8080'], groups=['RESIDENTIAL'])
|
|
92
94
|
# Sentinel-only list may combine with groups (platform slot options).
|
|
93
95
|
pc = ProxyConfiguration(proxy_urls=['auto'], groups=['RESIDENTIAL'])
|
|
@@ -222,7 +224,7 @@ async def test_subdivision_and_country_validation() -> None:
|
|
|
222
224
|
with pytest.raises(ValueError, match='1-3 character ISO 3166-2'):
|
|
223
225
|
ProxyConfiguration(country_code='US', subdivision_code='ca')
|
|
224
226
|
# Subdivision combined with real BYOP URLs is a conflicting combination.
|
|
225
|
-
with pytest.raises(ValueError, match='Cannot combine custom proxies with
|
|
227
|
+
with pytest.raises(ValueError, match='Cannot combine custom proxies with Scrapely Proxy'):
|
|
226
228
|
ProxyConfiguration(
|
|
227
229
|
proxy_urls=['http://byok.example.com:8080'],
|
|
228
230
|
country_code='US',
|
|
@@ -500,6 +502,50 @@ async def test_access_check_runs_for_mixed_byop_and_sentinel(
|
|
|
500
502
|
assert any('access check succeeded' in r.message for r in caplog.records)
|
|
501
503
|
|
|
502
504
|
|
|
505
|
+
UNKNOWN_GROUP_ERROR = (
|
|
506
|
+
'Proxy group "GOOGLE_SERP" is not available on Scrapely. Available groups: RESIDENTIAL, DATACENTER.'
|
|
507
|
+
)
|
|
508
|
+
REFUSED_PAYLOAD = json.dumps({'connected': False, 'connectionError': UNKNOWN_GROUP_ERROR, 'isManInTheMiddle': False})
|
|
509
|
+
CONNECTED_PAYLOAD = '{"connected": true, "isManInTheMiddle": false}'
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _proxy_username(auth: str | None) -> str:
|
|
513
|
+
import base64
|
|
514
|
+
|
|
515
|
+
return base64.b64decode((auth or '').removeprefix('Basic ')).decode().split(':')[0]
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
async def test_access_check_raises_gateway_connection_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
519
|
+
with FakeGateway(200, REFUSED_PAYLOAD) as gateway:
|
|
520
|
+
_point_env_at_gateway(monkeypatch, gateway)
|
|
521
|
+
pc = ProxyConfiguration(groups=['GOOGLE_SERP'])
|
|
522
|
+
with pytest.raises(ConnectionError) as excinfo:
|
|
523
|
+
await pc.initialize()
|
|
524
|
+
|
|
525
|
+
assert str(excinfo.value) == UNKNOWN_GROUP_ERROR
|
|
526
|
+
assert [_proxy_username(auth) for _, auth in gateway.requests] == ['sub1,groups-GOOGLE_SERP']
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def test_access_check_known_groups_succeed(
|
|
530
|
+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
531
|
+
) -> None:
|
|
532
|
+
import logging
|
|
533
|
+
|
|
534
|
+
with FakeGateway(200, CONNECTED_PAYLOAD) as gateway:
|
|
535
|
+
_point_env_at_gateway(monkeypatch, gateway)
|
|
536
|
+
with caplog.at_level(logging.INFO, logger='scrapely.proxy_configuration'):
|
|
537
|
+
for groups in (None, ['RESIDENTIAL'], ['DATACENTER']):
|
|
538
|
+
await ProxyConfiguration(groups=groups).initialize()
|
|
539
|
+
|
|
540
|
+
assert [_proxy_username(auth) for _, auth in gateway.requests] == [
|
|
541
|
+
'sub1',
|
|
542
|
+
'sub1,groups-RESIDENTIAL',
|
|
543
|
+
'sub1,groups-DATACENTER',
|
|
544
|
+
]
|
|
545
|
+
assert sum('access check succeeded' in r.message for r in caplog.records) == 3
|
|
546
|
+
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
|
|
547
|
+
|
|
548
|
+
|
|
503
549
|
|
|
504
550
|
# ── Actor.create_proxy_configuration useApifyProxy semantics (Node parity) ────
|
|
505
551
|
|
|
@@ -567,6 +613,56 @@ async def test_actor_create_use_apify_proxy_false_with_explicit_kwarg_keeps_byop
|
|
|
567
613
|
assert await pc.new_url() == 'http://byop-b:8080'
|
|
568
614
|
|
|
569
615
|
|
|
616
|
+
# useScrapelyProxy is the canonical key; a boolean useScrapelyProxy wins over useApifyProxy.
|
|
617
|
+
|
|
618
|
+
async def test_actor_create_use_scrapely_proxy_true_uses_platform(initialized_actor) -> None:
|
|
619
|
+
pc = await initialized_actor.create_proxy_configuration(
|
|
620
|
+
actor_proxy_input={'useScrapelyProxy': True, 'apifyProxyGroups': ['RESIDENTIAL']},
|
|
621
|
+
check_access=False,
|
|
622
|
+
)
|
|
623
|
+
assert pc is not None
|
|
624
|
+
assert 'groups-RESIDENTIAL' in await pc.new_url()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
async def test_actor_create_use_scrapely_proxy_false_disables(initialized_actor) -> None:
|
|
628
|
+
pc = await initialized_actor.create_proxy_configuration(
|
|
629
|
+
actor_proxy_input={'useScrapelyProxy': False, 'apifyProxyGroups': ['RESIDENTIAL']},
|
|
630
|
+
check_access=False,
|
|
631
|
+
)
|
|
632
|
+
assert pc is None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
async def test_actor_create_use_scrapely_proxy_false_with_proxy_urls_keeps_byop(initialized_actor) -> None:
|
|
636
|
+
pc = await initialized_actor.create_proxy_configuration(
|
|
637
|
+
actor_proxy_input={'useScrapelyProxy': False, 'proxyUrls': ['http://byop-a:8080']},
|
|
638
|
+
check_access=False,
|
|
639
|
+
)
|
|
640
|
+
assert pc is not None
|
|
641
|
+
assert await pc.new_url() == 'http://byop-a:8080'
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
async def test_actor_create_use_scrapely_proxy_wins_over_use_apify_proxy(initialized_actor) -> None:
|
|
645
|
+
off = await initialized_actor.create_proxy_configuration(
|
|
646
|
+
actor_proxy_input={'useScrapelyProxy': False, 'useApifyProxy': True},
|
|
647
|
+
check_access=False,
|
|
648
|
+
)
|
|
649
|
+
assert off is None
|
|
650
|
+
on = await initialized_actor.create_proxy_configuration(
|
|
651
|
+
actor_proxy_input={'useScrapelyProxy': True, 'useApifyProxy': False, 'apifyProxyGroups': ['RESIDENTIAL']},
|
|
652
|
+
check_access=False,
|
|
653
|
+
)
|
|
654
|
+
assert on is not None
|
|
655
|
+
assert 'groups-RESIDENTIAL' in await on.new_url()
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def test_proxy_configuration_actor_input_use_scrapely_proxy() -> None:
|
|
659
|
+
pc = ProxyConfiguration(actor_proxy_input={'useScrapelyProxy': True, 'apifyProxyGroups': ['RESIDENTIAL']})
|
|
660
|
+
assert pc._groups == ['RESIDENTIAL']
|
|
661
|
+
custom = ProxyConfiguration(actor_proxy_input={'useScrapelyProxy': False, 'proxyUrls': ['http://byop-c:8080']})
|
|
662
|
+
assert custom._proxy_urls == ['http://byop-c:8080']
|
|
663
|
+
assert custom._groups == []
|
|
664
|
+
|
|
665
|
+
|
|
570
666
|
async def test_actor_create_no_options_without_env_returns_none(
|
|
571
667
|
initialized_actor, monkeypatch: pytest.MonkeyPatch
|
|
572
668
|
) -> None:
|
|
@@ -577,6 +673,37 @@ async def test_actor_create_no_options_without_env_returns_none(
|
|
|
577
673
|
assert pc is None
|
|
578
674
|
|
|
579
675
|
|
|
676
|
+
async def test_actor_create_refused_group_raises(initialized_actor, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
677
|
+
with FakeGateway(200, REFUSED_PAYLOAD) as gateway:
|
|
678
|
+
_point_env_at_gateway(monkeypatch, gateway)
|
|
679
|
+
with pytest.raises(ConnectionError, match='Proxy group "GOOGLE_SERP" is not available'):
|
|
680
|
+
await initialized_actor.create_proxy_configuration(groups=['GOOGLE_SERP'])
|
|
681
|
+
with pytest.raises(ConnectionError, match='Proxy group "GOOGLE_SERP" is not available'):
|
|
682
|
+
await initialized_actor.create_proxy_configuration(
|
|
683
|
+
actor_proxy_input={'useApifyProxy': True, 'apifyProxyGroups': ['GOOGLE_SERP']}
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
assert [_proxy_username(auth) for _, auth in gateway.requests] == [
|
|
687
|
+
'sub1,groups-GOOGLE_SERP',
|
|
688
|
+
'sub1,groups-GOOGLE_SERP',
|
|
689
|
+
]
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
async def test_actor_create_http_errors_and_timeouts_only_warn(
|
|
693
|
+
initialized_actor, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
694
|
+
) -> None:
|
|
695
|
+
import logging
|
|
696
|
+
|
|
697
|
+
with caplog.at_level(logging.WARNING, logger='scrapely.proxy_configuration'):
|
|
698
|
+
with FakeGateway(407, '') as gateway:
|
|
699
|
+
_point_env_at_gateway(monkeypatch, gateway)
|
|
700
|
+
assert await initialized_actor.create_proxy_configuration(groups=['GOOGLE_SERP']) is not None
|
|
701
|
+
assert await initialized_actor.create_proxy_configuration(groups=['GOOGLE_SERP']) is not None
|
|
702
|
+
|
|
703
|
+
assert any('authentication failed (HTTP 407)' in r.message for r in caplog.records)
|
|
704
|
+
assert any('access check timed out' in r.message for r in caplog.records)
|
|
705
|
+
|
|
706
|
+
|
|
580
707
|
async def test_actor_create_proxy_urls_from_input_without_flag(initialized_actor) -> None:
|
|
581
708
|
pc = await initialized_actor.create_proxy_configuration(
|
|
582
709
|
actor_proxy_input={'proxyUrls': ['http://byop-a:8080']},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/_storage_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_dataset_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_storage_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|