proxyhubb 0.1.0__tar.gz → 0.3.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.3.0/PKG-INFO +70 -0
- proxyhubb-0.3.0/README.md +50 -0
- proxyhubb-0.3.0/proxyhubb/__init__.py +24 -0
- proxyhubb-0.3.0/proxyhubb/client.py +453 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/proxyhubb/proxy.py +19 -7
- proxyhubb-0.3.0/proxyhubb.egg-info/PKG-INFO +70 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/pyproject.toml +2 -2
- proxyhubb-0.3.0/tests/test_client.py +153 -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.3.0}/LICENSE +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/proxyhubb.egg-info/SOURCES.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/proxyhubb.egg-info/dependency_links.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/proxyhubb.egg-info/requires.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/proxyhubb.egg-info/top_level.txt +0 -0
- {proxyhubb-0.1.0 → proxyhubb-0.3.0}/setup.cfg +0 -0
proxyhubb-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxyhubb
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official Python SDK for ProxyHubb (buyer Connect/bulk + seller/provider APIs)
|
|
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.3.0"
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""ProxyHubb Python SDK — Control API client (buyer + seller/provider)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
from urllib.parse import quote, urlencode
|
|
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 (buyer Connect/bulk + seller/provider).
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
api_key:
|
|
54
|
+
``phub_…`` / ``pk_…`` key for bulk pool endpoints (``X-API-Key``).
|
|
55
|
+
Buyer keys do **not** authorize provider/seller routes.
|
|
56
|
+
access_token:
|
|
57
|
+
Dashboard JWT for session intents, order credentials, and seller ops
|
|
58
|
+
(``Authorization: Bearer``).
|
|
59
|
+
provider_token:
|
|
60
|
+
Provider CLI/dashboard token; alias of ``access_token`` (same Bearer header).
|
|
61
|
+
base_url:
|
|
62
|
+
Control API base URL. Defaults to production.
|
|
63
|
+
proxy_endpoint:
|
|
64
|
+
Buyer CONNECT host:port. Defaults to ``PROXYHUBB_PROXY_ENDPOINT`` or production.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
api_key: Optional[str] = None,
|
|
70
|
+
*,
|
|
71
|
+
access_token: Optional[str] = None,
|
|
72
|
+
provider_token: Optional[str] = None,
|
|
73
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
74
|
+
proxy_endpoint: Optional[str] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
bearer = access_token or provider_token or ""
|
|
77
|
+
if not api_key and not bearer:
|
|
78
|
+
raise ValueError("api_key, access_token, or provider_token is required")
|
|
79
|
+
self._api_key = api_key or ""
|
|
80
|
+
self._access_token = bearer
|
|
81
|
+
self._base_url = base_url.rstrip("/")
|
|
82
|
+
self._proxy_endpoint = (
|
|
83
|
+
proxy_endpoint.strip()
|
|
84
|
+
if proxy_endpoint
|
|
85
|
+
else default_proxy_endpoint()
|
|
86
|
+
)
|
|
87
|
+
self._session = requests.Session()
|
|
88
|
+
if self._api_key:
|
|
89
|
+
self._session.headers.update({"X-API-Key": self._api_key})
|
|
90
|
+
if self._access_token:
|
|
91
|
+
self._session.headers.update(
|
|
92
|
+
{"Authorization": f"Bearer {self._access_token}"}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def proxy_endpoint(self) -> str:
|
|
97
|
+
return self._proxy_endpoint
|
|
98
|
+
|
|
99
|
+
def set_access_token(self, token: str) -> None:
|
|
100
|
+
"""Update Bearer JWT / provider token used for authenticated calls."""
|
|
101
|
+
self._access_token = token
|
|
102
|
+
if token:
|
|
103
|
+
self._session.headers["Authorization"] = f"Bearer {token}"
|
|
104
|
+
else:
|
|
105
|
+
self._session.headers.pop("Authorization", None)
|
|
106
|
+
|
|
107
|
+
def set_provider_token(self, token: str) -> None:
|
|
108
|
+
"""Alias for :meth:`set_access_token` (same Bearer header)."""
|
|
109
|
+
self.set_access_token(token)
|
|
110
|
+
|
|
111
|
+
def _url(self, path: str, query: Optional[Mapping[str, Any]] = None) -> str:
|
|
112
|
+
url = f"{self._base_url}{path}"
|
|
113
|
+
if query:
|
|
114
|
+
filtered = {
|
|
115
|
+
k: v for k, v in query.items() if v is not None and v != ""
|
|
116
|
+
}
|
|
117
|
+
if filtered:
|
|
118
|
+
url = f"{url}?{urlencode(filtered)}"
|
|
119
|
+
return url
|
|
120
|
+
|
|
121
|
+
def _raise(self, resp: requests.Response) -> None:
|
|
122
|
+
if resp.ok:
|
|
123
|
+
return
|
|
124
|
+
try:
|
|
125
|
+
data = resp.json()
|
|
126
|
+
message = data.get("error") or resp.reason
|
|
127
|
+
except Exception:
|
|
128
|
+
message = resp.reason or resp.text
|
|
129
|
+
raise requests.HTTPError(
|
|
130
|
+
f"ProxyHubb API error {resp.status_code}: {message}",
|
|
131
|
+
response=resp,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _get(self, path: str, *, query: Optional[Mapping[str, Any]] = None) -> dict[str, Any]:
|
|
135
|
+
resp = self._session.get(self._url(path, query))
|
|
136
|
+
self._raise(resp)
|
|
137
|
+
return resp.json()
|
|
138
|
+
|
|
139
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
resp = self._session.post(self._url(path), json=payload)
|
|
141
|
+
self._raise(resp)
|
|
142
|
+
return resp.json()
|
|
143
|
+
|
|
144
|
+
def _put(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
+
resp = self._session.put(self._url(path), json=payload)
|
|
146
|
+
self._raise(resp)
|
|
147
|
+
return resp.json()
|
|
148
|
+
|
|
149
|
+
def _patch(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
resp = self._session.patch(self._url(path), json=payload)
|
|
151
|
+
self._raise(resp)
|
|
152
|
+
return resp.json()
|
|
153
|
+
|
|
154
|
+
def _delete(self, path: str) -> dict[str, Any] | None:
|
|
155
|
+
resp = self._session.delete(self._url(path))
|
|
156
|
+
self._raise(resp)
|
|
157
|
+
if resp.status_code == 204 or not resp.content:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
return resp.json()
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# ── Session intent (Connect) ──────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def create_session_intent(
|
|
167
|
+
self,
|
|
168
|
+
session_type: str = "rotating",
|
|
169
|
+
*,
|
|
170
|
+
listing_id: Optional[str] = None,
|
|
171
|
+
bandwidth_limit: Optional[int] = None,
|
|
172
|
+
target_geo: Optional[str] = None,
|
|
173
|
+
target_isp: Optional[str] = None,
|
|
174
|
+
routing_profile: Optional[str] = None,
|
|
175
|
+
allowed_domains: Optional[list[str]] = None,
|
|
176
|
+
blocked_domains: Optional[list[str]] = None,
|
|
177
|
+
rotation_type: Optional[str] = None,
|
|
178
|
+
rotation_interval_seconds: Optional[int] = None,
|
|
179
|
+
time_limit_minutes: Optional[int] = None,
|
|
180
|
+
preferred_tier: Optional[str] = None,
|
|
181
|
+
max_price_per_gb: Optional[float] = None,
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Issue a signed session intent (requires ``access_token``).
|
|
184
|
+
|
|
185
|
+
Returns ``token``, ``expires_at``, ``lifecycle_state``, and ``intent``.
|
|
186
|
+
"""
|
|
187
|
+
if session_type not in ("rotating", "sticky"):
|
|
188
|
+
raise ValueError("session_type must be 'rotating' or 'sticky'")
|
|
189
|
+
payload: dict[str, Any] = {"type": session_type}
|
|
190
|
+
if listing_id is not None:
|
|
191
|
+
payload["listing_id"] = listing_id
|
|
192
|
+
if bandwidth_limit is not None:
|
|
193
|
+
payload["bandwidth_limit"] = bandwidth_limit
|
|
194
|
+
if target_geo is not None:
|
|
195
|
+
payload["target_geo"] = target_geo
|
|
196
|
+
if target_isp is not None:
|
|
197
|
+
payload["target_isp"] = target_isp
|
|
198
|
+
if routing_profile is not None:
|
|
199
|
+
payload["routing_profile"] = routing_profile
|
|
200
|
+
if allowed_domains is not None:
|
|
201
|
+
payload["allowed_domains"] = allowed_domains
|
|
202
|
+
if blocked_domains is not None:
|
|
203
|
+
payload["blocked_domains"] = blocked_domains
|
|
204
|
+
if rotation_type is not None:
|
|
205
|
+
payload["rotation_type"] = rotation_type
|
|
206
|
+
if rotation_interval_seconds is not None:
|
|
207
|
+
payload["rotation_interval_seconds"] = rotation_interval_seconds
|
|
208
|
+
if time_limit_minutes is not None:
|
|
209
|
+
payload["time_limit_minutes"] = time_limit_minutes
|
|
210
|
+
if preferred_tier is not None:
|
|
211
|
+
payload["preferred_tier"] = preferred_tier
|
|
212
|
+
if max_price_per_gb is not None:
|
|
213
|
+
payload["max_price_per_gb"] = max_price_per_gb
|
|
214
|
+
return self._post("/v1/sessions/intent", payload)
|
|
215
|
+
|
|
216
|
+
def as_requests_proxy_from_intent(self, token: str) -> dict[str, str]:
|
|
217
|
+
"""``requests`` proxies dict for a session-intent token."""
|
|
218
|
+
return as_requests_proxies(
|
|
219
|
+
session_intent_proxy_url(token, host=self._proxy_endpoint)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# ── Order credentials (marketplace) ───────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def get_order_credentials(self, order_id: str) -> dict[str, Any]:
|
|
225
|
+
"""Fetch proxy credentials after escrow activation (requires JWT).
|
|
226
|
+
|
|
227
|
+
Returns the API payload, typically
|
|
228
|
+
``{"proxy_credentials": {username, password, host, port, protocol, endpoint}}``.
|
|
229
|
+
"""
|
|
230
|
+
if not order_id:
|
|
231
|
+
raise ValueError("order_id is required")
|
|
232
|
+
return self._get(f"/v1/orders/{order_id}/credentials")
|
|
233
|
+
|
|
234
|
+
def as_requests_proxy_from_credentials(
|
|
235
|
+
self, credentials: Mapping[str, Any]
|
|
236
|
+
) -> dict[str, str]:
|
|
237
|
+
"""Build ``requests`` proxies from a credentials object or envelope."""
|
|
238
|
+
creds = credentials.get("proxy_credentials", credentials)
|
|
239
|
+
username = creds["username"]
|
|
240
|
+
password = creds["password"]
|
|
241
|
+
host = creds.get("host") or creds.get("endpoint") or self._proxy_endpoint
|
|
242
|
+
port = creds.get("port")
|
|
243
|
+
if port and ":" not in str(host):
|
|
244
|
+
host = f"{host}:{port}"
|
|
245
|
+
return as_requests_proxies(proxy_url(username, password, host=str(host)))
|
|
246
|
+
|
|
247
|
+
# ── Bulk pools ────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
def create_pool(
|
|
250
|
+
self,
|
|
251
|
+
name: str,
|
|
252
|
+
size: int = 10,
|
|
253
|
+
session_type: str = "rotating",
|
|
254
|
+
description: str = "",
|
|
255
|
+
bytes_limit: int = 0,
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
"""Create a new proxy pool."""
|
|
258
|
+
return self._post(
|
|
259
|
+
"/v1/bulk/pools",
|
|
260
|
+
{
|
|
261
|
+
"name": name,
|
|
262
|
+
"size": size,
|
|
263
|
+
"session_type": session_type,
|
|
264
|
+
"description": description,
|
|
265
|
+
"bytes_limit": bytes_limit,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def list_pools(self) -> dict[str, Any]:
|
|
270
|
+
"""List proxy pools for the authenticated account."""
|
|
271
|
+
return self._get("/v1/bulk/pools")
|
|
272
|
+
|
|
273
|
+
def get_pool(self, pool_id: str) -> dict[str, Any]:
|
|
274
|
+
"""Retrieve the endpoints of a proxy pool."""
|
|
275
|
+
return self._get(f"/v1/bulk/pools/{pool_id}/endpoints")
|
|
276
|
+
|
|
277
|
+
def get_proxies(self, pool_id: str) -> list[dict[str, Any]]:
|
|
278
|
+
"""Return the list of endpoint dicts for a pool."""
|
|
279
|
+
data = self.get_pool(pool_id)
|
|
280
|
+
return data.get("endpoints", [])
|
|
281
|
+
|
|
282
|
+
def revoke_pool(self, pool_id: str) -> None:
|
|
283
|
+
"""Soft-delete a proxy pool."""
|
|
284
|
+
self._delete(f"/v1/bulk/pools/{pool_id}")
|
|
285
|
+
|
|
286
|
+
def as_requests_proxy(self, pool_id: str, index: int = 0) -> dict[str, str]:
|
|
287
|
+
"""Return a ``proxies`` dict compatible with the ``requests`` library.
|
|
288
|
+
|
|
289
|
+
Endpoint host/port come from the API (production buyer proxy is
|
|
290
|
+
``proxy.proxyhubb.com:8090``).
|
|
291
|
+
"""
|
|
292
|
+
endpoints = self.get_proxies(pool_id)
|
|
293
|
+
if not endpoints:
|
|
294
|
+
raise ValueError(f"Pool {pool_id!r} has no endpoints")
|
|
295
|
+
if index >= len(endpoints):
|
|
296
|
+
raise IndexError(
|
|
297
|
+
f"index {index} is out of range for pool with {len(endpoints)} endpoints"
|
|
298
|
+
)
|
|
299
|
+
ep = endpoints[index]
|
|
300
|
+
return as_requests_proxies(
|
|
301
|
+
proxy_url(
|
|
302
|
+
ep["username"],
|
|
303
|
+
ep["password"],
|
|
304
|
+
host=f"{ep['host']}:{ep['port']}",
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# ── Provider tokens ───────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def list_provider_tokens(self) -> dict[str, Any]:
|
|
311
|
+
"""List provider tokens (masked). Requires JWT or provider token Bearer."""
|
|
312
|
+
return self._get("/v1/provider/tokens")
|
|
313
|
+
|
|
314
|
+
def create_provider_token(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
name: Optional[str] = None,
|
|
318
|
+
expires_in: Optional[int] = None,
|
|
319
|
+
node_id: Optional[str] = None,
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Create a provider token. Plaintext ``token`` is returned once."""
|
|
322
|
+
payload: dict[str, Any] = {}
|
|
323
|
+
if name is not None:
|
|
324
|
+
payload["name"] = name
|
|
325
|
+
if expires_in is not None:
|
|
326
|
+
payload["expires_in"] = expires_in
|
|
327
|
+
if node_id is not None:
|
|
328
|
+
payload["node_id"] = node_id
|
|
329
|
+
return self._post("/v1/provider/tokens", payload)
|
|
330
|
+
|
|
331
|
+
def rotate_provider_token(self, token_id: str) -> dict[str, Any]:
|
|
332
|
+
"""Rotate a provider token; returns new plaintext ``token`` once."""
|
|
333
|
+
if not token_id:
|
|
334
|
+
raise ValueError("token_id is required")
|
|
335
|
+
return self._post(f"/v1/provider/tokens/{token_id}/rotate", {})
|
|
336
|
+
|
|
337
|
+
def revoke_provider_token(self, token_id: str) -> dict[str, Any] | None:
|
|
338
|
+
"""Revoke a provider token."""
|
|
339
|
+
if not token_id:
|
|
340
|
+
raise ValueError("token_id is required")
|
|
341
|
+
return self._delete(f"/v1/provider/tokens/{token_id}")
|
|
342
|
+
|
|
343
|
+
# ── Provider nodes ────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def get_provider_readiness(self) -> dict[str, Any]:
|
|
346
|
+
return self._get("/v1/provider/readiness")
|
|
347
|
+
|
|
348
|
+
def list_provider_nodes(self) -> dict[str, Any]:
|
|
349
|
+
return self._get("/v1/provider/nodes")
|
|
350
|
+
|
|
351
|
+
def create_provider_node(
|
|
352
|
+
self,
|
|
353
|
+
name: str,
|
|
354
|
+
*,
|
|
355
|
+
ip_address: Optional[str] = None,
|
|
356
|
+
country: Optional[str] = None,
|
|
357
|
+
city: Optional[str] = None,
|
|
358
|
+
connection_type: Optional[str] = None,
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
if not name:
|
|
361
|
+
raise ValueError("name is required")
|
|
362
|
+
payload: dict[str, Any] = {"name": name}
|
|
363
|
+
if ip_address is not None:
|
|
364
|
+
payload["ip_address"] = ip_address
|
|
365
|
+
if country is not None:
|
|
366
|
+
payload["country"] = country
|
|
367
|
+
if city is not None:
|
|
368
|
+
payload["city"] = city
|
|
369
|
+
if connection_type is not None:
|
|
370
|
+
payload["connection_type"] = connection_type
|
|
371
|
+
return self._post("/v1/provider/nodes", payload)
|
|
372
|
+
|
|
373
|
+
def update_provider_node(
|
|
374
|
+
self,
|
|
375
|
+
node_id: str,
|
|
376
|
+
*,
|
|
377
|
+
name: Optional[str] = None,
|
|
378
|
+
ip_address: Optional[str] = None,
|
|
379
|
+
country: Optional[str] = None,
|
|
380
|
+
city: Optional[str] = None,
|
|
381
|
+
connection_type: Optional[str] = None,
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
if not node_id:
|
|
384
|
+
raise ValueError("node_id is required")
|
|
385
|
+
payload: dict[str, Any] = {}
|
|
386
|
+
if name is not None:
|
|
387
|
+
payload["name"] = name
|
|
388
|
+
if ip_address is not None:
|
|
389
|
+
payload["ip_address"] = ip_address
|
|
390
|
+
if country is not None:
|
|
391
|
+
payload["country"] = country
|
|
392
|
+
if city is not None:
|
|
393
|
+
payload["city"] = city
|
|
394
|
+
if connection_type is not None:
|
|
395
|
+
payload["connection_type"] = connection_type
|
|
396
|
+
return self._put(f"/v1/provider/nodes/{node_id}", payload)
|
|
397
|
+
|
|
398
|
+
def delete_provider_node(self, node_id: str) -> dict[str, Any] | None:
|
|
399
|
+
if not node_id:
|
|
400
|
+
raise ValueError("node_id is required")
|
|
401
|
+
return self._delete(f"/v1/provider/nodes/{node_id}")
|
|
402
|
+
|
|
403
|
+
def get_node_connect(self, node_id: str) -> dict[str, Any]:
|
|
404
|
+
"""Connect hints for provider-cli / provider-node join."""
|
|
405
|
+
if not node_id:
|
|
406
|
+
raise ValueError("node_id is required")
|
|
407
|
+
return self._get(f"/v1/provider/nodes/{node_id}/connect")
|
|
408
|
+
|
|
409
|
+
# ── Listings (seller) ─────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
def list_my_listings(
|
|
412
|
+
self, *, page: Optional[int] = None, limit: Optional[int] = None
|
|
413
|
+
) -> dict[str, Any]:
|
|
414
|
+
return self._get("/v1/listings/my", query={"page": page, "limit": limit})
|
|
415
|
+
|
|
416
|
+
def get_listing(self, listing_id: str) -> dict[str, Any]:
|
|
417
|
+
if not listing_id:
|
|
418
|
+
raise ValueError("listing_id is required")
|
|
419
|
+
return self._get(f"/v1/listings/{listing_id}")
|
|
420
|
+
|
|
421
|
+
def create_listing(self, **fields: Any) -> dict[str, Any]:
|
|
422
|
+
"""Create a marketplace listing as seller.
|
|
423
|
+
|
|
424
|
+
Required fields typically include ``title``, ``proxy_type``, ``protocol``,
|
|
425
|
+
``price_per_gb``, ``available_bandwidth_gb``.
|
|
426
|
+
"""
|
|
427
|
+
return self._post("/v1/listings", dict(fields))
|
|
428
|
+
|
|
429
|
+
def update_listing(self, listing_id: str, **fields: Any) -> dict[str, Any]:
|
|
430
|
+
"""Update a listing. Pause/activate with ``status="active"|"paused"``."""
|
|
431
|
+
if not listing_id:
|
|
432
|
+
raise ValueError("listing_id is required")
|
|
433
|
+
return self._patch(f"/v1/listings/{listing_id}", dict(fields))
|
|
434
|
+
|
|
435
|
+
def delete_listing(self, listing_id: str) -> dict[str, Any] | None:
|
|
436
|
+
if not listing_id:
|
|
437
|
+
raise ValueError("listing_id is required")
|
|
438
|
+
return self._delete(f"/v1/listings/{listing_id}")
|
|
439
|
+
|
|
440
|
+
# ── Earnings ──────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
def get_earnings_summary(self) -> dict[str, Any]:
|
|
443
|
+
return self._get("/v1/provider/earnings/summary")
|
|
444
|
+
|
|
445
|
+
def list_earnings(
|
|
446
|
+
self, *, page: Optional[int] = None, limit: Optional[int] = None
|
|
447
|
+
) -> dict[str, Any]:
|
|
448
|
+
return self._get(
|
|
449
|
+
"/v1/provider/earnings", query={"page": page, "limit": limit}
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def list_earnings_history(self) -> dict[str, Any]:
|
|
453
|
+
return self._get("/v1/provider/earnings/history")
|
|
@@ -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.3.0
|
|
4
|
+
Summary: Official Python SDK for ProxyHubb (buyer Connect/bulk + seller/provider APIs)
|
|
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.3.0"
|
|
8
|
+
description = "Official Python SDK for ProxyHubb (buyer Connect/bulk + seller/provider APIs)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,153 @@
|
|
|
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_provider_token_alias():
|
|
36
|
+
client = ProxyHubClient(provider_token="ptok")
|
|
37
|
+
assert client._session.headers["Authorization"] == "Bearer ptok"
|
|
38
|
+
client.set_provider_token("ptok2")
|
|
39
|
+
assert client._session.headers["Authorization"] == "Bearer ptok2"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_proxy_url_helpers():
|
|
43
|
+
assert (
|
|
44
|
+
proxy_url("user", "pass", host="proxy.proxyhubb.com:8090")
|
|
45
|
+
== "http://user:pass@proxy.proxyhubb.com:8090"
|
|
46
|
+
)
|
|
47
|
+
assert session_intent_proxy_url("tok").startswith(
|
|
48
|
+
"http://session-intent:tok@proxy.proxyhubb.com:8090"
|
|
49
|
+
)
|
|
50
|
+
assert as_requests_proxies("http://a:b@h:1") == {
|
|
51
|
+
"http": "http://a:b@h:1",
|
|
52
|
+
"https": "http://a:b@h:1",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_proxy_client_default_gateway():
|
|
57
|
+
gw = ProxyClient()
|
|
58
|
+
assert "proxy.proxyhubb.com" in gw.gateway_url
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_create_session_intent_posts():
|
|
62
|
+
client = ProxyHubClient(access_token="jwt")
|
|
63
|
+
mock_resp = MagicMock()
|
|
64
|
+
mock_resp.raise_for_status = MagicMock()
|
|
65
|
+
mock_resp.json.return_value = {
|
|
66
|
+
"token": "abc",
|
|
67
|
+
"expires_at": "2099-01-01T00:00:00Z",
|
|
68
|
+
"lifecycle_state": "authorized",
|
|
69
|
+
"intent": {"intent_id": "i1", "session_type": "rotating"},
|
|
70
|
+
}
|
|
71
|
+
with patch.object(client._session, "post", return_value=mock_resp) as post:
|
|
72
|
+
out = client.create_session_intent("sticky", target_geo="US")
|
|
73
|
+
assert out["token"] == "abc"
|
|
74
|
+
post.assert_called_once()
|
|
75
|
+
args, kwargs = post.call_args
|
|
76
|
+
assert args[0].endswith("/v1/sessions/intent")
|
|
77
|
+
assert kwargs["json"]["type"] == "sticky"
|
|
78
|
+
assert kwargs["json"]["target_geo"] == "US"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_get_order_credentials_gets():
|
|
82
|
+
client = ProxyHubClient(access_token="jwt")
|
|
83
|
+
mock_resp = MagicMock()
|
|
84
|
+
mock_resp.raise_for_status = MagicMock()
|
|
85
|
+
mock_resp.json.return_value = {
|
|
86
|
+
"proxy_credentials": {
|
|
87
|
+
"username": "u",
|
|
88
|
+
"password": "p",
|
|
89
|
+
"host": "proxy.proxyhubb.com",
|
|
90
|
+
"port": 8090,
|
|
91
|
+
"endpoint": "proxy.proxyhubb.com:8090",
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
with patch.object(client._session, "get", return_value=mock_resp) as get:
|
|
95
|
+
out = client.get_order_credentials("order-1")
|
|
96
|
+
assert out["proxy_credentials"]["username"] == "u"
|
|
97
|
+
get.assert_called_once()
|
|
98
|
+
assert get.call_args[0][0].endswith("/v1/orders/order-1/credentials")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_as_requests_proxy_from_credentials():
|
|
102
|
+
client = ProxyHubClient("phub_x")
|
|
103
|
+
proxies = client.as_requests_proxy_from_credentials(
|
|
104
|
+
{
|
|
105
|
+
"username": "u",
|
|
106
|
+
"password": "p",
|
|
107
|
+
"host": "proxy.proxyhubb.com",
|
|
108
|
+
"port": 8090,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
assert proxies["https"] == "http://u:p@proxy.proxyhubb.com:8090"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_as_requests_proxy_from_intent():
|
|
115
|
+
client = ProxyHubClient(access_token="jwt")
|
|
116
|
+
proxies = client.as_requests_proxy_from_intent("tok")
|
|
117
|
+
assert "session-intent:tok@" in proxies["http"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_create_provider_token():
|
|
121
|
+
client = ProxyHubClient(provider_token="ptok")
|
|
122
|
+
mock_resp = MagicMock()
|
|
123
|
+
mock_resp.ok = True
|
|
124
|
+
mock_resp.status_code = 201
|
|
125
|
+
mock_resp.json.return_value = {"id": "t1", "token": "raw-once"}
|
|
126
|
+
with patch.object(client._session, "post", return_value=mock_resp) as post:
|
|
127
|
+
out = client.create_provider_token(name="edge", node_id="n1")
|
|
128
|
+
assert out["token"] == "raw-once"
|
|
129
|
+
assert post.call_args[0][0].endswith("/v1/provider/tokens")
|
|
130
|
+
assert post.call_args[1]["json"]["node_id"] == "n1"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_seller_listing_and_earnings():
|
|
134
|
+
client = ProxyHubClient(access_token="jwt")
|
|
135
|
+
mock_resp = MagicMock()
|
|
136
|
+
mock_resp.ok = True
|
|
137
|
+
mock_resp.status_code = 200
|
|
138
|
+
mock_resp.content = b"{}"
|
|
139
|
+
mock_resp.json.return_value = {"ok": True}
|
|
140
|
+
|
|
141
|
+
with patch.object(client._session, "get", return_value=mock_resp) as get:
|
|
142
|
+
client.get_node_connect("n1")
|
|
143
|
+
client.get_earnings_summary()
|
|
144
|
+
client.list_my_listings(page=1, limit=5)
|
|
145
|
+
paths = [c.args[0] for c in get.call_args_list]
|
|
146
|
+
assert paths[0].endswith("/v1/provider/nodes/n1/connect")
|
|
147
|
+
assert paths[1].endswith("/v1/provider/earnings/summary")
|
|
148
|
+
assert "page=1" in paths[2] and "limit=5" in paths[2]
|
|
149
|
+
|
|
150
|
+
with patch.object(client._session, "patch", return_value=mock_resp) as patch_call:
|
|
151
|
+
client.update_listing("l1", status="paused")
|
|
152
|
+
assert patch_call.call_args[0][0].endswith("/v1/listings/l1")
|
|
153
|
+
assert patch_call.call_args[1]["json"]["status"] == "paused"
|
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
|