proxyhubb 0.1.0__tar.gz → 0.2.0__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.
- proxyhubb-0.2.0/PKG-INFO +70 -0
- proxyhubb-0.2.0/README.md +50 -0
- proxyhubb-0.2.0/proxyhubb/__init__.py +24 -0
- proxyhubb-0.2.0/proxyhubb/client.py +260 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/proxyhubb/proxy.py +19 -7
- proxyhubb-0.2.0/proxyhubb.egg-info/PKG-INFO +70 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/pyproject.toml +2 -2
- proxyhubb-0.2.0/tests/test_client.py +110 -0
- proxyhubb-0.1.0/PKG-INFO +0 -54
- proxyhubb-0.1.0/README.md +0 -34
- proxyhubb-0.1.0/proxyhubb/__init__.py +0 -7
- proxyhubb-0.1.0/proxyhubb/client.py +0 -106
- proxyhubb-0.1.0/proxyhubb.egg-info/PKG-INFO +0 -54
- proxyhubb-0.1.0/tests/test_client.py +0 -20
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/LICENSE +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/proxyhubb.egg-info/SOURCES.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/proxyhubb.egg-info/dependency_links.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/proxyhubb.egg-info/requires.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/proxyhubb.egg-info/top_level.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.2.0}/setup.cfg +0 -0
proxyhubb-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxyhubb
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for ProxyHubb (session intents, orders, bulk pools)
|
|
5
|
+
Author: ProxyHubb
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://proxyhubb.com
|
|
8
|
+
Project-URL: Repository, https://github.com/proxyhubb/proxyhubb
|
|
9
|
+
Project-URL: Issues, https://github.com/proxyhubb/proxyhubb/issues
|
|
10
|
+
Keywords: proxyhubb,proxy,sdk,api
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.31.0
|
|
18
|
+
Requires-Dist: urllib3>=2.0.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# proxyhubb
|
|
22
|
+
|
|
23
|
+
Official Python client for ProxyHubb Connect (session intents), marketplace credentials, and bulk pools.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install proxyhubb
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Auth
|
|
32
|
+
|
|
33
|
+
- **API key** (`phub_…`): bulk pools — `X-API-Key`
|
|
34
|
+
- **JWT** (dashboard): session intents + order credentials — `Authorization: Bearer`
|
|
35
|
+
|
|
36
|
+
## Quick start — Connect (rotating / sticky)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from proxyhubb import ProxyHubClient, session_intent_proxy_url
|
|
40
|
+
import requests
|
|
41
|
+
|
|
42
|
+
client = ProxyHubClient(access_token="eyJ...") # dashboard JWT
|
|
43
|
+
intent = client.create_session_intent("rotating", target_geo="US")
|
|
44
|
+
proxies = client.as_requests_proxy_from_intent(intent["token"])
|
|
45
|
+
print(requests.get("https://httpbin.org/ip", proxies=proxies).json())
|
|
46
|
+
|
|
47
|
+
# or curl:
|
|
48
|
+
# curl -x "http://session-intent:TOKEN@proxy.proxyhubb.com:8090" https://httpbin.org/ip
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Marketplace credentials
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
creds = client.get_order_credentials("ORDER_UUID")
|
|
55
|
+
proxies = client.as_requests_proxy_from_credentials(creds)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Bulk pools
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
client = ProxyHubClient("phub_...")
|
|
62
|
+
pool = client.create_pool("scrapers", size=5)
|
|
63
|
+
proxies = client.as_requests_proxy(pool["id"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Buyer proxy is HTTP CONNECT / SOCKS5 (`proxy.proxyhubb.com:8090`). See [docs/buyer-proxy.md](../../docs/buyer-proxy.md).
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# proxyhubb
|
|
2
|
+
|
|
3
|
+
Official Python client for ProxyHubb Connect (session intents), marketplace credentials, and bulk pools.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install proxyhubb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Auth
|
|
12
|
+
|
|
13
|
+
- **API key** (`phub_…`): bulk pools — `X-API-Key`
|
|
14
|
+
- **JWT** (dashboard): session intents + order credentials — `Authorization: Bearer`
|
|
15
|
+
|
|
16
|
+
## Quick start — Connect (rotating / sticky)
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from proxyhubb import ProxyHubClient, session_intent_proxy_url
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
client = ProxyHubClient(access_token="eyJ...") # dashboard JWT
|
|
23
|
+
intent = client.create_session_intent("rotating", target_geo="US")
|
|
24
|
+
proxies = client.as_requests_proxy_from_intent(intent["token"])
|
|
25
|
+
print(requests.get("https://httpbin.org/ip", proxies=proxies).json())
|
|
26
|
+
|
|
27
|
+
# or curl:
|
|
28
|
+
# curl -x "http://session-intent:TOKEN@proxy.proxyhubb.com:8090" https://httpbin.org/ip
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Marketplace credentials
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
creds = client.get_order_credentials("ORDER_UUID")
|
|
35
|
+
proxies = client.as_requests_proxy_from_credentials(creds)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Bulk pools
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
client = ProxyHubClient("phub_...")
|
|
42
|
+
pool = client.create_pool("scrapers", size=5)
|
|
43
|
+
proxies = client.as_requests_proxy(pool["id"])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Buyer proxy is HTTP CONNECT / SOCKS5 (`proxy.proxyhubb.com:8090`). See [docs/buyer-proxy.md](../../docs/buyer-proxy.md).
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Official ProxyHubb Python SDK."""
|
|
2
|
+
|
|
3
|
+
from proxyhubb.client import (
|
|
4
|
+
DEFAULT_BASE_URL,
|
|
5
|
+
DEFAULT_PROXY_ENDPOINT,
|
|
6
|
+
ProxyHubClient,
|
|
7
|
+
as_requests_proxies,
|
|
8
|
+
default_proxy_endpoint,
|
|
9
|
+
proxy_url,
|
|
10
|
+
session_intent_proxy_url,
|
|
11
|
+
)
|
|
12
|
+
from proxyhubb.proxy import ProxyClient
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ProxyHubClient",
|
|
16
|
+
"ProxyClient",
|
|
17
|
+
"DEFAULT_BASE_URL",
|
|
18
|
+
"DEFAULT_PROXY_ENDPOINT",
|
|
19
|
+
"as_requests_proxies",
|
|
20
|
+
"default_proxy_endpoint",
|
|
21
|
+
"proxy_url",
|
|
22
|
+
"session_intent_proxy_url",
|
|
23
|
+
]
|
|
24
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""ProxyHubb Python SDK — Control API client (Connect + bulk pools)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.proxyhubb.com"
|
|
12
|
+
DEFAULT_PROXY_ENDPOINT = "proxy.proxyhubb.com:8090"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def default_proxy_endpoint() -> str:
|
|
16
|
+
"""Buyer CONNECT host:port (env ``PROXYHUBB_PROXY_ENDPOINT`` overrides)."""
|
|
17
|
+
return (
|
|
18
|
+
os.environ.get("PROXYHUBB_PROXY_ENDPOINT", "").strip()
|
|
19
|
+
or DEFAULT_PROXY_ENDPOINT
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def proxy_url(
|
|
24
|
+
username: str,
|
|
25
|
+
password: str,
|
|
26
|
+
*,
|
|
27
|
+
host: Optional[str] = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Build an HTTP proxy URL for CONNECT clients (curl ``-x``, requests, etc.)."""
|
|
30
|
+
endpoint = (host or default_proxy_endpoint()).removeprefix("http://").removeprefix(
|
|
31
|
+
"https://"
|
|
32
|
+
)
|
|
33
|
+
user = quote(username, safe="")
|
|
34
|
+
pwd = quote(password, safe="")
|
|
35
|
+
return f"http://{user}:{pwd}@{endpoint}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def session_intent_proxy_url(token: str, *, host: Optional[str] = None) -> str:
|
|
39
|
+
"""Proxy URL using session-intent auth (Connect rotating/sticky)."""
|
|
40
|
+
return proxy_url("session-intent", token, host=host)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def as_requests_proxies(proxy: str) -> dict[str, str]:
|
|
44
|
+
"""``requests``-compatible ``proxies`` dict from a proxy URL string."""
|
|
45
|
+
return {"http": proxy, "https": proxy}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProxyHubClient:
|
|
49
|
+
"""Client for ProxyHubb Control API (session intents, orders, bulk pools).
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
api_key:
|
|
54
|
+
``phub_…`` / ``pk_…`` key for bulk pool endpoints (``X-API-Key``).
|
|
55
|
+
access_token:
|
|
56
|
+
Dashboard JWT for session intents and order credentials (``Authorization: Bearer``).
|
|
57
|
+
base_url:
|
|
58
|
+
Control API base URL. Defaults to production.
|
|
59
|
+
proxy_endpoint:
|
|
60
|
+
Buyer CONNECT host:port. Defaults to ``PROXYHUBB_PROXY_ENDPOINT`` or production.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: Optional[str] = None,
|
|
66
|
+
*,
|
|
67
|
+
access_token: Optional[str] = None,
|
|
68
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
69
|
+
proxy_endpoint: Optional[str] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
if not api_key and not access_token:
|
|
72
|
+
raise ValueError("api_key or access_token is required")
|
|
73
|
+
self._api_key = api_key or ""
|
|
74
|
+
self._access_token = access_token or ""
|
|
75
|
+
self._base_url = base_url.rstrip("/")
|
|
76
|
+
self._proxy_endpoint = (
|
|
77
|
+
proxy_endpoint.strip()
|
|
78
|
+
if proxy_endpoint
|
|
79
|
+
else default_proxy_endpoint()
|
|
80
|
+
)
|
|
81
|
+
self._session = requests.Session()
|
|
82
|
+
if self._api_key:
|
|
83
|
+
self._session.headers.update({"X-API-Key": self._api_key})
|
|
84
|
+
if self._access_token:
|
|
85
|
+
self._session.headers.update(
|
|
86
|
+
{"Authorization": f"Bearer {self._access_token}"}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def proxy_endpoint(self) -> str:
|
|
91
|
+
return self._proxy_endpoint
|
|
92
|
+
|
|
93
|
+
def set_access_token(self, token: str) -> None:
|
|
94
|
+
"""Update Bearer JWT used for session intent / order credential calls."""
|
|
95
|
+
self._access_token = token
|
|
96
|
+
if token:
|
|
97
|
+
self._session.headers["Authorization"] = f"Bearer {token}"
|
|
98
|
+
else:
|
|
99
|
+
self._session.headers.pop("Authorization", None)
|
|
100
|
+
|
|
101
|
+
def _url(self, path: str) -> str:
|
|
102
|
+
return f"{self._base_url}{path}"
|
|
103
|
+
|
|
104
|
+
def _get(self, path: str) -> dict[str, Any]:
|
|
105
|
+
resp = self._session.get(self._url(path))
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
return resp.json()
|
|
108
|
+
|
|
109
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
110
|
+
resp = self._session.post(self._url(path), json=payload)
|
|
111
|
+
resp.raise_for_status()
|
|
112
|
+
return resp.json()
|
|
113
|
+
|
|
114
|
+
def _delete(self, path: str) -> None:
|
|
115
|
+
resp = self._session.delete(self._url(path))
|
|
116
|
+
resp.raise_for_status()
|
|
117
|
+
|
|
118
|
+
# ── Session intent (Connect) ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def create_session_intent(
|
|
121
|
+
self,
|
|
122
|
+
session_type: str = "rotating",
|
|
123
|
+
*,
|
|
124
|
+
listing_id: Optional[str] = None,
|
|
125
|
+
bandwidth_limit: Optional[int] = None,
|
|
126
|
+
target_geo: Optional[str] = None,
|
|
127
|
+
target_isp: Optional[str] = None,
|
|
128
|
+
routing_profile: Optional[str] = None,
|
|
129
|
+
allowed_domains: Optional[list[str]] = None,
|
|
130
|
+
blocked_domains: Optional[list[str]] = None,
|
|
131
|
+
rotation_type: Optional[str] = None,
|
|
132
|
+
rotation_interval_seconds: Optional[int] = None,
|
|
133
|
+
time_limit_minutes: Optional[int] = None,
|
|
134
|
+
preferred_tier: Optional[str] = None,
|
|
135
|
+
max_price_per_gb: Optional[float] = None,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""Issue a signed session intent (requires ``access_token``).
|
|
138
|
+
|
|
139
|
+
Returns ``token``, ``expires_at``, ``lifecycle_state``, and ``intent``.
|
|
140
|
+
"""
|
|
141
|
+
if session_type not in ("rotating", "sticky"):
|
|
142
|
+
raise ValueError("session_type must be 'rotating' or 'sticky'")
|
|
143
|
+
payload: dict[str, Any] = {"type": session_type}
|
|
144
|
+
if listing_id is not None:
|
|
145
|
+
payload["listing_id"] = listing_id
|
|
146
|
+
if bandwidth_limit is not None:
|
|
147
|
+
payload["bandwidth_limit"] = bandwidth_limit
|
|
148
|
+
if target_geo is not None:
|
|
149
|
+
payload["target_geo"] = target_geo
|
|
150
|
+
if target_isp is not None:
|
|
151
|
+
payload["target_isp"] = target_isp
|
|
152
|
+
if routing_profile is not None:
|
|
153
|
+
payload["routing_profile"] = routing_profile
|
|
154
|
+
if allowed_domains is not None:
|
|
155
|
+
payload["allowed_domains"] = allowed_domains
|
|
156
|
+
if blocked_domains is not None:
|
|
157
|
+
payload["blocked_domains"] = blocked_domains
|
|
158
|
+
if rotation_type is not None:
|
|
159
|
+
payload["rotation_type"] = rotation_type
|
|
160
|
+
if rotation_interval_seconds is not None:
|
|
161
|
+
payload["rotation_interval_seconds"] = rotation_interval_seconds
|
|
162
|
+
if time_limit_minutes is not None:
|
|
163
|
+
payload["time_limit_minutes"] = time_limit_minutes
|
|
164
|
+
if preferred_tier is not None:
|
|
165
|
+
payload["preferred_tier"] = preferred_tier
|
|
166
|
+
if max_price_per_gb is not None:
|
|
167
|
+
payload["max_price_per_gb"] = max_price_per_gb
|
|
168
|
+
return self._post("/v1/sessions/intent", payload)
|
|
169
|
+
|
|
170
|
+
def as_requests_proxy_from_intent(self, token: str) -> dict[str, str]:
|
|
171
|
+
"""``requests`` proxies dict for a session-intent token."""
|
|
172
|
+
return as_requests_proxies(
|
|
173
|
+
session_intent_proxy_url(token, host=self._proxy_endpoint)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ── Order credentials (marketplace) ───────────────────────────────────
|
|
177
|
+
|
|
178
|
+
def get_order_credentials(self, order_id: str) -> dict[str, Any]:
|
|
179
|
+
"""Fetch proxy credentials after escrow activation (requires JWT).
|
|
180
|
+
|
|
181
|
+
Returns the API payload, typically
|
|
182
|
+
``{"proxy_credentials": {username, password, host, port, protocol, endpoint}}``.
|
|
183
|
+
"""
|
|
184
|
+
if not order_id:
|
|
185
|
+
raise ValueError("order_id is required")
|
|
186
|
+
return self._get(f"/v1/orders/{order_id}/credentials")
|
|
187
|
+
|
|
188
|
+
def as_requests_proxy_from_credentials(
|
|
189
|
+
self, credentials: Mapping[str, Any]
|
|
190
|
+
) -> dict[str, str]:
|
|
191
|
+
"""Build ``requests`` proxies from a credentials object or envelope."""
|
|
192
|
+
creds = credentials.get("proxy_credentials", credentials)
|
|
193
|
+
username = creds["username"]
|
|
194
|
+
password = creds["password"]
|
|
195
|
+
host = creds.get("host") or creds.get("endpoint") or self._proxy_endpoint
|
|
196
|
+
port = creds.get("port")
|
|
197
|
+
if port and ":" not in str(host):
|
|
198
|
+
host = f"{host}:{port}"
|
|
199
|
+
return as_requests_proxies(proxy_url(username, password, host=str(host)))
|
|
200
|
+
|
|
201
|
+
# ── Bulk pools ────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def create_pool(
|
|
204
|
+
self,
|
|
205
|
+
name: str,
|
|
206
|
+
size: int = 10,
|
|
207
|
+
session_type: str = "rotating",
|
|
208
|
+
description: str = "",
|
|
209
|
+
bytes_limit: int = 0,
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""Create a new proxy pool."""
|
|
212
|
+
return self._post(
|
|
213
|
+
"/v1/bulk/pools",
|
|
214
|
+
{
|
|
215
|
+
"name": name,
|
|
216
|
+
"size": size,
|
|
217
|
+
"session_type": session_type,
|
|
218
|
+
"description": description,
|
|
219
|
+
"bytes_limit": bytes_limit,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def list_pools(self) -> dict[str, Any]:
|
|
224
|
+
"""List proxy pools for the authenticated account."""
|
|
225
|
+
return self._get("/v1/bulk/pools")
|
|
226
|
+
|
|
227
|
+
def get_pool(self, pool_id: str) -> dict[str, Any]:
|
|
228
|
+
"""Retrieve the endpoints of a proxy pool."""
|
|
229
|
+
return self._get(f"/v1/bulk/pools/{pool_id}/endpoints")
|
|
230
|
+
|
|
231
|
+
def get_proxies(self, pool_id: str) -> list[dict[str, Any]]:
|
|
232
|
+
"""Return the list of endpoint dicts for a pool."""
|
|
233
|
+
data = self.get_pool(pool_id)
|
|
234
|
+
return data.get("endpoints", [])
|
|
235
|
+
|
|
236
|
+
def revoke_pool(self, pool_id: str) -> None:
|
|
237
|
+
"""Soft-delete a proxy pool."""
|
|
238
|
+
self._delete(f"/v1/bulk/pools/{pool_id}")
|
|
239
|
+
|
|
240
|
+
def as_requests_proxy(self, pool_id: str, index: int = 0) -> dict[str, str]:
|
|
241
|
+
"""Return a ``proxies`` dict compatible with the ``requests`` library.
|
|
242
|
+
|
|
243
|
+
Endpoint host/port come from the API (production buyer proxy is
|
|
244
|
+
``proxy.proxyhubb.com:8090``).
|
|
245
|
+
"""
|
|
246
|
+
endpoints = self.get_proxies(pool_id)
|
|
247
|
+
if not endpoints:
|
|
248
|
+
raise ValueError(f"Pool {pool_id!r} has no endpoints")
|
|
249
|
+
if index >= len(endpoints):
|
|
250
|
+
raise IndexError(
|
|
251
|
+
f"index {index} is out of range for pool with {len(endpoints)} endpoints"
|
|
252
|
+
)
|
|
253
|
+
ep = endpoints[index]
|
|
254
|
+
return as_requests_proxies(
|
|
255
|
+
proxy_url(
|
|
256
|
+
ep["username"],
|
|
257
|
+
ep["password"],
|
|
258
|
+
host=f"{ep['host']}:{ep['port']}",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
@@ -8,21 +8,25 @@ from typing import Any, Optional
|
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
from proxyhubb.client import DEFAULT_PROXY_ENDPOINT, default_proxy_endpoint, proxy_url
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class ProxyClient:
|
|
15
15
|
"""Thin helper that builds ``requests``-compatible proxy URLs for the gateway.
|
|
16
16
|
|
|
17
|
-
Prefer :class:`proxyhubb.ProxyHubClient`
|
|
17
|
+
Prefer :class:`proxyhubb.ProxyHubClient` for session intents, order credentials,
|
|
18
|
+
and bulk pools.
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
|
-
gateway_url: str =
|
|
23
|
+
gateway_url: Optional[str] = None,
|
|
23
24
|
proxy_auth: Optional[str] = None,
|
|
24
25
|
) -> None:
|
|
25
|
-
|
|
26
|
+
endpoint = gateway_url or f"http://{default_proxy_endpoint()}"
|
|
27
|
+
if not endpoint.startswith("http://") and not endpoint.startswith("https://"):
|
|
28
|
+
endpoint = f"http://{endpoint}"
|
|
29
|
+
self.gateway_url = endpoint
|
|
26
30
|
self.proxy_auth = proxy_auth
|
|
27
31
|
self.current_session_id = str(uuid.uuid4())
|
|
28
32
|
self.session_start = time.time()
|
|
@@ -32,10 +36,15 @@ class ProxyClient:
|
|
|
32
36
|
self.current_session_id = str(uuid.uuid4())
|
|
33
37
|
self.session_start = time.time()
|
|
34
38
|
|
|
35
|
-
auth_str = self.proxy_auth if self.proxy_auth else "anonymous"
|
|
36
39
|
host = self.gateway_url.split("//")[-1]
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if self.proxy_auth and ":" in self.proxy_auth:
|
|
41
|
+
user, _, password = self.proxy_auth.partition(":")
|
|
42
|
+
url = proxy_url(user, password, host=host)
|
|
43
|
+
elif self.proxy_auth:
|
|
44
|
+
url = proxy_url(self.proxy_auth, self.current_session_id, host=host)
|
|
45
|
+
else:
|
|
46
|
+
url = proxy_url("anonymous", self.current_session_id, host=host)
|
|
47
|
+
return {"http": url, "https": url}
|
|
39
48
|
|
|
40
49
|
def request(self, method: str, url: str, rotate: bool = False, **kwargs: Any):
|
|
41
50
|
proxies = self.get_proxy_config(rotate=rotate)
|
|
@@ -45,3 +54,6 @@ class ProxyClient:
|
|
|
45
54
|
return requests.request(
|
|
46
55
|
method, url, proxies=self.get_proxy_config(rotate=True), **kwargs
|
|
47
56
|
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["ProxyClient", "DEFAULT_PROXY_ENDPOINT"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxyhubb
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for ProxyHubb (session intents, orders, bulk pools)
|
|
5
|
+
Author: ProxyHubb
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://proxyhubb.com
|
|
8
|
+
Project-URL: Repository, https://github.com/proxyhubb/proxyhubb
|
|
9
|
+
Project-URL: Issues, https://github.com/proxyhubb/proxyhubb/issues
|
|
10
|
+
Keywords: proxyhubb,proxy,sdk,api
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.31.0
|
|
18
|
+
Requires-Dist: urllib3>=2.0.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# proxyhubb
|
|
22
|
+
|
|
23
|
+
Official Python client for ProxyHubb Connect (session intents), marketplace credentials, and bulk pools.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install proxyhubb
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Auth
|
|
32
|
+
|
|
33
|
+
- **API key** (`phub_…`): bulk pools — `X-API-Key`
|
|
34
|
+
- **JWT** (dashboard): session intents + order credentials — `Authorization: Bearer`
|
|
35
|
+
|
|
36
|
+
## Quick start — Connect (rotating / sticky)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from proxyhubb import ProxyHubClient, session_intent_proxy_url
|
|
40
|
+
import requests
|
|
41
|
+
|
|
42
|
+
client = ProxyHubClient(access_token="eyJ...") # dashboard JWT
|
|
43
|
+
intent = client.create_session_intent("rotating", target_geo="US")
|
|
44
|
+
proxies = client.as_requests_proxy_from_intent(intent["token"])
|
|
45
|
+
print(requests.get("https://httpbin.org/ip", proxies=proxies).json())
|
|
46
|
+
|
|
47
|
+
# or curl:
|
|
48
|
+
# curl -x "http://session-intent:TOKEN@proxy.proxyhubb.com:8090" https://httpbin.org/ip
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Marketplace credentials
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
creds = client.get_order_credentials("ORDER_UUID")
|
|
55
|
+
proxies = client.as_requests_proxy_from_credentials(creds)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Bulk pools
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
client = ProxyHubClient("phub_...")
|
|
62
|
+
pool = client.create_pool("scrapers", size=5)
|
|
63
|
+
proxies = client.as_requests_proxy(pool["id"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Buyer proxy is HTTP CONNECT / SOCKS5 (`proxy.proxyhubb.com:8090`). See [docs/buyer-proxy.md](../../docs/buyer-proxy.md).
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proxyhubb"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Official Python SDK for
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Official Python SDK for ProxyHubb (session intents, orders, bulk pools)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from proxyhubb import (
|
|
4
|
+
DEFAULT_BASE_URL,
|
|
5
|
+
DEFAULT_PROXY_ENDPOINT,
|
|
6
|
+
ProxyClient,
|
|
7
|
+
ProxyHubClient,
|
|
8
|
+
as_requests_proxies,
|
|
9
|
+
proxy_url,
|
|
10
|
+
session_intent_proxy_url,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_defaults():
|
|
15
|
+
assert DEFAULT_BASE_URL == "https://api.proxyhubb.com"
|
|
16
|
+
client = ProxyHubClient("phub_test")
|
|
17
|
+
assert client._base_url == "https://api.proxyhubb.com"
|
|
18
|
+
assert client.proxy_endpoint == DEFAULT_PROXY_ENDPOINT
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_requires_auth():
|
|
22
|
+
try:
|
|
23
|
+
ProxyHubClient()
|
|
24
|
+
assert False, "expected ValueError"
|
|
25
|
+
except ValueError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_access_token_only():
|
|
30
|
+
client = ProxyHubClient(access_token="jwt_test")
|
|
31
|
+
assert client._session.headers["Authorization"] == "Bearer jwt_test"
|
|
32
|
+
assert "X-API-Key" not in client._session.headers
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_proxy_url_helpers():
|
|
36
|
+
assert (
|
|
37
|
+
proxy_url("user", "pass", host="proxy.proxyhubb.com:8090")
|
|
38
|
+
== "http://user:pass@proxy.proxyhubb.com:8090"
|
|
39
|
+
)
|
|
40
|
+
assert session_intent_proxy_url("tok").startswith(
|
|
41
|
+
"http://session-intent:tok@proxy.proxyhubb.com:8090"
|
|
42
|
+
)
|
|
43
|
+
assert as_requests_proxies("http://a:b@h:1") == {
|
|
44
|
+
"http": "http://a:b@h:1",
|
|
45
|
+
"https": "http://a:b@h:1",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_proxy_client_default_gateway():
|
|
50
|
+
gw = ProxyClient()
|
|
51
|
+
assert "proxy.proxyhubb.com" in gw.gateway_url
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_create_session_intent_posts():
|
|
55
|
+
client = ProxyHubClient(access_token="jwt")
|
|
56
|
+
mock_resp = MagicMock()
|
|
57
|
+
mock_resp.raise_for_status = MagicMock()
|
|
58
|
+
mock_resp.json.return_value = {
|
|
59
|
+
"token": "abc",
|
|
60
|
+
"expires_at": "2099-01-01T00:00:00Z",
|
|
61
|
+
"lifecycle_state": "authorized",
|
|
62
|
+
"intent": {"intent_id": "i1", "session_type": "rotating"},
|
|
63
|
+
}
|
|
64
|
+
with patch.object(client._session, "post", return_value=mock_resp) as post:
|
|
65
|
+
out = client.create_session_intent("sticky", target_geo="US")
|
|
66
|
+
assert out["token"] == "abc"
|
|
67
|
+
post.assert_called_once()
|
|
68
|
+
args, kwargs = post.call_args
|
|
69
|
+
assert args[0].endswith("/v1/sessions/intent")
|
|
70
|
+
assert kwargs["json"]["type"] == "sticky"
|
|
71
|
+
assert kwargs["json"]["target_geo"] == "US"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_get_order_credentials_gets():
|
|
75
|
+
client = ProxyHubClient(access_token="jwt")
|
|
76
|
+
mock_resp = MagicMock()
|
|
77
|
+
mock_resp.raise_for_status = MagicMock()
|
|
78
|
+
mock_resp.json.return_value = {
|
|
79
|
+
"proxy_credentials": {
|
|
80
|
+
"username": "u",
|
|
81
|
+
"password": "p",
|
|
82
|
+
"host": "proxy.proxyhubb.com",
|
|
83
|
+
"port": 8090,
|
|
84
|
+
"endpoint": "proxy.proxyhubb.com:8090",
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
with patch.object(client._session, "get", return_value=mock_resp) as get:
|
|
88
|
+
out = client.get_order_credentials("order-1")
|
|
89
|
+
assert out["proxy_credentials"]["username"] == "u"
|
|
90
|
+
get.assert_called_once()
|
|
91
|
+
assert get.call_args[0][0].endswith("/v1/orders/order-1/credentials")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_as_requests_proxy_from_credentials():
|
|
95
|
+
client = ProxyHubClient("phub_x")
|
|
96
|
+
proxies = client.as_requests_proxy_from_credentials(
|
|
97
|
+
{
|
|
98
|
+
"username": "u",
|
|
99
|
+
"password": "p",
|
|
100
|
+
"host": "proxy.proxyhubb.com",
|
|
101
|
+
"port": 8090,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
assert proxies["https"] == "http://u:p@proxy.proxyhubb.com:8090"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_as_requests_proxy_from_intent():
|
|
108
|
+
client = ProxyHubClient(access_token="jwt")
|
|
109
|
+
proxies = client.as_requests_proxy_from_intent("tok")
|
|
110
|
+
assert "session-intent:tok@" in proxies["http"]
|
proxyhubb-0.1.0/PKG-INFO
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: proxyhubb
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Official Python SDK for the ProxyHubb Bulk Proxy Pool API
|
|
5
|
-
Author: ProxyHubb
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://proxyhubb.com
|
|
8
|
-
Project-URL: Repository, https://github.com/proxyhubb/proxyhubb
|
|
9
|
-
Project-URL: Issues, https://github.com/proxyhubb/proxyhubb/issues
|
|
10
|
-
Keywords: proxyhubb,proxy,sdk,api
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Requires-Python: >=3.9
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
Requires-Dist: requests>=2.31.0
|
|
18
|
-
Requires-Dist: urllib3>=2.0.0
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
|
-
# proxyhubb
|
|
22
|
-
|
|
23
|
-
Official Python client for the ProxyHubb Bulk Proxy Pool API.
|
|
24
|
-
|
|
25
|
-
## Install
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pip install proxyhubb
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from proxyhubb import ProxyHubClient
|
|
35
|
-
|
|
36
|
-
client = ProxyHubClient("phub_...") # default: https://api.proxyhubb.com
|
|
37
|
-
pool = client.create_pool("scrapers", size=5)
|
|
38
|
-
proxies = client.as_requests_proxy(pool["id"])
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Create an API key in the dashboard (`phub_…`).
|
|
42
|
-
|
|
43
|
-
Gateway helper (optional):
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
from proxyhubb import ProxyClient
|
|
47
|
-
|
|
48
|
-
gw = ProxyClient(proxy_auth="user:pass")
|
|
49
|
-
resp = gw.request("GET", "https://api.ipify.org")
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## License
|
|
53
|
-
|
|
54
|
-
MIT
|
proxyhubb-0.1.0/README.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# proxyhubb
|
|
2
|
-
|
|
3
|
-
Official Python client for the ProxyHubb Bulk Proxy Pool API.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install proxyhubb
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from proxyhubb import ProxyHubClient
|
|
15
|
-
|
|
16
|
-
client = ProxyHubClient("phub_...") # default: https://api.proxyhubb.com
|
|
17
|
-
pool = client.create_pool("scrapers", size=5)
|
|
18
|
-
proxies = client.as_requests_proxy(pool["id"])
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Create an API key in the dashboard (`phub_…`).
|
|
22
|
-
|
|
23
|
-
Gateway helper (optional):
|
|
24
|
-
|
|
25
|
-
```python
|
|
26
|
-
from proxyhubb import ProxyClient
|
|
27
|
-
|
|
28
|
-
gw = ProxyClient(proxy_auth="user:pass")
|
|
29
|
-
resp = gw.request("GET", "https://api.ipify.org")
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## License
|
|
33
|
-
|
|
34
|
-
MIT
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""ProxyHubb Python SDK — Bulk Proxy Pool Control API client."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import requests
|
|
8
|
-
|
|
9
|
-
DEFAULT_BASE_URL = "https://api.proxyhubb.com"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ProxyHubClient:
|
|
13
|
-
"""Client for the ProxyHubb Bulk Proxy Pool API.
|
|
14
|
-
|
|
15
|
-
Parameters
|
|
16
|
-
----------
|
|
17
|
-
api_key:
|
|
18
|
-
A ProxyHubb API key starting with ``phub_`` (or ``pk_``).
|
|
19
|
-
base_url:
|
|
20
|
-
Base URL of the Control API. Defaults to production.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
api_key: str,
|
|
26
|
-
base_url: str = DEFAULT_BASE_URL,
|
|
27
|
-
) -> None:
|
|
28
|
-
if not api_key:
|
|
29
|
-
raise ValueError("api_key is required")
|
|
30
|
-
self._api_key = api_key
|
|
31
|
-
self._base_url = base_url.rstrip("/")
|
|
32
|
-
self._session = requests.Session()
|
|
33
|
-
self._session.headers.update({"X-API-Key": self._api_key})
|
|
34
|
-
|
|
35
|
-
def _url(self, path: str) -> str:
|
|
36
|
-
return f"{self._base_url}{path}"
|
|
37
|
-
|
|
38
|
-
def _get(self, path: str) -> dict[str, Any]:
|
|
39
|
-
resp = self._session.get(self._url(path))
|
|
40
|
-
resp.raise_for_status()
|
|
41
|
-
return resp.json()
|
|
42
|
-
|
|
43
|
-
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
-
resp = self._session.post(self._url(path), json=payload)
|
|
45
|
-
resp.raise_for_status()
|
|
46
|
-
return resp.json()
|
|
47
|
-
|
|
48
|
-
def _delete(self, path: str) -> None:
|
|
49
|
-
resp = self._session.delete(self._url(path))
|
|
50
|
-
resp.raise_for_status()
|
|
51
|
-
|
|
52
|
-
def create_pool(
|
|
53
|
-
self,
|
|
54
|
-
name: str,
|
|
55
|
-
size: int = 10,
|
|
56
|
-
session_type: str = "rotating",
|
|
57
|
-
description: str = "",
|
|
58
|
-
bytes_limit: int = 0,
|
|
59
|
-
) -> dict[str, Any]:
|
|
60
|
-
"""Create a new proxy pool."""
|
|
61
|
-
return self._post(
|
|
62
|
-
"/v1/bulk/pools",
|
|
63
|
-
{
|
|
64
|
-
"name": name,
|
|
65
|
-
"size": size,
|
|
66
|
-
"session_type": session_type,
|
|
67
|
-
"description": description,
|
|
68
|
-
"bytes_limit": bytes_limit,
|
|
69
|
-
},
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
def list_pools(self) -> dict[str, Any]:
|
|
73
|
-
"""List proxy pools for the authenticated account."""
|
|
74
|
-
return self._get("/v1/bulk/pools")
|
|
75
|
-
|
|
76
|
-
def get_pool(self, pool_id: str) -> dict[str, Any]:
|
|
77
|
-
"""Retrieve the endpoints of a proxy pool."""
|
|
78
|
-
return self._get(f"/v1/bulk/pools/{pool_id}/endpoints")
|
|
79
|
-
|
|
80
|
-
def get_proxies(self, pool_id: str) -> list[dict[str, Any]]:
|
|
81
|
-
"""Return the list of endpoint dicts for a pool."""
|
|
82
|
-
data = self.get_pool(pool_id)
|
|
83
|
-
return data.get("endpoints", [])
|
|
84
|
-
|
|
85
|
-
def revoke_pool(self, pool_id: str) -> None:
|
|
86
|
-
"""Soft-delete a proxy pool."""
|
|
87
|
-
self._delete(f"/v1/bulk/pools/{pool_id}")
|
|
88
|
-
|
|
89
|
-
def as_requests_proxy(self, pool_id: str, index: int = 0) -> dict[str, str]:
|
|
90
|
-
"""Return a ``proxies`` dict compatible with the ``requests`` library.
|
|
91
|
-
|
|
92
|
-
Endpoint host/port come from the API (production buyer proxy is
|
|
93
|
-
``proxy.proxyhubb.com:8090``).
|
|
94
|
-
"""
|
|
95
|
-
endpoints = self.get_proxies(pool_id)
|
|
96
|
-
if not endpoints:
|
|
97
|
-
raise ValueError(f"Pool {pool_id!r} has no endpoints")
|
|
98
|
-
if index >= len(endpoints):
|
|
99
|
-
raise IndexError(
|
|
100
|
-
f"index {index} is out of range for pool with {len(endpoints)} endpoints"
|
|
101
|
-
)
|
|
102
|
-
ep = endpoints[index]
|
|
103
|
-
proxy_url = (
|
|
104
|
-
f"http://{ep['username']}:{ep['password']}@{ep['host']}:{ep['port']}"
|
|
105
|
-
)
|
|
106
|
-
return {"http": proxy_url, "https": proxy_url}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: proxyhubb
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Official Python SDK for the ProxyHubb Bulk Proxy Pool API
|
|
5
|
-
Author: ProxyHubb
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://proxyhubb.com
|
|
8
|
-
Project-URL: Repository, https://github.com/proxyhubb/proxyhubb
|
|
9
|
-
Project-URL: Issues, https://github.com/proxyhubb/proxyhubb/issues
|
|
10
|
-
Keywords: proxyhubb,proxy,sdk,api
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Requires-Python: >=3.9
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
Requires-Dist: requests>=2.31.0
|
|
18
|
-
Requires-Dist: urllib3>=2.0.0
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
|
-
# proxyhubb
|
|
22
|
-
|
|
23
|
-
Official Python client for the ProxyHubb Bulk Proxy Pool API.
|
|
24
|
-
|
|
25
|
-
## Install
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pip install proxyhubb
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Usage
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from proxyhubb import ProxyHubClient
|
|
35
|
-
|
|
36
|
-
client = ProxyHubClient("phub_...") # default: https://api.proxyhubb.com
|
|
37
|
-
pool = client.create_pool("scrapers", size=5)
|
|
38
|
-
proxies = client.as_requests_proxy(pool["id"])
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Create an API key in the dashboard (`phub_…`).
|
|
42
|
-
|
|
43
|
-
Gateway helper (optional):
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
from proxyhubb import ProxyClient
|
|
47
|
-
|
|
48
|
-
gw = ProxyClient(proxy_auth="user:pass")
|
|
49
|
-
resp = gw.request("GET", "https://api.ipify.org")
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## License
|
|
53
|
-
|
|
54
|
-
MIT
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from proxyhubb import ProxyHubClient, ProxyClient, DEFAULT_BASE_URL
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_defaults():
|
|
5
|
-
assert DEFAULT_BASE_URL == "https://api.proxyhubb.com"
|
|
6
|
-
client = ProxyHubClient("phub_test")
|
|
7
|
-
assert client._base_url == "https://api.proxyhubb.com"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_requires_api_key():
|
|
11
|
-
try:
|
|
12
|
-
ProxyHubClient("")
|
|
13
|
-
assert False, "expected ValueError"
|
|
14
|
-
except ValueError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_proxy_client_default_gateway():
|
|
19
|
-
gw = ProxyClient()
|
|
20
|
-
assert "proxy.proxyhubb.com" in gw.gateway_url
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|