Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion proxyproviders/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from . import algorithms
from .models.proxy import Proxy
from .models.proxy import Proxy, ProxyFormat
from .providers.brightdata import BrightData
from .providers.webshare import Webshare
from .proxy_provider import ProxyConfig, ProxyProvider
from .version import __version__

__all__ = [
"ProxyProvider",
"ProxyConfig",
"Proxy",
"ProxyFormat",
"Webshare",
"BrightData",
"algorithms",
Expand Down
81 changes: 56 additions & 25 deletions proxyproviders/models/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class ProxyFormat(Enum):
AIOHTTP = "aiohttp"
"""AIOHTTP format, for use in aiohttp library HTTP calls"""

PLAYWRIGHT = "playwright"
"""Playwright format, for use in Playwright browser automation"""

URL = "url"
"""URL format, for use in URL strings"""

Expand Down Expand Up @@ -90,29 +93,57 @@ def format(
>>> proxy.format(ProxyFormat.HTTPX)
{'http://': 'http://user:pass@192.168.1.1:8080', 'https://': 'http://user:pass@192.168.1.1:8080'}
"""
# Convert to ProxyFormat enum, handling both string and enum inputs
if isinstance(format_type, str):
format_type = ProxyFormat(format_type)

if format_type == ProxyFormat.URL:
protocol = kwargs.get("protocol", "http")
return self.to_url(protocol)

elif format_type == ProxyFormat.REQUESTS:
protocols = kwargs.get("protocols", self.protocols or ["http", "https"])
proxy_url = self.to_url("http")
return {protocol: proxy_url for protocol in protocols}

elif format_type == ProxyFormat.CURL:
return ["-x", self.to_url("http")]

elif format_type == ProxyFormat.HTTPX:
# httpx uses 'http://' and 'https://' as keys
proxy_url = self.to_url("http")
return {"http://": proxy_url, "https://": proxy_url}

elif format_type == ProxyFormat.AIOHTTP:
# aiohttp takes a single URL string
return self.to_url("http")

else:
raise ValueError(f"Unsupported format: {format_type}")
try:
format_type = ProxyFormat(format_type)
except ValueError:
raise ValueError(
f"Invalid format type: '{format_type}'. Valid options are: {[f.value for f in ProxyFormat]}"
)
elif not isinstance(format_type, ProxyFormat):
raise ValueError( # pyright: ignore[reportUnreachable] this is actually reachable
f"Invalid format type: {type(format_type).__name__}. Expected ProxyFormat enum or string."
)

format_handlers = {
ProxyFormat.URL: lambda: self.to_url(kwargs.get("protocol", "http")),
ProxyFormat.REQUESTS: lambda: self._format_requests(kwargs),
ProxyFormat.CURL: lambda: ["-x", self.to_url("http")],
ProxyFormat.HTTPX: lambda: self._format_httpx(),
ProxyFormat.AIOHTTP: lambda: self.to_url("http"),
ProxyFormat.PLAYWRIGHT: lambda: self._format_playwright(**kwargs),
}

handler = format_handlers[format_type]
return handler()

def _format_requests(self, kwargs):
"""Format proxy for requests library."""
protocols = kwargs.get("protocols", self.protocols or ["http", "https"])
proxy_url = self.to_url("http")
return {protocol: proxy_url for protocol in protocols}

def _format_httpx(self):
"""Format proxy for httpx library."""
proxy_url = self.to_url("http")
return {"http://": proxy_url, "https://": proxy_url}

def _format_playwright(self, **kwargs):
"""Format proxy for Playwright."""
# Playwright expects server with protocol (e.g., 'http://ip:port', 'socks5://ip:port')
# Allow protocol selection via kwargs, default to http
protocol = kwargs.get("protocol", "http")

# Validate protocol is supported by the proxy
if self.protocols and protocol not in self.protocols:
# If proxy has specific protocols, use the first available one
protocol = self.protocols[0]

playwright_proxy = {"server": f"{protocol}://{self.proxy_address}:{self.port}"}

if self.username and self.password:
playwright_proxy["username"] = self.username
playwright_proxy["password"] = self.password

return playwright_proxy
3 changes: 3 additions & 0 deletions proxyproviders/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Version information for proxyproviders package."""

__version__ = "0.2.1"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[project]
name = "proxyproviders"
version = "0.2.0"
version = "0.2.1"
description = "A unified interface for different proxy providers"
readme = "README.md"
authors = [{ name = "David Teather", email = "contact.davidteather@gmail.com" }]
Expand Down
25 changes: 20 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
[bumpversion]
current_version = 0.2.1

[bumpversion:file:setup.cfg]
search = version = {current_version}
replace = version = {new_version}

[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"

[bumpversion:file:proxyproviders/version.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

[metadata]
name = proxyproviders
version = 0.2.0
version = attr: proxyproviders.version.__version__
description = A unified interface for different proxy providers
long_description = file: README.md
long_description_content_type = text/markdown
author = David Teather
author_email = contact.davidteather@gmail.com
license = MIT
url = https://github.com/davidteather/proxyproviders
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent

[options]
packages = find:
Expand Down
113 changes: 113 additions & 0 deletions tests/integration/test_algorithms_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,119 @@ def test_webshare_proxy_conversion_methods():
assert "https" in requests_dict
print(f"Requests dict: {requests_dict}")

# Test Playwright format (default protocol)
playwright_dict = proxy.format(ProxyFormat.PLAYWRIGHT)
assert isinstance(playwright_dict, dict)
assert "server" in playwright_dict
assert playwright_dict["server"].startswith("http://")
assert f"{proxy.proxy_address}:{proxy.port}" in playwright_dict["server"]
assert "username" in playwright_dict
assert "password" in playwright_dict
assert playwright_dict["username"] == proxy.username
assert playwright_dict["password"] == proxy.password

# Test Playwright format with different protocols
for protocol in ["http", "https"]:
playwright_protocol = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
assert playwright_protocol["server"].startswith(f"{protocol}://")


@skip_integration
def test_webshare_playwright_format_comprehensive():
"""Comprehensive test of Playwright format with real Webshare API."""
api_key = os.getenv("WEBSHARE_API_KEY")
provider = Webshare(api_key=api_key)

proxy = provider.get_proxy()

# Test 1: Default Playwright format
default_format = proxy.format(ProxyFormat.PLAYWRIGHT)
assert isinstance(default_format, dict)
assert "server" in default_format
assert "username" in default_format
assert "password" in default_format
assert default_format["server"].startswith("http://")

# Test 2: Protocol variations
protocols_to_test = ["http", "https"]

for protocol in protocols_to_test:
result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
assert result["server"].startswith(f"{protocol}://")
assert result["username"] == proxy.username
assert result["password"] == proxy.password

# Test 3: String format
string_format = proxy.format("playwright")
assert string_format == default_format

# Test 4: Format consistency
enum_format = proxy.format(ProxyFormat.PLAYWRIGHT)
string_format = proxy.format("playwright")
assert enum_format == string_format

# Test 5: Playwright documentation compliance
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)

# Verify structure matches Playwright docs
required_fields = ["server"]
optional_fields = ["username", "password"]

for field in required_fields:
assert field in playwright_config, f"Missing required field: {field}"

for field in optional_fields:
if field in playwright_config:
assert True # Field is present
else:
assert False, f"Missing optional field: {field}"

# Verify server format
server = playwright_config["server"]
assert "://" in server, "Server should include protocol"
assert (
f"{proxy.proxy_address}:{proxy.port}" in server
), "Server should include address and port"


@skip_integration
def test_webshare_playwright_e2e_simulation():
"""End-to-end simulation of Playwright usage with real Webshare proxy."""
api_key = os.getenv("WEBSHARE_API_KEY")
provider = Webshare(api_key=api_key)

proxy = provider.get_proxy()

# Convert to Playwright format
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)

# Verify the format is exactly what Playwright expects
# Check server format
server = playwright_config["server"]
assert "://" in server, "Server must include protocol"
assert (
f"{proxy.proxy_address}:{proxy.port}" in server
), "Server must include address and port"

# Check authentication
assert "username" in playwright_config, "Username field is required"
assert "password" in playwright_config, "Password field is required"

# Check required fields
assert "server" in playwright_config, "Server field is required"

# Check optional fields
optional_fields = ["username", "password"]
for field in optional_fields:
assert field in playwright_config, f"Missing optional field: {field}"

# Test different protocols
protocols = ["http", "https"]
for protocol in protocols:
config = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
server = config["server"]
assert server.startswith(f"{protocol}://")


@skip_integration
def test_webshare_e2e_with_requests():
Expand Down
80 changes: 80 additions & 0 deletions tests/integration/test_brightdata_algorithms_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,86 @@ def test_brightdata_proxy_conversion_methods():
assert "https" in requests_dict
print(f"BrightData Requests dict: {requests_dict}")

# Test Playwright format (default protocol)
playwright_dict = proxy.format(ProxyFormat.PLAYWRIGHT)
assert isinstance(playwright_dict, dict)
assert "server" in playwright_dict
assert playwright_dict["server"].startswith("http://")
assert f"{proxy.proxy_address}:{proxy.port}" in playwright_dict["server"]
assert "username" in playwright_dict
assert "password" in playwright_dict
assert playwright_dict["username"] == proxy.username
assert playwright_dict["password"] == proxy.password

# Test Playwright format with different protocols
for protocol in ["http", "https"]:
playwright_protocol = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
assert playwright_protocol["server"].startswith(f"{protocol}://")


@skip_integration
def test_brightdata_playwright_format_comprehensive():
"""Comprehensive test of Playwright format with real BrightData API."""
api_key = os.getenv("BRIGHTDATA_API_KEY")
provider = BrightData(api_key=api_key, zone="static")

proxy = provider.get_proxy()

# Test 1: Default Playwright format
default_format = proxy.format(ProxyFormat.PLAYWRIGHT)
assert isinstance(default_format, dict)
assert "server" in default_format
assert "username" in default_format
assert "password" in default_format
assert default_format["server"].startswith("http://")

# Test 2: Protocol variations
protocols_to_test = ["http", "https"]

for protocol in protocols_to_test:
result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol=protocol)
assert result["server"].startswith(f"{protocol}://")
assert result["username"] == proxy.username
assert result["password"] == proxy.password

# Test 3: String format
string_format = proxy.format("playwright")
assert string_format == default_format

# Test 4: Format consistency
enum_format = proxy.format(ProxyFormat.PLAYWRIGHT)
string_format = proxy.format("playwright")
assert enum_format == string_format

# Test 5: Playwright documentation compliance
playwright_config = proxy.format(ProxyFormat.PLAYWRIGHT)

# Verify structure matches Playwright docs
required_fields = ["server"]
optional_fields = ["username", "password"]

for field in required_fields:
assert field in playwright_config, f"Missing required field: {field}"

for field in optional_fields:
if field in playwright_config:
assert True # Field is present
else:
assert False, f"Missing optional field: {field}"

# Verify server format
server = playwright_config["server"]
assert "://" in server, "Server should include protocol"
assert (
f"{proxy.proxy_address}:{proxy.port}" in server
), "Server should include address and port"

# Test 6: Protocol fallback behavior
# Try to use an unsupported protocol - should fallback gracefully
fallback_result = proxy.format(ProxyFormat.PLAYWRIGHT, protocol="socks5")
assert "server" in fallback_result
assert fallback_result["server"].startswith("http://") # Should fallback to http


@skip_integration
def test_brightdata_e2e_with_requests():
Expand Down
Loading