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/__init__.py +151 -0
- dominusnode/admin.py +252 -0
- dominusnode/agent_wallet.py +240 -0
- dominusnode/auth.py +271 -0
- dominusnode/client.py +457 -0
- dominusnode/constants.py +18 -0
- dominusnode/errors.py +91 -0
- dominusnode/http_client.py +435 -0
- dominusnode/keys.py +88 -0
- dominusnode/plans.py +93 -0
- dominusnode/proxy.py +248 -0
- dominusnode/py.typed +0 -0
- dominusnode/sessions.py +55 -0
- dominusnode/slots.py +60 -0
- dominusnode/teams.py +339 -0
- dominusnode/token_manager.py +235 -0
- dominusnode/types.py +521 -0
- dominusnode/usage.py +230 -0
- dominusnode/wallet.py +189 -0
- dominusnode/wallet_auth.py +241 -0
- dominusnode/x402.py +88 -0
- dominusnode-1.0.0.dist-info/LICENSE +21 -0
- dominusnode-1.0.0.dist-info/METADATA +13 -0
- dominusnode-1.0.0.dist-info/RECORD +26 -0
- dominusnode-1.0.0.dist-info/WHEEL +5 -0
- dominusnode-1.0.0.dist-info/top_level.txt +1 -0
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()
|
dominusnode/constants.py
ADDED
|
@@ -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)
|