diff --git a/blockrun_llm/__init__.py b/blockrun_llm/__init__.py index b5d93fc..df16b8c 100644 --- a/blockrun_llm/__init__.py +++ b/blockrun_llm/__init__.py @@ -132,6 +132,7 @@ RealFaceList, RealFaceListItem, RealFaceStatus, + RetiredEndpointError, # Smart routing types RoutingDecision, RpcError, @@ -230,6 +231,7 @@ "RealFaceList", "RealFaceListItem", "RealFaceStatus", + "RetiredEndpointError", # Smart routing types "RoutingDecision", "RpcClient", diff --git a/blockrun_llm/client.py b/blockrun_llm/client.py index 1379b29..6e56a34 100644 --- a/blockrun_llm/client.py +++ b/blockrun_llm/client.py @@ -64,6 +64,7 @@ ChatResponse, ImageResponse, PaymentError, + RetiredEndpointError, RoutingDecision, RoutingProfile, SearchResult, @@ -1904,22 +1905,55 @@ def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: # All accept arbitrary keyword filters that are forwarded as query params. def pm_markets(self, **params: Any) -> dict[str, Any]: - """List canonical cross-venue markets (Predexon v2). + """RETIRED — ``/v1/pm/markets`` no longer exists. - Filter with venue=, status=, category=, league=, event_id=, - pagination_key=. Tier 1 ($0.001/call). + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. """ - return self.pm("markets", **params) + raise RetiredEndpointError( + "/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_listings(self, **params: Any) -> dict[str, Any]: - """List venue-native executable listings flattened across canonical - markets (Predexon v2). Tier 1 ($0.001/call).""" - return self.pm("markets/listings", **params) + """RETIRED — ``/v1/pm/markets/listings`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_outcome(self, predexon_id: str) -> dict[str, Any]: - """Resolve a canonical Predexon outcome ID to its market context and - venue listings (Predexon v2). Tier 1 ($0.001/call).""" - return self.pm(f"outcomes/{predexon_id}") + """RETIRED — ``/v1/pm/outcomes/{predexon_id}`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call). @@ -1971,12 +2005,24 @@ def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: return self.pm("limitless/markets", **params) def pm_sports_categories(self) -> dict[str, Any]: - """List available sports categories. Tier 1 ($0.001/call).""" + """List available sports categories. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return self.pm("sports/categories") def pm_sports_markets(self, **params: Any) -> dict[str, Any]: """List sports markets grouped by game. Filter with league=, - sport_type=, status=, venue=. Tier 1 ($0.001/call).""" + sport_type=, status=, venue=. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return self.pm("sports/markets", **params) def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: @@ -3364,16 +3410,55 @@ async def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: return await self._request_with_payment_raw(f"/v1/pm/{path}", query) async def pm_markets(self, **params: Any) -> dict[str, Any]: - """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm("markets", **params) + """RETIRED — ``/v1/pm/markets`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_listings(self, **params: Any) -> dict[str, Any]: - """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm("markets/listings", **params) + """RETIRED — ``/v1/pm/markets/listings`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_outcome(self, predexon_id: str) -> dict[str, Any]: - """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm(f"outcomes/{predexon_id}") + """RETIRED — ``/v1/pm/outcomes/{predexon_id}`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" @@ -3413,11 +3498,23 @@ async def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: return await self.pm("limitless/markets", **params) async def pm_sports_categories(self) -> dict[str, Any]: - """List available sports categories. Tier 1 ($0.001/call).""" + """List available sports categories. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return await self.pm("sports/categories") async def pm_sports_markets(self, **params: Any) -> dict[str, Any]: - """List sports markets grouped by game. Tier 1 ($0.001/call).""" + """List sports markets grouped by game. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return await self.pm("sports/markets", **params) async def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: diff --git a/blockrun_llm/solana_client.py b/blockrun_llm/solana_client.py index 9242313..cdf750f 100644 --- a/blockrun_llm/solana_client.py +++ b/blockrun_llm/solana_client.py @@ -59,6 +59,7 @@ RealFaceInit, RealFaceList, RealFaceStatus, + RetiredEndpointError, RpcResponse, SearchResult, SpeechResponse, @@ -2539,16 +2540,55 @@ def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: return self._request_with_payment_raw(f"/v1/pm/{path}", query) def pm_markets(self, **params: Any) -> dict[str, Any]: - """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" - return self.pm("markets", **params) + """RETIRED — ``/v1/pm/markets`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_listings(self, **params: Any) -> dict[str, Any]: - """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" - return self.pm("markets/listings", **params) + """RETIRED — ``/v1/pm/markets/listings`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_outcome(self, predexon_id: str) -> dict[str, Any]: - """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" - return self.pm(f"outcomes/{predexon_id}") + """RETIRED — ``/v1/pm/outcomes/{predexon_id}`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; calling it fails immediately + instead of after a paid round trip. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" @@ -2588,11 +2628,23 @@ def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: return self.pm("limitless/markets", **params) def pm_sports_categories(self) -> dict[str, Any]: - """List available sports categories. Tier 1 ($0.001/call).""" + """List available sports categories. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return self.pm("sports/categories") def pm_sports_markets(self, **params: Any) -> dict[str, Any]: - """List sports markets grouped by game. Tier 1 ($0.001/call).""" + """List sports markets grouped by game. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return self.pm("sports/markets", **params) def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: @@ -4449,16 +4501,55 @@ async def pm_query(self, path: str, query: dict[str, Any]) -> dict[str, Any]: return await self._request_with_payment_raw(f"/v1/pm/{path}", query) async def pm_markets(self, **params: Any) -> dict[str, Any]: - """List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm("markets", **params) + """RETIRED — ``/v1/pm/markets`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_listings(self, **params: Any) -> dict[str, Any]: - """List venue-native executable listings (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm("markets/listings", **params) + """RETIRED — ``/v1/pm/markets/listings`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_outcome(self, predexon_id: str) -> dict[str, Any]: - """Resolve a canonical Predexon outcome ID (Predexon v2). Tier 1 ($0.001/call).""" - return await self.pm(f"outcomes/{predexon_id}") + """RETIRED — ``/v1/pm/outcomes/{predexon_id}`` no longer exists. + + Predexon sunset market matching on 2026-07-20 and the whole + canonical layer went with it, so this path returns 410 upstream. + Use ``pm("markets/search", q=...)`` for cross-venue lookups. + + Kept as a raising stub rather than deleted so upgrading does not + break imports or attribute access; it raises before any network + I/O, so you never pay a round trip to learn it is gone. + + :raises RetiredEndpointError: always. + """ + raise RetiredEndpointError( + "/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). Use pm('markets/search', q=...) for cross-venue lookups." + ) async def pm_polymarket_markets(self, **params: Any) -> dict[str, Any]: """List Polymarket markets (Predexon v2). Tier 1 ($0.001/call).""" @@ -4497,11 +4588,23 @@ async def pm_limitless_markets(self, **params: Any) -> dict[str, Any]: return await self.pm("limitless/markets", **params) async def pm_sports_categories(self) -> dict[str, Any]: - """List available sports categories. Tier 1 ($0.001/call).""" + """List available sports categories. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return await self.pm("sports/categories") async def pm_sports_markets(self, **params: Any) -> dict[str, Any]: - """List sports markets grouped by game. Tier 1 ($0.001/call).""" + """List sports markets grouped by game. Tier 1 ($0.001/call). + + .. warning:: + Upstream is returning 500 for every ``sports/*`` path as of + 2026-08-04. The route still resolves, so this keeps working the + moment Predexon restores it, but do not build on it yet. + """ return await self.pm("sports/markets", **params) async def pm_wallet_identity(self, wallet: str) -> dict[str, Any]: diff --git a/blockrun_llm/types.py b/blockrun_llm/types.py index 9b15850..0dc0108 100644 --- a/blockrun_llm/types.py +++ b/blockrun_llm/types.py @@ -310,6 +310,15 @@ class BlockrunError(Exception): """Base exception for BlockRun SDK.""" +class RetiredEndpointError(BlockrunError): + """Raised by a helper whose upstream endpoint no longer exists. + + Kept as a raising method rather than deleted so upgrading does not break + imports or attribute access — the failure is explicit and immediate instead + of a paid round trip that returns 410/404. + """ + + class PaymentError(BlockrunError): """Payment-related error. diff --git a/tests/unit/test_retired_endpoints.py b/tests/unit/test_retired_endpoints.py new file mode 100644 index 0000000..44bde7f --- /dev/null +++ b/tests/unit/test_retired_endpoints.py @@ -0,0 +1,59 @@ +"""Helpers for endpoints Predexon retired must fail fast, not silently 410. + +Probed upstream 2026-08-04 (3 runs each): /v1/pm/markets, /v1/pm/markets/listings +and /v1/pm/outcomes/{id} all return + 410 "This endpoint has been sunset as of 2026-07-20. Market matching is + discontinued." +The helpers are kept rather than deleted so upgrading does not break imports; +this pins that they raise instead of quietly costing a round trip. +""" + +import pytest + +from blockrun_llm import LLMClient, RetiredEndpointError +from blockrun_llm.client import AsyncLLMClient + + +def _bare(cls): + """Instance without running __init__ — no wallet or network needed.""" + return cls.__new__(cls) + + +@pytest.mark.parametrize( + "method,args", + [ + ("pm_markets", ()), + ("pm_listings", ()), + ("pm_outcome", ("PXM-12345",)), + ], +) +def test_sync_helpers_raise(method, args): + with pytest.raises(RetiredEndpointError, match="2026-07-20"): + getattr(_bare(LLMClient), method)(*args) + + +@pytest.mark.parametrize( + "method,args", + [ + ("pm_markets", ()), + ("pm_listings", ()), + ("pm_outcome", ("PXM-12345",)), + ], +) +@pytest.mark.asyncio +async def test_async_helpers_raise(method, args): + # An async def raises on await, not on call — but still before any network + # I/O, which is the point: no paid round trip to learn it is gone. + with pytest.raises(RetiredEndpointError, match="2026-07-20"): + await getattr(_bare(AsyncLLMClient), method)(*args) + + +def test_message_points_at_the_replacement(): + with pytest.raises(RetiredEndpointError) as exc: + _bare(LLMClient).pm_markets() + assert "markets/search" in str(exc.value) + + +def test_surviving_helper_is_untouched(): + # markets/search survived the sunset (422 on a missing q, i.e. alive). + assert not (getattr(LLMClient, "pm_wallet_identity", None) is None)