cexy 0.1.0.dev1__tar.gz
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.
- cexy-0.1.0.dev1/.gitignore +14 -0
- cexy-0.1.0.dev1/CHANGELOG.md +33 -0
- cexy-0.1.0.dev1/LICENSE +21 -0
- cexy-0.1.0.dev1/PKG-INFO +331 -0
- cexy-0.1.0.dev1/README.md +295 -0
- cexy-0.1.0.dev1/SECURITY.md +18 -0
- cexy-0.1.0.dev1/cexy/__init__.py +56 -0
- cexy-0.1.0.dev1/cexy/_async/__init__.py +0 -0
- cexy-0.1.0.dev1/cexy/_async/client.py +144 -0
- cexy-0.1.0.dev1/cexy/_async/pagination.py +58 -0
- cexy-0.1.0.dev1/cexy/_async/resources.py +478 -0
- cexy-0.1.0.dev1/cexy/_async/transport.py +165 -0
- cexy-0.1.0.dev1/cexy/_common.py +108 -0
- cexy-0.1.0.dev1/cexy/_decimal.py +41 -0
- cexy-0.1.0.dev1/cexy/_enum.py +31 -0
- cexy-0.1.0.dev1/cexy/_generated/__init__.py +2 -0
- cexy-0.1.0.dev1/cexy/_generated/models.py +1478 -0
- cexy-0.1.0.dev1/cexy/_generated/operations.py +183 -0
- cexy-0.1.0.dev1/cexy/_opmap.py +15 -0
- cexy-0.1.0.dev1/cexy/_ratelimit.py +72 -0
- cexy-0.1.0.dev1/cexy/_retry.py +38 -0
- cexy-0.1.0.dev1/cexy/_sync/__init__.py +1 -0
- cexy-0.1.0.dev1/cexy/_sync/client.py +146 -0
- cexy-0.1.0.dev1/cexy/_sync/pagination.py +59 -0
- cexy-0.1.0.dev1/cexy/_sync/resources.py +478 -0
- cexy-0.1.0.dev1/cexy/_sync/transport.py +167 -0
- cexy-0.1.0.dev1/cexy/_version.py +1 -0
- cexy-0.1.0.dev1/cexy/auth.py +106 -0
- cexy-0.1.0.dev1/cexy/errors.py +241 -0
- cexy-0.1.0.dev1/cexy/models.py +6 -0
- cexy-0.1.0.dev1/cexy/py.typed +0 -0
- cexy-0.1.0.dev1/cexy/ws.py +665 -0
- cexy-0.1.0.dev1/pyproject.toml +89 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The SDK stays at 0.x until API
|
|
4
|
+
request signing (HMAC) ships; see "Versioning" in README.md.
|
|
5
|
+
|
|
6
|
+
## 0.1.0.dev1 (unreleased)
|
|
7
|
+
|
|
8
|
+
First published pre-release. 0.1.0.dev0 was tagged but never reached PyPI: the pinned publish
|
|
9
|
+
action (v1.12.4, twine 6.1.0) rejected wheel metadata version 2.5. The publish action is now
|
|
10
|
+
v1.14.2 (twine 7.0.0), and the build job runs `twine check --strict` with the same twine.
|
|
11
|
+
|
|
12
|
+
## 0.1.0.dev0 (not published)
|
|
13
|
+
|
|
14
|
+
Built from the `cexy-api-spec` snapshot (implementation notes stripped from descriptions) `spec/openapi.sdk.json` (public spec `info.version`
|
|
15
|
+
1.0.0, 40 allowlisted operations).
|
|
16
|
+
|
|
17
|
+
- Sync `Client` and asyncio `AsyncClient` covering all 40 SDK operations: public market
|
|
18
|
+
data, account, exports, wallet reads, trading and liquidity pools.
|
|
19
|
+
- Typed pydantic v2 models generated from the spec; every amount is a `decimal.Decimal`,
|
|
20
|
+
requests send amounts as strings and reject floats.
|
|
21
|
+
- API-key header authentication behind an `Authenticator` extension point (for HMAC
|
|
22
|
+
signing later); secrets redacted from reprs, logs and exceptions.
|
|
23
|
+
- Error hierarchy from `errors.yaml` (provisional), unknown codes tolerated.
|
|
24
|
+
- Retries with exponential backoff and jitter and `Retry-After` handling. Order safety
|
|
25
|
+
rests on `client_order_id` plus a by-client-id lookup (the server ignores
|
|
26
|
+
`Idempotency-Key` on order endpoints). A cancel retried into `INVALID_STATE` returns the
|
|
27
|
+
order's current state. Pool join/exit send an automatic `Idempotency-Key`.
|
|
28
|
+
- Client-side token-bucket rate limiter (100/min without a key, 300/min with one) adapting
|
|
29
|
+
to `X-RateLimit-*` (`X-RateLimit-Reset` in seconds).
|
|
30
|
+
- Cursor pagination with `auto_paging_iter()`.
|
|
31
|
+
- WebSocket client (`cexy.ws`) with id-correlated acknowledgements (auth waits for
|
|
32
|
+
`authenticated`), heartbeat, reconnect/resubscribe and an `OrderBook`
|
|
33
|
+
helper that follows the snapshot/sequence rules.
|
cexy-0.1.0.dev1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CEXY.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cexy-0.1.0.dev1/PKG-INFO
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cexy
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Python SDK for the CEXY.io exchange REST and WebSocket API
|
|
5
|
+
Project-URL: Homepage, https://cexy.io
|
|
6
|
+
Project-URL: Source, https://github.com/cexyio/cexy-python
|
|
7
|
+
Author: CEXY.io
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx<1,>=0.25
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
24
|
+
Requires-Dist: websockets<17,>=13
|
|
25
|
+
Provides-Extra: codegen
|
|
26
|
+
Requires-Dist: datamodel-code-generator==0.83.0; extra == 'codegen'
|
|
27
|
+
Requires-Dist: ruff==0.16.9; extra == 'codegen'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff==0.16.9; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# cexy: Python SDK for CEXY.io
|
|
38
|
+
|
|
39
|
+
Typed Python client for the [CEXY.io](https://cexy.io) exchange REST and WebSocket API.
|
|
40
|
+
|
|
41
|
+
- Sync (`cexy.Client`) and asyncio (`cexy.AsyncClient`) clients
|
|
42
|
+
- Pydantic v2 models generated from the public OpenAPI spec; every amount is a `Decimal`
|
|
43
|
+
- Safe retries (backoff, `Retry-After`, `client_order_id` for orders, idempotency keys for pools)
|
|
44
|
+
- Client-side rate limiting, cursor pagination
|
|
45
|
+
- WebSocket client with heartbeat, reconnect and a self-syncing order book
|
|
46
|
+
|
|
47
|
+
> Status: **0.1.0.dev1, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install cexy # Python 3.9+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quickstart: public market data
|
|
56
|
+
|
|
57
|
+
No API key is needed for market data.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from cexy import Client
|
|
61
|
+
|
|
62
|
+
with Client() as client:
|
|
63
|
+
print(client.time().iso)
|
|
64
|
+
for market in client.markets.list():
|
|
65
|
+
print(market.symbol, market.last_price, market.status)
|
|
66
|
+
|
|
67
|
+
book = client.markets.orderbook("BTC/USDT", depth=10)
|
|
68
|
+
best_bid_price, best_bid_qty = book.bids[0] # Decimal, Decimal
|
|
69
|
+
candles = client.markets.candles("BTC/USDT", "1h", limit=24)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Also available: `client.markets.get(symbol)`, `client.markets.trades(symbol)`,
|
|
73
|
+
`client.assets.list()/get()`, `client.networks.list()`, `client.fees.list()`,
|
|
74
|
+
`client.config()`, `client.pools.list()/get()`.
|
|
75
|
+
|
|
76
|
+
## Quickstart: your account
|
|
77
|
+
|
|
78
|
+
Create an API key in your account settings. Keep the secret out of source code:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export CEXY_API_KEY=ak_your_key_here
|
|
82
|
+
export CEXY_API_SECRET=your_secret_here
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import os
|
|
87
|
+
from decimal import Decimal
|
|
88
|
+
from cexy import Client
|
|
89
|
+
|
|
90
|
+
client = Client(api_key=os.environ["CEXY_API_KEY"], api_secret=os.environ["CEXY_API_SECRET"])
|
|
91
|
+
|
|
92
|
+
for balance in client.account.balances():
|
|
93
|
+
print(balance.asset, balance.available)
|
|
94
|
+
|
|
95
|
+
# Places a REAL order (needs the `trade` scope). Amounts: Decimal or str, never float.
|
|
96
|
+
placed = client.trading.place_order(
|
|
97
|
+
"BTC/USDT", "buy", "limit", quantity=Decimal("0.001"), price="30000", time_in_force="post_only"
|
|
98
|
+
)
|
|
99
|
+
client.trading.cancel_order(placed.order.id)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Both `api_key` and `api_secret` are required together; passing only one raises
|
|
103
|
+
`ValueError` immediately.
|
|
104
|
+
|
|
105
|
+
| Resource | Methods |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `client.account` | `balances()`, `balance(asset)`, `ledger()`, `notifications()`, `sub_accounts()`, `api_keys()` |
|
|
108
|
+
| `client.wallet` | `deposits()`, `deposit(id)`, `withdrawals()`, `withdrawal(id)`, `withdrawal_addresses()`, `deposit_address(asset, network)` |
|
|
109
|
+
| `client.trading` | `open_orders()`, `order(id)`, `order_by_client_id(id)`, `order_history()`, `trades()`, `place_order(...)`, `cancel_order(id)`, `cancel_all(symbol=...)` |
|
|
110
|
+
| `client.exports` | `deposits()`, `ledger()`, `orders()`, `trades()`, `withdrawals()` (CSV bytes) |
|
|
111
|
+
| `client.pools` | `join(symbol, base_amount=, quote_amount=)`, `exit(symbol, shares=)` |
|
|
112
|
+
|
|
113
|
+
Note: `wallet.deposit_address()` **creates** a deposit address the first time it is called
|
|
114
|
+
for an asset/network pair, then returns the same address on later calls.
|
|
115
|
+
|
|
116
|
+
`cancel_all` requires the `symbol` keyword; pass `symbol=None` explicitly to cancel orders
|
|
117
|
+
in every market. The server allows 30 cancel-all calls per minute per account.
|
|
118
|
+
|
|
119
|
+
The async client has the same methods:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import asyncio
|
|
123
|
+
from cexy import AsyncClient
|
|
124
|
+
|
|
125
|
+
async def main() -> None:
|
|
126
|
+
async with AsyncClient() as client:
|
|
127
|
+
markets = await client.markets.list()
|
|
128
|
+
|
|
129
|
+
asyncio.run(main())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Amounts
|
|
133
|
+
|
|
134
|
+
The API sends every amount as a decimal string. The SDK parses them into
|
|
135
|
+
`decimal.Decimal`, and sends amounts as strings. Passing a `float` raises `TypeError`
|
|
136
|
+
before anything is sent, because a float cannot represent most decimal amounts exactly.
|
|
137
|
+
|
|
138
|
+
## Errors
|
|
139
|
+
|
|
140
|
+
Every API error raises `cexy.CexyApiError` (or a subclass) with `code`, `message`,
|
|
141
|
+
`details`, `fields`, `request_id`, `retryable` and `status`. Branch on `code`, never on the
|
|
142
|
+
message.
|
|
143
|
+
|
|
144
|
+
| Exception | When |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `ValidationError` | 400, e.g. `VALIDATION_FAILED`, `PRECISION_EXCEEDED` (see `fields`) |
|
|
147
|
+
| `AuthenticationError` | 401, e.g. `UNAUTHENTICATED`, `INVALID_CREDENTIALS` |
|
|
148
|
+
| `ForbiddenError` | 403: `FORBIDDEN` (key lacks a scope), `API_KEY_NOT_ALLOWED` (session-only endpoint) |
|
|
149
|
+
| `NotFoundError` | 404 |
|
|
150
|
+
| `ConflictError` | 409, e.g. `ALREADY_EXISTS`, `IDEMPOTENCY_KEY_CONFLICT` |
|
|
151
|
+
| `UnprocessableError` | 422, e.g. `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE` |
|
|
152
|
+
| `RateLimitError` | 429; `.retry_after` gives the server's requested wait |
|
|
153
|
+
| `ServerError` | 5xx, e.g. `UNDER_MAINTENANCE`, `ENGINE_OVERLOADED` |
|
|
154
|
+
|
|
155
|
+
An error code this SDK version does not know is raised as the base `CexyApiError`: it never
|
|
156
|
+
crashes the client. Network failures after retries raise `CexyConnectionError`. Calling a
|
|
157
|
+
private endpoint on a client without keys raises `MissingCredentialsError` locally.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from cexy import UnprocessableError
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
client.trading.place_order("BTC/USDT", "buy", "market", quote_quantity="50")
|
|
164
|
+
except UnprocessableError as err:
|
|
165
|
+
if err.code == "INSUFFICIENT_FUNDS":
|
|
166
|
+
print("need", err.details.get("required"))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Retries and idempotency
|
|
170
|
+
|
|
171
|
+
`Client(max_retries=3, timeout=10)` retries with exponential backoff and full jitter:
|
|
172
|
+
|
|
173
|
+
- **GET** requests retry on network errors, 429, 502/503/504 and any error with
|
|
174
|
+
`retryable: true`.
|
|
175
|
+
- **429** waits for the larger of the `Retry-After` header and `details.retry_after_seconds`.
|
|
176
|
+
- **Orders: safety rests on `client_order_id`, not on `Idempotency-Key`.** The server
|
|
177
|
+
does not honour `Idempotency-Key` on `POST /trading/orders`, `DELETE /trading/orders/{id}`
|
|
178
|
+
or cancel-all, so the SDK does not send one there.
|
|
179
|
+
- **`place_order`** always sends a `client_order_id` (a UUID unless you pass one). It is
|
|
180
|
+
unique per account, and a repeat is refused before any funds move. After an ambiguous
|
|
181
|
+
failure (timeout, dropped connection, 500/502/504) the SDK first looks the order up with
|
|
182
|
+
`trading.order_by_client_id()`. If the order exists, it is returned. Otherwise the order
|
|
183
|
+
is resent with the same `client_order_id`. If a retry is refused as a duplicate
|
|
184
|
+
(`ALREADY_EXISTS`), the existing order is fetched and returned.
|
|
185
|
+
- **`cancel_order`** is naturally repeatable and is retried. If a retry is refused with
|
|
186
|
+
`INVALID_STATE` (an earlier attempt already cancelled the order, or it filled
|
|
187
|
+
meanwhile), the SDK fetches and returns the order's current state, so check `status`.
|
|
188
|
+
- **`cancel_all`** is retried: repeating it only cancels whatever is still open.
|
|
189
|
+
- **Pool join/exit** carry an automatically generated `Idempotency-Key` header, reused on
|
|
190
|
+
every retry of the same call. A `409 CONCURRENT_MODIFICATION` (the same key still in
|
|
191
|
+
flight) is retried with the same key. Pass `idempotency_key=` to use your own.
|
|
192
|
+
|
|
193
|
+
## Pagination
|
|
194
|
+
|
|
195
|
+
History endpoints return a `Page` with `items`, `next_cursor` and `has_more`:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
page = client.trading.order_history(symbol="BTC/USDT", limit=100)
|
|
199
|
+
for order in page.auto_paging_iter(): # fetches later pages lazily
|
|
200
|
+
print(order.id, order.status)
|
|
201
|
+
|
|
202
|
+
# or one page at a time
|
|
203
|
+
next_page = page.next_page() # None on the last page
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
With `AsyncClient`: `async for order in page.auto_paging_iter(): ...`.
|
|
207
|
+
|
|
208
|
+
## Rate limits
|
|
209
|
+
|
|
210
|
+
Server-side limits:
|
|
211
|
+
|
|
212
|
+
- **Anonymous (public) requests:** about **120 requests/minute per IP address**.
|
|
213
|
+
- **API-key requests:** each key has its own limit, about **600 requests/minute per key**
|
|
214
|
+
(configurable server-side; this applies after the upcoming key release).
|
|
215
|
+
|
|
216
|
+
The client keeps itself below these with a token bucket: **100/min by default without a
|
|
217
|
+
key, 300/min with a key**. Override it with `Client(rate_limit_per_minute=...)`. When
|
|
218
|
+
responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
|
|
219
|
+
(the number of **seconds** until the window resets), the client slows down to match them. A 429
|
|
220
|
+
is retried after the server's `Retry-After`.
|
|
221
|
+
|
|
222
|
+
## WebSocket
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import asyncio
|
|
226
|
+
from cexy.ws import WebSocketClient, BOOK_STALE, RECONNECTED
|
|
227
|
+
|
|
228
|
+
async def main() -> None:
|
|
229
|
+
async with WebSocketClient() as ws: # wss://api.cexy.io/api/v1/ws
|
|
230
|
+
await ws.subscribe("ticker:BTC/USDT", "trades:BTC/USDT")
|
|
231
|
+
book = await ws.order_book("BTC/USDT")
|
|
232
|
+
async for event in ws:
|
|
233
|
+
if event.type == "orderbook.update":
|
|
234
|
+
print(book.sequence, book.best_bid(), book.best_ask())
|
|
235
|
+
elif event.type == BOOK_STALE:
|
|
236
|
+
print("gap detected; book is stale until the next update")
|
|
237
|
+
|
|
238
|
+
asyncio.run(main())
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The client handles the protocol rules for you:
|
|
242
|
+
|
|
243
|
+
- It sends `{"op":"ping"}` every 30 s (required: the server closes idle connections), accepts
|
|
244
|
+
the server's unsolicited `pong` frames, and reconnects if nothing arrives for 75 s.
|
|
245
|
+
- It reconnects with exponential backoff and jitter, then re-authenticates, re-subscribes
|
|
246
|
+
and emits a `reconnected` event.
|
|
247
|
+
- It refuses more than 100 subscriptions locally (reported in `SubscribeResult.refused`)
|
|
248
|
+
and keeps its own send rate under 200 messages per minute.
|
|
249
|
+
- Every request carries an `id` and waits for the server's acknowledgement with that id
|
|
250
|
+
(`authenticated`, `subscribed`, `unsubscribed`, `pong`). An `error` acknowledgement, or none
|
|
251
|
+
within `request_timeout`, raises `cexy.ws.WebSocketError`. `await ws.ping()` returns the
|
|
252
|
+
round-trip time.
|
|
253
|
+
- Unknown event types are ignored; an unknown `protocol_version` logs one warning.
|
|
254
|
+
- You can register callbacks with `ws.on("trade.new", handler)` instead of iterating.
|
|
255
|
+
|
|
256
|
+
**Order-book rules** (implemented by `ws.order_book()`):
|
|
257
|
+
|
|
258
|
+
1. Subscribe to `orderbook:{symbol}` first, then take a REST snapshot at sequence `S`.
|
|
259
|
+
2. Drop updates with `sequence <= S`.
|
|
260
|
+
3. Every `orderbook.update` carries the complete top 50 of both sides (`"full": true`) and
|
|
261
|
+
replaces the book outright; there are no deltas. REST levels deeper than 50 are never
|
|
262
|
+
merged into the live book.
|
|
263
|
+
4. A sequence gap marks the book `stale` (and emits `book_stale`) until the next update heals
|
|
264
|
+
it. There is no forced resync.
|
|
265
|
+
5. After every reconnect a fresh snapshot is taken: sequences reset when the server
|
|
266
|
+
restarts and are never compared across connections.
|
|
267
|
+
6. A `CONCURRENT_MODIFICATION` error frame (messages were dropped) triggers a fresh snapshot
|
|
268
|
+
of every book and a `resync` event, so you can refresh other state too.
|
|
269
|
+
|
|
270
|
+
**Private channels** (`orders`, `balances`, `deposits`, `withdrawals`, `account`) need
|
|
271
|
+
`await ws.auth(token)` with a session access token; it returns once the server sends
|
|
272
|
+
`authenticated`. **API-key authentication on the
|
|
273
|
+
WebSocket is not available yet**; with an API key, poll the REST endpoints for private
|
|
274
|
+
state. If the session is revoked, the `account` channel delivers `session.revoked` and the
|
|
275
|
+
client emits `auth_lost`; the socket stays open for public channels.
|
|
276
|
+
|
|
277
|
+
## Security notes
|
|
278
|
+
|
|
279
|
+
- **API keys can never withdraw or transfer funds**, whatever their scopes.
|
|
280
|
+
- Use a `read`-only key unless you trade, and restrict keys to your IPs with `allowed_ips`.
|
|
281
|
+
- Keys are sent only as headers, only to endpoints that need them, and never in a URL.
|
|
282
|
+
Never put secrets in URLs or source code.
|
|
283
|
+
- The SDK redacts keys from `repr()`, logs and exception messages, and never sends an
|
|
284
|
+
`Authorization` header. If a server response echoes the key or secret, it is replaced
|
|
285
|
+
with `***` in the exception's message, details, fields and request id.
|
|
286
|
+
- Connections use `https://` and `wss://` only. `allow_insecure=True` permits `http://` or
|
|
287
|
+
`ws://` for a loopback host (localhost, 127.0.0.1, ::1) only, for local testing.
|
|
288
|
+
- Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
|
|
289
|
+
|
|
290
|
+
## Authentication extension point
|
|
291
|
+
|
|
292
|
+
Credentials are applied by an `cexy.auth.Authenticator` (`apply(method, url, headers, body)`).
|
|
293
|
+
Today's implementation, `HeaderKeyAuth`, sends the static key headers. HMAC request signing
|
|
294
|
+
is planned before 1.0 and will plug in as another authenticator without changing the
|
|
295
|
+
resource API.
|
|
296
|
+
|
|
297
|
+
## Versioning
|
|
298
|
+
|
|
299
|
+
The SDK stays at **0.x** until API request signing ships, then moves to 1.x, which targets
|
|
300
|
+
`/api/v1`. Additive API changes produce minor releases. Each release notes the spec snapshot
|
|
301
|
+
it was built from in [CHANGELOG.md](CHANGELOG.md).
|
|
302
|
+
|
|
303
|
+
## Development
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
python -m venv .venv && . .venv/bin/activate
|
|
307
|
+
pip install -e ".[dev,codegen]"
|
|
308
|
+
pytest # offline: mocked HTTP and a local fake WebSocket server
|
|
309
|
+
CEXY_LIVE_TESTS=1 pytest -m live # opt-in: 3 public, unauthenticated GETs to api.cexy.io
|
|
310
|
+
ruff check . && ruff format --check . && mypy
|
|
311
|
+
scripts/generate.sh # regenerate models (cexy/_generated) and sync code (cexy/_sync)
|
|
312
|
+
scripts/sync_spec.sh ../cexy-api-spec # refresh the vendored spec and conformance fixtures
|
|
313
|
+
python tools/scan_internal.py . # extra local patterns: $CEXY_SCAN_PATTERNS_FILE
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`tools/scan_internal.py` is a verbatim copy from cexy-api-spec. Infrastructure-specific
|
|
317
|
+
patterns are not in the repository: they are read from the file named by
|
|
318
|
+
`CEXY_SCAN_PATTERNS_FILE` (default `~/.config/cexy/scan-patterns.txt`), which CI writes from
|
|
319
|
+
a repository secret.
|
|
320
|
+
|
|
321
|
+
- `spec/openapi.sdk.json` and `tests/fixtures/conformance/` are vendored from
|
|
322
|
+
[cexy-api-spec](https://github.com/cexyio/cexy-api-spec); CI checks they are in sync.
|
|
323
|
+
- Type checking: mypy runs with `python_version = "3.10"` (current mypy no longer accepts
|
|
324
|
+
3.9 as a target), while the CI test matrix still runs the suite on Python 3.9 to 3.13,
|
|
325
|
+
so 3.9 runtime compatibility is covered by the tests.
|
|
326
|
+
- `cexy/_generated/` is generated with datamodel-code-generator; `cexy/_sync/` is generated
|
|
327
|
+
from `cexy/_async/`. Edit the sources, then run `scripts/generate.sh`; CI fails on drift.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT, see [LICENSE](LICENSE).
|