dominusnode 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dominusnode/proxy.py ADDED
@@ -0,0 +1,248 @@
1
+ """Proxy resource -- build proxy URLs, health, status, and config."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List, Optional
6
+ from urllib.parse import quote
7
+
8
+ from .errors import ProxyError
9
+ from .http_client import AsyncHttpClient, SyncHttpClient
10
+ from .types import (
11
+ GeoTargeting,
12
+ ProxyConfig,
13
+ ProxyEndpointConfig,
14
+ ProxyEndpoints,
15
+ ProxyHealth,
16
+ ProxyStatus,
17
+ ProxyUrlOptions,
18
+ ProviderStat,
19
+ )
20
+
21
+
22
+ def build_proxy_url(
23
+ api_key: str,
24
+ *,
25
+ proxy_host: str,
26
+ http_proxy_port: int,
27
+ socks5_proxy_port: int,
28
+ options: Optional[ProxyUrlOptions] = None,
29
+ ) -> str:
30
+ """Build a proxy URL string for use with HTTP clients.
31
+
32
+ The Dominus Node proxy uses HTTP Basic auth where:
33
+ - username encodes geo-targeting and session options
34
+ - password is the API key
35
+
36
+ Args:
37
+ api_key: The ``dn_live_...`` API key.
38
+ proxy_host: Hostname of the proxy server.
39
+ http_proxy_port: HTTP proxy port (default 8080).
40
+ socks5_proxy_port: SOCKS5 proxy port (default 1080).
41
+ options: Optional geo-targeting and protocol settings.
42
+
43
+ Returns:
44
+ A proxy URL like ``http://user:key@host:port``.
45
+ """
46
+ if not api_key:
47
+ raise ProxyError("API key is required to build proxy URL")
48
+
49
+ # Validate proxy_host to prevent URL injection via crafted hostnames
50
+ if any(c in proxy_host for c in " /\\@?#"):
51
+ raise ValueError("proxy_host contains invalid characters")
52
+
53
+ opts = options or ProxyUrlOptions()
54
+
55
+ # Build username with geo-targeting segments
56
+ username_parts: list[str] = ["user"]
57
+
58
+ if opts.country:
59
+ username_parts.append(f"country-{quote(opts.country.upper(), safe='')}")
60
+
61
+ if opts.state:
62
+ username_parts.append(f"state-{quote(opts.state, safe='')}")
63
+
64
+ if opts.city:
65
+ username_parts.append(f"city-{quote(opts.city, safe='')}")
66
+
67
+ if opts.asn is not None:
68
+ username_parts.append(f"asn-{opts.asn}")
69
+
70
+ if opts.session_id:
71
+ username_parts.append(f"session-{quote(opts.session_id, safe='')}")
72
+
73
+ username = "-".join(username_parts)
74
+
75
+ # Select port based on protocol
76
+ protocol = opts.protocol.lower()
77
+ if protocol == "socks5":
78
+ port = socks5_proxy_port
79
+ scheme = "socks5"
80
+ else:
81
+ port = http_proxy_port
82
+ scheme = "http"
83
+
84
+ # URL-encode password in case of special characters
85
+ encoded_key = quote(api_key, safe="")
86
+
87
+ return f"{scheme}://{username}:{encoded_key}@{proxy_host}:{port}"
88
+
89
+
90
+ def _parse_proxy_health(data: dict) -> ProxyHealth:
91
+ return ProxyHealth(
92
+ status=data.get("status", "unknown"),
93
+ active_sessions=data.get("activeSessions", 0),
94
+ uptime_seconds=data.get("uptimeSeconds", 0),
95
+ )
96
+
97
+
98
+ def _parse_provider_stat(p: dict) -> ProviderStat:
99
+ return ProviderStat(
100
+ name=p["name"],
101
+ state=p["state"],
102
+ consecutive_failures=p.get("consecutiveFailures", 0),
103
+ total_requests=p.get("totalRequests", 0),
104
+ total_errors=p.get("totalErrors", 0),
105
+ avg_latency_ms=p.get("avgLatencyMs", 0),
106
+ fallback_only=p.get("fallbackOnly", False),
107
+ )
108
+
109
+
110
+ def _parse_proxy_status(data: dict) -> ProxyStatus:
111
+ endpoints_data = data.get("endpoints", {})
112
+ endpoints = ProxyEndpoints(
113
+ http=endpoints_data.get("http", ""),
114
+ socks5=endpoints_data.get("socks5", ""),
115
+ )
116
+ providers = [_parse_provider_stat(p) for p in data.get("providers", [])]
117
+ return ProxyStatus(
118
+ status=data.get("status", "unknown"),
119
+ active_sessions=data.get("activeSessions", 0),
120
+ user_active_sessions=data.get("userActiveSessions", 0),
121
+ avg_latency_ms=data.get("avgLatencyMs", 0),
122
+ providers=providers,
123
+ endpoints=endpoints,
124
+ supported_countries=data.get("supportedCountries", []),
125
+ uptime_seconds=data.get("uptimeSeconds", 0),
126
+ )
127
+
128
+
129
+ def _parse_proxy_config(data: dict) -> ProxyConfig:
130
+ hp = data.get("httpProxy", {})
131
+ sp = data.get("socks5Proxy", {})
132
+ gt = data.get("geoTargeting", {})
133
+ return ProxyConfig(
134
+ http_proxy=ProxyEndpointConfig(host=hp.get("host", ""), port=hp.get("port", 8080)),
135
+ socks5_proxy=ProxyEndpointConfig(host=sp.get("host", ""), port=sp.get("port", 1080)),
136
+ supported_countries=data.get("supportedCountries", []),
137
+ blocked_countries=data.get("blockedCountries", []),
138
+ max_rotation_interval_minutes=data.get("maxRotationIntervalMinutes", 60),
139
+ min_rotation_interval_minutes=data.get("minRotationIntervalMinutes", 1),
140
+ geo_targeting=GeoTargeting(
141
+ state_support=gt.get("stateSupport", False),
142
+ city_support=gt.get("citySupport", False),
143
+ asn_support=gt.get("asnSupport", False),
144
+ us_states=gt.get("usStates", []),
145
+ major_us_cities=gt.get("majorUsCities", []),
146
+ ),
147
+ )
148
+
149
+
150
+ # ──────────────────────────────────────────────────────────────────────
151
+ # Sync
152
+ # ──────────────────────────────────────────────────────────────────────
153
+
154
+
155
+ class ProxyResource:
156
+ """Synchronous proxy operations."""
157
+
158
+ def __init__(
159
+ self,
160
+ http: SyncHttpClient,
161
+ *,
162
+ proxy_host: str,
163
+ http_proxy_port: int,
164
+ socks5_proxy_port: int,
165
+ get_api_key: callable,
166
+ ) -> None:
167
+ self._http = http
168
+ self._proxy_host = proxy_host
169
+ self._http_proxy_port = http_proxy_port
170
+ self._socks5_proxy_port = socks5_proxy_port
171
+ self._get_api_key = get_api_key
172
+
173
+ def build_url(self, options: Optional[ProxyUrlOptions] = None) -> str:
174
+ """Build a proxy URL using the current API key and options."""
175
+ api_key = self._get_api_key()
176
+ if not api_key:
177
+ raise ProxyError("No API key set. Call connect_with_key() first.")
178
+ return build_proxy_url(
179
+ api_key,
180
+ proxy_host=self._proxy_host,
181
+ http_proxy_port=self._http_proxy_port,
182
+ socks5_proxy_port=self._socks5_proxy_port,
183
+ options=options,
184
+ )
185
+
186
+ def get_health(self) -> ProxyHealth:
187
+ """Get public proxy health status (no auth required)."""
188
+ data = self._http.get("/api/proxy/health")
189
+ return _parse_proxy_health(data)
190
+
191
+ def get_status(self) -> ProxyStatus:
192
+ """Get detailed proxy status (auth required)."""
193
+ data = self._http.get("/api/proxy/status")
194
+ return _parse_proxy_status(data)
195
+
196
+ def get_config(self) -> ProxyConfig:
197
+ """Get proxy server configuration (auth required)."""
198
+ data = self._http.get("/api/proxy/config")
199
+ return _parse_proxy_config(data)
200
+
201
+
202
+ # ──────────────────────────────────────────────────────────────────────
203
+ # Async
204
+ # ──────────────────────────────────────────────────────────────────────
205
+
206
+
207
+ class AsyncProxyResource:
208
+ """Asynchronous proxy operations."""
209
+
210
+ def __init__(
211
+ self,
212
+ http: AsyncHttpClient,
213
+ *,
214
+ proxy_host: str,
215
+ http_proxy_port: int,
216
+ socks5_proxy_port: int,
217
+ get_api_key: callable,
218
+ ) -> None:
219
+ self._http = http
220
+ self._proxy_host = proxy_host
221
+ self._http_proxy_port = http_proxy_port
222
+ self._socks5_proxy_port = socks5_proxy_port
223
+ self._get_api_key = get_api_key
224
+
225
+ def build_url(self, options: Optional[ProxyUrlOptions] = None) -> str:
226
+ """Build a proxy URL (synchronous -- no network call needed)."""
227
+ api_key = self._get_api_key()
228
+ if not api_key:
229
+ raise ProxyError("No API key set. Call connect_with_key() first.")
230
+ return build_proxy_url(
231
+ api_key,
232
+ proxy_host=self._proxy_host,
233
+ http_proxy_port=self._http_proxy_port,
234
+ socks5_proxy_port=self._socks5_proxy_port,
235
+ options=options,
236
+ )
237
+
238
+ async def get_health(self) -> ProxyHealth:
239
+ data = await self._http.get("/api/proxy/health")
240
+ return _parse_proxy_health(data)
241
+
242
+ async def get_status(self) -> ProxyStatus:
243
+ data = await self._http.get("/api/proxy/status")
244
+ return _parse_proxy_status(data)
245
+
246
+ async def get_config(self) -> ProxyConfig:
247
+ data = await self._http.get("/api/proxy/config")
248
+ return _parse_proxy_config(data)
dominusnode/py.typed ADDED
File without changes
@@ -0,0 +1,55 @@
1
+ """Sessions resource -- active proxy sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List
6
+
7
+ from .http_client import AsyncHttpClient, SyncHttpClient
8
+ from .types import ActiveSession
9
+
10
+
11
+ # ──────────────────────────────────────────────────────────────────────
12
+ # Sync
13
+ # ──────────────────────────────────────────────────────────────────────
14
+
15
+
16
+ class SessionsResource:
17
+ """Synchronous session operations."""
18
+
19
+ def __init__(self, http: SyncHttpClient) -> None:
20
+ self._http = http
21
+
22
+ def get_active(self) -> List[ActiveSession]:
23
+ """Get currently active proxy sessions."""
24
+ data = self._http.get("/api/sessions/active")
25
+ return [
26
+ ActiveSession(
27
+ id=s["id"],
28
+ started_at=str(s["startedAt"]),
29
+ status=s.get("status", "active"),
30
+ )
31
+ for s in data.get("sessions", [])
32
+ ]
33
+
34
+
35
+ # ──────────────────────────────────────────────────────────────────────
36
+ # Async
37
+ # ──────────────────────────────────────────────────────────────────────
38
+
39
+
40
+ class AsyncSessionsResource:
41
+ """Asynchronous session operations."""
42
+
43
+ def __init__(self, http: AsyncHttpClient) -> None:
44
+ self._http = http
45
+
46
+ async def get_active(self) -> List[ActiveSession]:
47
+ data = await self._http.get("/api/sessions/active")
48
+ return [
49
+ ActiveSession(
50
+ id=s["id"],
51
+ started_at=str(s["startedAt"]),
52
+ status=s.get("status", "active"),
53
+ )
54
+ for s in data.get("sessions", [])
55
+ ]
dominusnode/slots.py ADDED
@@ -0,0 +1,60 @@
1
+ """Slots and waitlist resource -- check registration availability and join waitlist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .http_client import AsyncHttpClient, SyncHttpClient
6
+ from .types import SlotsInfo, WaitlistCount, WaitlistJoinResult
7
+
8
+
9
+ class SlotsResource:
10
+ """Synchronous slots and waitlist operations."""
11
+
12
+ def __init__(self, http: SyncHttpClient) -> None:
13
+ self._http = http
14
+
15
+ def get_slots(self) -> SlotsInfo:
16
+ """Check available registration slots."""
17
+ data = self._http.get("/api/slots", requires_auth=False)
18
+ return SlotsInfo(
19
+ total=data["total"],
20
+ used=data["used"],
21
+ remaining=data["remaining"],
22
+ unlimited=data["unlimited"],
23
+ )
24
+
25
+ def join_waitlist(self, email: str) -> WaitlistJoinResult:
26
+ """Join the waitlist when registration slots are full."""
27
+ data = self._http.post("/api/waitlist/join", json={"email": email}, requires_auth=False)
28
+ return WaitlistJoinResult(message=data.get("message", ""))
29
+
30
+ def get_waitlist_count(self) -> WaitlistCount:
31
+ """Get the number of people on the waitlist."""
32
+ data = self._http.get("/api/waitlist/count", requires_auth=False)
33
+ return WaitlistCount(pending=data["pending"])
34
+
35
+
36
+ class AsyncSlotsResource:
37
+ """Asynchronous slots and waitlist operations."""
38
+
39
+ def __init__(self, http: AsyncHttpClient) -> None:
40
+ self._http = http
41
+
42
+ async def get_slots(self) -> SlotsInfo:
43
+ """Check available registration slots."""
44
+ data = await self._http.get("/api/slots", requires_auth=False)
45
+ return SlotsInfo(
46
+ total=data["total"],
47
+ used=data["used"],
48
+ remaining=data["remaining"],
49
+ unlimited=data["unlimited"],
50
+ )
51
+
52
+ async def join_waitlist(self, email: str) -> WaitlistJoinResult:
53
+ """Join the waitlist when registration slots are full."""
54
+ data = await self._http.post("/api/waitlist/join", json={"email": email}, requires_auth=False)
55
+ return WaitlistJoinResult(message=data.get("message", ""))
56
+
57
+ async def get_waitlist_count(self) -> WaitlistCount:
58
+ """Get the number of people on the waitlist."""
59
+ data = await self._http.get("/api/waitlist/count", requires_auth=False)
60
+ return WaitlistCount(pending=data["pending"])