cexy 0.1.0.dev1__tar.gz → 0.1.0.dev3__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 → cexy-0.1.0.dev3}/CHANGELOG.md +18 -1
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/PKG-INFO +8 -3
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/README.md +7 -2
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/__init__.py +2 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_async/transport.py +14 -1
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_sync/transport.py +14 -1
- cexy-0.1.0.dev3/cexy/_version.py +1 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/errors.py +9 -2
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/pyproject.toml +1 -1
- cexy-0.1.0.dev1/cexy/_version.py +0 -1
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/.gitignore +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/LICENSE +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/SECURITY.md +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_async/__init__.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_async/client.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_async/pagination.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_async/resources.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_common.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_decimal.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_enum.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_generated/__init__.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_generated/models.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_generated/operations.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_opmap.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_ratelimit.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_retry.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_sync/__init__.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_sync/client.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_sync/pagination.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/_sync/resources.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/auth.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/models.py +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/py.typed +0 -0
- {cexy-0.1.0.dev1 → cexy-0.1.0.dev3}/cexy/ws.py +0 -0
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
All notable changes to this project are documented here. The SDK stays at 0.x until API
|
|
4
4
|
request signing (HMAC) ships; see "Versioning" in README.md.
|
|
5
5
|
|
|
6
|
-
## 0.1.0.
|
|
6
|
+
## 0.1.0.dev3 (unreleased)
|
|
7
|
+
|
|
8
|
+
### Security
|
|
9
|
+
- **Redirects are never followed**, including on a caller-supplied `http_client` created with
|
|
10
|
+
`follow_redirects=True`. Earlier versions set `follow_redirects=False` only on the client they
|
|
11
|
+
created themselves, so a caller's following client re-sent `X-API-Key` and `X-API-Secret` to a
|
|
12
|
+
redirect target (httpx strips only `Authorization`), and a 307/308 re-posted an order. A 3xx
|
|
13
|
+
was also treated as success. Every request now passes `follow_redirects=False`, and a 3xx
|
|
14
|
+
raises `CexyApiError` with code `UNEXPECTED_REDIRECT`, which is not retried. Upgrade from
|
|
15
|
+
0.1.0.dev2 or earlier if you pass your own `http_client`.
|
|
16
|
+
|
|
17
|
+
## 0.1.0.dev2 (2026-09-27)
|
|
18
|
+
|
|
19
|
+
- `JurisdictionBlockedError` (a `ForbiddenError` subclass) for `JURISDICTION_BLOCKED` / HTTP 451.
|
|
20
|
+
- README: pre-releases install with `pip install --pre cexy` (the PyPI page for 0.1.0.dev1 still says `pip install cexy`, which does not install a pre-release).
|
|
21
|
+
- `spec/errors.yaml` is now generated upstream from the spec's `ErrorCode` (same 44 codes).
|
|
22
|
+
|
|
23
|
+
## 0.1.0.dev1 (2026-09-27)
|
|
7
24
|
|
|
8
25
|
First published pre-release. 0.1.0.dev0 was tagged but never reached PyPI: the pinned publish
|
|
9
26
|
action (v1.12.4, twine 6.1.0) rejected wheel metadata version 2.5. The publish action is now
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cexy
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: Python SDK for the CEXY.io exchange REST and WebSocket API
|
|
5
5
|
Project-URL: Homepage, https://cexy.io
|
|
6
6
|
Project-URL: Source, https://github.com/cexyio/cexy-python
|
|
@@ -44,12 +44,13 @@ Typed Python client for the [CEXY.io](https://cexy.io) exchange REST and WebSock
|
|
|
44
44
|
- Client-side rate limiting, cursor pagination
|
|
45
45
|
- WebSocket client with heartbeat, reconnect and a self-syncing order book
|
|
46
46
|
|
|
47
|
-
> Status: **0.1.0.
|
|
47
|
+
> Status: **0.1.0.dev3, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
|
|
48
|
+
> Pre-releases need `--pre`: `pip install --pre cexy`.
|
|
48
49
|
|
|
49
50
|
## Install
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
|
-
pip install cexy
|
|
53
|
+
pip install --pre cexy # Python 3.9+ (--pre while releases are 0.1.0.devN)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
## Quickstart: public market data
|
|
@@ -146,6 +147,7 @@ message.
|
|
|
146
147
|
| `ValidationError` | 400, e.g. `VALIDATION_FAILED`, `PRECISION_EXCEEDED` (see `fields`) |
|
|
147
148
|
| `AuthenticationError` | 401, e.g. `UNAUTHENTICATED`, `INVALID_CREDENTIALS` |
|
|
148
149
|
| `ForbiddenError` | 403: `FORBIDDEN` (key lacks a scope), `API_KEY_NOT_ALLOWED` (session-only endpoint) |
|
|
150
|
+
| `JurisdictionBlockedError` | 451: `JURISDICTION_BLOCKED` (not available in the caller's jurisdiction); a `ForbiddenError` subclass |
|
|
149
151
|
| `NotFoundError` | 404 |
|
|
150
152
|
| `ConflictError` | 409, e.g. `ALREADY_EXISTS`, `IDEMPOTENCY_KEY_CONFLICT` |
|
|
151
153
|
| `UnprocessableError` | 422, e.g. `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE` |
|
|
@@ -283,6 +285,9 @@ client emits `auth_lost`; the socket stays open for public channels.
|
|
|
283
285
|
- The SDK redacts keys from `repr()`, logs and exception messages, and never sends an
|
|
284
286
|
`Authorization` header. If a server response echoes the key or secret, it is replaced
|
|
285
287
|
with `***` in the exception's message, details, fields and request id.
|
|
288
|
+
- The SDK **never follows HTTP redirects**, even on an `http_client` you created with
|
|
289
|
+
`follow_redirects=True`. A 3xx answer raises `CexyApiError` with code `UNEXPECTED_REDIRECT`
|
|
290
|
+
(not retried), so keys are never re-sent to another host and an order is never re-posted.
|
|
286
291
|
- Connections use `https://` and `wss://` only. `allow_insecure=True` permits `http://` or
|
|
287
292
|
`ws://` for a loopback host (localhost, 127.0.0.1, ::1) only, for local testing.
|
|
288
293
|
- Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
|
|
@@ -8,12 +8,13 @@ Typed Python client for the [CEXY.io](https://cexy.io) exchange REST and WebSock
|
|
|
8
8
|
- Client-side rate limiting, cursor pagination
|
|
9
9
|
- WebSocket client with heartbeat, reconnect and a self-syncing order book
|
|
10
10
|
|
|
11
|
-
> Status: **0.1.0.
|
|
11
|
+
> Status: **0.1.0.dev3, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
|
|
12
|
+
> Pre-releases need `--pre`: `pip install --pre cexy`.
|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
pip install cexy
|
|
17
|
+
pip install --pre cexy # Python 3.9+ (--pre while releases are 0.1.0.devN)
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
## Quickstart: public market data
|
|
@@ -110,6 +111,7 @@ message.
|
|
|
110
111
|
| `ValidationError` | 400, e.g. `VALIDATION_FAILED`, `PRECISION_EXCEEDED` (see `fields`) |
|
|
111
112
|
| `AuthenticationError` | 401, e.g. `UNAUTHENTICATED`, `INVALID_CREDENTIALS` |
|
|
112
113
|
| `ForbiddenError` | 403: `FORBIDDEN` (key lacks a scope), `API_KEY_NOT_ALLOWED` (session-only endpoint) |
|
|
114
|
+
| `JurisdictionBlockedError` | 451: `JURISDICTION_BLOCKED` (not available in the caller's jurisdiction); a `ForbiddenError` subclass |
|
|
113
115
|
| `NotFoundError` | 404 |
|
|
114
116
|
| `ConflictError` | 409, e.g. `ALREADY_EXISTS`, `IDEMPOTENCY_KEY_CONFLICT` |
|
|
115
117
|
| `UnprocessableError` | 422, e.g. `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE` |
|
|
@@ -247,6 +249,9 @@ client emits `auth_lost`; the socket stays open for public channels.
|
|
|
247
249
|
- The SDK redacts keys from `repr()`, logs and exception messages, and never sends an
|
|
248
250
|
`Authorization` header. If a server response echoes the key or secret, it is replaced
|
|
249
251
|
with `***` in the exception's message, details, fields and request id.
|
|
252
|
+
- The SDK **never follows HTTP redirects**, even on an `http_client` you created with
|
|
253
|
+
`follow_redirects=True`. A 3xx answer raises `CexyApiError` with code `UNEXPECTED_REDIRECT`
|
|
254
|
+
(not retried), so keys are never re-sent to another host and an order is never re-posted.
|
|
250
255
|
- Connections use `https://` and `wss://` only. `allow_insecure=True` permits `http://` or
|
|
251
256
|
`ws://` for a loopback host (localhost, 127.0.0.1, ::1) only, for local testing.
|
|
252
257
|
- Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
|
|
@@ -22,6 +22,7 @@ from cexy.errors import (
|
|
|
22
22
|
ConfigurationError,
|
|
23
23
|
ConflictError,
|
|
24
24
|
ForbiddenError,
|
|
25
|
+
JurisdictionBlockedError,
|
|
25
26
|
MissingCredentialsError,
|
|
26
27
|
NotFoundError,
|
|
27
28
|
RateLimitError,
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"ConfigurationError",
|
|
45
46
|
"ConflictError",
|
|
46
47
|
"ForbiddenError",
|
|
48
|
+
"JurisdictionBlockedError",
|
|
47
49
|
"HeaderKeyAuth",
|
|
48
50
|
"MissingCredentialsError",
|
|
49
51
|
"NotFoundError",
|
|
@@ -114,7 +114,12 @@ class AsyncTransport:
|
|
|
114
114
|
if op.auth == "api_key" and self.auth is not None:
|
|
115
115
|
self.auth.apply(op.method, url, send_headers, content)
|
|
116
116
|
try:
|
|
117
|
-
|
|
117
|
+
# Never follow redirects, even on a caller-supplied client created with
|
|
118
|
+
# follow_redirects=True: httpx would re-send X-API-Key/X-API-Secret to the target
|
|
119
|
+
# (it strips only Authorization), and a 307/308 would re-POST an order.
|
|
120
|
+
resp = await self.http.request(
|
|
121
|
+
op.method, url, params=params, content=content, headers=send_headers, follow_redirects=False
|
|
122
|
+
)
|
|
118
123
|
except httpx.TransportError as exc:
|
|
119
124
|
ambiguous = not isinstance(exc, _NOT_SENT)
|
|
120
125
|
reason = redact_text(f"{type(exc).__name__}: {exc}", self._secrets())
|
|
@@ -132,6 +137,14 @@ class AsyncTransport:
|
|
|
132
137
|
resp_headers = lower_headers(resp.headers)
|
|
133
138
|
self.limiter.update_from_headers(resp_headers)
|
|
134
139
|
logger.debug("cexy: %s %s -> %d (attempt %d)", op.method, op.path, resp.status_code, attempt)
|
|
140
|
+
if 300 <= resp.status_code < 400:
|
|
141
|
+
raise CexyApiError(
|
|
142
|
+
"UNEXPECTED_REDIRECT",
|
|
143
|
+
f"{op.method} {op.path}: the server answered with a redirect (HTTP {resp.status_code}); "
|
|
144
|
+
"the SDK does not follow redirects. Check base_url.",
|
|
145
|
+
status=resp.status_code,
|
|
146
|
+
retryable=False,
|
|
147
|
+
)
|
|
135
148
|
if resp.status_code < 400:
|
|
136
149
|
if raw:
|
|
137
150
|
return resp.content
|
|
@@ -116,7 +116,12 @@ class SyncTransport:
|
|
|
116
116
|
if op.auth == "api_key" and self.auth is not None:
|
|
117
117
|
self.auth.apply(op.method, url, send_headers, content)
|
|
118
118
|
try:
|
|
119
|
-
|
|
119
|
+
# Never follow redirects, even on a caller-supplied client created with
|
|
120
|
+
# follow_redirects=True: httpx would re-send X-API-Key/X-API-Secret to the target
|
|
121
|
+
# (it strips only Authorization), and a 307/308 would re-POST an order.
|
|
122
|
+
resp = self.http.request(
|
|
123
|
+
op.method, url, params=params, content=content, headers=send_headers, follow_redirects=False
|
|
124
|
+
)
|
|
120
125
|
except httpx.TransportError as exc:
|
|
121
126
|
ambiguous = not isinstance(exc, _NOT_SENT)
|
|
122
127
|
reason = redact_text(f"{type(exc).__name__}: {exc}", self._secrets())
|
|
@@ -134,6 +139,14 @@ class SyncTransport:
|
|
|
134
139
|
resp_headers = lower_headers(resp.headers)
|
|
135
140
|
self.limiter.update_from_headers(resp_headers)
|
|
136
141
|
logger.debug("cexy: %s %s -> %d (attempt %d)", op.method, op.path, resp.status_code, attempt)
|
|
142
|
+
if 300 <= resp.status_code < 400:
|
|
143
|
+
raise CexyApiError(
|
|
144
|
+
"UNEXPECTED_REDIRECT",
|
|
145
|
+
f"{op.method} {op.path}: the server answered with a redirect (HTTP {resp.status_code}); "
|
|
146
|
+
"the SDK does not follow redirects. Check base_url.",
|
|
147
|
+
status=resp.status_code,
|
|
148
|
+
retryable=False,
|
|
149
|
+
)
|
|
137
150
|
if resp.status_code < 400:
|
|
138
151
|
if raw:
|
|
139
152
|
return resp.content
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0.dev3"
|
|
@@ -81,6 +81,13 @@ class ForbiddenError(CexyApiError):
|
|
|
81
81
|
(``API_KEY_NOT_ALLOWED``), or the account/region is restricted."""
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
class JurisdictionBlockedError(ForbiddenError):
|
|
85
|
+
"""451: the service is not available in the caller's jurisdiction (``JURISDICTION_BLOCKED``).
|
|
86
|
+
|
|
87
|
+
A subclass of :class:`ForbiddenError`, so existing ``except ForbiddenError`` handlers still
|
|
88
|
+
catch it; catch it directly to show a dedicated message. Retrying will not help."""
|
|
89
|
+
|
|
90
|
+
|
|
84
91
|
class ValidationError(CexyApiError):
|
|
85
92
|
"""400: the request failed validation. ``fields`` maps field names to problems."""
|
|
86
93
|
|
|
@@ -139,8 +146,8 @@ for _cls, _codes in {
|
|
|
139
146
|
"ACCOUNT_ON_HOLD",
|
|
140
147
|
"EMAIL_NOT_VERIFIED",
|
|
141
148
|
"REGION_BLOCKED",
|
|
142
|
-
"JURISDICTION_BLOCKED", # HTTP 451
|
|
143
149
|
),
|
|
150
|
+
JurisdictionBlockedError: ("JURISDICTION_BLOCKED",),
|
|
144
151
|
NotFoundError: ("NOT_FOUND",),
|
|
145
152
|
ConflictError: (
|
|
146
153
|
"ALREADY_EXISTS",
|
|
@@ -170,7 +177,7 @@ _BY_STATUS: Dict[int, Type[CexyApiError]] = {
|
|
|
170
177
|
400: ValidationError,
|
|
171
178
|
401: AuthenticationError,
|
|
172
179
|
403: ForbiddenError,
|
|
173
|
-
451:
|
|
180
|
+
451: JurisdictionBlockedError, # unavailable for legal reasons
|
|
174
181
|
404: NotFoundError,
|
|
175
182
|
409: ConflictError,
|
|
176
183
|
422: UnprocessableError,
|
cexy-0.1.0.dev1/cexy/_version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0.dev1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|