Skip to content

Commit 822da37

Browse files
committed
feat: add BrightData provider
1 parent 0fb7b9a commit 822da37

File tree

10 files changed

+439
-4
lines changed

10 files changed

+439
-4
lines changed

.sphinx/providers/brightdata.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.. automodule:: proxyproviders.providers.brightdata
2+
:members:
3+
:undoc-members:
4+
:show-inheritance:

.sphinx/providers/webshare.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.. automodule:: proxyproviders.providers.webshare
22
:members:
3+
:undoc-members:
34
:show-inheritance:

.sphinx/proxyproviders.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ ProxyProviders Supported Providers
44
Current Providers Supported:
55

66
* `Webshare.io <https://www.webshare.io/?referral_code=3x5812idzzzp>`_ (referral link) - try out 10 free datacenter proxies
7+
* `BrightData <https://get.brightdata.com/davidteather>`_ (affiliate link)
78

89
Don't see a provider you need? Open an issue or submit a PR!
910

1011
.. _webshare:
11-
.. include:: providers/webshare.rst
12+
.. include:: providers/webshare.rst
13+
14+
.. _brightdata:
15+
.. include:: providers/brightdata.rst

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def some_function(provider: ProxyProvider):
9898
print(proxies)
9999

100100
webshare = Webshare(api_key="your_api_key")
101-
brightdata = BrightData(api_key="your_api_key")
101+
brightdata = BrightData(api_key="your_api_key", zone="my_zone")
102102

103103
some_function(webshare)
104104
some_function(brightdata)

proxyproviders/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .proxy_provider import ProxyProvider, ProxyConfig
22
from .providers.webshare import Webshare
3+
from .providers.brightdata import BrightData
34
from .models.proxy import Proxy
45

5-
__all__ = ["ProxyProvider", "ProxyConfig", "Webshare", "Proxy"]
6+
__all__ = ["ProxyProvider", "ProxyConfig", "Proxy", "Webshare", "BrightData"]

proxyproviders/models/proxy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ class Proxy:
3030
created_at: Optional[datetime] = None
3131
"""The timestamp when the proxy was created. Optional"""
3232

