polish(models): uniform extra="allow" on all response models (#114)#143
Conversation
Pre-PR audit of all 102 exported model classes:
- 80 response models already on `extra="allow"`
- 17 request bodies on `extra="forbid"` (per the v0.8.0+ convention)
- 5 response models falling back to Pydantic's default `extra="ignore"`:
Page, Orderbook, OrderbookLevel, BidAskDistribution, PriceDistribution
Added `model_config = {"extra": "allow"}` to all 5 so the SDK behaves
uniformly when Kalshi adds new fields: unknown fields preserved on
`__pydantic_extra__` rather than silently dropped.
Drift guard: tests/test_model_extra_policy.py iterates
kalshi.models.__all__ and parametrizes two asserts — every response
model must use `extra="allow"`, every `*Request` body must use
`extra="forbid"`. A future model added without the right policy fails
at collect time.
Behavior change called out in CHANGELOG under [Unreleased] → Changed:
instances of the 5 affected classes will now preserve unknown fields
where they previously discarded them.
Closes #114
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8259139 to
62202c6
Compare
Code Review — PR #143:
|
Code Review — PR #143: uniform
|
Per two-pass bot review on #143: - Page model_config moved adjacent to fields (was between fields and @Property block — visual inconsistency with sibling models). - BidAskDistribution + PriceDistribution model_config key order flipped to {"extra": ..., "populate_by_name": ...} matching Market and Candlestick siblings in the same file. - test_model_extra_policy.py: * Added ids= to both parametrize decorators so pytest -v output reads test_response_models_use_extra_allow[Market] instead of [name0-cls0]. * Cached _ALL_MODELS / _RESPONSE_MODELS / _REQUEST_MODELS at module scope (was evaluating _exported_model_classes() twice at import). * Added a comment explaining why _REQUEST_BODY_SUFFIXES needs both "Request" and "RequestOrder" — BatchCancelOrdersRequestOrder doesn't end in plain "Request" (bot was incorrect that "RequestOrder" was unused; this comment prevents the next reviewer from chasing it). * Added a docstring on _exported_model_classes noting the __all__ scope assumption. Skipped (justified): - WS envelope classes (kalshi/ws/models/) are out of scope per the issue; separate audit if we want the policy applied there too. Verify: tests/test_model_extra_policy.py 102 passed. ruff + mypy --strict clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Both reviews addressed in latest commit: From bot #1:
From bot #2:
Verify: |
Code Review — PR #143: Uniform
|
* release: v2.0.0 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + WS payloads + flip warn\u2192fail (#161, #162, #163) (#168) * feat(models): backfill v3.18.0 fields on communications + order_groups (#161) PR3 of the response-side spec-drift hardening stack. 8 new optional fields across 5 mid-tier response models. Eliminates 8 additive-drift warning bullets and clears 5 test_additive_drift cases. Fields added ------------ `RFQ` (+1): creator_subaccount `Quote` (+3): creator_subaccount, rfq_creator_subaccount, post_only `OrderGroup` (+1): exchange_index `GetOrderGroupResponse` (+1): exchange_index `CreateOrderGroupResponse` (+2): subaccount, exchange_index Design decisions ---------------- - All new fields are `int | None = None` (or `bool | None` for post_only), matching the spec types exactly. - post_only mirrors CreateQuoteRequest.post_only (already on request side from v2.1.0) — server echoes when the caller is the quote creator. - subaccount/exchange_index on CreateOrderGroupResponse echoes the routing context the server resolved. - All new fields appended at end of each model with `# v3.18.0 backfill (#161)` marker. Matches PR1/PR2 pattern. Tests ----- 3 new test classes in tests/test_models.py (8 tests). Hoisted RFQ/Quote/ OrderGroup/* to module-level imports. Annotated _MINIMAL dict on TestQuoteV3180Fields as `ClassVar[dict[str, str]]` per ruff RUF012. Closes #161. * feat(ws): backfill v0.14+ fields on WebSocket payloads (#162) PR4 of the response-side spec-drift hardening stack. 33 new optional fields across 11 WebSocket payload models. Clears all 11 test_ws_additive_drift cases. Key themes ---------- 1. **`*_ts_ms` Unix-ms timestamps** — spec promotes ms integer as the primary form (TickerPayload.ts_ms, OrderbookDeltaPayload.ts_ms, TradePayload.ts_ms, FillPayload.ts_ms, OrderGroupPayload.ts_ms, UserOrdersPayload.{created,last_updated,expiration}_ts_ms). Existing `ts: int|str|datetime` fields stay for back-compat. NO auto-conversion — surface both so latency-sensitive callers get the int form directly. 2. **Direction encoding** — outcome_side / book_side / taker_outcome_side / taker_book_side on FillPayload, TradePayload, UserOrdersPayload. Mirrors the REST-side backfill from PR1 (#159). Reuses SideLiteral / BookSideLiteral from kalshi.models.orders. 3. **MVE + RFQ context echoes** on Communications payloads — mve_collection_ticker, mve_selected_legs, contracts_fp, target_cost_dollars, yes_contracts_offered_fp, no_contracts_offered_fp, rfq_target_cost_dollars across RfqCreatedPayload, RfqDeletedPayload, QuoteCreatedPayload, QuoteAcceptedPayload. Field counts per file --------------------- ticker.py +2: price, ts_ms fill.py +3: outcome_side, book_side, ts_ms orderbook_delta.py +1: ts_ms trade.py +3: taker_outcome_side, taker_book_side, ts_ms user_orders.py +6: outcome_side, book_side, self_trade_prevention_type, created_ts_ms, last_updated_ts_ms, expiration_ts_ms market_lifecycle.py +4: additional_metadata, floor_strike, price_level_structure, yes_sub_title order_group.py +1: ts_ms communications.py +13: across 4 payload classes (RFQ + Quote echoes) Design decisions ---------------- - `ts_ms` is plain `int | None`, NOT datetime — spec gives ms since epoch as an integer. Do NOT auto-convert. - `MarketLifecyclePayload.floor_strike` typed as `Decimal | None` (spec: `type: number, format: double`) — matches the REST Market.floor_strike precedent rather than the issue's `str | None` text (which contradicted the spec). - `additional_metadata` typed as `dict[str, Any] | None` — spec says object with subproperties. - `mve_selected_legs` typed as `list[dict[str, object]] | None` — matches CommunicationsMessage.msg's existing `dict[str, object]` pattern, avoids the `typing.Any` import. - WS communications additions use SDK short names with `validation_alias` to match the existing per-payload pattern (e.g. `contracts` aliased to `contracts_fp`/`contracts`) — keeps sibling consistency. Tests ----- One new test class `TestWsV0140Backfill` in tests/ws/test_models.py with 11 tests — one per payload covering at least one new field. Defaults-to- None paths are already exercised by the existing per-payload `*_minimal` tests. Added `QuoteCreatedPayload` to the test_models imports. Closes #162. * test(infra): promote additive drift to hard-fail; register ErrorPayload; WS envelopes extra=allow (#163) PR5 of the response-side spec-drift hardening stack. With backfills from #159–#162 landed, additive-drift warnings are at zero. Flip `warnings.warn(...)` → `pytest.fail(...)` so future spec syncs that introduce unmodelled fields fail CI instead of slipping through five rounds of review (the v2.1.0 `Balance.balance_dollars` failure mode). Also closes two ROADMAP carry-overs: 1. Register `kalshi.ws.models.base.ErrorPayload` in `WS_CONTRACT_MAP` (spec name: `errorResponsePayload`). Clears the WS completeness warning so it can be promoted. 2. Apply `model_config = {"extra": "allow"}` to every WS envelope and helper class (14 classes across 9 files). PR #143 covered payloads; this completes the WS surface. Test `test_ws_models_use_extra_allow` (new) iterates every BaseModel in `kalshi.ws.models.*` and pins the policy. Changes ------- tests/test_contracts.py: - Flip 3 warning sites to `pytest.fail`: - TestSpecDrift.test_additive_drift - TestWsSpecDrift.test_ws_additive_drift - TestWsSpecDrift.test_ws_contract_map_completeness - REST `test_contract_map_completeness` stays warn-only (the issue expected it to pass, but 42 pre-existing sub-models / V2 family classes / internal containers remain unmapped — out of scope for this PR; tracked for a follow-up). - Required-drift and required-ws-drift stay as warnings (issue explicitly defers as separate ≈204-entry policy decision). - Update module docstring to reflect new FAIL semantics. kalshi/_contract_map.py: - Add ContractEntry for ErrorPayload → errorResponsePayload. - Drop `ErrorPayload` from the intentionally-excluded comment. kalshi/ws/models/*.py: - base.py: add extra=allow on SubscriptionInfo, ErrorPayload, SubscribedMessage, UnsubscribedMessage, ErrorMessage. - 8 per-channel files: add extra=allow on every *Message envelope plus SelectedMarket helper in multivariate.py. tests/test_model_extra_policy.py: - Add `test_ws_models_use_extra_allow` parameterized across every BaseModel discovered in `kalshi.ws.models.*` modules. Pins the policy so future model additions that forget the config fail CI. CHANGELOG.md: - New Unreleased section summarizing the full drift hardening stack (#159–#163). Verification ------------ uv run pytest tests/ --ignore=tests/integration -q -> 2023 passed, 35 warnings (all required-drift, kept warn-only). Up from 1965 baseline (+58 new tests: PR3 +8, PR4 +11, PR5 +38 extra-policy entries + +1 net for hoisted imports/refactors). uv run mypy kalshi/ -> Success: no issues uv run ruff check . -> All checks passed! Smoke-tested both flips: - Delete Market.exchange_index locally -> test_additive_drift[Market] now FAILS with pytest.fail (was a UserWarning). Exit 1, message: "Additive drift in kalshi.models.markets.Market: Spec field 'exchange_index' has no SDK mapping". - Delete ErrorPayload from WS_CONTRACT_MAP locally -> test_ws_contract_map_completeness FAILS with pytest.fail. Exit 1, message: "WS payload models without contract map entries: kalshi.ws.models.base.ErrorPayload". Both reverted after smoke test. Closes #163.
Summary
Audit + fix for #114 — make response models forwards-compatible by default.
Audit findings
extra="allow"extra="forbid"(request bodies)extra="ignore"(implicit Pydantic default)The 5 outliers:
Page,Orderbook,OrderbookLevel,BidAskDistribution,PriceDistribution. All gainedmodel_config = {"extra": "allow"}(preserving the existingpopulate_by_name=Truewhere present).Behavior change
Instances of those 5 classes will now preserve unknown fields on
__pydantic_extra__rather than silently dropping them. Called out inCHANGELOG.md [Unreleased] → Changed.Drift guard
New
tests/test_model_extra_policy.pyparametrizes overkalshi.models.__all__:extra="allow"(everyBaseModelnot ending inRequest/RequestOrder)extra="forbid"A future model added with the wrong policy fails at collect time.
Closes #114
Test plan
uv run ruff check .cleanuv run mypy kalshi/clean (strict)