Skip to content

perps: typed list-response envelopes for REQUIRED-array endpoints (part of #404)#418

Merged
TexasCoding merged 1 commit into
mainfrom
fix/typed-list-envelopes-404
Jun 5, 2026
Merged

perps: typed list-response envelopes for REQUIRED-array endpoints (part of #404)#418
TexasCoding merged 1 commit into
mainfrom
fix/typed-list-envelopes-404

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

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 raises ValidationError:

Endpoint Envelope
markets.list GetMarginMarketsResponse{markets}
markets.candlesticks GetMarginMarketCandlesticksResponse{ticker, candlesticks} — also validates the previously-discarded required ticker
funding.historical_rates GetMarginHistoricalFundingRatesResponse{funding_rates}
funding.history 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 missing or null → [].

Verification

  • All 4 envelopes registered in PERPS_CONTRACT_MAP and pass the drift harness.
  • Tests: the 4 required arrays hard-fail on missing and null; optional order_groups tolerates both; candlesticks rejects a missing ticker.
  • 3.2.0 is unreleased → pre-release hardening, no breaking change for any published version.
  • mypy --strict + ruff clean; 1140 perps + contract-drift tests pass.

Remaining for #404

Prediction-API parity (kalshi/resources/markets.py candlesticks / bulk_candlesticks / bulk_orderbooks) — separate PR, since it changes released behavior (missing required array → raise instead of []).

🤖 Generated with Claude Code

…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.
@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown

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 order_groups .get(key) or [] distinction (vs the hard-fail envelopes) is exactly right and well-explained. A few items below, mostly minor.


Correctness

  • builtins.list[T] usage — correct per CLAUDE.md; the .list() method shadow in resource classes makes bare list[T] unsafe in type annotations here.
  • extra="allow" on envelope models — consistent with every sibling model in perps/models/. Correct: additive server fields should not blow up deserialization.
  • order_groups null-tolerance fix — the change from .get(key, []) to .get(key) or [] is the right call: .get(key, []) only guards against a missing key, not an explicit null. This is the exact gap resources: validate typed list-response envelopes for REQUIRED-array endpoints (perps + prediction) #404 describes for optional fields.
  • ContractEntry placements — correctly inserted near their logical neighbors (markets/funding sections).

Issues

1. 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)

test_missing_or_null_markets_raises, test_missing_ticker_or_candlesticks_raises, and the two funding equivalents only exercise the sync client. The underlying parse path is identical (same model_validate call), so the practical regression risk is near-zero — but the async variants currently have zero coverage for the new failure modes. A single parametrized or explicit async test per new hard-fail case would close that gap.

3. Pydantic ValidationError leaks through the SDK boundary (pre-existing, but worth noting)

When the new envelopes reject a malformed response, callers receive a raw pydantic.ValidationError, not a KalshiError subclass. KalshiValidationError is currently only raised for HTTP 400/422. This is a pre-existing pattern (other model_validate(data) calls in the codebase behave the same way), so this PR doesn't regress anything — but it means callers who want to handle drift-detection failures must import from Pydantic rather than using the SDK's own exception hierarchy. Worth a follow-up issue.

4. Observation: ticker is validated but not surfaced (by design)

GetMarginMarketCandlesticksResponse.ticker is captured during validation and then immediately dropped (.candlesticks is returned). This is explicitly stated in the PR description and is fine for the current interface. Just flagging it as context: if the prediction-API parity PR (candlesticks/bulk_candlesticks) follows the same envelope pattern, and ticker is ever needed by callers, the return type would have to change.

5. New envelope models not exported from kalshi/perps/__init__.py (intentional?)

GetMarginMarketsResponse, GetMarginMarketCandlesticksResponse, GetMarginHistoricalFundingRatesResponse, and GetMarginFundingHistoryResponse are not in the public perps namespace, unlike GetMarginPositionsResponse which is exported. Since the public return type for these methods remains list[X] (not the envelope), this is probably intentional — users never hold a reference to the envelope. But given the existing pattern of exporting GetMargin*Response types, confirm this is deliberate and not an oversight.


Tests

The test updates are well-structured:

  • Both the missing-key and explicit-null cases are asserted for each required-array endpoint. ✓
  • The order_groups null-tolerance test uses the same two-shot pattern for the symmetric optional case. ✓
  • Renamed test methods (test_missing_or_null_*) accurately describe what's being tested. ✓

The only gap is async coverage noted in item 2 above.


Summary

The 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.

@TexasCoding
TexasCoding merged commit 94fa0e8 into main Jun 5, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the fix/typed-list-envelopes-404 branch June 5, 2026 17:37
TexasCoding added a commit that referenced this pull request Jun 5, 2026
…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.
TexasCoding added a commit that referenced this pull request Jun 5, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resources: validate typed list-response envelopes for REQUIRED-array endpoints (perps + prediction)

1 participant