pmxt 2.46.2__tar.gz → 2.46.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. {pmxt-2.46.2 → pmxt-2.46.4}/PKG-INFO +1 -1
  2. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/__init__.py +1 -1
  3. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/api_client.py +1 -1
  4. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/configuration.py +1 -1
  5. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/__init__.py +1 -1
  6. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/_server/server/bundled.js +56 -11
  7. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/client.py +1 -0
  8. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/ws_client.py +124 -49
  9. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt.egg-info/PKG-INFO +1 -1
  10. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt.egg-info/SOURCES.txt +2 -1
  11. {pmxt-2.46.2 → pmxt-2.46.4}/pyproject.toml +1 -1
  12. {pmxt-2.46.2 → pmxt-2.46.4}/tests/test_streaming_ws_transport.py +2 -0
  13. pmxt-2.46.4/tests/test_ws_client.py +135 -0
  14. {pmxt-2.46.2 → pmxt-2.46.4}/README.md +0 -0
  15. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/api/__init__.py +0 -0
  16. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/api/data_feeds_api.py +0 -0
  17. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/api/default_api.py +0 -0
  18. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/api_response.py +0 -0
  19. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/exceptions.py +0 -0
  20. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/__init__.py +0 -0
  21. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/arbitrage_opportunity.py +0 -0
  22. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/balance.py +0 -0
  23. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/base_request.py +0 -0
  24. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/base_response.py +0 -0
  25. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/build_order200_response.py +0 -0
  26. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/build_order_request.py +0 -0
  27. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/built_order.py +0 -0
  28. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/built_order_tx.py +0 -0
  29. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/cancel_order_request.py +0 -0
  30. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/close_request.py +0 -0
  31. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/compare_market_prices200_response.py +0 -0
  32. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/compare_market_prices_request.py +0 -0
  33. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/create_order200_response.py +0 -0
  34. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/create_order_params.py +0 -0
  35. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/create_order_request.py +0 -0
  36. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/error_detail.py +0 -0
  37. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/error_response.py +0 -0
  38. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/event_fetch_params.py +0 -0
  39. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/event_filter_criteria.py +0 -0
  40. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/event_filter_criteria_total_volume.py +0 -0
  41. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/event_match_result.py +0 -0
  42. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/exchange_credentials.py +0 -0
  43. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/exchange_credentials_signature_type.py +0 -0
  44. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/execution_price_result.py +0 -0
  45. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_historical_prices200_response.py +0 -0
  46. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_ohlcv200_response.py +0 -0
  47. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_oracle_history200_response.py +0 -0
  48. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_oracle_round200_response.py +0 -0
  49. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_order_book200_response.py +0 -0
  50. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_order_book200_response_data.py +0 -0
  51. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_ticker200_response.py +0 -0
  52. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_fetch_tickers200_response.py +0 -0
  53. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_list200_response.py +0 -0
  54. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_load_markets200_response.py +0 -0
  55. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_market.py +0 -0
  56. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_oracle_round.py +0 -0
  57. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/feed_ticker.py +0 -0
  58. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_arbitrage200_response.py +0 -0
  59. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_arbitrage_params.py +0 -0
  60. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_balance200_response.py +0 -0
  61. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_event200_response.py +0 -0
  62. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_event_matches200_response.py +0 -0
  63. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_event_matches_params.py +0 -0
  64. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_events200_response.py +0 -0
  65. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_events_paginated200_response.py +0 -0
  66. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_market200_response.py +0 -0
  67. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_market_matches200_response.py +0 -0
  68. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_market_matches_params.py +0 -0
  69. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_markets200_response.py +0 -0
  70. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_markets_paginated200_response.py +0 -0
  71. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_matched_markets200_response.py +0 -0
  72. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_matched_markets_params.py +0 -0
  73. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_my_trades200_response.py +0 -0
  74. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_ohlcv200_response.py +0 -0
  75. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_open_orders200_response.py +0 -0
  76. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_order_book200_response.py +0 -0
  77. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_order_book_params.py +0 -0
  78. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_order_books200_response.py +0 -0
  79. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_order_books_request.py +0 -0
  80. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_positions200_response.py +0 -0
  81. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/fetch_trades200_response.py +0 -0
  82. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/filter_events_request.py +0 -0
  83. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/filter_events_request_args_inner.py +0 -0
  84. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/filter_markets_request.py +0 -0
  85. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/filter_markets_request_args_inner.py +0 -0
  86. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/get_execution_price200_response.py +0 -0
  87. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/get_execution_price_detailed200_response.py +0 -0
  88. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/get_execution_price_detailed_request.py +0 -0
  89. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/get_execution_price_request.py +0 -0
  90. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/get_execution_price_request_args_inner.py +0 -0
  91. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/health_check200_response.py +0 -0
  92. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/history_filter_params.py +0 -0
  93. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/load_markets200_response.py +0 -0
  94. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/load_markets_request.py +0 -0
  95. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria.py +0 -0
  96. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_liquidity.py +0 -0
  97. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_open_interest.py +0 -0
  98. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_price.py +0 -0
  99. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_resolution_date.py +0 -0
  100. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_volume.py +0 -0
  101. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_criteria_volume24h.py +0 -0
  102. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_filter_params.py +0 -0
  103. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/market_outcome.py +0 -0
  104. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/match_result.py +0 -0
  105. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/matched_market_pair.py +0 -0
  106. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/my_trades_params.py +0 -0
  107. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/ohlcv_params.py +0 -0
  108. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/order.py +0 -0
  109. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/order_book.py +0 -0
  110. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/order_history_params.py +0 -0
  111. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/order_level.py +0 -0
  112. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/paginated_events_result.py +0 -0
  113. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/paginated_markets_result.py +0 -0
  114. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/position.py +0 -0
  115. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/price_candle.py +0 -0
  116. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/price_comparison.py +0 -0
  117. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/submit_order_request.py +0 -0
  118. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/trade.py +0 -0
  119. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/trades_params.py +0 -0
  120. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/unified_event.py +0 -0
  121. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/unified_market.py +0 -0
  122. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/models/user_trade.py +0 -0
  123. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/py.typed +0 -0
  124. {pmxt-2.46.2 → pmxt-2.46.4}/generated/pmxt_internal/rest.py +0 -0
  125. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/_exchanges.py +0 -0
  126. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/_server/__init__.py +0 -0
  127. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/_server/bin/pmxt-ensure-server +0 -0
  128. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/_server/bin/pmxt-ensure-server.js +0 -0
  129. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/constants.py +0 -0
  130. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/errors.py +0 -0
  131. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/feed_client.py +0 -0
  132. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/models.py +0 -0
  133. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/router.py +0 -0
  134. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt/server_manager.py +0 -0
  135. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt.egg-info/dependency_links.txt +0 -0
  136. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt.egg-info/requires.txt +0 -0
  137. {pmxt-2.46.2 → pmxt-2.46.4}/pmxt.egg-info/top_level.txt +0 -0
  138. {pmxt-2.46.2 → pmxt-2.46.4}/setup.cfg +0 -0
  139. {pmxt-2.46.2 → pmxt-2.46.4}/tests/test_converters.py +0 -0
  140. {pmxt-2.46.2 → pmxt-2.46.4}/tests/test_public_exports.py +0 -0
  141. {pmxt-2.46.2 → pmxt-2.46.4}/tests/test_sdk_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmxt
3
- Version: 2.46.2
3
+ Version: 2.46.4
4
4
  Summary: Unified prediction market data API - The ccxt for prediction markets
5
5
  Author: PMXT Contributors
6
6
  License: MIT
@@ -14,7 +14,7 @@
14
14
  """ # noqa: E501
15
15
 
16
16
 
17
- __version__ = "2.46.2"
17
+ __version__ = "2.46.4"
18
18
 
19
19
  # Define package exports
20
20
  __all__ = [
@@ -91,7 +91,7 @@ class ApiClient:
91
91
  self.default_headers[header_name] = header_value
92
92
  self.cookie = cookie
93
93
  # Set default User-Agent.
94
- self.user_agent = 'OpenAPI-Generator/2.46.2/python'
94
+ self.user_agent = 'OpenAPI-Generator/2.46.4/python'
95
95
  self.client_side_validation = configuration.client_side_validation
96
96
 
97
97
  def __enter__(self):
@@ -506,7 +506,7 @@ class Configuration:
506
506
  "OS: {env}\n"\
507
507
  "Python Version: {pyversion}\n"\
508
508
  "Version of the API: 0.4.4\n"\
509
- "SDK Package Version: 2.46.2".\
509
+ "SDK Package Version: 2.46.4".\
510
510
  format(env=sys.platform, pyversion=sys.version)
511
511
 
512
512
  def get_host_settings(self) -> List[HostSetting]:
@@ -145,7 +145,7 @@ def restart_server() -> None:
145
145
  )
146
146
  _default_manager.restart()
147
147
 
148
- __version__ = "2.46.2"
148
+ __version__ = "2.46.4"
149
149
  __all__ = [
150
150
  # Exchanges
151
151
  "Polymarket",
@@ -214440,10 +214440,20 @@ var require_utils25 = __commonJS({
214440
214440
  exports2.paginateLimitlessMarkets = paginateLimitlessMarkets;
214441
214441
  var market_utils_1 = require_market_utils();
214442
214442
  exports2.DEFAULT_LIMITLESS_API_URL = "https://api.limitless.exchange";
214443
- function mapMarketToUnified(market) {
214443
+ function mapMarketToUnified(market, context = {}) {
214444
214444
  if (!market)
214445
214445
  return null;
214446
+ const resolvedContext = {
214447
+ eventId: getText(market.__pmxtEventId) || context.eventId,
214448
+ eventTitle: getText(market.__pmxtEventTitle) || context.eventTitle,
214449
+ eventDescription: getText(market.__pmxtEventDescription) || context.eventDescription,
214450
+ categories: getStringArray(market.__pmxtCategories) || context.categories,
214451
+ tags: getStringArray(market.__pmxtTags) || context.tags
214452
+ };
214446
214453
  const outcomes = [];
214454
+ const rawTitle = getText(market.title) || getText(market.question) || market.slug;
214455
+ const title = composeMarketTitle(rawTitle, resolvedContext.eventTitle);
214456
+ const hasParentContext = Boolean(resolvedContext.eventTitle && rawTitle);
214447
214457
  if (market.tokens) {
214448
214458
  if (!market.tokens.yes || !market.tokens.no) {
214449
214459
  throw new Error(`[limitless] Market "${market.slug}" is missing token addresses`);
@@ -214451,10 +214461,12 @@ var require_utils25 = __commonJS({
214451
214461
  const prices = Array.isArray(market.prices) ? market.prices : [];
214452
214462
  const yesPrice = prices[0] || 0;
214453
214463
  const noPrice = prices[1] || 0;
214464
+ const yesLabel = hasParentContext ? rawTitle : "Yes";
214465
+ const noLabel = hasParentContext ? `Not ${rawTitle}` : "No";
214454
214466
  outcomes.push({
214455
214467
  outcomeId: market.tokens.yes,
214456
214468
  marketId: market.slug,
214457
- label: "Yes",
214469
+ label: yesLabel,
214458
214470
  price: yesPrice,
214459
214471
  priceChange24h: 0,
214460
214472
  metadata: { clobTokenId: market.tokens.yes }
@@ -214462,7 +214474,7 @@ var require_utils25 = __commonJS({
214462
214474
  outcomes.push({
214463
214475
  outcomeId: market.tokens.no,
214464
214476
  marketId: market.slug,
214465
- label: "No",
214477
+ label: noLabel,
214466
214478
  price: noPrice,
214467
214479
  priceChange24h: 0,
214468
214480
  metadata: { clobTokenId: market.tokens.no }
@@ -214476,9 +214488,9 @@ var require_utils25 = __commonJS({
214476
214488
  const um = {
214477
214489
  id: market.slug,
214478
214490
  marketId: market.slug,
214479
- eventId: market.slug,
214480
- title: market.title || market.question,
214481
- description: market.description,
214491
+ eventId: resolvedContext.eventId || market.slug,
214492
+ title,
214493
+ description: market.description || resolvedContext.eventDescription,
214482
214494
  slug: market.slug,
214483
214495
  outcomes,
214484
214496
  resolutionDate: market.expirationTimestamp ? new Date(market.expirationTimestamp) : /* @__PURE__ */ new Date(),
@@ -214490,13 +214502,32 @@ var require_utils25 = __commonJS({
214490
214502
  // Not directly in the flat market list
214491
214503
  url: `https://limitless.exchange/markets/${market.slug}`,
214492
214504
  image: market.logo || `https://limitless.exchange/api/og?slug=${market.slug}`,
214493
- category: market.categories?.[0],
214494
- tags: market.tags || [],
214505
+ category: market.categories?.[0] || resolvedContext.categories?.[0],
214506
+ tags: market.tags || resolvedContext.tags || [],
214495
214507
  status
214496
214508
  };
214497
214509
  (0, market_utils_1.addBinaryOutcomes)(um);
214498
214510
  return um;
214499
214511
  }
214512
+ function getText(value) {
214513
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
214514
+ }
214515
+ function getStringArray(value) {
214516
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
214517
+ }
214518
+ function normalizeTitle(value) {
214519
+ return value.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
214520
+ }
214521
+ function composeMarketTitle(childTitle, eventTitle) {
214522
+ if (!eventTitle)
214523
+ return childTitle;
214524
+ const normalizedChild = normalizeTitle(childTitle);
214525
+ const normalizedEvent = normalizeTitle(eventTitle);
214526
+ if (!normalizedChild || !normalizedEvent || normalizedChild.startsWith(normalizedEvent)) {
214527
+ return childTitle;
214528
+ }
214529
+ return `${eventTitle} - ${childTitle}`;
214530
+ }
214500
214531
  function mapIntervalToFidelity(interval) {
214501
214532
  const mapping = {
214502
214533
  "1m": 1,
@@ -214707,7 +214738,14 @@ var require_fetcher2 = __commonJS({
214707
214738
  for (const res of rawResults) {
214708
214739
  if (res.markets && Array.isArray(res.markets)) {
214709
214740
  for (const child of res.markets) {
214710
- allRawMarkets.push(child);
214741
+ allRawMarkets.push({
214742
+ ...child,
214743
+ __pmxtEventId: res.slug,
214744
+ __pmxtEventTitle: res.title || res.question,
214745
+ __pmxtEventDescription: res.description,
214746
+ __pmxtCategories: res.categories,
214747
+ __pmxtTags: res.tags
214748
+ });
214711
214749
  }
214712
214750
  } else {
214713
214751
  allRawMarkets.push(res);
@@ -214840,7 +214878,14 @@ var require_normalizer2 = __commonJS({
214840
214878
  return null;
214841
214879
  let marketsList = [];
214842
214880
  if (raw.markets && Array.isArray(raw.markets)) {
214843
- marketsList = raw.markets.map((child) => (0, utils_1.mapMarketToUnified)(child)).filter((m) => m !== null);
214881
+ const eventTitle = raw.title || raw.question || "";
214882
+ marketsList = raw.markets.map((child) => (0, utils_1.mapMarketToUnified)(child, {
214883
+ eventId: raw.slug,
214884
+ eventTitle,
214885
+ eventDescription: raw.description,
214886
+ categories: raw.categories,
214887
+ tags: raw.tags
214888
+ })).filter((m) => m !== null);
214844
214889
  } else {
214845
214890
  const unifiedMarket = (0, utils_1.mapMarketToUnified)(raw);
214846
214891
  if (unifiedMarket)
@@ -276713,7 +276758,7 @@ var crypto_1 = require("crypto");
276713
276758
  var crypto_2 = require("crypto");
276714
276759
  var fs_1 = require("fs");
276715
276760
  function getServerVersion() {
276716
- const baseVersion = true ? "2.46.2" : "0.0.0-dev";
276761
+ const baseVersion = true ? "2.46.4" : "0.0.0-dev";
276717
276762
  const isDev = process.env.NODE_ENV === "development" || process.env.PMXT_ALWAYS_RESTART === "1" || __dirname.includes("/core/src/") || __dirname.includes("/core/dist/");
276718
276763
  if (!isDev) {
276719
276764
  return baseVersion;
@@ -1808,6 +1808,7 @@ class Exchange(ABC):
1808
1808
  existing_id = ws._active_subs.pop(sub_key, None)
1809
1809
  if existing_id:
1810
1810
  ws._subscriptions.pop(existing_id, None)
1811
+ ws._data_queues.pop(existing_id, None)
1811
1812
  ws._data_store.pop(existing_id, None)
1812
1813
  except PmxtError:
1813
1814
  raise
@@ -8,12 +8,42 @@ does not support the /ws endpoint.
8
8
  """
