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 +35 -0
- problee/api/__init__.py +13 -0
- problee/api/markets.py +119 -0
- problee/api/positions.py +49 -0
- problee/api/quotes.py +85 -0
- problee/client.py +255 -0
- problee/exceptions.py +84 -0
- problee/models/__init__.py +16 -0
- problee/models/market.py +116 -0
- problee/models/position.py +51 -0
- problee/models/quote.py +65 -0
- problee/streaming/__init__.py +11 -0
- problee/streaming/sse.py +112 -0
- problee/streaming/websocket.py +198 -0
- problee-0.1.0.dist-info/LICENSE +21 -0
- problee-0.1.0.dist-info/METADATA +209 -0
- problee-0.1.0.dist-info/RECORD +19 -0
- problee-0.1.0.dist-info/WHEEL +5 -0
- problee-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
]
|
problee/api/__init__.py
ADDED
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)
|
problee/api/positions.py
ADDED
|
@@ -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
|
+
]
|