Skip to content

fix(models): relax subaccount-number request constraint (drop le=32) (#164)#169

Merged
TexasCoding merged 1 commit into
mainfrom
fix/relax-subaccount-le32-constraint
May 19, 2026
Merged

fix(models): relax subaccount-number request constraint (drop le=32) (#164)#169
TexasCoding merged 1 commit into
mainfrom
fix/relax-subaccount-le32-constraint

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

Closes #164.

TL;DR

The nightly integration job authentication is fine (the workflow ran 224 tests against demo with KALSHI_KEY_ID set). The single failure is a real SDK bug: ApplySubaccountTransferRequest.to_subaccount rejected 41 client-side because the model carried le=32, but the demo server now allocates ephemeral subaccount numbers above 32.

Root cause

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

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]

Fix

Dropped le=32 from 7 request-side model fields, kept ge=0:

File Class Field
kalshi/models/subaccounts.py ApplySubaccountTransferRequest from_subaccount, to_subaccount
kalshi/models/subaccounts.py UpdateSubaccountNettingRequest subaccount_number
kalshi/models/orders.py CreateOrderRequest subaccount
kalshi/models/orders.py BatchCancelOrdersV2RequestOrder subaccount
kalshi/models/communications.py CreateRFQRequest subaccount
kalshi/models/communications.py CreateQuoteRequest subaccount

Server 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 ApplySubaccountTransferRequest and SubaccountsResource updated to drop the 1-32 claim 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 (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 ge=0 coverage.

The new positive-path test is a direct regression guard against the exact failure from issue #164.

Verification

uv run pytest tests/test_subaccounts.py        -> 43 passed in 1.60s
uv run pytest tests/ --ignore=tests/integration -q
                                               -> 2023 passed (unchanged)
uv run mypy kalshi/                            -> Success: no issues
uv run ruff check .                            -> All checks passed!

The failing integration test (test_transfer_between_subaccounts) cannot be exercised locally without KALSHI_KEY_ID, but the new unit-test regression guard directly mirrors the failure mode from the nightly run.

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

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Code Review — PR #169: relax subaccount-number le=32 constraint

Verdict: Approve with minor nits. This is a clean, well-reasoned bug fix with clear root-cause analysis. The change is minimal and surgical; the docstring updates are warranted because the spec-prose-vs-JSON-schema asymmetry is exactly the kind of non-obvious invariant worth explaining.


What the PR does

Drops the le=32 upper-bound validator from 7 request-side model fields across subaccounts.py, orders.py, and communications.py. The fix is motivated by the demo server allocating ephemeral subaccount numbers above 32 (observed: 41), causing a client-side ValidationError before the request could leave the SDK. The actual OpenAPI schema defines no upper bound; only spec prose mentioned 1–32. The ge=0 lower bound is preserved.


Code quality

  • Fix is minimal and correct. All 7 fields get the same treatment (ge=0, le=32ge=0), no surrounding churn.
  • Docstrings are appropriately updated. The "server is the source of truth on the upper bound" language explains the why precisely — this is the kind of non-obvious constraint that deserves a comment (CLAUDE.md §2 agrees).
  • CHANGELOG.md entry is clear and thorough — it lists all affected fields and explains the root cause.

Potential issues

1. Missing blank line before test_update_netting_request_serializes (minor)

The diff leaves no blank line between the last line of the new test and the next test method:

        assert req.to_subaccount == 41
    def test_update_netting_request_serializes(self) -> None:   # ← missing blank line

This is a style nit but ruff / flake8 E302 would normally flag it. Worth fixing.

2. Only to_subaccount gets a regression test for values > 32

The PR fixes 7 fields but the new test only asserts to_subaccount=41. The following are untested for the above-32 case:

  • ApplySubaccountTransferRequest.from_subaccount
  • UpdateSubaccountNettingRequest.subaccount_number
  • CreateOrderRequest.subaccount
  • BatchCancelOrdersV2RequestOrder.subaccount
  • CreateRFQRequest.subaccount
  • CreateQuoteRequest.subaccount

The fix is identical for all of them so the risk is very low, but a test for at least one more field (e.g., UpdateSubaccountNettingRequest) would widen the regression guard. Up to you whether the marginal confidence is worth the lines.

3. CHANGELOG placement (non-blocking)

The ### Fixed section is appended to the in-progress unreleased block above ## 2.1.0. That looks intentional, but confirm the version bump story is covered — is this targeting a 2.1.1 patch release?


Test coverage

  • test_transfer_request_accepts_subaccount_above_32 directly mirrors the nightly failure mode — good regression guard.
  • test_transfer_request_rejects_negative_subaccount continues to exercise ge=0 — correct.
  • Removing test_transfer_request_rejects_out_of_range_subaccount is the right call; keeping a test that asserts wrong behavior is worse than having no test.

Security

No concerns. Relaxing a client-side numeric upper bound to match the server spec is not a security change. The server remains the authoritative validator; the SDK is not a security boundary here.


Summary

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 ⚠️ minor formatting issue

Ready to merge after the blank-line nit is resolved.

@TexasCoding

Copy link
Copy Markdown
Owner Author

End-to-end demo verification (local .env has KALSHI_KEY_ID):

On main (pre-fix):

tests/integration/test_subaccounts.py::TestSubaccountsSync::test_transfer_between_subaccounts FAILED
E   pydantic_core._pydantic_core.ValidationError: 1 validation error for ApplySubaccountTransferRequest
E   to_subaccount
E     Input should be less than or equal to 32 [type=less_than_equal, input_value=44, input_type=int]

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 fix/relax-subaccount-le32-constraint:

tests/integration/test_subaccounts.py::TestSubaccountsSync::test_transfer_between_subaccounts PASSED [100%]
============================== 1 passed in 2.30s ===============================

The fix closes #164 against the live demo API.

@TexasCoding
TexasCoding merged commit 61fedc6 into main May 19, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the fix/relax-subaccount-le32-constraint branch May 19, 2026 23:54
@TexasCoding TexasCoding mentioned this pull request May 20, 2026
TexasCoding added a commit that referenced this pull request May 20, 2026
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.
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.

Integration tests failing on demo API

1 participant