Skip to content

feat(rest): pluggable JSON loader + plumb extra_headers through resources (#260, #253) - #288

Merged
TexasCoding merged 2 commits into
mainfrom
feat/260-253-rest-dx
May 21, 2026
Merged

TexasCoding merged 2 commits into
mainfrom
feat/260-253-rest-dx

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

Closes #260
Closes #253

Part of #273 (post-v2.4 audit), Wave 2.2.

agent added 2 commits May 21, 2026 07:57
Mirrors ws_json_loads. Setting KalshiConfig.rest_json_loads to
orjson.loads cuts parsing on list endpoints (markets.list_all,
trades.list_all, fcm.orders) 2-3x while keeping stdlib as the
zero-dep default.

The shared _load_json helper on Sync/AsyncResource calls
config.rest_json_loads(response.content) when set, falling back
to httpx's response.json() otherwise. Threaded through every
helper: _get, _post, _post_json, _put, _delete, _delete_with_body,
_delete_with_body_json (sync + async).

Also widens base helpers to accept extra_headers — #253 plumbs it
through resource methods next.

Closes #260
Per-request extra_headers (Idempotency-Key, X-Request-ID, tracing
headers) used to require reaching into client._transport.request(...) —
making the feature private despite being documented as additive.

Every public method on every Sync/AsyncResource subclass now takes
extra_headers: dict[str, str] | None = None and forwards it through the
base _get/_post/_put/_delete/_list/_list_all helpers to the transport.
Auth-header precedence is preserved: KALSHI-ACCESS-* always wins over
extra_headers (transport-level merge order: config_extra <
extra_headers < body_headers < signed_auth) — verified by tests.

Contract drift test (tests/test_contracts.py) excludes extra_headers
globally — it's an SDK-meta kwarg with no OpenAPI counterpart.

302 method signatures updated across 19 resource files; codemod via
libcst kept docstrings, overloads, and existing kwargs intact.
ruff format normalized line breaks introduced by the kwarg.

Tests:
* TestOrders/TestMarkets/TestPortfolio assert the header reaches the
  wire and that KALSHI-ACCESS-* spoofs are rejected.
* test_every_public_method_accepts_extra_headers parametrically
  enumerates every Sync/AsyncResource subclass via reflection.

Closes #253
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown

Code Review — PR #288: Pluggable JSON loader + per-request extra_headers

Overall: Clean, well-scoped addition. The two features (pluggable JSON loader and extra_headers plumbing) are conceptually independent but share enough surface area to ship together. No blocking issues — a few minor observations below.


Feature 1: Pluggable REST JSON loader (rest_json_loads)

What it does: KalshiConfig.rest_json_loads: Callable[[bytes], Any] | None lets callers swap in orjson.loads (or anything byte-callable) to speed up list-heavy endpoints.

Looks good:

  • _load_json correctly passes response.content (bytes) to the custom loader, consistent with the WS ws_json_loads pattern already in the codebase.
  • Fallback to response.json() when None preserves 100% backward compatibility.
  • Both SyncResource and AsyncResource get the same implementation — the duplication is unavoidable without restructuring the base class hierarchy, and the code is small.

Minor concern — type safety: _load_json is annotated -> Any, but every call site casts the result to dict[str, Any]. If a custom loader returns a list or some other type by mistake, the failure will surface deep inside model_validate() with a confusing traceback rather than at the boundary. Documenting this expectation (loader must return a JSON object for these endpoints) is sufficient; adding a runtime guard would be overkill given project conventions.

_base.py private attribute access:

loader = self._transport._config.rest_json_loads

This accesses a private attribute. It follows existing patterns in the file, but it does create a coupling. Not a blocker, but worth a note if _config ever becomes a property.


Feature 2: Per-request extra_headers

Transport-layer implementation is correct. The merge order in _base_client.py:

base_headers = {
    **(self._config.extra_headers or {}),   # client-wide defaults
    **(extra_headers or {}),                 # per-request overrides
    **(headers or {}),                       # method-level (e.g. Content-Type)
}
# ...then auth headers are applied last, always winning
merged_headers = {**base_headers, **auth_headers}

Auth headers cannot be spoofed — the signed values always overwrite anything in extra_headers. The tests in test_extra_headers_plumbing.py verify this explicitly. ✓

Overload signatures: extra_headers appears in both @overload variants and the implementation for methods like create() in api_keys.py. This is correct — it ensures type checkers see the param in both dispatch paths.

Minor: parametric test runs at module import time.

_METHODS = _collect_resource_methods()  # module-level, runs on import

This will fail test collection (not just test execution) if any resource module fails to import. Fine for CI, but worth being aware of during local development with partially-complete branches.

Missing parametric enforcement for extra_headers being keyword-only: The test at line 6310 checks param.kind in (KEYWORD_ONLY, POSITIONAL_OR_KEYWORD). Since every resource method uses *, extra_headers, they're all KEYWORD_ONLY. The broader check is correct defensively but could be tightened to only KEYWORD_ONLY to catch any future accidental positional placement.


Test coverage

Area Coverage
rest_json_loads sync GET/POST ✓ test_rest_json_loader.py
rest_json_loads async GET ✓
rest_json_loads default (stdlib) ✓
extra_headers wire delivery (orders, markets, portfolio) ✓ test_extra_headers_plumbing.py
Auth header spoof prevention ✓ (three independent resources)
Every public method has extra_headers kwarg ✓ parametric test
ws_json_loads / ws_json_dumps config additions Not in new tests (pre-existing?)

If ws_json_loads/ws_json_dumps are new additions (the config diff shows them), confirm they have test coverage elsewhere.


Contract drift tests

The exclusion in test_contracts.py is the right call:

sdk_params.discard("extra_headers")

extra_headers is an SDK meta-kwarg with no OpenAPI counterpart, so excluding it globally rather than adding per-endpoint EXCLUSIONS entries is cleaner. The comment explains the rationale. ✓


Summary

  • No bugs found
  • Auth protection is implemented correctly and tested
  • The approach is minimal and consistent with project conventions
  • Two minor notes worth considering before merge: (1) document the type contract on custom loaders in the docstring for _load_json, (2) consider whether ws_json_loads/ws_json_dumps need test coverage in this PR or can follow separately

@TexasCoding
TexasCoding merged commit 14937a0 into main May 21, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the feat/260-253-rest-dx branch May 21, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant