Skip to content

Commit ddcc822

Browse files
authored
V0.2.1 - feat: Playwright Format Support (#2)
- Add ProxyFormat.PLAYWRIGHT enum with protocol handling - Implement comprehensive error type validation - Add test-based sanity check for format handlers - Fix IDE unreachable code warnings with pyright ignore - Remove runtime overhead, use test coverage instead
2 parents 84936cb + acd91c7 commit ddcc822

File tree

8 files changed

+445
-33
lines changed

8 files changed

+445
-33
lines changed

proxyproviders/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from . import algorithms
2-
from .models.proxy import Proxy
2+
from .models.proxy import Proxy, ProxyFormat
33
from .providers.brightdata import BrightData
44
from .providers.webshare import Webshare
55
from .proxy_provider import ProxyConfig, ProxyProvider
6+
from .version import __version__
67

78
__all__ = [
89
"ProxyProvider",
910
"ProxyConfig",
1011
"Proxy",
12+
"ProxyFormat",
1113
"Webshare",
1214
"BrightData",
1315
"algorithms",

proxyproviders/models/proxy.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class ProxyFormat(Enum):
1919
AIOHTTP = "aiohttp"
2020
"""AIOHTTP format, for use in aiohttp library HTTP calls"""
2121

22+
PLAYWRIGHT = "playwright"
23+
"""Playwright format, for use in Playwright browser automation"""
24+
2225
URL = "url"
2326
"""URL format, for use in URL strings"""
2427

@@ -90,29 +93,57 @@ def format(
9093
>>> proxy.format(ProxyFormat.HTTPX)
9194
{'http://': 'http://user:[email protected]:8080', 'https://': 'http://user:[email protected]:8080'}
9295
"""
96+
# Convert to ProxyFormat enum, handling both string and enum inputs
9397
if isinstance(format_type, str):
94-
format_type = ProxyFormat(format_type)
95-
96-
if format_type == ProxyFormat.URL:
97-
protocol = kwargs.get("protocol", "http")
98-
return self.to_url(protocol)
99-
100-
elif format_type == ProxyFormat.REQUESTS:
101-
protocols = kwargs.get("protocols", self.protocols or ["http", "https"])
102-
proxy_url = self.to_url("http")
103-
return {protocol: proxy_url for protocol in protocols}
104-
105-
elif format_type == ProxyFormat.CURL:
106-
return ["-x", self.to_url("http")]
107-
108-
elif format_type == ProxyFormat.HTTPX:
109-
# httpx uses 'http://' and 'https://' as keys
110-
proxy_url = self.to_url("http")
111-
return {"http://": proxy_url, "https://": proxy_url}
112-
113-
elif format_type == ProxyFormat.AIOHTTP:
114-
# aiohttp takes a single URL string
115-
return self.to_url("http")
116-
117-
else:
118-
raise ValueError(f"Unsupported format: {format_type}")
98+
try:
99+
format_type = ProxyFormat(format_type)
100+
except ValueError:
101+
raise ValueError(
102+
f"Invalid format type: '{format_type}'. Valid options are: {[f.value for f in ProxyFormat]}"
103+
)
104+
elif not isinstance(format_type, ProxyFormat):
105+
raise ValueError( # pyright: ignore[reportUnreachable] this is actually reachable
106+
f"Invalid format type: {type(format_type).__name__}. Expected ProxyFormat enum or string."
107+
)
108+
109+
format_handlers = {
110+
ProxyFormat.URL: lambda: self.to_url(kwargs.get("protocol", "http")),
111+
ProxyFormat.REQUESTS: lambda: self._format_requests(kwargs),
112+
ProxyFormat.CURL: lambda: ["-x", self.to_url("http")],
113+
ProxyFormat.HTTPX: lambda: self._format_httpx(),
114+
ProxyFormat.AIOHTTP: lambda: self.to_url("http"),
115+
ProxyFormat.PLAYWRIGHT: lambda: self._format_playwright(**kwargs),
116+
}
117+
118+
handler = format_handlers[format_type]
119+
return handler()
120+
121+
def _format_requests(self, kwargs):
122+
"""Format proxy for requests library."""
123+
protocols = kwargs.get("protocols", self.protocols or ["http", "https"])
124+
proxy_url = self.to_url("http")
125+
return {protocol: proxy_url for protocol in protocols}
126+
127+
def _format_httpx(self):
128+
"""Format proxy for httpx library."""
129+
proxy_url = self.to_url("http")
130+
return {"http://": proxy_url, "https://": proxy_url}
131+
132+
def _format_playwright(self, **kwargs):
133+
"""Format proxy for Playwright."""
134+
# Playwright expects server with protocol (e.g., 'http://ip:port', 'socks5://ip:port')
135+
# Allow protocol selection via kwargs, default to http
136+
protocol = kwargs.get("protocol", "http")
137+
138+
# Validate protocol is supported by the proxy
139+
if self.protocols and protocol not in self.protocols:
140+
# If proxy has specific protocols, use the first available one
141+
protocol = self.protocols[0]
142+
143+
playwright_proxy = {"server": f"{protocol}://{self.proxy_address}:{self.port}"}
144+
145+
if self.username and self.password:
146+
playwright_proxy["username"] = self.username
147+
playwright_proxy["password"] = self.password
148+
149+
return playwright_proxy

proxyproviders/version.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Version information for proxyproviders package."""
2+
3+
__version__ = "0.2.1"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[project]
66
name = "proxyproviders"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "A unified interface for different proxy providers"
99
readme = "README.md"
1010
authors = [{ name = "David Teather", email = "[email protected]" }]

setup.cfg

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1+
[bumpversion]
2+
current_version = 0.2.1
3+
4+
[bumpversion:file:setup.cfg]
5+
search = version = {current_version}
6+
replace = version = {new_version}
7+
8+
[bumpversion:file:pyproject.toml]
9+
search = version = "{current_version}"
10+
replace = version = "{new_version}"
11+
12+
[bumpversion:file:proxyproviders/version.py]
13+
search = __version__ = "{current_version}"
14+
replace = __version__ = "{new_version}"
15+
116
[metadata]
217
name = proxyproviders
3-
version = 0.2.0
18+
version = attr: proxyproviders.version.__version__
419
description = A unified interface for different proxy providers
520
long_description = file: README.md
621
long_description_content_type = text/markdown
722
author = David Teather
823
author_email = [email protected]
924
license = MIT
1025
url = https://github.com/davidteather/proxyproviders
11-
classifiers =
12-
Programming Language :: Python :: 3
13-
License :: OSI Approved :: MIT License
14-
Operating System :: OS Independent
26+
classifiers =
27+
Programming Language :: Python :: 3
28+
License :: OSI Approved :: MIT License
29+
Operating System :: OS Independent
1530

1631
[options]
1732
packages = find:

tests/integration/test_algorithms_integration.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,119 @@ def test_webshare_proxy_conversion_methods():
9696
assert "https" in requests_dict
9797
print(f"Requests dict: {requests_dict}")
9898

99+
# Test Playwright format (default protocol)
100+
playwright_dict = proxy.format(ProxyFormat.PLAYWRIGHT)
101+
assert isinstance(playwright_dict, dict)
102+
assert "server" in playwright_dict
103+
assert playwright_dict["server"].startswith("http://")
104+
assert f"{proxy.proxy_address}:{proxy.port}" in playwright_dict["server"]
105+
assert "username" in playwright_dict
106+
assert "password" in playwright_dict
107+
assert playwright_dict["username"] == proxy.username
108+
assert playwright_dict["password"] == proxy.password
109+
110+
# Test Playwright format with different protocols
111+
for protocol in ["http", "https"]:
112+
playwright_protocol = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
113+
assert playwright_protocol["server"].startswith(f"{protocol}://")
114+
115+
116+
@skip_integration
117+
def test_webshare_playwright_format_comprehensive():
118+
"""Comprehensive test of Playwright format with real Webshare API."""
119+
api_key = os.getenv("WEBSHARE_API_KEY")
120+
provider = Webshare(api_key=api_key)
121+
122+
proxy = provider.get_proxy()
123+
124+
# Test 1: Default Playwright format
125+
default_format = proxy.format(ProxyFormat.PLAYWRIGHT)
126+
assert isinstance(default_format, dict)
127+
assert "server" in default_format
128+
assert "username" in default_format
129+
assert "password" in default_format
130+
assert default_format["server"].startswith("http://")
131+
132+
# Test 2: Protocol variations
133+
protocols_to_test = ["http", "https"]
134+
135+
for protocol in protocols_to_test:
136+
result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
137+
assert result["server"].startswith(f"{protocol}://")
138+
assert result["username"] == proxy.username
139+
assert result["password"] == proxy.password
140+
141+
# Test 3: String format
142+
string_format = proxy.format("playwright")
143+
assert string_format == default_format
144+
145+
# Test 4: Format consistency
146+
enum_format = proxy.format(ProxyFormat.PLAYWRIGHT)
147+
string_format = proxy.format("playwright")
148+
assert enum_format == string_format
149+
150+
# Test 5: Playwright documentation compliance
151+
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)
152+
153+
# Verify structure matches Playwright docs
154+
required_fields = ["server"]
155+
optional_fields = ["username", "password"]
156+
157+
for field in required_fields:
158+
assert field in playwright_config, f"Missing required field: {field}"
159+
160+
for field in optional_fields:
161+
if field in playwright_config:
162+
assert True # Field is present
163+
else:
164+
assert False, f"Missing optional field: {field}"
165+
166+
# Verify server format
167+
server = playwright_config["server"]
168+
assert "://" in server, "Server should include protocol"
169+
assert (
170+
f"{proxy.proxy_address}:{proxy.port}" in server
171+
), "Server should include address and port"
172+
173+
174+
@skip_integration
175+
def test_webshare_playwright_e2e_simulation():
176+
"""End-to-end simulation of Playwright usage with real Webshare proxy."""
177+
api_key = os.getenv("WEBSHARE_API_KEY")
178+
provider = Webshare(api_key=api_key)
179+
180+
proxy = provider.get_proxy()
181+
182+
# Convert to Playwright format
183+
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)
184+
185+
# Verify the format is exactly what Playwright expects
186+
# Check server format
187+
server = playwright_config["server"]
188+
assert "://" in server, "Server must include protocol"
189+
assert (
190+
f"{proxy.proxy_address}:{proxy.port}" in server
191+
), "Server must include address and port"
192+
193+
# Check authentication
194+
assert "username" in playwright_config, "Username field is required"
195+
assert "password" in playwright_config, "Password field is required"
196+
197+
# Check required fields
198+
assert "server" in playwright_config, "Server field is required"
199+
200+
# Check optional fields
201+
optional_fields = ["username", "password"]
202+
for field in optional_fields:
203+
assert field in playwright_config, f"Missing optional field: {field}"
204+
205+
# Test different protocols
206+
protocols = ["http", "https"]
207+
for protocol in protocols:
208+
config = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
209+
server = config["server"]
210+
assert server.startswith(f"{protocol}://")
211+
99212

100213
@skip_integration
101214
def test_webshare_e2e_with_requests():

tests/integration/test_brightdata_algorithms_integration.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,86 @@ def test_brightdata_proxy_conversion_methods():
9494
assert "https" in requests_dict
9595
print(f"BrightData Requests dict: {requests_dict}")
9696

97+
# Test Playwright format (default protocol)
98+
playwright_dict = proxy.format(ProxyFormat.PLAYWRIGHT)
99+
assert isinstance(playwright_dict, dict)
100+
assert "server" in playwright_dict
101+
assert playwright_dict["server"].startswith("http://")
102+
assert f"{proxy.proxy_address}:{proxy.port}" in playwright_dict["server"]
103+
assert "username" in playwright_dict
104+
assert "password" in playwright_dict
105+
assert playwright_dict["username"] == proxy.username
106+
assert playwright_dict["password"] == proxy.password
107+
108+
# Test Playwright format with different protocols
109+
for protocol in ["http", "https"]:
110+
playwright_protocol = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
111+
assert playwright_protocol["server"].startswith(f"{protocol}://")
112+
113+
114+
@skip_integration
115+
def test_brightdata_playwright_format_comprehensive():
116+
"""Comprehensive test of Playwright format with real BrightData API."""
117+
api_key = os.getenv("BRIGHTDATA_API_KEY")
118+
provider = BrightData(api_key=api_key, zone="static")
119+
120+
proxy = provider.get_proxy()
121+
122+
# Test 1: Default Playwright format
123+
default_format = proxy.format(ProxyFormat.PLAYWRIGHT)
124+
assert isinstance(default_format, dict)
125+
assert "server" in default_format
126+
assert "username" in default_format
127+
assert "password" in default_format
128+
assert default_format["server"].startswith("http://")
129+
130+
# Test 2: Protocol variations
131+
protocols_to_test = ["http", "https"]
132+
133+
for protocol in protocols_to_test:
134+
result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
135+
assert result["server"].startswith(f"{protocol}://")
136+
assert result["username"] == proxy.username
137+
assert result["password"] == proxy.password
138+
139+
# Test 3: String format
140+
string_format = proxy.format("playwright")
141+
assert string_format == default_format
142+
143+
# Test 4: Format consistency
144+
enum_format = proxy.format(ProxyFormat.PLAYWRIGHT)
145+
string_format = proxy.format("playwright")
146+
assert enum_format == string_format
147+
148+
# Test 5: Playwright documentation compliance
149+
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)
150+
151+
# Verify structure matches Playwright docs
152+
required_fields = ["server"]
153+
optional_fields = ["username", "password"]
154+
155+
for field in required_fields:
156+
assert field in playwright_config, f"Missing required field: {field}"
157+
158+
for field in optional_fields:
159+
if field in playwright_config:
160+
assert True # Field is present
161+
else:
162+
assert False, f"Missing optional field: {field}"
163+
164+
# Verify server format
165+
server = playwright_config["server"]
166+
assert "://" in server, "Server should include protocol"
167+
assert (
168+
f"{proxy.proxy_address}:{proxy.port}" in server
169+
), "Server should include address and port"
170+
171+
# Test 6: Protocol fallback behavior
172+
# Try to use an unsupported protocol - should fallback gracefully
173+
fallback_result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol="socks5")
174+
assert "server" in fallback_result
175+
assert fallback_result["server"].startswith("http://") # Should fallback to http
176+
97177

98178
@skip_integration
99179
def test_brightdata_e2e_with_requests():

0 commit comments

Comments
 (0)