From cf128af775ea9b85acca2d294be42f9d008b7210 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sun, 17 May 2026 14:20:02 -0500 Subject: [PATCH 1/2] release: v2.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cuts the v2.0.0 release. Captures the full audit-driven hardening wave (30 audit findings closed across 5 parallel waves + cleanup) and the 3 deliberate breaking changes: Order.type rename, AccountApiLimits restructure, count/size/volume type retype. Changes: - CHANGELOG.md: finalize [Unreleased] → ## 2.0.0 — 2026-05-17 with a full release summary (Breaking, Added, Changed, Fixed, Security, Performance, Internal). - ROADMAP.md: rewrite around Shipped / Open trackers / Next milestone / Execution conventions. The wave-by-wave v1.2 plan is captured under Shipped as v2.0.0; CHANGELOG carries the detail. - README.md: document max_pages in the Pagination section, surface http2/limits/extra_headers in the KalshiConfig example. - docs/migration.md: new v1.x → v2.0 section at the top covering the 3 breaking changes + the non-breaking behavior shifts (unbounded *_all() by default, uniform extra="allow", WS callback fan-out). - pyproject.toml + kalshi/__init__.py: 1.1.0 → 2.0.0. - CLAUDE.md: active milestone → post-v2.0. - tests/integration/helpers.py: add wait_until_not_found inverse poller. - tests/integration/test_order_groups.py::test_delete: use wait_until_not_found so the demo's query-exchange propagation lag after DELETE doesn't make the test flap. - AGENTS.md: GitNexus stat refresh after Wave 5 + #142 + #143 merges. Verify: - uv run pytest tests/ --ignore=tests/integration -q → 1808 passed - uv run pytest tests/integration/ → 215 passed, 30 skipped - uv run ruff check . → clean - uv run mypy kalshi/ --strict → clean (76 source files) Closes #11 (release-cut milestone tracker). Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- CHANGELOG.md | 258 ++++++++++++++++++------- CLAUDE.md | 4 +- README.md | 11 ++ ROADMAP.md | 187 +++++------------- docs/migration.md | 73 +++++++ kalshi/__init__.py | 2 +- pyproject.toml | 2 +- tests/integration/helpers.py | 26 +++ tests/integration/test_order_groups.py | 8 +- 10 files changed, 355 insertions(+), 218 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fea0184..9168e56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **kalshi-python-sdk** (5829 symbols, 12434 relationships, 236 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **kalshi-python-sdk** (6261 symbols, 13379 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4be9da3..3f18101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,107 +2,217 @@ All notable changes to kalshi-sdk will be documented in this file. -## [Unreleased] - -### Added +## 2.0.0 — 2026-05-17 -- **`max_pages: int | None` kwarg on every public `*_all()` method** — sync and - async (19 + 19 method signatures). Bounded iteration without manual pagination. - `None` (default) iterates until the server returns no cursor; the existing - cursor-repeat guard remains the safety net against infinite loops (#98). -- **`RateLimit` model** exposed via `kalshi.RateLimit` — represents the per- - direction token-bucket structure on `AccountApiLimits.read` / `.write`. -- **`KalshiConfig.http2` and `KalshiConfig.limits`** — opt-in HTTP/2 and - `httpx.Limits` (connection pool sizing, keep-alive) on the transport. - Defaults preserve existing behavior (`http2=False`, `limits=None`). - -### Changed - -- **All response models uniformly use `extra="allow"` (#114).** Previously - 5 response models (`Page`, `Orderbook`, `OrderbookLevel`, - `BidAskDistribution`, `PriceDistribution`) fell back to Pydantic's default - `extra="ignore"`, silently dropping unknown fields — while the 80 sibling - response models exposed them via `__pydantic_extra__`. Instances of those - 5 classes will now preserve unknown fields. Request bodies remain - `extra="forbid"` (unchanged). -- **Count/size/volume response fields retyped from `DollarDecimal` to - `FixedPointCount` (#90).** Fields with `_fp` wire aliases were annotated - as `DollarDecimal` — the type that signals "dollar amount." Runtime - behavior is unchanged (both validators are byte-identical and parse the - same wire strings into `Decimal`), but the annotation now communicates - the correct semantics and prevents silent corruption if either parser - ever diverges. Affected: `Market.{yes_bid_size, yes_ask_size, no_bid_size, - no_ask_size, volume, volume_24h, open_interest}`, `Candlestick.{volume, - open_interest}`, `OrderbookLevel.quantity`, `Fill.count`, `Trade.count`, - `MarketPosition.position`, `EventPosition.total_cost_shares`, - `Settlement.{yes_count, no_count}`, `Series.volume`. Both types resolve - to `Decimal` at the type level, so user code that handles these fields - as `Decimal` is unaffected. -- **`*_all()` methods are now unbounded by default.** Previously, internal - `_list_all` had an invisible 1000-page safety cap that silently truncated - callers iterating beyond ~100k items. The cap is gone; the cursor-repeat - guard (`KalshiError` on a repeating cursor) provides the runaway protection - it always did. Callers who want a cap pass `max_pages=N` explicitly. No - user-visible change for anyone iterating <1000 pages (#98). -- **WS callbacks no longer suppress queue delivery (#80).** Previously, - registering a callback for a channel silently disabled the iterator queue - for that channel — a user who held both an `@on()` callback AND an - `async for msg in subscription` consumer would never see the iterator - fire. Now messages fan out to both. A WARNING is logged at - `register_callback` time if an active subscription already exists, so - users relying on the suppress-side-effect are alerted. Callback-only - users now accumulate up to the queue's `maxsize=1000`; existing - `DROP_OLDEST` backpressure prevents unbounded growth. - -### Fixed - -- **`/account/limits` response now parses against the live server.** The - published OpenAPI spec (v3.13.0) declares `read_limit`/`write_limit` as - ints, but the live API returns nested `read`/`write` token-bucket objects - with `bucket_capacity` + `refill_rate`. `AccountApiLimits` now matches the - server. New `RateLimit` model exposed for the bucket structure. -- **`/search/tags_by_categories` no longer crashes** when a category (e.g. - `Social`) returns `null` instead of an empty list. `tags_by_categories` - values are now `NullableList[str]`, collapsing `null` → `[]`. +Audit-driven hardening release. 30 audit-findings landed across five +parallel waves (Wave 1 – Wave 5) plus follow-ups: a WebSocket recv-loop +overhaul, a spec-sync supply-chain rewrite, double-parse/double-validate +elimination on the WS hot path, and surface-level cleanup including +three deliberate breaking changes called out below. ### Breaking - **`AccountApiLimits.read_limit` / `.write_limit` removed.** Replaced with `AccountApiLimits.read` / `.write`, both of type `RateLimit` - (`bucket_capacity: int`, `refill_rate: int`). The previous int fields - never worked against the live server, so practical migration impact is - expected to be limited to code written from the spec rather than tested - against the API. + (`bucket_capacity: int`, `refill_rate: int`). The published OpenAPI spec + declares the limits as ints, but the live server returns nested token + buckets — v2 matches the server. The old int fields never worked against + the live API. ```python - # Before + # v1 limits = client.account.limits() limits.read_limit # AttributeError after upgrade - # After + # v2 limits.read.bucket_capacity # int limits.read.refill_rate # int ``` - **`Order.type` renamed to `Order.order_type`.** Wire format is unchanged - (`validation_alias=AliasChoices("type", "order_type")` accepts both names on - deserialization), but any user code reading `.type` on an `Order` instance - must migrate to `.order_type`. Rationale: matches the project's existing + (`validation_alias=AliasChoices("type", "order_type")` accepts both names + on deserialization), but any user code reading `.type` on an `Order` + instance must migrate to `.order_type`. Matches the project's existing builtin-shadow-avoidance convention (`milestone_type`, `target_type`, `incentive_type`). Spec v3.13.0 still defines `type` as required, so the field is preserved on the wire — only the Python attribute name changed (#91). ```python - # Before + # v1 order = client.portfolio.orders.get(order_id="...") - print(order.type) # AttributeError after upgrade + print(order.type) # AttributeError after upgrade - # After + # v2 print(order.order_type) ``` - Version-bump decision (v1.2 vs v2.0) deferred to release cut. +- **Count/size/volume response fields retyped `DollarDecimal` → `FixedPointCount` + (#90).** Fields with `_fp` wire aliases were annotated as the type that + signals "dollar amount." Runtime behavior is unchanged (both validators + resolve to `Decimal`), but the annotation now communicates the right + semantics. `mypy --strict` users may need to update narrow assertions; + `isinstance(x, Decimal)` remains valid. Affected: `Market.{yes_bid_size, + yes_ask_size, no_bid_size, no_ask_size, volume, volume_24h, open_interest}`, + `Candlestick.{volume, open_interest}`, `OrderbookLevel.quantity`, + `Fill.count`, `Trade.count`, `MarketPosition.position`, + `EventPosition.total_cost_shares`, `Settlement.{yes_count, no_count}`, + `Series.volume`. + +### Added + +- **`max_pages: int | None` kwarg on every public `*_all()` method** — sync + and async (19 + 19 method signatures). Bounded iteration without manual + pagination. `None` (default) iterates until the server returns no cursor + (#98). +- **`RateLimit` model** exposed via `kalshi.RateLimit` — represents the + per-direction token-bucket structure on `AccountApiLimits.read` / `.write`. +- **`KalshiConfig.http2` and `KalshiConfig.limits`** — opt-in HTTP/2 and + `httpx.Limits` (connection pool sizing, keep-alive) on the transport. + Defaults preserve existing behavior (#141, F-R-15). +- **23 model classes re-exported from `kalshi.__all__`** — every name in + `kalshi.models.__all__` is now also importable from the top-level + `kalshi` package. New dynamic parity test enforces the invariant (#89). +- **`ConnectionManager.mark_streaming()`** public API — replaces the + recv-loop's prior `_set_state` reach-through (#88). + +### Changed + +- **`*_all()` methods are now unbounded by default.** Previous internal + 1000-page cap silently truncated callers iterating beyond ~100k items. + The cursor-repeat guard remains the runaway protection it always was. + Callers wanting a cap pass `max_pages=N` explicitly (#98). +- **All response models uniformly use `extra="allow"` (#114).** Previously + 5 response models (`Page`, `Orderbook`, `OrderbookLevel`, + `BidAskDistribution`, `PriceDistribution`) fell back to Pydantic's default + `extra="ignore"`, silently dropping unknown fields. They now preserve + them on `__pydantic_extra__`. Request bodies remain `extra="forbid"`. + Drift guard test (`tests/test_model_extra_policy.py`) enforces the + policy across every exported model. +- **WS callbacks no longer suppress queue delivery (#80).** Previously, + registering a callback for a channel silently disabled the iterator + queue for that channel — a user holding both an `@on()` callback AND an + iterator on the same channel would never see the iterator fire. Now + messages fan out to both. A WARNING is logged at `register_callback` + time if an active subscription already exists, so upgraders see the + signal. Callback-only users now accumulate up to `maxsize=1000` (existing + `DROP_OLDEST` backpressure prevents unbounded growth). +- **`OrderbookManager` returns fresh snapshots instead of mutating in + place (#85).** Consumers holding a reference to a previously-emitted + `Orderbook` no longer see leaked mutations on the next delta. +- **`KalshiConfig` validates `base_url` and `ws_base_url`** at construction + time. Non-`https`/`wss` to remote hosts is rejected; loopback HTTP/WS + permitted for local mock servers. Unknown but secure hosts log a + WARNING (proxies still work). Trailing slashes are normalized (#94). + +### Fixed + +- **`/account/limits` response now parses against the live server.** The + published OpenAPI spec declares `read_limit`/`write_limit` as ints, but + the live API returns nested `read`/`write` token-bucket objects. + `AccountApiLimits` now matches the server. +- **`/search/tags_by_categories` no longer crashes** when a category (e.g. + `Social`) returns `null` instead of an empty list. `tags_by_categories` + values are now `NullableList[str]`, collapsing `null` → `[]`. +- **WS recv-loop reconnect/resubscribe correctness** — 5 race conditions + fixed (per-sub isolation in resubscribe-all, `_subscribe_lock` covering + the full reconnect+resubscribe sequence, `asyncio.shield` around the + recv→dispatch critical section, `ConnectionClosed` surfacing as + `KalshiConnectionError`, sentinel-before-cleanup ordering in + `unsubscribe`) (#77). +- **WS recv-loop exception ladder narrowed** — `KalshiBackpressureError` / + `KalshiSubscriptionError` break the loop and broadcast sentinels to all + consumers; `json.JSONDecodeError` / `pydantic.ValidationError` / `KeyError` + log+continue; unexpected exceptions broadcast sentinels then re-raise + (#83). +- **WS server-initiated unsubscribe reaps `_sid_to_client`** mappings + alongside the subscription, and resets `SequenceTracker._last_seq[sid]` + via the now-wired `seq_tracker` kwarg (#81). +- **WS channel-level error envelopes surface via `on_error`** instead of + being silently dropped, with a fallback log if no handler is registered + (#82). +- **WS seq watermark rolls back on backpressure** — `_process_frame` + captures the pre-`track` watermark and restores it if dispatch raises + `KalshiBackpressureError`, so the dropped message stays visible as a + future gap rather than being silently treated as already-seen (#78). +- **WS multi-ticker seq-gap clears every ticker** in the affected + subscription instead of only `tickers[0]` (#79). +- **`Retry-After` parser rejects negative, NaN, and infinite values** + (busy-loop / sleep-crash / cap-bypass) and honors `Retry-After: 0` + end-to-end through the retry loop (was dropped by a falsy-check) (#96). +- **`Page.to_dataframe()` / `.to_polars()` nested-model serialization** + pinned by tests; behavior is now under regression guard (#101). +- **WS double-parse + double-validate eliminated** — recv loop parses + JSON once and hands the parsed dict (plus a `pre_validated` typed + message for orderbook channels) to the dispatcher (#86). + +### Security + +- **Spec-sync workflow hardened against upstream compromise.** Reduced + permissions from `contents: write + pull-requests: write` to + `contents: read + issues: write`. Removed automatic PR creation and + in-CI code-generation. Drift now opens a new `spec-drift`-labeled issue + per distinct fingerprint (sha256 of upstream specs), deduped to prevent + spam. SHA-pinned all third-party actions. Body rendered via Python + template (no shell expansion of upstream content) (#92). +- **URL leakage scrubbed from `KalshiError.__str__`** — httpx exception + strings include the full request URL with query parameters, which + surfaced credentials in Sentry/log sinks. Surface method + path only + (no host, no query); the underlying exception is on `__cause__`. Same + fix in `KalshiConnectionError` for the WebSocket connect path (#84 + F-O-09). +- **Trade-data leakage scrubbed from WS dispatch log** — Pydantic's + `ValidationError.__str__` echoes the full input including trade + payload (price, count, user identifiers). Dropped `exc_info=True` on + the failure log; surface type + exception class only (#84 F-O-05). +- **Claude action workflows SHA-pinned** + Dependabot enabled + nightly + `pip-audit` workflow added (#93, #95). +- **Integration-nightly workflow shreds the PEM** on exit so a paused + job can't leak the demo private key (#106 F-O-11). +- **PyPI release workflow uploads sigstore attestations** for trusted + publisher verification (#106 F-O-12). +- **pytest bumped to `>=9,<10`** to clear CVE-2025-71176 (predictable + `/tmp/pytest-of-{user}` directory on UNIX) (#123). + +### Performance + +- **`MessageQueue.qsize()` O(n) → O(1)** via a counter updated on + put/get/`__anext__`/DROP_OLDEST eviction (#103). +- **REST retry backoff switched to AWS Full Jitter** — + `uniform(0, min(cap, base * 2**attempt))`, cap applied before + randomization (#104). +- **`RecordingTransport` O(N²) → amortized O(1) per request** — buffered + in-memory, flushed on close instead of rewriting the recording on + every request (#105). +- **`OrderbookManager.apply_delta` O(n) → O(1)** via a price-indexed + dict, materializing sorted level lists lazily on snapshot emit (#87). +- **Transport caches `urlparse(base_url).path` once** instead of + re-parsing per request (#106 F-R-04). + +### Internal + +- **`kalshi/__init__.py` re-export parity** — 23 model classes now + re-exported from the top-level package with a dynamic parity test + that prevents silent drift (#89). +- **`ExclusionKind = "client_only"`** for SDK-only kwargs with no spec + counterpart (e.g. `max_pages`). Distinguishes from `paginator_handled` + (spec params the SDK hides). +- **`tests/integration/helpers.py`** gained `wait_for_resource` / + `await_resource` for demo's eventual-consistency lag on + `POST → GET-by-id` (orders / order_groups). Fixes 11 integration + failures that surfaced after configuring the demo secrets in CI. +- **`tests/integration/test_subaccounts.py`** ephemeral fixture polls + `list_balances` for the new subaccount instead of asserting immediate + visibility. +- **Test count delta: 1407 → 1808** (+401 across all waves). +- **Dependency-resolution drift fix**: pinned `ast-serialize<0.5` (a + transitive of mypy 2.1) whose 0.5.0 release dropped `cp39-abi3` + wheels, breaking CI on Python 3.12/3.13. + +### Migration + +See [`docs/migration.md`](docs/migration.md) for a focused v1.x → v2.0 +migration guide. ## 1.1.0 — 2026-05-16 diff --git a/CLAUDE.md b/CLAUDE.md index 784266f..c52fdee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,7 @@ All work — bugs, enhancements, polish, spec drift — is tracked in **GitHub I on `TexasCoding/kalshi-python-sdk`. Use the `gh` CLI or the GitHub UI; do not add markdown trackers (TODOS/BACKLOG) back to the repo. -- Active milestone: `v1.1` (post-1.0 enhancements and polish) +- Active milestone: post-v2.0 (after the audit-driven hardening wave that shipped v2.0.0) - Labels in use: `bug`, `enhancement`, `documentation`, `polish`, `breaking`, `spec-drift`, `testing`, `infra`, `ws`, `cli` - `ROADMAP.md` — short pointer to the active milestone - `CHANGELOG.md` — release-facing history; updated per release @@ -144,7 +144,7 @@ Reference issues from PRs via `Closes #N` so the issue closes on merge. # GitNexus — Code Intelligence -This project is indexed by GitNexus as **kalshi-python-sdk** (5829 symbols, 12434 relationships, 236 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **kalshi-python-sdk** (6261 symbols, 13379 relationships, 257 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/README.md b/README.md index f3d5424..727dfb9 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,10 @@ config = KalshiConfig( max_retries=5, retry_base_delay=0.5, retry_max_delay=15.0, + # Connection pool / HTTP-2 tuning (opt-in; defaults preserve v1 behavior) + http2=False, + limits=None, # httpx.Limits(max_connections=..., keepalive_expiry=...) + extra_headers={"X-My-Tag": "foo"}, ) client = KalshiClient(key_id="...", private_key_path="...", config=config) ``` @@ -228,8 +232,15 @@ while True: # Or just: for market in client.markets.list_all(status="open"): ... + +# Need a hard cap on pages (e.g. preview / quick sample)? +for market in client.markets.list_all(status="open", max_pages=5): + ... ``` +`*_all()` iterates until the server returns no cursor by default. Pass +`max_pages=N` for an explicit bound; passing `0` raises `ValueError`. + `Page[T]` also converts to a DataFrame when the optional extras are installed: ```bash diff --git a/ROADMAP.md b/ROADMAP.md index 2470992..3a63542 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,154 +1,71 @@ # Roadmap -## v1.2 — Audit-driven hardening (in progress) - -30 issues opened from the post-v1.1.0 audit swarm (`#77`–`#106`). Implementation -plan below mirrors the wave-based parallel execution that produced v1.1: each -wave is a set of disjoint-file branches worked by parallel agents off `main`, -reviewed and merged one PR at a time. Sequential waves depend on prior waves -landing first. - -### Wave 1 — ✅ Shipped (2026-05-17) - -5 PRs squash-merged, closing 10 issues. All disjoint-file work landed in one -day via parallel agents off `main`. - -| PR | Closes | Scope | -|---|---|---| -| [#108](https://github.com/TexasCoding/kalshi-python-sdk/pull/108) | [#93](https://github.com/TexasCoding/kalshi-python-sdk/issues/93) [#95](https://github.com/TexasCoding/kalshi-python-sdk/issues/95) | SHA-pin Claude workflows, add Dependabot + `pip-audit` | -| [#109](https://github.com/TexasCoding/kalshi-python-sdk/pull/109) | [#92](https://github.com/TexasCoding/kalshi-python-sdk/issues/92) | Spec-sync supply-chain mitigations (drift now opens per-fingerprint issues, not auto-PRs) | -| [#110](https://github.com/TexasCoding/kalshi-python-sdk/pull/110) | [#89](https://github.com/TexasCoding/kalshi-python-sdk/issues/89) | Re-export 23 model classes from `kalshi.__all__` + dynamic parity test | -| [#111](https://github.com/TexasCoding/kalshi-python-sdk/pull/111) | [#103](https://github.com/TexasCoding/kalshi-python-sdk/issues/103) [#104](https://github.com/TexasCoding/kalshi-python-sdk/issues/104) [#105](https://github.com/TexasCoding/kalshi-python-sdk/issues/105) | `MessageQueue.qsize()` O(1), AWS Full Jitter retry, buffered `RecordingTransport` | -| [#112](https://github.com/TexasCoding/kalshi-python-sdk/pull/112) | [#91](https://github.com/TexasCoding/kalshi-python-sdk/issues/91) [#94](https://github.com/TexasCoding/kalshi-python-sdk/issues/94) [#96](https://github.com/TexasCoding/kalshi-python-sdk/issues/96) | ⚠️ **Breaking** `Order.type` → `Order.order_type` + base URL validation (http/https + ws/wss) + `Retry-After` NaN/negative/zero handling | -| [#120](https://github.com/TexasCoding/kalshi-python-sdk/pull/120) | (CI hotfix) | `uv pip install pip` so pip-audit can introspect the uv-managed venv | - -**Wave 1 learnings worth carrying forward:** - -- Bot review iterates multiple passes. Spec-sync (#109) needed 4 rounds — the long-lived tracking-issue pattern was the wrong shape; per-drift fingerprint-deduped issues replaced it. -- The `Order.type` rename in #112 is the only breaking change in Wave 1 and triggers the v1.2 vs v2.0 release decision (see Release-cut criteria below). -- Worktree CWD slips between Bash calls in the harness — agents that don't pass an explicit `cd` to every Bash call leak files into the parent repo. Reinforce in agent prompts. -- pip-audit needs `pip` seeded into the uv venv; uv doesn't put it there by default. - -### Wave 2 — Test coverage backfill (⏸ paused; planned next) - -Paused — interim work in flight (see *Interim work* below). Resume when those -items land. Wave 2 still depends on Wave 1's correctness fixes (already in -main), so unblocked technically; the pause is scope ordering, not dependency. - -| Branch | Issues | -|---|---| -| `test/issue-97-retry-coverage` | [#97](https://github.com/TexasCoding/kalshi-python-sdk/issues/97) | -| `test/issue-98-max-pages` | [#98](https://github.com/TexasCoding/kalshi-python-sdk/issues/98) (test + public API kwarg) | -| `test/issue-99-config-coverage` | [#99](https://github.com/TexasCoding/kalshi-python-sdk/issues/99) | -| `test/issue-100-recorder-html` | [#100](https://github.com/TexasCoding/kalshi-python-sdk/issues/100) | -| `test/issue-101-dataframe-nested` | [#101](https://github.com/TexasCoding/kalshi-python-sdk/issues/101) | -| `test/issue-102-ws-backlog` | [#102](https://github.com/TexasCoding/kalshi-python-sdk/issues/102) | - -### Interim work (before Wave 2) - -Items in flight after Wave 1 landed. List grows / shrinks as work is scoped. - -- TBD — fill in as items are scoped. - -### Follow-ups opened during Wave 1 review - -- [#114](https://github.com/TexasCoding/kalshi-python-sdk/issues/114) — audit response models for consistent `extra=` policy. Opened during #112 review; pre-existing gap unrelated to #91's scope. Candidate for Wave 5 polish. -- [#113](https://github.com/TexasCoding/kalshi-python-sdk/issues/113) — closed as superseded by #109's per-drift fingerprint pattern. - -### Wave 3 — WebSocket overhaul (parallel by file boundary, 3 agents) - -Three disjoint WS file scopes can land in parallel since they touch different -modules. - -| Branch | Files | Issues | -|---|---|---| -| `fix/ws-orderbook-overhaul` | `kalshi/ws/orderbook.py` (+ model) | [#85](https://github.com/TexasCoding/kalshi-python-sdk/issues/85) mutate in place + [#87](https://github.com/TexasCoding/kalshi-python-sdk/issues/87) O(n) → dict-backed | -| `fix/ws-dispatcher-correctness` | `kalshi/ws/dispatch.py` | [#80](https://github.com/TexasCoding/kalshi-python-sdk/issues/80) callback collision + [#81](https://github.com/TexasCoding/kalshi-python-sdk/issues/81) server unsubscribe + [#82](https://github.com/TexasCoding/kalshi-python-sdk/issues/82) error envelope | -| `fix/ws-recv-loop-overhaul` | `kalshi/ws/client.py` + `channels.py` | [#77](https://github.com/TexasCoding/kalshi-python-sdk/issues/77) reconnect races + [#83](https://github.com/TexasCoding/kalshi-python-sdk/issues/83) broad except + [#84](https://github.com/TexasCoding/kalshi-python-sdk/issues/84) log leakage + [#86](https://github.com/TexasCoding/kalshi-python-sdk/issues/86) double-parse + [#88](https://github.com/TexasCoding/kalshi-python-sdk/issues/88) `_set_state` reach-through | - -The recv-loop branch (`#77` umbrella) is the biggest unit of work in v1.2 and -must integrate the broad-except fix at the same time — its 5 reconnect race -fixes change the same `_recv_loop` exception block that `#83` narrows. One -agent, sequential commits within the branch. - -### Wave 4 — Backpressure & gap correctness (after Wave 3) - -Depends on the recv-loop overhaul because it changes when seq-tracking and -orderbook-apply happen relative to dispatch. - -| Branch | Issues | -|---|---| -| `fix/ws-backpressure-gap-correctness` | [#78](https://github.com/TexasCoding/kalshi-python-sdk/issues/78) ERROR-overflow desync + [#79](https://github.com/TexasCoding/kalshi-python-sdk/issues/79) multi-ticker seq-gap | +## Shipped -### Wave 5 — Type-annotation cleanup + polish backlog +See `CHANGELOG.md` for full release history. -| Branch | Issues | -|---|---| -| `polish/issue-90-type-drift` | [#90](https://github.com/TexasCoding/kalshi-python-sdk/issues/90) | -| `polish/issue-106-backlog` | [#106](https://github.com/TexasCoding/kalshi-python-sdk/issues/106) (umbrella; pick off items opportunistically) | +- **v2.0.0 (2026-05-17)** — audit-driven hardening. 30 audit findings closed + across five parallel waves (`#77`–`#106`) plus follow-ups: WebSocket + recv-loop overhaul (5 reconnect races + narrowed exceptions), + spec-sync supply-chain rewrite, single-parse WS hot path, public + `max_pages` pagination cap, `extra="allow"` policy enforcement, + trade-data + URL log-leak scrubs, and 3 deliberate breaking changes + (`Order.type` → `.order_type`, `AccountApiLimits.{read,write}_limit` + removed, count/size/volume fields retyped to `FixedPointCount`). See + [`docs/migration.md`](docs/migration.md) for the v1 → v2 migration + guide. +- **v1.1.0 (2026-05-16)** — model-first request API, DataFrame integration, + record/replay mock transport, MkDocs documentation site, `Literal` enum + kwargs, sync/async dedup refactor, weekly spec sync + nightly integration + CI workflows. +- **v1.0.0 (2026-05-10)** — public API stable. 89/89 REST endpoints, 11 WS + channels, contract drift tests, PyPI trusted-publisher release pipeline. -### Release-cut criteria +## Open trackers -Ready to tag when: +- [#106](https://github.com/TexasCoding/kalshi-python-sdk/issues/106) — + Wave 5 polish backlog umbrella. 5 items landed via #141; remaining + sub-items (e.g. `MessageQueue.maxlen` defense-in-depth, RSA-sign via + executor, `run_forever` foot-gun) are opportunistic. +- [#45](https://github.com/TexasCoding/kalshi-python-sdk/issues/45) — + verify `json={}` workaround under prod credentials. Blocked on + prod-key access. +- [#53](https://github.com/TexasCoding/kalshi-python-sdk/issues/53) — + resolve nested `$ref` pointers in body-schema drift check. Spec + currently has no nested refs; implement when one lands. -- All **HIGH** severity items merged: - - ✅ `#89` (Wave 1, #110), ✅ `#92` (Wave 1, #109) - - ⏳ `#77`, `#78`, `#79` (Waves 3 + 4) - - ⏳ `#97` (Wave 2) -- All **MEDIUM** items merged or explicitly deferred with a comment in `ROADMAP.md`. -- `CHANGELOG.md` `[Unreleased]` section finalized into a versioned section. -- `pyproject.toml` and `kalshi/__init__.py` version bumped. +## Next milestone -**Version-bump decision: v1.2.0 vs v2.0.0** — Wave 1 #112 renamed -`Order.type` → `Order.order_type` (wire format preserved via -`validation_alias`, but the Python attribute changed). The breaking-change -entry is already in `CHANGELOG.md` under `[Unreleased] → Breaking`. Decide -at tag time: +Not scoped. Candidates from the v2.0 audit backlog: -- **v1.2.0** treats it as a small-blast-radius break (the attribute is on - a return-only model; no user-constructed `Order.type=` to migrate). Risk: - semver-strict consumers on `^1.x` pins get an `AttributeError` with no - deprecation period. -- **v2.0.0** is the semver-clean call. Heavier release narrative for what - is otherwise mostly hardening work. +- Apply `extra="allow"` policy to WS envelope models (`kalshi/ws/models/`) + to mirror the response-model uniformity from `#114`. +- `MessageQueue._buffer = collections.deque(maxlen=maxsize+1)` for + defense-in-depth (#106 F-P-15 / F-R-02). +- `_to_decimal_dollars` / `_to_decimal_fp` consolidation (#106 F-N-09 + + the follow-up noted in #140). +- WS UX foot-guns: auto-start recv loop when no `subscribe_*` was called, + resubscribe-time data-frame stashing (#106 F-P-16 / F-R-13). -Then `git tag && git push origin ` per `docs/RELEASING.md`. +Pick from `gh issue list` and the deferred items in `#106` opportunistically. -### Execution conventions +## Execution conventions (carried from v2.0) -Matches the wave pattern from v1.1: +These are the patterns that proved out across the five v2.0 waves: - Each wave's branches off `main` at the wave-start commit. - Each branch isolated in a git worktree (`isolation: "worktree"` for agent runs). - Each agent is **`general-purpose`**, not `octo:droids:octo-*` — the octo - droids failed reliably in the v1.1 audit swarm (rejected, hung, hallucinated - reports). + droids failed reliably in the v1.1 audit swarm (rejected, hung, + hallucinated reports). - Each agent commits in its worktree with `Closes #N`; PR opened by the - orchestrator; bot review addressed; squash-merge; gitnexus index refreshed. + orchestrator; bot review addressed; squash-merge; gitnexus index + refreshed after each merge that changes `kalshi/` symbols. - Sequential waves wait for prior waves to merge before starting. -- Worktree CWD must be specified explicitly in every Bash call — the harness - resets CWD between calls and multiple v1.1 agents wrote files to the parent - repo by accident. - -### Deferred from v1.1 (not blockers) - -- [#45](https://github.com/TexasCoding/kalshi-python-sdk/issues/45) — verify - `json={}` workaround under production credentials. Blocked on prod-key access. -- [#53](https://github.com/TexasCoding/kalshi-python-sdk/issues/53) — resolve - nested `$ref` pointers in body-schema drift check. Premature — the spec - currently has no nested refs. - -Both stay in the v1.1 GitHub milestone as tracking placeholders; they unblock -when their preconditions land, independent of v1.2. - -## Shipped - -See `CHANGELOG.md` for full release history. - -- **v1.1.0 (2026-05-16)** — model-first request API, DataFrame integration, - record/replay mock transport, MkDocs documentation site, `Literal` enum - kwargs, sync/async dedup refactor, weekly spec sync + nightly integration - CI workflows. -- **v1.0.0 (2026-05-10)** — public API stable. 89/89 REST endpoints, 11 WS - channels, contract drift tests, PyPI trusted-publisher release pipeline. +- **Worktree CWD must be specified explicitly in every Bash call** — the + harness resets CWD between calls and Wave 2's #102 and Wave 5's #106 + agents leaked commits onto local `main` because of CWD slip. +- For breaking changes, surface the version-bump decision (next minor vs + next major) explicitly in the PR body and CHANGELOG so the release + cutter doesn't have to dig. diff --git a/docs/migration.md b/docs/migration.md index d512fe1..381cf77 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,78 @@ # Migration +## v1.x → v2.0 + +v2.0 is mostly additive (new `max_pages` kwarg, `KalshiConfig.http2`/`limits`, +`RateLimit` model export). There are **3 deliberate breaking changes** — +all on the response-model surface, all driven by spec-or-server reality. +Migrate by find/replace. + +### `Order.type` → `Order.order_type` + +Renamed to avoid shadowing the Python builtin (matching the existing +`milestone_type` / `target_type` / `incentive_type` pattern). Wire format +is unchanged — incoming JSON `type` still populates the field via +`validation_alias`. + +```python +# v1 +order = client.portfolio.orders.get(order_id="...") +print(order.type) # → AttributeError after upgrade + +# v2 +print(order.order_type) # str | None — "limit" or "market" +``` + +### `AccountApiLimits.read_limit` / `.write_limit` removed + +Replaced with `AccountApiLimits.read` / `.write` of type `RateLimit`. The +published OpenAPI spec declares the limits as ints; the live server +actually returns nested token buckets. v2 matches the server. The old int +fields never worked against the live API. + +```python +# v1 +limits = client.account.limits() +limits.read_limit # → AttributeError after upgrade + +# v2 +limits.read.bucket_capacity # int +limits.read.refill_rate # int +# new model exposed: kalshi.RateLimit +``` + +### Response-model count/size/volume fields retyped + +Fields like `Market.volume`, `Fill.count`, `Trade.count`, +`MarketPosition.position` were annotated `DollarDecimal` but semantically +represent integer counts. v2 retypes them to `FixedPointCount`. Runtime +values still come back as `Decimal` — only the type annotation changed, +so `mypy --strict` users may need to update narrow assertions. +`isinstance(x, Decimal)` checks remain valid. + +### Non-breaking but worth knowing + +- **`*_all()` is now unbounded by default.** The previous internal 1000-page + cap silently truncated callers iterating beyond ~100k items. Cursor-repeat + guard is still the safety net against server bugs. Pass `max_pages=N` for + an explicit cap. +- **Response models uniformly use `extra="allow"`.** 5 models (`Page`, + `Orderbook`, `OrderbookLevel`, `BidAskDistribution`, `PriceDistribution`) + that previously silently dropped unknown fields now preserve them on + `__pydantic_extra__`. +- **WS callbacks no longer suppress queue delivery.** Holding both an `@on()` + callback and an iterator on the same channel now sees both fire. A + WARNING logs at register-time so the change is visible to upgraders. + +See [CHANGELOG.md](https://github.com/TexasCoding/kalshi-python-sdk/blob/main/CHANGELOG.md) +for the full list including the WS recv-loop overhaul (5 reconnect races +fixed), URL/trade-data log-leak scrubs, and spec-sync supply-chain +hardening. + +--- + +## From `kalshi_python_async` + If you're coming from `kalshi_python_async` (the predecessor community client referenced in the project README), this page summarizes the v1 SDK differences you'll hit. **If you don't recognize that name, you can diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 35d99c8..6d58fdc 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -272,4 +272,4 @@ "WeeklySchedule", ] -__version__ = "1.1.0" +__version__ = "2.0.0" diff --git a/pyproject.toml b/pyproject.toml index ec5e2a1..66c47ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "1.1.0" +version = "2.0.0" description = "A professional Python SDK for the Kalshi prediction markets API" readme = "README.md" license = { text = "MIT" } diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 15891dd..c208a41 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -56,6 +56,32 @@ async def await_resource[T]( raise await asyncio.sleep(interval) + +def wait_until_not_found( + fetch: Callable[[], object], + *, + timeout: float = 15.0, + interval: float = 0.5, +) -> None: + """Poll ``fetch()`` until it raises KalshiNotFoundError. + + The inverse of :func:`wait_for_resource`: needed after a DELETE on demo, + where the query-exchange replica may still serve the pre-delete state + for ~10 seconds. Raises AssertionError if the resource never 404s. + """ + deadline = time.monotonic() + timeout + while True: + try: + fetch() + except KalshiNotFoundError: + return + if time.monotonic() >= deadline: + raise AssertionError( + "Resource did not 404 within timeout — demo may not have " + "propagated the delete, or the delete didn't take effect." + ) + time.sleep(interval) + logger = logging.getLogger(__name__) F = TypeVar("F", bound=Callable[..., Any]) diff --git a/tests/integration/test_order_groups.py b/tests/integration/test_order_groups.py index ee02f20..5ea91ba 100644 --- a/tests/integration/test_order_groups.py +++ b/tests/integration/test_order_groups.py @@ -18,7 +18,7 @@ from tests.integration.assertions import assert_model_fields from tests.integration.conftest import skip_if_low_balance from tests.integration.coverage_harness import register -from tests.integration.helpers import await_resource, wait_for_resource +from tests.integration.helpers import await_resource, wait_for_resource, wait_until_not_found logger = logging.getLogger(__name__) @@ -99,9 +99,9 @@ def test_delete( skip_if_low_balance(demo_balance_cents, threshold_cents=100) resp = sync_client.order_groups.create(contracts_limit=1) sync_client.order_groups.delete(resp.order_group_id) - # Follow-up GET should 404 - with pytest.raises(KalshiNotFoundError): - sync_client.order_groups.get(resp.order_group_id) + # Follow-up GET eventually 404s — demo's query-exchange lags writes, + # so poll for the 404 rather than asserting it immediately. + wait_until_not_found(lambda: sync_client.order_groups.get(resp.order_group_id)) @pytest.mark.integration From 911396e66cc8d7e09c7c11c746bb59385a8e83eb Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sun, 17 May 2026 14:24:59 -0500 Subject: [PATCH 2/2] release: drop orphaned KalshiNotFoundError import The release commit replaced `with pytest.raises(KalshiNotFoundError):` in test_order_groups.py::test_delete with the new wait_until_not_found helper, but left the import behind. CI's ruff caught F401. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/test_order_groups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_order_groups.py b/tests/integration/test_order_groups.py index 5ea91ba..e4f44af 100644 --- a/tests/integration/test_order_groups.py +++ b/tests/integration/test_order_groups.py @@ -9,7 +9,6 @@ from kalshi.async_client import AsyncKalshiClient from kalshi.client import KalshiClient -from kalshi.errors import KalshiNotFoundError from kalshi.models.order_groups import ( CreateOrderGroupResponse, GetOrderGroupResponse,