problee 0.1.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.
problee/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """
2
+ Problee Python SDK
3
+
4
+ Official Python client for the Problee prediction market API.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from .client import ProbClient
10
+ from .models.market import Market, MarketStatus, MarketCategory
11
+ from .models.position import Position
12
+ from .models.quote import Quote
13
+ from .exceptions import (
14
+ ProbError,
15
+ AuthenticationError,
16
+ RateLimitError,
17
+ NotFoundError,
18
+ ValidationError,
19
+ APIError,
20
+ )
21
+
22
+ __all__ = [
23
+ "ProbClient",
24
+ "Market",
25
+ "MarketStatus",
26
+ "MarketCategory",
27
+ "Position",
28
+ "Quote",
29
+ "ProbError",
30
+ "AuthenticationError",
31
+ "RateLimitError",
32
+ "NotFoundError",
33
+ "ValidationError",
34
+ "APIError",
35
+ ]
@@ -0,0 +1,13 @@
1
+ """
2
+ Problee API Modules
3
+ """
4
+
5
+ from .markets import MarketsAPI
6
+ from .positions import PositionsAPI
7
+ from .quotes import QuotesAPI
8
+
9
+ __all__ = [
10
+ "MarketsAPI",
11
+ "PositionsAPI",
12
+ "QuotesAPI",
13
+ ]
problee/api/markets.py ADDED
@@ -0,0 +1,119 @@
1
+ """
2
+ Markets API
3
+ """
4
+
5
+ from typing import Optional, List, TYPE_CHECKING
6
+ from ..models.market import Market, MarketListResponse, MarketStatus, MarketCategory
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import ProbClient
10
+
11
+
12
+ class MarketsAPI:
13
+ """Markets API methods."""
14
+
15
+ def __init__(self, client: "ProbClient"):
16
+ self._client = client
17
+
18
+ def list(
19
+ self,
20
+ status: Optional[str] = None,
21
+ category: Optional[str] = None,
22
+ sort: Optional[str] = None,
23
+ limit: int = 20,
24
+ cursor: Optional[str] = None,
25
+ ) -> MarketListResponse:
26
+ """
27
+ List markets with optional filters.
28
+
29
+ Args:
30
+ status: Filter by status (open, closed, resolved)
31
+ category: Filter by category (sports, politics, crypto, entertainment, other)
32
+ sort: Sort order (new, trending, volume_24h, closing_soon)
33
+ limit: Max results per page (default 20, max 100)
34
+ cursor: Pagination cursor
35
+
36
+ Returns:
37
+ MarketListResponse with list of markets and pagination info
38
+ """
39
+ params = {"limit": limit}
40
+ if status:
41
+ params["status"] = status
42
+ if category:
43
+ params["category"] = category
44
+ if sort:
45
+ params["sort"] = sort
46
+ if cursor:
47
+ params["cursor"] = cursor
48
+
49
+ response = self._client._request("GET", "/api/v1/markets", params=params)
50
+ return MarketListResponse.from_dict(response)
51
+
52
+ def get(self, market_id: str) -> Market:
53
+ """
54
+ Get a single market by ID (address).
55
+
56
+ Args:
57
+ market_id: Market address
58
+
59
+ Returns:
60
+ Market object
61
+ """
62
+ response = self._client._request("GET", f"/api/v1/markets/{market_id}")
63
+ return Market.from_dict(response)
64
+
65
+ def get_history(
66
+ self,
67
+ market_id: str,
68
+ interval: str = "1h",
69
+ limit: int = 100,
70
+ ) -> List[dict]:
71
+ """
72
+ Get price history for a market.
73
+
74
+ Args:
75
+ market_id: Market address
76
+ interval: Time bucket (1m, 5m, 1h, 1d)
77
+ limit: Max data points
78
+
79
+ Returns:
80
+ List of price history points
81
+ """
82
+ params = {"interval": interval, "limit": limit}
83
+ response = self._client._request(
84
+ "GET", f"/api/v1/markets/{market_id}/history", params=params
85
+ )
86
+ return response.get("history", [])
87
+
88
+ def get_trades(
89
+ self,
90
+ market_id: str,
91
+ limit: int = 50,
92
+ ) -> List[dict]:
93
+ """
94
+ Get recent trades for a market.
95
+
96
+ Args:
97
+ market_id: Market address
98
+ limit: Max trades to return
99
+
100
+ Returns:
101
+ List of trade objects
102
+ """
103
+ params = {"limit": limit}
104
+ response = self._client._request(
105
+ "GET", f"/api/v1/markets/{market_id}/trades", params=params
106
+ )
107
+ return response.get("trades", [])
108
+
109
+ def list_open(self, **kwargs) -> MarketListResponse:
110
+ """List open markets."""
111
+ return self.list(status="open", **kwargs)
112
+
113
+ def list_resolved(self, **kwargs) -> MarketListResponse:
114
+ """List resolved markets."""
115
+ return self.list(status="resolved", **kwargs)
116
+
117
+ def list_by_category(self, category: str, **kwargs) -> MarketListResponse:
118
+ """List markets by category."""
119
+ return self.list(category=category, **kwargs)
@@ -0,0 +1,49 @@
1
+ """
2
+ Positions API
3
+ """
4
+
5
+ from typing import Optional, TYPE_CHECKING
6
+ from ..models.position import Position, PositionListResponse
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import ProbClient
10
+
11
+
12
+ class PositionsAPI:
13
+ """Positions API methods."""
14
+
15
+ def __init__(self, client: "ProbClient"):
16
+ self._client = client
17
+
18
+ def list(self, address: str) -> PositionListResponse:
19
+ """
20
+ Get all positions for a wallet address.
21
+
22
+ Args:
23
+ address: Wallet address (0x...)
24
+
25
+ Returns:
26
+ PositionListResponse with list of positions
27
+ """
28
+ response = self._client._request("GET", f"/api/v1/positions/{address}")
29
+ return PositionListResponse.from_dict(response)
30
+
31
+ def get(self, address: str, market_id: str) -> Optional[Position]:
32
+ """
33
+ Get position for a specific market.
34
+
35
+ Args:
36
+ address: Wallet address
37
+ market_id: Market address
38
+
39
+ Returns:
40
+ Position or None if no position
41
+ """
42
+ params = {"market_id": market_id}
43
+ response = self._client._request(
44
+ "GET", f"/api/v1/positions/{address}", params=params
45
+ )
46
+ positions = response.get("positions", [])
47
+ if positions:
48
+ return Position.from_dict(positions[0])
49
+ return None
problee/api/quotes.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Quotes API
3
+ """
4
+
5
+ from typing import Optional, TYPE_CHECKING
6
+ from ..models.quote import Quote, TransactionData
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import ProbClient
10
+
11
+
12
+ class QuotesAPI:
13
+ """Quotes API methods."""
14
+
15
+ def __init__(self, client: "ProbClient"):
16
+ self._client = client
17
+
18
+ def get(
19
+ self,
20
+ market_id: str,
21
+ side: str,
22
+ outcome: str,
23
+ amount: str,
24
+ slippage_bps: int = 50,
25
+ ) -> Quote:
26
+ """
27
+ Get a quote for a trade.
28
+
29
+ Args:
30
+ market_id: Market address
31
+ side: "buy" or "sell"
32
+ outcome: "yes", "no", or "draw"
33
+ amount: Amount in wei (as string)
34
+ slippage_bps: Slippage tolerance in basis points (default 50 = 0.5%)
35
+
36
+ Returns:
37
+ Quote object with price and execution details
38
+ """
39
+ payload = {
40
+ "market_id": market_id,
41
+ "side": side,
42
+ "outcome": outcome,
43
+ "amount": amount,
44
+ "slippage_bps": slippage_bps,
45
+ }
46
+ response = self._client._request("POST", "/api/v1/quote", json=payload)
47
+ return Quote.from_dict(response)
48
+
49
+ def get_buy_yes(self, market_id: str, amount: str, **kwargs) -> Quote:
50
+ """Get quote to buy YES shares."""
51
+ return self.get(market_id, "buy", "yes", amount, **kwargs)
52
+
53
+ def get_buy_no(self, market_id: str, amount: str, **kwargs) -> Quote:
54
+ """Get quote to buy NO shares."""
55
+ return self.get(market_id, "buy", "no", amount, **kwargs)
56
+
57
+ def get_sell_yes(self, market_id: str, amount: str, **kwargs) -> Quote:
58
+ """Get quote to sell YES shares."""
59
+ return self.get(market_id, "sell", "yes", amount, **kwargs)
60
+
61
+ def get_sell_no(self, market_id: str, amount: str, **kwargs) -> Quote:
62
+ """Get quote to sell NO shares."""
63
+ return self.get(market_id, "sell", "no", amount, **kwargs)
64
+
65
+ def build_transaction(
66
+ self,
67
+ quote_id: str,
68
+ rpc_url: str,
69
+ ) -> TransactionData:
70
+ """
71
+ Build transaction data from a quote.
72
+
73
+ Args:
74
+ quote_id: Quote ID from get() response
75
+ rpc_url: RPC URL for the chain
76
+
77
+ Returns:
78
+ TransactionData with calldata for execution
79
+ """
80
+ payload = {"quote_id": quote_id}
81
+ headers = {"X-RPC-URL": rpc_url}
82
+ response = self._client._request(
83
+ "POST", "/api/v1/tx/build", json=payload, headers=headers
84
+ )
85
+ return TransactionData.from_dict(response)
problee/client.py ADDED
@@ -0,0 +1,255 @@
1
+ """
2
+ Problee API Client
3
+
4
+ The main client class for interacting with the Problee API.
5
+ """
6
+
7
+ import time
8
+ from typing import Optional, Dict, Any
9
+ from urllib.parse import urljoin
10
+
11
+ import requests
12
+
13
+ from .api.markets import MarketsAPI
14
+ from .api.positions import PositionsAPI
15
+ from .api.quotes import QuotesAPI
16
+ from .streaming.sse import SSEClient
17
+ from .streaming.websocket import WebSocketClient
18
+ from .exceptions import (
19
+ ProbError,
20
+ AuthenticationError,
21
+ RateLimitError,
22
+ NotFoundError,
23
+ ValidationError,
24
+ APIError,
25
+ )
26
+
27
+
28
+ class ProbClient:
29
+ """
30
+ Problee API Client.
31
+
32
+ Example:
33
+ ```python
34
+ from problee import ProbClient
35
+
36
+ # Initialize client
37
+ client = ProbClient(api_key="pk_live_...")
38
+
39
+ # List open markets
40
+ markets = client.markets.list(status="open")
41
+ for market in markets.markets:
42
+ print(f"{market.question}: YES={market.prices.yes:.2%}")
43
+
44
+ # Get a quote
45
+ quote = client.quotes.get(
46
+ market_id="0x...",
47
+ side="buy",
48
+ outcome="yes",
49
+ amount="1000000000000000000" # 1 USDC in wei
50
+ )
51
+ print(f"Price: {quote.price:.4f}, Shares: {quote.shares_out}")
52
+
53
+ # Check positions
54
+ positions = client.positions.list("0xYourWallet...")
55
+ ```
56
+
57
+ Attributes:
58
+ markets: Markets API
59
+ positions: Positions API
60
+ quotes: Quotes API
61
+ stream: SSE streaming client
62
+ ws: WebSocket client
63
+ """
64
+
65
+ DEFAULT_BASE_URL = "https://api.problee.com"
66
+ DEFAULT_TIMEOUT = 30
67
+
68
+ def __init__(
69
+ self,
70
+ api_key: Optional[str] = None,
71
+ base_url: Optional[str] = None,
72
+ timeout: int = DEFAULT_TIMEOUT,
73
+ max_retries: int = 3,
74
+ builder_id: Optional[str] = None,
75
+ ):
76
+ """
77
+ Initialize the Problee client.
78
+
79
+ Args:
80
+ api_key: API key (pk_live_... or pk_test_...)
81
+ base_url: API base URL (defaults to https://api.problee.com)
82
+ timeout: Request timeout in seconds
83
+ max_retries: Max retries for failed requests
84
+ builder_id: Optional builder ID for attribution
85
+ """
86
+ self._api_key = api_key
87
+ self._base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
88
+ self._timeout = timeout
89
+ self._max_retries = max_retries
90
+ self._builder_id = builder_id
91
+
92
+ # Initialize session
93
+ self._session = requests.Session()
94
+ self._session.headers.update({
95
+ "Content-Type": "application/json",
96
+ "Accept": "application/json",
97
+ "User-Agent": "problee-python/0.1.0",
98
+ })
99
+
100
+ if api_key:
101
+ self._session.headers["Authorization"] = f"Bearer {api_key}"
102
+
103
+ if builder_id:
104
+ self._session.headers["X-Builder-ID"] = builder_id
105
+
106
+ # Initialize API modules
107
+ self.markets = MarketsAPI(self)
108
+ self.positions = PositionsAPI(self)
109
+ self.quotes = QuotesAPI(self)
110
+
111
+ # Initialize streaming clients (lazy)
112
+ self._sse_client: Optional[SSEClient] = None
113
+ self._ws_client: Optional[WebSocketClient] = None
114
+
115
+ @property
116
+ def stream(self) -> SSEClient:
117
+ """Get SSE streaming client."""
118
+ if self._sse_client is None:
119
+ self._sse_client = SSEClient(self._base_url, self._api_key)
120
+ return self._sse_client
121
+
122
+ @property
123
+ def ws(self) -> WebSocketClient:
124
+ """Get WebSocket client."""
125
+ if self._ws_client is None:
126
+ self._ws_client = WebSocketClient(self._base_url, self._api_key)
127
+ return self._ws_client
128
+
129
+ def _request(
130
+ self,
131
+ method: str,
132
+ path: str,
133
+ params: Optional[Dict[str, Any]] = None,
134
+ json: Optional[Dict[str, Any]] = None,
135
+ headers: Optional[Dict[str, str]] = None,
136
+ ) -> Dict[str, Any]:
137
+ """
138
+ Make an API request.
139
+
140
+ Args:
141
+ method: HTTP method
142
+ path: API path
143
+ params: Query parameters
144
+ json: JSON body
145
+ headers: Additional headers
146
+
147
+ Returns:
148
+ Response JSON
149
+
150
+ Raises:
151
+ AuthenticationError: Invalid API key
152
+ RateLimitError: Rate limit exceeded
153
+ NotFoundError: Resource not found
154
+ ValidationError: Invalid request
155
+ APIError: Other API errors
156
+ """
157
+ url = urljoin(self._base_url, path)
158
+ request_headers = dict(self._session.headers)
159
+ if headers:
160
+ request_headers.update(headers)
161
+
162
+ last_exception = None
163
+
164
+ for attempt in range(self._max_retries):
165
+ try:
166
+ response = self._session.request(
167
+ method=method,
168
+ url=url,
169
+ params=params,
170
+ json=json,
171
+ headers=request_headers,
172
+ timeout=self._timeout,
173
+ )
174
+
175
+ # Handle successful response
176
+ if response.ok:
177
+ return response.json()
178
+
179
+ # Handle errors
180
+ self._handle_error(response)
181
+
182
+ except requests.exceptions.Timeout:
183
+ last_exception = APIError("Request timed out", code="TIMEOUT")
184
+ except requests.exceptions.ConnectionError:
185
+ last_exception = APIError("Connection failed", code="CONNECTION_ERROR")
186
+ except ProbError:
187
+ raise
188
+ except Exception as e:
189
+ last_exception = APIError(str(e))
190
+
191
+ # Retry with backoff
192
+ if attempt < self._max_retries - 1:
193
+ time.sleep(2 ** attempt)
194
+
195
+ if last_exception:
196
+ raise last_exception
197
+ raise APIError("Request failed after retries")
198
+
199
+ def _handle_error(self, response: requests.Response) -> None:
200
+ """Handle API error response."""
201
+ status_code = response.status_code
202
+
203
+ try:
204
+ data = response.json()
205
+ message = data.get("error", data.get("message", "Unknown error"))
206
+ code = data.get("code")
207
+ except Exception:
208
+ message = response.text or "Unknown error"
209
+ code = None
210
+
211
+ if status_code == 401:
212
+ raise AuthenticationError(message)
213
+ elif status_code == 429:
214
+ retry_after = response.headers.get("Retry-After")
215
+ raise RateLimitError(
216
+ message,
217
+ retry_after=int(retry_after) if retry_after else None,
218
+ )
219
+ elif status_code == 404:
220
+ raise NotFoundError(message)
221
+ elif status_code == 400:
222
+ errors = data.get("errors") if isinstance(data, dict) else None
223
+ raise ValidationError(message, errors=errors)
224
+ else:
225
+ raise APIError(message, code=code, status_code=status_code)
226
+
227
+ def get_rate_limit_info(self) -> Dict[str, Any]:
228
+ """
229
+ Get current rate limit status.
230
+
231
+ Returns:
232
+ Dict with rate limit headers from last request
233
+ """
234
+ # Make a lightweight request to get headers
235
+ response = self._session.get(
236
+ urljoin(self._base_url, "/healthz"),
237
+ timeout=self._timeout,
238
+ )
239
+ return {
240
+ "limit": response.headers.get("RateLimit-Limit"),
241
+ "remaining": response.headers.get("RateLimit-Remaining"),
242
+ "reset": response.headers.get("RateLimit-Reset"),
243
+ }
244
+
245
+ def close(self) -> None:
246
+ """Close the client and clean up resources."""
247
+ self._session.close()
248
+ if self._sse_client:
249
+ self._sse_client.close()
250
+
251
+ def __enter__(self) -> "ProbClient":
252
+ return self
253
+
254
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
255
+ self.close()
problee/exceptions.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ Problee SDK Exceptions
3
+
4
+ Custom exception classes for handling API errors.
5
+ """
6
+
7
+ from typing import Optional, Dict, Any
8
+
9
+
10
+ class ProbError(Exception):
11
+ """Base exception for all Problee SDK errors."""
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ code: Optional[str] = None,
17
+ status_code: Optional[int] = None,
18
+ response: Optional[Dict[str, Any]] = None,
19
+ ):
20
+ super().__init__(message)
21
+ self.message = message
22
+ self.code = code
23
+ self.status_code = status_code
24
+ self.response = response
25
+
26
+ def __str__(self) -> str:
27
+ if self.code:
28
+ return f"[{self.code}] {self.message}"
29
+ return self.message
30
+
31
+
32
+ class AuthenticationError(ProbError):
33
+ """Raised when API key is invalid or missing."""
34
+
35
+ def __init__(self, message: str = "Invalid or missing API key"):
36
+ super().__init__(message, code="AUTHENTICATION_ERROR", status_code=401)
37
+
38
+
39
+ class RateLimitError(ProbError):
40
+ """Raised when rate limit is exceeded."""
41
+
42
+ def __init__(
43
+ self,
44
+ message: str = "Rate limit exceeded",
45
+ retry_after: Optional[int] = None,
46
+ ):
47
+ super().__init__(message, code="RATE_LIMIT_EXCEEDED", status_code=429)
48
+ self.retry_after = retry_after
49
+
50
+
51
+ class NotFoundError(ProbError):
52
+ """Raised when a resource is not found."""
53
+
54
+ def __init__(self, message: str = "Resource not found", resource_type: Optional[str] = None):
55
+ super().__init__(message, code="NOT_FOUND", status_code=404)
56
+ self.resource_type = resource_type
57
+
58
+
59
+ class ValidationError(ProbError):
60
+ """Raised when request validation fails."""
61
+
62
+ def __init__(self, message: str, errors: Optional[list] = None):
63
+ super().__init__(message, code="VALIDATION_ERROR", status_code=400)
64
+ self.errors = errors or []
65
+
66
+
67
+ class APIError(ProbError):
68
+ """Raised for general API errors."""
69
+
70
+ pass
71
+
72
+
73
+ class QuoteExpiredError(ProbError):
74
+ """Raised when a quote has expired."""
75
+
76
+ def __init__(self, message: str = "Quote has expired"):
77
+ super().__init__(message, code="QUOTE_EXPIRED", status_code=400)
78
+
79
+
80
+ class InsufficientSharesError(ProbError):
81
+ """Raised when user doesn't have enough shares."""
82
+
83
+ def __init__(self, message: str = "Insufficient shares"):
84
+ super().__init__(message, code="INSUFFICIENT_SHARES", status_code=400)
@@ -0,0 +1,16 @@
1
+ """
2
+ Problee SDK Data Models
3
+ """
4
+
5
+ from .market import Market, MarketStatus, MarketCategory, MarketPrices
6
+ from .position import Position
7
+ from .quote import Quote
8
+
9
+ __all__ = [
10
+ "Market",
11
+ "MarketStatus",
12
+ "MarketCategory",
13
+ "MarketPrices",
14
+ "Position",
15
+ "Quote",
16
+ ]