pmxt 1.0.0__py3-none-any.whl

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 (56) hide show
  1. pmxt/__init__.py +58 -0
  2. pmxt/client.py +713 -0
  3. pmxt/models.py +296 -0
  4. pmxt/server_manager.py +242 -0
  5. pmxt-1.0.0.dist-info/METADATA +250 -0
  6. pmxt-1.0.0.dist-info/RECORD +56 -0
  7. pmxt-1.0.0.dist-info/WHEEL +5 -0
  8. pmxt-1.0.0.dist-info/top_level.txt +2 -0
  9. pmxt_internal/__init__.py +124 -0
  10. pmxt_internal/api/__init__.py +5 -0
  11. pmxt_internal/api/default_api.py +3722 -0
  12. pmxt_internal/api_client.py +804 -0
  13. pmxt_internal/api_response.py +21 -0
  14. pmxt_internal/configuration.py +578 -0
  15. pmxt_internal/exceptions.py +219 -0
  16. pmxt_internal/models/__init__.py +54 -0
  17. pmxt_internal/models/balance.py +93 -0
  18. pmxt_internal/models/base_request.py +91 -0
  19. pmxt_internal/models/base_response.py +93 -0
  20. pmxt_internal/models/cancel_order_request.py +94 -0
  21. pmxt_internal/models/create_order200_response.py +99 -0
  22. pmxt_internal/models/create_order_params.py +111 -0
  23. pmxt_internal/models/create_order_request.py +102 -0
  24. pmxt_internal/models/error_detail.py +87 -0
  25. pmxt_internal/models/error_response.py +93 -0
  26. pmxt_internal/models/exchange_credentials.py +93 -0
  27. pmxt_internal/models/fetch_balance200_response.py +103 -0
  28. pmxt_internal/models/fetch_markets200_response.py +103 -0
  29. pmxt_internal/models/fetch_markets_request.py +102 -0
  30. pmxt_internal/models/fetch_ohlcv200_response.py +103 -0
  31. pmxt_internal/models/fetch_ohlcv_request.py +102 -0
  32. pmxt_internal/models/fetch_ohlcv_request_args_inner.py +140 -0
  33. pmxt_internal/models/fetch_open_orders200_response.py +103 -0
  34. pmxt_internal/models/fetch_open_orders_request.py +94 -0
  35. pmxt_internal/models/fetch_order_book200_response.py +99 -0
  36. pmxt_internal/models/fetch_order_book_request.py +94 -0
  37. pmxt_internal/models/fetch_positions200_response.py +103 -0
  38. pmxt_internal/models/fetch_positions_request.py +94 -0
  39. pmxt_internal/models/fetch_trades200_response.py +103 -0
  40. pmxt_internal/models/fetch_trades_request.py +102 -0
  41. pmxt_internal/models/get_markets_by_slug_request.py +94 -0
  42. pmxt_internal/models/health_check200_response.py +89 -0
  43. pmxt_internal/models/history_filter_params.py +101 -0
  44. pmxt_internal/models/market_filter_params.py +113 -0
  45. pmxt_internal/models/market_outcome.py +95 -0
  46. pmxt_internal/models/order.py +139 -0
  47. pmxt_internal/models/order_book.py +106 -0
  48. pmxt_internal/models/order_level.py +89 -0
  49. pmxt_internal/models/position.py +101 -0
  50. pmxt_internal/models/price_candle.py +97 -0
  51. pmxt_internal/models/search_markets_request.py +102 -0
  52. pmxt_internal/models/search_markets_request_args_inner.py +140 -0
  53. pmxt_internal/models/trade.py +105 -0
  54. pmxt_internal/models/unified_market.py +120 -0
  55. pmxt_internal/py.typed +0 -0
  56. pmxt_internal/rest.py +263 -0
