fix(models): relax subaccount-number request constraint (drop le=32) (#164)#169
Conversation
…164) Nightly integration test `test_transfer_between_subaccounts` failed with: pydantic_core._pydantic_core.ValidationError: 1 validation error for ApplySubaccountTransferRequest to_subaccount Input should be less than or equal to 32 [type=less_than_equal, input_value=41, input_type=int] Root cause ---------- The demo server created an ephemeral subaccount and returned `subaccount_number=41`. The test then tried to use that value as `to_subaccount` in a transfer, but the SDK refused client-side because 7 request-side model fields carried `le=32`. The `le=32` bound was added in v0.11.0 from a code-review pass that read the spec's prose description ("0 for primary, 1-32 for numbered subaccounts") as a hard limit. The actual OpenAPI schema defines no `minimum`/`maximum` constraints on any of these fields. Demo has clearly extended past 32 in practice, and response-side subaccount numbers were already unbounded — the asymmetry was the bug. Fix --- Drop `le=32` from 7 request-side model fields, keep `ge=0`: ApplySubaccountTransferRequest.{from,to}_subaccount UpdateSubaccountNettingRequest.subaccount_number CreateOrderRequest.subaccount BatchCancelOrdersV2RequestOrder.subaccount CreateRFQRequest.subaccount CreateQuoteRequest.subaccount Server is the source of truth on the upper bound. Negative values still fail-fast at the SDK boundary; out-of-range positives now round-trip cleanly. Docstrings on ApplySubaccountTransferRequest and SubaccountsResource updated to drop the "1-32" claim and explain the constraint asymmetry. Tests ----- Replaced `test_transfer_request_rejects_out_of_range_subaccount` (asserted `to_subaccount=33` failed) with `test_transfer_request_accepts_subaccount_above_32` (asserts `41` round-trips). The existing `test_transfer_request_rejects_negative_subaccount` keeps the `ge=0` coverage; the new test locks in the new contract so a future code- review pass can't re-introduce the bound from prose alone. Verification ------------ uv run pytest tests/test_subaccounts.py -> 43 passed uv run pytest tests/ --ignore=tests/integration -> 2023 passed (unchanged) uv run mypy kalshi/ -> Success uv run ruff check . -> All checks passed The failing integration test cannot be reproduced locally without hitting demo with KALSHI_KEY_ID, but the unit-test regression guard (`accepts_subaccount_above_32`) directly mirrors the failure mode from the nightly run. Closes #164.
Code Review — PR #169: relax subaccount-number
|
| Area | Status |
|---|---|
| Correctness | ✅ Fix matches the spec and the observed failure |
| Minimal diff | ✅ Only the 7 fields, no unrelated changes |
| Tests | ✅ with nit (see item 2 above) |
| mypy / ruff | ✅ per PR description |
| CHANGELOG | ✅ |
| Missing blank line |
Ready to merge after the blank-line nit is resolved.
|
End-to-end demo verification (local On Reproduces the nightly failure exactly. Demo allocated 44 this time (the nightly run got 41); both > 32 confirms the upper bound was never enforced server-side. On The fix closes #164 against the live demo API. |
Cuts v2.2.0 — the response-side spec drift hardening stack (#157). Version bumps: - pyproject.toml: 2.1.0 → 2.2.0 - kalshi/__init__.py: 2.1.0 → 2.2.0 Docs: - CHANGELOG.md: finalize ## Unreleased → ## 2.2.0 — 2026-05-19 with a release-summary headline. The Added/Changed/Fixed entries written incrementally across #166–#169 stay as-is. - ROADMAP.md: add v2.2.0 to Shipped with a coverage summary; drop the two "Next milestone" items now done by this release (response-side drift detection; WS envelope extra=allow); add two new follow-ups (map remaining REST sub-models into CONTRACT_MAP; required-but- optional drift policy decision). - CLAUDE.md: active milestone → post-v2.2. Release scope (65 new optional fields + warn→fail flip + 1 bugfix): - REST response models: Market +11, Order +8, Fill +4, Event +3, EventMetadata +2, Settlement +1, Trade +2, IncentiveProgram +1, RFQ +1, Quote +3, OrderGroup +1, GetOrderGroupResponse +1, CreateOrderGroupResponse +2 (#166, #167, #168). - WS payload models: 33 fields across 11 payloads — Unix-ms timestamps (`*_ts_ms`), outcome_side/book_side direction encoding, MVE linkage, RFQ/Quote context echoes (#168). - Test infrastructure: additive drift now hard-fails CI; unmapped WS models hard-fail; required-but-optional stays warn-only (~204 entries, separate policy decision). EXCLUSIONS allowlist wired through response-side checks (#165). ErrorPayload registered in WS_CONTRACT_MAP; WS envelopes/helpers all use extra=allow (#168). - Bugfix: dropped le=32 cap on 6 subaccount request fields; demo allocates ephemeral subaccount numbers above 32 (observed 41 in nightly, 44 locally). Spec defines no upper bound (#169). No API breaks. All changes additive or relaxing. Bump is semver-minor. Verification: - uv run pytest tests/ --ignore=tests/integration -q -> 2023 passed, 35 warnings (all required-drift, kept warn-only) - uv run pytest tests/integration/test_subaccounts.py::TestSubaccountsSync::test_transfer_between_subaccounts -> 1 passed against live demo (#164 unblocked end-to-end) - uv run mypy kalshi/ -> Success: no issues found in 76 source files - uv run ruff check . -> All checks passed! - kalshi.__version__ check -> 2.2.0 After merge, push tag v2.2.0 to trigger the release workflow (.github/workflows/release.yml): build sdist+wheel → PyPI trusted- publish → GitHub release with CHANGELOG section as notes.
Closes #164.
TL;DR
The nightly integration job authentication is fine (the workflow ran 224 tests against demo with
KALSHI_KEY_IDset). The single failure is a real SDK bug:ApplySubaccountTransferRequest.to_subaccountrejected41client-side because the model carriedle=32, but the demo server now allocates ephemeral subaccount numbers above 32.Root cause
The
le=32bound was added in v0.11.0 from a code-review pass that read the spec's prose description (0 for primary, 1-32 for numbered subaccounts) as a hard limit. The actual OpenAPI schema defines nominimum/maximumconstraints on any of these fields. Response-side subaccount numbers were already unbounded — the asymmetry between request and response was the latent bug; the nightly test surfaced it the moment demo started allocating above 32.Failure log:
Fix
Dropped
le=32from 7 request-side model fields, keptge=0:kalshi/models/subaccounts.pyApplySubaccountTransferRequestfrom_subaccount,to_subaccountkalshi/models/subaccounts.pyUpdateSubaccountNettingRequestsubaccount_numberkalshi/models/orders.pyCreateOrderRequestsubaccountkalshi/models/orders.pyBatchCancelOrdersV2RequestOrdersubaccountkalshi/models/communications.pyCreateRFQRequestsubaccountkalshi/models/communications.pyCreateQuoteRequestsubaccountServer is the source of truth on the upper bound. Negative values still fail-fast at the SDK boundary (
ge=0); out-of-range positives now round-trip cleanly.Docstrings on
ApplySubaccountTransferRequestandSubaccountsResourceupdated to drop the1-32claim and explain the constraint asymmetry so a future review pass doesn't re-introduce the bound from prose alone.Tests
Replaced
test_transfer_request_rejects_out_of_range_subaccount(assertedto_subaccount=33failed) withtest_transfer_request_accepts_subaccount_above_32(asserts41round-trips). The existingtest_transfer_request_rejects_negative_subaccountkeepsge=0coverage.The new positive-path test is a direct regression guard against the exact failure from issue #164.
Verification
The failing integration test (
test_transfer_between_subaccounts) cannot be exercised locally withoutKALSHI_KEY_ID, but the new unit-test regression guard directly mirrors the failure mode from the nightly run.