perps: typed list-response envelopes for REQUIRED-array endpoints (part of #404)#418
Conversation
…rt of #404) Addresses the perps half of #404 (prediction-API parity is a separate follow-up PR; #404 stays open). 3.2.0 is unreleased, so this is pre-release hardening. The perps non-paginated list endpoints extracted their array with `data.get(key) or []`, silently coercing a missing/null REQUIRED array to [] and masking server-side spec drift. Now each validates a typed envelope whose array field is non-optional, so a missing/null required array raises ValidationError: - markets: `GetMarginMarketsResponse{markets}`, `GetMarginMarketCandlesticksResponse{ticker, candlesticks}` (the envelope also validates the previously-discarded required `ticker`). - funding: `GetMarginHistoricalFundingRatesResponse{funding_rates}`, `GetMarginFundingHistoryResponse{funding_history}`. `order_groups.list` is spec-OPTIONAL, so it STAYS tolerant — switched from `.get(key, [])` (which missed an explicit null) to `.get(key) or []` so a missing OR null array yields []. All 4 envelopes registered in `PERPS_CONTRACT_MAP` (drift-checked). Tests updated to assert the required arrays hard-fail on missing/null, the optional order_groups tolerates both, and candlesticks rejects a missing ticker. mypy strict + ruff + 1140 perps/contract tests green.
Code Review — perps typed list-response envelopes (#418)Overall: Clean, well-scoped fix. The core approach (Pydantic envelope → non-optional field → hard-fail on missing/null) is correct and consistent with how other response models in this codebase are structured. The Correctness
Issues1. Docstrings violate CLAUDE.md style (minor) CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." All four new envelope models have 3–4 line docstrings. The explanatory content is good — it just belongs in the commit message or PR description, not inline. Suggest trimming to a single line, e.g.: class GetMarginMarketsResponse(BaseModel):
"""Envelope for GET /margin/markets; ``markets`` is spec-required."""
markets: builtins.list[MarginMarket]
model_config = {"extra": "allow"}2. Async paths not covered by the new hard-fail tests (minor)
3. Pydantic When the new envelopes reject a malformed response, callers receive a raw 4. Observation:
5. New envelope models not exported from
TestsThe test updates are well-structured:
The only gap is async coverage noted in item 2 above. SummaryThe logic is sound and the diff is minimal relative to what it fixes. The two actionable items are the docstring length (CLAUDE.md violation) and async test gaps. Items 3–5 are observations for follow-ups, not blockers. Happy to approve once docstrings are trimmed. |
…points (closes #404) (#419) Completes #404 (the perps half shipped in #418). The prediction-API list endpoints extracted their REQUIRED array with `data.get(key, [])`, silently masking spec drift on a missing key and TypeError-ing on a null array. Each now validates a typed envelope whose array field is `NullableList`: - markets.candlesticks -> GetMarketCandlesticksResponse{ticker, candlesticks} - markets.bulk_candlesticks -> BatchGetMarketCandlesticksResponse{markets} - markets.bulk_orderbooks -> GetMarketOrderbooksResponse{orderbooks} `NullableList` is the right model for the released API: a MISSING key hard-fails (ValidationError — surfaces drift, the #404 goal), while a NULL array coerces to [] (Kalshi's documented empty-as-null convention; the prior extraction would TypeError on null). The candlesticks envelope also validates the previously-discarded required `ticker`. All 3 envelopes registered in CONTRACT_MAP (drift-checked). Tests: missing key raises + null -> [] for all three (sync + async); candlesticks rejects a missing ticker. CHANGELOG "Changed" note added. mypy strict + ruff + full suite (3660) green. Note: the perps envelopes (#418) used a non-nullable list (hard-fail on null) per the issue's strict criteria; aligning them to NullableList — restoring the pre-#418 `or []` null tolerance — is a reasonable small follow-up.
…t) (#420) Follow-up to #418/#419. The perps list-response envelopes (markets/funding) used a non-nullable list — hard-failing on a null array per #404's strict wording — while the prediction envelopes (#419) use NullableList (null -> []). This restores the pre-#418 `or []` null tolerance and makes null-handling identical across both surfaces. `GetMarginMarketsResponse.markets`, `GetMarginMarketCandlesticksResponse.candlesticks`, `GetMarginHistoricalFundingRatesResponse.funding_rates`, and `GetMarginFundingHistoryResponse.funding_history` now use NullableList: a MISSING key still hard-fails (surfacing drift), a NULL array coerces to []. 3.2.0 is unreleased, so this refines the envelopes before release. Removed the now-unused `import builtins` from both model files. Tests updated: null -> [] instead of raising. mypy strict + ruff + 1140 perps/contract tests green.
Addresses the perps half of #404. The prediction-API parity is a focused follow-up PR (it changes released-code behavior), so this PR does not close #404 — it stays open for that part.
What
Perps non-paginated list endpoints extracted their array with
data.get(key) or [], silently coercing a missing/null REQUIRED array to[]and masking spec drift. Each now validates a typed envelope with a non-optional array field, so a missing/null required array raisesValidationError:markets.listGetMarginMarketsResponse{markets}markets.candlesticksGetMarginMarketCandlesticksResponse{ticker, candlesticks}— also validates the previously-discarded requiredtickerfunding.historical_ratesGetMarginHistoricalFundingRatesResponse{funding_rates}funding.historyGetMarginFundingHistoryResponse{funding_history}order_groups.listis spec-OPTIONAL, so it stays tolerant — switched from.get(key, [])(which missed an explicitnull) to.get(key) or [], so missing or null →[].Verification
PERPS_CONTRACT_MAPand pass the drift harness.order_groupstolerates both; candlesticks rejects a missingticker.mypy --strict+ruffclean; 1140 perps + contract-drift tests pass.Remaining for #404
Prediction-API parity (
kalshi/resources/markets.pycandlesticks/bulk_candlesticks/bulk_orderbooks) — separate PR, since it changes released behavior (missing required array → raise instead of[]).🤖 Generated with Claude Code