Summary
Bundle of small perf / testing polish items.
1. Cursor-loop guard memory grows unbounded
_list_all builds a set[str] of every cursor seen and never bounds it. A 10M-row enumeration with ~1KB cursors retains ~10MB purely for loop detection. Since the guard only needs to detect immediate cycles (server bug pattern), retaining the last cursor is sufficient.
Fix: Replace seen_cursors: set[str] = set() with last_cursor: str | None = None and check equality only against the previous page's cursor. O(1) memory, same protection against the realistic bug shape (server replays last page).
2. model_dump(mode='json') then httpx re-serializes
Every POST/DELETE-with-body builds the body via request.model_dump(exclude_none=True, by_alias=True, mode='json') (walks model, runs Decimal serializer, returns dict). That dict is then handed to httpx as json=body, which serializes the same dict to bytes again. For batch_create_orders (up to 100 orders × ~10 Decimal fields each) this is two full passes.
Fix: Add _post_json / _delete_with_body_json helpers in resource base that accept bytes (request.model_dump_json(...)) and pass content=body, headers={'Content-Type': 'application/json'} to httpx. Use from batch order paths first. Keep dict-based _post for retrofit-friendliness.
3. No performance regression benchmarks
scripts/bench_sign_offload.py is the only perf harness. No bench for: per-request allocations on SyncTransport.request, recv-loop throughput (frames/sec), OrderbookManager.apply_delta cost vs book depth, Decimal coercion cost across wide models. Regressions like the discarded-snapshot waste (separate high issue) are invisible to CI.
Fix: Add:
scripts/bench_ws_recv.py — synthetic snapshot+delta frames through _process_frame, reports frames/sec + GC counts.
scripts/bench_orderbook_delta.py — parameterized by book depth (10, 100, 500, 2000).
scripts/bench_request_hot_path.py — mock transport, measures requests/sec for sync + async.
Wire into nightly CI that compares against a checked-in baseline JSON (gating optional; emitting numbers is the minimum).
4. from_env precedence (KALSHI_PRIVATE_KEY vs KALSHI_PRIVATE_KEY_PATH) untested
from_env() checks KALSHI_PRIVATE_KEY first, falls back to KALSHI_PRIVATE_KEY_PATH. Existing tests only set one or the other, so neither pins the precedence when BOTH are set (common in CI environments).
Fix: Add test_pem_takes_precedence_over_path: monkeypatch both env vars to different keys, call from_env(), assert the loaded public key matches the PEM source. ~12 lines.
5. tests/integration/conftest.py mutates os.environ at import time
_bridge_env_vars() unconditionally writes KALSHI_KEY_ID / KALSHI_PRIVATE_KEY_PATH / KALSHI_DEMO into process env at conftest import time. Imported whenever pytest collects under tests/integration/, including under pytest tests/. Unit tests that read env without monkeypatch override silently see bridged demo credentials.
Fix: Move _bridge_env_vars() into a session-scoped autouse fixture using monkeypatch.setenv (test-scoped restoration), OR wrap in pytest_sessionstart / pytest_sessionfinish hooks that snapshot+restore.
6. KalshiAuth.close() reusability surprise
KalshiAuth.close() calls self._sign_executor.shutdown(wait=False) and sets _sign_executor = None. The lazy-init in _get_sign_executor then sees None and silently creates a NEW executor. close() is not actually terminal — a subsequent sign_request_async() spins up a fresh ThreadPoolExecutor and the close was effectively a no-op for resource lifetime.
Fix: Track self._closed: bool = False. Set in close(). Have _get_sign_executor raise RuntimeError("KalshiAuth has been closed; create a new instance") when _closed is True. Pairs with the "shared-auth ownership" medium issue.
Severity & category
Bundle of low items, performance + testing + polish.
Summary
Bundle of small perf / testing polish items.
1. Cursor-loop guard memory grows unbounded
_list_allbuilds aset[str]of every cursor seen and never bounds it. A 10M-row enumeration with ~1KB cursors retains ~10MB purely for loop detection. Since the guard only needs to detect immediate cycles (server bug pattern), retaining the last cursor is sufficient.Fix: Replace
seen_cursors: set[str] = set()withlast_cursor: str | None = Noneand check equality only against the previous page's cursor. O(1) memory, same protection against the realistic bug shape (server replays last page).2.
model_dump(mode='json')then httpx re-serializesEvery POST/DELETE-with-body builds the body via
request.model_dump(exclude_none=True, by_alias=True, mode='json')(walks model, runs Decimal serializer, returns dict). That dict is then handed to httpx asjson=body, which serializes the same dict to bytes again. For batch_create_orders (up to 100 orders × ~10 Decimal fields each) this is two full passes.Fix: Add
_post_json/_delete_with_body_jsonhelpers in resource base that accept bytes (request.model_dump_json(...)) and passcontent=body, headers={'Content-Type': 'application/json'}to httpx. Use from batch order paths first. Keep dict-based_postfor retrofit-friendliness.3. No performance regression benchmarks
scripts/bench_sign_offload.pyis the only perf harness. No bench for: per-request allocations onSyncTransport.request, recv-loop throughput (frames/sec),OrderbookManager.apply_deltacost vs book depth, Decimal coercion cost across wide models. Regressions like the discarded-snapshot waste (separate high issue) are invisible to CI.Fix: Add:
scripts/bench_ws_recv.py— synthetic snapshot+delta frames through_process_frame, reports frames/sec + GC counts.scripts/bench_orderbook_delta.py— parameterized by book depth (10, 100, 500, 2000).scripts/bench_request_hot_path.py— mock transport, measures requests/sec for sync + async.Wire into nightly CI that compares against a checked-in baseline JSON (gating optional; emitting numbers is the minimum).
4.
from_envprecedence (KALSHI_PRIVATE_KEY vs KALSHI_PRIVATE_KEY_PATH) untestedfrom_env()checksKALSHI_PRIVATE_KEYfirst, falls back toKALSHI_PRIVATE_KEY_PATH. Existing tests only set one or the other, so neither pins the precedence when BOTH are set (common in CI environments).Fix: Add
test_pem_takes_precedence_over_path: monkeypatch both env vars to different keys, callfrom_env(), assert the loaded public key matches the PEM source. ~12 lines.5.
tests/integration/conftest.pymutatesos.environat import time_bridge_env_vars()unconditionally writesKALSHI_KEY_ID/KALSHI_PRIVATE_KEY_PATH/KALSHI_DEMOinto process env at conftest import time. Imported whenever pytest collects undertests/integration/, including underpytest tests/. Unit tests that read env without monkeypatch override silently see bridged demo credentials.Fix: Move
_bridge_env_vars()into a session-scoped autouse fixture usingmonkeypatch.setenv(test-scoped restoration), OR wrap inpytest_sessionstart/pytest_sessionfinishhooks that snapshot+restore.6. KalshiAuth.close() reusability surprise
KalshiAuth.close()callsself._sign_executor.shutdown(wait=False)and sets_sign_executor = None. The lazy-init in_get_sign_executorthen seesNoneand silently creates a NEW executor.close()is not actually terminal — a subsequentsign_request_async()spins up a freshThreadPoolExecutorand the close was effectively a no-op for resource lifetime.Fix: Track
self._closed: bool = False. Set inclose(). Have_get_sign_executorraiseRuntimeError("KalshiAuth has been closed; create a new instance")when_closedis True. Pairs with the "shared-auth ownership" medium issue.Severity & category
Bundle of low items, performance + testing + polish.