Skip to content

Commit 07d240f

Browse files
authored
release: 2.7.0 — post-v2.6 independent multi-LLM reviewer SDK audit closure (#311-#357) (#375)
* release: 2.7.0 — post-v2.6 independent multi-LLM reviewer audit closure (#311-#357) 47 issues filed, 44 closed across 4 sequential waves (W0 docs, W1 HIGH integrity, W2 MEDIUM correctness, W3 LOW polish/perf/deps); 17 PRs merged in disjoint git worktrees. 3 breaking-rename issues (#348, #349, #351) deferred to v3.0.0. Identified by a 9-reviewer parallel pass: 7 internal specialist agents plus fresh-eyes external LLM reviews via the Codex CLI (GPT-5) and Gemini CLI. Five behavioral fences: AmendOrderRequest Literal narrowing (#312), KalshiConfig.extra_headers post-construction immutability (#313), communications status Literal narrowing (#324), to_decimal NaN/Inf guard (#325), DollarDecimal request-side bounds (#343). Critical fixes: from_env ownership invariant (#311), WS TimeoutError wrap (#314), WS zombie-subscription cleanup (#315). Plus 36 medium/low correctness, performance, polish, docs, and dependency improvements — see CHANGELOG.md for the full per-issue breakdown. Main is mypy --strict clean, ruff clean, 2876 unit tests passing. * polish(release): clarify semver policy on bug-surfacing behavioral fences
1 parent 8bef6f5 commit 07d240f

5 files changed

Lines changed: 238 additions & 3 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!-- gitnexus:start -->
22
# GitNexus — Code Intelligence
33

4-
This project is indexed by GitNexus as **kalshi-python-sdk** (6266 symbols, 13386 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
4+
This project is indexed by GitNexus as **kalshi-python-sdk** (8490 symbols, 17402 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
55

66
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
77

CHANGELOG.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,212 @@
22

33
All notable changes to kalshi-sdk will be documented in this file.
44

5+
## 2.7.0 — 2026-05-22
6+
7+
Post-v2.6 independent multi-LLM reviewer audit closure. **47 issues filed
8+
(`#311``#357`) by a 9-reviewer parallel pass** combining 7 internal specialist
9+
agents (security/auth, HTTP transport, WebSocket, models/contracts, sync/async
10+
parity, performance, resources/API) with **fresh-eyes external LLM reviews via
11+
the Codex CLI (GPT-5) and Gemini CLI**. 44 issues closed in this release across
12+
4 sequential waves (W0 docs, W1 HIGH integrity, W2 MEDIUM correctness, W3 LOW
13+
polish/perf/deps); 17 PRs merged. 3 breaking-rename issues (`#348`, `#349`,
14+
`#351`) deferred to **v3.0.0** milestone. Main is mypy `--strict` clean, ruff
15+
clean, **2876 unit tests passing** (≈100 new regression tests added).
16+
17+
### Breaking changes
18+
19+
Five behavioral fences; all surface bugs that were silently wrong or invariants
20+
the SDK now enforces. Per project policy (established by v2.0–v2.6),
21+
bug-surfacing behavioral fences ship in minor releases; intentional public-API
22+
removals or renames are reserved for major releases — that's why the three
23+
breaking renames (`#348`, `#349`, `#351`) are deferred to v3.0.0.
24+
25+
- **`AmendOrderRequest.side` / `.action` narrowed to `Literal`** (`#312`).
26+
Mirrors the v2.5 `#270` narrowing on `CreateOrderRequest`. Previously any
27+
string passed validation and the server rejected with a 400; now invalid
28+
values raise `pydantic.ValidationError` at construction.
29+
- **`KalshiConfig.extra_headers` immutable post-construction** (`#313`). The
30+
`#298` `KALSHI-ACCESS-*` fence ran once at construction; post-construction
31+
mutation of `extra_headers` could reopen the auth-header forge surface.
32+
`extra_headers` is now stored as `MappingProxyType` over a defensive copy
33+
`config.extra_headers["k"] = "v"` raises `TypeError`. Construction-time
34+
fence unchanged.
35+
- **`CommunicationsResource.list_rfqs` / `list_quotes` `status` kwarg
36+
narrowed to `Literal`** (`#324`). New `RfqStatusLiteral` / `QuoteStatusLiteral`
37+
match the spec's closed set; arbitrary `str` is rejected by mypy at the
38+
call site. Consistent with the existing `OrdersResource.list(status=...)`
39+
pattern.
40+
- **`to_decimal()` rejects `NaN` / `Infinity`** (`#325`). The public helper
41+
promised safety in its docstring but accepted any `Decimal`. Now raises
42+
`ValueError` on non-finite inputs, matching the `_coerce_decimal` validator.
43+
- **`DollarDecimal` request-side fields reject negative prices and
44+
sub-tick precision** (`#343`). `CreateOrderRequest`,
45+
`AmendOrderRequest`, `CreateOrderV2Request`, `AmendOrderV2Request` now
46+
reject negative `yes_price` / `no_price` and sub-$0.0001-tick precision
47+
at construction. Response-side `DollarDecimal` is unchanged (servers may
48+
emit any value).
49+
50+
### Critical (HIGH-severity money/auth/data-integrity fixes)
51+
52+
- **`KalshiClient.from_env` preserves caller ownership of `KalshiAuth`**
53+
(`#311`). The `from_env` classmethod (sync and async) overwrote `_auth_owned`
54+
from the input kwarg instead of recomputing from what `__init__` actually
55+
did. Two real bugs: (a) `from_env(key_id=..., private_key=...)` with no env
56+
vars set leaked the sign `ThreadPoolExecutor` because the SDK-built auth
57+
was flagged "not owned"; (b) `from_env(auth=my_auth)` with env vars set
58+
caused `client.close()` to close the caller's still-referenced auth,
59+
raising `RuntimeError("KalshiAuth has been closed")` on the next
60+
`sign_request`. The fix recomputes ownership from `__init__`'s invariant.
61+
- **WS `_wait_for_response` wraps `TimeoutError` as `KalshiSubscriptionError`**
62+
(`#314`). `asyncio.wait_for` raises bare `TimeoutError` on timeout, which
63+
escaped `subscribe()` / `unsubscribe()` unhandled because only
64+
`ConnectionClosed` was caught. Now wrapped with `channel` / `client_id` /
65+
`op` context so consumers branching on SDK exceptions actually see the
66+
expected type.
67+
- **WS zombie-subscription cleanup on gap-recovery failure** (`#315`).
68+
`broadcast_error` never popped the dead `Subscription`. A failed
69+
`resubscribe_one` in `_handle_seq_gap` (and the `KalshiBackpressureError`
70+
path) left a zombie sub whose closed queue persisted; the next reconnect's
71+
`resubscribe_all` resurrected it on the server — silent data loss + a
72+
server-quota leak. `broadcast_error` now pops `_subscriptions` and
73+
`_sid_to_client` so reconnects can't resurrect dead subs.
74+
75+
### High-impact correctness
76+
77+
- **`from_env` lazy `try_from_env()` evaluation** (`#316`). The classmethod
78+
eagerly evaluated `KalshiAuth.try_from_env()` even when the caller passed
79+
`auth=` / `key_id` / `private_key` / `private_key_path`. A malformed
80+
`KALSHI_PRIVATE_KEY` in the process env then raised on every CI worker
81+
that bypassed env. Now gated on `caller_supplied_auth`.
82+
- **`sign_request` strips URL fragment** (`#317`). `path.split("?")[0]`
83+
stripped query strings but not `#fragment`. httpx drops fragments before
84+
sending, producing a guaranteed signature mismatch (401) on any path
85+
containing `#`. Now strips both.
86+
- **`Retry-After` honored across 408 / 429 / 503 / 504** (`#322`). Previously
87+
only 429 parsed `Retry-After`. Per RFC 7231 §7.1.3 the header applies to
88+
all four. `_parse_retry_after` extracted; `retry_after` lifted to
89+
`KalshiError` base class so `KalshiServerError` and `KalshiTimeoutError`
90+
also surface the hint.
91+
- **`Retry-After` path applies jitter** (`#321`). Synchronized clients
92+
hitting the same 429/503 window would stampede the rate limit at the
93+
exact server-suggested moment. Full Jitter is now added on top of the
94+
server floor, capped at `retry_max_delay`. Preserves RFC compliance
95+
(`Retry-After` is a "no sooner than" hint).
96+
- **Response body size cap on success path** (`#323`). The `#203` 16 KB
97+
cap applied only to error responses; the success path was unbounded.
98+
New `_enforce_response_body_cap` enforces the same protection on
99+
non-streaming success bodies via a Content-Length pre-check plus a
100+
post-buffer guard.
101+
- **V1 order request models gain `ge=0` parity** (`#326`). `subaccount`,
102+
`exchange_index`, `subaccount_number` on `CreateOrderRequest`,
103+
`AmendOrderRequest`, `BatchCancelOrderRequest`, and related V1 + group
104+
models now reject negative integers (matching V2 and the `#295` sweep).
105+
- **`Page._columns` handles `None`-first nullable nested columns** (`#328`).
106+
Detection inspected only `cols[0]`; a `None`-first nullable nested
107+
`BaseModel` column skipped the `model_dump` pass and broke
108+
`to_dataframe` / `to_polars` for nullable-Struct schemas.
109+
- **WS `OrderbookDeltaPayload.ts` typed as `AwareDatetime`** (`#331`).
110+
Missed by the v2.5 `#270` WS datetime sweep. Tightening across
111+
`orderbook_delta`, `user_orders`, and `communications` payloads.
112+
- **WS backpressure error closes connection cleanly** (`#332`).
113+
`KalshiBackpressureError` in the recv loop previously broadcast sentinels
114+
and broke, but left the WS open and `_running=True`. The next
115+
`subscribe_*` restarted the recv loop on top of orphan server-side subs.
116+
Now closes the connection and clears `_running` / manager refs so the
117+
next session starts fresh.
118+
119+
### Performance
120+
121+
- **Orderbook materialization via `model_construct`** (`#327`).
122+
`_BookState.to_orderbook` previously re-validated every price level through
123+
Pydantic on each `apply_delta`. Data is SDK-canonical after the snapshot
124+
validation; switched to `model_construct` to skip the redundant pass.
125+
- **Zero-delta orderbook updates preserve cache** (`#347`). A no-op delta
126+
(quantity unchanged) used to invalidate the memoized `Orderbook` view,
127+
defeating the `#244` cache. Now skipped when the price-level dict is
128+
unchanged.
129+
- **Public `apply_snapshot` single-copy** (`#344`). The public path
130+
allocated `yes` / `no` dicts twice — once via identity in
131+
`_apply_snapshot_inplace`, then again via `dict(msg.msg.yes)` to copy
132+
defensively. The bypass path (recv hot loop, `#296`) keeps identity
133+
adoption; the public path now copies once.
134+
- **V2 batch endpoints use bytes-fast-path** (`#329`). `batch_create_v2` /
135+
`batch_cancel_v2` paid the dict-walk serializer twice; the v2.4 `#223`
136+
fast-path was never extended. Now serialize via `model_dump_json` once
137+
and send the bytes.
138+
- **RSA-PSS / MGF1 / SHA256 config cached** (`#345`). Previously
139+
allocated per signature. Hoisted to module-level constants — measurable
140+
on the auth hot path.
141+
- **`SequenceTracker.track` sync fast-path** (`#330`). New public
142+
`track_sync` lets the recv hot loop skip the per-frame coroutine-object
143+
allocation; the async wrapper remains for callers that need to await on
144+
the gap path.
145+
- **WS recv `asyncio.timeout` swap** (`#356`). Replaced `asyncio.wait_for`
146+
in the recv hot path with `async with asyncio.timeout(...)` (Python 3.11+)
147+
— fewer TimerHandle allocations per frame.
148+
- **`method.upper()` hoisted out of retry loop** (`#342`). Computed once
149+
per request instead of once per retry attempt.
150+
151+
### Polish
152+
153+
- **`AsyncTransport.close()` cancellation-safe** (`#333`). Sets `_closed`
154+
after `aclose()` returns, not before — cancellation between the two no
155+
longer leaks the httpx pool. Mirrors the v2.6 `#301` sync fix.
156+
- **`_delete_with_body(params=...)` symmetry** (`#340`). The body-only
157+
variant gains the `params` kwarg that `_delete_with_body_json` already
158+
accepted.
159+
- **Pagination cycle detector catches non-adjacent loops** (`#352`). The
160+
prior detector only caught adjacent cursor repeats; an `A→B→A→B` loop
161+
from a load-balanced pod-state drift now raises before exhausting
162+
`max_pages`. Memory-bounded at 1024 seen cursors.
163+
- **WS `_handle_orphan_subscribed` correlation** (`#354`). Server
164+
`unsubscribed` acks for sids the client never owned no longer clobber
165+
unrelated state.
166+
- **WS `ConnectionManager.reconnect` exc_info on failure** (`#355`). Per-
167+
attempt failures now log with `exc_info=True` at DEBUG so root cause
168+
(auth / TLS / DNS) survives to max-retry.
169+
- **WS `_stop()` teardown order** (`#357`). Closes the connection FIRST,
170+
then broadcasts sentinels — late in-flight frames no longer land on
171+
closed queues. `close()` wrapped in `try/finally` so sentinels always
172+
fire even if `close()` raises.
173+
- **`KalshiConfig.extra_headers` doubled-pipeline removed** (`#341`).
174+
Previously attached to `httpx.Client(headers=...)` defaults AND merged
175+
per-request via `_ci_merge`. Now `_ci_merge` is the single source of
176+
truth; the `#298` precedence contract (config defaults < per-call
177+
extras < signed auth) is structurally enforced.
178+
- **`orders.create()` overload tightening** (`#350`). The `**kwargs`
179+
overload statically required `action` and `count` since v2.5 `#242`;
180+
the type signature is now updated to match runtime so mypy catches
181+
omissions at the call site.
182+
- **Documentation drift sweep** (`#318`, `#319`, `#320`, `#334`, `#336`,
183+
`#337`, `#338`, `#339`). Three live-data endpoint paths and the
184+
`batch()` return shape corrected; `positions_all()` "does not exist"
185+
claim dropped (it shipped in v2.5 `#269`); `structured_targets.get()`
186+
404 mapping corrected (raises `KalshiNotFoundError`, not `None`);
187+
sync/async docstring drift normalized across `orders`, `markets`,
188+
`portfolio`, `communications`; `CreateOrderV2Request` docstring
189+
corrected (`DollarDecimal`, not the nonexistent `FixedPointDollars`);
190+
`LiveDataResource.get_typed` clarified as the legacy URL form.
191+
- **`KalshiAuth.from_pem` OpenSSH-format error** (`#335`). Detecting an
192+
`-----BEGIN OPENSSH PRIVATE KEY-----` header now raises a targeted
193+
`KalshiAuthError` with the exact `ssh-keygen -p -m PKCS8 -f <path>`
194+
conversion command instead of the generic PEM parse failure.
195+
196+
### Additive
197+
198+
- **`kalshi.OrderPrice`** — public type alias for the request-side
199+
bounded `DollarDecimal` used by order-request models (`#343`).
200+
- **`kalshi.RfqStatusLiteral`, `kalshi.QuoteStatusLiteral`** — public
201+
Literal aliases for communications status filtering (`#324`).
202+
- **`SequenceTracker.track_sync`** — public sync entry point for the WS
203+
recv hot path (`#330`).
204+
205+
### Dependencies
206+
207+
- **`pydantic>=2.4`** (`#346`). Bumped from `>=2.0` to ensure the
208+
`StrictInt` / `_coerce_decimal` invariants the SDK relies on get the
209+
2.4+ semantics. Pydantic 2.0–2.3 also shipped JSON-parser bugs.
210+
5211
## 2.6.0 — 2026-05-22
6212

7213
Post-v2.5 independent reviewer audit closure (`#273` follow-on). 7 issues

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Reference issues from PRs via `Closes #N` so the issue closes on merge.
144144
<!-- gitnexus:start -->
145145
# GitNexus — Code Intelligence
146146

147-
This project is indexed by GitNexus as **kalshi-python-sdk** (6266 symbols, 13386 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
147+
This project is indexed by GitNexus as **kalshi-python-sdk** (8490 symbols, 17402 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
148148

149149
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
150150

ROADMAP.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@
44

55
See `CHANGELOG.md` for full release history.
66

7+
- **v2.7.0 (2026-05-22)** — post-v2.6 independent multi-LLM reviewer audit
8+
closure (47 issues `#311``#357`, 44 closed; `#348` / `#349` / `#351`
9+
deferred to v3.0.0). Identified by a 9-reviewer parallel pass combining 7
10+
internal specialist agents with fresh-eyes Codex CLI (GPT-5) + Gemini CLI
11+
reviews. Five breaking fences folded in: `AmendOrderRequest.side`/`.action`
12+
narrowed to `Literal` (`#312`); `KalshiConfig.extra_headers` immutable
13+
post-construction (`#313`); communications `status` kwarg `Literal`
14+
(`#324`); `to_decimal()` rejects NaN/Inf (`#325`); `DollarDecimal` request
15+
fields reject negative/sub-tick (`#343`). Critical fixes: `from_env`
16+
caller-ownership invariant (`#311`), WS `TimeoutError` wrap (`#314`), WS
17+
zombie-subscription cleanup (`#315`). High-impact correctness: lazy
18+
`try_from_env` (`#316`), `sign_request` strips URL fragment (`#317`),
19+
`Retry-After` honored across 408/429/503/504 with jitter (`#321`, `#322`),
20+
success-body size cap (`#323`), V1 `ge=0` parity (`#326`), `Page._columns`
21+
None-first nested (`#328`), WS `OrderbookDeltaPayload.ts` AwareDatetime
22+
(`#331`), WS backpressure WS teardown (`#332`). Performance: orderbook
23+
`model_construct` (`#327`), zero-delta cache preservation (`#347`),
24+
`apply_snapshot` single-copy (`#344`), V2 batch bytes fast-path (`#329`),
25+
PSS config cache (`#345`), `track_sync` (`#330`), `asyncio.timeout` recv
26+
(`#356`), `method.upper` hoist (`#342`). Polish: async close
27+
cancellation-safe (`#333`), `_delete_with_body(params=)` (`#340`),
28+
non-adjacent pagination cycle (`#352`), orphan unsubscribe correlation
29+
(`#354`), reconnect exc_info (`#355`), `_stop()` teardown order (`#357`),
30+
dropped `extra_headers` dual pipeline (`#341`), `orders.create()` overload
31+
(`#350`), docs + docstring drift (`#318``#320`, `#334``#339`), OpenSSH
32+
key error message (`#335`). Dependencies: `pydantic>=2.4` (`#346`).
33+
Executed across 4 sequential waves (W0 docs, W1 HIGH, W2 MEDIUM, W3 LOW)
34+
in disjoint git worktrees — 17 PRs merged.
35+
736
- **v2.6.0 (2026-05-22)** — post-v2.5 independent reviewer audit closure
837
(`#273` follow-on, 7 issues `#295``#301`). Two breaking changes folded
938
in: `int` request fields reject `bool` across V1+V2 via new

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "kalshi-sdk"
3-
version = "2.6.0"
3+
version = "2.7.0"
44
description = "A professional Python SDK for the Kalshi prediction markets API"
55
readme = "README.md"
66
license = { text = "MIT" }

0 commit comments

Comments
 (0)