diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 3e176c67..f2b28a73 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-02T00:34:44.916Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-05T12:49:16.156Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", @@ -566,7 +566,7 @@ "type": "UnifiedEvent[]", "description": "Filtered array of events" }, - "source": "BaseExchange.ts:1317" + "source": "BaseExchange.ts:1318" }, "watchOrderBook": { "summary": "Watch order book updates in real-time via WebSocket.", @@ -595,7 +595,7 @@ "type": "OrderBook", "description": "Promise that resolves with the current orderbook state" }, - "source": "BaseExchange.ts:1413" + "source": "BaseExchange.ts:1414" }, "watchOrderBooks": { "summary": "Watch multiple order books simultaneously via WebSocket.", @@ -624,7 +624,7 @@ "type": "Record", "description": "Promise that resolves with order books keyed by ID" }, - "source": "BaseExchange.ts:1426" + "source": "BaseExchange.ts:1427" }, "unwatchOrderBook": { "summary": "Unsubscribe from a previously watched order book stream.", @@ -641,7 +641,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1454" + "source": "BaseExchange.ts:1455" }, "watchTrades": { "summary": "Watch trade executions in real-time via WebSocket.", @@ -676,7 +676,7 @@ "type": "Trade[]", "description": "Promise that resolves with recent trades" }, - "source": "BaseExchange.ts:1467" + "source": "BaseExchange.ts:1468" }, "watchAddress": { "summary": "Stream activity for a public wallet address", @@ -699,7 +699,7 @@ "type": "SubscribedAddressSnapshot", "description": "Promise that resolves with the latest SubscribedAddressSnapshot snapshot" }, - "source": "BaseExchange.ts:1481" + "source": "BaseExchange.ts:1482" }, "unwatchAddress": { "summary": "Stop watching a previously registered wallet address and release its resource updates.", @@ -716,7 +716,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1494" + "source": "BaseExchange.ts:1495" }, "close": { "summary": "Close all WebSocket connections and clean up resources.", @@ -726,7 +726,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1503" + "source": "BaseExchange.ts:1504" }, "fetchMarketMatches": { "summary": "Find the same or related market on other venues. Two modes:", @@ -743,7 +743,7 @@ "type": "MatchResult[]", "description": "Array of matched markets with relation and confidence" }, - "source": "BaseExchange.ts:1517" + "source": "BaseExchange.ts:1518" }, "fetchMatches": { "summary": "fetchMatches", @@ -760,7 +760,7 @@ "type": "MatchResult[]", "description": "Result" }, - "source": "BaseExchange.ts:1533" + "source": "BaseExchange.ts:1534" }, "fetchEventMatches": { "summary": "Find the same or related event on other venues. Two modes:", @@ -777,7 +777,7 @@ "type": "EventMatchResult[]", "description": "Array of matched events with market-level match details" }, - "source": "BaseExchange.ts:1541" + "source": "BaseExchange.ts:1542" }, "compareMarketPrices": { "summary": "Compare live prices for the same market across venues. Finds identity matches and returns side-by-side best bid/ask prices so you can spot price differences at a glance.", @@ -794,7 +794,7 @@ "type": "PriceComparison[]", "description": "Array of price comparisons across venues" }, - "source": "BaseExchange.ts:1557" + "source": "BaseExchange.ts:1558" }, "fetchRelatedMarkets": { "summary": "Find related markets across venues. Discovers subset/superset market relationships", @@ -811,7 +811,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1567" + "source": "BaseExchange.ts:1568" }, "fetchMatchedMarkets": { "summary": "fetchMatchedMarkets", @@ -828,7 +828,7 @@ "type": "MatchedMarketPair[]", "description": "Result" }, - "source": "BaseExchange.ts:1578" + "source": "BaseExchange.ts:1579" }, "fetchMatchedPrices": { "summary": "fetchMatchedPrices", @@ -845,7 +845,7 @@ "type": "MatchedPricePair[]", "description": "Array of matched market pairs with prices from each venue" }, - "source": "BaseExchange.ts:1586" + "source": "BaseExchange.ts:1587" }, "fetchHedges": { "summary": "fetchHedges", @@ -862,7 +862,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1597" + "source": "BaseExchange.ts:1598" }, "fetchArbitrage": { "summary": "fetchArbitrage", @@ -879,7 +879,7 @@ "type": "ArbitrageOpportunity[]", "description": "Array of arbitrage opportunities sorted by spread" }, - "source": "BaseExchange.ts:1607" + "source": "BaseExchange.ts:1608" }, "watchPrices": { "summary": "Watch AMM price updates for a market address (Limitless only).", diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index dac681ce..b20f3aa2 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -1456,7 +1456,7 @@ title: str # The market title (e.g., "Will BTC close above $100k on Dec 31?"). description: str # Long-form market description or resolution criteria. slug: str # URL-friendly slug for the market. outcomes: List[MarketOutcome] # The possible outcomes for this market. -resolution_date: str # When the market is scheduled to resolve. +resolution_date: str # When the market is scheduled to resolve. Optional because some venues do not publish a cutoff for every market (e.g. Opinion categorical children) — emit `undefined` rather than coercing to epoch. volume24h: float # Trading volume over the past 24 hours (USD). volume: float # Total / Lifetime volume liquidity: float # Current market liquidity (USD). diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index 171a6ca8..deb28e39 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -19,6 +19,7 @@ from typing import Any, Dict, List from .client import Exchange +from .constants import ENV, ENV_BASE_URL, ENV_API_KEY from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, PolymarketUS, Polymarket_us, Hyperliquid, GeminiTitan, SuiBets, Suibets, Mock, Router from .router import Router from .feed_client import FeedClient @@ -61,6 +62,13 @@ EventFilterCriteria, MarketFetchParams, EventFetchParams, + SeriesFetchParams, + TradesParams, + FetchOrderBookParams, + ExchangeOptions, + PolymarketOptions, + RouterOptions, + FeedClientOptions, MatchResult, EventMatchResult, MatchedMarketCluster, @@ -73,6 +81,9 @@ ExecutionPriceResult, MatchRelation, ClusterSortOption, + MatchedClusterSort, + FetchMatchedMarketClustersParams, + FetchMatchedEventClustersParams, SortOption, SearchIn, OrderSide, @@ -170,6 +181,14 @@ def restart_server() -> None: "Router", "Exchange", "FeedClient", + "ExchangeOptions", + "PolymarketOptions", + "RouterOptions", + "FeedClientOptions", + # Environment + "ENV", + "ENV_BASE_URL", + "ENV_API_KEY", # Server Management "ServerManager", "server", @@ -220,10 +239,16 @@ def restart_server() -> None: "SubscribedAddressSnapshot", "MatchRelation", "ClusterSortOption", + "MatchedClusterSort", + "FetchMatchedMarketClustersParams", + "FetchMatchedEventClustersParams", "MarketFilterCriteria", "EventFilterCriteria", "MarketFetchParams", "EventFetchParams", + "SeriesFetchParams", + "TradesParams", + "FetchOrderBookParams", "SortOption", "SearchIn", "OrderSide", diff --git a/sdks/python/pmxt/_exchanges.py b/sdks/python/pmxt/_exchanges.py index 5368bc6f..2854a8d2 100644 --- a/sdks/python/pmxt/_exchanges.py +++ b/sdks/python/pmxt/_exchanges.py @@ -547,3 +547,5 @@ def __init__( # Backwards-compatible aliases for exchange classes generated before underscore handling. Polymarket_us = PolymarketUS Suibets = SuiBets + +from .router import Router as Router diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 741a8d92..162c2065 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -46,6 +46,9 @@ MarketFilterFunction, EventFilterCriteria, EventFilterFunction, + SeriesFetchParams, + TradesParams, + FetchOrderBookParams, SubscribedAddressSnapshot, FirehoseEvent, MatchResult, @@ -262,9 +265,9 @@ def _convert_subscription_snapshot(raw: Dict[str, Any]) -> SubscribedAddressSnap raw_positions = raw.get("positions") raw_balances = raw.get("balances") return _auto_convert(SubscribedAddressSnapshot, raw, - trades=[_convert_trade(t) for t in raw_trades] if raw_trades else None, - positions=[_convert_position(p) for p in raw_positions] if raw_positions else None, - balances=[_convert_balance(b) for b in raw_balances] if raw_balances else None, + trades=[_convert_trade(t) for t in (raw_trades or [])], + positions=[_convert_position(p) for p in (raw_positions or [])], + balances=[_convert_balance(b) for b in (raw_balances or [])], ) @@ -368,7 +371,7 @@ def __init__( effective_base_url = f"http://localhost:{actual_port}" except Exception as e: - raise Exception( + raise PmxtError( f"Failed to start PMXT server: {e}\n\n" f"Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n" f"Or start the server manually: pmxt-server" @@ -571,6 +574,7 @@ def _sidecar_read_request( same ``_parse_api_exception`` path as the POST fallback. """ base_url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/{method_name}" + query = _convert_params_to_camel(query) creds = self._get_credentials_dict() has_credentials = creds is not None @@ -772,7 +776,7 @@ def fetch_markets(self, params: Optional[dict] = None, **kwargs) -> List[Unified if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -795,7 +799,7 @@ def fetch_markets_paginated(self, params: Optional[dict] = None, **kwargs) -> Pa if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -810,7 +814,7 @@ def fetch_markets_paginated(self, params: Optional[dict] = None, **kwargs) -> Pa data = self._handle_response(json.loads(response.data)) return PaginatedMarketsResult( data=[_convert_market(m) for m in data.get("data", [])], - total=data.get("total", 0), + total=data.get("total"), next_cursor=data.get("nextCursor"), ) except ApiException as e: @@ -822,7 +826,7 @@ def fetch_events(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedE if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -845,7 +849,7 @@ def fetch_series(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedS if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -868,7 +872,7 @@ def fetch_market(self, params: Optional[dict] = None, **kwargs) -> UnifiedMarket if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -891,7 +895,7 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -920,7 +924,7 @@ def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, lim if params is not None: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1026,7 +1030,7 @@ def fetch_my_trades(self, params: Optional[dict] = None, **kwargs) -> List[UserT if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1049,7 +1053,7 @@ def fetch_closed_orders(self, params: Optional[dict] = None, **kwargs) -> List[O if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1072,7 +1076,7 @@ def fetch_all_orders(self, params: Optional[dict] = None, **kwargs) -> List[Orde if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1194,7 +1198,7 @@ def fetch_market_matches(self, params: Optional[dict] = None, **kwargs) -> List[ if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1216,7 +1220,7 @@ def fetch_matches(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1239,7 +1243,7 @@ def fetch_event_matches(self, params: Optional[dict] = None, **kwargs) -> List[A if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1261,7 +1265,7 @@ def compare_market_prices(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1283,7 +1287,7 @@ def fetch_related_markets(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1306,7 +1310,7 @@ def fetch_matched_markets(self, params: Optional[dict] = None, **kwargs) -> List if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1329,7 +1333,7 @@ def fetch_matched_prices(self, params: Optional[dict] = None, **kwargs) -> List[ if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1351,7 +1355,7 @@ def fetch_hedges(self, params: dict, **kwargs) -> List[Any]: args = [] if kwargs: params = {**(params or {}), **kwargs} - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1374,7 +1378,7 @@ def fetch_arbitrage(self, params: Optional[dict] = None, **kwargs) -> List[Any]: if kwargs: params = {**(params or {}), **kwargs} if params is not None: - args.append(params) + args.append(_convert_params_to_camel(params)) body: dict = {"args": args} creds = self._get_credentials_dict() if creds: @@ -1908,7 +1912,7 @@ def watch_order_book( if params: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) ws_data = self._watch_required_via_ws( "watch_order_book", @@ -1917,23 +1921,6 @@ def watch_order_book( ) return _convert_order_book(ws_data) - def unwatch_order_book(self, outcome_id: Union[str, "MarketOutcome"]) -> None: - """ - Unsubscribe from a previously watched order book stream. - - Args: - outcome_id: Outcome ID to stop watching - - Returns: - None - """ - outcome_id = _resolve_outcome_id(outcome_id) - self._unwatch_required_via_ws( - "unwatch_order_book", - "unwatchOrderBook", - [outcome_id], - ) - def watch_order_books( self, outcome_ids: List[Union[str, "MarketOutcome"]] = _UNSET, @@ -1985,7 +1972,7 @@ def watch_order_books( if params: if limit is None: args.append(None) - args.append(params) + args.append(_convert_params_to_camel(params)) raw_result = self._watch_batch_required_via_ws( "watch_order_books", diff --git a/sdks/python/pmxt/constants.py b/sdks/python/pmxt/constants.py index c8b93cd5..3a16303b 100644 --- a/sdks/python/pmxt/constants.py +++ b/sdks/python/pmxt/constants.py @@ -10,6 +10,7 @@ import os from typing import Mapping, NamedTuple, Optional +from types import SimpleNamespace #: The hosted pmxt production endpoint. #: @@ -28,8 +29,9 @@ #: Environment variable names. Centralised so tests and docs can reference #: a single source of truth. -ENV_BASE_URL = "PMXT_BASE_URL" -ENV_API_KEY = "PMXT_API_KEY" +ENV = SimpleNamespace(BASE_URL="PMXT_BASE_URL", API_KEY="PMXT_API_KEY") +ENV_BASE_URL = ENV.BASE_URL +ENV_API_KEY = ENV.API_KEY class ResolvedBaseUrl(NamedTuple): diff --git a/sdks/python/pmxt/errors.py b/sdks/python/pmxt/errors.py index 16d999c2..a0e38f8f 100644 --- a/sdks/python/pmxt/errors.py +++ b/sdks/python/pmxt/errors.py @@ -29,6 +29,10 @@ def __str__(self) -> str: # 4xx Client Errors +def _format_not_found_message(prefix: str, identifier: str) -> str: + return identifier if identifier.startswith(prefix) else f"{prefix}{identifier}" + + class BadRequest(PmxtError): """400 Bad Request - The request was malformed or contains invalid parameters.""" pass @@ -51,23 +55,38 @@ class NotFoundError(PmxtError): class OrderNotFound(NotFoundError): """404 Not Found - The requested order doesn't exist.""" - pass + def __init__(self, order_id: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Order not found: ", order_id), + code="ORDER_NOT_FOUND", + exchange=exchange, + ) class MarketNotFound(NotFoundError): """404 Not Found - The requested market doesn't exist.""" - pass + def __init__(self, market_id: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Market not found: ", market_id), + code="MARKET_NOT_FOUND", + exchange=exchange, + ) class EventNotFound(NotFoundError): """404 Not Found - The requested event doesn't exist.""" - pass + def __init__(self, identifier: str, exchange: str | None = None): + super().__init__( + _format_not_found_message("Event not found: ", identifier), + code="EVENT_NOT_FOUND", + exchange=exchange, + ) class RateLimitExceeded(PmxtError): """429 Too Many Requests - Rate limit exceeded.""" - def __init__(self, message: str, retry_after: int | None = None, **kwargs): + def __init__(self, message: str, retry_after: float | None = None, **kwargs): super().__init__(message, **kwargs) self.retry_after = retry_after @@ -94,12 +113,16 @@ def __init__(self, message: str, field: str | None = None, **kwargs): class NetworkError(PmxtError): """503 Service Unavailable - Network connectivity issues.""" - pass + + def __init__(self, message: str, exchange: str | None = None): + super().__init__(message, code="NETWORK_ERROR", retryable=True, exchange=exchange) class ExchangeNotAvailable(PmxtError): """503 Service Unavailable - Exchange is down or unreachable.""" - pass + + def __init__(self, message: str, exchange: str | None = None): + super().__init__(message, code="EXCHANGE_NOT_AVAILABLE", retryable=True, exchange=exchange) # Mapping from server error codes to error classes diff --git a/sdks/python/pmxt/feed_client.py b/sdks/python/pmxt/feed_client.py index ea9e741d..5ce92c5c 100644 --- a/sdks/python/pmxt/feed_client.py +++ b/sdks/python/pmxt/feed_client.py @@ -180,7 +180,7 @@ def _get(self, method: str, params: Dict[str, Any]) -> Any: req = urllib.request.Request(url, headers=self._headers) try: - with urllib.request.urlopen(req, timeout=15) as resp: + with urllib.request.urlopen(req, timeout=30) as resp: body = json.loads(resp.read()) except urllib.error.HTTPError as e: body = json.loads(e.read()) if e.fp else {} diff --git a/sdks/python/pmxt/models.py b/sdks/python/pmxt/models.py index 8dd12ac6..9d98e391 100644 --- a/sdks/python/pmxt/models.py +++ b/sdks/python/pmxt/models.py @@ -134,7 +134,7 @@ def question(self) -> str: return self.title -class MarketList(list): +class MarketList(List[UnifiedMarket]): """A list of UnifiedMarket objects with a convenience match() method.""" def match( @@ -543,9 +543,42 @@ class SubscribedAddressSnapshot: """Balances of this address""" balances: Optional[List[Balance]] = None -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# Public SDK option types +# ----------------------------------------------------------------------------- + +class ExchangeOptions(TypedDict, total=False): + """Constructor options shared by the exchange clients.""" + pmxt_api_key: str + base_url: str + auto_start_server: bool + api_key: str + private_key: str + api_token: str + proxy_address: str + signature_type: Union[str, int] + + +class PolymarketOptions(ExchangeOptions, total=False): + """Constructor options for Polymarket clients.""" + signature_type: Union[Literal["eoa", "poly-proxy", "gnosis-safe"], int] + + +class RouterOptions(TypedDict, total=False): + """Constructor options for Router clients.""" + pmxt_api_key: str + base_url: str + auto_start_server: bool + + +class FeedClientOptions(TypedDict, total=False): + """Constructor options for FeedClient.""" + pmxt_api_key: str + base_url: str + +# ----------------------------------------------------------------------------- # Filtering Types -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- from typing import TypedDict, Callable @@ -644,9 +677,35 @@ class EventFetchParams(TypedDict, total=False): filter: EventFilterCriteria -# ---------------------------------------------------------------------------- +class SeriesFetchParams(TypedDict, total=False): + """Parameters for fetching recurring venue series.""" + id: str + slug: str + query: str + recurrence: str + limit: int + offset: int + + +class TradesParams(TypedDict, total=False): + """Parameters for fetching public trade history.""" + since: int + until: int + limit: int + cursor: str + + +class FetchOrderBookParams(TypedDict, total=False): + """Parameters for historical order book queries.""" + side: Literal["yes", "no"] + outcome: str + since: int + until: int + + +# ----------------------------------------------------------------------------- # Router Types -# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- MatchRelation = Literal["identity", "complement", "subset", "superset", "overlap", "disjoint"] ClusterSortOption = Literal["volume", "confidence"] @@ -655,6 +714,7 @@ class EventFetchParams(TypedDict, total=False): class MatchedMarketClusterParams(TypedDict, total=False): """Parameters for fetching matched market clusters.""" + market: UnifiedMarket market_id: str slug: str url: str @@ -677,6 +737,7 @@ class MatchedMarketClusterParams(TypedDict, total=False): class MatchedEventClusterParams(TypedDict, total=False): """Parameters for fetching matched event clusters.""" + event: UnifiedEvent event_id: str slug: str url: str @@ -740,6 +801,11 @@ def __getattr__(self, name: str) -> Any: return getattr(self.event, name) +MatchedClusterSort = ClusterSortOption +FetchMatchedMarketClustersParams = MatchedMarketClusterParams +FetchMatchedEventClustersParams = MatchedEventClusterParams + + @dataclass class MatchedMarketCluster: """A connected cluster of semantically matched markets across venues.""" diff --git a/sdks/python/pmxt/router.py b/sdks/python/pmxt/router.py index da2cc666..21142fb6 100644 --- a/sdks/python/pmxt/router.py +++ b/sdks/python/pmxt/router.py @@ -242,6 +242,7 @@ def fetch_event_matches( *, event_id: Optional[str] = None, slug: Optional[str] = None, + url: Optional[str] = None, query: Optional[str] = None, category: Optional[str] = None, relation: Optional[MatchRelation] = None, @@ -276,6 +277,8 @@ def fetch_event_matches( params["eventId"] = event_id if slug is not None: params["slug"] = slug + if url is not None: + params["url"] = url if query is not None: params["query"] = query if category is not None: diff --git a/sdks/python/pmxt/ws_client.py b/sdks/python/pmxt/ws_client.py index 329dd680..51a705c2 100644 --- a/sdks/python/pmxt/ws_client.py +++ b/sdks/python/pmxt/ws_client.py @@ -260,7 +260,8 @@ def subscribe( method: str, args: List[Any], credentials: Optional[Dict[str, Any]] = None, - timeout: float = 30.0, + timeout_ms: float = 30000.0, + timeout: Optional[float] = None, ) -> Dict[str, Any]: """Send a subscribe message and block until the first data event. @@ -302,7 +303,8 @@ def subscribe( self._ws.send(json.dumps(message)) - return self._wait_for_subscription_data(sub, timeout) + effective_timeout = timeout if timeout is not None else timeout_ms / 1000.0 + return self._wait_for_subscription_data(sub, effective_timeout) def subscribe_batch( self, @@ -310,7 +312,8 @@ def subscribe_batch( method: str, args: List[Any], credentials: Optional[Dict[str, Any]] = None, - timeout: float = 30.0, + timeout_ms: float = 30000.0, + timeout: Optional[float] = None, ) -> Dict[str, Any]: """Subscribe to a batch method (e.g. watchOrderBooks) and collect data events for all symbols. @@ -340,7 +343,8 @@ def subscribe_batch( # Wait for data event (the server may push one consolidated event # or multiple per-symbol events) - first_data = self._wait_for_subscription_data(sub, timeout) + effective_timeout = timeout if timeout is not None else timeout_ms / 1000.0 + first_data = self._wait_for_subscription_data(sub, effective_timeout) # Collect per-symbol data result: Dict[str, Any] = {} diff --git a/sdks/python/scripts/generate-client-methods.js b/sdks/python/scripts/generate-client-methods.js index b3d33700..d944555a 100644 --- a/sdks/python/scripts/generate-client-methods.js +++ b/sdks/python/scripts/generate-client-methods.js @@ -341,7 +341,9 @@ function buildPyArgsLines(params, sf) { ? `_resolve_outcome_id(${snakeName})` : isOutcomeIds ? `[_resolve_outcome_id(x) for x in ${snakeName}]` - : snakeName; + : tsName === 'params' + ? `_convert_params_to_camel(${snakeName})` + : snakeName; if (p.questionToken) { lines.push(` if ${snakeName} is not None:`); lines.push(` args.append(${value})`); @@ -376,7 +378,7 @@ function buildPyReturnLines(config) { `${i}data = self._handle_response(json.loads(response.data))`, `${i}return PaginatedMarketsResult(`, `${i} data=[_convert_market(m) for m in data.get("data", [])],`, - `${i} total=data.get("total", 0),`, + `${i} total=data.get("total"),`, `${i} next_cursor=data.get("nextCursor"),`, `${i})`, ].join('\n'); @@ -402,7 +404,7 @@ function generatePyMethod(name, params, config, sf) { ` if params is not None:`, ` if limit is None:`, ` args.append(None)`, - ` args.append(params)`, + ` args.append(_convert_params_to_camel(params))`, ` body: dict = {"args": args}`, ` creds = self._get_credentials_dict()`, ` if creds:`, diff --git a/sdks/python/tests/test_converters.py b/sdks/python/tests/test_converters.py index 70090757..9de6c771 100644 --- a/sdks/python/tests/test_converters.py +++ b/sdks/python/tests/test_converters.py @@ -22,6 +22,7 @@ _convert_trade, _convert_user_trade, _convert_order, + _convert_subscription_snapshot, ) from pmxt.models import ( UnifiedMarket, @@ -34,6 +35,7 @@ UserTrade, Order, MarketList, + SubscribedAddressSnapshot, ) @@ -935,3 +937,15 @@ def test_partially_filled_order(self): assert order.filled == 30.0 assert order.remaining == 20.0 assert order.amount == 50.0 + + +class TestConvertSubscriptionSnapshot: + def test_missing_lists_default_to_empty_lists(self): + snapshot = _convert_subscription_snapshot({ + "address": "0xabc", + "timestamp": 123, + }) + assert isinstance(snapshot, SubscribedAddressSnapshot) + assert snapshot.trades == [] + assert snapshot.positions == [] + assert snapshot.balances == [] diff --git a/sdks/python/tests/test_errors.py b/sdks/python/tests/test_errors.py new file mode 100644 index 00000000..19974a77 --- /dev/null +++ b/sdks/python/tests/test_errors.py @@ -0,0 +1,24 @@ +from pmxt.errors import ( + EventNotFound, + ExchangeNotAvailable, + MarketNotFound, + NetworkError, + OrderNotFound, + RateLimitExceeded, +) + + +def test_not_found_errors_format_their_messages(): + assert str(OrderNotFound("abc-123")) == "Order not found: abc-123" + assert str(MarketNotFound("mkt-456")) == "Market not found: mkt-456" + assert str(EventNotFound("evt-789")) == "Event not found: evt-789" + + +def test_retryable_errors_are_marked_retryable(): + assert NetworkError("network down").retryable is True + assert ExchangeNotAvailable("venue offline").retryable is True + + +def test_rate_limit_retry_after_accepts_float_values(): + err = RateLimitExceeded("slow down", retry_after=1.5) + assert err.retry_after == 1.5 diff --git a/sdks/python/tests/test_public_exports.py b/sdks/python/tests/test_public_exports.py index c3ba6321..deaf9d32 100644 --- a/sdks/python/tests/test_public_exports.py +++ b/sdks/python/tests/test_public_exports.py @@ -25,7 +25,7 @@ def test_websocket_return_types_are_public_exports(): if isinstance(item, ast.Constant) and isinstance(item.value, str) ) - expected = {"FirehoseEvent", "SubscribedAddressSnapshot"} + expected = {"FirehoseEvent", "SubscribedAddressSnapshot", "ExchangeOptions", "PolymarketOptions", "RouterOptions", "FeedClientOptions", "SeriesFetchParams", "TradesParams", "FetchOrderBookParams", "MatchedClusterSort", "FetchMatchedMarketClustersParams", "FetchMatchedEventClustersParams"} assert expected <= imported_models assert expected <= public_exports @@ -99,6 +99,38 @@ def test_feed_client_is_top_level_public_export(): assert "FeedClient" in public_exports +def test_environment_constants_are_top_level_public_exports(): + init_path = Path(__file__).resolve().parents[1] / "pmxt" / "__init__.py" + tree = ast.parse(init_path.read_text(encoding="utf-8")) + + imported_modules = { + alias.name: node.module + for node in tree.body + if isinstance(node, ast.ImportFrom) + for alias in node.names + } + public_exports = set() + + for node in tree.body: + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id == "__all__" + and isinstance(node.value, ast.List) + ): + public_exports.update( + item.value + for item in node.value.elts + if isinstance(item, ast.Constant) and isinstance(item.value, str) + ) + + assert imported_modules["ENV"] == "constants" + assert imported_modules["ENV_BASE_URL"] == "constants" + assert imported_modules["ENV_API_KEY"] == "constants" + assert {"ENV", "ENV_BASE_URL", "ENV_API_KEY"} <= public_exports + + def test_polymarket_init_auth_is_generated(): exchanges_path = Path(__file__).resolve().parents[1] / "pmxt" / "_exchanges.py" tree = ast.parse(exchanges_path.read_text(encoding="utf-8")) diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index f8504e0a..ec2f3280 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -1456,7 +1456,7 @@ title: string; // The market title (e.g., "Will BTC close above $100k on Dec 31? description: string; // Long-form market description or resolution criteria. slug: string; // URL-friendly slug for the market. outcomes: MarketOutcome[]; // The possible outcomes for this market. -resolutionDate: string; // When the market is scheduled to resolve. +resolutionDate: string; // When the market is scheduled to resolve. Optional because some venues do not publish a cutoff for every market (e.g. Opinion categorical children) — emit `undefined` rather than coercing to epoch. volume24h: number; // Trading volume over the past 24 hours (USD). volume: number; // Total / Lifetime volume liquidity: number; // Current market liquidity (USD). diff --git a/sdks/typescript/pmxt/server-manager.ts b/sdks/typescript/pmxt/server-manager.ts index abcaadfd..e6a0f80b 100644 --- a/sdks/typescript/pmxt/server-manager.ts +++ b/sdks/typescript/pmxt/server-manager.ts @@ -121,6 +121,13 @@ export class ServerManager { } } + /** + * Backwards-compatible alias for `isServerRunning()`. + */ + async isServerAlive(): Promise { + return this.isServerRunning(); + } + /** * Wait for the server to be ready. * Requires a lock file to be present to avoid falsely matching an unrelated