revolut-x-python 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.
- revolut_x/__init__.py +103 -0
- revolut_x/_auth.py +139 -0
- revolut_x/_http.py +270 -0
- revolut_x/_version.py +10 -0
- revolut_x/account.py +203 -0
- revolut_x/client.py +170 -0
- revolut_x/exceptions.py +147 -0
- revolut_x/helpers.py +540 -0
- revolut_x/market.py +469 -0
- revolut_x/orders.py +522 -0
- revolut_x/py.typed +0 -0
- revolut_x/types.py +397 -0
- revolut_x_python-1.0.0.dist-info/METADATA +164 -0
- revolut_x_python-1.0.0.dist-info/RECORD +17 -0
- revolut_x_python-1.0.0.dist-info/WHEEL +5 -0
- revolut_x_python-1.0.0.dist-info/licenses/LICENSE +21 -0
- revolut_x_python-1.0.0.dist-info/top_level.txt +1 -0
revolut_x/account.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Account and wallet methods for the Revolut X API.
|
|
2
|
+
|
|
3
|
+
This module provides :class:`AccountMixin`, which is mixed into
|
|
4
|
+
:class:`~revolut_x.client.RevolutXClient` to add authenticated endpoints
|
|
5
|
+
for account balances, transaction history, and private trade history.
|
|
6
|
+
|
|
7
|
+
All methods in this mixin **require** an API key and private key.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, TYPE_CHECKING
|
|
13
|
+
from revolut_x.types import Balance, Trade
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from revolut_x._http import HttpClient
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AccountMixin:
|
|
22
|
+
"""Authenticated account endpoints — balances, transactions, private trades.
|
|
23
|
+
|
|
24
|
+
This mixin is not meant to be used directly. It is mixed into
|
|
25
|
+
:class:`~revolut_x.client.RevolutXClient`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Provided by RevolutXClient
|
|
29
|
+
_http: HttpClient
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Balances
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def get_balances(self) -> list[Balance]:
|
|
36
|
+
"""Retrieve current balances for all currencies on the account.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A list of :class:`~revolut_x.types.Balance` dictionaries, each
|
|
40
|
+
containing ``currency``, ``available``, ``reserved``, and
|
|
41
|
+
``total`` fields.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
AuthenticationError: If the client is not authenticated.
|
|
45
|
+
ApiError: If the API returns a non-2xx response.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
>>> balances = client.get_balances()
|
|
50
|
+
>>> for b in balances:
|
|
51
|
+
... if float(b["total"]) > 0:
|
|
52
|
+
... print(f"{b['currency']}: {b['available']} available")
|
|
53
|
+
EUR: 150.00 available
|
|
54
|
+
BTC: 0.00050463 available
|
|
55
|
+
"""
|
|
56
|
+
_status, data = self._http.request(
|
|
57
|
+
"GET", "/balances", authenticated=True
|
|
58
|
+
)
|
|
59
|
+
return data if isinstance(data, list) else []
|
|
60
|
+
|
|
61
|
+
def get_balance(self, currency: str) -> Balance:
|
|
62
|
+
"""Retrieve the balance for a specific currency.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
currency: Currency symbol (e.g. ``'EUR'``, ``'BTC'``, ``'ETH'``).
|
|
66
|
+
Case-insensitive.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A :class:`~revolut_x.types.Balance` dictionary. If the currency
|
|
70
|
+
is not found on the account, returns a zero-balance entry.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AuthenticationError: If the client is not authenticated.
|
|
74
|
+
|
|
75
|
+
Example::
|
|
76
|
+
|
|
77
|
+
>>> eur = client.get_balance("EUR")
|
|
78
|
+
>>> print(f"Available: {eur['available']} EUR")
|
|
79
|
+
Available: 150.00 EUR
|
|
80
|
+
"""
|
|
81
|
+
balances = self.get_balances()
|
|
82
|
+
target = currency.upper()
|
|
83
|
+
|
|
84
|
+
for item in balances:
|
|
85
|
+
if isinstance(item, dict) and item.get("currency", "").upper() == target:
|
|
86
|
+
return item
|
|
87
|
+
|
|
88
|
+
# Currency not found — return zero balance
|
|
89
|
+
return {
|
|
90
|
+
"currency": target,
|
|
91
|
+
"available": "0.00",
|
|
92
|
+
"reserved": "0.00",
|
|
93
|
+
"total": "0.00",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Transactions
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def get_transactions(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
limit: int = 50,
|
|
104
|
+
cursor: str | None = None,
|
|
105
|
+
) -> tuple[list[dict[str, Any]], str | None]:
|
|
106
|
+
"""Retrieve account transaction history (ledger).
|
|
107
|
+
|
|
108
|
+
Transactions include deposits, withdrawals, trade settlements, and
|
|
109
|
+
fee charges. Each entry shows the source and destination amounts
|
|
110
|
+
and currencies.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
limit: Number of transactions per page (default: 50).
|
|
114
|
+
cursor: Pagination cursor from a previous response.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A tuple of ``(transactions, next_cursor)``. Each transaction is
|
|
118
|
+
a dictionary with keys like ``source``, ``destination``,
|
|
119
|
+
``created_date``, ``processed_date``. ``next_cursor`` is ``None``
|
|
120
|
+
if there are no more pages.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
AuthenticationError: If the client is not authenticated.
|
|
124
|
+
|
|
125
|
+
Example::
|
|
126
|
+
|
|
127
|
+
>>> txns, cursor = client.get_transactions(limit=10)
|
|
128
|
+
>>> for tx in txns:
|
|
129
|
+
... print(f"{tx['source']['currency']} -> {tx['destination']['currency']}")
|
|
130
|
+
"""
|
|
131
|
+
params: dict[str, Any] = {"limit": limit}
|
|
132
|
+
if cursor:
|
|
133
|
+
params["cursor"] = cursor
|
|
134
|
+
|
|
135
|
+
_status, resp = self._http.request(
|
|
136
|
+
"GET", "/transactions", params=params, authenticated=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if isinstance(resp, dict):
|
|
140
|
+
transactions = resp.get("data", [])
|
|
141
|
+
next_cursor = resp.get("metadata", {}).get("next_cursor")
|
|
142
|
+
else:
|
|
143
|
+
transactions = resp if isinstance(resp, list) else []
|
|
144
|
+
next_cursor = None
|
|
145
|
+
|
|
146
|
+
return transactions, next_cursor
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Private Trades
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def get_account_trades(
|
|
153
|
+
self,
|
|
154
|
+
symbol: str,
|
|
155
|
+
*,
|
|
156
|
+
limit: int = 50,
|
|
157
|
+
cursor: str | None = None,
|
|
158
|
+
) -> tuple[list[Trade], str | None]:
|
|
159
|
+
"""Retrieve private (your own) trade history for a trading pair.
|
|
160
|
+
|
|
161
|
+
Unlike public trades from :meth:`get_trades`, these are your
|
|
162
|
+
personal executed trades with full fee information.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
symbol: Trading pair (e.g. ``'BTC-EUR'``).
|
|
166
|
+
limit: Number of trades per page (default: 50).
|
|
167
|
+
cursor: Pagination cursor from a previous response.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A tuple of ``(trades, next_cursor)`` where ``trades`` is a list
|
|
171
|
+
of :class:`~revolut_x.types.Trade` dicts with your private
|
|
172
|
+
execution details.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
AuthenticationError: If the client is not authenticated.
|
|
176
|
+
ApiError: If the API returns a non-2xx response.
|
|
177
|
+
|
|
178
|
+
Example::
|
|
179
|
+
|
|
180
|
+
>>> trades, cursor = client.get_account_trades("BTC-EUR", limit=10)
|
|
181
|
+
>>> for t in trades:
|
|
182
|
+
... print(f"{t['side']} {t['quantity']} BTC @ {t['price']} EUR")
|
|
183
|
+
"""
|
|
184
|
+
api_symbol = symbol.replace("/", "-").upper()
|
|
185
|
+
params: dict[str, Any] = {"limit": limit}
|
|
186
|
+
if cursor:
|
|
187
|
+
params["cursor"] = cursor
|
|
188
|
+
|
|
189
|
+
_status, resp = self._http.request(
|
|
190
|
+
"GET",
|
|
191
|
+
f"/trades/private/{api_symbol}",
|
|
192
|
+
params=params,
|
|
193
|
+
authenticated=True,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if isinstance(resp, dict):
|
|
197
|
+
trades = resp.get("data", [])
|
|
198
|
+
next_cursor = resp.get("metadata", {}).get("next_cursor")
|
|
199
|
+
else:
|
|
200
|
+
trades = resp if isinstance(resp, list) else []
|
|
201
|
+
next_cursor = None
|
|
202
|
+
|
|
203
|
+
return trades, next_cursor
|
revolut_x/client.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Main client for the Revolut X Crypto Exchange REST API.
|
|
2
|
+
|
|
3
|
+
This module provides :class:`RevolutXClient`, the single entry point for all
|
|
4
|
+
Revolut X API operations. It combines public market data, order management,
|
|
5
|
+
and account endpoints into one unified interface.
|
|
6
|
+
|
|
7
|
+
Example — public data (no API key needed)::
|
|
8
|
+
|
|
9
|
+
from revolut_x import RevolutXClient
|
|
10
|
+
|
|
11
|
+
client = RevolutXClient()
|
|
12
|
+
ticker = client.get_ticker("BTC-EUR")
|
|
13
|
+
print(f"BTC price: {ticker['last_price']} EUR")
|
|
14
|
+
|
|
15
|
+
Example — trading (requires API key and private key)::
|
|
16
|
+
|
|
17
|
+
from revolut_x import RevolutXClient, OrderSide
|
|
18
|
+
|
|
19
|
+
client = RevolutXClient(
|
|
20
|
+
api_key="Vw7fXD...",
|
|
21
|
+
private_key_path="keys/private.pem",
|
|
22
|
+
)
|
|
23
|
+
order = client.place_market_order("BTC-EUR", OrderSide.BUY, quote_size="50.00")
|
|
24
|
+
print(f"Order ID: {order['venue_order_id']}")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
import logging
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from revolut_x._auth import load_private_key
|
|
31
|
+
from revolut_x._http import HttpClient
|
|
32
|
+
from revolut_x.account import AccountMixin
|
|
33
|
+
from revolut_x.market import MarketMixin
|
|
34
|
+
from revolut_x.orders import OrdersMixin
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RevolutXClient(MarketMixin, OrdersMixin, AccountMixin):
|
|
40
|
+
"""Client for the Revolut X Crypto Exchange REST API.
|
|
41
|
+
|
|
42
|
+
Provides complete access to:
|
|
43
|
+
|
|
44
|
+
* **Public market data** (no API key needed): trading pair configuration,
|
|
45
|
+
currency info, tickers, order book, OHLCV candles, and public trades.
|
|
46
|
+
* **Order management** (API key required): place market/limit orders,
|
|
47
|
+
cancel orders, inspect fills, view active and historical orders.
|
|
48
|
+
* **Account data** (API key required): balances, transaction ledger,
|
|
49
|
+
private trade history.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
api_key: Your Revolut X API key. Required only for authenticated
|
|
53
|
+
endpoints (orders, balances, etc.). Get it from your
|
|
54
|
+
`Revolut X account settings <https://revx.revolut.com>`_.
|
|
55
|
+
private_key_path: Path to your Ed25519 private key PEM file.
|
|
56
|
+
Mutually exclusive with ``private_key_bytes``.
|
|
57
|
+
private_key_bytes: Ed25519 private key as raw PEM bytes.
|
|
58
|
+
Mutually exclusive with ``private_key_path``.
|
|
59
|
+
base_url: API base URL (default: ``'https://revx.revolut.com/api'``).
|
|
60
|
+
Change for sandbox/testing environments.
|
|
61
|
+
api_version: API version string (default: ``'1.0'``).
|
|
62
|
+
request_delay: Minimum seconds between HTTP requests to respect
|
|
63
|
+
rate limits (default: ``0.85``). The Revolut X public API
|
|
64
|
+
allows 1 request per second.
|
|
65
|
+
timeout: HTTP request timeout in seconds (default: ``15``).
|
|
66
|
+
max_retries: Maximum retry attempts for rate-limited (429) or
|
|
67
|
+
failed requests (default: ``3``).
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
AuthenticationError: If ``private_key_path`` is provided but the
|
|
71
|
+
file does not exist or contains an invalid key.
|
|
72
|
+
|
|
73
|
+
Example::
|
|
74
|
+
|
|
75
|
+
# Public data only — no credentials needed
|
|
76
|
+
client = RevolutXClient()
|
|
77
|
+
pairs = client.get_pairs()
|
|
78
|
+
candles = client.get_candles("BTC-EUR", Interval.HOUR_1)
|
|
79
|
+
|
|
80
|
+
# Full access — trading and account management
|
|
81
|
+
client = RevolutXClient(
|
|
82
|
+
api_key="your-api-key",
|
|
83
|
+
private_key_path="/path/to/private.pem",
|
|
84
|
+
)
|
|
85
|
+
balances = client.get_balances()
|
|
86
|
+
order = client.place_market_order("BTC-EUR", OrderSide.BUY, quote_size="50")
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
api_key: str | None = None,
|
|
93
|
+
private_key_path: str | Path | None = None,
|
|
94
|
+
private_key_bytes: bytes | None = None,
|
|
95
|
+
base_url: str = "https://revx.revolut.com/api",
|
|
96
|
+
api_version: str = "1.0",
|
|
97
|
+
request_delay: float = 0.85,
|
|
98
|
+
timeout: int = 15,
|
|
99
|
+
max_retries: int = 3,
|
|
100
|
+
timestamp_offset_ms: int = 0,
|
|
101
|
+
) -> None:
|
|
102
|
+
# Load private key if credentials are provided
|
|
103
|
+
private_key = None
|
|
104
|
+
if private_key_path is not None:
|
|
105
|
+
private_key = load_private_key(private_key_path)
|
|
106
|
+
logger.info("Ed25519 private key loaded from %s", private_key_path)
|
|
107
|
+
elif private_key_bytes is not None:
|
|
108
|
+
private_key = load_private_key(private_key_bytes)
|
|
109
|
+
logger.info("Ed25519 private key loaded from bytes")
|
|
110
|
+
|
|
111
|
+
# Create the internal HTTP client shared by all mixins
|
|
112
|
+
self._http = HttpClient(
|
|
113
|
+
base_url=base_url,
|
|
114
|
+
api_version=api_version,
|
|
115
|
+
api_key=api_key,
|
|
116
|
+
private_key=private_key,
|
|
117
|
+
request_delay=request_delay,
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
max_retries=max_retries,
|
|
120
|
+
timestamp_offset_ms=timestamp_offset_ms,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Store for diagnostics
|
|
124
|
+
self._api_key = api_key
|
|
125
|
+
self._has_private_key = private_key is not None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def timestamp_offset(self) -> int:
|
|
129
|
+
"""Timestamp clock offset in milliseconds."""
|
|
130
|
+
return self._http.timestamp_offset_ms
|
|
131
|
+
|
|
132
|
+
@timestamp_offset.setter
|
|
133
|
+
def timestamp_offset(self, val: int) -> None:
|
|
134
|
+
self._http.timestamp_offset_ms = int(val)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def timestamp_offset_ms(self) -> int:
|
|
138
|
+
"""Alias for timestamp_offset."""
|
|
139
|
+
return self._http.timestamp_offset_ms
|
|
140
|
+
|
|
141
|
+
@timestamp_offset_ms.setter
|
|
142
|
+
def timestamp_offset_ms(self, val: int) -> None:
|
|
143
|
+
self._http.timestamp_offset_ms = int(val)
|
|
144
|
+
|
|
145
|
+
def sync_time(self) -> int:
|
|
146
|
+
"""Synchronize timestamp offset with Revolut X server time."""
|
|
147
|
+
return self._http.sync_time()
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def is_authenticated(self) -> bool:
|
|
151
|
+
"""Whether the client has API key and private key configured.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
``True`` if both ``api_key`` and a private key were provided,
|
|
155
|
+
meaning authenticated endpoints (orders, balances) can be used.
|
|
156
|
+
|
|
157
|
+
Example::
|
|
158
|
+
|
|
159
|
+
>>> client = RevolutXClient()
|
|
160
|
+
>>> client.is_authenticated
|
|
161
|
+
False
|
|
162
|
+
>>> client = RevolutXClient(api_key="...", private_key_path="...")
|
|
163
|
+
>>> client.is_authenticated
|
|
164
|
+
True
|
|
165
|
+
"""
|
|
166
|
+
return bool(self._api_key and self._has_private_key)
|
|
167
|
+
|
|
168
|
+
def __repr__(self) -> str:
|
|
169
|
+
auth = "authenticated" if self.is_authenticated else "public-only"
|
|
170
|
+
return f"<RevolutXClient({auth}, base_url='{self._http.base_url}')>"
|
revolut_x/exceptions.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Custom exceptions for the revolut-x-python library.
|
|
2
|
+
|
|
3
|
+
Exception hierarchy::
|
|
4
|
+
|
|
5
|
+
RevolutXError (base)
|
|
6
|
+
├── AuthenticationError — missing/invalid API key or private key
|
|
7
|
+
├── RateLimitError — HTTP 429, retry after N seconds
|
|
8
|
+
├── ApiError — any non-2xx API response
|
|
9
|
+
├── OrderValidationError — payload violates exchange rules
|
|
10
|
+
└── NetworkError — connection timeout, DNS failure, etc.
|
|
11
|
+
|
|
12
|
+
All exceptions inherit from :class:`RevolutXError`, so you can catch them all
|
|
13
|
+
with a single ``except RevolutXError`` block, or handle each type individually.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RevolutXError(Exception):
|
|
21
|
+
"""Base exception for all errors raised by the revolut-x-python library.
|
|
22
|
+
|
|
23
|
+
You can catch this to handle any library error generically::
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
client.get_ticker("BTC-EUR")
|
|
27
|
+
except RevolutXError as exc:
|
|
28
|
+
logging.error("Revolut X error: %s", exc)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthenticationError(RevolutXError):
|
|
33
|
+
"""Raised when authentication is missing, misconfigured, or rejected.
|
|
34
|
+
|
|
35
|
+
Common causes:
|
|
36
|
+
|
|
37
|
+
* ``api_key`` was not provided when creating :class:`RevolutXClient`.
|
|
38
|
+
* The private key file does not exist or has an invalid format.
|
|
39
|
+
* The API returned HTTP 401 or 403 (expired key, wrong signature, …).
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
hint: A human-readable suggestion on how to fix the problem.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, *, hint: str | None = None) -> None:
|
|
46
|
+
self.hint = hint
|
|
47
|
+
full = message
|
|
48
|
+
if hint:
|
|
49
|
+
full = f"{message}\n Hint: {hint}"
|
|
50
|
+
super().__init__(full)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RateLimitError(RevolutXError):
|
|
54
|
+
"""Raised when the Revolut X API returns HTTP 429 (Too Many Requests).
|
|
55
|
+
|
|
56
|
+
The library retries automatically up to ``max_retries`` times. This
|
|
57
|
+
exception is raised only when all retries have been exhausted.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
retry_after_seconds: Suggested wait time (from the ``Retry-After``
|
|
61
|
+
header), in seconds. May be ``None`` if the header was absent.
|
|
62
|
+
endpoint: The API endpoint that triggered the rate limit.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
message: str,
|
|
68
|
+
*,
|
|
69
|
+
retry_after_seconds: float | None = None,
|
|
70
|
+
endpoint: str | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.retry_after_seconds = retry_after_seconds
|
|
73
|
+
self.endpoint = endpoint
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ApiError(RevolutXError):
|
|
78
|
+
"""Raised when the Revolut X API returns a non-2xx HTTP status code.
|
|
79
|
+
|
|
80
|
+
This covers client errors (4xx) and server errors (5xx) that are not
|
|
81
|
+
authentication (401/403) or rate-limit (429) related.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
status_code: The HTTP status code returned by the API (e.g. 400, 404, 500).
|
|
85
|
+
response_body: The parsed JSON response body, or raw text if parsing failed.
|
|
86
|
+
endpoint: The API endpoint that was called (e.g. ``POST /api/1.0/orders``).
|
|
87
|
+
method: The HTTP method used (e.g. ``GET``, ``POST``).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
*,
|
|
94
|
+
status_code: int,
|
|
95
|
+
response_body: Any = None,
|
|
96
|
+
endpoint: str = "",
|
|
97
|
+
method: str = "",
|
|
98
|
+
) -> None:
|
|
99
|
+
self.status_code = status_code
|
|
100
|
+
self.response_body = response_body
|
|
101
|
+
self.endpoint = endpoint
|
|
102
|
+
self.method = method
|
|
103
|
+
super().__init__(message)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class OrderValidationError(RevolutXError, ValueError):
|
|
107
|
+
"""Raised when an order payload violates exchange rules before submission.
|
|
108
|
+
|
|
109
|
+
This is a **client-side** validation — the order is not sent to the
|
|
110
|
+
exchange. Fix the payload and try again.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
errors: A list of human-readable strings describing each violated rule.
|
|
114
|
+
|
|
115
|
+
Example::
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
client.place_market_order("BTC-EUR", OrderSide.BUY, quote_size="0.01")
|
|
119
|
+
except OrderValidationError as exc:
|
|
120
|
+
for err in exc.errors:
|
|
121
|
+
print(f" - {err}")
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, message: str, *, errors: list[str] | None = None) -> None:
|
|
125
|
+
self.errors = errors or []
|
|
126
|
+
if self.errors:
|
|
127
|
+
details = "\n".join(f" - {e}" for e in self.errors)
|
|
128
|
+
full = f"{message}\n{details}"
|
|
129
|
+
else:
|
|
130
|
+
full = message
|
|
131
|
+
super().__init__(full)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class NetworkError(RevolutXError):
|
|
135
|
+
"""Raised when a network-level error prevents the request from completing.
|
|
136
|
+
|
|
137
|
+
This wraps connection timeouts, DNS resolution failures, refused
|
|
138
|
+
connections, and other transport-layer errors.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
original_error: The underlying :class:`requests.RequestException`
|
|
142
|
+
(or similar) that caused this error.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, message: str, *, original_error: Exception | None = None) -> None:
|
|
146
|
+
self.original_error = original_error
|
|
147
|
+
super().__init__(message)
|