proxyhat 0.2.0__py3-none-any.whl
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.
- proxyhat/__init__.py +39 -0
- proxyhat/_base_client.py +104 -0
- proxyhat/async_client.py +124 -0
- proxyhat/client.py +129 -0
- proxyhat/connection.py +90 -0
- proxyhat/errors.py +72 -0
- proxyhat/py.typed +0 -0
- proxyhat/resources/__init__.py +0 -0
- proxyhat/resources/analytics.py +140 -0
- proxyhat/resources/auth.py +155 -0
- proxyhat/resources/coupons.py +99 -0
- proxyhat/resources/email.py +57 -0
- proxyhat/resources/locations.py +227 -0
- proxyhat/resources/payments.py +107 -0
- proxyhat/resources/plans.py +83 -0
- proxyhat/resources/profile.py +75 -0
- proxyhat/resources/proxy_descriptors.py +64 -0
- proxyhat/resources/proxy_presets.py +71 -0
- proxyhat/resources/sub_user_groups.py +77 -0
- proxyhat/resources/sub_users.py +171 -0
- proxyhat/resources/two_factor.py +107 -0
- proxyhat/types/__init__.py +49 -0
- proxyhat/types/analytics.py +52 -0
- proxyhat/types/auth.py +106 -0
- proxyhat/types/coupons.py +39 -0
- proxyhat/types/email.py +13 -0
- proxyhat/types/locations.py +101 -0
- proxyhat/types/payments.py +101 -0
- proxyhat/types/plans.py +59 -0
- proxyhat/types/profile.py +30 -0
- proxyhat/types/proxy_descriptor.py +33 -0
- proxyhat/types/proxy_presets.py +21 -0
- proxyhat/types/sub_user.py +69 -0
- proxyhat/types/sub_user_groups.py +25 -0
- proxyhat/types/two_factor.py +37 -0
- proxyhat-0.2.0.dist-info/METADATA +195 -0
- proxyhat-0.2.0.dist-info/RECORD +39 -0
- proxyhat-0.2.0.dist-info/WHEEL +4 -0
- proxyhat-0.2.0.dist-info/licenses/LICENSE +21 -0
proxyhat/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ProxyHat Python SDK — official client for the ProxyHat residential proxy API."""
|
|
2
|
+
|
|
3
|
+
from proxyhat.async_client import AsyncProxyHat
|
|
4
|
+
from proxyhat.client import ProxyHat
|
|
5
|
+
from proxyhat.connection import (
|
|
6
|
+
PROXYHAT_GATEWAY,
|
|
7
|
+
PROXYHAT_PORT_HTTP,
|
|
8
|
+
PROXYHAT_PORT_SOCKS5,
|
|
9
|
+
build_connection_url,
|
|
10
|
+
build_proxy_username,
|
|
11
|
+
)
|
|
12
|
+
from proxyhat.errors import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PermissionError,
|
|
17
|
+
ProxyHatError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PROXYHAT_GATEWAY",
|
|
24
|
+
"PROXYHAT_PORT_HTTP",
|
|
25
|
+
"PROXYHAT_PORT_SOCKS5",
|
|
26
|
+
"APIError",
|
|
27
|
+
"AsyncProxyHat",
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"PermissionError",
|
|
31
|
+
"ProxyHat",
|
|
32
|
+
"ProxyHatError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"ValidationError",
|
|
35
|
+
"build_connection_url",
|
|
36
|
+
"build_proxy_username",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "0.2.0"
|
proxyhat/_base_client.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from proxyhat.errors import (
|
|
6
|
+
APIError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
PermissionError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://api.proxyhat.com/v1"
|
|
15
|
+
DEFAULT_TIMEOUT = 30.0
|
|
16
|
+
USER_AGENT = "proxyhat-python/0.1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_headers(api_key: str | None) -> dict[str, str]:
|
|
20
|
+
headers: dict[str, str] = {
|
|
21
|
+
"User-Agent": USER_AGENT,
|
|
22
|
+
"Accept": "application/json",
|
|
23
|
+
}
|
|
24
|
+
if api_key:
|
|
25
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
26
|
+
return headers
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _raise_for_status(status_code: int, body: Any, headers: Any) -> None:
|
|
30
|
+
if 200 <= status_code < 300:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
message = ""
|
|
34
|
+
errors = None
|
|
35
|
+
|
|
36
|
+
if isinstance(body, dict):
|
|
37
|
+
message = body.get("description", "") or body.get("message", "")
|
|
38
|
+
errors = body.get("errors")
|
|
39
|
+
|
|
40
|
+
if not message:
|
|
41
|
+
message = f"HTTP {status_code}"
|
|
42
|
+
|
|
43
|
+
if status_code == 401:
|
|
44
|
+
raise AuthenticationError(message=message, errors=errors)
|
|
45
|
+
elif status_code == 403:
|
|
46
|
+
raise PermissionError(message=message, errors=errors)
|
|
47
|
+
elif status_code == 404:
|
|
48
|
+
raise NotFoundError(message=message, errors=errors)
|
|
49
|
+
elif status_code == 422:
|
|
50
|
+
raise ValidationError(message=message, errors=errors)
|
|
51
|
+
elif status_code == 429:
|
|
52
|
+
retry_after = None
|
|
53
|
+
retry_header = headers.get("retry-after") if hasattr(headers, "get") else None
|
|
54
|
+
if retry_header:
|
|
55
|
+
try:
|
|
56
|
+
retry_after = int(retry_header)
|
|
57
|
+
except (ValueError, TypeError):
|
|
58
|
+
pass
|
|
59
|
+
raise RateLimitError(message=message, errors=errors, retry_after=retry_after)
|
|
60
|
+
else:
|
|
61
|
+
raise APIError(message=message, status_code=status_code, errors=errors)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_json_body(response: Any) -> Any:
|
|
65
|
+
"""Attempt to parse response as JSON, return empty dict on failure."""
|
|
66
|
+
content_type = response.headers.get("content-type", "")
|
|
67
|
+
if "application/json" not in content_type:
|
|
68
|
+
return {}
|
|
69
|
+
try:
|
|
70
|
+
return response.json()
|
|
71
|
+
except Exception:
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BaseClientConfig:
|
|
76
|
+
"""Shared configuration for sync and async clients."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
api_key: str | None = None,
|
|
81
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
82
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.api_key = api_key
|
|
85
|
+
self.base_url = base_url.rstrip("/")
|
|
86
|
+
self.timeout = timeout
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def headers(self) -> dict[str, str]:
|
|
90
|
+
return _build_headers(self.api_key)
|
|
91
|
+
|
|
92
|
+
def build_url(self, path: str) -> str:
|
|
93
|
+
return f"{self.base_url}{path}"
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def handle_response(status_code: int, body: Any, headers: Any) -> Any:
|
|
97
|
+
_raise_for_status(status_code, body, headers)
|
|
98
|
+
|
|
99
|
+
if isinstance(body, dict):
|
|
100
|
+
if "payload" in body:
|
|
101
|
+
return body["payload"]
|
|
102
|
+
if "data" in body:
|
|
103
|
+
return body["data"]
|
|
104
|
+
return body
|
proxyhat/async_client.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from proxyhat._base_client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, BaseClientConfig, _parse_json_body
|
|
8
|
+
from proxyhat.connection import ProxyProtocol, build_connection_url
|
|
9
|
+
from proxyhat.resources.analytics import AsyncAnalyticsResource
|
|
10
|
+
from proxyhat.resources.auth import AsyncAuthResource
|
|
11
|
+
from proxyhat.resources.coupons import AsyncCouponsResource
|
|
12
|
+
from proxyhat.resources.email import AsyncEmailResource
|
|
13
|
+
from proxyhat.resources.locations import AsyncLocationsResource
|
|
14
|
+
from proxyhat.resources.payments import AsyncPaymentsResource
|
|
15
|
+
from proxyhat.resources.plans import AsyncPlansResource
|
|
16
|
+
from proxyhat.resources.profile import AsyncProfileResource
|
|
17
|
+
from proxyhat.resources.proxy_descriptors import AsyncProxyDescriptorsResource
|
|
18
|
+
from proxyhat.resources.proxy_presets import AsyncProxyPresetsResource
|
|
19
|
+
from proxyhat.resources.sub_user_groups import AsyncSubUserGroupsResource
|
|
20
|
+
from proxyhat.resources.sub_users import AsyncSubUsersResource
|
|
21
|
+
from proxyhat.resources.two_factor import AsyncTwoFactorResource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncProxyHat:
|
|
25
|
+
"""Asynchronous client for the ProxyHat API."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
31
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._config = BaseClientConfig(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
34
|
+
self._http = httpx.AsyncClient(headers=self._config.headers, timeout=self._config.timeout)
|
|
35
|
+
|
|
36
|
+
self.auth = AsyncAuthResource(self)
|
|
37
|
+
self.sub_users = AsyncSubUsersResource(self)
|
|
38
|
+
self.sub_user_groups = AsyncSubUserGroupsResource(self)
|
|
39
|
+
self.locations = AsyncLocationsResource(self)
|
|
40
|
+
self.analytics = AsyncAnalyticsResource(self)
|
|
41
|
+
self.proxy_presets = AsyncProxyPresetsResource(self)
|
|
42
|
+
self.proxy_descriptors = AsyncProxyDescriptorsResource(self)
|
|
43
|
+
self.profile = AsyncProfileResource(self)
|
|
44
|
+
self.two_factor = AsyncTwoFactorResource(self)
|
|
45
|
+
self.email = AsyncEmailResource(self)
|
|
46
|
+
self.coupons = AsyncCouponsResource(self)
|
|
47
|
+
self.plans = AsyncPlansResource(self)
|
|
48
|
+
self.payments = AsyncPaymentsResource(self)
|
|
49
|
+
|
|
50
|
+
async def connection_url(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
sub_user: str | None = None,
|
|
54
|
+
country: str | None = None,
|
|
55
|
+
region: str | None = None,
|
|
56
|
+
city: str | None = None,
|
|
57
|
+
sticky: bool | str | None = None,
|
|
58
|
+
filter: str | None = None,
|
|
59
|
+
protocol: ProxyProtocol = "http",
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Return a ready-to-use residential proxy URL (see ``ProxyHat.connection_url``)."""
|
|
62
|
+
users = await self.sub_users.list()
|
|
63
|
+
want = sub_user.strip() if sub_user else None
|
|
64
|
+
if want:
|
|
65
|
+
chosen = next((s for s in users if s.uuid == want or s.name == want), None)
|
|
66
|
+
else:
|
|
67
|
+
usable = [
|
|
68
|
+
s for s in users if not s.suspended_at and (s.traffic_limit == 0 or s.used_traffic < s.traffic_limit)
|
|
69
|
+
]
|
|
70
|
+
chosen = usable[0] if usable else None
|
|
71
|
+
if chosen is None or not chosen.proxy_username or not chosen.proxy_password:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f'No sub-user matched "{want}" (or it has no proxy credentials).'
|
|
74
|
+
if want
|
|
75
|
+
else "No usable sub-user found (all suspended or out of traffic). "
|
|
76
|
+
"Create one, top up, or pass sub_user=."
|
|
77
|
+
)
|
|
78
|
+
return build_connection_url(
|
|
79
|
+
username=chosen.proxy_username,
|
|
80
|
+
password=chosen.proxy_password,
|
|
81
|
+
country=country,
|
|
82
|
+
region=region,
|
|
83
|
+
city=city,
|
|
84
|
+
sticky=sticky,
|
|
85
|
+
filter=filter,
|
|
86
|
+
protocol=protocol,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def request(
|
|
90
|
+
self,
|
|
91
|
+
method: str,
|
|
92
|
+
path: str,
|
|
93
|
+
*,
|
|
94
|
+
json: dict[str, Any] | None = None,
|
|
95
|
+
params: dict[str, Any] | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
url = self._config.build_url(path)
|
|
98
|
+
if params:
|
|
99
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
100
|
+
response = await self._http.request(method, url, json=json, params=params)
|
|
101
|
+
body = _parse_json_body(response)
|
|
102
|
+
return self._config.handle_response(response.status_code, body, response.headers)
|
|
103
|
+
|
|
104
|
+
async def request_raw(
|
|
105
|
+
self,
|
|
106
|
+
method: str,
|
|
107
|
+
path: str,
|
|
108
|
+
*,
|
|
109
|
+
params: dict[str, Any] | None = None,
|
|
110
|
+
) -> httpx.Response:
|
|
111
|
+
"""Make a request and return the raw httpx.Response (for binary downloads)."""
|
|
112
|
+
url = self._config.build_url(path)
|
|
113
|
+
if params:
|
|
114
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
115
|
+
return await self._http.request(method, url, params=params)
|
|
116
|
+
|
|
117
|
+
async def close(self) -> None:
|
|
118
|
+
await self._http.aclose()
|
|
119
|
+
|
|
120
|
+
async def __aenter__(self) -> AsyncProxyHat:
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
124
|
+
await self.close()
|
proxyhat/client.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from proxyhat._base_client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, BaseClientConfig, _parse_json_body
|
|
8
|
+
from proxyhat.connection import ProxyProtocol, build_connection_url
|
|
9
|
+
from proxyhat.resources.analytics import AnalyticsResource
|
|
10
|
+
from proxyhat.resources.auth import AuthResource
|
|
11
|
+
from proxyhat.resources.coupons import CouponsResource
|
|
12
|
+
from proxyhat.resources.email import EmailResource
|
|
13
|
+
from proxyhat.resources.locations import LocationsResource
|
|
14
|
+
from proxyhat.resources.payments import PaymentsResource
|
|
15
|
+
from proxyhat.resources.plans import PlansResource
|
|
16
|
+
from proxyhat.resources.profile import ProfileResource
|
|
17
|
+
from proxyhat.resources.proxy_descriptors import ProxyDescriptorsResource
|
|
18
|
+
from proxyhat.resources.proxy_presets import ProxyPresetsResource
|
|
19
|
+
from proxyhat.resources.sub_user_groups import SubUserGroupsResource
|
|
20
|
+
from proxyhat.resources.sub_users import SubUsersResource
|
|
21
|
+
from proxyhat.resources.two_factor import TwoFactorResource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProxyHat:
|
|
25
|
+
"""Synchronous client for the ProxyHat API."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
31
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._config = BaseClientConfig(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
34
|
+
self._http = httpx.Client(headers=self._config.headers, timeout=self._config.timeout)
|
|
35
|
+
|
|
36
|
+
self.auth = AuthResource(self)
|
|
37
|
+
self.sub_users = SubUsersResource(self)
|
|
38
|
+
self.sub_user_groups = SubUserGroupsResource(self)
|
|
39
|
+
self.locations = LocationsResource(self)
|
|
40
|
+
self.analytics = AnalyticsResource(self)
|
|
41
|
+
self.proxy_presets = ProxyPresetsResource(self)
|
|
42
|
+
self.proxy_descriptors = ProxyDescriptorsResource(self)
|
|
43
|
+
self.profile = ProfileResource(self)
|
|
44
|
+
self.two_factor = TwoFactorResource(self)
|
|
45
|
+
self.email = EmailResource(self)
|
|
46
|
+
self.coupons = CouponsResource(self)
|
|
47
|
+
self.plans = PlansResource(self)
|
|
48
|
+
self.payments = PaymentsResource(self)
|
|
49
|
+
|
|
50
|
+
def connection_url(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
sub_user: str | None = None,
|
|
54
|
+
country: str | None = None,
|
|
55
|
+
region: str | None = None,
|
|
56
|
+
city: str | None = None,
|
|
57
|
+
sticky: bool | str | None = None,
|
|
58
|
+
filter: str | None = None,
|
|
59
|
+
protocol: ProxyProtocol = "http",
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Return a ready-to-use residential proxy URL.
|
|
62
|
+
|
|
63
|
+
Looks up your sub-users, picks an active one with remaining traffic (or
|
|
64
|
+
the one named by ``sub_user``), and builds the gateway URL locally.
|
|
65
|
+
"""
|
|
66
|
+
users = self.sub_users.list()
|
|
67
|
+
want = sub_user.strip() if sub_user else None
|
|
68
|
+
if want:
|
|
69
|
+
chosen = next((s for s in users if s.uuid == want or s.name == want), None)
|
|
70
|
+
else:
|
|
71
|
+
usable = [
|
|
72
|
+
s for s in users if not s.suspended_at and (s.traffic_limit == 0 or s.used_traffic < s.traffic_limit)
|
|
73
|
+
]
|
|
74
|
+
chosen = usable[0] if usable else None
|
|
75
|
+
if chosen is None or not chosen.proxy_username or not chosen.proxy_password:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f'No sub-user matched "{want}" (or it has no proxy credentials).'
|
|
78
|
+
if want
|
|
79
|
+
else "No usable sub-user found (all suspended or out of traffic). "
|
|
80
|
+
"Create one, top up, or pass sub_user=."
|
|
81
|
+
)
|
|
82
|
+
return build_connection_url(
|
|
83
|
+
username=chosen.proxy_username,
|
|
84
|
+
password=chosen.proxy_password,
|
|
85
|
+
country=country,
|
|
86
|
+
region=region,
|
|
87
|
+
city=city,
|
|
88
|
+
sticky=sticky,
|
|
89
|
+
filter=filter,
|
|
90
|
+
protocol=protocol,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def request(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
json: dict[str, Any] | None = None,
|
|
99
|
+
params: dict[str, Any] | None = None,
|
|
100
|
+
) -> Any:
|
|
101
|
+
url = self._config.build_url(path)
|
|
102
|
+
# Filter out None params
|
|
103
|
+
if params:
|
|
104
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
105
|
+
response = self._http.request(method, url, json=json, params=params)
|
|
106
|
+
body = _parse_json_body(response)
|
|
107
|
+
return self._config.handle_response(response.status_code, body, response.headers)
|
|
108
|
+
|
|
109
|
+
def request_raw(
|
|
110
|
+
self,
|
|
111
|
+
method: str,
|
|
112
|
+
path: str,
|
|
113
|
+
*,
|
|
114
|
+
params: dict[str, Any] | None = None,
|
|
115
|
+
) -> httpx.Response:
|
|
116
|
+
"""Make a request and return the raw httpx.Response (for binary downloads)."""
|
|
117
|
+
url = self._config.build_url(path)
|
|
118
|
+
if params:
|
|
119
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
120
|
+
return self._http.request(method, url, params=params)
|
|
121
|
+
|
|
122
|
+
def close(self) -> None:
|
|
123
|
+
self._http.close()
|
|
124
|
+
|
|
125
|
+
def __enter__(self) -> ProxyHat:
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def __exit__(self, *args: Any) -> None:
|
|
129
|
+
self.close()
|
proxyhat/connection.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""ProxyHat gateway connection helpers.
|
|
2
|
+
|
|
3
|
+
The API resources manage your account; these helpers turn a sub-user's
|
|
4
|
+
``proxy_username`` / ``proxy_password`` into a ready-to-use proxy URL, building
|
|
5
|
+
the gateway username with ProxyHat's targeting grammar. Everything here is
|
|
6
|
+
offline (no network) — see https://docs.proxyhat.com/connecting.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import secrets
|
|
12
|
+
from typing import Literal
|
|
13
|
+
from urllib.parse import quote
|
|
14
|
+
|
|
15
|
+
PROXYHAT_GATEWAY = "gate.proxyhat.com"
|
|
16
|
+
PROXYHAT_PORT_HTTP = 8080
|
|
17
|
+
PROXYHAT_PORT_SOCKS5 = 1080
|
|
18
|
+
|
|
19
|
+
ProxyProtocol = Literal["http", "socks5"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _slug(value: str) -> str:
|
|
23
|
+
return "_".join(value.strip().lower().split())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_proxy_username(
|
|
27
|
+
base: str,
|
|
28
|
+
*,
|
|
29
|
+
country: str | None = None,
|
|
30
|
+
region: str | None = None,
|
|
31
|
+
city: str | None = None,
|
|
32
|
+
sticky: bool | str | None = None,
|
|
33
|
+
filter: str | None = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
"""Build a gateway username from a base ``proxy_username`` plus targeting tokens.
|
|
36
|
+
|
|
37
|
+
Fixed order: ``<base>-country-<iso>[-region-<slug>][-city-<slug>]``
|
|
38
|
+
``[-sid-<16hex>-ttl-<dur>]-filter-<tier>``.
|
|
39
|
+
|
|
40
|
+
``sticky``: ``True`` → 30m default; a string sets the TTL ("30m"/"12h");
|
|
41
|
+
omit / ``False`` for a fresh rotating IP per request.
|
|
42
|
+
"""
|
|
43
|
+
parts = [base.strip(), "country", (country or "any").strip().lower()]
|
|
44
|
+
|
|
45
|
+
if region and region.strip().lower() != "any":
|
|
46
|
+
parts += ["region", _slug(region)]
|
|
47
|
+
if city:
|
|
48
|
+
parts += ["city", _slug(city)]
|
|
49
|
+
if sticky is not None and sticky is not False:
|
|
50
|
+
if sticky is True:
|
|
51
|
+
ttl = "30m"
|
|
52
|
+
else:
|
|
53
|
+
raw = str(sticky).strip()
|
|
54
|
+
ttl = "30m" if raw.lower() in ("", "true", "1", "yes", "on") else raw
|
|
55
|
+
parts += ["sid", secrets.token_hex(8), "ttl", ttl]
|
|
56
|
+
if isinstance(filter, str):
|
|
57
|
+
tier = filter.strip().lower()
|
|
58
|
+
if tier.startswith("filter-"):
|
|
59
|
+
tier = tier[len("filter-") :]
|
|
60
|
+
if tier and tier != "none":
|
|
61
|
+
parts += ["filter", tier]
|
|
62
|
+
|
|
63
|
+
return "-".join(parts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_connection_url(
|
|
67
|
+
*,
|
|
68
|
+
username: str,
|
|
69
|
+
password: str,
|
|
70
|
+
country: str | None = None,
|
|
71
|
+
region: str | None = None,
|
|
72
|
+
city: str | None = None,
|
|
73
|
+
sticky: bool | str | None = None,
|
|
74
|
+
filter: str | None = None,
|
|
75
|
+
protocol: ProxyProtocol = "http",
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Build a full proxy connection URL for the ProxyHat gateway.
|
|
78
|
+
|
|
79
|
+
Example: ``http://<user>-country-us:<pass>@gate.proxyhat.com:8080``.
|
|
80
|
+
"""
|
|
81
|
+
port = PROXYHAT_PORT_SOCKS5 if protocol == "socks5" else PROXYHAT_PORT_HTTP
|
|
82
|
+
user = build_proxy_username(
|
|
83
|
+
username,
|
|
84
|
+
country=country,
|
|
85
|
+
region=region,
|
|
86
|
+
city=city,
|
|
87
|
+
sticky=sticky,
|
|
88
|
+
filter=filter,
|
|
89
|
+
)
|
|
90
|
+
return f"{protocol}://{quote(user, safe='')}:{quote(password, safe='')}@{PROXYHAT_GATEWAY}:{port}"
|
proxyhat/errors.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProxyHatError(Exception):
|
|
7
|
+
"""Base exception for all ProxyHat SDK errors."""
|
|
8
|
+
|
|
9
|
+
message: str
|
|
10
|
+
status_code: int | None
|
|
11
|
+
errors: Any
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
status_code: int | None = None,
|
|
17
|
+
errors: Any = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.message = message
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.errors = errors
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthenticationError(ProxyHatError):
|
|
26
|
+
"""Raised on 401 Unauthorized responses."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str = "Unauthorized", errors: Any = None) -> None:
|
|
29
|
+
super().__init__(message, status_code=401, errors=errors)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PermissionError(ProxyHatError):
|
|
33
|
+
"""Raised on 403 Forbidden responses."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str = "Forbidden", errors: Any = None) -> None:
|
|
36
|
+
super().__init__(message, status_code=403, errors=errors)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(ProxyHatError):
|
|
40
|
+
"""Raised on 404 Not Found responses."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str = "Not Found", errors: Any = None) -> None:
|
|
43
|
+
super().__init__(message, status_code=404, errors=errors)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ValidationError(ProxyHatError):
|
|
47
|
+
"""Raised on 422 Unprocessable Entity responses. Includes field-level errors."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str = "Validation Error", errors: Any = None) -> None:
|
|
50
|
+
super().__init__(message, status_code=422, errors=errors)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RateLimitError(ProxyHatError):
|
|
54
|
+
"""Raised on 429 Too Many Requests responses."""
|
|
55
|
+
|
|
56
|
+
retry_after: int | None
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
message: str = "Rate Limited",
|
|
61
|
+
errors: Any = None,
|
|
62
|
+
retry_after: int | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__(message, status_code=429, errors=errors)
|
|
65
|
+
self.retry_after = retry_after
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class APIError(ProxyHatError):
|
|
69
|
+
"""Raised for all other HTTP errors (500, etc.)."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str = "API Error", status_code: int = 500, errors: Any = None) -> None:
|
|
72
|
+
super().__init__(message, status_code=status_code, errors=errors)
|
proxyhat/py.typed
ADDED
|
File without changes
|
|
File without changes
|