9
9
 
10
10
  import json
11
+ import socket
11
12
  import threading
13
+ import time
12
14
  import uuid
13
- from typing import Any, Callable, Dict, List, Optional
15
+ from collections import deque
16
+ from typing import Any, Deque, Dict, List, Optional
17
+ from urllib.parse import urlparse
14
18
 
15
19
  from .errors import PmxtError
16
20
 
21
+ MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION = 100_000
22
+ CONNECT_ATTEMPTS = 3
23
+ _NO_DATA = object()
24
+
25
+
26
+ def _connect_websocket(ws: Any, url: str, timeout: float) -> None:
27
+ """Connect, preferring IPv4 for hosted pmxt custom-domain websockets."""
28
+ host = urlparse(url).hostname
29
+ if host != "api.pmxt.dev":
30
+ ws.connect(url, timeout=timeout)
31
+ return
32
+
33
+ original_getaddrinfo = socket.getaddrinfo
34
+
35
+ def getaddrinfo_ipv4_first(*args: Any, **kwargs: Any) -> Any:
36
+ results = original_getaddrinfo(*args, **kwargs)
37
+ ipv4 = [item for item in results if item[0] == socket.AF_INET]
38
+ other = [item for item in results if item[0] != socket.AF_INET]
39
+ return ipv4 + other
40
+
41
+ socket.getaddrinfo = getaddrinfo_ipv4_first
42
+ try:
43
+ ws.connect(url, timeout=timeout)
44
+ finally:
45
+ socket.getaddrinfo = original_getaddrinfo
46
+
17
47
 
