fix: spec drift — settlement_sources on Event + lifecycle strike fields (#451)#452
Conversation
The live v3.21.0 spec gained fields after #449 synced it, redding the nightly strict contract run (#451): - EventData.settlement_sources (spec-required, nullable) → Event.settlement_sources, typed NullableList[SettlementSource] so a JSON null coerces to []. Mirrors EventMetadata/Series. SettlementSource moved above Event so the forward ref resolves under `from __future__ import annotations`. event_dict() fixture gains the required key. - marketLifecycleV2Payload.{strike_type,cap_strike,custom_strike} → MarketLifecyclePayload (all optional; present only on metadata_updated). specs/openapi.yaml: the freshly-fetched live spec also *transiently* dropped CreateOrder / BatchCreateOrders / AmendOrder (104→101 ops), which would red the body-drift tests for those endpoints. That is an upstream publishing glitch, not a real API change — so the committed openapi was kept at its known-good 104-op state and settlement_sources added surgically to EventData. specs/asyncapi.yaml was synced normally (additive strike fields + WS error-doc refinements; no dropped channels). Verified: ruff clean, mypy clean, strict nightly contract suite (-W error::UserWarning) green, full unit suite 4315 passed / 3 skipped. Closes #451 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — fix/spec-drift-451Summary: Minimal, well-targeted spec-drift fix. Two new field groups are added faithfully against the live v3.21.0/asyncapi specs, contract tests pass (676 / 4315), and the decision to hold the openapi.yaml at the known-good 104-operation state rather than import a transient breakage is the right call and well-documented. ✅ What's done well
|
Addresses the PR #452 review (claude[bot]): add unit coverage for the new fields rather than relying on the contract suite alone. - TestEventSettlementSources: null -> [] coercion, typed SettlementSource list parsing, and required-key enforcement. - TestMarketLifecycleStrikeFields: the asyncapi 'between' example (strike_type + floor_strike + cap_strike + custom_strike) and the default-None path on non-metadata events. - Tighten the Event.settlement_sources comment: only Series mirrors the nullable shape; EventMetadata's field is a plain list (not nullable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — addressed in 1577e81. ① Missing unit tests — fixed. Added two test classes:
③ Comment precision — fixed. Reworded to "same nullable shape as ② No default = de-facto breaking — acknowledged. ④ Nit (comment length) — kept the 4-liner. The "between/greater/less → floor/cap interpretation" detail earns its space, and you flagged it as non-blocking. Local: ruff + mypy clean, new tests green, affected suites ( |
Code Review: PR 452 — Spec drift fix (settlement_sources + lifecycle strike fields)OverviewClean, targeted spec drift fix. Changes are minimal, well-explained, and consistent with existing SDK conventions. The PR description is thorough about the upstream publishing glitch; not importing the transiently-broken spec state was the right call. Positives
Issues1. Breaking change not flagged in changelog (medium)
Suggestion: add 2.
3. The spec documents 4.
Summary
Recommend merging after addressing item 1. Items 2–4 can be tracked as follow-up issues. Reviewed by Claude Sonnet 4.6 |
Minor bump covering the two changes merged since 4.1.0: - #452 (closes #451): additive response fields reconciling in-place upstream spec drift — Event.settlement_sources and WS MarketLifecyclePayload strike_type / cap_strike / custom_strike. - #448: cryptography dependency ceiling widened <49 -> <50. Additive-minor per the 4.1.0 precedent (MarginPosition.subaccount, also a spec-required response field, shipped in a minor with the same direct- construction note). No breaking changes. Bumps pyproject.toml + kalshi.__version__ to 4.2.0 and adds the CHANGELOG entry. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#472) * ci(spec-drift): stop weekly/daily issue spam — ack/snooze, auto-close, content titles The recurring #467 (nightly) / #470 (weekly) issues are two detectors on one real, deterministic upstream drift — but the reporting mechanics amplified the pain. Fix the mechanics without weakening the read-only security model. Nightly (spec-drift.yml): - Comment only when the failing-test-ID set changes (a fingerprint), not every failing night — a deterministic upstream produced identical spam indefinitely. - `spec-drift-ack` / `snoozed` label silences comments while a reconcile is in flight, without closing the tracker. - New report-success job auto-closes the tracker on a green scheduled run (the workflow previously only ever opened/commented). Weekly (spec-sync.yml): - Title leads with a content signal ([content-changed] + path delta + short fingerprint) instead of a bare version — Kalshi changes content in place without bumping info.version, so "3.23.0 → 3.23.0" read as a false alarm. - Supersede stale weekly snapshots so exactly one weekly drift issue stays open instead of one per upstream micro-revision. Scoped to spec-sync-bot so the nightly tracker is never touched. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(contracts): de-brittle drift suite + close false-negative gaps Direction 1 — less brittle (stop benign churn redding CI against a fast-moving, loosely-versioned upstream): - Additive drift split: a spec-REQUIRED field the SDK lacks still hard-fails (act now); a spec-OPTIONAL field becomes a non-blocking AdditiveOptionalDriftWarning. It deliberately subclasses Warning (not UserWarning) so the nightly `-W error::UserWarning` gate cannot promote benign optional drift to a red build. +3 regression tests lock the contract. - test_schema_coverage batches all unresolved schemas into one failure instead of failing on the first (one-at-a-time reruns on multi-removal drift). Direction 2 — close false-negatives (catch real drift the suite missed): - test_every_spec_endpoint_is_mapped asserts every spec OPERATION (core+perps+perps_scm) is in a METHOD_ENDPOINT_MAP, minus a reasoned _UNIMPLEMENTED_ENDPOINTS allowlist. Catches ADDED endpoints/operations the schema/path-count diff misses — verified against live 3.24.0, which had it caught the two new /margin/* endpoints AND a new GET on an existing /communications/rfqs/.../quotes path that the weekly path diff reported as "Added endpoints: (none)". - kwarg_rename exclusions are now spec-anchored (test_exclusion_map_is_current): the spec-side param must still exist for the endpoint, so a rename can't go stale and silently mask an upstream param removal/rename. Docstring updated to the actual post-#171/#172 behavior and to document why response-side field REMOVAL is intentionally NOT gated: the vendored "Kalshi Trade API Manual Endpoints" spec is a curated subset, so SDK response models are a superset by design; gating removals would red CI on every intentional SDK field. Request-BODY drift already gates removals (_check_model_drift), which is where a rename's dropped old name is caught. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat!: spec sync 3.23.0 → 3.24.0 (v7.0.0) — price_cents→price, announcements soft-deprecate Reconciles the vendored specs to upstream 3.24.0 and the hand-written SDK to match. Kalshi changed some content in place without bumping info.version, so this also absorbs edits made under the same string. BREAKING: - Position-transfer price is now fixed-point dollars (Decimal), not integer cents. `price_cents` → `price` (OrderPrice / DollarDecimal) on ApplySubaccountPositionTransferRequest, SubaccountTransfer, and transfer_position() (sync + async). Pass price=Decimal("0.50") for a 50¢ basis. The old le=100c cap is gone — OrderPrice guards non-negativity + the $0.0001 tick and leaves the upper bound to the server (matches CreateOrderRequest). Deprecated: - exchange.announcements() (sync + async) now emits DeprecationWarning. Upstream removed GET /exchange/announcements + the Announcement schema in 3.24.0 (404s live). The method + model are RETAINED pending confirmation the removal is permanent (transient-drop precedent: #452). Contract bindings dropped; _SOFT_DEPRECATED_MODELS / soft_deprecated allowlists keep the completeness suites green without re-mapping a gone endpoint. Added: - SubaccountNettingConfig.exchange_index (int, required) - portfolio.balance() optional exchange_index query param (sync + async) - ObligationEntry.asset_class (perps SCM / Klear; Literal["Crypto"], required) Deferred to follow-up feature work (allowlisted in _UNIMPLEMENTED_ENDPOINTS): - GET /communications/rfqs/{rfq_id}/quotes/{quote_id} - GET /margin/active_obligations - GET /margin/settlement_estimate_by_asset_class Verified: full suite 4142 passed / 311 skipped, ruff clean, mypy kalshi/ clean. Closes #467 Closes #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(perps,communications): implement the 3 new 3.24.0 endpoints Follow-up within the 3.23.0 → 3.24.0 reconcile: implement the endpoints that were recognized-but-deferred in _UNIMPLEMENTED_ENDPOINTS. - communications.quotes.get_for_rfq(rfq_id, quote_id) (sync + async) — GET /communications/rfqs/{rfq_id}/quotes/{quote_id} -> GetQuoteResponse (RFQ-scoped variant of the flat quotes.get). - klear.margin.active_obligations() (sync + async) — GET /margin/active_obligations -> GetActiveMarginObligationsResponse (plural sibling of active_obligation()). - klear.margin.settlement_estimate_by_asset_class() (sync + async) — GET /margin/settlement_estimate_by_asset_class -> GetSettlementEstimateByAssetClassResponse + AssetClassSettlementEstimate. ObligationEntry.asset_class refactored onto the shared AssetClassLiteral. Wired into METHOD_ENDPOINT_MAP / PERPS_SCM_* maps + CONTRACT_MAP; removed from _UNIMPLEMENTED_ENDPOINTS; +20 unit tests. Full suite 4162 passed / 311 skipped, ruff + mypy kalshi/ clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Resolves the nightly strict contract failures in #451. The live OpenAPI/AsyncAPI specs gained fields after #449 synced them, redding
pytest tests/test_contracts.py -W error::UserWarning:EventDatasettlement_sources(required, nullable)kalshi.models.events.EventmarketLifecycleV2Payloadstrike_type,cap_strike,custom_strikekalshi.ws.models.market_lifecycle.MarketLifecyclePayloadChanges
Event.settlement_sources→NullableList[SettlementSource]. Spec marks itrequired+nullable: true, so the SDK field is required (passestest_required_drift) but coerces a JSONnull→[](matchesEventMetadata.settlement_sources/Series.settlement_sources).SettlementSourcewas moved aboveEventso the forward reference resolves underfrom __future__ import annotations. The centralevent_dict()fixture gains the now-required key.MarketLifecyclePayloadgainsstrike_type: str | None,cap_strike: DollarDecimal | None,custom_strike: dict | None— all optional (present only onmetadata_updated), mirroring the existingfloor_strikehandling.Spec handling note⚠️
The freshly-fetched live
openapi.yamlalso transiently droppedCreateOrder/BatchCreateOrders/AmendOrder(104 → 101 operations) — this reds the body-drift tests for those endpoints (KeyError: path '…/amend' not found). That is an upstream publishing glitch, not a real API change, so I did not import it. The committedopenapi.yamlwas kept at its known-good 104-operation state andsettlement_sourceswas added toEventDatasurgically.asyncapi.yamlwas synced normally (additive strike fields + WS error-doc refinements; no dropped channels).If a future nightly still reds on the order-endpoint drop, that's a separate upstream-flap issue (the drift workflow may want to tolerate transient operation removals).
Verification
uv run ruff check .— cleanuv run mypy kalshi/— cleanuv run pytest tests/test_contracts.py -W error::UserWarning(nightly strict) — 676 passeduv run pytest tests/ --ignore=tests/integration— 4315 passed, 3 skippedCloses #451
🤖 Generated with Claude Code