test(contracts): map remaining 42 REST sub-models + promote completeness gate (#171)#181
Conversation
…ess gate (#171) Maps all REST sub-models, V2 orders family, and internal containers into CONTRACT_MAP (49 → 91 entries). Promotes test_contract_map_completeness from warnings.warn to pytest.fail so future unmapped models block CI. Adds dotted-path syntax to _get_schema_fields / _get_required_fields (\`Parent.field.items\`) for inline-object schemas the spec doesn't name at the top level (Batch*OrdersV2* per-entry shapes). Mapping the new models surfaced drift that was previously invisible. Tightened (per #172 policy): - BidAskDistribution (4 OHLC fields) - Candlestick (6 fields) - MarketMetadata (2 fields) - Schedule + WeeklySchedule - PositionsResponse - EventCandlesticks + ForecastPercentilesPoint - AssociatedEvent + LookupPoint (multivariate) PriceDistribution gains 4 v3.18.0 spec fields (mean/previous/min/max), all optional per spec (no required-drift, just additive). OrderbookLevel maps to spec's PriceLevelDollarsCountFp (a positional 2-tuple). _get_schema_fields returns {} for array-typed schemas, so the field-by-field drift checks skip cleanly. 3 new fixture builders (candlestick_dict, bid_ask_distribution_dict, price_distribution_dict). 6 test files migrated to use them. Closes #171
Code Review — PR #181: Contract map completeness + hard-fail gateOverall: Solid infrastructure PR with clear motivation. The hard-fail promotion is the right call, and the dotted-path syntax for inline schemas is a clean solution. A few nits and one structural inconsistency worth addressing before merge. What this does well
Issues1.
# tests/test_contracts.py
if "." in schema_name:
schema = _resolve_schema(spec, schema_name)
else:
schema = schemas.get(schema_name, {}) # ← silently returns {} if missingFor non-dotted names, a missing or misspelled schema silently returns an empty required set instead of failing. Since def _get_required_fields(spec, schema_name):
schema = _resolve_schema(spec, schema_name)
...2. Tautological assertion in After # tests/test_models.py ~line 766
assert c.yes_bid is not NonePydantic won't populate 3. Import ordering in
# current
from tests._model_fixtures import (
create_order_group_response_dict,
candlestick_dict, # ← should be first
...Observations (not blocking)
SummaryThe hard-fail promotion and fixture builder migration are both CI-safety wins. The |
- ruff I001: sort \`candlestick_dict\` alphabetically in test_models.py
and test_series_models.py import blocks. Project lints I001 in CI;
the unsorted insert broke test (3.13). Mechanical fix.
- _get_required_fields now goes through _resolve_schema unconditionally
(was special-casing dotted paths only, with silent fallback to {}
for missing plain names). Matches _get_schema_fields behavior and
removes the latent silent-pass case.
- Drop tautological \`assert c.yes_bid is not None\` /
\`assert c.price is not None\` from test_candlestick_nested_structure.
Both fields became required (non-nullable) in this PR, so the
is-not-None checks were always true and assert nothing now. The
downstream attribute assertions (\`c.yes_bid.open == …\`) still
exercise the structure.
|
Addressed in d858d90: CI failure (lint I001) — fixed. 1. 2. Tautological 3. Import order in test_models.py — covered by the CI ruff fix above. Non-blocking observations not actioned:
Verification: full unit suite still |
Code Review — PR #181: Contract-map completeness + hard-fail gateOverall this is a clean, well-scoped infrastructure PR. The motivation is solid: unmapped models are invisible to drift checks, and the What's Good
IssuesMinor bug: stale
|
| Area | Status |
|---|---|
Infrastructure (_resolve_schema, dotted-path) |
✅ Clean |
| Hard-fail promotion | ✅ Correct |
42 new CONTRACT_MAP entries |
✅ Well-mapped |
| Model field tightening | ✅ Spec-aligned |
| Fixture builders | ✅ Good pattern |
Stale is not None assertion |
|
price_distribution_dict body |
|
| CHANGELOG whitespace | nit |
The stale assertion is the only change worth making before merge. Everything else is either a nit or a verification question.
- Drop 7 more tautological \`is not None\` assertions across
test_async_markets.py, test_historical.py, and test_markets.py.
Same shape as the round-1 cleanup in test_models.py; the bot caught
the ones I missed.
- Rewrite _resolve_schema's path walk to distinguish a missing-key
failure from a legitimately empty schema. Was using \`.get(segment, {})\`
defaults followed by \`if not node\`, which would false-fail on any
spec schema that happens to be \`{}\` (unusual but allowed). Now uses
explicit \`in\` checks and emits a specific message per failure mode.
- Document price_distribution_dict's intentionally empty base.
- Drop the double blank line between #171 and #172 changelog sections.
|
Round 2 addressed in 1736b7c: Stale
CHANGELOG double blank line — fixed. Verification questions ( Verification: full unit suite still |
Code Review — PR #181: Contract-map completeness (#171)Overall: This is solid, well-scoped infrastructure work. The approach is correct, the execution is clean, and the PR description matches the diff faithfully. A few things worth noting below. What's done well
Issues / things to verify1. # exchange.py
monday: NullableList[DailySchedule] # was = []
tuesday: NullableList[DailySchedule] # was = []
...
2. # multivariate.py
is_yes_only: bool # was: bool = FalseA bool default of 3.
4. def price_distribution_dict(**overrides: Any) -> dict[str, Any]:
base: dict[str, Any] = {}
base.update(overrides)
return baseSemantically correct (all fields are optional), but future devs calling 5. One test still builds a partial candlestick without In candlestick_dict(
end_period_ts=1700000000,
yes_bid={...},
volume_fp="500.00",
)
Minor nit# test_contracts.py, updated docstring
# "Hard-fails via ``pytest.fail``. See the docstring of
# tests/_contract_support.py for the broader rationale; as of #172/#171
# every contract gate in this file is now hard-fail."The No concerns with
Verdict: Approve with the above notes. Issue 2 (bool tightening) and issue 1 (WeeklySchedule) are the only ones worth a quick sanity check before merge; the rest are informational. |
Round-2 bot review on PR #184: 1. matched_field_info lookup missed validation_alias / AliasChoices. My round-1 fix only checked serialization_alias-or-field-name (same set _model_aliases returns). If a future server_omits_despite_required entry keys by a spec wire name accepted only via validation_alias, the lookup returned None and falsely reported the entry as stale. Swapped the bespoke comprehension for _extract_spec_names, which is the project's canonical wire-name resolver and walks AliasChoices, str validation_alias, and serialization_alias exhaustively. 2. EXCLUSIONS docstring required "demo+prod" citation; reality is we don't have prod API access from CI. Softened to "at least one verified server observation (demo nightly run ID, or a prod-traffic capture date)" with "Prod confirmation is preferred but not always achievable from CI". Not actioned: - "market_details should be NullableList = [] for defense-in-depth" declined. Per #172 policy, tightened NullableList fields don't carry defaults (matches Series.tags, Milestone.related_event_tickers). The bot's reasoning ("server might start omitting the key later") is speculative; we react to real failures via EXCLUSIONS, not hypothetical ones. The "key present, value null" failure mode is fixed; the hypothetical "key missing entirely" mode would surface in the next nightly and get its own EXCLUSIONS entry. - "Multi-line test docstrings violate CLAUDE.md" - the cited rule ("multi-line comment blocks - one short line max") doesn't exist in the project's CLAUDE.md or AGENTS.md. Same fabrication the bot raised on PR #181. Not changing.
…179) Closes #179. Pre-release docs audit before cutting the next release. Findings compiled from a six-way parallel agent audit of disjoint doc-file partitions, triaged, ground-truthed against the actual code/spec, then applied centrally. ## Changes **`ROADMAP.md`** (the #179 core) - "Open trackers" dropped (was: #45, #53, #106). #45 and #53 are closed; #106 was a PR whose remaining sub-items all shipped post-v2.2.0. - "Next milestone" carry-overs that landed (`MessageQueue` `maxlen` via #173, `_coerce_decimal` via #174, WS UX foot-guns via #175/#176/#177, CONTRACT_MAP completeness via #181) removed. - Added an "Unreleased (post-v2.2.0)" bullet under "Shipped". **`docs/resources/multivariate.md`** - Quick-reference table claimed `lookup_history` hits `/lookup_history`. Actual code uses `/lookup` with a `lookback_seconds` query param. - Example called `hist.lookups` on the return value, but `lookup_history` returns `list[LookupPoint]` directly. Rewritten to iterate the list with real field names. **`docs/index.md`** - "85 endpoints" → "98 operations" against current spec; WebSocket-async-only exception called out on the sync/async parity bullet. **`docs/websockets.md`** - New "Resubscribe-window frame stashing" subsection documenting the #176 mechanism, `stash_maxlen` bound (internal, fixed at 1000), per-sid per-cycle WARNING dedup, and the per-sub-failure drop case. **`docs/authentication.md`** - New "Async RSA-PSS sign offload" subsection documenting `KalshiAuth.sign_request_async()` (#178), the dedicated 2-worker `ThreadPoolExecutor` lifecycle, and the `close()` chain. **`docs/configuration.md`** - New "Lifecycle" section with both sync `try/finally: client.close()` and async `async with` examples. **`docs/resources/events.md`** - Note documenting `Event.product_metadata` and `EventMetadata.market_details` server-omission handling from #183 (plain-English coercion behavior, not the internal `NullableList` type name). **`README.md`** - WS quickstart uses package-level `from kalshi.ws import KalshiWebSocket`. Channel list clarifies 11/13 channels have dedicated `subscribe_*` methods; the remaining two ride the generic `subscribe()` escape hatch. - Endpoint count updated to "98 operations". **`CLAUDE.md`** - Endpoint count synced to "98 operations". ## Review iteration Five rounds of bot review. Real items addressed each round: 1. `CLAUDE.md` endpoint count out of sync; `docs/resources/events.md` missing blank line before `## Reference` 2. ROADMAP tombstone prose trimmed (would decay by next release) 3. Lifecycle async parity — `AsyncKalshiClient` `async with` example added 4. Stash WARNING dedup wording aligned to `set[int]` implementation 5. `NullableList` replaced with plain-English coercion behavior in user-facing docs; `stash_maxlen` configurability clarified as internal-only Not actioned: CHANGELOG `(#179)` → `(#188)` (bot had convention backwards — existing CHANGELOG entries use issue numbers, verified via `closedByPullRequestsReferences`); "98 operations" recount disclaimer (delta is mixed real-additions + tighter count, either framing would be misleading). ## Verification - `mkdocs build --strict` clean (zero warnings on internal links or missing pages) - `ruff check .` clean - `mypy kalshi/` strict clean - All CI checks green: `test (3.12)`, `test (3.13)`, `claude-review`, `drift-check` - Pure docs/markdown change, no Python source touched After this lands, `Unreleased` contains the WS reliability + auth polish batch (#173–#178, #183) plus this docs sweep — ready to cut a release.
WS reliability + auth polish batch on top of v2.2.0's spec-required tightening. See CHANGELOG.md for the full notes; high points: * WS resubscribe-window frame stashing — per-sid bounded stash closes a silent message-loss window during reconnect bursts (#176) * run_forever(stop_event=...) cooperative shutdown for SIGINT (#177) * run_forever() now raises KalshiSubscriptionError instead of silently returning when no subscribe_* has landed (#175) * Async RSA-PSS sign offload onto a dedicated 2-worker executor so signs don't queue behind getaddrinfo during reconnect storms (#178) * 226 spec-required fields tightened to non-optional with hard-fail drift gates (#172 via #180) * All 49→91 REST contract-map entries; completeness gate hard-fails (#171 via #181) * First two server_omits_despite_required exclusions for fields the live demo omits (#183) * MessageQueue maxlen defense-in-depth (#173) * _to_decimal_* consolidation into _coerce_decimal (#174) * Pre-release docs audit sweep across the full mkdocs site (#179) Soft-breaking at the response-parse boundary only (per #172): server omission of a previously-optional spec-required field now raises pydantic.ValidationError instead of silently producing field=None. Wire format unchanged. Verification: 2126 unit tests pass, ruff + mypy strict clean, mkdocs --strict clean.
Closes #171.
Summary
Maps the remaining 42 SDK response/request models into
CONTRACT_MAP, taking it from 49 → 91 entries. Promotestest_contract_map_completenessfromwarnings.warntopytest.failso the next unmapped model fails CI loudly.This closes the same regression class that hid
Balance.balance_dollarsfor five review rounds in v2.1.0 — an unmapped model is invisible to drift checks, so additive spec changes slide through silently. The WS side has been hard-fail since #157; this brings the REST side to parity.Acceptance (per #171)
len(CONTRACT_MAP)increases from 49 to 91. ✅ (exactly the issue's "~91" target)pytest tests/test_contracts.py -W error::Warning -k test_contract_map_completenesspasses. ✅warnings.warn(...)is replaced withpytest.fail(...). ✅test_additive_driftregression-free. ✅ (one finding addressed by adding 4 spec fields toPriceDistribution)What changed
Infrastructure
_get_schema_fields/_get_required_fieldsgain a dotted-path syntax for inline schemas the spec doesn't name at the top level.BatchCancelOrdersV2Request.orders.itemsresolves to the per-entry inline object inside the batch wrapper. Used for 3 V2 batch wrapper entries (Batch{Cancel,Create}OrdersV2{Request,Response}per-entry shapes)._resolve_schemahelper centralizes schema lookup; returns{}for array-typed spec schemas so field-by-field drift checks skip them cleanly. Used for theOrderbookLevel↔PriceLevelDollarsCountFpmapping.42 new
CONTRACT_MAPentriesmarketsBidAskDistribution,Candlestick,OrderbookLevel,PriceDistributionorders*V2*request/response + 3 inline batch entrieseventsMarketMetadata,SettlementSourceexchangeSchedule,DailySchedule,WeeklySchedule,MaintenanceWindowportfolioDeposit,Withdrawal,IndexedBalance,PositionsResponseseriesEventCandlesticks,PercentilePoint,ForecastPercentilesPointmultivariateAssociatedEvent,LookupPoint,Create/LookupTickersrequest/response pairs5 entries use renamed spec names (
Candlestick→MarketCandlestick,PositionsResponse→GetPositionsResponse, etc.); 3 use the new dotted-path syntax for inline schemas; 1 (OrderbookLevel) maps to an array-typed spec schema.Drift surfaced by mapping (tightened per #172 policy)
Mapping these models for the first time made
test_required_driftandtest_additive_driftsee them — and they immediately caught real drift. Tightened to required (no default):BidAskDistribution:open,high,low,closeCandlestick:yes_bid,yes_ask,price,volume,open_interest,end_period_tsMarketMetadata:image_url,color_codeSchedule:standard_hours,maintenance_windowsWeeklySchedule: all 7 day-of-week fieldsPositionsResponse:market_positions,event_positionsEventCandlesticks:market_tickers,market_candlesticks,adjusted_end_tsForecastPercentilesPoint:percentile_pointsAssociatedEvent:is_yes_only,active_quotersLookupPoint:selected_markets,last_queried_tsAdded (additive drift in
PriceDistribution):mean,previous,min,max(_dollarswire form, all optional per spec — candlestick window may contain no trades)Test fixtures
3 new builders in
tests/_model_fixtures.py:candlestick_dict— spec-completeCandlestickwire dictbid_ask_distribution_dict— OHLC sub-dict forCandlestick.yes_bid/.yes_askprice_distribution_dict—Candlestick.price(all fields optional)6 tests migrated to use them (
test_historical.py,test_markets.py,test_models.py,test_series.py,test_series_models.py).Verification
uv run pytest tests/ --ignore=tests/integration→ 2107 passed, 0 warnings.uv run pytest tests/test_contracts.py -W error::Warning→ 459 passed, gate is live.ruff checkclean on every file touched by this PR.Soft-breaking footprint
Same shape as #172: callers who construct
Candlestick/BidAskDistribution/MarketMetadata/Schedule/WeeklySchedule/PositionsResponse/EventCandlesticks/ForecastPercentilesPoint/AssociatedEvent/LookupPointdirectly with partial values will now hitValidationError. Common-case real construction is viamodel_validate(server_json), which is unaffected. Test fixtures should use the new_model_fixturesbuilders.Out of scope
OrderbookLevel↔ positional-tuple semantic gap (SDK uses object, spec uses 2-tuple) is documented in theContractEntrynotes but not "fixed" — the SDK's object shape is the more useful ergonomic. Worth a separate discussion if we want to harmonize, but the drift gate now correctly skips the field-by-field check on this one.