18
48
  class _WsSubscription:
19
49
  """Tracks a single subscription and its pending data event."""
@@ -45,7 +75,9 @@ class SidecarWsClient:
45
75
  self._reader_thread: Optional[threading.Thread] = None
46
76
  self._closed = False
47
77
 
48
- # request_id -> latest data payload
78
+ # request_id -> queued data payloads for single-event watch methods
79
+ self._data_queues: Dict[str, Deque[Dict[str, Any]]] = {}
80
+ # request_id[:symbol] -> latest data payload for batch snapshots/errors
49
81
  self._data_store: Dict[str, Dict[str, Any]] = {}
50
82
  # request_id -> subscription metadata
51
83
  self._subscriptions: Dict[str, _WsSubscription] = {}
@@ -85,8 +117,28 @@ class SidecarWsClient:
85
117
  elif self._access_token:
86
118
  url = f"{url}?token={self._access_token}"
87
119
 
88
- ws = websocket.WebSocket()
89
- ws.connect(url, timeout=10)
120
+ last_error: Optional[Exception] = None
121
+ ws = None
122
+ for attempt in range(CONNECT_ATTEMPTS):
123
+ ws = websocket.WebSocket()
124
+ try:
125
+ _connect_websocket(ws, url, timeout=10)
126
+ last_error = None
127
+ break
128
+ except Exception as exc:
129
+ last_error = exc
130
+ try:
131
+ ws.close()
132
+ except Exception:
133
+ pass
134
+ if attempt < CONNECT_ATTEMPTS - 1:
135
+ time.sleep(0.25 * (attempt + 1))
136
+
137
+ if last_error is not None:
138
+ raise last_error
139
+ if ws is None:
140
+ raise PmxtError("WebSocket connection failed")
141
+ ws.settimeout(None)
90
142
  self._ws = ws
