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.
@@ -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
- 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.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.1.0"
8
- description = "Official Python SDK for the ProxyHubb Bulk Proxy Pool API"
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,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