diff --git a/docs/resources/communications.md b/docs/resources/communications.md index 80823e8..27a0680 100644 --- a/docs/resources/communications.md +++ b/docs/resources/communications.md @@ -6,24 +6,33 @@ answer. The requester accepts a side; the maker confirms; the trade settles. Auth required throughout. +!!! warning "Deprecated since v3.0.0" + The flat method names (`list_rfqs`, `get_rfq`, `create_rfq`, `delete_rfq`, + `list_all_rfqs`, `list_quotes`, `get_quote`, `create_quote`, + `delete_quote`, `list_all_quotes`, `accept_quote`, `confirm_quote`) on + `CommunicationsResource` still work but emit `DeprecationWarning` and will + be removed in a future release. Switch to the `rfqs.` / `quotes.` + sub-namespaces documented below. + ## Quick reference | Method | Endpoint | |---|---| | `get_id()` | `GET /communications/id` | -| `list_rfqs(...)` / `list_all_rfqs(...)` | `GET /communications/rfqs` | -| `get_rfq(rfq_id)` | `GET /communications/rfqs/{rfq_id}` | -| `create_rfq(...)` | `POST /communications/rfqs` | -| `delete_rfq(rfq_id)` | `DELETE /communications/rfqs/{rfq_id}` | -| `list_quotes(...)` / `list_all_quotes(...)` | `GET /communications/quotes` | -| `get_quote(quote_id)` | `GET /communications/quotes/{quote_id}` | -| `create_quote(...)` | `POST /communications/quotes` | -| `delete_quote(quote_id)` | `DELETE /communications/quotes/{quote_id}` | -| `accept_quote(quote_id, *, accepted_side)` | `POST /communications/quotes/{quote_id}/accept` | -| `confirm_quote(quote_id)` | `POST /communications/quotes/{quote_id}/confirm` | +| `rfqs.list(...)` / `rfqs.list_all(...)` | `GET /communications/rfqs` | +| `rfqs.get(rfq_id)` | `GET /communications/rfqs/{rfq_id}` | +| `rfqs.create(...)` | `POST /communications/rfqs` | +| `rfqs.delete(rfq_id)` | `DELETE /communications/rfqs/{rfq_id}` | +| `quotes.list(...)` / `quotes.list_all(...)` | `GET /communications/quotes` | +| `quotes.get(quote_id)` | `GET /communications/quotes/{quote_id}` | +| `quotes.create(...)` | `POST /communications/quotes` | +| `quotes.delete(quote_id)` | `DELETE /communications/quotes/{quote_id}` | +| `quotes.accept(quote_id, *, accepted_side)` | `POST /communications/quotes/{quote_id}/accept` | +| `quotes.confirm(quote_id)` | `POST /communications/quotes/{quote_id}/confirm` | `get_id()` returns your `participant_id` — the value you'll pass as -`quote_creator_user_id` / `rfq_creator_user_id` when filtering lists. +`quote_creator_user_id` / `rfq_creator_user_id` when filtering lists. It stays +at the top level because it has no sub-noun. ## Requester flow @@ -32,7 +41,7 @@ Auth required throughout. me = client.communications.get_id() # 1) Post an RFQ asking for a price on 500 contracts. -rfq = client.communications.create_rfq( +rfq = client.communications.rfqs.create( market_ticker="KXPRES-24-DJT", contracts=500, rest_remainder=True, @@ -40,12 +49,14 @@ rfq = client.communications.create_rfq( print(rfq.rfq.rfq_id) # 2) Poll for incoming quotes (or subscribe to the `communications` WS channel). -quotes = client.communications.list_quotes(rfq_creator_user_id=me.user_id) +quotes = client.communications.quotes.list(rfq_creator_user_id=me.user_id) for q in quotes: print(q.quote_id, q.yes_bid, q.no_bid) # 3) Accept a side on one of them. -accepted = client.communications.accept_quote(quotes.items[0].quote_id, accepted_side="yes") +accepted = client.communications.quotes.accept( + quotes.items[0].quote_id, accepted_side="yes" +) ``` ## Maker flow @@ -54,11 +65,11 @@ accepted = client.communications.accept_quote(quotes.items[0].quote_id, accepted me = client.communications.get_id() # 1) Watch for incoming RFQs. -for rfq in client.communications.list_all_rfqs(status="open"): +for rfq in client.communications.rfqs.list_all(status="open"): print(rfq.rfq_id, rfq.market_ticker, rfq.contracts) # 2) Quote one. -resp = client.communications.create_quote( +resp = client.communications.quotes.create( rfq_id="rfq_abc", yes_bid="0.60", no_bid="0.40", @@ -67,11 +78,11 @@ resp = client.communications.create_quote( # 3) Wait for the counterparty to accept. # 4) Confirm to lock the fill. -client.communications.confirm_quote(resp.quote.quote_id) +client.communications.quotes.confirm(resp.quote.quote_id) ``` -!!! warning "`list_quotes` requires a user-id filter" - `list_quotes` and `list_all_quotes` **must** be called with at least one of: +!!! warning "`quotes.list` requires a user-id filter" + `quotes.list` and `quotes.list_all` **must** be called with at least one of: - `quote_creator_user_id=` (filter to a specific quoter) - `rfq_creator_user_id=` (filter to a specific RFQ originator) @@ -87,15 +98,15 @@ client.communications.confirm_quote(resp.quote.quote_id) ```python # All quotes you made — no get_id() needed. -for q in client.communications.list_all_quotes(user_filter="self"): +for q in client.communications.quotes.list_all(user_filter="self"): print(q.quote_id, q.yes_bid) # All quotes against RFQs you originated. -for q in client.communications.list_all_quotes(rfq_user_filter="self"): +for q in client.communications.quotes.list_all(rfq_user_filter="self"): ... # Same shortcut on RFQs: -for rfq in client.communications.list_all_rfqs(user_filter="self"): +for rfq in client.communications.rfqs.list_all(user_filter="self"): ... ``` @@ -105,11 +116,11 @@ upgrade. ## Post-only quotes -`create_quote()` accepts `post_only=True` (added in v2.1.0) to ensure your +`quotes.create()` accepts `post_only=True` (added in v2.1.0) to ensure your resting order is canceled rather than crossed if it would take liquidity: ```python -client.communications.create_quote( +client.communications.quotes.create( rfq_id="rfq_abc", yes_bid="0.60", no_bid="0.40", rest_remainder=True, @@ -118,7 +129,7 @@ client.communications.create_quote( ``` !!! info "Delete is not server-idempotent" - `delete_rfq(rfq_id)` and `delete_quote(quote_id)` propagate a 404 as + `rfqs.delete(rfq_id)` and `quotes.delete(quote_id)` propagate a 404 as `KalshiNotFoundError` when the RFQ or quote is already canceled, expired, or never existed. The SDK does **not** swallow it — the caller owns safe-retry idempotency: @@ -127,7 +138,7 @@ client.communications.create_quote( from kalshi.errors import KalshiNotFoundError try: - client.communications.delete_rfq(rfq_id) + client.communications.rfqs.delete(rfq_id) except KalshiNotFoundError: pass # already canceled — idempotent ``` @@ -137,12 +148,47 @@ client.communications.create_quote( `open`, `accepted`, `confirmed`, `canceled`. Status filtering accepts these literal strings. +## Migrating from v2.x + +| v2.x (deprecated) | v3.0.0 (canonical) | +|---|---| +| `client.communications.list_rfqs(...)` | `client.communications.rfqs.list(...)` | +| `client.communications.list_all_rfqs(...)` | `client.communications.rfqs.list_all(...)` | +| `client.communications.get_rfq(rfq_id)` | `client.communications.rfqs.get(rfq_id)` | +| `client.communications.create_rfq(...)` | `client.communications.rfqs.create(...)` | +| `client.communications.delete_rfq(rfq_id)` | `client.communications.rfqs.delete(rfq_id)` | +| `client.communications.list_quotes(...)` | `client.communications.quotes.list(...)` | +| `client.communications.list_all_quotes(...)` | `client.communications.quotes.list_all(...)` | +| `client.communications.get_quote(quote_id)` | `client.communications.quotes.get(quote_id)` | +| `client.communications.create_quote(...)` | `client.communications.quotes.create(...)` | +| `client.communications.delete_quote(quote_id)` | `client.communications.quotes.delete(quote_id)` | +| `client.communications.accept_quote(quote_id, accepted_side=...)` | `client.communications.quotes.accept(quote_id, accepted_side=...)` | +| `client.communications.confirm_quote(quote_id)` | `client.communications.quotes.confirm(quote_id)` | + +`get_id()` is unchanged. + ## Reference ::: kalshi.resources.communications.CommunicationsResource options: heading_level: 3 +::: kalshi.resources.communications.RFQsResource + options: + heading_level: 3 + +::: kalshi.resources.communications.QuotesResource + options: + heading_level: 3 + ::: kalshi.resources.communications.AsyncCommunicationsResource options: heading_level: 3 + +::: kalshi.resources.communications.AsyncRFQsResource + options: + heading_level: 3 + +::: kalshi.resources.communications.AsyncQuotesResource + options: + heading_level: 3 diff --git a/docs/resources/markets.md b/docs/resources/markets.md index b316d1f..1bff248 100644 --- a/docs/resources/markets.md +++ b/docs/resources/markets.md @@ -16,7 +16,7 @@ recent trades. | `bulk_candlesticks(*, market_tickers, ...)` | `GET /markets/candlesticks` | no | | `bulk_orderbooks(*, tickers)` | `GET /markets/orderbooks` | **yes** | | `list_trades(...)` | `GET /markets/trades` | no | -| `list_trades_all(...)` | walks `list_trades` | no | +| `list_all_trades(...)` | walks `list_trades` | no | !!! warning "Orderbook endpoints require auth" `orderbook()` and `bulk_orderbooks()` raise `AuthRequiredError` on an @@ -130,6 +130,13 @@ for trade in page: print(trade.trade_id, trade.taker_side, trade.yes_price, trade.count) ``` +!!! warning "Deprecated since v3.0.0" + `list_trades_all` is the legacy name; it still works but emits + `DeprecationWarning` and will be removed in a future release. Use + `list_all_trades` instead — see [#349][issue-349]. + + [issue-349]: https://github.com/TexasCoding/kalshi-python-sdk/issues/349 + ## Reference ::: kalshi.resources.markets.MarketsResource diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 8415a09..ad42e43 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -169,6 +169,12 @@ WeeklySchedule, Withdrawal, ) +from kalshi.resources.communications import ( + AsyncQuotesResource, + AsyncRFQsResource, + QuotesResource, + RFQsResource, +) from kalshi.types import NullableList, StrictInt __all__ = [ @@ -186,6 +192,8 @@ "ApplySubaccountTransferRequest", "AssociatedEvent", "AsyncKalshiClient", + "AsyncQuotesResource", + "AsyncRFQsResource", "AuthRequiredError", "Balance", "BatchCancelOrdersRequest", @@ -312,6 +320,8 @@ "PriceDistribution", "Quote", "QuoteStatusLiteral", + "QuotesResource", + "RFQsResource", "RateLimit", "RfqStatusLiteral", "Schedule", diff --git a/kalshi/resources/communications.py b/kalshi/resources/communications.py index 95f1f98..2794910 100644 --- a/kalshi/resources/communications.py +++ b/kalshi/resources/communications.py @@ -1,4 +1,14 @@ -"""Communications / RFQ resource — request-for-quote + quote API.""" +"""Communications / RFQ resource — request-for-quote + quote API. + +v3.0.0 reorganized the surface into sub-namespaces matching the OpenAPI tag +structure: ``client.communications.rfqs`` and ``client.communications.quotes``. +The flat ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / ``delete_rfq`` / +``list_all_rfqs`` / ``list_quotes`` / ``get_quote`` / ``create_quote`` / +``delete_quote`` / ``list_all_quotes`` / ``accept_quote`` / ``confirm_quote`` +methods remain as ``@deprecated`` forwarders for one release; they emit +``DeprecationWarning`` on call and will be removed in a future release. +``get_id`` is a misc top-level endpoint and stays on the parent class. +""" from __future__ import annotations @@ -6,6 +16,9 @@ from decimal import Decimal from typing import Any, Literal, overload +from typing_extensions import deprecated + +from kalshi._base_client import AsyncTransport, SyncTransport from kalshi.models.common import Page from kalshi.models.communications import ( RFQ, @@ -204,15 +217,30 @@ def _build_accept_quote_body( return request.model_dump(exclude_none=True, by_alias=True, mode="json") -class CommunicationsResource(SyncResource): - """Sync communications / RFQ API.""" +# v3.0.0 deprecation messages shown by ``typing_extensions.deprecated`` at the +# call site. Templated so each forwarder names its replacement explicitly. +def _rfq_dep(new: str, old: str) -> str: + return ( + f"`CommunicationsResource.{old}` is deprecated since v3.0.0 and will " + f"be removed in a future release; use `client.communications.rfqs.{new}` instead." + ) - def get_id(self, *, extra_headers: dict[str, str] | None = None) -> GetCommunicationsIDResponse: - self._require_auth() - data = self._get("/communications/id", extra_headers=extra_headers) - return GetCommunicationsIDResponse.model_validate(data) - def list_rfqs( +def _quote_dep(new: str, old: str) -> str: + return ( + f"`CommunicationsResource.{old}` is deprecated since v3.0.0 and will " + f"be removed in a future release; use `client.communications.quotes.{new}` instead." + ) + + +class RFQsResource(SyncResource): + """Sync RFQ sub-resource — ``client.communications.rfqs``. + + Reached via :attr:`CommunicationsResource.rfqs`. Backs + ``/communications/rfqs`` endpoints. + """ + + def list( self, *, cursor: str | None = None, @@ -240,7 +268,7 @@ def list_rfqs( "/communications/rfqs", RFQ, "rfqs", params=params, extra_headers=extra_headers ) - def list_all_rfqs( + def list_all( self, *, limit: int | None = None, @@ -274,7 +302,7 @@ def list_all_rfqs( extra_headers=extra_headers, ) - def get_rfq( + def get( self, rfq_id: str, *, extra_headers: dict[str, str] | None = None ) -> GetRFQResponse: self._require_auth() @@ -284,11 +312,11 @@ def get_rfq( return GetRFQResponse.model_validate(data) @overload - def create_rfq( + def create( self, *, request: CreateRFQRequest, extra_headers: dict[str, str] | None = None ) -> CreateRFQResponse: ... @overload - def create_rfq( + def create( self, *, market_ticker: str, @@ -300,7 +328,7 @@ def create_rfq( subaccount: int | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... - def create_rfq( + def create( self, *, request: CreateRFQRequest | None = None, @@ -327,13 +355,21 @@ def create_rfq( data = self._post("/communications/rfqs", json=body, extra_headers=extra_headers) return CreateRFQResponse.model_validate(data) - def delete_rfq(self, rfq_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + def delete(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')}", extra_headers=extra_headers ) - def list_quotes( + +class QuotesResource(SyncResource): + """Sync Quote sub-resource — ``client.communications.quotes``. + + Reached via :attr:`CommunicationsResource.quotes`. Backs + ``/communications/quotes`` endpoints. + """ + + def list( self, *, cursor: str | None = None, @@ -373,7 +409,7 @@ def list_quotes( "/communications/quotes", Quote, "quotes", params=params, extra_headers=extra_headers ) - def list_all_quotes( + def list_all( self, *, limit: int | None = None, @@ -419,7 +455,7 @@ def list_all_quotes( extra_headers=extra_headers, ) - def get_quote( + def get( self, quote_id: str, *, extra_headers: dict[str, str] | None = None ) -> GetQuoteResponse: self._require_auth() @@ -429,11 +465,11 @@ def get_quote( return GetQuoteResponse.model_validate(data) @overload - def create_quote( + def create( self, *, request: CreateQuoteRequest, extra_headers: dict[str, str] | None = None ) -> CreateQuoteResponse: ... @overload - def create_quote( + def create( self, *, rfq_id: str, @@ -444,7 +480,7 @@ def create_quote( post_only: bool | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: ... - def create_quote( + def create( self, *, request: CreateQuoteRequest | None = None, @@ -469,14 +505,14 @@ def create_quote( data = self._post("/communications/quotes", json=body, extra_headers=extra_headers) return CreateQuoteResponse.model_validate(data) - def delete_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + def delete(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')}", extra_headers=extra_headers ) @overload - def accept_quote( + def accept( self, quote_id: str, *, @@ -484,14 +520,14 @@ def accept_quote( extra_headers: dict[str, str] | None = None, ) -> None: ... @overload - def accept_quote( + def accept( self, quote_id: str, *, accepted_side: Literal["yes", "no"], extra_headers: dict[str, str] | None = None, ) -> None: ... - def accept_quote( + def accept( self, quote_id: str, *, @@ -507,7 +543,7 @@ def accept_quote( extra_headers=extra_headers, ) - def confirm_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + def confirm(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( @@ -517,17 +553,260 @@ def confirm_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = ) -class AsyncCommunicationsResource(AsyncResource): - """Async communications / RFQ API.""" +class CommunicationsResource(SyncResource): + """Sync communications / RFQ API. - async def get_id( - self, *, extra_headers: dict[str, str] | None = None - ) -> GetCommunicationsIDResponse: + Sub-namespaces: + + - :attr:`rfqs` — :class:`RFQsResource` — ``/communications/rfqs`` surface. + - :attr:`quotes` — :class:`QuotesResource` — ``/communications/quotes`` surface. + + The misc :meth:`get_id` endpoint stays at the top level because it has no + sub-noun. The flat ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / + ``delete_rfq`` / ``list_all_rfqs`` / ``list_quotes`` / ``get_quote`` / + ``create_quote`` / ``delete_quote`` / ``list_all_quotes`` / ``accept_quote`` + / ``confirm_quote`` methods are :class:`typing_extensions.deprecated` + forwarders kept for one release. + """ + + rfqs: RFQsResource + quotes: QuotesResource + + def __init__(self, transport: SyncTransport) -> None: + super().__init__(transport) + self.rfqs = RFQsResource(transport) + self.quotes = QuotesResource(transport) + + def get_id(self, *, extra_headers: dict[str, str] | None = None) -> GetCommunicationsIDResponse: self._require_auth() - data = await self._get("/communications/id", extra_headers=extra_headers) + data = self._get("/communications/id", extra_headers=extra_headers) return GetCommunicationsIDResponse.model_validate(data) - async def list_rfqs( + # ── Deprecated flat forwarders (v3.0.0) ──────────────────────────────── + + @deprecated(_rfq_dep("list", "list_rfqs")) + def list_rfqs( + self, + *, + cursor: str | None = None, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + subaccount: int | None = None, + status: RfqStatusLiteral | None = None, + creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Page[RFQ]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.list` instead.""" + return self.rfqs.list( + 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, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("list_all", "list_all_rfqs")) + def list_all_rfqs( + self, + *, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + subaccount: int | None = None, + status: RfqStatusLiteral | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.list_all` instead.""" + return self.rfqs.list_all( + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, + user_filter=user_filter, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("get", "get_rfq")) + def get_rfq( + self, rfq_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.get` instead.""" + return self.rfqs.get(rfq_id, extra_headers=extra_headers) + + @deprecated(_rfq_dep("create", "create_rfq")) + def create_rfq( + self, + *, + request: CreateRFQRequest | None = None, + market_ticker: str | None = None, + rest_remainder: bool | None = None, + contracts: int | None = None, + target_cost: Decimal | str | float | int | None = None, + replace_existing: bool | None = None, + subtrader_id: str | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.create` instead.""" + return self.rfqs.create( # type: ignore[call-overload, no-any-return, misc] # `misc`: mypy's "too many union combinations" overload limit + request=request, + market_ticker=market_ticker, + rest_remainder=rest_remainder, + contracts=contracts, + target_cost=target_cost, + replace_existing=replace_existing, + subtrader_id=subtrader_id, + subaccount=subaccount, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("delete", "delete_rfq")) + def delete_rfq(self, rfq_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.delete` instead.""" + self.rfqs.delete(rfq_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("list", "list_quotes")) + def list_quotes( + self, + *, + cursor: str | None = None, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + status: QuoteStatusLiteral | None = None, + quote_creator_user_id: str | None = None, + rfq_creator_user_id: str | None = None, + rfq_creator_subtrader_id: str | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.list` instead.""" + return self.quotes.list( + 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, + rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("list_all", "list_all_quotes")) + def list_all_quotes( + self, + *, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + status: QuoteStatusLiteral | None = None, + quote_creator_user_id: str | None = None, + rfq_creator_user_id: str | None = None, + rfq_creator_subtrader_id: str | None = None, + rfq_id: str | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.list_all` instead.""" + return self.quotes.list_all( + 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, + rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("get", "get_quote")) + def get_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetQuoteResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.get` instead.""" + return self.quotes.get(quote_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("create", "create_quote")) + def create_quote( + self, + *, + request: CreateQuoteRequest | None = None, + rfq_id: str | None = None, + yes_bid: Decimal | str | float | int | None = None, + no_bid: Decimal | str | float | int | None = None, + rest_remainder: bool | None = None, + subaccount: int | None = None, + post_only: bool | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateQuoteResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.create` instead.""" + return self.quotes.create( # type: ignore[call-overload, no-any-return, misc] # `misc`: mypy's "too many union combinations" overload limit + request=request, + rfq_id=rfq_id, + yes_bid=yes_bid, + no_bid=no_bid, + rest_remainder=rest_remainder, + subaccount=subaccount, + post_only=post_only, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("delete", "delete_quote")) + def delete_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.delete` instead.""" + self.quotes.delete(quote_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("accept", "accept_quote")) + def accept_quote( + self, + quote_id: str, + *, + request: AcceptQuoteRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.accept` instead.""" + self.quotes.accept( # type: ignore[call-overload] + quote_id, + request=request, + accepted_side=accepted_side, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("confirm", "confirm_quote")) + def confirm_quote(self, quote_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.confirm` instead.""" + self.quotes.confirm(quote_id, extra_headers=extra_headers) + + +class AsyncRFQsResource(AsyncResource): + """Async RFQ sub-resource — ``client.communications.rfqs``.""" + + async def list( self, *, cursor: str | None = None, @@ -555,7 +834,7 @@ async def list_rfqs( "/communications/rfqs", RFQ, "rfqs", params=params, extra_headers=extra_headers ) - def list_all_rfqs( + def list_all( self, *, limit: int | None = None, @@ -569,7 +848,7 @@ def list_all_rfqs( extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[RFQ]: """Returns an async iterator — use ``async for``.""" - # Plain `def` so _require_auth + _validate_max_pages run at call time. + # Plain ``def`` so _require_auth + _validate_max_pages run at call time. self._require_auth() _validate_max_pages(max_pages) params = _list_rfqs_params( @@ -591,7 +870,7 @@ def list_all_rfqs( extra_headers=extra_headers, ) - async def get_rfq( + async def get( self, rfq_id: str, *, extra_headers: dict[str, str] | None = None ) -> GetRFQResponse: self._require_auth() @@ -601,11 +880,11 @@ async def get_rfq( return GetRFQResponse.model_validate(data) @overload - async def create_rfq( + async def create( self, *, request: CreateRFQRequest, extra_headers: dict[str, str] | None = None ) -> CreateRFQResponse: ... @overload - async def create_rfq( + async def create( self, *, market_ticker: str, @@ -617,7 +896,7 @@ async def create_rfq( subaccount: int | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateRFQResponse: ... - async def create_rfq( + async def create( self, *, request: CreateRFQRequest | None = None, @@ -644,13 +923,17 @@ async def create_rfq( 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, *, extra_headers: dict[str, str] | None = None) -> None: + async def delete(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')}", extra_headers=extra_headers ) - async def list_quotes( + +class AsyncQuotesResource(AsyncResource): + """Async Quote sub-resource — ``client.communications.quotes``.""" + + async def list( self, *, cursor: str | None = None, @@ -690,7 +973,7 @@ async def list_quotes( "/communications/quotes", Quote, "quotes", params=params, extra_headers=extra_headers ) - def list_all_quotes( + def list_all( self, *, limit: int | None = None, @@ -737,7 +1020,7 @@ def list_all_quotes( extra_headers=extra_headers, ) - async def get_quote( + async def get( self, quote_id: str, *, extra_headers: dict[str, str] | None = None ) -> GetQuoteResponse: self._require_auth() @@ -747,11 +1030,11 @@ async def get_quote( return GetQuoteResponse.model_validate(data) @overload - async def create_quote( + async def create( self, *, request: CreateQuoteRequest, extra_headers: dict[str, str] | None = None ) -> CreateQuoteResponse: ... @overload - async def create_quote( + async def create( self, *, rfq_id: str, @@ -762,7 +1045,7 @@ async def create_quote( post_only: bool | None = ..., extra_headers: dict[str, str] | None = None, ) -> CreateQuoteResponse: ... - async def create_quote( + async def create( self, *, request: CreateQuoteRequest | None = None, @@ -787,7 +1070,7 @@ async def create_quote( data = await self._post("/communications/quotes", json=body, extra_headers=extra_headers) return CreateQuoteResponse.model_validate(data) - async def delete_quote( + async def delete( self, quote_id: str, *, extra_headers: dict[str, str] | None = None ) -> None: self._require_auth() @@ -796,7 +1079,7 @@ async def delete_quote( ) @overload - async def accept_quote( + async def accept( self, quote_id: str, *, @@ -804,14 +1087,14 @@ async def accept_quote( extra_headers: dict[str, str] | None = None, ) -> None: ... @overload - async def accept_quote( + async def accept( self, quote_id: str, *, accepted_side: Literal["yes", "no"], extra_headers: dict[str, str] | None = None, ) -> None: ... - async def accept_quote( + async def accept( self, quote_id: str, *, @@ -827,7 +1110,7 @@ async def accept_quote( extra_headers=extra_headers, ) - async def confirm_quote( + async def confirm( self, quote_id: str, *, extra_headers: dict[str, str] | None = None ) -> None: self._require_auth() @@ -837,3 +1120,260 @@ async def confirm_quote( json={}, extra_headers=extra_headers, ) + + +class AsyncCommunicationsResource(AsyncResource): + """Async communications / RFQ API. + + Sub-namespaces: + + - :attr:`rfqs` — :class:`AsyncRFQsResource` — ``/communications/rfqs``. + - :attr:`quotes` — :class:`AsyncQuotesResource` — ``/communications/quotes``. + + The flat ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / ``delete_rfq`` / + ``list_all_rfqs`` / ``list_quotes`` / ``get_quote`` / ``create_quote`` / + ``delete_quote`` / ``list_all_quotes`` / ``accept_quote`` / ``confirm_quote`` + coroutines are :class:`typing_extensions.deprecated` forwarders kept for + one release. + """ + + rfqs: AsyncRFQsResource + quotes: AsyncQuotesResource + + def __init__(self, transport: AsyncTransport) -> None: + super().__init__(transport) + self.rfqs = AsyncRFQsResource(transport) + self.quotes = AsyncQuotesResource(transport) + + async def get_id( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetCommunicationsIDResponse: + self._require_auth() + data = await self._get("/communications/id", extra_headers=extra_headers) + return GetCommunicationsIDResponse.model_validate(data) + + # ── Deprecated flat forwarders (v3.0.0) ──────────────────────────────── + + @deprecated(_rfq_dep("list", "list_rfqs")) + async def list_rfqs( + self, + *, + cursor: str | None = None, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + subaccount: int | None = None, + status: RfqStatusLiteral | None = None, + creator_user_id: str | None = None, + user_filter: UserFilterLiteral | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Page[RFQ]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.list` instead.""" + return await self.rfqs.list( + 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, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("list_all", "list_all_rfqs")) + def list_all_rfqs( + self, + *, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + subaccount: int | None = None, + status: RfqStatusLiteral | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.list_all` instead.""" + # Plain ``def`` so deprecation + validation fire at call time, before + # the user awaits/iterates. Mirrors the sub-resource shape. + return self.rfqs.list_all( + limit=limit, + event_ticker=event_ticker, + market_ticker=market_ticker, + subaccount=subaccount, + status=status, + creator_user_id=creator_user_id, + user_filter=user_filter, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("get", "get_rfq")) + async def get_rfq( + self, rfq_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetRFQResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.get` instead.""" + return await self.rfqs.get(rfq_id, extra_headers=extra_headers) + + @deprecated(_rfq_dep("create", "create_rfq")) + async def create_rfq( + self, + *, + request: CreateRFQRequest | None = None, + market_ticker: str | None = None, + rest_remainder: bool | None = None, + contracts: int | None = None, + target_cost: Decimal | str | float | int | None = None, + replace_existing: bool | None = None, + subtrader_id: str | None = None, + subaccount: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateRFQResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.create` instead.""" + return await self.rfqs.create( # type: ignore[call-overload, no-any-return, misc] # `misc`: mypy's "too many union combinations" overload limit + request=request, + market_ticker=market_ticker, + rest_remainder=rest_remainder, + contracts=contracts, + target_cost=target_cost, + replace_existing=replace_existing, + subtrader_id=subtrader_id, + subaccount=subaccount, + extra_headers=extra_headers, + ) + + @deprecated(_rfq_dep("delete", "delete_rfq")) + async def delete_rfq(self, rfq_id: str, *, extra_headers: dict[str, str] | None = None) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.rfqs.delete` instead.""" + await self.rfqs.delete(rfq_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("list", "list_quotes")) + async def list_quotes( + self, + *, + cursor: str | None = None, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + status: QuoteStatusLiteral | None = None, + quote_creator_user_id: str | None = None, + rfq_creator_user_id: str | None = None, + rfq_creator_subtrader_id: str | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.list` instead.""" + return await self.quotes.list( + 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, + rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("list_all", "list_all_quotes")) + def list_all_quotes( + self, + *, + limit: int | None = None, + event_ticker: str | None = None, + market_ticker: str | None = None, + status: QuoteStatusLiteral | None = None, + quote_creator_user_id: str | None = None, + rfq_creator_user_id: str | None = None, + rfq_creator_subtrader_id: str | None = None, + rfq_id: str | None = None, + 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]: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.list_all` instead.""" + return self.quotes.list_all( + 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, + rfq_id=rfq_id, + user_filter=user_filter, + rfq_user_filter=rfq_user_filter, + max_pages=max_pages, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("get", "get_quote")) + async def get_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> GetQuoteResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.get` instead.""" + return await self.quotes.get(quote_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("create", "create_quote")) + async def create_quote( + self, + *, + request: CreateQuoteRequest | None = None, + rfq_id: str | None = None, + yes_bid: Decimal | str | float | int | None = None, + no_bid: Decimal | str | float | int | None = None, + rest_remainder: bool | None = None, + subaccount: int | None = None, + post_only: bool | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateQuoteResponse: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.create` instead.""" + return await self.quotes.create( # type: ignore[call-overload, no-any-return, misc] # `misc`: mypy's "too many union combinations" overload limit + request=request, + rfq_id=rfq_id, + yes_bid=yes_bid, + no_bid=no_bid, + rest_remainder=rest_remainder, + subaccount=subaccount, + post_only=post_only, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("delete", "delete_quote")) + async def delete_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.delete` instead.""" + await self.quotes.delete(quote_id, extra_headers=extra_headers) + + @deprecated(_quote_dep("accept", "accept_quote")) + async def accept_quote( + self, + quote_id: str, + *, + request: AcceptQuoteRequest | None = None, + accepted_side: Literal["yes", "no"] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.accept` instead.""" + await self.quotes.accept( # type: ignore[call-overload] + quote_id, + request=request, + accepted_side=accepted_side, + extra_headers=extra_headers, + ) + + @deprecated(_quote_dep("confirm", "confirm_quote")) + async def confirm_quote( + self, quote_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """.. deprecated:: 3.0.0 Use :meth:`client.communications.quotes.confirm` instead.""" + await self.quotes.confirm(quote_id, extra_headers=extra_headers) diff --git a/kalshi/resources/markets.py b/kalshi/resources/markets.py index 6608bc8..b494faa 100644 --- a/kalshi/resources/markets.py +++ b/kalshi/resources/markets.py @@ -6,6 +6,8 @@ from collections.abc import AsyncIterator, Iterator from typing import Any +from typing_extensions import deprecated + from kalshi.errors import KalshiError from kalshi.models.common import Page from kalshi.models.historical import Trade @@ -357,7 +359,7 @@ def list_trades( "/markets/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - def list_trades_all( + def list_all_trades( self, *, ticker: str | None = None, @@ -384,6 +386,30 @@ def list_trades_all( extra_headers=extra_headers, ) + @deprecated( + "`MarketsResource.list_trades_all` is deprecated since v3.0.0 and will be " + "removed in a future release; use `list_all_trades` instead." + ) + def list_trades_all( + self, + *, + ticker: str | None = None, + min_ts: int | None = None, + max_ts: int | None = None, + limit: int | None = None, + max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> Iterator[Trade]: + """.. deprecated:: 3.0.0 Use :meth:`list_all_trades` instead.""" + return self.list_all_trades( + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + max_pages=max_pages, + extra_headers=extra_headers, + ) + def bulk_candlesticks( self, *, @@ -580,7 +606,7 @@ async def list_trades( "/markets/trades", Trade, "trades", params=params, extra_headers=extra_headers ) - def list_trades_all( + def list_all_trades( self, *, ticker: str | None = None, @@ -608,6 +634,30 @@ def list_trades_all( extra_headers=extra_headers, ) + @deprecated( + "`AsyncMarketsResource.list_trades_all` is deprecated since v3.0.0 and " + "will be removed in a future release; use `list_all_trades` instead." + ) + def list_trades_all( + self, + *, + ticker: str | None = None, + min_ts: int | None = None, + max_ts: int | None = None, + limit: int | None = None, + max_pages: int | None = None, + extra_headers: dict[str, str] | None = None, + ) -> AsyncIterator[Trade]: + """.. deprecated:: 3.0.0 Use :meth:`list_all_trades` instead.""" + return self.list_all_trades( + ticker=ticker, + min_ts=min_ts, + max_ts=max_ts, + limit=limit, + max_pages=max_pages, + extra_headers=extra_headers, + ) + async def bulk_candlesticks( self, *, diff --git a/tests/_contract_support.py b/tests/_contract_support.py index 5e57e67..e6e0645 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -134,6 +134,13 @@ class Exclusion: http_method="GET", path_template="/markets/trades", ), + # v3.0.0 standardization on ``list_all_`` (#349); old + # ``list_trades_all`` is a deprecated forwarder. + MethodEndpointEntry( + sdk_method="kalshi.resources.markets.MarketsResource.list_all_trades", + http_method="GET", + path_template="/markets/trades", + ), MethodEndpointEntry( sdk_method="kalshi.resources.markets.MarketsResource.bulk_candlesticks", http_method="GET", @@ -537,6 +544,72 @@ class Exclusion: http_method="PUT", path_template="/communications/quotes/{quote_id}/confirm", ), + # v3.0.0 sub-namespaces (#348) — same endpoints, new shape. + # ``CommunicationsResource.{rfqs,quotes}.`` is canonical; the flat + # forwarders above are kept as deprecated aliases for one release. + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQsResource.list", + http_method="GET", + path_template="/communications/rfqs", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQsResource.list_all", + http_method="GET", + path_template="/communications/rfqs", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQsResource.create", + http_method="POST", + path_template="/communications/rfqs", + request_body_schema="#/components/schemas/CreateRFQRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQsResource.get", + http_method="GET", + path_template="/communications/rfqs/{rfq_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.RFQsResource.delete", + http_method="DELETE", + path_template="/communications/rfqs/{rfq_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.list", + http_method="GET", + path_template="/communications/quotes", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.list_all", + http_method="GET", + path_template="/communications/quotes", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.create", + http_method="POST", + path_template="/communications/quotes", + request_body_schema="#/components/schemas/CreateQuoteRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.get", + http_method="GET", + path_template="/communications/quotes/{quote_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.delete", + http_method="DELETE", + path_template="/communications/quotes/{quote_id}", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.accept", + http_method="PUT", + path_template="/communications/quotes/{quote_id}/accept", + request_body_schema="#/components/schemas/AcceptQuoteRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.resources.communications.QuotesResource.confirm", + http_method="PUT", + path_template="/communications/quotes/{quote_id}/confirm", + ), # ── subaccounts ───────────────────────────────────────────────────────── MethodEndpointEntry( sdk_method="kalshi.resources.subaccounts.SubaccountsResource.create", @@ -989,6 +1062,18 @@ class Exclusion: reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", ), + ("kalshi.resources.communications.RFQsResource.list_all", "cursor"): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), + ("kalshi.resources.communications.QuotesResource.list_all", "cursor"): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), + ("kalshi.resources.markets.MarketsResource.list_all_trades", "cursor"): Exclusion( + reason="paginator-handled; not a caller-facing kwarg on list_all", + kind="paginator_handled", + ), ("kalshi.resources.milestones.MilestonesResource.list_all", "cursor"): Exclusion( reason="paginator-handled; not a caller-facing kwarg on list_all", kind="paginator_handled", @@ -1179,6 +1264,9 @@ class Exclusion: _MAX_PAGES_FQNS: tuple[str, ...] = ( "kalshi.resources.markets.MarketsResource.list_all", "kalshi.resources.markets.MarketsResource.list_trades_all", + "kalshi.resources.markets.MarketsResource.list_all_trades", + "kalshi.resources.communications.RFQsResource.list_all", + "kalshi.resources.communications.QuotesResource.list_all", "kalshi.resources.milestones.MilestonesResource.list_all", "kalshi.resources.events.EventsResource.list_all", "kalshi.resources.events.EventsResource.list_all_multivariate", @@ -1203,6 +1291,9 @@ class Exclusion: # Async counterparts — same kwarg, same client-only semantics. "kalshi.resources.markets.AsyncMarketsResource.list_all", "kalshi.resources.markets.AsyncMarketsResource.list_trades_all", + "kalshi.resources.markets.AsyncMarketsResource.list_all_trades", + "kalshi.resources.communications.AsyncRFQsResource.list_all", + "kalshi.resources.communications.AsyncQuotesResource.list_all", "kalshi.resources.milestones.AsyncMilestonesResource.list_all", "kalshi.resources.events.AsyncEventsResource.list_all", "kalshi.resources.events.AsyncEventsResource.list_all_multivariate", diff --git a/tests/test_async_markets.py b/tests/test_async_markets.py index e6adf88..99ff5a2 100644 --- a/tests/test_async_markets.py +++ b/tests/test_async_markets.py @@ -13,7 +13,7 @@ from kalshi.config import KalshiConfig from kalshi.errors import AuthRequiredError, KalshiNotFoundError from kalshi.resources.markets import AsyncMarketsResource -from tests._model_fixtures import market_dict +from tests._model_fixtures import market_dict, trade_dict @pytest.fixture @@ -424,3 +424,63 @@ async def test_trailing_commas_do_not_inflate_count( end_ts=2, period_interval=60, ) + + +class TestAsyncMarketsListAllTradesRename: + """v3.0.0 standardization of ``list__all`` → ``list_all_`` (#349).""" + + @respx.mock + @pytest.mark.asyncio + async def test_issue_349_list_all_trades_works( + self, markets: AsyncMarketsResource, + ) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/markets/trades").mock( + return_value=httpx.Response( + 200, + json={ + "trades": [ + trade_dict( + trade_id="t-1", + ticker="MKT-A", + count_fp="1.00", + yes_price_dollars="0.50", + no_price_dollars="0.50", + taker_side="yes", + created_time="2026-04-18T12:00:00Z", + ), + ], + "cursor": "", + }, + ), + ) + items = [t async for t in markets.list_all_trades(limit=1)] + assert [t.trade_id for t in items] == ["t-1"] + + @respx.mock + @pytest.mark.asyncio + async def test_issue_349_list_trades_all_emits_deprecation_warning( + self, markets: AsyncMarketsResource, + ) -> None: + respx.get("https://test.kalshi.com/trade-api/v2/markets/trades").mock( + return_value=httpx.Response( + 200, + json={ + "trades": [ + trade_dict( + trade_id="t-1", + ticker="MKT-A", + count_fp="1.00", + yes_price_dollars="0.50", + no_price_dollars="0.50", + taker_side="yes", + created_time="2026-04-18T12:00:00Z", + ), + ], + "cursor": "", + }, + ), + ) + with pytest.warns(DeprecationWarning, match=r"list_all_trades"): + iterator = markets.list_trades_all(limit=1) + items = [t async for t in iterator] + assert [t.trade_id for t in items] == ["t-1"] diff --git a/tests/test_communications.py b/tests/test_communications.py index bcc7dc6..c9dd6df 100644 --- a/tests/test_communications.py +++ b/tests/test_communications.py @@ -34,7 +34,11 @@ ) from kalshi.resources.communications import ( AsyncCommunicationsResource, + AsyncQuotesResource, + AsyncRFQsResource, CommunicationsResource, + QuotesResource, + RFQsResource, ) @@ -944,3 +948,254 @@ async def test_issue_324_valid_quote_status_flows_through_to_query_async( ).mock(return_value=httpx.Response(200, json={"quotes": []})) await async_comms.list_quotes(status="executed", quote_creator_user_id="u1") assert route.calls[0].request.url.params["status"] == "executed" + + +class TestV3DeprecationAliases: + """v3.0.0 sub-namespace migration (#348). + + ``client.communications.{rfqs,quotes}.`` is the new shape. The flat + ``list_rfqs`` / ``get_rfq`` / ``create_rfq`` / ``delete_rfq`` / + ``list_all_rfqs`` / ``list_quotes`` / ``get_quote`` / ``create_quote`` / + ``delete_quote`` / ``list_all_quotes`` / ``accept_quote`` / + ``confirm_quote`` methods are kept as deprecated forwarders for one + release. Each call emits ``DeprecationWarning`` and delegates to the + sub-namespace equivalent. + """ + + @respx.mock + def test_issue_348_rfqs_sub_namespace_works( + self, comms: CommunicationsResource, + ) -> None: + assert isinstance(comms.rfqs, RFQsResource) + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(200, json={"rfqs": [_MINIMAL_RFQ]})) + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/rfq-1", + ).mock(return_value=httpx.Response(200, json={"rfq": _MINIMAL_RFQ})) + respx.post( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(201, json={"id": "rfq-new"})) + + page = comms.rfqs.list(limit=10) + assert isinstance(page.items[0], RFQ) + + got = comms.rfqs.get("rfq-1") + assert got.rfq.id == "rfq-1" + + created = comms.rfqs.create(market_ticker="MKT-1", rest_remainder=True) + assert created.id == "rfq-new" + + @respx.mock + def test_issue_348_quotes_sub_namespace_works( + self, comms: CommunicationsResource, + ) -> None: + assert isinstance(comms.quotes, QuotesResource) + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": [_MINIMAL_QUOTE]})) + respx.post( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(201, json={"id": "q-new"})) + respx.put( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1/accept", + ).mock(return_value=httpx.Response(204)) + + page = comms.quotes.list(quote_creator_user_id="u1") + assert isinstance(page.items[0], Quote) + + created = comms.quotes.create( + rfq_id="rfq-1", + yes_bid=Decimal("0.56"), + no_bid=Decimal("0.44"), + rest_remainder=True, + ) + assert created.id == "q-new" + + comms.quotes.accept("q-1", accepted_side="yes") + + def test_issue_348_async_rfqs_sub_namespace_class( + self, async_comms: AsyncCommunicationsResource, + ) -> None: + # Wiring check — full async I/O is exercised in TestAsyncCommunications + # via the deprecated forwarders, which delegate here. + assert isinstance(async_comms.rfqs, AsyncRFQsResource) + assert isinstance(async_comms.quotes, AsyncQuotesResource) + + @respx.mock + def test_issue_348_flat_names_still_work_emit_deprecation_warning( + self, comms: CommunicationsResource, + ) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(200, json={"rfqs": [_MINIMAL_RFQ]})) + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/rfq-1", + ).mock(return_value=httpx.Response(200, json={"rfq": _MINIMAL_RFQ})) + respx.post( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(201, json={"id": "rfq-new"})) + respx.delete( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/rfq-1", + ).mock(return_value=httpx.Response(204)) + + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": [_MINIMAL_QUOTE]})) + respx.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1", + ).mock(return_value=httpx.Response(200, json={"quote": _MINIMAL_QUOTE})) + respx.post( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(201, json={"id": "q-new"})) + respx.delete( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1", + ).mock(return_value=httpx.Response(204)) + respx.put( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1/accept", + ).mock(return_value=httpx.Response(204)) + respx.put( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1/confirm", + ).mock(return_value=httpx.Response(204)) + + # Each old flat method emits exactly one DeprecationWarning per call + # and produces a response shape matching the sub-namespace method. + with pytest.warns(DeprecationWarning, match=r"rfqs\.list"): + page_rfqs = comms.list_rfqs(limit=10) + assert isinstance(page_rfqs.items[0], RFQ) + + with pytest.warns(DeprecationWarning, match=r"rfqs\.list_all"): + rfqs_all = list(comms.list_all_rfqs()) + assert isinstance(rfqs_all[0], RFQ) + + with pytest.warns(DeprecationWarning, match=r"rfqs\.get"): + got_rfq = comms.get_rfq("rfq-1") + assert got_rfq.rfq.id == "rfq-1" + + with pytest.warns(DeprecationWarning, match=r"rfqs\.create"): + new_rfq = comms.create_rfq(market_ticker="MKT-1", rest_remainder=True) + assert new_rfq.id == "rfq-new" + + with pytest.warns(DeprecationWarning, match=r"rfqs\.delete"): + comms.delete_rfq("rfq-1") + + with pytest.warns(DeprecationWarning, match=r"quotes\.list"): + page_quotes = comms.list_quotes(quote_creator_user_id="u1") + assert isinstance(page_quotes.items[0], Quote) + + with pytest.warns(DeprecationWarning, match=r"quotes\.list_all"): + quotes_all = list(comms.list_all_quotes(quote_creator_user_id="u1")) + assert isinstance(quotes_all[0], Quote) + + with pytest.warns(DeprecationWarning, match=r"quotes\.get"): + got_quote = comms.get_quote("q-1") + assert got_quote.quote.id == "q-1" + + with pytest.warns(DeprecationWarning, match=r"quotes\.create"): + new_quote = comms.create_quote( + rfq_id="rfq-1", + yes_bid=Decimal("0.5"), + no_bid=Decimal("0.5"), + rest_remainder=True, + ) + assert new_quote.id == "q-new" + + with pytest.warns(DeprecationWarning, match=r"quotes\.delete"): + comms.delete_quote("q-1") + + with pytest.warns(DeprecationWarning, match=r"quotes\.accept"): + comms.accept_quote("q-1", accepted_side="yes") + + with pytest.warns(DeprecationWarning, match=r"quotes\.confirm"): + comms.confirm_quote("q-1") + + @pytest.mark.asyncio + async def test_issue_348_async_flat_names_emit_deprecation_warning( + self, + async_comms: AsyncCommunicationsResource, + respx_mock: respx.MockRouter, + ) -> None: + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(200, json={"rfqs": [_MINIMAL_RFQ]})) + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/rfq-1", + ).mock(return_value=httpx.Response(200, json={"rfq": _MINIMAL_RFQ})) + respx_mock.post( + "https://test.kalshi.com/trade-api/v2/communications/rfqs", + ).mock(return_value=httpx.Response(201, json={"id": "rfq-new"})) + respx_mock.delete( + "https://test.kalshi.com/trade-api/v2/communications/rfqs/rfq-1", + ).mock(return_value=httpx.Response(204)) + + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(200, json={"quotes": [_MINIMAL_QUOTE]})) + respx_mock.get( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1", + ).mock(return_value=httpx.Response(200, json={"quote": _MINIMAL_QUOTE})) + respx_mock.post( + "https://test.kalshi.com/trade-api/v2/communications/quotes", + ).mock(return_value=httpx.Response(201, json={"id": "q-new"})) + respx_mock.delete( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1", + ).mock(return_value=httpx.Response(204)) + respx_mock.put( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1/accept", + ).mock(return_value=httpx.Response(204)) + respx_mock.put( + "https://test.kalshi.com/trade-api/v2/communications/quotes/q-1/confirm", + ).mock(return_value=httpx.Response(204)) + + with pytest.warns(DeprecationWarning, match=r"rfqs\.list"): + page_rfqs = await async_comms.list_rfqs(limit=10) + assert isinstance(page_rfqs.items[0], RFQ) + + with pytest.warns(DeprecationWarning, match=r"rfqs\.list_all"): + rfqs_all = [r async for r in async_comms.list_all_rfqs()] + assert isinstance(rfqs_all[0], RFQ) + + with pytest.warns(DeprecationWarning, match=r"rfqs\.get"): + got_rfq = await async_comms.get_rfq("rfq-1") + assert got_rfq.rfq.id == "rfq-1" + + with pytest.warns(DeprecationWarning, match=r"rfqs\.create"): + new_rfq = await async_comms.create_rfq( + market_ticker="MKT-1", rest_remainder=True, + ) + assert new_rfq.id == "rfq-new" + + with pytest.warns(DeprecationWarning, match=r"rfqs\.delete"): + await async_comms.delete_rfq("rfq-1") + + with pytest.warns(DeprecationWarning, match=r"quotes\.list"): + page_quotes = await async_comms.list_quotes(quote_creator_user_id="u1") + assert isinstance(page_quotes.items[0], Quote) + + with pytest.warns(DeprecationWarning, match=r"quotes\.list_all"): + quotes_all = [ + q async for q in async_comms.list_all_quotes(quote_creator_user_id="u1") + ] + assert isinstance(quotes_all[0], Quote) + + with pytest.warns(DeprecationWarning, match=r"quotes\.get"): + got_quote = await async_comms.get_quote("q-1") + assert got_quote.quote.id == "q-1" + + with pytest.warns(DeprecationWarning, match=r"quotes\.create"): + new_quote = await async_comms.create_quote( + rfq_id="rfq-1", + yes_bid=Decimal("0.5"), + no_bid=Decimal("0.5"), + rest_remainder=True, + ) + assert new_quote.id == "q-new" + + with pytest.warns(DeprecationWarning, match=r"quotes\.delete"): + await async_comms.delete_quote("q-1") + + with pytest.warns(DeprecationWarning, match=r"quotes\.accept"): + await async_comms.accept_quote("q-1", accepted_side="yes") + + with pytest.warns(DeprecationWarning, match=r"quotes\.confirm"): + await async_comms.confirm_quote("q-1") diff --git a/tests/test_markets.py b/tests/test_markets.py index c259f17..81d8365 100644 --- a/tests/test_markets.py +++ b/tests/test_markets.py @@ -484,6 +484,58 @@ def test_list_trades_all_paginates(self, markets: MarketsResource) -> None: items = list(markets.list_trades_all(limit=1)) assert [t.trade_id for t in items] == ["t-1", "t-2"] + @respx.mock + def test_issue_349_list_all_trades_works(self, markets: MarketsResource) -> None: + """``list_all_trades`` is the v3.0.0 canonical name (#349).""" + page1 = { + "trades": [ + trade_dict( + trade_id="t-1", + ticker="MKT-A", + count_fp="1.00", + yes_price_dollars="0.50", + no_price_dollars="0.50", + taker_side="yes", + created_time="2026-04-18T12:00:00Z", + ), + ], + "cursor": "", + } + respx.get("https://test.kalshi.com/trade-api/v2/markets/trades").mock( + return_value=httpx.Response(200, json=page1), + ) + items = list(markets.list_all_trades(limit=1)) + assert [t.trade_id for t in items] == ["t-1"] + + @respx.mock + def test_issue_349_list_trades_all_emits_deprecation_warning( + self, markets: MarketsResource, + ) -> None: + """``list_trades_all`` (the suffix-style legacy name) is a deprecated + forwarder to ``list_all_trades`` for one release; calling it must emit + ``DeprecationWarning`` and still return the same result. + """ + page1 = { + "trades": [ + trade_dict( + trade_id="t-1", + ticker="MKT-A", + count_fp="1.00", + yes_price_dollars="0.50", + no_price_dollars="0.50", + taker_side="yes", + created_time="2026-04-18T12:00:00Z", + ), + ], + "cursor": "", + } + respx.get("https://test.kalshi.com/trade-api/v2/markets/trades").mock( + return_value=httpx.Response(200, json=page1), + ) + with pytest.warns(DeprecationWarning, match=r"list_all_trades"): + items = list(markets.list_trades_all(limit=1)) + assert [t.trade_id for t in items] == ["t-1"] + class TestMarketsBulkCandlesticks: @respx.mock