33-
protocols: List[str] = None
33+
protocols: Optional[List[str]] = None
3434
"""A list of connection protocols supported by the proxy, e.g., ['http', 'https']"""
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import requests
2+
from typing import List, Dict, Optional
3+
from ..proxy_provider import ProxyProvider, ProxyConfig
4+
from ..exceptions import ProxyFetchException,ProxyInvalidResponseException
5+
from ..models.proxy import Proxy
6+
import threading
7+
8+
class BrightData(ProxyProvider):
9+
"""BrightData (formerly luminati) is a proxy provider that offers residential and datacenter proxies.
10+
11+
Create an account `here <https://get.brightdata.com/davidteather>`_ (affiliate link).
12+
13+
You can find your API key in the account settings `here <https://brightdata.com/cp/setting/users>`_ then create "add token" with scope "limit" (`BrightData article <https://docs.brightdata.com/general/account/api-token>`_ for more info)
14+
15+
The BrightData API documentation is `here <https://docs.brightdata.com/api-reference/account-management-api/Get_active_Zones?playground=open>`_
16+
17+
:param api_key: Your BrightData API key
18+
:param zone: The zone ID/name to fetch proxies for (you can get this from the BrightData dashboard)
19+
:param username_suffix: Optional suffix to append to the username `more info <https://docs.brightdata.com/proxy-networks/config-options>`_ allows you to target region, city, etc. (requires use_super_proxy=True)
20+
:param use_super_proxy: Optional flag to use super proxy instead of targeting specific IPs, this is enabled by default. If you want to target specific IPs or have consistent IPs for a session, set this to False.
21+
:param config: Configuration for the proxy provider. View the ProxyConfig class docs for more information.
22+
23+
Example:
24+
25+
.. code-block:: python
26+
27+
from proxyproviders import BrightData
28+
29+
# Initialize the BrightData API client with an API key and a zone
30+
proxy_provider = BrightData(api_key="your-api-key", zone="my_zone")
31+
32+
# Fetch proxies
33+
proxies = proxy_provider.list_proxies() # returns one proxy for super proxy by default
34+
35+
# If you want to manage specific IPs
36+
proxy_provider = BrightData(api_key="your-api-key", zone="my_zone", use_super_proxy=False)
37+
proxies = proxy_provider.list_proxies() # returns multiple proxies for each IP in the zone (potentially thousands)
38+
"""
39+
_BASE_URL = "https://api.brightdata.com"
40+
_SUPER_PROXY_ADDRESS = "brd.superproxy.io"
41+
_SUPER_PROXY_PORT = 33335
42+
_PROTOCOLS: List[str] = ["http", "https"] # BrightData supports both HTTP and HTTPS
43+
44+
def __init__(self, api_key: str, zone: str, username_suffix: Optional[str] = None, use_super_proxy: Optional[bool] = True, config: Optional[ProxyConfig] = None):
45+
super().__init__(config)
46+
self.api_key = api_key
47+
self.zone = zone
48+
self.username_suffix = username_suffix
49+
self.use_super_proxy = use_super_proxy
50+
51+
def _fetch_proxies(self) -> List[Proxy]:
52+
username = self.get_zone_username(self.zone)
53+
if self.username_suffix:
54+
username += self.username_suffix
55+
56+
passwords = self.get_zone_passwords(self.zone)
57+
58+
if self.use_super_proxy:
59+
# Let the super proxy handle the IP rotation
60+
return [Proxy(
61+
id="super",
62+
username=username,
63+
password=passwords["passwords"][0],
64+
proxy_address=self._SUPER_PROXY_ADDRESS,
65+
port=self._SUPER_PROXY_PORT,
66+
protocols=self._PROTOCOLS,
67+
)]
68+
69+
proxies = []
70+
71+
# Fetch all IPs in the zone, and create a proxy for each
72+
# Brightdata doesn't let us target directly so we tell the superproxy what to do
73+
ips = self.list_all_ips_in_zone(self.zone)
74+
75+
for ip in ips:
76+
ip_targeted_username = username + f"-ip-{ip['ip']}"
77+
78+
proxies.append(Proxy(
79+
id=ip["ip"],
80+
username=ip_targeted_username,
81+
password=passwords["passwords"][0],
82+
proxy_address=self._SUPER_PROXY_ADDRESS,
83+
port=self._SUPER_PROXY_PORT,
84+
country_code=ip["country"],
85+
protocols=self._PROTOCOLS,
86+
))
87+
88+
return proxies
89+
90+
91+
def get_active_zones(self) -> Dict:
92+
"""Fetches active zones from BrightData API.
93+
94+
Response:
95+
96+
.. code-block:: json
97+
98+
[
99+
{
100+
"name": "zone1",
101+
"type": "dc",
102+
}
103+
]
104+
105+
"""
106+
return self._make_request("/zone/get_active_zones", "GET")
107+
108+
def get_zone_username(self, zone: str) -> str:
109+
"""Fetches zone username for the given zone ID from BrightData API.
110+
111+
Note: this isn't directly an API endpoint, I'm sort of reconstructing some things here and it seems to behave a little weird.
112+
113+
:param zone: The zone ID to fetch username for
114+
"""
115+
116+
data = self._make_request(f"/status", "GET", params={"zone": zone})
117+
118+
customer = data.get("customer")
119+
if not customer:
120+
raise ProxyInvalidResponseException("Failed to fetch customer data")
121+
122+
return f"brd-customer-{customer}-zone-{zone}"
123+
124+
125+
def get_zone_passwords(self, zone: str) -> Dict:
126+
"""Fetches zone passwords from BrightData API.
127+
128+
:param zone: The zone ID to fetch passwords for
129+
130+
Response:
131+
132+
133+
.. code-block:: json
134+
135+
{
136+
"passwords": [
137+
"password1",
138+
"password2",
139+
]
140+
}
141+
142+
"""
143+
return self._make_request(f"/zone/passwords", "GET", params={"zone": zone})
144+
145+
def list_all_ips_in_zone(self, zone: str, country: Optional[str] = None) -> Dict:
146+
"""Fetches all IPs in a zone from BrightData API.
147+
148+
:param zone: The zone ID to fetch IPs for
149+
:param country: Optional 2-letter country code to filter IPs by
150+
151+
Response:
152+
153+
154+
.. code-block:: json
155+
156+
[
157+
{
158+
"ip": "192.168.1.1",
159+
"country": "US",
160+
}
161+
]
162+
163+
"""
164+
return self._make_request(f"/zone/route_ips", "GET", params={"zone": zone, "list_countries": True, "country": country})
165+
166+
def _make_request(self, path: str, method: str, params: Optional[Dict] = None, json: Optional[Dict] = None) -> Dict:
167+
"""Makes a request to the BrightData API.
168+
169+
:param path: The path to the endpoint
170+
:param method: The HTTP method to use
171+
:param params: Optional parameters to include in the request
172+
:param json: Optional JSON data to include in the request
173+
"""
174+
175+
headers = {
176+
"Authorization": f"Bearer {self.api_key}",
177+
}
178+
179+
url = f"{self._BASE_URL}{path}"
180+
response = requests.request(method, url, headers=headers, params=params, json=json)
181+
182+
if response.status_code != 200:
183+
raise ProxyFetchException(f"Failed to fetch from BrightData, got status code {response.status_code}, text: {response.text}")
184+
185+
try:
186+
data = response.json()
187+
except Exception as e:
188+
raise ProxyInvalidResponseException(f"Failed to parse response: {str(e)}") from e
189+
190+
191+
return data

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import pytest
33
from proxyproviders.providers.webshare import Webshare
4+
from proxyproviders.providers.brightdata import BrightData
45
from proxyproviders.proxy_provider import ProxyConfig
56

