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/client.py ADDED
@@ -0,0 +1,457 @@
1
+ """Dominus Node SDK clients -- sync and async.
2
+
3
+ Provides ``DominusNodeClient`` for synchronous usage and
4
+ ``AsyncDominusNodeClient`` for asyncio-based usage. Both share the same
5
+ API surface and resource layout.
6
+
7
+ Example (sync)::
8
+
9
+ from dominusnode import DominusNodeClient
10
+
11
+ with DominusNodeClient(base_url="http://localhost:3000") as client:
12
+ client.connect_with_credentials("user@example.com", "password123")
13
+ balance = client.wallet.get_balance()
14
+ print(f"Balance: ${balance.balance_usd}")
15
+
16
+ Example (async)::
17
+
18
+ from dominusnode import AsyncDominusNodeClient
19
+
20
+ async with AsyncDominusNodeClient(base_url="http://localhost:3000") as client:
21
+ await client.connect_with_credentials("user@example.com", "password123")
22
+ balance = await client.wallet.get_balance()
23
+ print(f"Balance: ${balance.balance_usd}")
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Optional
29
+
30
+ import httpx
31
+
32
+ from .admin import AdminResource, AsyncAdminResource
33
+ from .errors import DominusNodeError
34
+ from .auth import AsyncAuthResource, AuthResource
35
+ from .constants import (
36
+ DEFAULT_BASE_URL,
37
+ DEFAULT_HTTP_PROXY_PORT,
38
+ DEFAULT_PROXY_HOST,
39
+ DEFAULT_SOCKS5_PROXY_PORT,
40
+ )
41
+ from .http_client import AsyncHttpClient, SyncHttpClient
42
+ from .keys import AsyncKeysResource, KeysResource
43
+ from .plans import AsyncPlansResource, PlansResource
44
+ from .proxy import AsyncProxyResource, ProxyResource
45
+ from .sessions import AsyncSessionsResource, SessionsResource
46
+ from .slots import AsyncSlotsResource, SlotsResource
47
+ from .agent_wallet import AgenticWalletResource, AsyncAgenticWalletResource
48
+ from .teams import TeamsResource as TeamsRes, AsyncTeamsResource as AsyncTeamsRes
49
+ from .x402 import X402Resource, AsyncX402Resource
50
+ from .wallet_auth import WalletAuthResource, AsyncWalletAuthResource
51
+ from .token_manager import AsyncTokenManager, TokenManager
52
+ from .types import LoginResult, User
53
+ from .usage import AsyncUsageResource, UsageResource
54
+ from .wallet import AsyncWalletResource, WalletResource
55
+
56
+
57
+ # ──────────────────────────────────────────────────────────────────────
58
+ # Sync Client
59
+ # ──────────────────────────────────────────────────────────────────────
60
+
61
+
62
+ class DominusNodeClient:
63
+ """Synchronous Dominus Node API client.
64
+
65
+ Args:
66
+ base_url: Base URL of the Dominus Node API server.
67
+ api_key: API key for immediate connection (optional).
68
+ email: Email for credential-based connection (optional).
69
+ password: Password for credential-based connection (optional).
70
+ access_token: Pre-existing access token (optional).
71
+ refresh_token: Pre-existing refresh token (optional).
72
+ proxy_host: Hostname of the proxy server.
73
+ http_proxy_port: HTTP proxy port.
74
+ socks5_proxy_port: SOCKS5 proxy port.
75
+ timeout: HTTP request timeout in seconds.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ base_url: str = DEFAULT_BASE_URL,
81
+ *,
82
+ api_key: Optional[str] = None,
83
+ email: Optional[str] = None,
84
+ password: Optional[str] = None,
85
+ access_token: Optional[str] = None,
86
+ refresh_token: Optional[str] = None,
87
+ proxy_host: str = DEFAULT_PROXY_HOST,
88
+ http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
89
+ socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
90
+ timeout: float = 30.0,
91
+ ) -> None:
92
+ self._api_key: Optional[str] = api_key
93
+ self._proxy_host = proxy_host
94
+ self._http_proxy_port = http_proxy_port
95
+ self._socks5_proxy_port = socks5_proxy_port
96
+
97
+ # Token management
98
+ self._token_manager = TokenManager()
99
+
100
+ # Wire up the token refresh function
101
+ def _sync_refresh(rt: str) -> tuple[str, str]:
102
+ return self.auth.refresh(rt)
103
+
104
+ self._token_manager.set_refresh_fn(_sync_refresh)
105
+
106
+ if access_token and refresh_token:
107
+ self._token_manager.set_tokens(access_token, refresh_token)
108
+
109
+ # HTTP client
110
+ self._http = SyncHttpClient(base_url, self._token_manager, timeout=timeout)
111
+
112
+ # Resource instances
113
+ self._auth = AuthResource(self._http, self._token_manager)
114
+ self._keys = KeysResource(self._http)
115
+ self._wallet = WalletResource(self._http)
116
+ self._usage = UsageResource(self._http)
117
+ self._plans = PlansResource(self._http)
118
+ self._sessions = SessionsResource(self._http)
119
+ self._proxy = ProxyResource(
120
+ self._http,
121
+ proxy_host=proxy_host,
122
+ http_proxy_port=http_proxy_port,
123
+ socks5_proxy_port=socks5_proxy_port,
124
+ get_api_key=lambda: self._api_key,
125
+ )
126
+ self._admin = AdminResource(self._http)
127
+ self._slots = SlotsResource(self._http)
128
+ self._agentic_wallets = AgenticWalletResource(self._http)
129
+ self._teams = TeamsRes(self._http)
130
+ self._x402 = X402Resource(self._http)
131
+ self._wallet_auth = WalletAuthResource(self._http, self._token_manager)
132
+
133
+ # Auto-connect if credentials provided.
134
+ # On failure, clear credentials to prevent retention in a partial object.
135
+ try:
136
+ if api_key:
137
+ self.connect_with_key(api_key)
138
+ elif email and password:
139
+ self.connect_with_credentials(email, password)
140
+ except Exception:
141
+ self._api_key = None
142
+ self._token_manager.clear()
143
+ raise
144
+
145
+ # ── Resource properties ──────────────────────────────────────────
146
+
147
+ @property
148
+ def auth(self) -> AuthResource:
149
+ """Auth operations (register, login, MFA, etc.)."""
150
+ return self._auth
151
+
152
+ @property
153
+ def keys(self) -> KeysResource:
154
+ """API key management."""
155
+ return self._keys
156
+
157
+ @property
158
+ def wallet(self) -> WalletResource:
159
+ """Wallet operations (balance, transactions, top-ups)."""
160
+ return self._wallet
161
+
162
+ @property
163
+ def usage(self) -> UsageResource:
164
+ """Usage statistics and data export."""
165
+ return self._usage
166
+
167
+ @property
168
+ def plans(self) -> PlansResource:
169
+ """Plan management."""
170
+ return self._plans
171
+
172
+ @property
173
+ def sessions(self) -> SessionsResource:
174
+ """Active proxy sessions."""
175
+ return self._sessions
176
+
177
+ @property
178
+ def proxy(self) -> ProxyResource:
179
+ """Proxy operations (URL building, health, status, config)."""
180
+ return self._proxy
181
+
182
+ @property
183
+ def admin(self) -> AdminResource:
184
+ """Admin operations (requires admin privileges)."""
185
+ return self._admin
186
+
187
+ @property
188
+ def slots(self) -> SlotsResource:
189
+ """Slots and waitlist operations (public, no auth required)."""
190
+ return self._slots
191
+
192
+ @property
193
+ def agentic_wallets(self) -> AgenticWalletResource:
194
+ """Agentic wallet operations (sub-wallets with spending limits)."""
195
+ return self._agentic_wallets
196
+
197
+ @property
198
+ def teams(self) -> TeamsRes:
199
+ """Team operations (shared wallets, members, invites, keys)."""
200
+ return self._teams
201
+
202
+ @property
203
+ def x402(self) -> X402Resource:
204
+ """x402 protocol information (HTTP 402 micropayments)."""
205
+ return self._x402
206
+
207
+ @property
208
+ def wallet_auth(self) -> WalletAuthResource:
209
+ """Wallet-based authentication (EIP-191 signed messages)."""
210
+ return self._wallet_auth
211
+
212
+ # ── Connection methods ───────────────────────────────────────────
213
+
214
+ def connect_with_key(self, api_key: str) -> LoginResult:
215
+ """Authenticate using an API key.
216
+
217
+ Verifies the key with the server and stores the resulting JWT tokens.
218
+ """
219
+ self._api_key = api_key
220
+ return self._auth.verify_key(api_key)
221
+
222
+ def connect_with_credentials(self, email: str, password: str) -> LoginResult:
223
+ """Authenticate using email and password.
224
+
225
+ If MFA is required, the returned LoginResult will have
226
+ ``mfa_required=True``. Call ``complete_mfa()`` to finish.
227
+ """
228
+ return self._auth.login(email, password)
229
+
230
+ def complete_mfa(
231
+ self,
232
+ code: str,
233
+ *,
234
+ mfa_challenge_token: Optional[str] = None,
235
+ is_backup_code: bool = False,
236
+ ) -> LoginResult:
237
+ """Complete MFA verification after login."""
238
+ return self._auth.verify_mfa(
239
+ code,
240
+ mfa_challenge_token=mfa_challenge_token,
241
+ is_backup_code=is_backup_code,
242
+ )
243
+
244
+ def disconnect(self) -> None:
245
+ """Log out and clear stored tokens."""
246
+ try:
247
+ self._auth.logout()
248
+ except (OSError, httpx.HTTPError, DominusNodeError):
249
+ pass # Best-effort — server may be unreachable
250
+ self._token_manager.clear()
251
+ self._api_key = None
252
+
253
+ def close(self) -> None:
254
+ """Close the underlying HTTP client."""
255
+ self._http.close()
256
+
257
+ def __enter__(self) -> DominusNodeClient:
258
+ return self
259
+
260
+ def __exit__(self, *args: object) -> None:
261
+ self.close()
262
+
263
+
264
+ # ──────────────────────────────────────────────────────────────────────
265
+ # Async Client
266
+ # ──────────────────────────────────────────────────────────────────────
267
+
268
+
269
+ class AsyncDominusNodeClient:
270
+ """Asynchronous Dominus Node API client.
271
+
272
+ Args:
273
+ base_url: Base URL of the Dominus Node API server.
274
+ api_key: API key for immediate connection (optional).
275
+ email: Email for credential-based connection (optional).
276
+ password: Password for credential-based connection (optional).
277
+ access_token: Pre-existing access token (optional).
278
+ refresh_token: Pre-existing refresh token (optional).
279
+ proxy_host: Hostname of the proxy server.
280
+ http_proxy_port: HTTP proxy port.
281
+ socks5_proxy_port: SOCKS5 proxy port.
282
+ timeout: HTTP request timeout in seconds.
283
+ """
284
+
285
+ def __init__(
286
+ self,
287
+ base_url: str = DEFAULT_BASE_URL,
288
+ *,
289
+ api_key: Optional[str] = None,
290
+ email: Optional[str] = None,
291
+ password: Optional[str] = None,
292
+ access_token: Optional[str] = None,
293
+ refresh_token: Optional[str] = None,
294
+ proxy_host: str = DEFAULT_PROXY_HOST,
295
+ http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
296
+ socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
297
+ timeout: float = 30.0,
298
+ ) -> None:
299
+ self._api_key: Optional[str] = api_key
300
+ self._proxy_host = proxy_host
301
+ self._http_proxy_port = http_proxy_port
302
+ self._socks5_proxy_port = socks5_proxy_port
303
+ self._deferred_api_key = api_key
304
+ self._deferred_email = email
305
+ self._deferred_password = password
306
+
307
+ # Token management
308
+ self._token_manager = AsyncTokenManager()
309
+
310
+ # Wire up the async token refresh function
311
+ async def _async_refresh(rt: str) -> tuple[str, str]:
312
+ return await self.auth.refresh(rt)
313
+
314
+ self._token_manager.set_refresh_fn(_async_refresh)
315
+
316
+ if access_token and refresh_token:
317
+ self._token_manager.set_tokens(access_token, refresh_token)
318
+
319
+ # HTTP client
320
+ self._http = AsyncHttpClient(base_url, self._token_manager, timeout=timeout)
321
+
322
+ # Resource instances
323
+ self._auth = AsyncAuthResource(self._http, self._token_manager)
324
+ self._keys = AsyncKeysResource(self._http)
325
+ self._wallet = AsyncWalletResource(self._http)
326
+ self._usage = AsyncUsageResource(self._http)
327
+ self._plans = AsyncPlansResource(self._http)
328
+ self._sessions = AsyncSessionsResource(self._http)
329
+ self._proxy = AsyncProxyResource(
330
+ self._http,
331
+ proxy_host=proxy_host,
332
+ http_proxy_port=http_proxy_port,
333
+ socks5_proxy_port=socks5_proxy_port,
334
+ get_api_key=lambda: self._api_key,
335
+ )
336
+ self._admin = AsyncAdminResource(self._http)
337
+ self._slots = AsyncSlotsResource(self._http)
338
+ self._agentic_wallets = AsyncAgenticWalletResource(self._http)
339
+ self._teams = AsyncTeamsRes(self._http)
340
+ self._x402 = AsyncX402Resource(self._http)
341
+ self._wallet_auth = AsyncWalletAuthResource(self._http, self._token_manager)
342
+
343
+ # ── Resource properties ──────────────────────────────────────────
344
+
345
+ @property
346
+ def auth(self) -> AsyncAuthResource:
347
+ return self._auth
348
+
349
+ @property
350
+ def keys(self) -> AsyncKeysResource:
351
+ return self._keys
352
+
353
+ @property
354
+ def wallet(self) -> AsyncWalletResource:
355
+ return self._wallet
356
+
357
+ @property
358
+ def usage(self) -> AsyncUsageResource:
359
+ return self._usage
360
+
361
+ @property
362
+ def plans(self) -> AsyncPlansResource:
363
+ return self._plans
364
+
365
+ @property
366
+ def sessions(self) -> AsyncSessionsResource:
367
+ return self._sessions
368
+
369
+ @property
370
+ def proxy(self) -> AsyncProxyResource:
371
+ return self._proxy
372
+
373
+ @property
374
+ def admin(self) -> AsyncAdminResource:
375
+ return self._admin
376
+
377
+ @property
378
+ def slots(self) -> AsyncSlotsResource:
379
+ return self._slots
380
+
381
+ @property
382
+ def agentic_wallets(self) -> AsyncAgenticWalletResource:
383
+ return self._agentic_wallets
384
+
385
+ @property
386
+ def teams(self) -> AsyncTeamsRes:
387
+ """Team operations (shared wallets, members, invites, keys)."""
388
+ return self._teams
389
+
390
+ @property
391
+ def x402(self) -> AsyncX402Resource:
392
+ """x402 protocol information (HTTP 402 micropayments)."""
393
+ return self._x402
394
+
395
+ @property
396
+ def wallet_auth(self) -> AsyncWalletAuthResource:
397
+ """Wallet-based authentication (EIP-191 signed messages)."""
398
+ return self._wallet_auth
399
+
400
+ # ── Connection methods ───────────────────────────────────────────
401
+
402
+ async def connect_with_key(self, api_key: str) -> LoginResult:
403
+ """Authenticate using an API key."""
404
+ self._api_key = api_key
405
+ return await self._auth.verify_key(api_key)
406
+
407
+ async def connect_with_credentials(self, email: str, password: str) -> LoginResult:
408
+ """Authenticate using email and password."""
409
+ return await self._auth.login(email, password)
410
+
411
+ async def complete_mfa(
412
+ self,
413
+ code: str,
414
+ *,
415
+ mfa_challenge_token: Optional[str] = None,
416
+ is_backup_code: bool = False,
417
+ ) -> LoginResult:
418
+ """Complete MFA verification after login."""
419
+ return await self._auth.verify_mfa(
420
+ code,
421
+ mfa_challenge_token=mfa_challenge_token,
422
+ is_backup_code=is_backup_code,
423
+ )
424
+
425
+ async def disconnect(self) -> None:
426
+ """Log out and clear stored tokens."""
427
+ try:
428
+ await self._auth.logout()
429
+ except (OSError, httpx.HTTPError, DominusNodeError):
430
+ pass # Best-effort — server may be unreachable
431
+ self._token_manager.clear()
432
+ self._api_key = None
433
+ # Clear deferred credentials to prevent retention after disconnect
434
+ self._deferred_api_key = None
435
+ self._deferred_email = None
436
+ self._deferred_password = None
437
+
438
+ async def close(self) -> None:
439
+ """Close the underlying HTTP client."""
440
+ await self._http.close()
441
+
442
+ async def __aenter__(self) -> AsyncDominusNodeClient:
443
+ # Handle deferred auto-connect for async
444
+ # Clear credentials in finally to prevent retention on failure
445
+ try:
446
+ if self._deferred_api_key and not self._token_manager.has_tokens:
447
+ await self.connect_with_key(self._deferred_api_key)
448
+ elif self._deferred_email and self._deferred_password and not self._token_manager.has_tokens:
449
+ await self.connect_with_credentials(self._deferred_email, self._deferred_password)
450
+ finally:
451
+ self._deferred_api_key = None
452
+ self._deferred_email = None
453
+ self._deferred_password = None
454
+ return self
455
+
456
+ async def __aexit__(self, *args: object) -> None:
457
+ await self.close()
@@ -0,0 +1,18 @@
1
+ """SDK constants."""
2
+
3
+ SDK_VERSION = "1.0.0"
4
+ USER_AGENT = f"dominusnode-sdk-python/{SDK_VERSION}"
5
+
6
+ DEFAULT_BASE_URL = "https://api.dominusnode.com"
7
+ DEFAULT_PROXY_HOST = "proxy.dominusnode.com"
8
+ DEFAULT_HTTP_PROXY_PORT = 8080
9
+ DEFAULT_SOCKS5_PROXY_PORT = 1080
10
+
11
+ # Token refresh buffer: refresh if token expires within this many seconds
12
+ TOKEN_REFRESH_BUFFER_SECONDS = 60
13
+
14
+ # Price per GB in cents (matches backend PRICE_PER_GB_CENTS)
15
+ PRICE_PER_GB_CENTS = 500
16
+
17
+ # Bytes per GB
18
+ BYTES_PER_GB = 1_073_741_824
dominusnode/errors.py ADDED
@@ -0,0 +1,91 @@
1
+ """Dominus Node SDK error hierarchy.
2
+
3
+ All SDK errors inherit from DominusNodeError. HTTP status codes are mapped
4
+ to specific exception types for easy catch-and-handle patterns.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class DominusNodeError(Exception):
11
+ """Base exception for all Dominus Node SDK errors."""
12
+
13
+ def __init__(self, message: str, status_code: int | None = None) -> None:
14
+ super().__init__(message)
15
+ self.message = message
16
+ self.status_code = status_code
17
+
18
+
19
+ class AuthenticationError(DominusNodeError):
20
+ """Raised on HTTP 401 -- invalid credentials or expired token."""
21
+
22
+ def __init__(self, message: str = "Authentication failed") -> None:
23
+ super().__init__(message, status_code=401)
24
+
25
+
26
+ class AuthorizationError(DominusNodeError):
27
+ """Raised on HTTP 403 -- insufficient permissions."""
28
+
29
+ def __init__(self, message: str = "Forbidden") -> None:
30
+ super().__init__(message, status_code=403)
31
+
32
+
33
+ class RateLimitError(DominusNodeError):
34
+ """Raised on HTTP 429 -- too many requests."""
35
+
36
+ def __init__(
37
+ self,
38
+ message: str = "Rate limit exceeded",
39
+ retry_after_seconds: int = 60,
40
+ ) -> None:
41
+ super().__init__(message, status_code=429)
42
+ self.retry_after_seconds = retry_after_seconds
43
+
44
+
45
+ class InsufficientBalanceError(DominusNodeError):
46
+ """Raised on HTTP 402 -- wallet balance too low."""
47
+
48
+ def __init__(self, message: str = "Insufficient balance") -> None:
49
+ super().__init__(message, status_code=402)
50
+
51
+
52
+ class ValidationError(DominusNodeError):
53
+ """Raised on HTTP 400 -- bad request / validation failure."""
54
+
55
+ def __init__(self, message: str = "Validation error") -> None:
56
+ super().__init__(message, status_code=400)
57
+
58
+
59
+ class NotFoundError(DominusNodeError):
60
+ """Raised on HTTP 404 -- resource not found."""
61
+
62
+ def __init__(self, message: str = "Not found") -> None:
63
+ super().__init__(message, status_code=404)
64
+
65
+
66
+ class ConflictError(DominusNodeError):
67
+ """Raised on HTTP 409 -- conflict (e.g. duplicate registration)."""
68
+
69
+ def __init__(self, message: str = "Conflict") -> None:
70
+ super().__init__(message, status_code=409)
71
+
72
+
73
+ class ServerError(DominusNodeError):
74
+ """Raised on HTTP 500+ -- server-side error."""
75
+
76
+ def __init__(self, message: str = "Internal server error", status_code: int = 500) -> None:
77
+ super().__init__(message, status_code=status_code)
78
+
79
+
80
+ class NetworkError(DominusNodeError):
81
+ """Raised on connection failures, timeouts, and DNS errors."""
82
+
83
+ def __init__(self, message: str = "Network error") -> None:
84
+ super().__init__(message, status_code=None)
85
+
86
+
87
+ class ProxyError(DominusNodeError):
88
+ """Raised on proxy connection or configuration errors."""
89
+
90
+ def __init__(self, message: str = "Proxy error") -> None:
91
+ super().__init__(message, status_code=None)