91
143
  self._closed = False
92
144
 
@@ -117,10 +169,9 @@ class SidecarWsClient:
117
169
  with self._lock:
118
170
  for request_id, sub in list(self._subscriptions.items()):
119
171
  if not sub.event.is_set():
120
- self._data_store[request_id] = {
172
+ self._enqueue_data_locked(request_id, {
121
173
  "_error": {"message": error_msg}
122
- }
123
- sub.event.set()
174
+ })
124
175
 
125
176
  def _dispatch(self, msg: Dict[str, Any]) -> None:
126
177
  """Route an incoming server message to the matching subscription."""
@@ -129,10 +180,10 @@ class SidecarWsClient:
129
180
 
130
181
  if event_type == "error":
131
182
  # Wake up any waiter with the error stored
132
- if request_id and request_id in self._subscriptions:
133
- sub = self._subscriptions[request_id]
134
- self._data_store[request_id] = {"_error": msg.get("error", {})}
135
- sub.event.set()
183
+ if request_id:
184
+ with self._lock:
185
+ self._data_store[request_id] = {"_error": msg.get("error", {})}
186
+ self._enqueue_data_locked(request_id, {"_error": msg.get("error", {})})
136
187
  return
137
188
 
138
189
  if event_type == "subscribed":
@@ -142,14 +193,62 @@ class SidecarWsClient:
142
193
  if event_type == "data" and request_id:
