From 4b5566ab60416fa24877002dc4f4300426df208a Mon Sep 17 00:00:00 2001 From: Jeffrey West Date: Fri, 22 May 2026 06:59:15 -0500 Subject: [PATCH 1/2] docs: fix resource-doc drift (live-data, portfolio, structured-targets) + LiveDataResource docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three resource reference pages had drifted from their implementations, and the LiveDataResource.get_typed docstring did not explain that the _typed suffix refers to the legacy URL form rather than to Python typing — readers scanning autocomplete were likely to pick get_typed over get for the wrong reason. docs/resources/live-data.md: corrected three quick-reference endpoint paths to include the /milestone segment and the /batch suffix, and fixed the batch() snippet to iterate the returned list[LiveData] directly (the prior .live_data attribute access raised AttributeError on first use). docs/resources/portfolio.md: removed the stale warning claiming positions_all() does not exist — it shipped in v2.4 (#269) on both sync and async resources. The page now shows the iterator usage and lifts the event_positions caveat from the method docstring, keeping the manual cursor loop as the documented fallback for callers that need the event view. docs/resources/structured-targets.md: rewrote the 404 paragraph to match reality — the resource calls plain _get() and 404 propagates as KalshiNotFoundError; the StructuredTarget | None return reflects the spec-optional envelope field (200 with structured_target=null), not a 404 coalesce. The snippet now uses try/except with the rare soft-not-found branch. kalshi/resources/live_data.py: expanded the get_typed docstring on both sync and async classes to state up front that get_typed and get return the same LiveData with the same annotations, and that the only difference is the URL form. No rename — that would be breaking. Closes #318, #319, #320, #336 --- docs/resources/live-data.md | 10 +++++----- docs/resources/portfolio.md | 21 +++++++++++++++++---- docs/resources/structured-targets.md | 18 +++++++++++++----- kalshi/resources/live_data.py | 20 ++++++++++++++++++-- 4 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/resources/live-data.md b/docs/resources/live-data.md index 64bf024d..1bea8d3f 100644 --- a/docs/resources/live-data.md +++ b/docs/resources/live-data.md @@ -10,9 +10,9 @@ Public — no auth required. | Method | Endpoint | |---|---| -| `get(milestone_id, *, include_player_stats=None)` | `GET /live_data/{milestone_id}` | -| `batch(milestone_ids, *, include_player_stats=None)` | `GET /live_data` | -| `game_stats(milestone_id)` | `GET /live_data/{milestone_id}/game_stats` | +| `get(milestone_id, *, include_player_stats=None)` | `GET /live_data/milestone/{milestone_id}` | +| `batch(milestone_ids, *, include_player_stats=None)` | `GET /live_data/batch` | +| `game_stats(milestone_id)` | `GET /live_data/milestone/{milestone_id}/game_stats` | | `get_typed(live_data_type, milestone_id)` | `GET /live_data/{type}/milestone/{milestone_id}` (legacy) | ## Get one milestone's live data @@ -25,11 +25,11 @@ print(live.live_data_type, live.payload) ## Batch (up to 100 milestones) ```python -resp = client.live_data.batch( +entries = client.live_data.batch( milestone_ids=["ms_a", "ms_b", "ms_c"], include_player_stats=False, ) -for entry in resp.live_data: +for entry in entries: print(entry.milestone_id, entry.payload) ``` diff --git a/docs/resources/portfolio.md b/docs/resources/portfolio.md index f4102e7c..4c77fd1e 100644 --- a/docs/resources/portfolio.md +++ b/docs/resources/portfolio.md @@ -64,16 +64,29 @@ for ep in resp.event_positions: print(ep.event_ticker, ep.event_exposure) ``` -!!! warning "`positions()` does not return Page[T]" +!!! note "`positions()` does not return Page[T]" It returns `PositionsResponse` — two parallel lists (`market_positions` - and `event_positions`) plus its own `cursor` and `has_next`. There is no - `positions_all()` helper. Walk it manually: + and `event_positions`) plus its own `cursor` and `has_next`. Use + `positions_all()` (sync and async, shipped in v2.4) to auto-paginate + over `market_positions` as an iterator of `MarketPosition`: + + ```python + for mp in client.portfolio.positions_all(ticker="KXPRES-24-DJT"): + print(mp.ticker, mp.position, mp.realized_pnl) + ``` + + `event_positions` are intentionally *not* surfaced by `positions_all()`: + they are aggregate roll-ups over the same underlying markets, and page + boundaries cut the aggregate arbitrarily — concatenating across pages + would not recompute a meaningful event-level total. Callers that need + the event view should iterate `positions()` page-by-page: ```python cursor = None while True: resp = client.portfolio.positions(cursor=cursor) - ... + for ep in resp.event_positions: + ... if not resp.has_next: break cursor = resp.cursor diff --git a/docs/resources/structured-targets.md b/docs/resources/structured-targets.md index 3d3818dd..12f68e82 100644 --- a/docs/resources/structured-targets.md +++ b/docs/resources/structured-targets.md @@ -37,15 +37,23 @@ for t in page: ## Get one structured target ```python -t = client.structured_targets.get("st_abc") -if t is None: +from kalshi.errors import KalshiNotFoundError + +try: + t = client.structured_targets.get("st_abc") +except KalshiNotFoundError: print("not found") else: - print(t.name, t.target_type) + if t is None: + # rare: server returned 200 with {"structured_target": null} + print("soft-not-found") + else: + print(t.name, t.target_type) ``` -`get()` may return `None` for unknown IDs (the underlying endpoint returns a -404 that's mapped to `None` rather than `KalshiNotFoundError`). +A 404 from the server raises `KalshiNotFoundError`. `get()` returns `None` +only if the server responds 200 with `{"structured_target": null}` — a +server-side soft-not-found that the spec permits but is rare in practice. ## Reference diff --git a/kalshi/resources/live_data.py b/kalshi/resources/live_data.py index 9f852456..fc4ebf34 100644 --- a/kalshi/resources/live_data.py +++ b/kalshi/resources/live_data.py @@ -48,7 +48,15 @@ def get_typed( include_player_stats: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> LiveData: - """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. + """Legacy ``/live_data/{type}/milestone/{milestone_id}`` URL form. + + Despite the name, ``get_typed`` is **not** a more-strongly-typed + variant of :meth:`get`: both return the same :class:`LiveData` + model with the same annotations. The only difference is the URL + — ``get_typed`` hits the legacy path that embeds the milestone + ``{type}`` segment, while :meth:`get` hits the canonical + ``/live_data/milestone/{milestone_id}``. The ``_typed`` suffix + refers to the typed URL form, not to Python typing. ``milestone_type`` populates the ``{type}`` path segment. Named ``milestone_type`` (not ``type``) to avoid shadowing the Python @@ -134,7 +142,15 @@ async def get_typed( include_player_stats: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> LiveData: - """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. + """Legacy ``/live_data/{type}/milestone/{milestone_id}`` URL form. + + Despite the name, ``get_typed`` is **not** a more-strongly-typed + variant of :meth:`get`: both return the same :class:`LiveData` + model with the same annotations. The only difference is the URL + — ``get_typed`` hits the legacy path that embeds the milestone + ``{type}`` segment, while :meth:`get` hits the canonical + ``/live_data/milestone/{milestone_id}``. The ``_typed`` suffix + refers to the typed URL form, not to Python typing. ``milestone_type`` populates the ``{type}`` path segment. Named ``milestone_type`` (not ``type``) to avoid shadowing the Python From cb6680e3ce0a1e91d563537c11afd1bd43282eaf Mon Sep 17 00:00:00 2001 From: Jeff West Date: Fri, 22 May 2026 07:07:27 -0500 Subject: [PATCH 2/2] =?UTF-8?q?polish(docs):=20correct=20positions=5Fall?= =?UTF-8?q?=20version=20reference=20v2.4=20=E2=86=92=20v2.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/resources/portfolio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/portfolio.md b/docs/resources/portfolio.md index 4c77fd1e..d1c9bd92 100644 --- a/docs/resources/portfolio.md +++ b/docs/resources/portfolio.md @@ -67,7 +67,7 @@ for ep in resp.event_positions: !!! note "`positions()` does not return Page[T]" It returns `PositionsResponse` — two parallel lists (`market_positions` and `event_positions`) plus its own `cursor` and `has_next`. Use - `positions_all()` (sync and async, shipped in v2.4) to auto-paginate + `positions_all()` (sync and async, shipped in v2.5.0) to auto-paginate over `market_positions` as an iterator of `MarketPosition`: ```python