cexy 0.1.0.dev2__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.
Files changed (34) hide show
  1. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/CHANGELOG.md +12 -1
  2. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/PKG-INFO +5 -2
  3. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/README.md +4 -1
  4. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_async/transport.py +14 -1
  5. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_sync/transport.py +14 -1
  6. cexy-0.1.0.dev3/cexy/_version.py +1 -0
  7. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/pyproject.toml +1 -1
  8. cexy-0.1.0.dev2/cexy/_version.py +0 -1
  9. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/.gitignore +0 -0
  10. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/LICENSE +0 -0
  11. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/SECURITY.md +0 -0
  12. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/__init__.py +0 -0
  13. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_async/__init__.py +0 -0
  14. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_async/client.py +0 -0
  15. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_async/pagination.py +0 -0
  16. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_async/resources.py +0 -0
  17. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_common.py +0 -0
  18. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_decimal.py +0 -0
  19. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_enum.py +0 -0
  20. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_generated/__init__.py +0 -0
  21. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_generated/models.py +0 -0
  22. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_generated/operations.py +0 -0
  23. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_opmap.py +0 -0
  24. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_ratelimit.py +0 -0
  25. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_retry.py +0 -0
  26. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_sync/__init__.py +0 -0
  27. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_sync/client.py +0 -0
  28. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_sync/pagination.py +0 -0
  29. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/_sync/resources.py +0 -0
  30. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/auth.py +0 -0
  31. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/errors.py +0 -0
  32. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/models.py +0 -0
  33. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/py.typed +0 -0
  34. {cexy-0.1.0.dev2 → cexy-0.1.0.dev3}/cexy/ws.py +0 -0
@@ -3,7 +3,18 @@
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.dev2 (unreleased)
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)
7
18
 
8
19
  - `JurisdictionBlockedError` (a `ForbiddenError` subclass) for `JURISDICTION_BLOCKED` / HTTP 451.
9
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).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cexy
3
- Version: 0.1.0.dev2
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,7 +44,7 @@ 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.dev2, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
47
+ > Status: **0.1.0.dev3, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
48
48
  > Pre-releases need `--pre`: `pip install --pre cexy`.
49
49
 
50
50
  ## Install
@@ -285,6 +285,9 @@ client emits `auth_lost`; the socket stays open for public channels.
285
285
  - The SDK redacts keys from `repr()`, logs and exception messages, and never sends an
286
286
  `Authorization` header. If a server response echoes the key or secret, it is replaced
287
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.
288
291
  - Connections use `https://` and `wss://` only. `allow_insecure=True` permits `http://` or
289
292
  `ws://` for a loopback host (localhost, 127.0.0.1, ::1) only, for local testing.
290
293
  - Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
@@ -8,7 +8,7 @@ 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.dev2, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
11
+ > Status: **0.1.0.dev3, pre-release.** The API may change before 1.0 (see [Versioning](#versioning)).
12
12
  > Pre-releases need `--pre`: `pip install --pre cexy`.
13
13
 
14
14
  ## Install
@@ -249,6 +249,9 @@ client emits `auth_lost`; the socket stays open for public channels.
249
249
  - The SDK redacts keys from `repr()`, logs and exception messages, and never sends an
250
250
  `Authorization` header. If a server response echoes the key or secret, it is replaced
251
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.
252
255
  - Connections use `https://` and `wss://` only. `allow_insecure=True` permits `http://` or
253
256
  `ws://` for a loopback host (localhost, 127.0.0.1, ::1) only, for local testing.
254
257
  - Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
@@ -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
- resp = await self.http.request(op.method, url, params=params, content=content, headers=send_headers)
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
- resp = self.http.request(op.method, url, params=params, content=content, headers=send_headers)
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"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cexy"
7
- version = "0.1.0.dev2"
7
+ version = "0.1.0.dev3"
8
8
  description = "Python SDK for the CEXY.io exchange REST and WebSocket API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.1.0.dev2"
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