diff --git a/docs/concepts.md b/docs/concepts.md index e3b95b9d..1481bc5d 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -163,6 +163,32 @@ returns the same shapes as `portfolio.*`. Non-FCM accounts get 401/403. See [FCM](resources/fcm.md). + +## Per-request headers (Idempotency-Key, tracing) + +Every public resource method accepts an ``extra_headers: dict[str, str] | None = None`` +kwarg that lands on the wire for that single call. Use it for per-request +idempotency keys, distributed-tracing headers, or any one-off header that +doesn't belong on every request (set those via ``KalshiConfig.extra_headers``). + +```python +import uuid + +order = client.orders.create( + ticker="KXPRES-24-DJT", + side="yes", + action="buy", + count=1, + yes_price="0.65", + client_order_id="cli-1", + extra_headers={"Idempotency-Key": str(uuid.uuid4())}, +) +``` + +Auth headers are sacred: ``KALSHI-ACCESS-KEY`` / ``KALSHI-ACCESS-SIGNATURE`` / +``KALSHI-ACCESS-TIMESTAMP`` cannot be overridden via ``extra_headers`` — the +SDK-signed values always win at the transport layer. + ## API key, scope Authentication identity. Has `read` and `write` scopes; `write` requires diff --git a/docs/configuration.md b/docs/configuration.md index 5a9696dd..1f2fb2e8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,6 +40,9 @@ with KalshiClient(key_id="...", private_key_path="...", config=config) as client | `ws_base_url` | `wss://api.elections.kalshi.com/trade-api/ws/v2` | WebSocket base URL. | | `ws_max_retries` | `10` | Maximum reconnect attempts before the WS gives up. | | `http2` | `False` | Enable HTTP/2 (requires `httpx[http2]` install extra). | +| `rest_json_loads` | `None` | Optional callable for parsing REST response bodies (e.g. `orjson.loads`). When set, receives `response.content` bytes; falls back to stdlib `json` otherwise. Useful for list-heavy endpoints (`markets.list_all`, `trades.list_all`). | +| `ws_json_loads` | `None` | Optional callable for parsing WS frames (e.g. `orjson.loads`). | +| `ws_json_dumps` | `None` | Optional callable for serializing outbound WS commands. | | `limits` | `None` | Connection-pool limits passed to `httpx`. When `None`, the underlying `httpx.Client` uses its own pool defaults. Pass an `httpx.Limits(...)` to tune. See [Performance](websockets.md#performance) for WS sizing guidance. | See [Retries & idempotency](retries.md) for what `max_retries`, diff --git a/kalshi/config.py b/kalshi/config.py index f91ff0f7..a590024b 100644 --- a/kalshi/config.py +++ b/kalshi/config.py @@ -48,6 +48,11 @@ class KalshiConfig: ws_ping_interval: Interval (s) between WS keepalive pings. Default 20. ws_close_timeout: Time (s) to wait for graceful WS close handshake. Default 5. + rest_json_loads: Optional callable used to parse REST response + bodies. ``None`` falls back to :func:`httpx.Response.json` + (stdlib :func:`json.loads`). Set to e.g. ``orjson.loads`` for + ~2-3x faster parsing on list endpoints (``markets.list_all``, + ``trades.list_all``, ``fcm.orders``). ws_json_loads: Optional callable used to parse WS frames. ``None`` falls back to :func:`json.loads`. Set to e.g. ``orjson.loads`` for ~2-3x faster recv-loop parsing on high-volume streams. @@ -68,6 +73,7 @@ class KalshiConfig: limits: httpx.Limits | None = None ws_ping_interval: float = 20.0 ws_close_timeout: float = 5.0 + rest_json_loads: Callable[[bytes], Any] | None = None ws_json_loads: Callable[[bytes | str], Any] | None = None ws_json_dumps: Callable[[Any], bytes | str] | None = None allow_unknown_host: bool = False diff --git a/kalshi/resources/_base.py b/kalshi/resources/_base.py index b4459f09..b3b364ab 100644 --- a/kalshi/resources/_base.py +++ b/kalshi/resources/_base.py @@ -6,6 +6,7 @@ from collections.abc import AsyncIterator, Iterator from typing import Any, TypeVar +import httpx from pydantic import BaseModel from kalshi._base_client import AsyncTransport, SyncTransport @@ -49,13 +50,10 @@ def _validate_limit( if value is None: return None if not (lo <= value <= hi): - raise ValueError( - f"{name} must be in [{lo}, {hi}], got {value}." - ) + raise ValueError(f"{name} must be in [{lo}, {hi}], got {value}.") return value - def _params(**kwargs: Any) -> dict[str, Any]: """Build a query-param dict, dropping None values.""" return {k: v for k, v in kwargs.items() if v is not None} @@ -95,9 +93,7 @@ def _validate_max_pages(max_pages: int | None) -> None: friendlier. """ if max_pages is not None and max_pages <= 0: - raise ValueError( - f"max_pages must be positive or None, got {max_pages}" - ) + raise ValueError(f"max_pages must be positive or None, got {max_pages}") def _join_tickers(value: list[str] | tuple[str, ...] | str | None) -> str | None: @@ -134,9 +130,31 @@ def _require_auth(self) -> None: if not self._transport.is_authenticated: raise AuthRequiredError() - def _get(self, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]: - response = self._transport.request("GET", path, params=params) - result: dict[str, Any] = response.json() + def _load_json(self, response: httpx.Response) -> Any: + """Parse a response body, honoring ``config.rest_json_loads`` if set. + + Falls back to ``response.json()`` (stdlib) when no custom loader is + configured. Custom loaders receive ``response.content`` (bytes). + """ + loader = self._transport._config.rest_json_loads + if loader is None: + return response.json() + return loader(response.content) + + def _get( + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any]: + response = self._transport.request( + "GET", + path, + params=params, + extra_headers=extra_headers, + ) + result: dict[str, Any] = self._load_json(response) return result def _post( @@ -145,9 +163,16 @@ def _post( *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any]: - response = self._transport.request("POST", path, params=params, json=json) - result: dict[str, Any] = response.json() + response = self._transport.request( + "POST", + path, + params=params, + json=json, + extra_headers=extra_headers, + ) + result: dict[str, Any] = self._load_json(response) return result def _post_json( @@ -156,6 +181,7 @@ def _post_json( *, content: bytes, params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any]: """POST a pre-serialized JSON body (bytes). @@ -164,10 +190,14 @@ def _post_json( ``model_dump_json(...).encode()``. """ response = self._transport.request( - "POST", path, params=params, content=content, + "POST", + path, + params=params, + content=content, headers={"Content-Type": "application/json"}, + extra_headers=extra_headers, ) - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result def _put( @@ -176,29 +206,54 @@ def _put( *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = self._transport.request("PUT", path, params=params, json=json) + response = self._transport.request( + "PUT", + path, + params=params, + json=json, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result def _delete( - self, path: str, *, params: dict[str, Any] | None = None + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = self._transport.request("DELETE", path, params=params) + response = self._transport.request( + "DELETE", + path, + params=params, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result def _delete_with_body( - self, path: str, *, json: dict[str, Any] + self, + path: str, + *, + json: dict[str, Any], + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = self._transport.request("DELETE", path, json=json) + response = self._transport.request( + "DELETE", + path, + json=json, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result def _delete_with_body_json( @@ -207,15 +262,20 @@ def _delete_with_body_json( *, content: bytes, params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: """DELETE with a pre-serialized JSON body (bytes). See :meth:`_post_json`.""" response = self._transport.request( - "DELETE", path, params=params, content=content, + "DELETE", + path, + params=params, + content=content, headers={"Content-Type": "application/json"}, + extra_headers=extra_headers, ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result def _list( @@ -226,6 +286,7 @@ def _list( *, params: dict[str, Any] | None = None, cursor_key: str = "cursor", + extra_headers: dict[str, str] | None = None, ) -> Page[T]: """Fetch a paginated list endpoint and return a Page[T]. @@ -234,7 +295,7 @@ def _list( every Kalshi endpoint except ``GET /incentive_programs`` which uses ``"next_cursor"``. """ - data = self._get(path, params=params) + data = self._get(path, params=params, extra_headers=extra_headers) # .get(key, []) misses explicit null; or [] coerces both. raw_items = data.get(items_key) or [] items = [model_cls.model_validate(item) for item in raw_items] @@ -250,6 +311,7 @@ def _list_all( params: dict[str, Any] | None = None, max_pages: int | None = None, cursor_key: str = "cursor", + extra_headers: dict[str, str] | None = None, ) -> Iterator[T]: """Auto-paginate through all pages, yielding items. @@ -272,8 +334,12 @@ def _list_all( pages_fetched = 0 while max_pages is None or pages_fetched < max_pages: page = self._list( - path, model_cls, items_key, - params=current_params, cursor_key=cursor_key, + path, + model_cls, + items_key, + params=current_params, + cursor_key=cursor_key, + extra_headers=extra_headers, ) yield from page.items pages_fetched += 1 @@ -303,9 +369,31 @@ def _require_auth(self) -> None: if not self._transport.is_authenticated: raise AuthRequiredError() - async def _get(self, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]: - response = await self._transport.request("GET", path, params=params) - result: dict[str, Any] = response.json() + def _load_json(self, response: httpx.Response) -> Any: + """Parse a response body, honoring ``config.rest_json_loads`` if set. + + Falls back to ``response.json()`` (stdlib) when no custom loader is + configured. Custom loaders receive ``response.content`` (bytes). + """ + loader = self._transport._config.rest_json_loads + if loader is None: + return response.json() + return loader(response.content) + + async def _get( + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any]: + response = await self._transport.request( + "GET", + path, + params=params, + extra_headers=extra_headers, + ) + result: dict[str, Any] = self._load_json(response) return result async def _post( @@ -314,11 +402,16 @@ async def _post( *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any]: response = await self._transport.request( - "POST", path, params=params, json=json, + "POST", + path, + params=params, + json=json, + extra_headers=extra_headers, ) - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _post_json( @@ -327,13 +420,18 @@ async def _post_json( *, content: bytes, params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any]: """Async :meth:`SyncResource._post_json`.""" response = await self._transport.request( - "POST", path, params=params, content=content, + "POST", + path, + params=params, + content=content, headers={"Content-Type": "application/json"}, + extra_headers=extra_headers, ) - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _put( @@ -342,29 +440,54 @@ async def _put( *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = await self._transport.request("PUT", path, params=params, json=json) + response = await self._transport.request( + "PUT", + path, + params=params, + json=json, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _delete( - self, path: str, *, params: dict[str, Any] | None = None + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = await self._transport.request("DELETE", path, params=params) + response = await self._transport.request( + "DELETE", + path, + params=params, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _delete_with_body( - self, path: str, *, json: dict[str, Any] + self, + path: str, + *, + json: dict[str, Any], + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: - response = await self._transport.request("DELETE", path, json=json) + response = await self._transport.request( + "DELETE", + path, + json=json, + extra_headers=extra_headers, + ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _delete_with_body_json( @@ -373,15 +496,20 @@ async def _delete_with_body_json( *, content: bytes, params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, ) -> dict[str, Any] | None: """Async :meth:`SyncResource._delete_with_body_json`.""" response = await self._transport.request( - "DELETE", path, params=params, content=content, + "DELETE", + path, + params=params, + content=content, headers={"Content-Type": "application/json"}, + extra_headers=extra_headers, ) if response.status_code == 204: return None - result: dict[str, Any] = response.json() + result: dict[str, Any] = self._load_json(response) return result async def _list( @@ -392,8 +520,9 @@ async def _list( *, params: dict[str, Any] | None = None, cursor_key: str = "cursor", + extra_headers: dict[str, str] | None = None, ) -> Page[T]: - data = await self._get(path, params=params) + data = await self._get(path, params=params, extra_headers=extra_headers) # .get(key, []) misses explicit null; or [] coerces both. raw_items = data.get(items_key) or [] items = [model_cls.model_validate(item) for item in raw_items] @@ -409,6 +538,7 @@ async def _list_all( params: dict[str, Any] | None = None, max_pages: int | None = None, cursor_key: str = "cursor", + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[T]: """Async counterpart of :meth:`SyncResource._list_all`. @@ -419,8 +549,12 @@ async def _list_all( pages_fetched = 0 while max_pages is None or pages_fetched < max_pages: page = await self._list( - path, model_cls, items_key, - params=current_params, cursor_key=cursor_key, + path, + model_cls, + items_key, + params=current_params, + cursor_key=cursor_key, + extra_headers=extra_headers, ) for item in page.items: yield item diff --git a/kalshi/resources/account.py b/kalshi/resources/account.py index d2242dca..54fcda08 100644 --- a/kalshi/resources/account.py +++ b/kalshi/resources/account.py @@ -9,28 +9,32 @@ class AccountResource(SyncResource): """Sync account API.""" - def limits(self) -> AccountApiLimits: + def limits(self, *, extra_headers: dict[str, str] | None = None) -> AccountApiLimits: self._require_auth() - data = self._get("/account/limits") + data = self._get("/account/limits", extra_headers=extra_headers) return AccountApiLimits.model_validate(data) - def endpoint_costs(self) -> AccountEndpointCosts: + def endpoint_costs( + self, *, extra_headers: dict[str, str] | None = None + ) -> AccountEndpointCosts: """List API v2 endpoints with non-default token costs.""" self._require_auth() - data = self._get("/account/endpoint_costs") + data = self._get("/account/endpoint_costs", extra_headers=extra_headers) return AccountEndpointCosts.model_validate(data) class AsyncAccountResource(AsyncResource): """Async account API.""" - async def limits(self) -> AccountApiLimits: + async def limits(self, *, extra_headers: dict[str, str] | None = None) -> AccountApiLimits: self._require_auth() - data = await self._get("/account/limits") + data = await self._get("/account/limits", extra_headers=extra_headers) return AccountApiLimits.model_validate(data) - async def endpoint_costs(self) -> AccountEndpointCosts: + async def endpoint_costs( + self, *, extra_headers: dict[str, str] | None = None + ) -> AccountEndpointCosts: """List API v2 endpoints with non-default token costs.""" self._require_auth() - data = await self._get("/account/endpoint_costs") + data = await self._get("/account/endpoint_costs", extra_headers=extra_headers) return AccountEndpointCosts.model_validate(data) diff --git a/kalshi/resources/api_keys.py b/kalshi/resources/api_keys.py index 790744b9..fff82271 100644 --- a/kalshi/resources/api_keys.py +++ b/kalshi/resources/api_keys.py @@ -30,16 +30,18 @@ def _build_create_api_key_body( scopes: builtins.list[str] | None, ) -> dict[str, Any]: _check_request_exclusive( - request, name=name, public_key=public_key, scopes=scopes, + request, + name=name, + public_key=public_key, + scopes=scopes, ) if request is None: if name is None or public_key is None: - raise TypeError( - "create() requires `name` and `public_key` " - "(or pass `request=...`)" - ) + raise TypeError("create() requires `name` and `public_key` (or pass `request=...`)") request = CreateApiKeyRequest( - name=name, public_key=public_key, scopes=scopes, + name=name, + public_key=public_key, + scopes=scopes, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -53,9 +55,7 @@ def _build_generate_api_key_body( _check_request_exclusive(request, name=name, scopes=scopes) if request is None: if name is None: - raise TypeError( - "generate() requires `name` (or pass `request=...`)" - ) + raise TypeError("generate() requires `name` (or pass `request=...`)") request = GenerateApiKeyRequest(name=name, scopes=scopes) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -68,13 +68,15 @@ class ApiKeysResource(SyncResource): private key once (see :class:`GenerateApiKeyResponse`). """ - def list(self) -> GetApiKeysResponse: + def list(self, *, extra_headers: dict[str, str] | None = None) -> GetApiKeysResponse: self._require_auth() - data = self._get("/api_keys") + data = self._get("/api_keys", extra_headers=extra_headers) return GetApiKeysResponse.model_validate(data) @overload - def create(self, *, request: CreateApiKeyRequest) -> CreateApiKeyResponse: ... + def create( + self, *, request: CreateApiKeyRequest, extra_headers: dict[str, str] | None = None + ) -> CreateApiKeyResponse: ... @overload def create( self, @@ -82,6 +84,7 @@ def create( name: str, public_key: str, scopes: builtins.list[str] | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateApiKeyResponse: ... def create( self, @@ -90,22 +93,29 @@ def create( name: str | None = None, public_key: str | None = None, scopes: builtins.list[str] | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateApiKeyResponse: self._require_auth() body = _build_create_api_key_body( - request, name=name, public_key=public_key, scopes=scopes, + request, + name=name, + public_key=public_key, + scopes=scopes, ) - data = self._post("/api_keys", json=body) + data = self._post("/api_keys", json=body, extra_headers=extra_headers) return CreateApiKeyResponse.model_validate(data) @overload - def generate(self, *, request: GenerateApiKeyRequest) -> GenerateApiKeyResponse: ... + def generate( + self, *, request: GenerateApiKeyRequest, extra_headers: dict[str, str] | None = None + ) -> GenerateApiKeyResponse: ... @overload def generate( self, *, name: str, scopes: builtins.list[str] | None = ..., + extra_headers: dict[str, str] | None = None, ) -> GenerateApiKeyResponse: ... def generate( self, @@ -113,27 +123,30 @@ def generate( request: GenerateApiKeyRequest | None = None, name: str | None = None, scopes: builtins.list[str] | None = None, + extra_headers: dict[str, str] | None = None, ) -> GenerateApiKeyResponse: self._require_auth() body = _build_generate_api_key_body(request, name=name, scopes=scopes) - data = self._post("/api_keys/generate", json=body) + data = self._post("/api_keys/generate", json=body, extra_headers=extra_headers) return GenerateApiKeyResponse.model_validate(data) - def delete(self, api_key: str) -> None: + def delete(self, api_key: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() - self._delete(f"/api_keys/{_seg(api_key, name='api_key')}") + self._delete(f"/api_keys/{_seg(api_key, name='api_key')}", extra_headers=extra_headers) class AsyncApiKeysResource(AsyncResource): """Async API keys resource.""" - async def list(self) -> GetApiKeysResponse: + async def list(self, *, extra_headers: dict[str, str] | None = None) -> GetApiKeysResponse: self._require_auth() - data = await self._get("/api_keys") + data = await self._get("/api_keys", extra_headers=extra_headers) return GetApiKeysResponse.model_validate(data) @overload - async def create(self, *, request: CreateApiKeyRequest) -> CreateApiKeyResponse: ... + async def create( + self, *, request: CreateApiKeyRequest, extra_headers: dict[str, str] | None = None + ) -> CreateApiKeyResponse: ... @overload async def create( self, @@ -141,6 +154,7 @@ async def create( name: str, public_key: str, scopes: builtins.list[str] | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateApiKeyResponse: ... async def create( self, @@ -149,17 +163,21 @@ async def create( name: str | None = None, public_key: str | None = None, scopes: builtins.list[str] | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateApiKeyResponse: self._require_auth() body = _build_create_api_key_body( - request, name=name, public_key=public_key, scopes=scopes, + request, + name=name, + public_key=public_key, + scopes=scopes, ) - data = await self._post("/api_keys", json=body) + data = await self._post("/api_keys", json=body, extra_headers=extra_headers) return CreateApiKeyResponse.model_validate(data) @overload async def generate( - self, *, request: GenerateApiKeyRequest, + self, *, request: GenerateApiKeyRequest, extra_headers: dict[str, str] | None = None ) -> GenerateApiKeyResponse: ... @overload async def generate( @@ -167,6 +185,7 @@ async def generate( *, name: str, scopes: builtins.list[str] | None = ..., + extra_headers: dict[str, str] | None = None, ) -> GenerateApiKeyResponse: ... async def generate( self, @@ -174,12 +193,15 @@ async def generate( request: GenerateApiKeyRequest | None = None, name: str | None = None, scopes: builtins.list[str] | None = None, + extra_headers: dict[str, str] | None = None, ) -> GenerateApiKeyResponse: self._require_auth() body = _build_generate_api_key_body(request, name=name, scopes=scopes) - data = await self._post("/api_keys/generate", json=body) + data = await self._post("/api_keys/generate", json=body, extra_headers=extra_headers) return GenerateApiKeyResponse.model_validate(data) - async def delete(self, api_key: str) -> None: + async def delete(self, api_key: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() - await self._delete(f"/api_keys/{_seg(api_key, name='api_key')}") + await self._delete( + f"/api_keys/{_seg(api_key, name='api_key')}", extra_headers=extra_headers + ) diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index 5832078b..f137f77e 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -128,16 +128,18 @@ def _build_create_rfq_body( ) -> dict[str, Any]: _check_request_exclusive( request, - market_ticker=market_ticker, rest_remainder=rest_remainder, - contracts=contracts, target_cost=target_cost, - replace_existing=replace_existing, subtrader_id=subtrader_id, + market_ticker=market_ticker, + rest_remainder=rest_remainder, + contracts=contracts, + target_cost=target_cost, + replace_existing=replace_existing, + subtrader_id=subtrader_id, subaccount=subaccount, ) if request is None: if market_ticker is None or rest_remainder is None: raise TypeError( - "create_rfq() requires `market_ticker` and `rest_remainder` " - "(or pass `request=...`)" + "create_rfq() requires `market_ticker` and `rest_remainder` (or pass `request=...`)" ) request = CreateRFQRequest( market_ticker=market_ticker, @@ -162,15 +164,16 @@ def _build_create_quote_body( post_only: bool | None, ) -> dict[str, Any]: _check_request_exclusive( - request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, - rest_remainder=rest_remainder, subaccount=subaccount, + request, + rfq_id=rfq_id, + yes_bid=yes_bid, + no_bid=no_bid, + rest_remainder=rest_remainder, + subaccount=subaccount, post_only=post_only, ) if request is None: - if ( - rfq_id is None or yes_bid is None or no_bid is None - or rest_remainder is None - ): + if rfq_id is None or yes_bid is None or no_bid is None or rest_remainder is None: raise TypeError( "create_quote() requires `rfq_id`, `yes_bid`, `no_bid`, and " "`rest_remainder` (or pass `request=...`)" @@ -194,10 +197,7 @@ def _build_accept_quote_body( _check_request_exclusive(request, accepted_side=accepted_side) if request is None: if accepted_side is None: - raise TypeError( - "accept_quote() requires `accepted_side` " - "(or pass `request=...`)" - ) + raise TypeError("accept_quote() requires `accepted_side` (or pass `request=...`)") request = AcceptQuoteRequest(accepted_side=accepted_side) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -205,9 +205,9 @@ def _build_accept_quote_body( class CommunicationsResource(SyncResource): """Sync communications / RFQ API.""" - def get_id(self) -> GetCommunicationsIDResponse: + def get_id(self, *, extra_headers: dict[str, str] | None = None) -> GetCommunicationsIDResponse: self._require_auth() - data = self._get("/communications/id") + data = self._get("/communications/id", extra_headers=extra_headers) return GetCommunicationsIDResponse.model_validate(data) def list_rfqs( @@ -221,15 +221,22 @@ def list_rfqs( status: str | None = None, creator_user_id: str | None = None, user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[RFQ]: self._require_auth() params = _list_rfqs_params( - cursor=cursor, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, subaccount=subaccount, - status=status, creator_user_id=creator_user_id, + cursor=cursor, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, user_filter=user_filter, ) - return self._list("/communications/rfqs", RFQ, "rfqs", params=params) + return self._list( + "/communications/rfqs", RFQ, "rfqs", params=params, extra_headers=extra_headers + ) def list_all_rfqs( self, @@ -242,27 +249,42 @@ def list_all_rfqs( creator_user_id: str | None = None, user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[RFQ]: self._require_auth() _validate_max_pages(max_pages) params = _list_rfqs_params( - cursor=None, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, subaccount=subaccount, - status=status, creator_user_id=creator_user_id, + cursor=None, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, user_filter=user_filter, ) return self._list_all( - "/communications/rfqs", RFQ, "rfqs", - params=params, max_pages=max_pages, + "/communications/rfqs", + RFQ, + "rfqs", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def get_rfq(self, rfq_id: str) -> GetRFQResponse: + def get_rfq( + self, rfq_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQResponse: self._require_auth() - data = self._get(f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}") + data = self._get( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}", extra_headers=extra_headers + ) return GetRFQResponse.model_validate(data) @overload - def create_rfq(self, *, request: CreateRFQRequest) -> CreateRFQResponse: ... + def create_rfq( + self, *, request: CreateRFQRequest, extra_headers: dict[str, str] | None = None + ) -> CreateRFQResponse: ... @overload def create_rfq( self, @@ -274,6 +296,7 @@ def create_rfq( replace_existing: bool | None = ..., subtrader_id: str | None = ..., subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... def create_rfq( self, @@ -286,21 +309,27 @@ def create_rfq( replace_existing: bool | None = None, subtrader_id: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: self._require_auth() body = _build_create_rfq_body( request, - market_ticker=market_ticker, rest_remainder=rest_remainder, - contracts=contracts, target_cost=target_cost, - replace_existing=replace_existing, subtrader_id=subtrader_id, + market_ticker=market_ticker, + rest_remainder=rest_remainder, + contracts=contracts, + target_cost=target_cost, + replace_existing=replace_existing, + subtrader_id=subtrader_id, subaccount=subaccount, ) - data = self._post("/communications/rfqs", json=body) + data = self._post("/communications/rfqs", json=body, extra_headers=extra_headers) return CreateRFQResponse.model_validate(data) - def delete_rfq(self, rfq_id: str) -> None: + def delete_rfq(self, rfq_id: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() - self._delete(f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}") + self._delete( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}", extra_headers=extra_headers + ) def list_quotes( self, @@ -316,15 +345,21 @@ def list_quotes( rfq_id: str | None = None, user_filter: UserFilterLiteral | None = None, rfq_user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Quote]: self._require_auth() _require_quote_filter( - quote_creator_user_id, rfq_creator_user_id, - user_filter, rfq_user_filter, + quote_creator_user_id, + rfq_creator_user_id, + user_filter, + rfq_user_filter, ) params = _list_quotes_params( - cursor=cursor, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, status=status, + cursor=cursor, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, @@ -332,7 +367,9 @@ def list_quotes( user_filter=user_filter, rfq_user_filter=rfq_user_filter, ) - return self._list("/communications/quotes", Quote, "quotes", params=params) + return self._list( + "/communications/quotes", Quote, "quotes", params=params, extra_headers=extra_headers + ) def list_all_quotes( self, @@ -348,16 +385,22 @@ def list_all_quotes( user_filter: UserFilterLiteral | None = None, rfq_user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Quote]: self._require_auth() _require_quote_filter( - quote_creator_user_id, rfq_creator_user_id, - user_filter, rfq_user_filter, + quote_creator_user_id, + rfq_creator_user_id, + user_filter, + rfq_user_filter, ) _validate_max_pages(max_pages) params = _list_quotes_params( - cursor=None, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, status=status, + cursor=None, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, @@ -366,17 +409,27 @@ def list_all_quotes( rfq_user_filter=rfq_user_filter, ) return self._list_all( - "/communications/quotes", Quote, "quotes", - params=params, max_pages=max_pages, + "/communications/quotes", + Quote, + "quotes", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def get_quote(self, quote_id: str) -> GetQuoteResponse: + def get_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetQuoteResponse: self._require_auth() - data = self._get(f"/communications/quotes/{_seg(quote_id, name='quote_id')}") + data = self._get( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}", extra_headers=extra_headers + ) return GetQuoteResponse.model_validate(data) @overload - def create_quote(self, *, request: CreateQuoteRequest) -> CreateQuoteResponse: ... + def create_quote( + self, *, request: CreateQuoteRequest, extra_headers: dict[str, str] | None = None + ) -> CreateQuoteResponse: ... @overload def create_quote( self, @@ -387,6 +440,7 @@ def create_quote( rest_remainder: bool, subaccount: int | None = ..., post_only: bool | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: ... def create_quote( self, @@ -398,27 +452,42 @@ def create_quote( rest_remainder: bool | None = None, subaccount: int | None = None, post_only: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: self._require_auth() body = _build_create_quote_body( - request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, - rest_remainder=rest_remainder, subaccount=subaccount, + request, + rfq_id=rfq_id, + yes_bid=yes_bid, + no_bid=no_bid, + rest_remainder=rest_remainder, + subaccount=subaccount, post_only=post_only, ) - data = self._post("/communications/quotes", json=body) + data = self._post("/communications/quotes", json=body, extra_headers=extra_headers) return CreateQuoteResponse.model_validate(data) - def delete_quote(self, quote_id: str) -> None: + def delete_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() - self._delete(f"/communications/quotes/{_seg(quote_id, name='quote_id')}") + self._delete( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}", extra_headers=extra_headers + ) @overload def accept_quote( - self, quote_id: str, *, request: AcceptQuoteRequest, + self, + quote_id: str, + *, + request: AcceptQuoteRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload def accept_quote( - self, quote_id: str, *, accepted_side: Literal["yes", "no"], + self, + quote_id: str, + *, + accepted_side: Literal["yes", "no"], + extra_headers: dict[str, str] | None = None, ) -> None: ... def accept_quote( self, @@ -426,23 +495,34 @@ def accept_quote( *, request: AcceptQuoteRequest | None = None, accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_accept_quote_body(request, accepted_side=accepted_side) - self._put(f"/communications/quotes/{_seg(quote_id, name='quote_id')}/accept", json=body) + self._put( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}/accept", + json=body, + extra_headers=extra_headers, + ) - def confirm_quote(self, quote_id: str) -> None: + def confirm_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() # json={} forces Content-Type: application/json — demo rejects empty PUTs. - self._put(f"/communications/quotes/{_seg(quote_id, name='quote_id')}/confirm", json={}) + self._put( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) class AsyncCommunicationsResource(AsyncResource): """Async communications / RFQ API.""" - async def get_id(self) -> GetCommunicationsIDResponse: + async def get_id( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetCommunicationsIDResponse: self._require_auth() - data = await self._get("/communications/id") + data = await self._get("/communications/id", extra_headers=extra_headers) return GetCommunicationsIDResponse.model_validate(data) async def list_rfqs( @@ -456,15 +536,22 @@ async def list_rfqs( status: str | None = None, creator_user_id: str | None = None, user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[RFQ]: self._require_auth() params = _list_rfqs_params( - cursor=cursor, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, subaccount=subaccount, - status=status, creator_user_id=creator_user_id, + cursor=cursor, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, user_filter=user_filter, ) - return await self._list("/communications/rfqs", RFQ, "rfqs", params=params) + return await self._list( + "/communications/rfqs", RFQ, "rfqs", params=params, extra_headers=extra_headers + ) def list_all_rfqs( self, @@ -477,28 +564,43 @@ def list_all_rfqs( creator_user_id: str | None = None, user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[RFQ]: # Plain `def` so _require_auth + _validate_max_pages run at call time. self._require_auth() _validate_max_pages(max_pages) params = _list_rfqs_params( - cursor=None, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, subaccount=subaccount, - status=status, creator_user_id=creator_user_id, + cursor=None, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, user_filter=user_filter, ) return self._list_all( - "/communications/rfqs", RFQ, "rfqs", - params=params, max_pages=max_pages, + "/communications/rfqs", + RFQ, + "rfqs", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def get_rfq(self, rfq_id: str) -> GetRFQResponse: + async def get_rfq( + self, rfq_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQResponse: self._require_auth() - data = await self._get(f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}") + data = await self._get( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}", extra_headers=extra_headers + ) return GetRFQResponse.model_validate(data) @overload - async def create_rfq(self, *, request: CreateRFQRequest) -> CreateRFQResponse: ... + async def create_rfq( + self, *, request: CreateRFQRequest, extra_headers: dict[str, str] | None = None + ) -> CreateRFQResponse: ... @overload async def create_rfq( self, @@ -510,6 +612,7 @@ async def create_rfq( replace_existing: bool | None = ..., subtrader_id: str | None = ..., subaccount: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... async def create_rfq( self, @@ -522,21 +625,27 @@ async def create_rfq( replace_existing: bool | None = None, subtrader_id: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: self._require_auth() body = _build_create_rfq_body( request, - market_ticker=market_ticker, rest_remainder=rest_remainder, - contracts=contracts, target_cost=target_cost, - replace_existing=replace_existing, subtrader_id=subtrader_id, + market_ticker=market_ticker, + rest_remainder=rest_remainder, + contracts=contracts, + target_cost=target_cost, + replace_existing=replace_existing, + subtrader_id=subtrader_id, subaccount=subaccount, ) - data = await self._post("/communications/rfqs", json=body) + data = await self._post("/communications/rfqs", json=body, extra_headers=extra_headers) return CreateRFQResponse.model_validate(data) - async def delete_rfq(self, rfq_id: str) -> None: + async def delete_rfq(self, rfq_id: str, *, extra_headers: dict[str, str] | None = None) -> None: self._require_auth() - await self._delete(f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}") + await self._delete( + f"/communications/rfqs/{_seg(rfq_id, name='rfq_id')}", extra_headers=extra_headers + ) async def list_quotes( self, @@ -552,15 +661,21 @@ async def list_quotes( rfq_id: str | None = None, user_filter: UserFilterLiteral | None = None, rfq_user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Quote]: self._require_auth() _require_quote_filter( - quote_creator_user_id, rfq_creator_user_id, - user_filter, rfq_user_filter, + quote_creator_user_id, + rfq_creator_user_id, + user_filter, + rfq_user_filter, ) params = _list_quotes_params( - cursor=cursor, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, status=status, + cursor=cursor, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, @@ -569,7 +684,7 @@ async def list_quotes( rfq_user_filter=rfq_user_filter, ) return await self._list( - "/communications/quotes", Quote, "quotes", params=params, + "/communications/quotes", Quote, "quotes", params=params, extra_headers=extra_headers ) def list_all_quotes( @@ -586,16 +701,22 @@ def list_all_quotes( user_filter: UserFilterLiteral | None = None, rfq_user_filter: UserFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Quote]: self._require_auth() _require_quote_filter( - quote_creator_user_id, rfq_creator_user_id, - user_filter, rfq_user_filter, + quote_creator_user_id, + rfq_creator_user_id, + user_filter, + rfq_user_filter, ) _validate_max_pages(max_pages) params = _list_quotes_params( - cursor=None, limit=limit, event_ticker=event_ticker, - market_ticker=market_ticker, status=status, + cursor=None, + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + status=status, quote_creator_user_id=quote_creator_user_id, rfq_creator_user_id=rfq_creator_user_id, rfq_creator_subtrader_id=rfq_creator_subtrader_id, @@ -604,18 +725,26 @@ def list_all_quotes( rfq_user_filter=rfq_user_filter, ) return self._list_all( - "/communications/quotes", Quote, "quotes", - params=params, max_pages=max_pages, + "/communications/quotes", + Quote, + "quotes", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def get_quote(self, quote_id: str) -> GetQuoteResponse: + async def get_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetQuoteResponse: self._require_auth() - data = await self._get(f"/communications/quotes/{_seg(quote_id, name='quote_id')}") + data = await self._get( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}", extra_headers=extra_headers + ) return GetQuoteResponse.model_validate(data) @overload async def create_quote( - self, *, request: CreateQuoteRequest, + self, *, request: CreateQuoteRequest, extra_headers: dict[str, str] | None = None ) -> CreateQuoteResponse: ... @overload async def create_quote( @@ -627,6 +756,7 @@ async def create_quote( rest_remainder: bool, subaccount: int | None = ..., post_only: bool | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: ... async def create_quote( self, @@ -638,27 +768,44 @@ async def create_quote( rest_remainder: bool | None = None, subaccount: int | None = None, post_only: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: self._require_auth() body = _build_create_quote_body( - request, rfq_id=rfq_id, yes_bid=yes_bid, no_bid=no_bid, - rest_remainder=rest_remainder, subaccount=subaccount, + request, + rfq_id=rfq_id, + yes_bid=yes_bid, + no_bid=no_bid, + rest_remainder=rest_remainder, + subaccount=subaccount, post_only=post_only, ) - data = await self._post("/communications/quotes", json=body) + data = await self._post("/communications/quotes", json=body, extra_headers=extra_headers) return CreateQuoteResponse.model_validate(data) - async def delete_quote(self, quote_id: str) -> None: + async def delete_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: self._require_auth() - await self._delete(f"/communications/quotes/{_seg(quote_id, name='quote_id')}") + await self._delete( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}", extra_headers=extra_headers + ) @overload async def accept_quote( - self, quote_id: str, *, request: AcceptQuoteRequest, + self, + quote_id: str, + *, + request: AcceptQuoteRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload async def accept_quote( - self, quote_id: str, *, accepted_side: Literal["yes", "no"], + self, + quote_id: str, + *, + accepted_side: Literal["yes", "no"], + extra_headers: dict[str, str] | None = None, ) -> None: ... async def accept_quote( self, @@ -666,12 +813,23 @@ async def accept_quote( *, request: AcceptQuoteRequest | None = None, accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_accept_quote_body(request, accepted_side=accepted_side) - await self._put(f"/communications/quotes/{_seg(quote_id, name='quote_id')}/accept", json=body) # noqa: E501 + await self._put( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}/accept", + json=body, + extra_headers=extra_headers, + ) - async def confirm_quote(self, quote_id: str) -> None: + async def confirm_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: self._require_auth() # json={} forces Content-Type: application/json — demo rejects empty PUTs. - await self._put(f"/communications/quotes/{_seg(quote_id, name='quote_id')}/confirm", json={}) # noqa: E501 + await self._put( + f"/communications/quotes/{_seg(quote_id, name='quote_id')}/confirm", + json={}, + extra_headers=extra_headers, + ) diff --git a/kalshi/resources/events.py b/kalshi/resources/events.py index 5d014b81..0cafbb43 100644 --- a/kalshi/resources/events.py +++ b/kalshi/resources/events.py @@ -76,15 +76,19 @@ def list( min_updated_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Event]: params = _list_events_params( - status=status, series_ticker=series_ticker, + status=status, + series_ticker=series_ticker, with_nested_markets=with_nested_markets, with_milestones=with_milestones, - min_close_ts=min_close_ts, min_updated_ts=min_updated_ts, - limit=limit, cursor=cursor, + min_close_ts=min_close_ts, + min_updated_ts=min_updated_ts, + limit=limit, + cursor=cursor, ) - return self._list("/events", Event, "events", params=params) + return self._list("/events", Event, "events", params=params, extra_headers=extra_headers) def list_all( self, @@ -97,17 +101,26 @@ def list_all( min_updated_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Event]: _validate_max_pages(max_pages) params = _list_events_params( - status=status, series_ticker=series_ticker, + status=status, + series_ticker=series_ticker, with_nested_markets=with_nested_markets, with_milestones=with_milestones, - min_close_ts=min_close_ts, min_updated_ts=min_updated_ts, - limit=limit, cursor=None, + min_close_ts=min_close_ts, + min_updated_ts=min_updated_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/events", Event, "events", params=params, max_pages=max_pages, + "/events", + Event, + "events", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def list_multivariate( @@ -118,14 +131,18 @@ def list_multivariate( with_nested_markets: bool | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Event]: params = _list_multivariate_events_params( series_ticker=series_ticker, collection_ticker=collection_ticker, with_nested_markets=with_nested_markets, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, + ) + return self._list( + "/events/multivariate", Event, "events", params=params, extra_headers=extra_headers ) - return self._list("/events/multivariate", Event, "events", params=params) def list_all_multivariate( self, @@ -135,17 +152,23 @@ def list_all_multivariate( with_nested_markets: bool | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Event]: _validate_max_pages(max_pages) params = _list_multivariate_events_params( series_ticker=series_ticker, collection_ticker=collection_ticker, with_nested_markets=with_nested_markets, - limit=limit, cursor=None, + limit=limit, + cursor=None, ) return self._list_all( - "/events/multivariate", Event, "events", - params=params, max_pages=max_pages, + "/events/multivariate", + Event, + "events", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def get( @@ -153,15 +176,25 @@ def get( event_ticker: str, *, with_nested_markets: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> Event: params = _params( with_nested_markets=_bool_param(with_nested_markets), ) - data = self._get(f"/events/{_seg(event_ticker, name='event_ticker')}", params=params) + data = self._get( + f"/events/{_seg(event_ticker, name='event_ticker')}", + params=params, + extra_headers=extra_headers, + ) return Event.model_validate(data.get("event", data)) - def metadata(self, event_ticker: str) -> EventMetadata: - data = self._get(f"/events/{_seg(event_ticker, name='event_ticker')}/metadata") + def metadata( + self, event_ticker: str, *, extra_headers: dict[str, str] | None = None + ) -> EventMetadata: + data = self._get( + f"/events/{_seg(event_ticker, name='event_ticker')}/metadata", + extra_headers=extra_headers, + ) return EventMetadata.model_validate(data) @@ -179,15 +212,21 @@ async def list( min_updated_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Event]: params = _list_events_params( - status=status, series_ticker=series_ticker, + status=status, + series_ticker=series_ticker, with_nested_markets=with_nested_markets, with_milestones=with_milestones, - min_close_ts=min_close_ts, min_updated_ts=min_updated_ts, - limit=limit, cursor=cursor, + min_close_ts=min_close_ts, + min_updated_ts=min_updated_ts, + limit=limit, + cursor=cursor, + ) + return await self._list( + "/events", Event, "events", params=params, extra_headers=extra_headers ) - return await self._list("/events", Event, "events", params=params) def list_all( self, @@ -200,17 +239,26 @@ def list_all( min_updated_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Event]: _validate_max_pages(max_pages) params = _list_events_params( - status=status, series_ticker=series_ticker, + status=status, + series_ticker=series_ticker, with_nested_markets=with_nested_markets, with_milestones=with_milestones, - min_close_ts=min_close_ts, min_updated_ts=min_updated_ts, - limit=limit, cursor=None, + min_close_ts=min_close_ts, + min_updated_ts=min_updated_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/events", Event, "events", params=params, max_pages=max_pages, + "/events", + Event, + "events", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def list_multivariate( @@ -221,14 +269,18 @@ async def list_multivariate( with_nested_markets: bool | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Event]: params = _list_multivariate_events_params( series_ticker=series_ticker, collection_ticker=collection_ticker, with_nested_markets=with_nested_markets, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, + ) + return await self._list( + "/events/multivariate", Event, "events", params=params, extra_headers=extra_headers ) - return await self._list("/events/multivariate", Event, "events", params=params) def list_all_multivariate( self, @@ -238,17 +290,23 @@ def list_all_multivariate( with_nested_markets: bool | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Event]: _validate_max_pages(max_pages) params = _list_multivariate_events_params( series_ticker=series_ticker, collection_ticker=collection_ticker, with_nested_markets=with_nested_markets, - limit=limit, cursor=None, + limit=limit, + cursor=None, ) return self._list_all( - "/events/multivariate", Event, "events", - params=params, max_pages=max_pages, + "/events/multivariate", + Event, + "events", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def get( @@ -256,13 +314,23 @@ async def get( event_ticker: str, *, with_nested_markets: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> Event: params = _params( with_nested_markets=_bool_param(with_nested_markets), ) - data = await self._get(f"/events/{_seg(event_ticker, name='event_ticker')}", params=params) + data = await self._get( + f"/events/{_seg(event_ticker, name='event_ticker')}", + params=params, + extra_headers=extra_headers, + ) return Event.model_validate(data.get("event", data)) - async def metadata(self, event_ticker: str) -> EventMetadata: - data = await self._get(f"/events/{_seg(event_ticker, name='event_ticker')}/metadata") + async def metadata( + self, event_ticker: str, *, extra_headers: dict[str, str] | None = None + ) -> EventMetadata: + data = await self._get( + f"/events/{_seg(event_ticker, name='event_ticker')}/metadata", + extra_headers=extra_headers, + ) return EventMetadata.model_validate(data) diff --git a/kalshi/resources/exchange.py b/kalshi/resources/exchange.py index 3e423e90..d82b8b21 100644 --- a/kalshi/resources/exchange.py +++ b/kalshi/resources/exchange.py @@ -16,49 +16,57 @@ class ExchangeResource(SyncResource): """Sync exchange API.""" - def status(self) -> ExchangeStatus: - data = self._get("/exchange/status") + def status(self, *, extra_headers: dict[str, str] | None = None) -> ExchangeStatus: + data = self._get("/exchange/status", extra_headers=extra_headers) return ExchangeStatus.model_validate(data) - def schedule(self) -> Schedule: - data = self._get("/exchange/schedule") + def schedule(self, *, extra_headers: dict[str, str] | None = None) -> Schedule: + data = self._get("/exchange/schedule", extra_headers=extra_headers) raw = data.get("schedule", data) return Schedule.model_validate(raw) - def announcements(self) -> builtins.list[Announcement]: - data = self._get("/exchange/announcements") + def announcements( + self, *, extra_headers: dict[str, str] | None = None + ) -> builtins.list[Announcement]: + data = self._get("/exchange/announcements", extra_headers=extra_headers) raw = data.get("announcements", []) return [Announcement.model_validate(a) for a in raw] - def user_data_timestamp(self) -> UserDataTimestamp: + def user_data_timestamp( + self, *, extra_headers: dict[str, str] | None = None + ) -> UserDataTimestamp: # Spec has no security block, but the endpoint reports lag on # user-scoped routes (balance/orders/fills/positions). Guard # client-side so unauth callers get a clear AuthRequiredError # instead of a server-side 401. self._require_auth() - data = self._get("/exchange/user_data_timestamp") + data = self._get("/exchange/user_data_timestamp", extra_headers=extra_headers) return UserDataTimestamp.model_validate(data) class AsyncExchangeResource(AsyncResource): """Async exchange API.""" - async def status(self) -> ExchangeStatus: - data = await self._get("/exchange/status") + async def status(self, *, extra_headers: dict[str, str] | None = None) -> ExchangeStatus: + data = await self._get("/exchange/status", extra_headers=extra_headers) return ExchangeStatus.model_validate(data) - async def schedule(self) -> Schedule: - data = await self._get("/exchange/schedule") + async def schedule(self, *, extra_headers: dict[str, str] | None = None) -> Schedule: + data = await self._get("/exchange/schedule", extra_headers=extra_headers) raw = data.get("schedule", data) return Schedule.model_validate(raw) - async def announcements(self) -> builtins.list[Announcement]: - data = await self._get("/exchange/announcements") + async def announcements( + self, *, extra_headers: dict[str, str] | None = None + ) -> builtins.list[Announcement]: + data = await self._get("/exchange/announcements", extra_headers=extra_headers) raw = data.get("announcements", []) return [Announcement.model_validate(a) for a in raw] - async def user_data_timestamp(self) -> UserDataTimestamp: + async def user_data_timestamp( + self, *, extra_headers: dict[str, str] | None = None + ) -> UserDataTimestamp: # See sync note on auth guard (user-scoped endpoint, spec omits security). self._require_auth() - data = await self._get("/exchange/user_data_timestamp") + data = await self._get("/exchange/user_data_timestamp", extra_headers=extra_headers) return UserDataTimestamp.model_validate(data) diff --git a/kalshi/resources/fcm.py b/kalshi/resources/fcm.py index 353f6d3a..ad113ce9 100644 --- a/kalshi/resources/fcm.py +++ b/kalshi/resources/fcm.py @@ -89,14 +89,22 @@ def orders( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _fcm_orders_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, + ) + return self._list( + "/fcm/orders", Order, "orders", params=params, extra_headers=extra_headers ) - return self._list("/fcm/orders", Order, "orders", params=params) def orders_all( self, @@ -109,17 +117,27 @@ def orders_all( max_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Order]: self._require_auth() _validate_max_pages(max_pages) params = _fcm_orders_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/fcm/orders", Order, "orders", - params=params, max_pages=max_pages, + "/fcm/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def positions( @@ -132,15 +150,19 @@ def positions( settlement_status: SettlementStatusLiteral | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> PositionsResponse: self._require_auth() params = _fcm_positions_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, count_filter=count_filter, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + count_filter=count_filter, settlement_status=settlement_status, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, ) - data = self._get("/fcm/positions", params=params) + data = self._get("/fcm/positions", params=params, extra_headers=extra_headers) return PositionsResponse.model_validate(data) @@ -158,14 +180,22 @@ async def orders( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _fcm_orders_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, + ) + return await self._list( + "/fcm/orders", Order, "orders", params=params, extra_headers=extra_headers ) - return await self._list("/fcm/orders", Order, "orders", params=params) def orders_all( self, @@ -178,18 +208,28 @@ def orders_all( max_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Order]: """Returns an async iterator — use ``async for``.""" self._require_auth() _validate_max_pages(max_pages) params = _fcm_orders_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/fcm/orders", Order, "orders", - params=params, max_pages=max_pages, + "/fcm/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def positions( @@ -202,13 +242,17 @@ async def positions( settlement_status: SettlementStatusLiteral | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> PositionsResponse: self._require_auth() params = _fcm_positions_params( - subtrader_id=subtrader_id, ticker=ticker, - event_ticker=event_ticker, count_filter=count_filter, + subtrader_id=subtrader_id, + ticker=ticker, + event_ticker=event_ticker, + count_filter=count_filter, settlement_status=settlement_status, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, ) - data = await self._get("/fcm/positions", params=params) + data = await self._get("/fcm/positions", params=params, extra_headers=extra_headers) return PositionsResponse.model_validate(data) diff --git a/kalshi/resources/historical.py b/kalshi/resources/historical.py index ebd453fd..55138a02 100644 --- a/kalshi/resources/historical.py +++ b/kalshi/resources/historical.py @@ -48,7 +48,10 @@ def _historical_markets_params( def _historical_candlesticks_params( - *, start_ts: int, end_ts: int, period_interval: int, + *, + start_ts: int, + end_ts: int, + period_interval: int, ) -> dict[str, Any]: return _params( period_interval=period_interval, @@ -78,15 +81,19 @@ def _historical_trades_params( ) -> dict[str, Any]: limit = _validate_limit(limit, hi=1000, lo=0) return _params( - limit=limit, cursor=cursor, ticker=ticker, min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, ) class HistoricalResource(SyncResource): """Sync historical data API.""" - def cutoff(self) -> HistoricalCutoff: - data = self._get("/historical/cutoff") + def cutoff(self, *, extra_headers: dict[str, str] | None = None) -> HistoricalCutoff: + data = self._get("/historical/cutoff", extra_headers=extra_headers) return HistoricalCutoff.model_validate(data) def markets( @@ -98,13 +105,19 @@ def markets( event_ticker: str | None = None, series_ticker: str | None = None, mve_filter: MveHistoricalFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Market]: params = _historical_markets_params( - limit=limit, cursor=cursor, tickers=tickers, - event_ticker=event_ticker, series_ticker=series_ticker, + limit=limit, + cursor=cursor, + tickers=tickers, + event_ticker=event_ticker, + series_ticker=series_ticker, mve_filter=mve_filter, ) - return self._list("/historical/markets", Market, "markets", params=params) + return self._list( + "/historical/markets", Market, "markets", params=params, extra_headers=extra_headers + ) def markets_all( self, @@ -115,20 +128,30 @@ def markets_all( series_ticker: str | None = None, mve_filter: MveHistoricalFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Market]: _validate_max_pages(max_pages) params = _historical_markets_params( - limit=limit, cursor=None, tickers=tickers, - event_ticker=event_ticker, series_ticker=series_ticker, + limit=limit, + cursor=None, + tickers=tickers, + event_ticker=event_ticker, + series_ticker=series_ticker, mve_filter=mve_filter, ) return self._list_all( - "/historical/markets", Market, "markets", - params=params, max_pages=max_pages, + "/historical/markets", + Market, + "markets", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def market(self, ticker: str) -> Market: - data = self._get(f"/historical/markets/{_seg(ticker, name='ticker')}") + def market(self, ticker: str, *, extra_headers: dict[str, str] | None = None) -> Market: + data = self._get( + f"/historical/markets/{_seg(ticker, name='ticker')}", extra_headers=extra_headers + ) raw = data.get("market", data) return Market.model_validate(raw) @@ -139,14 +162,17 @@ def candlesticks( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Candlestick]: params = _historical_candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = self._get( f"/historical/markets/{_seg(ticker, name='ticker')}/candlesticks", params=params, + extra_headers=extra_headers, ) raw = data.get("candlesticks", []) return [Candlestick.model_validate(c) for c in raw] @@ -158,12 +184,18 @@ def fills( cursor: str | None = None, ticker: str | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Fill]: self._require_auth() params = _historical_fills_or_orders_params( - limit=limit, cursor=cursor, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + max_ts=max_ts, + ) + return self._list( + "/historical/fills", Fill, "fills", params=params, extra_headers=extra_headers ) - return self._list("/historical/fills", Fill, "fills", params=params) def fills_all( self, @@ -172,15 +204,23 @@ def fills_all( ticker: str | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Fill]: self._require_auth() _validate_max_pages(max_pages) params = _historical_fills_or_orders_params( - limit=limit, cursor=None, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + max_ts=max_ts, ) return self._list_all( - "/historical/fills", Fill, "fills", - params=params, max_pages=max_pages, + "/historical/fills", + Fill, + "fills", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def orders( @@ -190,12 +230,18 @@ def orders( cursor: str | None = None, ticker: str | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _historical_fills_or_orders_params( - limit=limit, cursor=cursor, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + max_ts=max_ts, + ) + return self._list( + "/historical/orders", Order, "orders", params=params, extra_headers=extra_headers ) - return self._list("/historical/orders", Order, "orders", params=params) def orders_all( self, @@ -204,15 +250,23 @@ def orders_all( ticker: str | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Order]: self._require_auth() _validate_max_pages(max_pages) params = _historical_fills_or_orders_params( - limit=limit, cursor=None, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + max_ts=max_ts, ) return self._list_all( - "/historical/orders", Order, "orders", - params=params, max_pages=max_pages, + "/historical/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def trades( @@ -223,12 +277,18 @@ def trades( ticker: str | None = None, min_ts: int | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Trade]: params = _historical_trades_params( - limit=limit, cursor=cursor, ticker=ticker, - min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + ) + return self._list( + "/historical/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - return self._list("/historical/trades", Trade, "trades", params=params) def trades_all( self, @@ -238,22 +298,31 @@ def trades_all( min_ts: int | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Trade]: _validate_max_pages(max_pages) params = _historical_trades_params( - limit=limit, cursor=None, ticker=ticker, - min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, ) return self._list_all( - "/historical/trades", Trade, "trades", - params=params, max_pages=max_pages, + "/historical/trades", + Trade, + "trades", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) + class AsyncHistoricalResource(AsyncResource): """Async historical data API.""" - async def cutoff(self) -> HistoricalCutoff: - data = await self._get("/historical/cutoff") + async def cutoff(self, *, extra_headers: dict[str, str] | None = None) -> HistoricalCutoff: + data = await self._get("/historical/cutoff", extra_headers=extra_headers) return HistoricalCutoff.model_validate(data) async def markets( @@ -265,13 +334,19 @@ async def markets( event_ticker: str | None = None, series_ticker: str | None = None, mve_filter: MveHistoricalFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Market]: params = _historical_markets_params( - limit=limit, cursor=cursor, tickers=tickers, - event_ticker=event_ticker, series_ticker=series_ticker, + limit=limit, + cursor=cursor, + tickers=tickers, + event_ticker=event_ticker, + series_ticker=series_ticker, mve_filter=mve_filter, ) - return await self._list("/historical/markets", Market, "markets", params=params) + return await self._list( + "/historical/markets", Market, "markets", params=params, extra_headers=extra_headers + ) def markets_all( self, @@ -282,20 +357,30 @@ def markets_all( series_ticker: str | None = None, mve_filter: MveHistoricalFilterLiteral | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Market]: _validate_max_pages(max_pages) params = _historical_markets_params( - limit=limit, cursor=None, tickers=tickers, - event_ticker=event_ticker, series_ticker=series_ticker, + limit=limit, + cursor=None, + tickers=tickers, + event_ticker=event_ticker, + series_ticker=series_ticker, mve_filter=mve_filter, ) return self._list_all( - "/historical/markets", Market, "markets", - params=params, max_pages=max_pages, + "/historical/markets", + Market, + "markets", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def market(self, ticker: str) -> Market: - data = await self._get(f"/historical/markets/{_seg(ticker, name='ticker')}") + async def market(self, ticker: str, *, extra_headers: dict[str, str] | None = None) -> Market: + data = await self._get( + f"/historical/markets/{_seg(ticker, name='ticker')}", extra_headers=extra_headers + ) raw = data.get("market", data) return Market.model_validate(raw) @@ -306,14 +391,17 @@ async def candlesticks( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Candlestick]: params = _historical_candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = await self._get( f"/historical/markets/{_seg(ticker, name='ticker')}/candlesticks", params=params, + extra_headers=extra_headers, ) raw = data.get("candlesticks", []) return [Candlestick.model_validate(c) for c in raw] @@ -325,12 +413,18 @@ async def fills( cursor: str | None = None, ticker: str | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Fill]: self._require_auth() params = _historical_fills_or_orders_params( - limit=limit, cursor=cursor, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + max_ts=max_ts, + ) + return await self._list( + "/historical/fills", Fill, "fills", params=params, extra_headers=extra_headers ) - return await self._list("/historical/fills", Fill, "fills", params=params) def fills_all( self, @@ -339,15 +433,23 @@ def fills_all( ticker: str | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Fill]: self._require_auth() _validate_max_pages(max_pages) params = _historical_fills_or_orders_params( - limit=limit, cursor=None, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + max_ts=max_ts, ) return self._list_all( - "/historical/fills", Fill, "fills", - params=params, max_pages=max_pages, + "/historical/fills", + Fill, + "fills", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def orders( @@ -357,12 +459,18 @@ async def orders( cursor: str | None = None, ticker: str | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _historical_fills_or_orders_params( - limit=limit, cursor=cursor, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + max_ts=max_ts, + ) + return await self._list( + "/historical/orders", Order, "orders", params=params, extra_headers=extra_headers ) - return await self._list("/historical/orders", Order, "orders", params=params) def orders_all( self, @@ -371,15 +479,23 @@ def orders_all( ticker: str | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Order]: self._require_auth() _validate_max_pages(max_pages) params = _historical_fills_or_orders_params( - limit=limit, cursor=None, ticker=ticker, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + max_ts=max_ts, ) return self._list_all( - "/historical/orders", Order, "orders", - params=params, max_pages=max_pages, + "/historical/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def trades( @@ -390,12 +506,18 @@ async def trades( ticker: str | None = None, min_ts: int | None = None, max_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Trade]: params = _historical_trades_params( - limit=limit, cursor=cursor, ticker=ticker, - min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + ) + return await self._list( + "/historical/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - return await self._list("/historical/trades", Trade, "trades", params=params) def trades_all( self, @@ -405,13 +527,21 @@ def trades_all( min_ts: int | None = None, max_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Trade]: _validate_max_pages(max_pages) params = _historical_trades_params( - limit=limit, cursor=None, ticker=ticker, - min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, ) return self._list_all( - "/historical/trades", Trade, "trades", - params=params, max_pages=max_pages, + "/historical/trades", + Trade, + "trades", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) diff --git a/kalshi/resources/incentive_programs.py b/kalshi/resources/incentive_programs.py index 90a157d4..da8a90dc 100644 --- a/kalshi/resources/incentive_programs.py +++ b/kalshi/resources/incentive_programs.py @@ -39,6 +39,7 @@ def list( incentive_description: str | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[IncentiveProgram]: _validate_limit(limit, hi=10000) params = _params( @@ -54,6 +55,7 @@ def list( "incentive_programs", params=params, cursor_key="next_cursor", + extra_headers=extra_headers, ) def list_all( @@ -64,6 +66,7 @@ def list_all( incentive_description: str | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[IncentiveProgram]: _validate_max_pages(max_pages) _validate_limit(limit, hi=10000) @@ -80,6 +83,7 @@ def list_all( params=params, max_pages=max_pages, cursor_key="next_cursor", + extra_headers=extra_headers, ) @@ -94,6 +98,7 @@ async def list( incentive_description: str | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[IncentiveProgram]: _validate_limit(limit, hi=10000) params = _params( @@ -109,6 +114,7 @@ async def list( "incentive_programs", params=params, cursor_key="next_cursor", + extra_headers=extra_headers, ) def list_all( @@ -119,6 +125,7 @@ def list_all( incentive_description: str | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[IncentiveProgram]: """Returns an async iterator — use ``async for``.""" _validate_max_pages(max_pages) @@ -136,4 +143,5 @@ def list_all( params=params, max_pages=max_pages, cursor_key="next_cursor", + extra_headers=extra_headers, ) diff --git a/kalshi/resources/live_data.py b/kalshi/resources/live_data.py index fef62b6c..9f852456 100644 --- a/kalshi/resources/live_data.py +++ b/kalshi/resources/live_data.py @@ -28,6 +28,7 @@ def get( milestone_id: str, *, include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> LiveData: params = _params( include_player_stats=_bool_param(include_player_stats), @@ -35,6 +36,7 @@ def get( data = self._get( f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}", params=params, + extra_headers=extra_headers, ) return GetLiveDataResponse.model_validate(data).live_data @@ -44,6 +46,7 @@ def get_typed( milestone_id: str, *, include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> LiveData: """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. @@ -60,6 +63,7 @@ def get_typed( data = self._get( f"/live_data/{_seg(milestone_type, name='milestone_type')}/milestone/{_seg(milestone_id, name='milestone_id')}", # noqa: E501 params=params, + extra_headers=extra_headers, ) return GetLiveDataResponse.model_validate(data).live_data @@ -68,6 +72,7 @@ def batch( *, milestone_ids: builtins.list[str], include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[LiveData]: """Fetch up to 100 milestones in one call. @@ -87,12 +92,17 @@ def batch( milestone_ids=milestone_ids, include_player_stats=_bool_param(include_player_stats), ) - data = self._get("/live_data/batch", params=params) + data = self._get("/live_data/batch", params=params, extra_headers=extra_headers) return GetLiveDatasResponse.model_validate(data).live_datas - def game_stats(self, milestone_id: str) -> GetGameStatsResponse: + def game_stats( + self, milestone_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetGameStatsResponse: """Play-by-play stats. Returns ``pbp=None`` for unsupported sports.""" - data = self._get(f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}/game_stats") # noqa: E501 + data = self._get( + f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}/game_stats", + extra_headers=extra_headers, + ) return GetGameStatsResponse.model_validate(data) @@ -104,6 +114,7 @@ async def get( milestone_id: str, *, include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> LiveData: params = _params( include_player_stats=_bool_param(include_player_stats), @@ -111,6 +122,7 @@ async def get( data = await self._get( f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}", params=params, + extra_headers=extra_headers, ) return GetLiveDataResponse.model_validate(data).live_data @@ -120,6 +132,7 @@ async def get_typed( milestone_id: str, *, include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> LiveData: """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. @@ -136,6 +149,7 @@ async def get_typed( data = await self._get( f"/live_data/{_seg(milestone_type, name='milestone_type')}/milestone/{_seg(milestone_id, name='milestone_id')}", # noqa: E501 params=params, + extra_headers=extra_headers, ) return GetLiveDataResponse.model_validate(data).live_data @@ -144,6 +158,7 @@ async def batch( *, milestone_ids: builtins.list[str], include_player_stats: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[LiveData]: """Fetch up to 100 milestones in one call. @@ -163,10 +178,15 @@ async def batch( milestone_ids=milestone_ids, include_player_stats=_bool_param(include_player_stats), ) - data = await self._get("/live_data/batch", params=params) + data = await self._get("/live_data/batch", params=params, extra_headers=extra_headers) return GetLiveDatasResponse.model_validate(data).live_datas - async def game_stats(self, milestone_id: str) -> GetGameStatsResponse: + async def game_stats( + self, milestone_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetGameStatsResponse: """Play-by-play stats. Returns ``pbp=None`` for unsupported sports.""" - data = await self._get(f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}/game_stats") # noqa: E501 + data = await self._get( + f"/live_data/milestone/{_seg(milestone_id, name='milestone_id')}/game_stats", + extra_headers=extra_headers, + ) return GetGameStatsResponse.model_validate(data) diff --git a/kalshi/resources/markets.py b/kalshi/resources/markets.py index ad4e19f9..16f97cb3 100644 --- a/kalshi/resources/markets.py +++ b/kalshi/resources/markets.py @@ -123,13 +123,10 @@ def _bulk_candlesticks_params( if not market_tickers: raise ValueError("market_tickers must be a non-empty list or string") joined = _join_tickers(market_tickers) - ticker_count = ( - sum(1 for t in joined.split(",") if t.strip()) if joined else 0 - ) + ticker_count = sum(1 for t in joined.split(",") if t.strip()) if joined else 0 if ticker_count > _MAX_BULK: raise ValueError( - f"market_tickers accepts at most {_MAX_BULK} entries per spec " - f"(got {ticker_count})" + f"market_tickers accepts at most {_MAX_BULK} entries per spec (got {ticker_count})" ) return _params( market_tickers=joined, @@ -141,7 +138,8 @@ def _bulk_candlesticks_params( def _bulk_orderbooks_params( - *, tickers: builtins.list[str], + *, + tickers: builtins.list[str], ) -> dict[str, Any]: """Validate + build params for GET /markets/orderbooks (bulk). @@ -152,8 +150,7 @@ def _bulk_orderbooks_params( raise ValueError("tickers must be a non-empty list") if len(tickers) > _MAX_BULK: raise ValueError( - f"tickers accepts at most {_MAX_BULK} entries per spec " - f"(got {len(tickers)})" + f"tickers accepts at most {_MAX_BULK} entries per spec (got {len(tickers)})" ) return _params(tickers=tickers) @@ -168,14 +165,10 @@ def _parse_orderbook(data: dict[str, Any], ticker: str) -> Orderbook: no_raw = ob.get("no_dollars") or ob.get("no", []) or [] yes_levels = [ - OrderbookLevel(price=pair[0], quantity=pair[1]) - for pair in yes_raw - if len(pair) >= 2 + OrderbookLevel(price=pair[0], quantity=pair[1]) for pair in yes_raw if len(pair) >= 2 ] no_levels = [ - OrderbookLevel(price=pair[0], quantity=pair[1]) - for pair in no_raw - if len(pair) >= 2 + OrderbookLevel(price=pair[0], quantity=pair[1]) for pair in no_raw if len(pair) >= 2 ] return Orderbook(ticker=ticker, yes=yes_levels, no=no_levels) @@ -208,14 +201,10 @@ def _orderbook_from_item(item: dict[str, Any]) -> Orderbook: yes_raw = ob.get("yes_dollars") or ob.get("yes") or [] no_raw = ob.get("no_dollars") or ob.get("no") or [] yes_levels = [ - OrderbookLevel(price=pair[0], quantity=pair[1]) - for pair in yes_raw - if len(pair) >= 2 + OrderbookLevel(price=pair[0], quantity=pair[1]) for pair in yes_raw if len(pair) >= 2 ] no_levels = [ - OrderbookLevel(price=pair[0], quantity=pair[1]) - for pair in no_raw - if len(pair) >= 2 + OrderbookLevel(price=pair[0], quantity=pair[1]) for pair in no_raw if len(pair) >= 2 ] return Orderbook(ticker=ticker, yes=yes_levels, no=no_levels) @@ -240,18 +229,25 @@ def list( max_settled_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Market]: params = _list_markets_params( - status=status, series_ticker=series_ticker, - event_ticker=event_ticker, tickers=tickers, + status=status, + series_ticker=series_ticker, + event_ticker=event_ticker, + tickers=tickers, mve_filter=mve_filter, - min_created_ts=min_created_ts, max_created_ts=max_created_ts, + min_created_ts=min_created_ts, + max_created_ts=max_created_ts, min_updated_ts=min_updated_ts, - min_close_ts=min_close_ts, max_close_ts=max_close_ts, - min_settled_ts=min_settled_ts, max_settled_ts=max_settled_ts, - limit=limit, cursor=cursor, + min_close_ts=min_close_ts, + max_close_ts=max_close_ts, + min_settled_ts=min_settled_ts, + max_settled_ts=max_settled_ts, + limit=limit, + cursor=cursor, ) - return self._list("/markets", Market, "markets", params=params) + return self._list("/markets", Market, "markets", params=params, extra_headers=extra_headers) def list_all( self, @@ -270,32 +266,49 @@ def list_all( max_settled_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Market]: _validate_max_pages(max_pages) params = _list_markets_params( - status=status, series_ticker=series_ticker, - event_ticker=event_ticker, tickers=tickers, + status=status, + series_ticker=series_ticker, + event_ticker=event_ticker, + tickers=tickers, mve_filter=mve_filter, - min_created_ts=min_created_ts, max_created_ts=max_created_ts, + min_created_ts=min_created_ts, + max_created_ts=max_created_ts, min_updated_ts=min_updated_ts, - min_close_ts=min_close_ts, max_close_ts=max_close_ts, - min_settled_ts=min_settled_ts, max_settled_ts=max_settled_ts, - limit=limit, cursor=None, + min_close_ts=min_close_ts, + max_close_ts=max_close_ts, + min_settled_ts=min_settled_ts, + max_settled_ts=max_settled_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/markets", Market, "markets", - params=params, max_pages=max_pages, + "/markets", + Market, + "markets", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def get(self, ticker: str) -> Market: - data = self._get(f"/markets/{_seg(ticker, name='ticker')}") + def get(self, ticker: str, *, extra_headers: dict[str, str] | None = None) -> Market: + data = self._get(f"/markets/{_seg(ticker, name='ticker')}", extra_headers=extra_headers) market = data.get("market", data) return Market.model_validate(market) - def orderbook(self, ticker: str, *, depth: int | None = None) -> Orderbook: + def orderbook( + self, ticker: str, *, depth: int | None = None, extra_headers: dict[str, str] | None = None + ) -> Orderbook: self._require_auth() params = _params(depth=depth) - data = self._get(f"/markets/{_seg(ticker, name='ticker')}/orderbook", params=params) + data = self._get( + f"/markets/{_seg(ticker, name='ticker')}/orderbook", + params=params, + extra_headers=extra_headers, + ) return _parse_orderbook(data, ticker) def candlesticks( @@ -307,15 +320,18 @@ def candlesticks( end_ts: int, period_interval: int, include_latest_before_start: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Candlestick]: params = _candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, include_latest_before_start=include_latest_before_start, ) data = self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/markets/{_seg(ticker, name='ticker')}/candlesticks", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("candlesticks", []) return [Candlestick.model_validate(c) for c in raw] @@ -328,12 +344,18 @@ def list_trades( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Trade]: params = _list_trades_params( - ticker=ticker, min_ts=min_ts, max_ts=max_ts, - limit=limit, cursor=cursor, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, + ) + return self._list( + "/markets/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - return self._list("/markets/trades", Trade, "trades", params=params) def list_trades_all( self, @@ -343,15 +365,23 @@ def list_trades_all( max_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Trade]: _validate_max_pages(max_pages) params = _list_trades_params( - ticker=ticker, min_ts=min_ts, max_ts=max_ts, - limit=limit, cursor=None, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/markets/trades", Trade, "trades", - params=params, max_pages=max_pages, + "/markets/trades", + Trade, + "trades", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def bulk_candlesticks( @@ -362,6 +392,7 @@ def bulk_candlesticks( end_ts: int, period_interval: int, include_latest_before_start: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[MarketCandlesticks]: """Fetch candlesticks for up to 100 markets in a single call. @@ -371,16 +402,17 @@ def bulk_candlesticks( """ params = _bulk_candlesticks_params( market_tickers=market_tickers, - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, include_latest_before_start=include_latest_before_start, ) - data = self._get("/markets/candlesticks", params=params) + data = self._get("/markets/candlesticks", params=params, extra_headers=extra_headers) raw = data.get("markets", []) return [MarketCandlesticks.model_validate(m) for m in raw] def bulk_orderbooks( - self, *, tickers: builtins.list[str], + self, *, tickers: builtins.list[str], extra_headers: dict[str, str] | None = None ) -> builtins.list[Orderbook]: """Fetch orderbooks for up to 100 tickers in a single call. @@ -390,7 +422,7 @@ def bulk_orderbooks( """ self._require_auth() params = _bulk_orderbooks_params(tickers=tickers) - data = self._get("/markets/orderbooks", params=params) + data = self._get("/markets/orderbooks", params=params, extra_headers=extra_headers) raw = data.get("orderbooks", []) return [_orderbook_from_item(item) for item in raw] @@ -415,18 +447,27 @@ async def list( max_settled_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Market]: params = _list_markets_params( - status=status, series_ticker=series_ticker, - event_ticker=event_ticker, tickers=tickers, + status=status, + series_ticker=series_ticker, + event_ticker=event_ticker, + tickers=tickers, mve_filter=mve_filter, - min_created_ts=min_created_ts, max_created_ts=max_created_ts, + min_created_ts=min_created_ts, + max_created_ts=max_created_ts, min_updated_ts=min_updated_ts, - min_close_ts=min_close_ts, max_close_ts=max_close_ts, - min_settled_ts=min_settled_ts, max_settled_ts=max_settled_ts, - limit=limit, cursor=cursor, + min_close_ts=min_close_ts, + max_close_ts=max_close_ts, + min_settled_ts=min_settled_ts, + max_settled_ts=max_settled_ts, + limit=limit, + cursor=cursor, + ) + return await self._list( + "/markets", Market, "markets", params=params, extra_headers=extra_headers ) - return await self._list("/markets", Market, "markets", params=params) def list_all( self, @@ -445,33 +486,52 @@ def list_all( max_settled_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Market]: """Non-async method that returns an async iterator for direct use with `async for`.""" _validate_max_pages(max_pages) params = _list_markets_params( - status=status, series_ticker=series_ticker, - event_ticker=event_ticker, tickers=tickers, + status=status, + series_ticker=series_ticker, + event_ticker=event_ticker, + tickers=tickers, mve_filter=mve_filter, - min_created_ts=min_created_ts, max_created_ts=max_created_ts, + min_created_ts=min_created_ts, + max_created_ts=max_created_ts, min_updated_ts=min_updated_ts, - min_close_ts=min_close_ts, max_close_ts=max_close_ts, - min_settled_ts=min_settled_ts, max_settled_ts=max_settled_ts, - limit=limit, cursor=None, + min_close_ts=min_close_ts, + max_close_ts=max_close_ts, + min_settled_ts=min_settled_ts, + max_settled_ts=max_settled_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/markets", Market, "markets", - params=params, max_pages=max_pages, + "/markets", + Market, + "markets", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def get(self, ticker: str) -> Market: - data = await self._get(f"/markets/{_seg(ticker, name='ticker')}") + async def get(self, ticker: str, *, extra_headers: dict[str, str] | None = None) -> Market: + data = await self._get( + f"/markets/{_seg(ticker, name='ticker')}", extra_headers=extra_headers + ) market = data.get("market", data) return Market.model_validate(market) - async def orderbook(self, ticker: str, *, depth: int | None = None) -> Orderbook: + async def orderbook( + self, ticker: str, *, depth: int | None = None, extra_headers: dict[str, str] | None = None + ) -> Orderbook: self._require_auth() params = _params(depth=depth) - data = await self._get(f"/markets/{_seg(ticker, name='ticker')}/orderbook", params=params) + data = await self._get( + f"/markets/{_seg(ticker, name='ticker')}/orderbook", + params=params, + extra_headers=extra_headers, + ) return _parse_orderbook(data, ticker) async def candlesticks( @@ -483,15 +543,18 @@ async def candlesticks( end_ts: int, period_interval: int, include_latest_before_start: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Candlestick]: params = _candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, include_latest_before_start=include_latest_before_start, ) data = await self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/markets/{_seg(ticker, name='ticker')}/candlesticks", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("candlesticks", []) return [Candlestick.model_validate(c) for c in raw] @@ -504,12 +567,18 @@ async def list_trades( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Trade]: params = _list_trades_params( - ticker=ticker, min_ts=min_ts, max_ts=max_ts, - limit=limit, cursor=cursor, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, + ) + return await self._list( + "/markets/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - return await self._list("/markets/trades", Trade, "trades", params=params) def list_trades_all( self, @@ -519,16 +588,24 @@ def list_trades_all( max_ts: int | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Trade]: """Returns an async iterator — use ``async for``.""" _validate_max_pages(max_pages) params = _list_trades_params( - ticker=ticker, min_ts=min_ts, max_ts=max_ts, - limit=limit, cursor=None, + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, ) return self._list_all( - "/markets/trades", Trade, "trades", - params=params, max_pages=max_pages, + "/markets/trades", + Trade, + "trades", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def bulk_candlesticks( @@ -539,6 +616,7 @@ async def bulk_candlesticks( end_ts: int, period_interval: int, include_latest_before_start: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[MarketCandlesticks]: """Fetch candlesticks for up to 100 markets in a single call. @@ -548,16 +626,17 @@ async def bulk_candlesticks( """ params = _bulk_candlesticks_params( market_tickers=market_tickers, - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, include_latest_before_start=include_latest_before_start, ) - data = await self._get("/markets/candlesticks", params=params) + data = await self._get("/markets/candlesticks", params=params, extra_headers=extra_headers) raw = data.get("markets", []) return [MarketCandlesticks.model_validate(m) for m in raw] async def bulk_orderbooks( - self, *, tickers: builtins.list[str], + self, *, tickers: builtins.list[str], extra_headers: dict[str, str] | None = None ) -> builtins.list[Orderbook]: """Fetch orderbooks for up to 100 tickers in a single call. @@ -567,6 +646,6 @@ async def bulk_orderbooks( """ self._require_auth() params = _bulk_orderbooks_params(tickers=tickers) - data = await self._get("/markets/orderbooks", params=params) + data = await self._get("/markets/orderbooks", params=params, extra_headers=extra_headers) raw = data.get("orderbooks", []) return [_orderbook_from_item(item) for item in raw] diff --git a/kalshi/resources/milestones.py b/kalshi/resources/milestones.py index 6d98f595..37cfcd3c 100644 --- a/kalshi/resources/milestones.py +++ b/kalshi/resources/milestones.py @@ -91,15 +91,22 @@ def list( related_event_ticker: str | None = None, cursor: str | None = None, min_updated_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Milestone]: params = _list_milestones_params( - limit=limit, minimum_start_date=minimum_start_date, - category=category, competition=competition, - source_id=source_id, milestone_type=milestone_type, + limit=limit, + minimum_start_date=minimum_start_date, + category=category, + competition=competition, + source_id=source_id, + milestone_type=milestone_type, related_event_ticker=related_event_ticker, - cursor=cursor, min_updated_ts=min_updated_ts, + cursor=cursor, + min_updated_ts=min_updated_ts, + ) + return self._list( + "/milestones", Milestone, "milestones", params=params, extra_headers=extra_headers ) - return self._list("/milestones", Milestone, "milestones", params=params) def list_all( self, @@ -113,22 +120,33 @@ def list_all( related_event_ticker: str | None = None, min_updated_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Milestone]: _validate_max_pages(max_pages) params = _list_milestones_params( - limit=limit, minimum_start_date=minimum_start_date, - category=category, competition=competition, - source_id=source_id, milestone_type=milestone_type, + limit=limit, + minimum_start_date=minimum_start_date, + category=category, + competition=competition, + source_id=source_id, + milestone_type=milestone_type, related_event_ticker=related_event_ticker, - cursor=None, min_updated_ts=min_updated_ts, + cursor=None, + min_updated_ts=min_updated_ts, ) return self._list_all( - "/milestones", Milestone, "milestones", - params=params, max_pages=max_pages, + "/milestones", + Milestone, + "milestones", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def get(self, milestone_id: str) -> Milestone: - data = self._get(f"/milestones/{_seg(milestone_id, name='milestone_id')}") + def get(self, milestone_id: str, *, extra_headers: dict[str, str] | None = None) -> Milestone: + data = self._get( + f"/milestones/{_seg(milestone_id, name='milestone_id')}", extra_headers=extra_headers + ) return GetMilestoneResponse.model_validate(data).milestone @@ -147,16 +165,21 @@ async def list( related_event_ticker: str | None = None, cursor: str | None = None, min_updated_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Milestone]: params = _list_milestones_params( - limit=limit, minimum_start_date=minimum_start_date, - category=category, competition=competition, - source_id=source_id, milestone_type=milestone_type, + limit=limit, + minimum_start_date=minimum_start_date, + category=category, + competition=competition, + source_id=source_id, + milestone_type=milestone_type, related_event_ticker=related_event_ticker, - cursor=cursor, min_updated_ts=min_updated_ts, + cursor=cursor, + min_updated_ts=min_updated_ts, ) return await self._list( - "/milestones", Milestone, "milestones", params=params, + "/milestones", Milestone, "milestones", params=params, extra_headers=extra_headers ) def list_all( @@ -171,21 +194,34 @@ def list_all( related_event_ticker: str | None = None, min_updated_ts: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Milestone]: """Returns an async iterator — use ``async for``.""" _validate_max_pages(max_pages) params = _list_milestones_params( - limit=limit, minimum_start_date=minimum_start_date, - category=category, competition=competition, - source_id=source_id, milestone_type=milestone_type, + limit=limit, + minimum_start_date=minimum_start_date, + category=category, + competition=competition, + source_id=source_id, + milestone_type=milestone_type, related_event_ticker=related_event_ticker, - cursor=None, min_updated_ts=min_updated_ts, + cursor=None, + min_updated_ts=min_updated_ts, ) return self._list_all( - "/milestones", Milestone, "milestones", - params=params, max_pages=max_pages, + "/milestones", + Milestone, + "milestones", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def get(self, milestone_id: str) -> Milestone: - data = await self._get(f"/milestones/{_seg(milestone_id, name='milestone_id')}") + async def get( + self, milestone_id: str, *, extra_headers: dict[str, str] | None = None + ) -> Milestone: + data = await self._get( + f"/milestones/{_seg(milestone_id, name='milestone_id')}", extra_headers=extra_headers + ) return GetMilestoneResponse.model_validate(data).milestone diff --git a/kalshi/resources/multivariate.py b/kalshi/resources/multivariate.py index 0531f2d2..55d5089a 100644 --- a/kalshi/resources/multivariate.py +++ b/kalshi/resources/multivariate.py @@ -61,10 +61,7 @@ def _build_create_market_body( ) if request is None: if selected_markets is None: - raise TypeError( - "create_market() requires `selected_markets` " - "(or pass `request=...`)" - ) + raise TypeError("create_market() requires `selected_markets` (or pass `request=...`)") request = CreateMarketInMultivariateEventCollectionRequest( selected_markets=list(selected_markets), with_market_payload=with_market_payload, @@ -80,10 +77,7 @@ def _build_lookup_tickers_body( _check_request_exclusive(request, selected_markets=selected_markets) if request is None: if selected_markets is None: - raise TypeError( - "lookup_tickers() requires `selected_markets` " - "(or pass `request=...`)" - ) + raise TypeError("lookup_tickers() requires `selected_markets` (or pass `request=...`)") request = LookupTickersForMarketInMultivariateEventCollectionRequest( selected_markets=list(selected_markets), ) @@ -114,18 +108,21 @@ def list( series_ticker: str | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[MultivariateEventCollection]: params = _list_collections_params( status=status, associated_event_ticker=associated_event_ticker, series_ticker=series_ticker, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, ) return self._list( "/multivariate_event_collections", MultivariateEventCollection, "multivariate_contracts", params=params, + extra_headers=extra_headers, ) def list_all( @@ -136,13 +133,15 @@ def list_all( series_ticker: str | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[MultivariateEventCollection]: _validate_max_pages(max_pages) params = _list_collections_params( status=status, associated_event_ticker=associated_event_ticker, series_ticker=series_ticker, - limit=limit, cursor=None, + limit=limit, + cursor=None, ) return self._list_all( "/multivariate_event_collections", @@ -150,13 +149,17 @@ def list_all( "multivariate_contracts", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) - def get(self, collection_ticker: str) -> MultivariateEventCollection: - data = self._get(f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}") # noqa: E501 - return MultivariateEventCollection.model_validate( - data.get("multivariate_contract", data) + def get( + self, collection_ticker: str, *, extra_headers: dict[str, str] | None = None + ) -> MultivariateEventCollection: + data = self._get( + f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}", + extra_headers=extra_headers, ) + return MultivariateEventCollection.model_validate(data.get("multivariate_contract", data)) @overload def create_market( @@ -164,6 +167,7 @@ def create_market( collection_ticker: str, *, request: CreateMarketInMultivariateEventCollectionRequest, + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: ... @overload def create_market( @@ -172,6 +176,7 @@ def create_market( *, selected_markets: builtins.list[TickerPair], with_market_payload: bool | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: ... def create_market( self, @@ -180,6 +185,7 @@ def create_market( request: CreateMarketInMultivariateEventCollectionRequest | None = None, selected_markets: builtins.list[TickerPair] | None = None, with_market_payload: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: self._require_auth() body = _build_create_market_body( @@ -190,6 +196,7 @@ def create_market( data = self._post( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}", json=body, + extra_headers=extra_headers, ) return CreateMarketResponse.model_validate(data) @@ -199,6 +206,7 @@ def lookup_tickers( collection_ticker: str, *, request: LookupTickersForMarketInMultivariateEventCollectionRequest, + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: ... @overload def lookup_tickers( @@ -206,23 +214,25 @@ def lookup_tickers( collection_ticker: str, *, selected_markets: builtins.list[TickerPair], + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: ... def lookup_tickers( self, collection_ticker: str, *, - request: ( - LookupTickersForMarketInMultivariateEventCollectionRequest | None - ) = None, + request: (LookupTickersForMarketInMultivariateEventCollectionRequest | None) = None, selected_markets: builtins.list[TickerPair] | None = None, + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: self._require_auth() body = _build_lookup_tickers_body( - request, selected_markets=selected_markets, + request, + selected_markets=selected_markets, ) data = self._put( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}/lookup", # noqa: E501 json=body, + extra_headers=extra_headers, ) return _parse_lookup_tickers_response(data) @@ -231,11 +241,13 @@ def lookup_history( collection_ticker: str, *, lookback_seconds: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[LookupPoint]: params = _params(lookback_seconds=lookback_seconds) data = self._get( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}/lookup", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("lookup_points", []) return [LookupPoint.model_validate(item) for item in raw] @@ -252,18 +264,21 @@ async def list( series_ticker: str | None = None, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[MultivariateEventCollection]: params = _list_collections_params( status=status, associated_event_ticker=associated_event_ticker, series_ticker=series_ticker, - limit=limit, cursor=cursor, + limit=limit, + cursor=cursor, ) return await self._list( "/multivariate_event_collections", MultivariateEventCollection, "multivariate_contracts", params=params, + extra_headers=extra_headers, ) def list_all( @@ -274,13 +289,15 @@ def list_all( series_ticker: str | None = None, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[MultivariateEventCollection]: _validate_max_pages(max_pages) params = _list_collections_params( status=status, associated_event_ticker=associated_event_ticker, series_ticker=series_ticker, - limit=limit, cursor=None, + limit=limit, + cursor=None, ) return self._list_all( "/multivariate_event_collections", @@ -288,13 +305,17 @@ def list_all( "multivariate_contracts", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) - async def get(self, collection_ticker: str) -> MultivariateEventCollection: - data = await self._get(f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}") # noqa: E501 - return MultivariateEventCollection.model_validate( - data.get("multivariate_contract", data) + async def get( + self, collection_ticker: str, *, extra_headers: dict[str, str] | None = None + ) -> MultivariateEventCollection: + data = await self._get( + f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}", + extra_headers=extra_headers, ) + return MultivariateEventCollection.model_validate(data.get("multivariate_contract", data)) @overload async def create_market( @@ -302,6 +323,7 @@ async def create_market( collection_ticker: str, *, request: CreateMarketInMultivariateEventCollectionRequest, + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: ... @overload async def create_market( @@ -310,6 +332,7 @@ async def create_market( *, selected_markets: builtins.list[TickerPair], with_market_payload: bool | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: ... async def create_market( self, @@ -318,6 +341,7 @@ async def create_market( request: CreateMarketInMultivariateEventCollectionRequest | None = None, selected_markets: builtins.list[TickerPair] | None = None, with_market_payload: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateMarketResponse: self._require_auth() body = _build_create_market_body( @@ -328,6 +352,7 @@ async def create_market( data = await self._post( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}", json=body, + extra_headers=extra_headers, ) return CreateMarketResponse.model_validate(data) @@ -337,6 +362,7 @@ async def lookup_tickers( collection_ticker: str, *, request: LookupTickersForMarketInMultivariateEventCollectionRequest, + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: ... @overload async def lookup_tickers( @@ -344,23 +370,25 @@ async def lookup_tickers( collection_ticker: str, *, selected_markets: builtins.list[TickerPair], + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: ... async def lookup_tickers( self, collection_ticker: str, *, - request: ( - LookupTickersForMarketInMultivariateEventCollectionRequest | None - ) = None, + request: (LookupTickersForMarketInMultivariateEventCollectionRequest | None) = None, selected_markets: builtins.list[TickerPair] | None = None, + extra_headers: dict[str, str] | None = None, ) -> LookupTickersResponse: self._require_auth() body = _build_lookup_tickers_body( - request, selected_markets=selected_markets, + request, + selected_markets=selected_markets, ) data = await self._put( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}/lookup", # noqa: E501 json=body, + extra_headers=extra_headers, ) return _parse_lookup_tickers_response(data) @@ -369,11 +397,13 @@ async def lookup_history( collection_ticker: str, *, lookback_seconds: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[LookupPoint]: params = _params(lookback_seconds=lookback_seconds) data = await self._get( f"/multivariate_event_collections/{_seg(collection_ticker, name='collection_ticker')}/lookup", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("lookup_points", []) return [LookupPoint.model_validate(item) for item in raw] diff --git a/kalshi/resources/order_groups.py b/kalshi/resources/order_groups.py index 83996aae..d7d4cf20 100644 --- a/kalshi/resources/order_groups.py +++ b/kalshi/resources/order_groups.py @@ -31,16 +31,17 @@ def _build_create_order_group_body( exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( - request, contracts_limit=contracts_limit, subaccount=subaccount, + request, + contracts_limit=contracts_limit, + subaccount=subaccount, exchange_index=exchange_index, ) if request is None: if contracts_limit is None: - raise TypeError( - "create() requires `contracts_limit` (or pass `request=...`)" - ) + raise TypeError("create() requires `contracts_limit` (or pass `request=...`)") request = CreateOrderGroupRequest( - contracts_limit=contracts_limit, subaccount=subaccount, + contracts_limit=contracts_limit, + subaccount=subaccount, exchange_index=exchange_index, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -54,10 +55,7 @@ def _build_update_limit_body( _check_request_exclusive(request, contracts_limit=contracts_limit) if request is None: if contracts_limit is None: - raise TypeError( - "update_limit() requires `contracts_limit` " - "(or pass `request=...`)" - ) + raise TypeError("update_limit() requires `contracts_limit` (or pass `request=...`)") request = UpdateOrderGroupLimitRequest(contracts_limit=contracts_limit) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -65,25 +63,35 @@ def _build_update_limit_body( class OrderGroupsResource(SyncResource): """Sync order groups API.""" - def list(self, *, subaccount: int | None = None) -> builtins.list[OrderGroup]: + def list( + self, *, subaccount: int | None = None, extra_headers: dict[str, str] | None = None + ) -> builtins.list[OrderGroup]: # Returns plain list (not Page) — spec response has no cursor. self._require_auth() params = _params(subaccount=subaccount) - data = self._get("/portfolio/order_groups", params=params) + data = self._get("/portfolio/order_groups", params=params, extra_headers=extra_headers) raw = data.get("order_groups", []) return [OrderGroup.model_validate(item) for item in raw] def get( - self, order_group_id: str, *, subaccount: int | None = None, + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> GetOrderGroupResponse: self._require_auth() params = _params(subaccount=subaccount) - data = self._get(f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", params=params) # noqa: E501 + data = self._get( + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", + params=params, + extra_headers=extra_headers, + ) return GetOrderGroupResponse.model_validate(data) @overload def create( - self, *, request: CreateOrderGroupRequest, + self, *, request: CreateOrderGroupRequest, extra_headers: dict[str, str] | None = None ) -> CreateOrderGroupResponse: ... @overload def create( @@ -92,6 +100,7 @@ def create( contracts_limit: int, subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateOrderGroupResponse: ... def create( self, @@ -100,14 +109,17 @@ def create( contracts_limit: int | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateOrderGroupResponse: # POST path is /order_groups/create, not /order_groups. self._require_auth() body = _build_create_order_group_body( - request, contracts_limit=contracts_limit, subaccount=subaccount, + request, + contracts_limit=contracts_limit, + subaccount=subaccount, exchange_index=exchange_index, ) - data = self._post("/portfolio/order_groups/create", json=body) + data = self._post("/portfolio/order_groups/create", json=body, extra_headers=extra_headers) return CreateOrderGroupResponse.model_validate(data) def delete( @@ -116,34 +128,65 @@ def delete( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) - self._delete(f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", params=params) # noqa: E501 + self._delete( + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", + params=params, + extra_headers=extra_headers, + ) - def reset(self, order_group_id: str, *, subaccount: int | None = None) -> None: + def reset( + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: self._require_auth() params = _params(subaccount=subaccount) # json={} forces Content-Type: application/json — demo rejects the PUT without it. self._put( - f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/reset", params=params, json={}, # noqa: E501 + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/reset", + params=params, + json={}, + extra_headers=extra_headers, ) - def trigger(self, order_group_id: str, *, subaccount: int | None = None) -> None: + def trigger( + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: self._require_auth() params = _params(subaccount=subaccount) # json={} forces Content-Type: application/json — demo rejects the PUT without it. self._put( - f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/trigger", params=params, json={}, # noqa: E501 + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/trigger", + params=params, + json={}, + extra_headers=extra_headers, ) @overload def update_limit( - self, order_group_id: str, *, request: UpdateOrderGroupLimitRequest, + self, + order_group_id: str, + *, + request: UpdateOrderGroupLimitRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload def update_limit( - self, order_group_id: str, *, contracts_limit: int, + self, + order_group_id: str, + *, + contracts_limit: int, + extra_headers: dict[str, str] | None = None, ) -> None: ... def update_limit( self, @@ -151,38 +194,54 @@ def update_limit( *, request: UpdateOrderGroupLimitRequest | None = None, contracts_limit: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: # No subaccount kwarg — spec omits SubaccountQuery on /limit. self._require_auth() body = _build_update_limit_body( - request, contracts_limit=contracts_limit, + request, + contracts_limit=contracts_limit, + ) + self._put( + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/limit", + json=body, + extra_headers=extra_headers, ) - self._put(f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/limit", json=body) # noqa: E501 class AsyncOrderGroupsResource(AsyncResource): """Async order groups API.""" - async def list(self, *, subaccount: int | None = None) -> builtins.list[OrderGroup]: + async def list( + self, *, subaccount: int | None = None, extra_headers: dict[str, str] | None = None + ) -> builtins.list[OrderGroup]: self._require_auth() params = _params(subaccount=subaccount) - data = await self._get("/portfolio/order_groups", params=params) + data = await self._get( + "/portfolio/order_groups", params=params, extra_headers=extra_headers + ) raw = data.get("order_groups", []) return [OrderGroup.model_validate(item) for item in raw] async def get( - self, order_group_id: str, *, subaccount: int | None = None, + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> GetOrderGroupResponse: self._require_auth() params = _params(subaccount=subaccount) data = await self._get( - f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", params=params, + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", + params=params, + extra_headers=extra_headers, ) return GetOrderGroupResponse.model_validate(data) @overload async def create( - self, *, request: CreateOrderGroupRequest, + self, *, request: CreateOrderGroupRequest, extra_headers: dict[str, str] | None = None ) -> CreateOrderGroupResponse: ... @overload async def create( @@ -191,6 +250,7 @@ async def create( contracts_limit: int, subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> CreateOrderGroupResponse: ... async def create( self, @@ -199,13 +259,18 @@ async def create( contracts_limit: int | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CreateOrderGroupResponse: self._require_auth() body = _build_create_order_group_body( - request, contracts_limit=contracts_limit, subaccount=subaccount, + request, + contracts_limit=contracts_limit, + subaccount=subaccount, exchange_index=exchange_index, ) - data = await self._post("/portfolio/order_groups/create", json=body) + data = await self._post( + "/portfolio/order_groups/create", json=body, extra_headers=extra_headers + ) return CreateOrderGroupResponse.model_validate(data) async def delete( @@ -214,38 +279,65 @@ async def delete( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) - await self._delete(f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", params=params) # noqa: E501 + await self._delete( + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}", + params=params, + extra_headers=extra_headers, + ) async def reset( - self, order_group_id: str, *, subaccount: int | None = None, + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount) # json={} forces Content-Type: application/json — demo rejects the PUT without it. await self._put( - f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/reset", params=params, json={}, # noqa: E501 + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/reset", + params=params, + json={}, + extra_headers=extra_headers, ) async def trigger( - self, order_group_id: str, *, subaccount: int | None = None, + self, + order_group_id: str, + *, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount) # json={} forces Content-Type: application/json — demo rejects the PUT without it. await self._put( - f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/trigger", params=params, json={}, # noqa: E501 + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/trigger", + params=params, + json={}, + extra_headers=extra_headers, ) @overload async def update_limit( - self, order_group_id: str, *, request: UpdateOrderGroupLimitRequest, + self, + order_group_id: str, + *, + request: UpdateOrderGroupLimitRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload async def update_limit( - self, order_group_id: str, *, contracts_limit: int, + self, + order_group_id: str, + *, + contracts_limit: int, + extra_headers: dict[str, str] | None = None, ) -> None: ... async def update_limit( self, @@ -253,9 +345,15 @@ async def update_limit( *, request: UpdateOrderGroupLimitRequest | None = None, contracts_limit: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_update_limit_body( - request, contracts_limit=contracts_limit, + request, + contracts_limit=contracts_limit, + ) + await self._put( + f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/limit", + json=body, + extra_headers=extra_headers, ) - await self._put(f"/portfolio/order_groups/{_seg(order_group_id, name='order_group_id')}/limit", json=body) # noqa: E501 diff --git a/kalshi/resources/orders.py b/kalshi/resources/orders.py index c0f1509e..7b2277a0 100644 --- a/kalshi/resources/orders.py +++ b/kalshi/resources/orders.py @@ -83,11 +83,18 @@ def _build_create_order_body( ) -> dict[str, Any]: _check_request_exclusive( request, - ticker=ticker, side=side, action=action, count=count, - yes_price=yes_price, no_price=no_price, - client_order_id=client_order_id, expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, time_in_force=time_in_force, - post_only=post_only, reduce_only=reduce_only, + ticker=ticker, + side=side, + action=action, + count=count, + yes_price=yes_price, + no_price=no_price, + client_order_id=client_order_id, + expiration_ts=expiration_ts, + buy_max_cost=buy_max_cost, + time_in_force=time_in_force, + post_only=post_only, + reduce_only=reduce_only, self_trade_prevention_type=self_trade_prevention_type, order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, @@ -99,7 +106,7 @@ def _build_create_order_body( raise TypeError( "create() requires `ticker`, `side`, `count`, and `action` " "(or pass `request=...`). Pre-#242 the SDK silently defaulted " - "missing `count` to 1 contract and missing `action` to \"buy\" — " + 'missing `count` to 1 contract and missing `action` to "buy" — ' "that has been removed: a missing arg would otherwise translate " "into a real 1-contract BUY on the wire." ) @@ -138,9 +145,7 @@ def _build_batch_create_body( _check_request_exclusive(request, orders=orders) if request is None: if orders is None: - raise TypeError( - "batch_create() requires `orders` (or pass `request=...`)" - ) + raise TypeError("batch_create() requires `orders` (or pass `request=...`)") request = BatchCreateOrdersRequest(orders=list(orders)) return request.model_dump_json(exclude_none=True, by_alias=True).encode() @@ -154,14 +159,9 @@ def _build_batch_cancel_body( _check_request_exclusive(request, orders=orders) if request is None: if orders is None: - raise TypeError( - "batch_cancel() requires `orders` (or pass `request=...`)" - ) + raise TypeError("batch_cancel() requires `orders` (or pass `request=...`)") normalized = [ - ( - BatchCancelOrdersRequestOrder(order_id=o) if isinstance(o, str) else o - ) - for o in orders + (BatchCancelOrdersRequestOrder(order_id=o) if isinstance(o, str) else o) for o in orders ] request = BatchCancelOrdersRequest(orders=normalized) return request.model_dump_json(exclude_none=True, by_alias=True).encode() @@ -183,8 +183,12 @@ def _build_amend_body( ) -> dict[str, Any]: _check_request_exclusive( request, - ticker=ticker, side=side, action=action, - yes_price=yes_price, no_price=no_price, count=count, + ticker=ticker, + side=side, + action=action, + yes_price=yes_price, + no_price=no_price, + count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, @@ -193,13 +197,10 @@ def _build_amend_body( if request is None: if ticker is None or side is None or action is None: raise TypeError( - "amend() requires `ticker`, `side`, and `action` " - "(or pass `request=...`)" + "amend() requires `ticker`, `side`, and `action` (or pass `request=...`)" ) if yes_price is None and no_price is None and count is None: - raise ValueError( - "amend() requires at least one of yes_price, no_price, or count" - ) + raise ValueError("amend() requires at least one of yes_price, no_price, or count") request = AmendOrderRequest( ticker=ticker, side=side, @@ -224,8 +225,11 @@ def _build_decrease_body( exchange_index: int | None, ) -> dict[str, Any]: _check_request_exclusive( - request, reduce_by=reduce_by, reduce_to=reduce_to, - subaccount=subaccount, exchange_index=exchange_index, + request, + reduce_by=reduce_by, + reduce_to=reduce_to, + subaccount=subaccount, + exchange_index=exchange_index, ) if request is None: # Method-level guards mirror DecreaseOrderRequest._enforce_reduce_xor @@ -319,7 +323,9 @@ class OrdersResource(SyncResource): """Sync orders API.""" @overload - def create(self, *, request: CreateOrderRequest) -> Order: ... + def create( + self, *, request: CreateOrderRequest, extra_headers: dict[str, str] | None = None + ) -> Order: ... @overload def create( self, @@ -341,6 +347,7 @@ def create( cancel_order_on_pause: bool | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> Order: ... def create( self, @@ -363,6 +370,7 @@ def create( cancel_order_on_pause: bool | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Order: """Place a new order. @@ -389,24 +397,33 @@ def create( self._require_auth() body = _build_create_order_body( request, - ticker=ticker, side=side, action=action, count=count, - yes_price=yes_price, no_price=no_price, - client_order_id=client_order_id, expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, time_in_force=time_in_force, - post_only=post_only, reduce_only=reduce_only, + ticker=ticker, + side=side, + action=action, + count=count, + yes_price=yes_price, + no_price=no_price, + client_order_id=client_order_id, + expiration_ts=expiration_ts, + buy_max_cost=buy_max_cost, + time_in_force=time_in_force, + post_only=post_only, + reduce_only=reduce_only, self_trade_prevention_type=self_trade_prevention_type, order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, exchange_index=exchange_index, ) - data = self._post("/portfolio/orders", json=body) + data = self._post("/portfolio/orders", json=body, extra_headers=extra_headers) order_data = data.get("order", data) return Order.model_validate(order_data) - def get(self, order_id: str) -> Order: + def get(self, order_id: str, *, extra_headers: dict[str, str] | None = None) -> Order: self._require_auth() - data = self._get(f"/portfolio/orders/{_seg(order_id, name='order_id')}") + data = self._get( + f"/portfolio/orders/{_seg(order_id, name='order_id')}", extra_headers=extra_headers + ) order_data = data.get("order", data) return Order.model_validate(order_data) @@ -416,10 +433,15 @@ def cancel( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) - self._delete(f"/portfolio/orders/{_seg(order_id, name='order_id')}", params=params) + self._delete( + f"/portfolio/orders/{_seg(order_id, name='order_id')}", + params=params, + extra_headers=extra_headers, + ) def list( self, @@ -432,14 +454,22 @@ def list( limit: int | None = None, cursor: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _list_orders_params( - ticker=ticker, event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, subaccount=subaccount, ) - return self._list("/portfolio/orders", Order, "orders", params=params) + return self._list( + "/portfolio/orders", Order, "orders", params=params, extra_headers=extra_headers + ) def list_all( self, @@ -452,35 +482,43 @@ def list_all( limit: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Order]: self._require_auth() _validate_max_pages(max_pages) params = _list_orders_params( - ticker=ticker, event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, subaccount=subaccount, ) return self._list_all( - "/portfolio/orders", Order, "orders", - params=params, max_pages=max_pages, + "/portfolio/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) @overload def batch_create( - self, - *, - request: BatchCreateOrdersRequest, + self, *, request: BatchCreateOrdersRequest, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersResponse: ... @overload def batch_create( - self, - orders: Sequence[CreateOrderRequest], + self, orders: Sequence[CreateOrderRequest], *, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersResponse: ... def batch_create( self, orders: Sequence[CreateOrderRequest] | None = None, *, request: BatchCreateOrdersRequest | None = None, + extra_headers: dict[str, str] | None = None, ) -> BatchCreateOrdersResponse: """Place a batch of orders. @@ -494,25 +532,28 @@ def batch_create( """ self._require_auth() body = _build_batch_create_body(request, orders) - data = self._post_json("/portfolio/orders/batched", content=body) + data = self._post_json( + "/portfolio/orders/batched", content=body, extra_headers=extra_headers + ) return BatchCreateOrdersResponse.model_validate(data) @overload def batch_cancel( - self, - *, - request: BatchCancelOrdersRequest, + self, *, request: BatchCancelOrdersRequest, extra_headers: dict[str, str] | None = None ) -> BatchCancelOrdersResponse: ... @overload def batch_cancel( self, orders: Sequence[BatchCancelOrdersRequestOrder | str], + *, + extra_headers: dict[str, str] | None = None, ) -> BatchCancelOrdersResponse: ... def batch_cancel( self, orders: Sequence[BatchCancelOrdersRequestOrder | str] | None = None, *, request: BatchCancelOrdersRequest | None = None, + extra_headers: dict[str, str] | None = None, ) -> BatchCancelOrdersResponse: """Batch-cancel orders. @@ -532,11 +573,11 @@ def batch_cancel( """ self._require_auth() body = _build_batch_cancel_body(request, orders) - data = self._delete_with_body_json("/portfolio/orders/batched", content=body) + data = self._delete_with_body_json( + "/portfolio/orders/batched", content=body, extra_headers=extra_headers + ) if data is None: - raise KalshiError( - "Expected BatchCancelOrdersResponse body, got 204 No Content." - ) + raise KalshiError("Expected BatchCancelOrdersResponse body, got 204 No Content.") return BatchCancelOrdersResponse.model_validate(data) def fills( @@ -549,14 +590,21 @@ def fills( limit: int | None = None, cursor: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Fill]: self._require_auth() params = _fills_params( - ticker=ticker, order_id=order_id, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + ticker=ticker, + order_id=order_id, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, subaccount=subaccount, ) - return self._list("/portfolio/fills", Fill, "fills", params=params) + return self._list( + "/portfolio/fills", Fill, "fills", params=params, extra_headers=extra_headers + ) def fills_all( self, @@ -568,22 +616,35 @@ def fills_all( limit: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Fill]: self._require_auth() _validate_max_pages(max_pages) params = _fills_params( - ticker=ticker, order_id=order_id, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + ticker=ticker, + order_id=order_id, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, subaccount=subaccount, ) return self._list_all( - "/portfolio/fills", Fill, "fills", - params=params, max_pages=max_pages, + "/portfolio/fills", + Fill, + "fills", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) @overload def amend( - self, order_id: str, *, request: AmendOrderRequest, + self, + order_id: str, + *, + request: AmendOrderRequest, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: ... @overload def amend( @@ -600,6 +661,7 @@ def amend( updated_client_order_id: str | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: ... def amend( self, @@ -616,23 +678,36 @@ def amend( updated_client_order_id: str | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: self._require_auth() body = _build_amend_body( request, - ticker=ticker, side=side, action=action, - yes_price=yes_price, no_price=no_price, count=count, + ticker=ticker, + side=side, + action=action, + yes_price=yes_price, + no_price=no_price, + count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, exchange_index=exchange_index, ) - data = self._post(f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", json=body) + data = self._post( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", + json=body, + extra_headers=extra_headers, + ) return AmendOrderResponse.model_validate(data) @overload def decrease( - self, order_id: str, *, request: DecreaseOrderRequest, + self, + order_id: str, + *, + request: DecreaseOrderRequest, + extra_headers: dict[str, str] | None = None, ) -> Order: ... @overload def decrease( @@ -643,6 +718,7 @@ def decrease( reduce_to: int | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> Order: ... def decrease( self, @@ -653,13 +729,21 @@ def decrease( reduce_to: int | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Order: self._require_auth() body = _build_decrease_body( - request, reduce_by=reduce_by, reduce_to=reduce_to, - subaccount=subaccount, exchange_index=exchange_index, + request, + reduce_by=reduce_by, + reduce_to=reduce_to, + subaccount=subaccount, + exchange_index=exchange_index, + ) + data = self._post( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", + json=body, + extra_headers=extra_headers, ) - data = self._post(f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", json=body) # noqa: E501 order_data = data.get("order", data) return Order.model_validate(order_data) @@ -669,6 +753,7 @@ def queue_positions( market_tickers: builtins.list[str] | str | None = None, event_ticker: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[OrderQueuePosition]: self._require_auth() params = _queue_positions_params( @@ -676,13 +761,20 @@ def queue_positions( event_ticker=event_ticker, subaccount=subaccount, ) - data = self._get("/portfolio/orders/queue_positions", params=params) + data = self._get( + "/portfolio/orders/queue_positions", params=params, extra_headers=extra_headers + ) raw = data.get("queue_positions", []) return [OrderQueuePosition.model_validate(item) for item in raw] - def queue_position(self, order_id: str) -> Decimal: + def queue_position( + self, order_id: str, *, extra_headers: dict[str, str] | None = None + ) -> Decimal: self._require_auth() - data = self._get(f"/portfolio/orders/{_seg(order_id, name='order_id')}/queue_position") + data = self._get( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/queue_position", + extra_headers=extra_headers, + ) return _parse_queue_position(data) # ------------------------------------------------------------------ @@ -690,10 +782,12 @@ def queue_position(self, order_id: str) -> Decimal: # Model-only API surface — pass a fully-constructed request model. # ------------------------------------------------------------------ - def create_v2(self, *, request: CreateOrderV2Request) -> CreateOrderV2Response: + def create_v2( + self, *, request: CreateOrderV2Request, extra_headers: dict[str, str] | None = None + ) -> CreateOrderV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") - data = self._post("/portfolio/events/orders", json=body) + data = self._post("/portfolio/events/orders", json=body, extra_headers=extra_headers) return CreateOrderV2Response.model_validate(data) def cancel_v2( @@ -702,16 +796,17 @@ def cancel_v2( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CancelOrderV2Response: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) data = self._delete( - f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", params=params, + f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", + params=params, + extra_headers=extra_headers, ) if data is None: - raise KalshiError( - "Expected CancelOrderV2Response body, got 204 No Content." - ) + raise KalshiError("Expected CancelOrderV2Response body, got 204 No Content.") return CancelOrderV2Response.model_validate(data) def amend_v2( @@ -720,6 +815,7 @@ def amend_v2( *, request: AmendOrderV2Request, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderV2Response: """Amend an event-market order (V2). @@ -733,7 +829,9 @@ def amend_v2( body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = self._post( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}/amend", - params=params, json=body, + params=params, + json=body, + extra_headers=extra_headers, ) return AmendOrderV2Response.model_validate(data) @@ -743,6 +841,7 @@ def decrease_v2( *, request: DecreaseOrderV2Request, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> DecreaseOrderV2Response: """Decrease an event-market order (V2). @@ -757,30 +856,32 @@ def decrease_v2( body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = self._post( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}/decrease", - params=params, json=body, + params=params, + json=body, + extra_headers=extra_headers, ) return DecreaseOrderV2Response.model_validate(data) def batch_create_v2( - self, *, request: BatchCreateOrdersV2Request, + self, *, request: BatchCreateOrdersV2Request, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") - data = self._post("/portfolio/events/orders/batched", json=body) + data = self._post( + "/portfolio/events/orders/batched", json=body, extra_headers=extra_headers + ) return BatchCreateOrdersV2Response.model_validate(data) def batch_cancel_v2( - self, *, request: BatchCancelOrdersV2Request, + self, *, request: BatchCancelOrdersV2Request, extra_headers: dict[str, str] | None = None ) -> BatchCancelOrdersV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = self._delete_with_body( - "/portfolio/events/orders/batched", json=body, + "/portfolio/events/orders/batched", json=body, extra_headers=extra_headers ) if data is None: - raise KalshiError( - "Expected BatchCancelOrdersV2Response body, got 204 No Content." - ) + raise KalshiError("Expected BatchCancelOrdersV2Response body, got 204 No Content.") return BatchCancelOrdersV2Response.model_validate(data) @@ -788,7 +889,9 @@ class AsyncOrdersResource(AsyncResource): """Async orders API.""" @overload - async def create(self, *, request: CreateOrderRequest) -> Order: ... + async def create( + self, *, request: CreateOrderRequest, extra_headers: dict[str, str] | None = None + ) -> Order: ... @overload async def create( self, @@ -810,6 +913,7 @@ async def create( cancel_order_on_pause: bool | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> Order: ... async def create( self, @@ -832,6 +936,7 @@ async def create( cancel_order_on_pause: bool | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Order: """Place a new order. @@ -858,24 +963,33 @@ async def create( self._require_auth() body = _build_create_order_body( request, - ticker=ticker, side=side, action=action, count=count, - yes_price=yes_price, no_price=no_price, - client_order_id=client_order_id, expiration_ts=expiration_ts, - buy_max_cost=buy_max_cost, time_in_force=time_in_force, - post_only=post_only, reduce_only=reduce_only, + ticker=ticker, + side=side, + action=action, + count=count, + yes_price=yes_price, + no_price=no_price, + client_order_id=client_order_id, + expiration_ts=expiration_ts, + buy_max_cost=buy_max_cost, + time_in_force=time_in_force, + post_only=post_only, + reduce_only=reduce_only, self_trade_prevention_type=self_trade_prevention_type, order_group_id=order_group_id, cancel_order_on_pause=cancel_order_on_pause, subaccount=subaccount, exchange_index=exchange_index, ) - data = await self._post("/portfolio/orders", json=body) + data = await self._post("/portfolio/orders", json=body, extra_headers=extra_headers) order_data = data.get("order", data) return Order.model_validate(order_data) - async def get(self, order_id: str) -> Order: + async def get(self, order_id: str, *, extra_headers: dict[str, str] | None = None) -> Order: self._require_auth() - data = await self._get(f"/portfolio/orders/{_seg(order_id, name='order_id')}") + data = await self._get( + f"/portfolio/orders/{_seg(order_id, name='order_id')}", extra_headers=extra_headers + ) order_data = data.get("order", data) return Order.model_validate(order_data) @@ -885,10 +999,15 @@ async def cancel( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) - await self._delete(f"/portfolio/orders/{_seg(order_id, name='order_id')}", params=params) + await self._delete( + f"/portfolio/orders/{_seg(order_id, name='order_id')}", + params=params, + extra_headers=extra_headers, + ) async def list( self, @@ -901,14 +1020,22 @@ async def list( limit: int | None = None, cursor: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Order]: self._require_auth() params = _list_orders_params( - ticker=ticker, event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, subaccount=subaccount, ) - return await self._list("/portfolio/orders", Order, "orders", params=params) + return await self._list( + "/portfolio/orders", Order, "orders", params=params, extra_headers=extra_headers + ) def list_all( self, @@ -921,68 +1048,79 @@ def list_all( limit: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Order]: """Non-async method that returns an async iterator for direct use with `async for`.""" self._require_auth() _validate_max_pages(max_pages) params = _list_orders_params( - ticker=ticker, event_ticker=event_ticker, status=status, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + ticker=ticker, + event_ticker=event_ticker, + status=status, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, subaccount=subaccount, ) return self._list_all( - "/portfolio/orders", Order, "orders", - params=params, max_pages=max_pages, + "/portfolio/orders", + Order, + "orders", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) @overload async def batch_create( - self, - *, - request: BatchCreateOrdersRequest, + self, *, request: BatchCreateOrdersRequest, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersResponse: ... @overload async def batch_create( - self, - orders: Sequence[CreateOrderRequest], + self, orders: Sequence[CreateOrderRequest], *, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersResponse: ... async def batch_create( self, orders: Sequence[CreateOrderRequest] | None = None, *, request: BatchCreateOrdersRequest | None = None, + extra_headers: dict[str, str] | None = None, ) -> BatchCreateOrdersResponse: """Place a batch of orders. See :meth:`OrdersResource.batch_create`.""" self._require_auth() body = _build_batch_create_body(request, orders) - data = await self._post_json("/portfolio/orders/batched", content=body) + data = await self._post_json( + "/portfolio/orders/batched", content=body, extra_headers=extra_headers + ) return BatchCreateOrdersResponse.model_validate(data) @overload async def batch_cancel( - self, - *, - request: BatchCancelOrdersRequest, + self, *, request: BatchCancelOrdersRequest, extra_headers: dict[str, str] | None = None ) -> BatchCancelOrdersResponse: ... @overload async def batch_cancel( self, orders: Sequence[BatchCancelOrdersRequestOrder | str], + *, + extra_headers: dict[str, str] | None = None, ) -> BatchCancelOrdersResponse: ... async def batch_cancel( self, orders: Sequence[BatchCancelOrdersRequestOrder | str] | None = None, *, request: BatchCancelOrdersRequest | None = None, + extra_headers: dict[str, str] | None = None, ) -> BatchCancelOrdersResponse: """Batch-cancel orders. See :meth:`OrdersResource.batch_cancel`.""" self._require_auth() body = _build_batch_cancel_body(request, orders) - data = await self._delete_with_body_json("/portfolio/orders/batched", content=body) + data = await self._delete_with_body_json( + "/portfolio/orders/batched", content=body, extra_headers=extra_headers + ) if data is None: - raise KalshiError( - "Expected BatchCancelOrdersResponse body, got 204 No Content." - ) + raise KalshiError("Expected BatchCancelOrdersResponse body, got 204 No Content.") return BatchCancelOrdersResponse.model_validate(data) async def fills( @@ -995,14 +1133,21 @@ async def fills( limit: int | None = None, cursor: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Fill]: self._require_auth() params = _fills_params( - ticker=ticker, order_id=order_id, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=cursor, + ticker=ticker, + order_id=order_id, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=cursor, subaccount=subaccount, ) - return await self._list("/portfolio/fills", Fill, "fills", params=params) + return await self._list( + "/portfolio/fills", Fill, "fills", params=params, extra_headers=extra_headers + ) def fills_all( self, @@ -1014,22 +1159,35 @@ def fills_all( limit: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Fill]: self._require_auth() _validate_max_pages(max_pages) params = _fills_params( - ticker=ticker, order_id=order_id, - min_ts=min_ts, max_ts=max_ts, limit=limit, cursor=None, + ticker=ticker, + order_id=order_id, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + cursor=None, subaccount=subaccount, ) return self._list_all( - "/portfolio/fills", Fill, "fills", - params=params, max_pages=max_pages, + "/portfolio/fills", + Fill, + "fills", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) @overload async def amend( - self, order_id: str, *, request: AmendOrderRequest, + self, + order_id: str, + *, + request: AmendOrderRequest, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: ... @overload async def amend( @@ -1046,6 +1204,7 @@ async def amend( updated_client_order_id: str | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: ... async def amend( self, @@ -1062,23 +1221,36 @@ async def amend( updated_client_order_id: str | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderResponse: self._require_auth() body = _build_amend_body( request, - ticker=ticker, side=side, action=action, - yes_price=yes_price, no_price=no_price, count=count, + ticker=ticker, + side=side, + action=action, + yes_price=yes_price, + no_price=no_price, + count=count, client_order_id=client_order_id, updated_client_order_id=updated_client_order_id, subaccount=subaccount, exchange_index=exchange_index, ) - data = await self._post(f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", json=body) # noqa: E501 + data = await self._post( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/amend", + json=body, + extra_headers=extra_headers, + ) return AmendOrderResponse.model_validate(data) @overload async def decrease( - self, order_id: str, *, request: DecreaseOrderRequest, + self, + order_id: str, + *, + request: DecreaseOrderRequest, + extra_headers: dict[str, str] | None = None, ) -> Order: ... @overload async def decrease( @@ -1089,6 +1261,7 @@ async def decrease( reduce_to: int | None = ..., subaccount: int | None = ..., exchange_index: int | None = ..., + extra_headers: dict[str, str] | None = None, ) -> Order: ... async def decrease( self, @@ -1099,13 +1272,21 @@ async def decrease( reduce_to: int | None = None, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Order: self._require_auth() body = _build_decrease_body( - request, reduce_by=reduce_by, reduce_to=reduce_to, - subaccount=subaccount, exchange_index=exchange_index, + request, + reduce_by=reduce_by, + reduce_to=reduce_to, + subaccount=subaccount, + exchange_index=exchange_index, + ) + data = await self._post( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", + json=body, + extra_headers=extra_headers, ) - data = await self._post(f"/portfolio/orders/{_seg(order_id, name='order_id')}/decrease", json=body) # noqa: E501 order_data = data.get("order", data) return Order.model_validate(order_data) @@ -1115,6 +1296,7 @@ async def queue_positions( market_tickers: builtins.list[str] | str | None = None, event_ticker: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[OrderQueuePosition]: self._require_auth() params = _queue_positions_params( @@ -1122,23 +1304,30 @@ async def queue_positions( event_ticker=event_ticker, subaccount=subaccount, ) - data = await self._get("/portfolio/orders/queue_positions", params=params) + data = await self._get( + "/portfolio/orders/queue_positions", params=params, extra_headers=extra_headers + ) raw = data.get("queue_positions", []) return [OrderQueuePosition.model_validate(item) for item in raw] - async def queue_position(self, order_id: str) -> Decimal: + async def queue_position( + self, order_id: str, *, extra_headers: dict[str, str] | None = None + ) -> Decimal: self._require_auth() - data = await self._get(f"/portfolio/orders/{_seg(order_id, name='order_id')}/queue_position") # noqa: E501 + data = await self._get( + f"/portfolio/orders/{_seg(order_id, name='order_id')}/queue_position", + extra_headers=extra_headers, + ) return _parse_queue_position(data) # V2 event-market orders (spec v3.18.0). See OrdersResource counterparts. async def create_v2( - self, *, request: CreateOrderV2Request, + self, *, request: CreateOrderV2Request, extra_headers: dict[str, str] | None = None ) -> CreateOrderV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") - data = await self._post("/portfolio/events/orders", json=body) + data = await self._post("/portfolio/events/orders", json=body, extra_headers=extra_headers) return CreateOrderV2Response.model_validate(data) async def cancel_v2( @@ -1147,16 +1336,17 @@ async def cancel_v2( *, subaccount: int | None = None, exchange_index: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> CancelOrderV2Response: self._require_auth() params = _params(subaccount=subaccount, exchange_index=exchange_index) data = await self._delete( - f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", params=params, + f"/portfolio/events/orders/{_seg(order_id, name='order_id')}", + params=params, + extra_headers=extra_headers, ) if data is None: - raise KalshiError( - "Expected CancelOrderV2Response body, got 204 No Content." - ) + raise KalshiError("Expected CancelOrderV2Response body, got 204 No Content.") return CancelOrderV2Response.model_validate(data) async def amend_v2( @@ -1165,6 +1355,7 @@ async def amend_v2( *, request: AmendOrderV2Request, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AmendOrderV2Response: """Amend an event-market order (V2). @@ -1178,7 +1369,9 @@ async def amend_v2( body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = await self._post( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}/amend", - params=params, json=body, + params=params, + json=body, + extra_headers=extra_headers, ) return AmendOrderV2Response.model_validate(data) @@ -1188,6 +1381,7 @@ async def decrease_v2( *, request: DecreaseOrderV2Request, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> DecreaseOrderV2Response: """Decrease an event-market order (V2). @@ -1202,28 +1396,30 @@ async def decrease_v2( body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = await self._post( f"/portfolio/events/orders/{_seg(order_id, name='order_id')}/decrease", - params=params, json=body, + params=params, + json=body, + extra_headers=extra_headers, ) return DecreaseOrderV2Response.model_validate(data) async def batch_create_v2( - self, *, request: BatchCreateOrdersV2Request, + self, *, request: BatchCreateOrdersV2Request, extra_headers: dict[str, str] | None = None ) -> BatchCreateOrdersV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") - data = await self._post("/portfolio/events/orders/batched", json=body) + data = await self._post( + "/portfolio/events/orders/batched", json=body, extra_headers=extra_headers + ) return BatchCreateOrdersV2Response.model_validate(data) async def batch_cancel_v2( - self, *, request: BatchCancelOrdersV2Request, + self, *, request: BatchCancelOrdersV2Request, extra_headers: dict[str, str] | None = None ) -> BatchCancelOrdersV2Response: self._require_auth() body = request.model_dump(exclude_none=True, by_alias=True, mode="json") data = await self._delete_with_body( - "/portfolio/events/orders/batched", json=body, + "/portfolio/events/orders/batched", json=body, extra_headers=extra_headers ) if data is None: - raise KalshiError( - "Expected BatchCancelOrdersV2Response body, got 204 No Content." - ) + raise KalshiError("Expected BatchCancelOrdersV2Response body, got 204 No Content.") return BatchCancelOrdersV2Response.model_validate(data) diff --git a/kalshi/resources/portfolio.py b/kalshi/resources/portfolio.py index 0ca78220..d99c37a4 100644 --- a/kalshi/resources/portfolio.py +++ b/kalshi/resources/portfolio.py @@ -70,10 +70,12 @@ def _settlements_params( class PortfolioResource(SyncResource): """Sync portfolio API.""" - def balance(self, *, subaccount: int | None = None) -> Balance: + def balance( + self, *, subaccount: int | None = None, extra_headers: dict[str, str] | None = None + ) -> Balance: self._require_auth() params = _params(subaccount=subaccount) - data = self._get("/portfolio/balance", params=params) + data = self._get("/portfolio/balance", params=params, extra_headers=extra_headers) return Balance.model_validate(data) def positions( @@ -85,13 +87,18 @@ def positions( ticker: str | None = None, event_ticker: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> PositionsResponse: self._require_auth() params = _positions_params( - limit=limit, cursor=cursor, count_filter=count_filter, - ticker=ticker, event_ticker=event_ticker, subaccount=subaccount, + limit=limit, + cursor=cursor, + count_filter=count_filter, + ticker=ticker, + event_ticker=event_ticker, + subaccount=subaccount, ) - data = self._get("/portfolio/positions", params=params) + data = self._get("/portfolio/positions", params=params, extra_headers=extra_headers) return PositionsResponse.model_validate(data) def settlements( @@ -104,14 +111,25 @@ def settlements( min_ts: int | None = None, max_ts: int | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Settlement]: self._require_auth() params = _settlements_params( - limit=limit, cursor=cursor, ticker=ticker, - event_ticker=event_ticker, min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + event_ticker=event_ticker, + min_ts=min_ts, + max_ts=max_ts, subaccount=subaccount, ) - return self._list("/portfolio/settlements", Settlement, "settlements", params=params) + return self._list( + "/portfolio/settlements", + Settlement, + "settlements", + params=params, + extra_headers=extra_headers, + ) def settlements_all( self, @@ -123,27 +141,40 @@ def settlements_all( max_ts: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Settlement]: self._require_auth() _validate_max_pages(max_pages) params = _settlements_params( - limit=limit, cursor=None, ticker=ticker, - event_ticker=event_ticker, min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + event_ticker=event_ticker, + min_ts=min_ts, + max_ts=max_ts, subaccount=subaccount, ) return self._list_all( - "/portfolio/settlements", Settlement, "settlements", - params=params, max_pages=max_pages, + "/portfolio/settlements", + Settlement, + "settlements", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - def total_resting_order_value(self) -> TotalRestingOrderValue: + def total_resting_order_value( + self, *, extra_headers: dict[str, str] | None = None + ) -> TotalRestingOrderValue: """Total value of resting orders in cents. FCM-members only. Non-FCM accounts receive 403; demo mirrors prod on this route per Path B audit (2026-04-18). """ self._require_auth() - data = self._get("/portfolio/summary/total_resting_order_value") + data = self._get( + "/portfolio/summary/total_resting_order_value", extra_headers=extra_headers + ) return TotalRestingOrderValue.model_validate(data) def deposits( @@ -151,12 +182,13 @@ def deposits( *, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Deposit]: self._require_auth() _validate_limit(limit, hi=500) params = _params(limit=limit, cursor=cursor) return self._list( - "/portfolio/deposits", Deposit, "deposits", params=params, + "/portfolio/deposits", Deposit, "deposits", params=params, extra_headers=extra_headers ) def deposits_all( @@ -164,14 +196,19 @@ def deposits_all( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Deposit]: self._require_auth() _validate_max_pages(max_pages) _validate_limit(limit, hi=500) params = _params(limit=limit) return self._list_all( - "/portfolio/deposits", Deposit, "deposits", - params=params, max_pages=max_pages, + "/portfolio/deposits", + Deposit, + "deposits", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) def withdrawals( @@ -179,12 +216,17 @@ def withdrawals( *, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Withdrawal]: self._require_auth() _validate_limit(limit, hi=500) params = _params(limit=limit, cursor=cursor) return self._list( - "/portfolio/withdrawals", Withdrawal, "withdrawals", params=params, + "/portfolio/withdrawals", + Withdrawal, + "withdrawals", + params=params, + extra_headers=extra_headers, ) def withdrawals_all( @@ -192,24 +234,31 @@ def withdrawals_all( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[Withdrawal]: self._require_auth() _validate_max_pages(max_pages) _validate_limit(limit, hi=500) params = _params(limit=limit) return self._list_all( - "/portfolio/withdrawals", Withdrawal, "withdrawals", - params=params, max_pages=max_pages, + "/portfolio/withdrawals", + Withdrawal, + "withdrawals", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) class AsyncPortfolioResource(AsyncResource): """Async portfolio API.""" - async def balance(self, *, subaccount: int | None = None) -> Balance: + async def balance( + self, *, subaccount: int | None = None, extra_headers: dict[str, str] | None = None + ) -> Balance: self._require_auth() params = _params(subaccount=subaccount) - data = await self._get("/portfolio/balance", params=params) + data = await self._get("/portfolio/balance", params=params, extra_headers=extra_headers) return Balance.model_validate(data) async def positions( @@ -221,13 +270,18 @@ async def positions( ticker: str | None = None, event_ticker: str | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> PositionsResponse: self._require_auth() params = _positions_params( - limit=limit, cursor=cursor, count_filter=count_filter, - ticker=ticker, event_ticker=event_ticker, subaccount=subaccount, + limit=limit, + cursor=cursor, + count_filter=count_filter, + ticker=ticker, + event_ticker=event_ticker, + subaccount=subaccount, ) - data = await self._get("/portfolio/positions", params=params) + data = await self._get("/portfolio/positions", params=params, extra_headers=extra_headers) return PositionsResponse.model_validate(data) async def settlements( @@ -240,15 +294,24 @@ async def settlements( min_ts: int | None = None, max_ts: int | None = None, subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Settlement]: self._require_auth() params = _settlements_params( - limit=limit, cursor=cursor, ticker=ticker, - event_ticker=event_ticker, min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=cursor, + ticker=ticker, + event_ticker=event_ticker, + min_ts=min_ts, + max_ts=max_ts, subaccount=subaccount, ) return await self._list( - "/portfolio/settlements", Settlement, "settlements", params=params + "/portfolio/settlements", + Settlement, + "settlements", + params=params, + extra_headers=extra_headers, ) def settlements_all( @@ -261,23 +324,36 @@ def settlements_all( max_ts: int | None = None, subaccount: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Settlement]: self._require_auth() _validate_max_pages(max_pages) params = _settlements_params( - limit=limit, cursor=None, ticker=ticker, - event_ticker=event_ticker, min_ts=min_ts, max_ts=max_ts, + limit=limit, + cursor=None, + ticker=ticker, + event_ticker=event_ticker, + min_ts=min_ts, + max_ts=max_ts, subaccount=subaccount, ) return self._list_all( - "/portfolio/settlements", Settlement, "settlements", - params=params, max_pages=max_pages, + "/portfolio/settlements", + Settlement, + "settlements", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) - async def total_resting_order_value(self) -> TotalRestingOrderValue: + async def total_resting_order_value( + self, *, extra_headers: dict[str, str] | None = None + ) -> TotalRestingOrderValue: """Total value of resting orders in cents. FCM-members only.""" self._require_auth() - data = await self._get("/portfolio/summary/total_resting_order_value") + data = await self._get( + "/portfolio/summary/total_resting_order_value", extra_headers=extra_headers + ) return TotalRestingOrderValue.model_validate(data) async def deposits( @@ -285,12 +361,13 @@ async def deposits( *, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Deposit]: self._require_auth() _validate_limit(limit, hi=500) params = _params(limit=limit, cursor=cursor) return await self._list( - "/portfolio/deposits", Deposit, "deposits", params=params, + "/portfolio/deposits", Deposit, "deposits", params=params, extra_headers=extra_headers ) def deposits_all( @@ -298,6 +375,7 @@ def deposits_all( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Deposit]: """Returns an async iterator — use ``async for``.""" self._require_auth() @@ -305,8 +383,12 @@ def deposits_all( _validate_limit(limit, hi=500) params = _params(limit=limit) return self._list_all( - "/portfolio/deposits", Deposit, "deposits", - params=params, max_pages=max_pages, + "/portfolio/deposits", + Deposit, + "deposits", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) async def withdrawals( @@ -314,12 +396,17 @@ async def withdrawals( *, limit: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[Withdrawal]: self._require_auth() _validate_limit(limit, hi=500) params = _params(limit=limit, cursor=cursor) return await self._list( - "/portfolio/withdrawals", Withdrawal, "withdrawals", params=params, + "/portfolio/withdrawals", + Withdrawal, + "withdrawals", + params=params, + extra_headers=extra_headers, ) def withdrawals_all( @@ -327,6 +414,7 @@ def withdrawals_all( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[Withdrawal]: """Returns an async iterator — use ``async for``.""" self._require_auth() @@ -334,6 +422,10 @@ def withdrawals_all( _validate_limit(limit, hi=500) params = _params(limit=limit) return self._list_all( - "/portfolio/withdrawals", Withdrawal, "withdrawals", - params=params, max_pages=max_pages, + "/portfolio/withdrawals", + Withdrawal, + "withdrawals", + params=params, + max_pages=max_pages, + extra_headers=extra_headers, ) diff --git a/kalshi/resources/search.py b/kalshi/resources/search.py index 5cb596f4..e4975b85 100644 --- a/kalshi/resources/search.py +++ b/kalshi/resources/search.py @@ -12,22 +12,30 @@ class SearchResource(SyncResource): """Sync search/discovery API.""" - def tags_by_categories(self) -> GetTagsForSeriesCategoriesResponse: - data = self._get("/search/tags_by_categories") + def tags_by_categories( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetTagsForSeriesCategoriesResponse: + data = self._get("/search/tags_by_categories", extra_headers=extra_headers) return GetTagsForSeriesCategoriesResponse.model_validate(data) - def filters_by_sport(self) -> GetFiltersBySportsResponse: - data = self._get("/search/filters_by_sport") + def filters_by_sport( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetFiltersBySportsResponse: + data = self._get("/search/filters_by_sport", extra_headers=extra_headers) return GetFiltersBySportsResponse.model_validate(data) class AsyncSearchResource(AsyncResource): """Async search/discovery API.""" - async def tags_by_categories(self) -> GetTagsForSeriesCategoriesResponse: - data = await self._get("/search/tags_by_categories") + async def tags_by_categories( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetTagsForSeriesCategoriesResponse: + data = await self._get("/search/tags_by_categories", extra_headers=extra_headers) return GetTagsForSeriesCategoriesResponse.model_validate(data) - async def filters_by_sport(self) -> GetFiltersBySportsResponse: - data = await self._get("/search/filters_by_sport") + async def filters_by_sport( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetFiltersBySportsResponse: + data = await self._get("/search/filters_by_sport", extra_headers=extra_headers) return GetFiltersBySportsResponse.model_validate(data) diff --git a/kalshi/resources/series.py b/kalshi/resources/series.py index 0f97c1b4..8cc6719f 100644 --- a/kalshi/resources/series.py +++ b/kalshi/resources/series.py @@ -45,7 +45,10 @@ def _fee_changes_params( def _event_candlesticks_params( - *, start_ts: int, end_ts: int, period_interval: int, + *, + start_ts: int, + end_ts: int, + period_interval: int, ) -> dict[str, Any]: return _params( start_ts=start_ts, @@ -80,14 +83,16 @@ def list( include_product_metadata: bool | None = None, include_volume: bool | None = None, min_updated_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Series]: params = _list_series_params( - category=category, tags=tags, + category=category, + tags=tags, include_product_metadata=include_product_metadata, include_volume=include_volume, min_updated_ts=min_updated_ts, ) - data = self._get("/series", params=params) + data = self._get("/series", params=params, extra_headers=extra_headers) raw = data.get("series", []) return [Series.model_validate(item) for item in raw] @@ -96,11 +101,16 @@ def get( series_ticker: str, *, include_volume: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> Series: params = _params( include_volume=_bool_param(include_volume), ) - data = self._get(f"/series/{_seg(series_ticker, name='series_ticker')}", params=params) + data = self._get( + f"/series/{_seg(series_ticker, name='series_ticker')}", + params=params, + extra_headers=extra_headers, + ) return Series.model_validate(data.get("series", data)) def fee_changes( @@ -108,11 +118,13 @@ def fee_changes( *, series_ticker: str | None = None, show_historical: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[SeriesFeeChange]: params = _fee_changes_params( - series_ticker=series_ticker, show_historical=show_historical, + series_ticker=series_ticker, + show_historical=show_historical, ) - data = self._get("/series/fee_changes", params=params) + data = self._get("/series/fee_changes", params=params, extra_headers=extra_headers) raw = data.get("series_fee_change_arr", []) return [SeriesFeeChange.model_validate(item) for item in raw] @@ -124,14 +136,17 @@ def event_candlesticks( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> EventCandlesticks: params = _event_candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/events/{_seg(ticker, name='ticker')}/candlesticks", # noqa: E501 params=params, + extra_headers=extra_headers, ) return EventCandlesticks.model_validate(data) @@ -144,16 +159,19 @@ def forecast_percentile_history( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[ForecastPercentilesPoint]: self._require_auth() params = _forecast_history_params( percentiles=percentiles, - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/events/{_seg(ticker, name='ticker')}/forecast_percentile_history", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("forecast_history", []) return [ForecastPercentilesPoint.model_validate(item) for item in raw] @@ -170,14 +188,16 @@ async def list( include_product_metadata: bool | None = None, include_volume: bool | None = None, min_updated_ts: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[Series]: params = _list_series_params( - category=category, tags=tags, + category=category, + tags=tags, include_product_metadata=include_product_metadata, include_volume=include_volume, min_updated_ts=min_updated_ts, ) - data = await self._get("/series", params=params) + data = await self._get("/series", params=params, extra_headers=extra_headers) raw = data.get("series", []) return [Series.model_validate(item) for item in raw] @@ -186,11 +206,16 @@ async def get( series_ticker: str, *, include_volume: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> Series: params = _params( include_volume=_bool_param(include_volume), ) - data = await self._get(f"/series/{_seg(series_ticker, name='series_ticker')}", params=params) # noqa: E501 + data = await self._get( + f"/series/{_seg(series_ticker, name='series_ticker')}", + params=params, + extra_headers=extra_headers, + ) return Series.model_validate(data.get("series", data)) async def fee_changes( @@ -198,11 +223,13 @@ async def fee_changes( *, series_ticker: str | None = None, show_historical: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[SeriesFeeChange]: params = _fee_changes_params( - series_ticker=series_ticker, show_historical=show_historical, + series_ticker=series_ticker, + show_historical=show_historical, ) - data = await self._get("/series/fee_changes", params=params) + data = await self._get("/series/fee_changes", params=params, extra_headers=extra_headers) raw = data.get("series_fee_change_arr", []) return [SeriesFeeChange.model_validate(item) for item in raw] @@ -214,14 +241,17 @@ async def event_candlesticks( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> EventCandlesticks: params = _event_candlesticks_params( - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = await self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/events/{_seg(ticker, name='ticker')}/candlesticks", # noqa: E501 params=params, + extra_headers=extra_headers, ) return EventCandlesticks.model_validate(data) @@ -234,16 +264,19 @@ async def forecast_percentile_history( start_ts: int, end_ts: int, period_interval: int, + extra_headers: dict[str, str] | None = None, ) -> builtins.list[ForecastPercentilesPoint]: self._require_auth() params = _forecast_history_params( percentiles=percentiles, - start_ts=start_ts, end_ts=end_ts, + start_ts=start_ts, + end_ts=end_ts, period_interval=period_interval, ) data = await self._get( f"/series/{_seg(series_ticker, name='series_ticker')}/events/{_seg(ticker, name='ticker')}/forecast_percentile_history", # noqa: E501 params=params, + extra_headers=extra_headers, ) raw = data.get("forecast_history", []) return [ForecastPercentilesPoint.model_validate(item) for item in raw] diff --git a/kalshi/resources/structured_targets.py b/kalshi/resources/structured_targets.py index 4bc5eaa5..1febb760 100644 --- a/kalshi/resources/structured_targets.py +++ b/kalshi/resources/structured_targets.py @@ -37,6 +37,7 @@ def list( competition: str | None = None, page_size: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[StructuredTarget]: _validate_limit(page_size, hi=2000, name="page_size") params = _params( @@ -51,6 +52,7 @@ def list( StructuredTarget, "structured_targets", params=params, + extra_headers=extra_headers, ) def list_all( @@ -61,6 +63,7 @@ def list_all( competition: str | None = None, page_size: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[StructuredTarget]: _validate_max_pages(max_pages) _validate_limit(page_size, hi=2000, name="page_size") @@ -76,10 +79,16 @@ def list_all( "structured_targets", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) - def get(self, structured_target_id: str) -> StructuredTarget | None: - data = self._get(f"/structured_targets/{_seg(structured_target_id, name='structured_target_id')}") # noqa: E501 + def get( + self, structured_target_id: str, *, extra_headers: dict[str, str] | None = None + ) -> StructuredTarget | None: + data = self._get( + f"/structured_targets/{_seg(structured_target_id, name='structured_target_id')}", + extra_headers=extra_headers, + ) return GetStructuredTargetResponse.model_validate(data).structured_target @@ -94,6 +103,7 @@ async def list( competition: str | None = None, page_size: int | None = None, cursor: str | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[StructuredTarget]: _validate_limit(page_size, hi=2000, name="page_size") params = _params( @@ -108,6 +118,7 @@ async def list( StructuredTarget, "structured_targets", params=params, + extra_headers=extra_headers, ) def list_all( @@ -118,6 +129,7 @@ def list_all( competition: str | None = None, page_size: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[StructuredTarget]: """Returns an async iterator — use ``async for``.""" _validate_max_pages(max_pages) @@ -134,8 +146,14 @@ def list_all( "structured_targets", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) - async def get(self, structured_target_id: str) -> StructuredTarget | None: - data = await self._get(f"/structured_targets/{_seg(structured_target_id, name='structured_target_id')}") # noqa: E501 + async def get( + self, structured_target_id: str, *, extra_headers: dict[str, str] | None = None + ) -> StructuredTarget | None: + data = await self._get( + f"/structured_targets/{_seg(structured_target_id, name='structured_target_id')}", + extra_headers=extra_headers, + ) return GetStructuredTargetResponse.model_validate(data).structured_target diff --git a/kalshi/resources/subaccounts.py b/kalshi/resources/subaccounts.py index 97311dd7..f63157cb 100644 --- a/kalshi/resources/subaccounts.py +++ b/kalshi/resources/subaccounts.py @@ -44,8 +44,10 @@ def _build_transfer_body( ) if request is None: if ( - client_transfer_id is None or from_subaccount is None - or to_subaccount is None or amount_cents is None + client_transfer_id is None + or from_subaccount is None + or to_subaccount is None + or amount_cents is None ): raise TypeError( "transfer() requires `client_transfer_id`, `from_subaccount`, " @@ -54,9 +56,7 @@ def _build_transfer_body( # Accept str for caller ergonomics; coerce once to surface a clean # ValueError on malformed strings before the model validator sees them. uid = ( - client_transfer_id - if isinstance(client_transfer_id, UUID) - else UUID(client_transfer_id) + client_transfer_id if isinstance(client_transfer_id, UUID) else UUID(client_transfer_id) ) request = ApplySubaccountTransferRequest( client_transfer_id=uid, @@ -74,7 +74,9 @@ def _build_update_netting_body( enabled: bool | None, ) -> dict[str, Any]: _check_request_exclusive( - request, subaccount_number=subaccount_number, enabled=enabled, + request, + subaccount_number=subaccount_number, + enabled=enabled, ) if request is None: if subaccount_number is None or enabled is None: @@ -83,7 +85,8 @@ def _build_update_netting_body( "(or pass `request=...`)" ) request = UpdateSubaccountNettingRequest( - subaccount_number=subaccount_number, enabled=enabled, + subaccount_number=subaccount_number, + enabled=enabled, ) return request.model_dump(exclude_none=True, by_alias=True, mode="json") @@ -98,17 +101,22 @@ class SubaccountsResource(SyncResource): empty body (spec takes no request payload). """ - def create(self) -> CreateSubaccountResponse: + def create(self, *, extra_headers: dict[str, str] | None = None) -> CreateSubaccountResponse: self._require_auth() # Spec defines no requestBody, but httpx omits Content-Type when no # body is passed and demo rejects the POST with `invalid_content_type`. # json={} forces Content-Type: application/json — same workaround # used on order_groups reset/trigger PUTs. - data = self._post("/portfolio/subaccounts", json={}) + data = self._post("/portfolio/subaccounts", json={}, extra_headers=extra_headers) return CreateSubaccountResponse.model_validate(data) @overload - def transfer(self, *, request: ApplySubaccountTransferRequest) -> None: ... + def transfer( + self, + *, + request: ApplySubaccountTransferRequest, + extra_headers: dict[str, str] | None = None, + ) -> None: ... @overload def transfer( self, @@ -117,6 +125,7 @@ def transfer( from_subaccount: int, to_subaccount: int, amount_cents: int, + extra_headers: dict[str, str] | None = None, ) -> None: ... def transfer( self, @@ -126,6 +135,7 @@ def transfer( from_subaccount: int | None = None, to_subaccount: int | None = None, amount_cents: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_transfer_body( @@ -135,15 +145,21 @@ def transfer( to_subaccount=to_subaccount, amount_cents=amount_cents, ) - self._post("/portfolio/subaccounts/transfer", json=body) + self._post("/portfolio/subaccounts/transfer", json=body, extra_headers=extra_headers) - def list_balances(self) -> GetSubaccountBalancesResponse: + def list_balances( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetSubaccountBalancesResponse: self._require_auth() - data = self._get("/portfolio/subaccounts/balances") + data = self._get("/portfolio/subaccounts/balances", extra_headers=extra_headers) return GetSubaccountBalancesResponse.model_validate(data) def list_transfers( - self, *, cursor: str | None = None, limit: int | None = None, + self, + *, + cursor: str | None = None, + limit: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[SubaccountTransfer]: self._require_auth() _validate_limit(limit, hi=1000) @@ -153,6 +169,7 @@ def list_transfers( SubaccountTransfer, "transfers", params=params, + extra_headers=extra_headers, ) def list_all_transfers( @@ -160,6 +177,7 @@ def list_all_transfers( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Iterator[SubaccountTransfer]: self._require_auth() _validate_max_pages(max_pages) @@ -171,15 +189,19 @@ def list_all_transfers( "transfers", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) @overload def update_netting( - self, *, request: UpdateSubaccountNettingRequest, + self, + *, + request: UpdateSubaccountNettingRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload def update_netting( - self, *, subaccount_number: int, enabled: bool, + self, *, subaccount_number: int, enabled: bool, extra_headers: dict[str, str] | None = None ) -> None: ... def update_netting( self, @@ -187,32 +209,42 @@ def update_netting( request: UpdateSubaccountNettingRequest | None = None, subaccount_number: int | None = None, enabled: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_update_netting_body( - request, subaccount_number=subaccount_number, enabled=enabled, + request, + subaccount_number=subaccount_number, + enabled=enabled, ) - self._put("/portfolio/subaccounts/netting", json=body) + self._put("/portfolio/subaccounts/netting", json=body, extra_headers=extra_headers) - def get_netting(self) -> GetSubaccountNettingResponse: + def get_netting( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetSubaccountNettingResponse: self._require_auth() - data = self._get("/portfolio/subaccounts/netting") + data = self._get("/portfolio/subaccounts/netting", extra_headers=extra_headers) return GetSubaccountNettingResponse.model_validate(data) class AsyncSubaccountsResource(AsyncResource): """Async subaccounts API.""" - async def create(self) -> CreateSubaccountResponse: + async def create( + self, *, extra_headers: dict[str, str] | None = None + ) -> CreateSubaccountResponse: self._require_auth() # json={} forces Content-Type: application/json — demo rejects the # POST with `invalid_content_type` when no body is passed. - data = await self._post("/portfolio/subaccounts", json={}) + data = await self._post("/portfolio/subaccounts", json={}, extra_headers=extra_headers) return CreateSubaccountResponse.model_validate(data) @overload async def transfer( - self, *, request: ApplySubaccountTransferRequest, + self, + *, + request: ApplySubaccountTransferRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload async def transfer( @@ -222,6 +254,7 @@ async def transfer( from_subaccount: int, to_subaccount: int, amount_cents: int, + extra_headers: dict[str, str] | None = None, ) -> None: ... async def transfer( self, @@ -231,6 +264,7 @@ async def transfer( from_subaccount: int | None = None, to_subaccount: int | None = None, amount_cents: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_transfer_body( @@ -240,15 +274,21 @@ async def transfer( to_subaccount=to_subaccount, amount_cents=amount_cents, ) - await self._post("/portfolio/subaccounts/transfer", json=body) + await self._post("/portfolio/subaccounts/transfer", json=body, extra_headers=extra_headers) - async def list_balances(self) -> GetSubaccountBalancesResponse: + async def list_balances( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetSubaccountBalancesResponse: self._require_auth() - data = await self._get("/portfolio/subaccounts/balances") + data = await self._get("/portfolio/subaccounts/balances", extra_headers=extra_headers) return GetSubaccountBalancesResponse.model_validate(data) async def list_transfers( - self, *, cursor: str | None = None, limit: int | None = None, + self, + *, + cursor: str | None = None, + limit: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> Page[SubaccountTransfer]: self._require_auth() _validate_limit(limit, hi=1000) @@ -258,6 +298,7 @@ async def list_transfers( SubaccountTransfer, "transfers", params=params, + extra_headers=extra_headers, ) def list_all_transfers( @@ -265,6 +306,7 @@ def list_all_transfers( *, limit: int | None = None, max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[SubaccountTransfer]: # Plain `def` (not `async def`) so _require_auth and _validate_max_pages # run at call time, not when the returned AsyncIterator is awaited. @@ -278,15 +320,19 @@ def list_all_transfers( "transfers", params=params, max_pages=max_pages, + extra_headers=extra_headers, ) @overload async def update_netting( - self, *, request: UpdateSubaccountNettingRequest, + self, + *, + request: UpdateSubaccountNettingRequest, + extra_headers: dict[str, str] | None = None, ) -> None: ... @overload async def update_netting( - self, *, subaccount_number: int, enabled: bool, + self, *, subaccount_number: int, enabled: bool, extra_headers: dict[str, str] | None = None ) -> None: ... async def update_netting( self, @@ -294,14 +340,19 @@ async def update_netting( request: UpdateSubaccountNettingRequest | None = None, subaccount_number: int | None = None, enabled: bool | None = None, + extra_headers: dict[str, str] | None = None, ) -> None: self._require_auth() body = _build_update_netting_body( - request, subaccount_number=subaccount_number, enabled=enabled, + request, + subaccount_number=subaccount_number, + enabled=enabled, ) - await self._put("/portfolio/subaccounts/netting", json=body) + await self._put("/portfolio/subaccounts/netting", json=body, extra_headers=extra_headers) - async def get_netting(self) -> GetSubaccountNettingResponse: + async def get_netting( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetSubaccountNettingResponse: self._require_auth() - data = await self._get("/portfolio/subaccounts/netting") + data = await self._get("/portfolio/subaccounts/netting", extra_headers=extra_headers) return GetSubaccountNettingResponse.model_validate(data) diff --git a/tests/test_contracts.py b/tests/test_contracts.py index cdba9585..bd7fe229 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1081,6 +1081,10 @@ def _assert_params_match( assert hasattr(module, cls_name), f"Missing async sibling {cls_name} in {module_name}" sdk_params = _signature_params(sdk_fqn) + # #253: ``extra_headers`` is an SDK-meta kwarg (per-request header overrides) + # threaded onto every public resource method. It is never part of the + # OpenAPI spec, so exclude it from the drift comparison globally. + sdk_params.discard("extra_headers") spec_params_list = _resolve_path_params( self.spec, entry.path_template, diff --git a/tests/test_extra_headers_plumbing.py b/tests/test_extra_headers_plumbing.py new file mode 100644 index 00000000..81792707 --- /dev/null +++ b/tests/test_extra_headers_plumbing.py @@ -0,0 +1,209 @@ +"""Tests for #253: per-request ``extra_headers`` is reachable from every public +resource method. + +* Three representative resources (orders, markets, portfolio) exercise the + end-to-end wire delivery and the transport-level auth-header precedence + guarantee (``KALSHI-ACCESS-*`` cannot be spoofed via ``extra_headers``). +* A single parametric test enumerates every public method on every + ``SyncResource`` / ``AsyncResource`` subclass and asserts the ``extra_headers`` + kwarg exists with a ``None`` default. +""" + +from __future__ import annotations + +import inspect +import pkgutil +from typing import Any + +import httpx +import pytest +import respx + +from kalshi import KalshiClient +from kalshi.auth import KalshiAuth +from kalshi.config import DEMO_WS_URL, KalshiConfig +from kalshi.resources._base import AsyncResource, SyncResource +from tests._model_fixtures import market_dict, order_dict + +MOCK_BASE = "https://demo-api.kalshi.co/trade-api/v2" + + +def _config(**overrides: Any) -> KalshiConfig: + return KalshiConfig( + base_url=MOCK_BASE, + ws_base_url=DEMO_WS_URL, + timeout=5.0, + max_retries=0, + **overrides, + ) + + +def _example_order_payload() -> dict[str, Any]: + return order_dict(order_id="o1", ticker="BTC", side="yes", status="resting") + + +def _example_market_payload() -> dict[str, Any]: + return market_dict(ticker="BTC") + + +class TestOrders: + @respx.mock + def test_create_order_threads_extra_headers(self, test_auth: KalshiAuth) -> None: + route = respx.post(f"{MOCK_BASE}/portfolio/orders").mock( + return_value=httpx.Response(200, json={"order": _example_order_payload()}) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.orders.create( + ticker="BTC", + side="yes", + action="buy", + client_order_id="cli-1", + count=1, + yes_price=50, + extra_headers={"Idempotency-Key": "abc"}, + ) + assert route.calls[0].request.headers["Idempotency-Key"] == "abc" + + @respx.mock + def test_create_order_extra_headers_cannot_override_auth(self, test_auth: KalshiAuth) -> None: + route = respx.post(f"{MOCK_BASE}/portfolio/orders").mock( + return_value=httpx.Response(200, json={"order": _example_order_payload()}) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.orders.create( + ticker="BTC", + side="yes", + action="buy", + client_order_id="cli-1", + count=1, + yes_price=50, + extra_headers={ + "KALSHI-ACCESS-KEY": "spoofed", + "KALSHI-ACCESS-SIGNATURE": "spoofed-sig", + "KALSHI-ACCESS-TIMESTAMP": "0", + }, + ) + req = route.calls[0].request + assert req.headers["KALSHI-ACCESS-KEY"] == "test-key-id" + assert req.headers["KALSHI-ACCESS-SIGNATURE"] != "spoofed-sig" + assert req.headers["KALSHI-ACCESS-TIMESTAMP"] != "0" + + @respx.mock + def test_cancel_order_threads_extra_headers(self, test_auth: KalshiAuth) -> None: + route = respx.delete(f"{MOCK_BASE}/portfolio/orders/o1").mock( + return_value=httpx.Response(204) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.orders.cancel("o1", extra_headers={"X-Trace": "t"}) + assert route.calls[0].request.headers["X-Trace"] == "t" + + +class TestMarkets: + @respx.mock + def test_get_market_threads_extra_headers(self, test_auth: KalshiAuth) -> None: + route = respx.get(f"{MOCK_BASE}/markets/BTC").mock( + return_value=httpx.Response(200, json={"market": _example_market_payload()}) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.markets.get("BTC", extra_headers={"X-Test": "v"}) + assert route.calls[0].request.headers["X-Test"] == "v" + + @respx.mock + def test_get_market_extra_headers_cannot_override_auth(self, test_auth: KalshiAuth) -> None: + route = respx.get(f"{MOCK_BASE}/markets/BTC").mock( + return_value=httpx.Response(200, json={"market": _example_market_payload()}) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.markets.get("BTC", extra_headers={"KALSHI-ACCESS-KEY": "spoofed"}) + assert route.calls[0].request.headers["KALSHI-ACCESS-KEY"] == "test-key-id" + + +class TestPortfolio: + @respx.mock + def test_get_balance_threads_extra_headers(self, test_auth: KalshiAuth) -> None: + route = respx.get(f"{MOCK_BASE}/portfolio/balance").mock( + return_value=httpx.Response( + 200, + json={ + "balance": 0, + "payout": 0, + "balance_dollars": "0.00", + "portfolio_value": 0, + "updated_ts": 0, + }, + ) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.portfolio.balance(extra_headers={"X-Test": "v"}) + assert route.calls[0].request.headers["X-Test"] == "v" + + @respx.mock + def test_balance_extra_headers_cannot_override_auth(self, test_auth: KalshiAuth) -> None: + route = respx.get(f"{MOCK_BASE}/portfolio/balance").mock( + return_value=httpx.Response( + 200, + json={ + "balance": 0, + "payout": 0, + "balance_dollars": "0.00", + "portfolio_value": 0, + "updated_ts": 0, + }, + ) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + client.portfolio.balance(extra_headers={"KALSHI-ACCESS-KEY": "spoofed"}) + assert route.calls[0].request.headers["KALSHI-ACCESS-KEY"] == "test-key-id" + + +# --------------------------------------------------------------------------- +# Parametric coverage — every public resource method has the kwarg. +# --------------------------------------------------------------------------- + + +def _collect_resource_methods() -> list[tuple[type, str, Any]]: + import kalshi.resources as resources_pkg + + out: list[tuple[type, str, Any]] = [] + for info in pkgutil.iter_modules(resources_pkg.__path__): + if info.name.startswith("_"): + continue + mod = __import__(f"kalshi.resources.{info.name}", fromlist=["*"]) + for attr in dir(mod): + cls = getattr(mod, attr) + if not isinstance(cls, type): + continue + if cls.__module__ != mod.__name__: + continue + if not issubclass(cls, (SyncResource, AsyncResource)): + continue + for name, fn in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith("_"): + continue + # Skip methods inherited from the base (none have public methods, + # but be defensive). + if name not in cls.__dict__: + continue + out.append((cls, name, fn)) + return out + + +_METHODS = _collect_resource_methods() + + +@pytest.mark.parametrize( + "cls,name,fn", + _METHODS, + ids=[f"{c.__name__}.{n}" for c, n, _ in _METHODS], +) +def test_every_public_method_accepts_extra_headers(cls: type, name: str, fn: Any) -> None: + sig = inspect.signature(fn) + assert "extra_headers" in sig.parameters, f"{cls.__name__}.{name} missing extra_headers kwarg" + param = sig.parameters["extra_headers"] + assert param.default is None, ( + f"{cls.__name__}.{name} extra_headers default must be None, got {param.default!r}" + ) + assert param.kind in ( + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) diff --git a/tests/test_rest_json_loader.py b/tests/test_rest_json_loader.py new file mode 100644 index 00000000..3ed56200 --- /dev/null +++ b/tests/test_rest_json_loader.py @@ -0,0 +1,93 @@ +"""Tests for #260: pluggable REST JSON loader (``KalshiConfig.rest_json_loads``). + +The loader, when set, receives ``response.content`` (bytes) and its return value +drives every ``_get`` / ``_post`` / ``_put`` / ``_delete`` call. ``None`` falls +back to ``httpx.Response.json()`` (stdlib). +""" + +from __future__ import annotations + +from typing import Any + +import httpx +import pytest +import respx + +from kalshi import AsyncKalshiClient, KalshiClient +from kalshi.auth import KalshiAuth +from kalshi.config import DEMO_WS_URL, KalshiConfig + +MOCK_BASE = "https://demo-api.kalshi.co/trade-api/v2" + + +def _config(**overrides: Any) -> KalshiConfig: + return KalshiConfig( + base_url=MOCK_BASE, + ws_base_url=DEMO_WS_URL, + timeout=5.0, + max_retries=0, + **overrides, + ) + + +class TestConfigField: + def test_default_is_none(self) -> None: + assert _config().rest_json_loads is None + + def test_accepts_callable(self) -> None: + cfg = _config(rest_json_loads=lambda b: {"loaded": True}) + assert callable(cfg.rest_json_loads) + + +class TestLoaderApplied: + @respx.mock + def test_sync_loader_used_on_get(self, test_auth: KalshiAuth) -> None: + seen: list[bytes] = [] + sentinel = {"__sentinel__": True, "exchange_active": False} + + def loader(content: bytes) -> Any: + seen.append(content) + return sentinel + + cfg = _config(rest_json_loads=loader) + respx.get(f"{MOCK_BASE}/exchange/status").mock( + return_value=httpx.Response(200, json={"exchange_active": True}) + ) + with KalshiClient(auth=test_auth, config=cfg) as client: + data = client.exchange._get("/exchange/status") + assert data is sentinel + assert len(seen) == 1 + assert isinstance(seen[0], bytes) + assert b"exchange_active" in seen[0] + + @respx.mock + def test_sync_loader_used_on_post(self, test_auth: KalshiAuth) -> None: + sentinel = {"__sentinel__": "post"} + cfg = _config(rest_json_loads=lambda b: sentinel) + respx.post(f"{MOCK_BASE}/anything").mock( + return_value=httpx.Response(200, json={"k": 1}) + ) + with KalshiClient(auth=test_auth, config=cfg) as client: + data = client.exchange._post("/anything", json={}) + assert data is sentinel + + @respx.mock + def test_default_none_uses_response_json(self, test_auth: KalshiAuth) -> None: + respx.get(f"{MOCK_BASE}/exchange/status").mock( + return_value=httpx.Response(200, json={"exchange_active": True}) + ) + with KalshiClient(auth=test_auth, config=_config()) as client: + data = client.exchange._get("/exchange/status") + assert data == {"exchange_active": True} + + @pytest.mark.asyncio + @respx.mock + async def test_async_loader_used_on_get(self, test_auth: KalshiAuth) -> None: + sentinel = {"__sentinel__": "async"} + cfg = _config(rest_json_loads=lambda b: sentinel) + respx.get(f"{MOCK_BASE}/exchange/status").mock( + return_value=httpx.Response(200, json={"exchange_active": True}) + ) + async with AsyncKalshiClient(auth=test_auth, config=cfg) as client: + data = await client.exchange._get("/exchange/status") + assert data is sentinel