67
@pytest.fixture
@@ -11,3 +12,13 @@ def webshare_provider():
1112
"""
1213
api_key = os.getenv("WEBSHARE_API_KEY", "test-api-key")
1314
return Webshare(api_key=api_key, config=ProxyConfig(refresh_interval=0))
15+
16+
@pytest.fixture
17+
def brightdata_provider():
18+
"""
19+
Fixture to create a BrightData provider instance for unit tests.
20+
"""
21+
# Use dummy credentials for testing
22+
api_key = os.getenv("BRIGHTDATA_API_KEY", "test-brightdata-api-key")
23+
zone = "test-zone"
24+
return BrightData(api_key=api_key, zone=zone)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import pytest
3+
import responses
4+
from proxyproviders.providers.brightdata import BrightData
5+
from proxyproviders.exceptions import (
6+
ProxyFetchException,
7+
ProxyConversionException,
8+
ProxyInvalidResponseException,
9+
)
10+
from proxyproviders.models.proxy import Proxy
11+
12+
skip_integration = pytest.mark.skipif(
13+
not (
14+
os.getenv("RUN_INTEGRATION_TESTS")
15+
and os.getenv("BRIGHTDATA_API_KEY")
16+
),
17+
reason="Integration tests require RUN_INTEGRATION_TESTS flag and valid BRIGHTDATA_API_KEY",
18+
)
19+
20+
@skip_integration
21+
def test_brightdata_integration():
22+
"""
23+
Integration test: Perform a real API call to BrightData.
24+
This sanity test ensures that the provider's structure and conversion functions
25+
work correctly when interacting with the live API.
26+
"""
27+
api_key = os.getenv("BRIGHTDATA_API_KEY")
28+
provider = BrightData(api_key=api_key, zone="static")
29+
30+
# List proxies; note that behavior will vary based on provider.use_super_proxy.
31+
proxies = provider.list_proxies(force_refresh=True)
32+
33+
assert isinstance(proxies, list)
34+
# If using super proxy mode, one proxy is expected; otherwise, there may be many.
35+
if provider.use_super_proxy:
36+
assert len(proxies) == 1
37+
proxy = proxies[0]
38+
assert hasattr(proxy, "port")
39+
assert isinstance(proxy.port, (int, str))
40+
else:
41+
for proxy in proxies:
42+
assert hasattr(proxy, "port")
43+
assert isinstance(proxy.port, (int, str))

0 commit comments

Comments
 (0)