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.
@@ -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
- DEFAULT_GATEWAY = "http://proxy.proxyhubb.com:8090"
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` to create pools and obtain credentials.
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 = DEFAULT_GATEWAY,
23
+ gateway_url: Optional[str] = None,
23
24
  proxy_auth: Optional[str] = None,
24
25
  ) -> None:
25
- self.gateway_url = gateway_url
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
- proxy_str = f"http://{auth_str}:{self.current_session_id}@{host}"
38
- return {"http": proxy_str, "https": proxy_str}
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.1.0"
8
- description = "Official Python SDK for the ProxyHubb Bulk Proxy Pool API"
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,7 +0,0 @@
1
- """Official ProxyHubb Python SDK."""
2
-
3
- from proxyhubb.client import ProxyHubClient, DEFAULT_BASE_URL
4
- from proxyhubb.proxy import ProxyClient
5
-
6
- __all__ = ["ProxyHubClient", "ProxyClient", "DEFAULT_BASE_URL"]
7
- __version__ = "0.1.0"
@@ -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