dominusnode 2.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 +45 -0
- dominusnode/_ops.py +122 -0
- dominusnode/client.py +192 -0
- dominusnode/constants.py +24 -0
- dominusnode/errors.py +153 -0
- dominusnode/http_client.py +354 -0
- dominusnode/messages.py +79 -0
- dominusnode/pow.py +33 -0
- dominusnode/proxy_fetch.py +341 -0
- dominusnode/py.typed +0 -0
- dominusnode/resources.py +791 -0
- dominusnode/secret.py +60 -0
- dominusnode/signer.py +56 -0
- dominusnode/ssrf.py +176 -0
- dominusnode/token_manager.py +150 -0
- dominusnode/types.py +99 -0
- dominusnode/validation.py +117 -0
- dominusnode-2.0.0.dist-info/METADATA +177 -0
- dominusnode-2.0.0.dist-info/RECORD +22 -0
- dominusnode-2.0.0.dist-info/WHEEL +5 -0
- dominusnode-2.0.0.dist-info/licenses/LICENSE +21 -0
- dominusnode-2.0.0.dist-info/top_level.txt +1 -0
dominusnode/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Official DominusNode SDK for Python: AI-agent relay network (proxy, nodes, credits, payouts)."""
|
|
2
|
+
|
|
3
|
+
from .client import AsyncDominusNodeClient, DominusNodeClient
|
|
4
|
+
from .constants import (
|
|
5
|
+
DEFAULT_BASE_URL,
|
|
6
|
+
DEFAULT_HTTP_PROXY_PORT,
|
|
7
|
+
DEFAULT_PROXY_HOST,
|
|
8
|
+
DEFAULT_SOCKS5_PROXY_PORT,
|
|
9
|
+
MAX_AMOUNT_CENTS,
|
|
10
|
+
MAX_RESPONSE_BYTES,
|
|
11
|
+
SDK_VERSION,
|
|
12
|
+
USER_AGENT,
|
|
13
|
+
)
|
|
14
|
+
from .errors import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
AuthorizationError,
|
|
17
|
+
ConflictError,
|
|
18
|
+
DominusNodeError,
|
|
19
|
+
InsufficientBalanceError,
|
|
20
|
+
NetworkError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
ProtocolError,
|
|
23
|
+
ProxyError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
ValidationError,
|
|
27
|
+
scrub_credentials,
|
|
28
|
+
)
|
|
29
|
+
from .messages import build_attestation_message, build_payout_wallet_message, build_withdraw_message
|
|
30
|
+
from .proxy_fetch import GatewayTls, ProxyFetchResponse, build_proxy_username
|
|
31
|
+
from .secret import Secret, json_default
|
|
32
|
+
from .signer import Signer
|
|
33
|
+
from .ssrf import is_private_ip, validate_target_url
|
|
34
|
+
|
|
35
|
+
__version__ = SDK_VERSION
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"DominusNodeClient", "AsyncDominusNodeClient", "Signer", "Secret", "json_default",
|
|
39
|
+
"build_proxy_username", "ProxyFetchResponse", "GatewayTls", "is_private_ip", "validate_target_url",
|
|
40
|
+
"build_attestation_message", "build_payout_wallet_message", "build_withdraw_message",
|
|
41
|
+
"DominusNodeError", "AuthenticationError", "AuthorizationError", "RateLimitError", "InsufficientBalanceError",
|
|
42
|
+
"ValidationError", "NotFoundError", "ConflictError", "ServerError", "NetworkError", "ProtocolError", "ProxyError",
|
|
43
|
+
"scrub_credentials", "SDK_VERSION", "USER_AGENT", "DEFAULT_BASE_URL", "DEFAULT_PROXY_HOST",
|
|
44
|
+
"DEFAULT_HTTP_PROXY_PORT", "DEFAULT_SOCKS5_PROXY_PORT", "MAX_RESPONSE_BYTES", "MAX_AMOUNT_CENTS", "__version__",
|
|
45
|
+
]
|
dominusnode/_ops.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Sans-I/O core shared by the sync and async clients (internal).
|
|
2
|
+
|
|
3
|
+
Each resource method is written once as a generator that yields operations
|
|
4
|
+
(an API ``Request``, a ``Sign`` call or a blocking ``Call``) and receives their
|
|
5
|
+
results. ``drive_sync`` / ``drive_async`` execute those operations, so the sync
|
|
6
|
+
and async clients expose identical behaviour from a single implementation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import functools
|
|
13
|
+
import inspect
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Callable, Dict, Generator, Tuple, Type, TypeVar
|
|
16
|
+
|
|
17
|
+
from .errors import ProtocolError
|
|
18
|
+
from .http_client import AsyncHttpClient, Request, SyncHttpClient
|
|
19
|
+
|
|
20
|
+
Op = Any
|
|
21
|
+
Gen = Generator[Op, Any, Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Sign:
|
|
26
|
+
signer: Any
|
|
27
|
+
message: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Call:
|
|
32
|
+
"""Blocking work (PoW, socket I/O). Sync: called inline. Async: run in a worker thread."""
|
|
33
|
+
|
|
34
|
+
fn: Callable[..., Any]
|
|
35
|
+
args: Tuple[Any, ...] = ()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _exec_sync(http: SyncHttpClient, op: Op) -> Any:
|
|
39
|
+
if isinstance(op, Request):
|
|
40
|
+
return http.request(op)
|
|
41
|
+
if isinstance(op, Sign):
|
|
42
|
+
result = op.signer.sign_message(op.message)
|
|
43
|
+
if inspect.isawaitable(result):
|
|
44
|
+
if hasattr(result, "close"):
|
|
45
|
+
result.close()
|
|
46
|
+
raise ProtocolError("sign_message returned an awaitable; use AsyncDominusNodeClient for async signers")
|
|
47
|
+
return result
|
|
48
|
+
if isinstance(op, Call):
|
|
49
|
+
return op.fn(*op.args)
|
|
50
|
+
raise TypeError(f"unknown op {op!r}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _exec_async(http: AsyncHttpClient, op: Op) -> Any:
|
|
54
|
+
if isinstance(op, Request):
|
|
55
|
+
return await http.request(op)
|
|
56
|
+
if isinstance(op, Sign):
|
|
57
|
+
result = op.signer.sign_message(op.message)
|
|
58
|
+
if inspect.isawaitable(result):
|
|
59
|
+
result = await result
|
|
60
|
+
return result
|
|
61
|
+
if isinstance(op, Call):
|
|
62
|
+
return await asyncio.to_thread(op.fn, *op.args)
|
|
63
|
+
raise TypeError(f"unknown op {op!r}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def drive_sync(http: SyncHttpClient, gen: Gen) -> Any:
|
|
67
|
+
value: Any = None
|
|
68
|
+
exc: Any = None
|
|
69
|
+
while True:
|
|
70
|
+
try:
|
|
71
|
+
op = gen.throw(exc) if exc is not None else gen.send(value)
|
|
72
|
+
except StopIteration as stop:
|
|
73
|
+
return stop.value
|
|
74
|
+
try:
|
|
75
|
+
value, exc = _exec_sync(http, op), None
|
|
76
|
+
except Exception as e: # noqa: BLE001 - delivered into the generator
|
|
77
|
+
value, exc = None, e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def drive_async(http: AsyncHttpClient, gen: Gen) -> Any:
|
|
81
|
+
value: Any = None
|
|
82
|
+
exc: Any = None
|
|
83
|
+
while True:
|
|
84
|
+
try:
|
|
85
|
+
op = gen.throw(exc) if exc is not None else gen.send(value)
|
|
86
|
+
except StopIteration as stop:
|
|
87
|
+
return stop.value
|
|
88
|
+
try:
|
|
89
|
+
value, exc = await _exec_async(http, op), None
|
|
90
|
+
except Exception as e: # noqa: BLE001
|
|
91
|
+
value, exc = None, e
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
T = TypeVar("T")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sync_method(fn: Callable[..., Gen]) -> Callable[..., Any]:
|
|
98
|
+
@functools.wraps(fn)
|
|
99
|
+
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
100
|
+
return drive_sync(self._http, fn(self, *args, **kwargs))
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _async_method(fn: Callable[..., Gen]) -> Callable[..., Any]:
|
|
106
|
+
@functools.wraps(fn)
|
|
107
|
+
async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
108
|
+
return await drive_async(self._http, fn(self, *args, **kwargs))
|
|
109
|
+
|
|
110
|
+
return wrapper
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def make_variants(core: Type[T]) -> Tuple[Type[T], Type[T]]:
|
|
114
|
+
"""Build (SyncResource, AsyncResource) subclasses: every public generator method becomes a plain/async method."""
|
|
115
|
+
name = core.__name__.lstrip("_")
|
|
116
|
+
sync_ns: Dict[str, Any] = {"__doc__": core.__doc__, "__module__": core.__module__}
|
|
117
|
+
async_ns: Dict[str, Any] = {"__doc__": core.__doc__, "__module__": core.__module__}
|
|
118
|
+
for attr, value in vars(core).items():
|
|
119
|
+
if not attr.startswith("_") and inspect.isgeneratorfunction(value):
|
|
120
|
+
sync_ns[attr] = _sync_method(value)
|
|
121
|
+
async_ns[attr] = _async_method(value)
|
|
122
|
+
return type(name, (core,), sync_ns), type("Async" + name, (core,), async_ns) # type: ignore[return-value]
|
dominusnode/client.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""DominusNode relay-network clients (sync and async, identical surface)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Generator, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._ops import drive_async, drive_sync
|
|
10
|
+
from .constants import DEFAULT_BASE_URL, DEFAULT_HTTP_PROXY_PORT, DEFAULT_PROXY_HOST, DEFAULT_SOCKS5_PROXY_PORT
|
|
11
|
+
from .errors import DominusNodeError
|
|
12
|
+
from .http_client import AsyncHttpClient, SyncHttpClient
|
|
13
|
+
from .resources import (
|
|
14
|
+
AgenticWalletResource, AsyncAgenticWalletResource, AsyncAuthResource, AsyncCreditsResource,
|
|
15
|
+
AsyncKeysResource, AsyncNetworkResource, AsyncNodesResource, AsyncPayoutsResource,
|
|
16
|
+
AsyncPayoutWalletResource, AsyncProxyResource, AsyncSessionsResource, AsyncTeamsResource,
|
|
17
|
+
AsyncUsageResource, AsyncWalletAuthResource, AsyncWalletResource, AsyncX402Resource, AuthResource,
|
|
18
|
+
CreditsResource, KeysResource, NetworkResource, NodesResource, PayoutsResource, PayoutWalletResource,
|
|
19
|
+
ProxyResource, SessionsResource, TeamsResource, UsageResource, WalletAuthResource, WalletResource,
|
|
20
|
+
X402Resource, _AuthResource, _Ctx,
|
|
21
|
+
)
|
|
22
|
+
from .token_manager import AsyncTokenManager, TokenManager
|
|
23
|
+
from .validation import validate_api_key
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _connect_gen(ctx: _Ctx, auth: Any, api_key: str) -> Generator[Any, Any, Dict[str, Any]]:
|
|
27
|
+
validate_api_key(api_key)
|
|
28
|
+
try:
|
|
29
|
+
r = yield from _AuthResource.verify_key(auth, api_key)
|
|
30
|
+
except DominusNodeError as err:
|
|
31
|
+
if err.code == "MFA_REQUIRED":
|
|
32
|
+
ctx.api_key = api_key
|
|
33
|
+
return {"mfaRequired": True}
|
|
34
|
+
raise
|
|
35
|
+
ctx.api_key = api_key
|
|
36
|
+
ctx.tokens.set_tokens(r.get("token"), r.get("refreshToken"))
|
|
37
|
+
return {"mfaRequired": False, "userId": r.get("userId")}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _disconnect_gen(ctx: _Ctx, auth: Any) -> Generator[Any, Any, None]:
|
|
41
|
+
try:
|
|
42
|
+
if ctx.tokens.has_tokens():
|
|
43
|
+
try:
|
|
44
|
+
yield from _AuthResource.logout(auth)
|
|
45
|
+
except Exception: # noqa: BLE001 - best effort
|
|
46
|
+
pass
|
|
47
|
+
finally:
|
|
48
|
+
ctx.api_key = None
|
|
49
|
+
ctx.tokens.clear()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _ClientBase:
|
|
53
|
+
_ctx: _Ctx
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_connected(self) -> bool:
|
|
57
|
+
"""True when an API key or any JWT is held."""
|
|
58
|
+
return self._ctx.api_key is not None or self._ctx.tokens.has_tokens()
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"{type(self).__name__}(connected={'true' if self.is_connected else 'false'})"
|
|
62
|
+
|
|
63
|
+
__str__ = __repr__
|
|
64
|
+
|
|
65
|
+
def to_json(self) -> Dict[str, Any]:
|
|
66
|
+
return {"connected": self.is_connected}
|
|
67
|
+
|
|
68
|
+
def __reduce__(self) -> Any:
|
|
69
|
+
raise TypeError("client cannot be pickled")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DominusNodeClient(_ClientBase):
|
|
73
|
+
"""Synchronous client::
|
|
74
|
+
|
|
75
|
+
with DominusNodeClient() as dn:
|
|
76
|
+
dn.connect_with_key(os.environ["DOMINUSNODE_API_KEY"])
|
|
77
|
+
res = dn.proxy.fetch("https://example.com", country="US")
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
83
|
+
proxy_host: str = DEFAULT_PROXY_HOST,
|
|
84
|
+
http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
|
|
85
|
+
socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
|
|
86
|
+
access_token: Optional[str] = None,
|
|
87
|
+
refresh_token: Optional[str] = None,
|
|
88
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
89
|
+
gateway_tls: Any = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
tokens = TokenManager()
|
|
92
|
+
self._ctx = ctx = _Ctx(tokens)
|
|
93
|
+
self._http = http = SyncHttpClient(base_url, tokens, lambda: ctx.api_key, transport)
|
|
94
|
+
self.auth = AuthResource(http, ctx)
|
|
95
|
+
self.wallet_auth = WalletAuthResource(http, ctx)
|
|
96
|
+
self.keys = KeysResource(http, ctx)
|
|
97
|
+
self.wallet = WalletResource(http, ctx)
|
|
98
|
+
self.agentic_wallets = AgenticWalletResource(http, ctx)
|
|
99
|
+
self.teams = TeamsResource(http, ctx)
|
|
100
|
+
self.usage = UsageResource(http, ctx)
|
|
101
|
+
self.sessions = SessionsResource(http, ctx)
|
|
102
|
+
self.x402 = X402Resource(http, ctx)
|
|
103
|
+
self.network = NetworkResource(http, ctx)
|
|
104
|
+
self.nodes = NodesResource(http, ctx)
|
|
105
|
+
self.credits = CreditsResource(http, ctx)
|
|
106
|
+
self.payout_wallet = PayoutWalletResource(http, ctx)
|
|
107
|
+
self.payouts = PayoutsResource(http, ctx)
|
|
108
|
+
self.proxy = ProxyResource(http, ctx, proxy_host, http_proxy_port, socks5_proxy_port, gateway_tls)
|
|
109
|
+
|
|
110
|
+
def refresh(rt: str) -> Any:
|
|
111
|
+
r = self.auth.refresh(rt)
|
|
112
|
+
return r.get("token"), r.get("refreshToken")
|
|
113
|
+
|
|
114
|
+
tokens.set_refresh_function(refresh) # never cleared
|
|
115
|
+
if access_token:
|
|
116
|
+
tokens.set_tokens(access_token, refresh_token)
|
|
117
|
+
|
|
118
|
+
def connect_with_key(self, api_key: str) -> Dict[str, Any]:
|
|
119
|
+
"""Returns {mfaRequired: False, userId} or {mfaRequired: True} (key-auth routes still work)."""
|
|
120
|
+
return drive_sync(self._http, _connect_gen(self._ctx, self.auth, api_key))
|
|
121
|
+
|
|
122
|
+
def disconnect(self) -> None:
|
|
123
|
+
"""Best-effort logout, then clear the API key, access token and refresh token."""
|
|
124
|
+
drive_sync(self._http, _disconnect_gen(self._ctx, self.auth))
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
self.disconnect()
|
|
128
|
+
self._http.close()
|
|
129
|
+
|
|
130
|
+
def __enter__(self) -> "DominusNodeClient":
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def __exit__(self, *exc: object) -> None:
|
|
134
|
+
self.close()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AsyncDominusNodeClient(_ClientBase):
|
|
138
|
+
"""asyncio client with the same surface; every resource method is a coroutine (signers may be async)."""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
143
|
+
proxy_host: str = DEFAULT_PROXY_HOST,
|
|
144
|
+
http_proxy_port: int = DEFAULT_HTTP_PROXY_PORT,
|
|
145
|
+
socks5_proxy_port: int = DEFAULT_SOCKS5_PROXY_PORT,
|
|
146
|
+
access_token: Optional[str] = None,
|
|
147
|
+
refresh_token: Optional[str] = None,
|
|
148
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
149
|
+
gateway_tls: Any = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
tokens = AsyncTokenManager()
|
|
152
|
+
self._ctx = ctx = _Ctx(tokens)
|
|
153
|
+
self._http = http = AsyncHttpClient(base_url, tokens, lambda: ctx.api_key, transport)
|
|
154
|
+
self.auth = AsyncAuthResource(http, ctx)
|
|
155
|
+
self.wallet_auth = AsyncWalletAuthResource(http, ctx)
|
|
156
|
+
self.keys = AsyncKeysResource(http, ctx)
|
|
157
|
+
self.wallet = AsyncWalletResource(http, ctx)
|
|
158
|
+
self.agentic_wallets = AsyncAgenticWalletResource(http, ctx)
|
|
159
|
+
self.teams = AsyncTeamsResource(http, ctx)
|
|
160
|
+
self.usage = AsyncUsageResource(http, ctx)
|
|
161
|
+
self.sessions = AsyncSessionsResource(http, ctx)
|
|
162
|
+
self.x402 = AsyncX402Resource(http, ctx)
|
|
163
|
+
self.network = AsyncNetworkResource(http, ctx)
|
|
164
|
+
self.nodes = AsyncNodesResource(http, ctx)
|
|
165
|
+
self.credits = AsyncCreditsResource(http, ctx)
|
|
166
|
+
self.payout_wallet = AsyncPayoutWalletResource(http, ctx)
|
|
167
|
+
self.payouts = AsyncPayoutsResource(http, ctx)
|
|
168
|
+
self.proxy = AsyncProxyResource(http, ctx, proxy_host, http_proxy_port, socks5_proxy_port, gateway_tls)
|
|
169
|
+
|
|
170
|
+
async def refresh(rt: str) -> Any:
|
|
171
|
+
r = await self.auth.refresh(rt)
|
|
172
|
+
return r.get("token"), r.get("refreshToken")
|
|
173
|
+
|
|
174
|
+
tokens.set_refresh_function(refresh)
|
|
175
|
+
if access_token:
|
|
176
|
+
tokens.set_tokens(access_token, refresh_token)
|
|
177
|
+
|
|
178
|
+
async def connect_with_key(self, api_key: str) -> Dict[str, Any]:
|
|
179
|
+
return await drive_async(self._http, _connect_gen(self._ctx, self.auth, api_key))
|
|
180
|
+
|
|
181
|
+
async def disconnect(self) -> None:
|
|
182
|
+
await drive_async(self._http, _disconnect_gen(self._ctx, self.auth))
|
|
183
|
+
|
|
184
|
+
async def close(self) -> None:
|
|
185
|
+
await self.disconnect()
|
|
186
|
+
await self._http.close()
|
|
187
|
+
|
|
188
|
+
async def __aenter__(self) -> "AsyncDominusNodeClient":
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
192
|
+
await self.close()
|
dominusnode/constants.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""SDK-wide constants (mirrors sdks/node/src/constants.ts)."""
|
|
2
|
+
|
|
3
|
+
SDK_VERSION = "2.0.0"
|
|
4
|
+
USER_AGENT = f"dominusnode-sdk-python/{SDK_VERSION}"
|
|
5
|
+
DEFAULT_BASE_URL = "https://api.dominusnode.daedalusdevelopmentgroup.com"
|
|
6
|
+
DEFAULT_PROXY_HOST = "proxy.dominusnode.daedalusdevelopmentgroup.com"
|
|
7
|
+
DEFAULT_HTTP_PROXY_PORT = 8080
|
|
8
|
+
DEFAULT_SOCKS5_PROXY_PORT = 1080
|
|
9
|
+
#: Gateway HTTPS proxy listener (PROXY_TLS_PORT), used with gateway_tls.
|
|
10
|
+
DEFAULT_TLS_PROXY_PORT = 8443
|
|
11
|
+
#: Refresh JWTs this many seconds before expiry.
|
|
12
|
+
TOKEN_REFRESH_BUFFER_SECONDS = 60
|
|
13
|
+
#: Hard cap on any response body the SDK reads (API or proxy fetch).
|
|
14
|
+
MAX_RESPONSE_BYTES = 10 * 1024 * 1024
|
|
15
|
+
#: API request timeout (seconds).
|
|
16
|
+
REQUEST_TIMEOUT_SECONDS = 30.0
|
|
17
|
+
#: 429 retry: one retry, wait = min(Retry-After, 10s) +/-10% jitter, floor 100ms.
|
|
18
|
+
MAX_RATE_LIMIT_RETRIES = 1
|
|
19
|
+
MAX_RETRY_WAIT_SECONDS = 10.0
|
|
20
|
+
MIN_RETRY_WAIT_SECONDS = 0.1
|
|
21
|
+
#: Largest value accepted for any *_cents argument (PostgreSQL INTEGER max).
|
|
22
|
+
MAX_AMOUNT_CENTS = 2_147_483_647
|
|
23
|
+
#: 1 USD = 1,000,000 µUSD.
|
|
24
|
+
MUSD_PER_USD = 1_000_000
|
dominusnode/errors.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Error hierarchy.
|
|
2
|
+
|
|
3
|
+
Every message passes through :func:`scrub_credentials` so API keys
|
|
4
|
+
(dn_live_/dn_test_/dn_node_/dn_proxy_) and JWTs never reach logs or callers
|
|
5
|
+
via error strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Dict, Optional, Union
|
|
12
|
+
|
|
13
|
+
_CREDENTIAL_PATTERNS = [
|
|
14
|
+
re.compile(r"dn_(?:live|test|node|proxy)_[A-Za-z0-9_-]+"),
|
|
15
|
+
re.compile(r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
Details = Dict[str, Union[int, float, str]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scrub_credentials(message: object) -> str:
|
|
22
|
+
out = str(message)
|
|
23
|
+
for pattern in _CREDENTIAL_PATTERNS:
|
|
24
|
+
out = pattern.sub("[REDACTED]", out)
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DominusNodeError(Exception):
|
|
29
|
+
"""Base class. ``code`` is the server's error code (e.g. ``INVALID_CHALLENGE``)."""
|
|
30
|
+
|
|
31
|
+
default_message = "DominusNode error"
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: Optional[str] = None,
|
|
36
|
+
status_code: Optional[int] = None,
|
|
37
|
+
code: Optional[str] = None,
|
|
38
|
+
details: Optional[Details] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.message = scrub_credentials(message if message is not None else self.default_message)
|
|
41
|
+
self.status_code = status_code
|
|
42
|
+
self.code = code
|
|
43
|
+
self.details = details
|
|
44
|
+
super().__init__(self.message)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return self.message
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
return f"{type(self).__name__}({self.message!r}, status_code={self.status_code!r}, code={self.code!r})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthenticationError(DominusNodeError):
|
|
54
|
+
default_message = "Authentication failed"
|
|
55
|
+
|
|
56
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
|
|
57
|
+
super().__init__(message, 401, code, details)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InsufficientBalanceError(DominusNodeError):
|
|
61
|
+
default_message = "Insufficient balance"
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
|
|
64
|
+
super().__init__(message, 402, code, details)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AuthorizationError(DominusNodeError):
|
|
68
|
+
default_message = "Access denied"
|
|
69
|
+
|
|
70
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
|
|
71
|
+
super().__init__(message, 403, code, details)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NotFoundError(DominusNodeError):
|
|
75
|
+
default_message = "Resource not found"
|
|
76
|
+
|
|
77
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
|
|
78
|
+
super().__init__(message, 404, code, details)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ConflictError(DominusNodeError):
|
|
82
|
+
default_message = "Resource conflict"
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: Optional[str] = None, code: Optional[str] = None, details: Optional[Details] = None) -> None:
|
|
85
|
+
super().__init__(message, 409, code, details)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RateLimitError(DominusNodeError):
|
|
89
|
+
default_message = "Rate limit exceeded"
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
message: Optional[str] = None,
|
|
94
|
+
retry_after_seconds: int = 60,
|
|
95
|
+
code: Optional[str] = None,
|
|
96
|
+
details: Optional[Details] = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
super().__init__(message, 429, code, details)
|
|
99
|
+
self.retry_after_seconds = retry_after_seconds
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ServerError(DominusNodeError):
|
|
103
|
+
default_message = "Server error"
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
message: Optional[str] = None,
|
|
108
|
+
status_code: int = 500,
|
|
109
|
+
code: Optional[str] = None,
|
|
110
|
+
details: Optional[Details] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
super().__init__(message, status_code, code, details)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ValidationError(DominusNodeError):
|
|
116
|
+
"""HTTP 400, or a client-side argument check (status_code None, code INVALID_ARGUMENT)."""
|
|
117
|
+
|
|
118
|
+
default_message = "Validation failed"
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
message: Optional[str] = None,
|
|
123
|
+
status_code: Optional[int] = 400,
|
|
124
|
+
code: Optional[str] = None,
|
|
125
|
+
details: Optional[Details] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(message, status_code, code, details)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class NetworkError(DominusNodeError):
|
|
131
|
+
default_message = "Network error"
|
|
132
|
+
|
|
133
|
+
def __init__(self, message: Optional[str] = None) -> None:
|
|
134
|
+
super().__init__(message)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ProtocolError(DominusNodeError):
|
|
138
|
+
"""The server sent something the SDK refuses to act on, or a signer failed."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, message: str) -> None:
|
|
141
|
+
super().__init__(message)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ProxyError(DominusNodeError):
|
|
145
|
+
"""Failure inside proxy.fetch(): blocked target, refused tunnel, timeout."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, message: str, status_code: Optional[int] = None) -> None:
|
|
148
|
+
super().__init__(message, status_code)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def invalid_arg(message: str) -> ValidationError:
|
|
152
|
+
"""Client-side argument validation failure (no request sent)."""
|
|
153
|
+
return ValidationError(message, None, "INVALID_ARGUMENT")
|