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.
Files changed (58) hide show
  1. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/PKG-INFO +4 -5
  2. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/README.md +1 -1
  3. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/pyproject.toml +4 -5
  4. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/__init__.py +1 -1
  5. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/actor.py +5 -4
  6. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/proxy_configuration.py +29 -6
  7. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/PKG-INFO +4 -5
  8. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/requires.txt +1 -1
  9. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_proxy_configuration.py +129 -2
  10. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/setup.cfg +0 -0
  11. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/_browser_locations.py +0 -0
  12. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/browser_configuration.py +0 -0
  13. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/charging.py +0 -0
  14. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/charging_client.py +0 -0
  15. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/configuration.py +0 -0
  16. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/crypto/__init__.py +0 -0
  17. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/crypto/_secrets.py +0 -0
  18. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/errors.py +0 -0
  19. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/__init__.py +0 -0
  20. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/_types.py +0 -0
  21. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/events/event_manager.py +0 -0
  22. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/log.py +0 -0
  23. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/patch_browser_pool.py +0 -0
  24. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/pricing_types.py +0 -0
  25. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/py.typed +0 -0
  26. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/request_loaders/__init__.py +0 -0
  27. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/request_loaders/_scrapely_request_list.py +0 -0
  28. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/__init__.py +0 -0
  29. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/__init__.py +0 -0
  30. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/_key_value_store_client.py +0 -0
  31. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_file_system/_storage_client.py +0 -0
  32. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/_metadata.py +0 -0
  33. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/api_client.py +0 -0
  34. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/client.py +0 -0
  35. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/patch_storage.py +0 -0
  36. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_dataset_client.py +0 -0
  37. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_kvs_client.py +0 -0
  38. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_rq_client.py +0 -0
  39. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/scrapely_storage_client.py +0 -0
  40. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/smart_scrapely_storage_client.py +0 -0
  41. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/types.py +0 -0
  42. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage/utils.py +0 -0
  43. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storage_clients/__init__.py +0 -0
  44. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/storages/__init__.py +0 -0
  45. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/types.py +0 -0
  46. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely/webhook.py +0 -0
  47. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/SOURCES.txt +0 -0
  48. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/dependency_links.txt +0 -0
  49. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/src/scrapely_python.egg-info/top_level.txt +0 -0
  50. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_apify_parity.py +0 -0
  51. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_browser_configuration.py +0 -0
  52. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_charging_parity.py +0 -0
  53. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_charging_pending.py +0 -0
  54. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_parity.py +0 -0
  55. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_patch_browser_pool.py +0 -0
  56. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_run_control.py +0 -0
  57. {scrapely_python-0.1.0 → scrapely_python-0.1.2}/tests/test_secrets.py +0 -0
  58. {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.0
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.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>=61.0", "wheel"]
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.0"
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 = {text = "MIT"}
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.0,<2",
30
+ "scrapely-client-python>=1.0.2,<2",
32
31
  "pydantic>=2.0.0",
33
32
  "cryptography>=42.0.0",
34
33
  ]
@@ -132,7 +132,7 @@ from .errors import (
132
132
  raise_for_status,
133
133
  )
134
134
 
135
- __version__ = '0.1.0'
135
+ __version__ = '0.1.2'
136
136
  __all__ = [
137
137
  # Core
138
138
  'Actor',
@@ -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): `useApifyProxy: false` explicitly disables the PLATFORM proxy only —
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
- # {useApifyProxy: false, proxyUrls: [...]}; returning None for it would
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.get('useApifyProxy') is False and not (
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.get('useApifyProxy', False):
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 Apify Proxy! It is not allowed to set '
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 (best-effort, never fatal): GET
643
- # {status_url}/?format=json *through the platform gateway* so the proxy
644
- # handshake itself authenticates the run's credentials. Parity with the
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.0
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.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.
@@ -1,7 +1,7 @@
1
1
  httpx>=0.25.0
2
2
  crawlee<2.0.0,>=1.8.0
3
3
  websockets>=12.0
4
- scrapely-client-python<2,>=1.0.0
4
+ scrapely-client-python<2,>=1.0.2
5
5
  pydantic>=2.0.0
6
6
  cryptography>=42.0.0
7
7
 
@@ -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 Apify Proxy'):
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 Apify Proxy'):
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']},