|
1 | 1 | # Migration |
2 | 2 |
|
| 3 | +## v2.3 → v2.4 |
| 4 | + |
| 5 | +v2.4 ships one user-visible breaking change — the V1 batch order |
| 6 | +return shape (#194) — plus several additive surfaces: new typed |
| 7 | +exceptions, passphrase-protected PEMs, opt-in HTTP/2, and per-request |
| 8 | +`extra_headers`. |
| 9 | + |
| 10 | +### Breaking: `batch_create` / `batch_cancel` return typed envelopes |
| 11 | + |
| 12 | +Per #194, `orders.batch_create` previously returned `list[Order]` and |
| 13 | +crashed with `ValidationError` on the first failed leg; |
| 14 | +`orders.batch_cancel` returned `None` and discarded the per-leg |
| 15 | +`reduced_by_fp` counts the server actually sent. Both now return |
| 16 | +typed envelopes matching the V2 family — pair legs by |
| 17 | +`client_order_id` and check `entry.error` per leg. |
| 18 | + |
| 19 | +```python |
| 20 | +# v2.3 |
| 21 | +orders: list[Order] = client.orders.batch_create(request=req) |
| 22 | +for order in orders: |
| 23 | + print(order.order_id, order.status) |
| 24 | + |
| 25 | +# v2.4 |
| 26 | +from kalshi.models import BatchCreateOrdersResponse |
| 27 | + |
| 28 | +resp: BatchCreateOrdersResponse = client.orders.batch_create(request=req) |
| 29 | +for entry in resp.orders: |
| 30 | + if entry.error is not None: |
| 31 | + logger.error("leg %s failed: %s", entry.client_order_id, entry.error) |
| 32 | + continue |
| 33 | + assert entry.order is not None |
| 34 | + print(entry.order.order_id, entry.order.status) |
| 35 | +``` |
| 36 | + |
| 37 | +`batch_cancel` follows the same shape: each `resp.orders[i]` exposes |
| 38 | +`order_id`, `reduced_by_fp` (the count of contracts that actually |
| 39 | +canceled — load-bearing for risk reconciliation), plus |
| 40 | +`order`/`error` for the affected leg. |
| 41 | + |
| 42 | +### New typed exceptions |
| 43 | + |
| 44 | +Per #201 / #204 / #226, three new subclasses of `KalshiError` join |
| 45 | +the existing hierarchy: |
| 46 | + |
| 47 | +- `KalshiConflictError` — HTTP **409** (e.g. duplicate |
| 48 | + `client_order_id`). |
| 49 | +- `KalshiTimeoutError` — `httpx.TimeoutException` at the request |
| 50 | + level. **The server may or may not have processed the request.** |
| 51 | + POST/DELETE still never retry on this; reconcile by querying with |
| 52 | + the original `client_order_id` before issuing a fresh attempt. |
| 53 | +- `KalshiPoolExhaustedError` — `httpx.PoolTimeout`. The request |
| 54 | + never reached the wire and IS safe to retry on POST/DELETE. |
| 55 | + |
| 56 | +422 now also routes to `KalshiValidationError` (was 400 only). |
| 57 | + |
| 58 | +```python |
| 59 | +from kalshi import ( |
| 60 | + KalshiConflictError, |
| 61 | + KalshiTimeoutError, |
| 62 | + KalshiPoolExhaustedError, |
| 63 | +) |
| 64 | + |
| 65 | +try: |
| 66 | + client.orders.create(request=req) |
| 67 | +except KalshiConflictError: |
| 68 | + existing = client.orders.get_by_client_order_id(req.client_order_id) |
| 69 | +except KalshiPoolExhaustedError: |
| 70 | + # never touched the wire — safe to retry |
| 71 | + client.orders.create(request=req) |
| 72 | +except KalshiTimeoutError: |
| 73 | + # reconcile before retrying — server may have committed |
| 74 | + existing = client.orders.get_by_client_order_id(req.client_order_id) |
| 75 | +``` |
| 76 | + |
| 77 | +### Passphrase-protected PEMs |
| 78 | + |
| 79 | +Per #217 / #233, `KalshiAuth.from_pem`, `from_key_path`, `from_env`, |
| 80 | +and `try_from_env` accept a `password=` kwarg (`str` / `bytes` / a |
| 81 | +zero-arg callable returning either — the callable form lets you |
| 82 | +defer fetching the secret from a vault until load-time). `from_env` |
| 83 | +/ `try_from_env` also read `KALSHI_PRIVATE_KEY_PASSPHRASE` from the |
| 84 | +environment; the explicit `password=` wins when both are set. |
| 85 | +Encrypted PEMs no longer have to be stripped to disk first. |
| 86 | + |
| 87 | +```python |
| 88 | +auth = KalshiAuth.from_key_path( |
| 89 | + "your-key-id", |
| 90 | + "~/.kalshi/private_key.pem", |
| 91 | + password=lambda: vault.get_secret("kalshi-pem-pass"), |
| 92 | +) |
| 93 | +``` |
| 94 | + |
| 95 | +### Opt-in HTTP/2 |
| 96 | + |
| 97 | +Per #233, `KalshiConfig(http2=True)` enables HTTP/2 on the REST |
| 98 | +client (off by default for compat). The `h2` dependency is not |
| 99 | +installed by default; `KalshiConfig.__post_init__` fail-fasts with a |
| 100 | +clear message when `http2=True` is set without it: |
| 101 | + |
| 102 | +```bash |
| 103 | +pip install 'kalshi-sdk[http2]' |
| 104 | +``` |
| 105 | + |
| 106 | +```python |
| 107 | +config = KalshiConfig(http2=True) |
| 108 | +client = KalshiClient(auth=auth, config=config) |
| 109 | +``` |
| 110 | + |
| 111 | +### Per-request `extra_headers` (transport-only, for now) |
| 112 | + |
| 113 | +Per #220, `KalshiConfig.extra_headers` sets client-wide default |
| 114 | +headers, and `SyncTransport.request(..., extra_headers=...)` / |
| 115 | +`AsyncTransport.request(..., extra_headers=...)` merge per-call |
| 116 | +overrides on top. `KALSHI-ACCESS-*` signing headers always win, so |
| 117 | +callers cannot forge them via this surface. Resource-method plumbing |
| 118 | +— so e.g. `client.orders.create(..., extra_headers=...)` works |
| 119 | +directly — is tracked in #253 and not yet shipped in v2.4; only the |
| 120 | +transport-level entry point is public. |
| 121 | + |
| 122 | +--- |
| 123 | + |
3 | 124 | ## v2.2 → v2.3 |
4 | 125 |
|
5 | 126 | v2.3 is additive on the REST surface and **soft-breaking on the WebSocket |
|
0 commit comments