Skip to content

Commit 9de0cee

Browse files
TexasCodingclaude
andauthored
refactor(resources): extract shared sync/async helpers (#46) (#71)
Issue #46: every body-endpoint method had a sync+async pair that differed only by `def` vs `async def` and `self._post(...)` vs `await self._post(...)`. The dispatcher logic (`_check_request_exclusive` + model construction + `model_dump(...)`) was duplicated verbatim across the two classes, so each new kwarg had to be added in two places. Conservative dedup (option 1 in the issue): pull the identical parts of each method-pair into module-level helpers; both sync and async methods call them. - _build_*_body(...) helpers: dispatcher inputs (request | individual kwargs) → dict ready for transport. Owns the exclusive-arg check, the model construction, and the `model_dump(...)` serialization. - Per-resource query-param builders for GET-list endpoints whose param dicts appeared 4× (sync list, sync list_all, async list, async list_all). - _parse_orderbook + _parse_queue_position response helpers in markets/orders. What stays per-class (unchanged): method signatures, @overload stubs from #69 (per-class user-facing typed surface), transport call sites, return-type wrapping. The dual-transport architecture and `async for` ergonomics are preserved. Files refactored: orders, api_keys, communications, multivariate, order_groups, subaccounts (high-payoff body-endpoint resources) plus markets, events, historical, series, portfolio, fcm, milestones (meaningful GET-list dedup). incentive_programs and structured_targets are left as-is — too small for a helper to be cleaner than the inline `_params(...)`. Notes - subaccounts.transfer: UUID coercion stays inside _build_transfer_body (was duplicated across sync+async); behavior identical. - multivariate.lookup_tickers: the sync `if data is None: raise RuntimeError` spec-drift guard is preserved on sync only (async never had it; this is a latent inconsistency, not in scope to fix here). Gates - ruff: clean - mypy --strict: clean (75 files) - pytest (unit): 1405 passed, 48 skipped (no test changes) Closes #46 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dd72fea commit 9de0cee

13 files changed

Lines changed: 1337 additions & 1008 deletions

kalshi/resources/api_keys.py

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import builtins
6-
from typing import overload
6+
from typing import Any, overload
77

88
from kalshi.models.api_keys import (
99
CreateApiKeyRequest,
@@ -18,6 +18,46 @@
1818
_check_request_exclusive,
1919
)
2020

21+
# Shared request-body builders (issue #46).
22+
23+
24+
def _build_create_api_key_body(
25+
request: CreateApiKeyRequest | None,
26+
*,
27+
name: str | None,
28+
public_key: str | None,
29+
scopes: builtins.list[str] | None,
30+
) -> dict[str, Any]:
31+
_check_request_exclusive(
32+
request, name=name, public_key=public_key, scopes=scopes,
33+
)
34+
if request is None:
35+
if name is None or public_key is None:
36+
raise TypeError(
37+
"create() requires `name` and `public_key` "
38+
"(or pass `request=...`)"
39+
)
40+
request = CreateApiKeyRequest(
41+
name=name, public_key=public_key, scopes=scopes,
42+
)
43+
return request.model_dump(exclude_none=True, by_alias=True, mode="json")
44+
45+
46+
def _build_generate_api_key_body(
47+
request: GenerateApiKeyRequest | None,
48+
*,
49+
name: str | None,
50+
scopes: builtins.list[str] | None,
51+
) -> dict[str, Any]:
52+
_check_request_exclusive(request, name=name, scopes=scopes)
53+
if request is None:
54+
if name is None:
55+
raise TypeError(
56+
"generate() requires `name` (or pass `request=...`)"
57+
)
58+
request = GenerateApiKeyRequest(name=name, scopes=scopes)
59+
return request.model_dump(exclude_none=True, by_alias=True, mode="json")
60+
2161

2262
class ApiKeysResource(SyncResource):
2363
"""Sync API keys resource.
@@ -51,19 +91,9 @@ def create(
5191
scopes: builtins.list[str] | None = None,
5292
) -> CreateApiKeyResponse:
5393
self._require_auth()
54-
_check_request_exclusive(
94+
body = _build_create_api_key_body(
5595
request, name=name, public_key=public_key, scopes=scopes,
5696
)
57-
if request is None:
58-
if name is None or public_key is None:
59-
raise TypeError(
60-
"create() requires `name` and `public_key` "
61-
"(or pass `request=...`)"
62-
)
63-
request = CreateApiKeyRequest(
64-
name=name, public_key=public_key, scopes=scopes,
65-
)
66-
body = request.model_dump(exclude_none=True, by_alias=True, mode="json")
6797
data = self._post("/api_keys", json=body)
6898
return CreateApiKeyResponse.model_validate(data)
6999

@@ -84,14 +114,7 @@ def generate(
84114
scopes: builtins.list[str] | None = None,
85115
) -> GenerateApiKeyResponse:
86116
self._require_auth()
87-
_check_request_exclusive(request, name=name, scopes=scopes)
88-
if request is None:
89-
if name is None:
90-
raise TypeError(
91-
"generate() requires `name` (or pass `request=...`)"
92-
)
93-
request = GenerateApiKeyRequest(name=name, scopes=scopes)
94-
body = request.model_dump(exclude_none=True, by_alias=True, mode="json")
117+
body = _build_generate_api_key_body(request, name=name, scopes=scopes)
95118
data = self._post("/api_keys/generate", json=body)
96119
return GenerateApiKeyResponse.model_validate(data)
97120

@@ -127,19 +150,9 @@ async def create(
127150
scopes: builtins.list[str] | None = None,
128151
) -> CreateApiKeyResponse:
129152
self._require_auth()
130-
_check_request_exclusive(
153+
body = _build_create_api_key_body(
131154
request, name=name, public_key=public_key, scopes=scopes,
132155
)
133-
if request is None:
134-
if name is None or public_key is None:
135-
raise TypeError(
136-
"create() requires `name` and `public_key` "
137-
"(or pass `request=...`)"
138-
)
139-
request = CreateApiKeyRequest(
140-
name=name, public_key=public_key, scopes=scopes,
141-
)
142-
body = request.model_dump(exclude_none=True, by_alias=True, mode="json")
143156
data = await self._post("/api_keys", json=body)
144157
return CreateApiKeyResponse.model_validate(data)
145158

@@ -162,14 +175,7 @@ async def generate(
162175
scopes: builtins.list[str] | None = None,
163176
) -> GenerateApiKeyResponse:
164177
self._require_auth()
165-
_check_request_exclusive(request, name=name, scopes=scopes)
166-
if request is None:
167-
if name is None:
168-
raise TypeError(
169-
"generate() requires `name` (or pass `request=...`)"
170-
)
171-
request = GenerateApiKeyRequest(name=name, scopes=scopes)
172-
body = request.model_dump(exclude_none=True, by_alias=True, mode="json")
178+
body = _build_generate_api_key_body(request, name=name, scopes=scopes)
173179
data = await self._post("/api_keys/generate", json=body)
174180
return GenerateApiKeyResponse.model_validate(data)
175181

0 commit comments

Comments
 (0)