diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 7c9c434b..b6f025b5 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1011,4 +1011,4 @@ "python": "import pmxt\nimport os\n\nexchange = pmxt.Polymarket(\n private_key=os.getenv('POLYMARKET_PRIVATE_KEY')\n)\n\n# 1. Check balance\nbalances = exchange.fetch_balance()\nif balances:\n balance = balances[0]\n print(f'Available: ${balance.available}')\n\n# 2. Search for a market\nmarkets = exchange.fetch_markets(query='Trump')\nmarket = markets[0]\noutcome = market.yes\n\nprint(f'{market.title}')\nprint(f'Price: {outcome.price * 100:.1f}%')\n\n# 3. Place a limit order\norder = exchange.create_order(\n market_id=market.market_id,\n outcome_id=outcome.outcome_id,\n side='buy',\n type='limit',\n amount=10,\n price=0.50\n)\n\nprint(f'Order placed: {order.id}')\n\n# 4. Check order status\nupdated_order = exchange.fetch_order(order.id)\nprint(f'Status: {updated_order.status}')\nprint(f'Filled: {updated_order.filled}/{updated_order.amount}')\n\n# 5. Cancel if needed\nif updated_order.status == 'open':\n exchange.cancel_order(order.id)\n print('Order cancelled')\n\n# 6. Check positions\npositions = exchange.fetch_positions()\nfor pos in positions:\n pnl_sign = '+' if pos.unrealized_pnl > 0 else ''\n print(f'{pos.outcome_label}: {pnl_sign}${pos.unrealized_pnl:.2f}')", "typescript": "import pmxt from 'pmxtjs';\n\nconst exchange = new pmxt.Polymarket({\n privateKey: process.env.POLYMARKET_PRIVATE_KEY\n});\n\n// 1. Check balance\nconst [balance] = await exchange.fetchBalance();\nconsole.log(`Available: $${balance.available}`);\n\n// 2. Search for a market\nconst markets = await exchange.fetchMarkets({ query: 'Trump' });\nconst market = markets[0];\nconst outcome = market.yes;\n\nconsole.log(market.title);\nconsole.log(`Price: ${(outcome.price * 100).toFixed(1)}%`);\n\n// 3. Place a limit order\nconst order = await exchange.createOrder({\n marketId: market.marketId,\n outcomeId: outcome.outcomeId,\n side: 'buy',\n type: 'limit',\n amount: 10,\n price: 0.50\n});\n\nconsole.log(`Order placed: ${order.id}`);\n\n// 4. Check order status\nconst updatedOrder = await exchange.fetchOrder(order.id);\nconsole.log(`Status: ${updatedOrder.status}`);\nconsole.log(`Filled: ${updatedOrder.filled}/${updatedOrder.amount}`);\n\n// 5. Cancel if needed\nif (updatedOrder.status === 'open') {\n await exchange.cancelOrder(order.id);\n console.log('Order cancelled');\n}\n\n// 6. Check positions\nconst positions = await exchange.fetchPositions();\npositions.forEach(pos => {\n console.log(`${pos.outcomeLabel}: ${pos.unrealizedPnL > 0 ? '+' : ''}$${pos.unrealizedPnL.toFixed(2)}`);\n});" } -} \ No newline at end of file +} 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 8f5e67bc..f7584c3f 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 1b31d68a..de1cc856 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( @@ -549,9 +549,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 @@ -652,9 +685,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"] @@ -663,6 +722,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 @@ -685,6 +745,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 @@ -748,6 +809,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/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