143
194
  symbol = msg.get("symbol", "")
144
195
  data = msg.get("data", {})
145
- # Store keyed by (request_id, symbol) for batch methods
146
- store_key = f"{request_id}:{symbol}"
147
- self._data_store[store_key] = data
148
- # Also store by request_id alone for single-symbol methods
149
- self._data_store[request_id] = data
150
- if request_id in self._subscriptions:
151
- sub = self._subscriptions[request_id]
152
- sub.event.set()
196
+ with self._lock:
197
+ # Store keyed by (request_id, symbol) for batch methods
198
+ store_key = f"{request_id}:{symbol}"
199
+ self._data_store[store_key] = data
200
+ self._enqueue_data_locked(request_id, data)
201
+
202
+ def _enqueue_data_locked(self, request_id: str, data: Dict[str, Any]) -> None:
203
+ queue = self._data_queues.setdefault(request_id, deque())
204
+ queue.append(data)
205
+ while len(queue) > MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION:
206
+ queue.popleft()
207
+
208
+ sub = self._subscriptions.get(request_id)
209
+ if sub:
210
+ sub.event.set()
211
+
212
+ def _pop_data_locked(self, request_id: str) -> Any:
213
+ queue = self._data_queues.get(request_id)
214
+ if not queue:
215
+ return _NO_DATA
216
+
217
+ data = queue.popleft()
218
+ if not queue:
219
+ self._data_queues.pop(request_id, None)
220
+ sub = self._subscriptions.get(request_id)
221
+ if sub:
222
+ sub.event.clear()
223
+ return data
224
+
225
+ def _wait_for_subscription_data(
226
+ self,
227
+ sub: _WsSubscription,
228
+ timeout: float,
229
+ ) -> Dict[str, Any]:
230
+ with self._lock:
231
+ data = self._pop_data_locked(sub.request_id)
232
+ if data is _NO_DATA:
233
+ sub.event.clear()
234
+
235
+ if data is _NO_DATA:
236
+ if not sub.event.wait(timeout=timeout):
237
+ raise PmxtError(f"Timeout waiting for WebSocket data (method={sub.method})")
238
+
239
+ with self._lock:
240
+ data = self._pop_data_locked(sub.request_id)
241
+
242
+ if data is _NO_DATA:
243
+ return {}
244
+
245
+ if isinstance(data, dict) and "_error" in data:
246
+ err = data["_error"]
247
+ raise PmxtError(
248
+ err.get("message", "WebSocket subscription error")
249
+ )
250
+
251
+ return data
153
252
 
154
253
  # ------------------------------------------------------------------
155
254
  # Public API
@@ -181,10 +280,6 @@ class SidecarWsClient:
181
280
  existing_id = self._active_subs.get(sub_key)
182
281
  if existing_id and existing_id in self._subscriptions:
183
282
  sub = self._subscriptions[existing_id]
184
- # Clear previous event so we wait for the NEXT push
185
- sub.event.clear()
186
- # Clear stale data
187
- self._data_store.pop(existing_id, None)
188
283
  else:
189
284
  self._ensure_connected()
190
285
  request_id = f"req-{uuid.uuid4().hex[:12]}"
@@ -207,19 +302,7 @@ class SidecarWsClient:
207
302
 
208
303
  self._ws.send(json.dumps(message))
209
304
 
210
- # Block until data arrives
211
- if not sub.event.wait(timeout=timeout):
212
- raise PmxtError(f"Timeout waiting for WebSocket data (method={method})")
213
-
214
- # Check for error
215
- error_data = self._data_store.get(sub.request_id, {})
216
- if isinstance(error_data, dict) and "_error" in error_data:
217
- err = error_data["_error"]
218
- raise PmxtError(
219
- err.get("message", "WebSocket subscription error")
220
- )
221
-
222
- return self._data_store.get(sub.request_id, {})
305
+ return self._wait_for_subscription_data(sub, timeout)
223
306
 