pmxt/models.py ADDED
@@ -0,0 +1,296 @@
1
+ """
2
+ Data models for PMXT.
3
+
4
+ These are clean Pythonic wrappers around the auto-generated OpenAPI models.
5
+ """
6
+
7
+ from typing import List, Optional, Dict, Any, Literal
8
+ from datetime import datetime
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class MarketOutcome:
14
+ """A single tradeable outcome within a market."""
15
+
16
+ id: str
17
+ """Outcome ID. Use this for fetchOHLCV/fetchOrderBook/fetchTrades.
18
+ - Polymarket: CLOB Token ID
19
+ - Kalshi: Market Ticker
20
+ """
21
+
22
+ label: str
23
+ """Human-readable label (e.g., "Trump", "Yes")"""
24
+
25
+ price: float
26
+ """Current price (0.0 to 1.0, representing probability)"""
27
+
28
+ price_change_24h: Optional[float] = None
29
+ """24-hour price change"""
30
+
31
+ metadata: Optional[Dict[str, Any]] = None
32
+ """Exchange-specific metadata"""
33
+
34
+
35
+ @dataclass
36
+ class UnifiedMarket:
37
+ """A unified market representation across exchanges."""
38
+
39
+ id: str
40
+ """Market ID"""
41
+
42
+ title: str
43
+ """Market title"""
44
+
45
+ outcomes: List[MarketOutcome]
46
+ """All tradeable outcomes"""
47
+
48
+ volume_24h: float
49
+ """24-hour trading volume (USD)"""
50
+
51
+ liquidity: float
52
+ """Current liquidity (USD)"""
53
+
54
+ url: str
55
+ """Direct URL to the market"""
56
+
57
+ description: Optional[str] = None
58
+ """Market description"""
59
+
60
+ resolution_date: Optional[datetime] = None
61
+ """Expected resolution date"""
62
+
63
+ volume: Optional[float] = None
64
+ """Total volume (USD)"""
65
+
66
+ open_interest: Optional[float] = None
67
+ """Open interest (USD)"""
68
+
69
+ image: Optional[str] = None
70
+ """Market image URL"""
71
+
72
+ category: Optional[str] = None
73
+ """Market category"""
74
+
75
+ tags: Optional[List[str]] = None
76
+ """Market tags"""
77
+
78
+
79
+ @dataclass
80
+ class PriceCandle:
81
+ """OHLCV price candle."""
82
+
83
+ timestamp: int
84
+ """Unix timestamp (milliseconds)"""
85
+
86
+ open: float
87
+ """Opening price (0.0 to 1.0)"""
88
+
89
+ high: float
90
+ """Highest price (0.0 to 1.0)"""
91
+
92
+ low: float
93
+ """Lowest price (0.0 to 1.0)"""
94
+
95
+ close: float
96
+ """Closing price (0.0 to 1.0)"""
97
+
98
+ volume: Optional[float] = None
99
+ """Trading volume"""
100
+
101
+
102
+ @dataclass
103
+ class OrderLevel:
104
+ """A single price level in the order book."""
105
+
106
+ price: float
107
+ """Price (0.0 to 1.0)"""
108
+
109
+ size: float
110
+ """Number of contracts"""
111
+
112
+
113
+ @dataclass
114
+ class OrderBook:
115
+ """Current order book for an outcome."""
116
+
117
+ bids: List[OrderLevel]
118
+ """Bid orders (sorted high to low)"""
119
+
120
+ asks: List[OrderLevel]
121
+ """Ask orders (sorted low to high)"""
122
+
123
+ timestamp: Optional[int] = None
124
+ """Unix timestamp (milliseconds)"""
125
+
126
+
127
+ @dataclass
128
+ class Trade:
129
+ """A historical trade."""
130
+
131
+ id: str
132
+ """Trade ID"""
133
+
134
+ timestamp: int
135
+ """Unix timestamp (milliseconds)"""
136
+
137
+ price: float
138
+ """Trade price (0.0 to 1.0)"""
139
+
140
+ amount: float
141
+ """Trade amount (contracts)"""
142
+
143
+ side: Literal["buy", "sell", "unknown"]
144
+ """Trade side"""
145
+
146
+
147
+ @dataclass
148
+ class Order:
149
+ """An order (open, filled, or cancelled)."""
150
+
151
+ id: str
152
+ """Order ID"""
153
+
154
+ market_id: str
155
+ """Market ID"""
156
+
157
+ outcome_id: str
158
+ """Outcome ID"""
159
+
160
+ side: Literal["buy", "sell"]
161
+ """Order side"""
162
+
163
+ type: Literal["market", "limit"]
164
+ """Order type"""
165
+
166
+ amount: float
167
+ """Order amount (contracts)"""
168
+
169
+ status: str
170
+ """Order status (pending, open, filled, cancelled, rejected)"""
171
+
172
+ filled: float
173
+ """Amount filled"""
174
+
175
+ remaining: float
176
+ """Amount remaining"""
177
+
178
+ timestamp: int
179
+ """Unix timestamp (milliseconds)"""
180
+
181
+ price: Optional[float] = None
182
+ """Limit price (for limit orders)"""
183
+
184
+ fee: Optional[float] = None
185
+ """Trading fee"""
186
+
187
+
188
+ @dataclass
189
+ class Position:
190
+ """A current position in a market."""
191
+
192
+ market_id: str
193
+ """Market ID"""
194
+
195
+ outcome_id: str
196
+ """Outcome ID"""
197
+
198
+ outcome_label: str
199
+ """Outcome label"""
200
+
201
+ size: float
202
+ """Position size (positive for long, negative for short)"""
203
+
204
+ entry_price: float
205
+ """Average entry price"""
206
+
207
+ current_price: float
208
+ """Current market price"""
209
+
210
+ unrealized_pnl: float
211
+ """Unrealized profit/loss"""
212
+
213
+ realized_pnl: Optional[float] = None
214
+ """Realized profit/loss"""
215
+
216
+
217
+ @dataclass
218
+ class Balance:
219
+ """Account balance."""
220
+
221
+ currency: str
222
+ """Currency (e.g., "USDC")"""
223
+
224
+ total: float
225
+ """Total balance"""
226
+
227
+ available: float
228
+ """Available for trading"""
229
+
230
+ locked: float
231
+ """Locked in open orders"""
232
+
233
+
234
+ # Parameter types
235
+ CandleInterval = Literal["1m", "5m", "15m", "1h", "6h", "1d"]
236
+ SortOption = Literal["volume", "liquidity", "newest"]
237
+ SearchIn = Literal["title", "description", "both"]
238
+ OrderSide = Literal["buy", "sell"]
239
+ OrderType = Literal["market", "limit"]
240
+
241
+
242
+ @dataclass
243
+ class MarketFilterParams:
244
+ """Parameters for filtering markets."""
245
+
246
+ limit: Optional[int] = None
247
+ """Maximum number of results"""
248
+
249
+ offset: Optional[int] = None
250
+ """Pagination offset"""
251
+
252
+ sort: Optional[SortOption] = None
253
+ """Sort order"""
254
+
255
+ search_in: Optional[SearchIn] = None
256
+ """Where to search (for searchMarkets)"""
257
+
258
+
259
+ @dataclass
260
+ class HistoryFilterParams:
261
+ """Parameters for fetching historical data."""
262
+
263
+ resolution: CandleInterval
264
+ """Candle resolution"""
265
+
266
+ start: Optional[datetime] = None
267
+ """Start time"""
268
+
269
+ end: Optional[datetime] = None
270
+ """End time"""
271
+
272
+ limit: Optional[int] = None
273
+ """Maximum number of results"""
274
+
275
+
276
+ @dataclass
277
+ class CreateOrderParams:
278
+ """Parameters for creating an order."""
279
+
280
+ market_id: str
281
+ """Market ID"""
282
+
283
+ outcome_id: str
284
+ """Outcome ID"""
285
+
286
+ side: OrderSide
287
+ """Order side (buy/sell)"""
288
+
289
+ type: OrderType
290
+ """Order type (market/limit)"""
291
+
292
+ amount: float
293
+ """Number of contracts"""
294
+
295
+ price: Optional[float] = None
296
+ """Limit price (required for limit orders, 0.0-1.0)"""
pmxt/server_manager.py ADDED
@@ -0,0 +1,242 @@
1
+ """
2
+ Server Manager for PMXT Python SDK
3
+
4
+ This module handles automatic server lifecycle management.
5
+ The pattern implemented here is universal and can be replicated in any language SDK.
6
+
7
+ Universal Pattern:
8
+ 1. Check if server is running (via lock file + process check)
9
+ 2. If not running, call pmxt-ensure-server launcher
10
+ 3. Wait for health check to confirm server is ready
11
+ 4. Proceed with API calls
12
+
13
+ This ensures zero-configuration usage across all SDKs.
14
+ """
15
+
16
+ import os
17
+ import json
18
+ import time
19
+ import subprocess
20
+ import shutil
21
+ from pathlib import Path
22
+ from typing import Optional, Dict, Any
23
+ import urllib.request
24
+ import urllib.error
25
+
26
+
27
+ class ServerManager:
28
+ """
29
+ Manages the PMXT sidecar server lifecycle.
30
+
31
+ This class implements the universal server management pattern that
32
+ should be replicated in all language SDKs (Java, C#, Go, etc.)
33
+ """
34
+
35
+ DEFAULT_PORT = 3847
36
+ HEALTH_CHECK_TIMEOUT = 10 # seconds
37
+ HEALTH_CHECK_INTERVAL = 0.1 # seconds
38
+
39
+ def __init__(self, base_url: str = "http://localhost:3847"):
40
+ """
41
+ Initialize the server manager.
42
+
43
+ Args:
44
+ base_url: Base URL where server should be running
45
+ """
46
+ self.base_url = base_url
47
+ self.lock_path = Path.home() / '.pmxt' / 'server.lock'
48
+ self._port = self._extract_port_from_url(base_url)
49
+
50
+ def _extract_port_from_url(self, url: str) -> int:
51
+ """Extract port number from URL."""
52
+ try:
53
+ from urllib.parse import urlparse
54
+ parsed = urlparse(url)
55
+ return parsed.port or self.DEFAULT_PORT
56
+ except:
57
+ return self.DEFAULT_PORT
58
+
59
+ def ensure_server_running(self) -> None:
60
+ """
61
+ Ensure the PMXT server is running.
62
+
63
+ This is the main entry point that SDKs should call.
64
+ It implements the universal pattern:
65
+ 1. Check if server is alive
66
+ 2. If not, start it via launcher
67
+ 3. Wait for health check
68
+
69
+ Raises:
70
+ Exception: If server fails to start or become healthy
71
+ """
72
+ # Step 1: Check if server is already running
73
+ if self.is_server_alive():
74
+ return
75
+
76
+ # Step 2: Start server via launcher
77
+ self._start_server_via_launcher()
78
+
79
+ # Step 3: Wait for health check
80
+ self._wait_for_health()
81
+
82
+ def is_server_alive(self) -> bool:
83
+ """
84
+ Check if the server is currently running and healthy.
85
+
86
+ This implements the universal alive check:
87
+ 1. Read lock file
88
+ 2. Check if process exists
89
+ 3. Optionally verify health endpoint
90
+
91
+ Returns:
92
+ True if server is running and healthy, False otherwise
93
+ """
94
+ # Check lock file exists
95
+ if not self.lock_path.exists():
96
+ return False
97
+
98
+ try:
99
+ # Read lock file
100
+ lock_data = json.loads(self.lock_path.read_text())
101
+ pid = lock_data.get('pid')
102
+ port = lock_data.get('port', self.DEFAULT_PORT)
103
+
104
+ if not pid:
105
+ return False
106
+
107
+ # Check if process exists (cross-platform)
108
+ if not self._is_process_running(pid):
109
+ # Process doesn't exist, remove stale lock file
110
+ self._remove_stale_lock()
111
+ return False
112
+
113
+ # Quick health check to verify server is responsive
114
+ try:
115
+ return self._check_health(port, timeout=1)
116
+ except:
117
+ # Process exists but not responding
118
+ return False
119
+
120
+ except (json.JSONDecodeError, OSError):
121
+ return False
122
+
123
+ def _is_process_running(self, pid: int) -> bool:
124
+ """
125
+ Check if a process with given PID is running.
126
+
127
+ Cross-platform implementation.
128
+ """
129
+ try:
130
+ # Signal 0 doesn't kill the process, just checks if it exists
131
+ os.kill(pid, 0)
132
+ return True
133
+ except (OSError, ProcessLookupError):
134
+ return False
135
+
136
+ def _remove_stale_lock(self) -> None:
137
+ """Remove stale lock file."""
138
+ try:
139
+ self.lock_path.unlink()
140
+ except:
141
+ pass
142
+
143
+ def _start_server_via_launcher(self) -> None:
144
+ """
145
+ Start the server using the pmxt-ensure-server launcher.
146
+ """
147
+ # 1. Check for local development paths first
148
+ current_file = Path(__file__).resolve()
149
+ # Look for ../../bin/pmxt-ensure-server (monorepo structure)
150
+ local_launcher = current_file.parent.parent.parent / 'bin' / 'pmxt-ensure-server'
151
+
152
+ launcher = str(local_launcher) if local_launcher.exists() else shutil.which('pmxt-ensure-server')
153
+
154
+ if not launcher:
155
+ raise Exception(
156
+ "pmxt-ensure-server not found.\n"
157
+ "Local search failed and not in PATH.\n"
158
+ "Please install the server: npm install -g pmxtjs"
159
+ )
160
+
161
+ # Call the launcher
162
+ try:
163
+ # If it's a JS file and we are calling it directly, might need node
164
+ cmd = [launcher]
165
+ if launcher.endswith('.js') or not os.access(launcher, os.X_OK):
166
+ cmd = ['node', launcher]
167
+
168
+ result = subprocess.run(
169
+ cmd,
170
+ capture_output=True,
171
+ text=True,
172
+ timeout=self.HEALTH_CHECK_TIMEOUT
173
+ )
174
+
175
+ if result.returncode != 0:
176
+ raise Exception(
177
+ f"Failed to start server: {result.stderr or result.stdout}"
178
+ )
179
+ except subprocess.TimeoutExpired:
180
+ raise Exception("Server startup timeout")
181
+ except Exception as e:
182
+ raise Exception(f"Failed to start server: {e}")
183
+
184
+ def _wait_for_health(self) -> None:
185
+ """
186
+ Wait for the server to respond to health checks.
187
+
188
+ Universal pattern: Poll /health endpoint until it responds or timeout.
189
+ """
190
+ start_time = time.time()
191
+
192
+ while time.time() - start_time < self.HEALTH_CHECK_TIMEOUT:
193
+ try:
194
+ if self._check_health(self._port):
195
+ return
196
+ except:
197
+ pass
198
+
199
+ time.sleep(self.HEALTH_CHECK_INTERVAL)
200
+
201
+ raise Exception(
202
+ f"Server failed to become healthy within {self.HEALTH_CHECK_TIMEOUT}s"
203
+ )
204
+
205
+ def _check_health(self, port: int, timeout: int = 2) -> bool:
206
+ """
207
+ Check if server is healthy by calling /health endpoint.
208
+
209
+ Args:
210
+ port: Port to check
211
+ timeout: Request timeout in seconds
212
+
213
+ Returns:
214
+ True if server responds with 200 OK
215
+ """
216
+ try:
217
+ url = f"http://localhost:{port}/health"
218
+ req = urllib.request.Request(url)
219
+
220
+ with urllib.request.urlopen(req, timeout=timeout) as response:
221
+ if response.status == 200:
222
+ data = json.loads(response.read().decode())
223
+ return data.get('status') == 'ok'
224
+
225
+ return False
226
+ except (urllib.error.URLError, urllib.error.HTTPError, Exception):
227
+ return False
228
+
229
+ def get_server_info(self) -> Optional[Dict[str, Any]]:
230
+ """
231
+ Get information about the running server from lock file.
232
+
233
+ Returns:
234
+ Dictionary with server info (port, pid, timestamp) or None
235
+ """
236
+ if not self.lock_path.exists():
237
+ return None
238
+
239
+ try:
240
+ return json.loads(self.lock_path.read_text())
241
+ except:
242
+ return None