224
307
  def subscribe_batch(
225
308
  self,
@@ -257,16 +340,7 @@ class SidecarWsClient:
257
340
 
258
341
  # Wait for data event (the server may push one consolidated event
259
342
  # or multiple per-symbol events)
260
- if not sub.event.wait(timeout=timeout):
261
- raise PmxtError(f"Timeout waiting for WebSocket data (method={method})")
262
-
263
- # Check for error
264
- error_data = self._data_store.get(request_id, {})
265
- if isinstance(error_data, dict) and "_error" in error_data:
266
- err = error_data["_error"]
267
- raise PmxtError(
268
- err.get("message", "WebSocket subscription error")
269
- )
343
+ first_data = self._wait_for_subscription_data(sub, timeout)
270
344
 
271
345
  # Collect per-symbol data
272
346
  result: Dict[str, Any] = {}
@@ -277,9 +351,7 @@ class SidecarWsClient:
277
351
  # If no per-symbol data found, return the single data event
278
352
  # (server may return a dict of all order books in one push)
279
353
  if not result:
280
- data = self._data_store.get(request_id, {})
281
- if isinstance(data, dict) and not data.get("_error"):
282
- result = data
354
+ result = first_data
283
355
  return result
284
356
 
285
357
  def close(self) -> None:
@@ -292,6 +364,9 @@ class SidecarWsClient:
292
364
  import logging
293
365
  logging.warning("WebSocket close error: %s", e)
294
366
  self._ws = None
367
+ with self._lock:
368
+ self._data_queues.clear()
369
+ self._data_store.clear()
295
370
 
296
371
  @property
297
372
  def connected(self) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmxt
3
- Version: 2.46.2
3
+ Version: 2.46.4
4
4
  Summary: Unified prediction market data API - The ccxt for prediction markets
5
5
  Author: PMXT Contributors
6
6
  License: MIT
@@ -135,4 +135,5 @@ pmxt/_server/server/bundled.js
135
135
  tests/test_converters.py
136
136
  tests/test_public_exports.py
137
137
  tests/test_sdk_integration.py
138
- tests/test_streaming_ws_transport.py
138
+ tests/test_streaming_ws_transport.py
139
+ tests/test_ws_client.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pmxt"
7
- version = "2.46.2"
7
+ version = "2.46.4"
8
8
  description = "Unified prediction market data API - The ccxt for prediction markets"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -138,6 +138,7 @@ def test_unwatch_order_book_sends_websocket_unsubscribe(monkeypatch):
138
138
  self._ws = FakeSocket()
139
139
  self._active_subs = {"watchOrderBook:outcome-1": "req-existing"}
140
140
  self._subscriptions = {"req-existing": object()}
141
+ self._data_queues = {"req-existing": [_raw_order_book()]}
141
142
  self._data_store = {"req-existing": _raw_order_book()}
142
143
 
143
144
  def _ensure_connected(self):
@@ -160,4 +161,5 @@ def test_unwatch_order_book_sends_websocket_unsubscribe(monkeypatch):
160
161
  assert fake_ws._ws.sent[0]["args"] == ["outcome-1"]
161
162
  assert "watchOrderBook:outcome-1" not in fake_ws._active_subs
162
163
  assert "req-existing" not in fake_ws._subscriptions
164
+ assert "req-existing" not in fake_ws._data_queues
163
165
  assert "req-existing" not in fake_ws._data_store
@@ -0,0 +1,135 @@
1
+ import threading
2
+ import time
3
+ import socket
4
+ import sys
5
+ import types
6
+
7
+ import pmxt.ws_client as ws_client
8
+ from pmxt.ws_client import SidecarWsClient, _WsSubscription, _connect_websocket
9
+
10
+
11
+ def _register_subscription(client, request_id="req-firehose"):
12
+ sub = _WsSubscription(request_id, "watchAllOrderBooks", [])
13
+ with client._lock:
14
+ client._subscriptions[request_id] = sub
15
+ client._active_subs["watchAllOrderBooks:"] = request_id
16
+ return sub
17
+
18
+
19
+ def test_queues_repeated_data_events_in_fifo_order():
20
+ client = SidecarWsClient("http://localhost:3847")
21
+ _register_subscription(client)
22
+
23
+ client._dispatch({"event": "data", "id": "req-firehose", "symbol": "a", "data": {"sequence": 1}})
24
+ client._dispatch({"event": "data", "id": "req-firehose", "symbol": "b", "data": {"sequence": 2}})
25
+ client._dispatch({"event": "data", "id": "req-firehose", "symbol": "c", "data": {"sequence": 3}})
26
+
27
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 1}
28
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 2}
29
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 3}
30
+
31
+
32
+ def test_resolves_pending_waiter_and_queues_later_events():
33
+ client = SidecarWsClient("http://localhost:3847")
34
+ _register_subscription(client, "req-pending")
35
+ result = []
36
+
37
+ thread = threading.Thread(
38
+ target=lambda: result.append(client.subscribe("mock", "watchAllOrderBooks", [], timeout=1.0))
39
+ )
40
+ thread.start()
41
+ time.sleep(0.05)
42
+
43
+ client._dispatch({"event": "data", "id": "req-pending", "symbol": "a", "data": {"sequence": 1}})
44
+ client._dispatch({"event": "data", "id": "req-pending", "symbol": "b", "data": {"sequence": 2}})
45
+
46
+ thread.join(timeout=1.0)
47
+ assert result == [{"sequence": 1}]
48
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 2}
49
+
50
+
51
+ def test_close_clears_queued_events():
52
+ client = SidecarWsClient("http://localhost:3847")
53
+ _register_subscription(client, "req-close")
54
+
55
+ client._dispatch({"event": "data", "id": "req-close", "symbol": "a", "data": {"sequence": 1}})
56
+ client._dispatch({"event": "data", "id": "req-close", "symbol": "b", "data": {"sequence": 2}})
57
+
58
+ assert len(client._data_queues["req-close"]) == 2
59
+ client.close()
60
+ assert "req-close" not in client._data_queues
61
+ assert client._data_store == {}
62
+
63
+
64
+ def test_drops_oldest_queued_events_after_cap():
65
+ client = SidecarWsClient("http://localhost:3847")
66
+ _register_subscription(client, "req-overflow")
67
+
68
+ for sequence in range(1, 100_002):
69
+ client._dispatch({
70
+ "event": "data",
71
+ "id": "req-overflow",
72
+ "symbol": str(sequence),
73
+ "data": {"sequence": sequence},
74
+ })
75
+
76
+ assert len(client._data_queues["req-overflow"]) == 100_000
77
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 2}
78
+ assert client.subscribe("mock", "watchAllOrderBooks", [], timeout=0.1) == {"sequence": 3}
79
+
80
+
81
+ def test_hosted_websocket_connect_prefers_ipv4(monkeypatch):
82
+ original = socket.getaddrinfo
83
+
84
+ def fake_getaddrinfo(*_args, **_kwargs):
85
+ return [
86
+ (socket.AF_INET6, None, None, None, ("::1", 443, 0, 0)),
87
+ (socket.AF_INET, None, None, None, ("127.0.0.1", 443)),
88
+ ]
89
+
90
+ observed = {}
91
+
92
+ class FakeWebSocket:
93
+ def connect(self, url, timeout):
94
+ observed["url"] = url
95
+ observed["timeout"] = timeout
96
+ observed["families"] = [item[0] for item in socket.getaddrinfo("api.pmxt.dev", 443)]
97
+
98
+ monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
99
+
100
+ _connect_websocket(FakeWebSocket(), "wss://api.pmxt.dev/ws?apiKey=test", timeout=10)
101
+
102
+ assert observed["url"] == "wss://api.pmxt.dev/ws?apiKey=test"
103
+ assert observed["timeout"] == 10
104
+ assert observed["families"] == [socket.AF_INET, socket.AF_INET6]
105
+ assert socket.getaddrinfo is fake_getaddrinfo
106
+ monkeypatch.setattr(socket, "getaddrinfo", original)
107
+
108
+
109
+ def test_connect_retries_transient_handshake_failures(monkeypatch):
110
+ attempts = {"count": 0}
111
+
112
+ class FakeWebSocket:
113
+ def settimeout(self, _timeout):
114
+ return None
115
+
116
+ def close(self):
117
+ return None
118
+
119
+ monkeypatch.setitem(sys.modules, "websocket", types.SimpleNamespace(WebSocket=FakeWebSocket))
120
+
121
+ def fake_connect(_ws, _url, timeout):
122
+ assert timeout == 10
123
+ attempts["count"] += 1
124
+ if attempts["count"] < 3:
125
+ raise OSError("transient handshake failure")
126
+
127
+ monkeypatch.setattr(ws_client, "_connect_websocket", fake_connect)
128
+ monkeypatch.setattr(ws_client.time, "sleep", lambda _seconds: None)
129
+
130
+ client = SidecarWsClient("https://api.pmxt.dev", api_key="pmxt_test")
131
+ with client._lock:
132
+ client._ensure_connected()
133
+
134
+ assert attempts["count"] == 3
135
+ assert client._ws is not None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes