sharpapi 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.
- sharpapi/__init__.py +93 -0
- sharpapi/_base.py +121 -0
- sharpapi/_utils.py +39 -0
- sharpapi/async_client.py +464 -0
- sharpapi/client.py +682 -0
- sharpapi/exceptions.py +52 -0
- sharpapi/models.py +433 -0
- sharpapi/py.typed +0 -0
- sharpapi/streaming.py +200 -0
- sharpapi-0.1.0.dist-info/METADATA +203 -0
- sharpapi-0.1.0.dist-info/RECORD +12 -0
- sharpapi-0.1.0.dist-info/WHEEL +4 -0
sharpapi/client.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""SharpAPI synchronous Python client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._base import (
|
|
10
|
+
DEFAULT_BASE_URL,
|
|
11
|
+
DEFAULT_TIMEOUT,
|
|
12
|
+
handle_errors,
|
|
13
|
+
make_headers,
|
|
14
|
+
parse_rate_limit,
|
|
15
|
+
parse_response,
|
|
16
|
+
)
|
|
17
|
+
from ._utils import _clean_params
|
|
18
|
+
from .models import (
|
|
19
|
+
APIResponse,
|
|
20
|
+
AccountInfo,
|
|
21
|
+
ArbitrageOpportunity,
|
|
22
|
+
EVOpportunity,
|
|
23
|
+
Event,
|
|
24
|
+
League,
|
|
25
|
+
LowHoldOpportunity,
|
|
26
|
+
MiddleOpportunity,
|
|
27
|
+
OddsLine,
|
|
28
|
+
RateLimitInfo,
|
|
29
|
+
Sportsbook,
|
|
30
|
+
Sport,
|
|
31
|
+
)
|
|
32
|
+
from .streaming import EventStream
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SharpAPI:
|
|
36
|
+
"""SharpAPI Python client.
|
|
37
|
+
|
|
38
|
+
Provides typed access to odds, +EV, arbitrage, middles, and streaming
|
|
39
|
+
endpoints.
|
|
40
|
+
|
|
41
|
+
Example::
|
|
42
|
+
|
|
43
|
+
from sharpapi import SharpAPI
|
|
44
|
+
|
|
45
|
+
client = SharpAPI("sk_live_xxx")
|
|
46
|
+
|
|
47
|
+
# Get arbitrage opportunities
|
|
48
|
+
arbs = client.arbitrage.get(min_profit=1.0)
|
|
49
|
+
for arb in arbs.data:
|
|
50
|
+
print(f"{arb.profit_percent}% — {arb.event_name}")
|
|
51
|
+
|
|
52
|
+
# Get +EV opportunities
|
|
53
|
+
evs = client.ev.get(min_ev=3.0, league="nba")
|
|
54
|
+
for opp in evs.data:
|
|
55
|
+
print(f"+{opp.ev_percent}% on {opp.selection} @ {opp.sportsbook}")
|
|
56
|
+
|
|
57
|
+
# Stream real-time updates
|
|
58
|
+
stream = client.stream.opportunities(league="nba")
|
|
59
|
+
for event_type, data in stream.iter_events():
|
|
60
|
+
print(event_type, data)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: str,
|
|
66
|
+
*,
|
|
67
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
68
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
69
|
+
):
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise ValueError("api_key is required")
|
|
72
|
+
|
|
73
|
+
self._api_key = api_key
|
|
74
|
+
self._base_url = base_url.rstrip("/")
|
|
75
|
+
self._timeout = timeout
|
|
76
|
+
self._http = httpx.Client(
|
|
77
|
+
base_url=f"{self._base_url}/api/v1",
|
|
78
|
+
headers=make_headers(api_key),
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
self._last_rate_limit = RateLimitInfo()
|
|
82
|
+
|
|
83
|
+
# Resource namespaces
|
|
84
|
+
self.odds = _OddsResource(self)
|
|
85
|
+
self.ev = _EVResource(self)
|
|
86
|
+
self.arbitrage = _ArbitrageResource(self)
|
|
87
|
+
self.middles = _MiddlesResource(self)
|
|
88
|
+
self.low_hold = _LowHoldResource(self)
|
|
89
|
+
self.sports = _SportsResource(self)
|
|
90
|
+
self.leagues = _LeaguesResource(self)
|
|
91
|
+
self.sportsbooks = _SportsbooksResource(self)
|
|
92
|
+
self.events = _EventsResource(self)
|
|
93
|
+
self.account = _AccountResource(self)
|
|
94
|
+
self.stream = _StreamResource(self)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def rate_limit(self) -> RateLimitInfo:
|
|
98
|
+
"""Rate limit info from the last request."""
|
|
99
|
+
return self._last_rate_limit
|
|
100
|
+
|
|
101
|
+
def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
|
|
102
|
+
"""Make an API request and return parsed JSON."""
|
|
103
|
+
if params:
|
|
104
|
+
params = _clean_params(params)
|
|
105
|
+
|
|
106
|
+
response = self._http.request(method, path, params=params, **kwargs)
|
|
107
|
+
self._last_rate_limit = parse_rate_limit(response)
|
|
108
|
+
handle_errors(response)
|
|
109
|
+
return response.json()
|
|
110
|
+
|
|
111
|
+
def _get(self, path: str, params: dict | None = None) -> Any:
|
|
112
|
+
return self._request("GET", path, params)
|
|
113
|
+
|
|
114
|
+
def _post(self, path: str, json_body: Any = None, params: dict | None = None) -> Any:
|
|
115
|
+
return self._request("POST", path, params, json=json_body)
|
|
116
|
+
|
|
117
|
+
def close(self) -> None:
|
|
118
|
+
"""Close the HTTP client."""
|
|
119
|
+
self._http.close()
|
|
120
|
+
|
|
121
|
+
def __enter__(self):
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, *args):
|
|
125
|
+
self.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# =============================================================================
|
|
129
|
+
# Resource Namespaces
|
|
130
|
+
# =============================================================================
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _OddsResource:
|
|
134
|
+
"""Access odds data."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, client: SharpAPI):
|
|
137
|
+
self._client = client
|
|
138
|
+
|
|
139
|
+
def get(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
143
|
+
add_sportsbook: Optional[Union[str, list[str]]] = None,
|
|
144
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
145
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
146
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
147
|
+
event: Optional[Union[str, list[str]]] = None,
|
|
148
|
+
live: Optional[bool] = None,
|
|
149
|
+
sort: Optional[str] = None,
|
|
150
|
+
group_by: Optional[str] = None,
|
|
151
|
+
fields: Optional[Union[str, list[str]]] = None,
|
|
152
|
+
limit: Optional[int] = None,
|
|
153
|
+
offset: Optional[int] = None,
|
|
154
|
+
) -> APIResponse[list[OddsLine]]:
|
|
155
|
+
"""Get current odds snapshot.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
sportsbook: Filter by sportsbook(s).
|
|
159
|
+
add_sportsbook: Add sportsbook(s) beyond tier defaults.
|
|
160
|
+
sport: Filter by sport(s).
|
|
161
|
+
league: Filter by league(s).
|
|
162
|
+
market: Filter by market type(s).
|
|
163
|
+
event: Filter by event ID(s).
|
|
164
|
+
live: Filter by live status.
|
|
165
|
+
sort: Sort field (prefix with - for descending).
|
|
166
|
+
group_by: Group results (e.g. "event").
|
|
167
|
+
fields: Cherry-pick response fields.
|
|
168
|
+
limit: Max results (1-500, default 50).
|
|
169
|
+
offset: Pagination offset.
|
|
170
|
+
"""
|
|
171
|
+
data = self._client._get("/odds", {
|
|
172
|
+
"sportsbook": sportsbook,
|
|
173
|
+
"add_sportsbook": add_sportsbook,
|
|
174
|
+
"sport": sport,
|
|
175
|
+
"league": league,
|
|
176
|
+
"market": market,
|
|
177
|
+
"event": event,
|
|
178
|
+
"live": live,
|
|
179
|
+
"sort": sort,
|
|
180
|
+
"group_by": group_by,
|
|
181
|
+
"fields": fields,
|
|
182
|
+
"limit": limit,
|
|
183
|
+
"offset": offset,
|
|
184
|
+
})
|
|
185
|
+
return _parse_response(data, OddsLine)
|
|
186
|
+
|
|
187
|
+
def best(
|
|
188
|
+
self,
|
|
189
|
+
*,
|
|
190
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
191
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
192
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
193
|
+
event: Optional[Union[str, list[str]]] = None,
|
|
194
|
+
live: Optional[bool] = None,
|
|
195
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
196
|
+
add_sportsbook: Optional[Union[str, list[str]]] = None,
|
|
197
|
+
limit: Optional[int] = None,
|
|
198
|
+
offset: Optional[int] = None,
|
|
199
|
+
) -> APIResponse[list[OddsLine]]:
|
|
200
|
+
"""Get best odds per selection across all sportsbooks."""
|
|
201
|
+
data = self._client._get("/odds/best", {
|
|
202
|
+
"sport": sport,
|
|
203
|
+
"league": league,
|
|
204
|
+
"market": market,
|
|
205
|
+
"event": event,
|
|
206
|
+
"live": live,
|
|
207
|
+
"sportsbook": sportsbook,
|
|
208
|
+
"add_sportsbook": add_sportsbook,
|
|
209
|
+
"limit": limit,
|
|
210
|
+
"offset": offset,
|
|
211
|
+
})
|
|
212
|
+
return _parse_response(data, OddsLine)
|
|
213
|
+
|
|
214
|
+
def comparison(
|
|
215
|
+
self,
|
|
216
|
+
event_id: str,
|
|
217
|
+
*,
|
|
218
|
+
market: Optional[str] = None,
|
|
219
|
+
) -> APIResponse[list[OddsLine]]:
|
|
220
|
+
"""Get side-by-side odds comparison for an event."""
|
|
221
|
+
data = self._client._get("/odds/comparison", {
|
|
222
|
+
"event_id": event_id,
|
|
223
|
+
"market": market,
|
|
224
|
+
})
|
|
225
|
+
return _parse_response(data, OddsLine)
|
|
226
|
+
|
|
227
|
+
def batch(self, event_ids: list[str]) -> APIResponse[list[OddsLine]]:
|
|
228
|
+
"""Batch odds lookup for multiple events."""
|
|
229
|
+
data = self._client._post("/odds/batch", {"event_ids": event_ids})
|
|
230
|
+
return _parse_response(data, OddsLine)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class _EVResource:
|
|
234
|
+
"""Access +EV opportunities."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, client: SharpAPI):
|
|
237
|
+
self._client = client
|
|
238
|
+
|
|
239
|
+
def get(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
243
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
244
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
245
|
+
add_sportsbook: Optional[Union[str, list[str]]] = None,
|
|
246
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
247
|
+
min_ev: Optional[float] = None,
|
|
248
|
+
max_ev: Optional[float] = None,
|
|
249
|
+
min_market_width: Optional[float] = None,
|
|
250
|
+
max_market_width: Optional[float] = None,
|
|
251
|
+
max_odds_age: Optional[int] = None,
|
|
252
|
+
date_range: Optional[str] = None,
|
|
253
|
+
live: Optional[bool] = None,
|
|
254
|
+
sort: Optional[str] = None,
|
|
255
|
+
limit: Optional[int] = None,
|
|
256
|
+
offset: Optional[int] = None,
|
|
257
|
+
) -> APIResponse[list[EVOpportunity]]:
|
|
258
|
+
"""Get +EV opportunities.
|
|
259
|
+
|
|
260
|
+
Requires Pro tier or higher.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
sport: Filter by sport(s).
|
|
264
|
+
league: Filter by league(s).
|
|
265
|
+
sportsbook: Filter by sportsbook(s).
|
|
266
|
+
add_sportsbook: Add sportsbook(s) beyond tier defaults.
|
|
267
|
+
market: Filter by market type(s).
|
|
268
|
+
min_ev: Minimum EV percentage (default: server-side 0).
|
|
269
|
+
max_ev: Maximum EV percentage.
|
|
270
|
+
min_market_width: Minimum market width.
|
|
271
|
+
max_market_width: Maximum market width.
|
|
272
|
+
max_odds_age: Max age of underlying odds in seconds.
|
|
273
|
+
date_range: "today", "tomorrow", or "week".
|
|
274
|
+
live: Filter live/prematch.
|
|
275
|
+
sort: Sort field ("-ev" default, "confidence", "kelly", "time", "book_count").
|
|
276
|
+
limit: Max results (1-500, default 50).
|
|
277
|
+
offset: Pagination offset.
|
|
278
|
+
"""
|
|
279
|
+
data = self._client._get("/opportunities/ev", {
|
|
280
|
+
"sport": sport,
|
|
281
|
+
"league": league,
|
|
282
|
+
"sportsbook": sportsbook,
|
|
283
|
+
"add_sportsbook": add_sportsbook,
|
|
284
|
+
"market": market,
|
|
285
|
+
"min_ev": min_ev,
|
|
286
|
+
"max_ev": max_ev,
|
|
287
|
+
"min_market_width": min_market_width,
|
|
288
|
+
"max_market_width": max_market_width,
|
|
289
|
+
"max_odds_age": max_odds_age,
|
|
290
|
+
"date_range": date_range,
|
|
291
|
+
"live": live,
|
|
292
|
+
"sort": sort,
|
|
293
|
+
"limit": limit,
|
|
294
|
+
"offset": offset,
|
|
295
|
+
})
|
|
296
|
+
return _parse_response(data, EVOpportunity)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _ArbitrageResource:
|
|
300
|
+
"""Access arbitrage opportunities."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, client: SharpAPI):
|
|
303
|
+
self._client = client
|
|
304
|
+
|
|
305
|
+
def get(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
309
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
310
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
311
|
+
add_sportsbook: Optional[Union[str, list[str]]] = None,
|
|
312
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
313
|
+
min_profit: Optional[float] = None,
|
|
314
|
+
max_odds_age: Optional[int] = None,
|
|
315
|
+
live: Optional[bool] = None,
|
|
316
|
+
sort: Optional[str] = None,
|
|
317
|
+
group: Optional[str] = None,
|
|
318
|
+
limit: Optional[int] = None,
|
|
319
|
+
offset: Optional[int] = None,
|
|
320
|
+
) -> APIResponse[list[ArbitrageOpportunity]]:
|
|
321
|
+
"""Get arbitrage opportunities.
|
|
322
|
+
|
|
323
|
+
Requires Hobby tier or higher.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
sport: Filter by sport(s).
|
|
327
|
+
league: Filter by league(s).
|
|
328
|
+
sportsbook: Filter by sportsbook(s).
|
|
329
|
+
add_sportsbook: Add sportsbook(s) beyond tier defaults.
|
|
330
|
+
market: Filter by market type(s).
|
|
331
|
+
min_profit: Minimum profit percentage (default: 0.5).
|
|
332
|
+
max_odds_age: Max age of odds in seconds.
|
|
333
|
+
live: Filter live/prematch.
|
|
334
|
+
sort: Sort field ("-profit" default, "time", "sport", "market").
|
|
335
|
+
group: "best" = highest-profit per event+market.
|
|
336
|
+
limit: Max results (1-500, default 50).
|
|
337
|
+
offset: Pagination offset.
|
|
338
|
+
"""
|
|
339
|
+
data = self._client._get("/opportunities/arbitrage", {
|
|
340
|
+
"sport": sport,
|
|
341
|
+
"league": league,
|
|
342
|
+
"sportsbook": sportsbook,
|
|
343
|
+
"add_sportsbook": add_sportsbook,
|
|
344
|
+
"market": market,
|
|
345
|
+
"min_profit": min_profit,
|
|
346
|
+
"max_odds_age": max_odds_age,
|
|
347
|
+
"live": live,
|
|
348
|
+
"sort": sort,
|
|
349
|
+
"group": group,
|
|
350
|
+
"limit": limit,
|
|
351
|
+
"offset": offset,
|
|
352
|
+
})
|
|
353
|
+
return _parse_response(data, ArbitrageOpportunity)
|
|
354
|
+
|
|
355
|
+
def csv(
|
|
356
|
+
self,
|
|
357
|
+
*,
|
|
358
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
359
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
360
|
+
min_profit: Optional[float] = None,
|
|
361
|
+
limit: Optional[int] = None,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Get arbitrage opportunities as CSV text."""
|
|
364
|
+
data = self._client._get("/opportunities/arbitrage", {
|
|
365
|
+
"sport": sport,
|
|
366
|
+
"league": league,
|
|
367
|
+
"min_profit": min_profit,
|
|
368
|
+
"limit": limit,
|
|
369
|
+
"format": "csv",
|
|
370
|
+
})
|
|
371
|
+
# CSV format returns raw text, but our _get parses JSON.
|
|
372
|
+
# The server returns JSON-wrapped CSV or raw text.
|
|
373
|
+
if isinstance(data, dict) and "data" in data:
|
|
374
|
+
return data["data"]
|
|
375
|
+
return str(data)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class _MiddlesResource:
|
|
379
|
+
"""Access middle opportunities."""
|
|
380
|
+
|
|
381
|
+
def __init__(self, client: SharpAPI):
|
|
382
|
+
self._client = client
|
|
383
|
+
|
|
384
|
+
def get(
|
|
385
|
+
self,
|
|
386
|
+
*,
|
|
387
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
388
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
389
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
390
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
391
|
+
min_size: Optional[float] = None,
|
|
392
|
+
max_odds_age: Optional[int] = None,
|
|
393
|
+
live: Optional[bool] = None,
|
|
394
|
+
state: Optional[str] = None,
|
|
395
|
+
sort: Optional[str] = None,
|
|
396
|
+
limit: Optional[int] = None,
|
|
397
|
+
offset: Optional[int] = None,
|
|
398
|
+
) -> APIResponse[list[MiddleOpportunity]]:
|
|
399
|
+
"""Get middle opportunities.
|
|
400
|
+
|
|
401
|
+
Requires Pro tier or higher.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
sport: Filter by sport(s).
|
|
405
|
+
league: Filter by league(s).
|
|
406
|
+
sportsbook: Filter by sportsbook(s).
|
|
407
|
+
market: Filter by market type(s) ("point_spread", "total_points").
|
|
408
|
+
min_size: Minimum middle size in points (default: 0.5).
|
|
409
|
+
max_odds_age: Max age of odds in seconds.
|
|
410
|
+
live: Filter live/prematch.
|
|
411
|
+
state: US state for deep links (default: "pa").
|
|
412
|
+
sort: Sort field ("quality" default, "ev", "probability", "middle_size").
|
|
413
|
+
limit: Max results (1-500, default 50).
|
|
414
|
+
offset: Pagination offset.
|
|
415
|
+
"""
|
|
416
|
+
data = self._client._get("/opportunities/middles", {
|
|
417
|
+
"sport": sport,
|
|
418
|
+
"league": league,
|
|
419
|
+
"sportsbook": sportsbook,
|
|
420
|
+
"market": market,
|
|
421
|
+
"min_size": min_size,
|
|
422
|
+
"max_odds_age": max_odds_age,
|
|
423
|
+
"live": live,
|
|
424
|
+
"state": state,
|
|
425
|
+
"sort": sort,
|
|
426
|
+
"limit": limit,
|
|
427
|
+
"offset": offset,
|
|
428
|
+
})
|
|
429
|
+
return _parse_response(data, MiddleOpportunity)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class _LowHoldResource:
|
|
433
|
+
"""Access low-hold (low vig) opportunities."""
|
|
434
|
+
|
|
435
|
+
def __init__(self, client: SharpAPI):
|
|
436
|
+
self._client = client
|
|
437
|
+
|
|
438
|
+
def get(
|
|
439
|
+
self,
|
|
440
|
+
*,
|
|
441
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
442
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
443
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
444
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
445
|
+
max_hold: Optional[float] = None,
|
|
446
|
+
live: Optional[bool] = None,
|
|
447
|
+
state: Optional[str] = None,
|
|
448
|
+
sort: Optional[str] = None,
|
|
449
|
+
limit: Optional[int] = None,
|
|
450
|
+
offset: Optional[int] = None,
|
|
451
|
+
) -> APIResponse[list[LowHoldOpportunity]]:
|
|
452
|
+
"""Get low-hold opportunities.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
sport: Filter by sport(s).
|
|
456
|
+
league: Filter by league(s).
|
|
457
|
+
sportsbook: Filter by sportsbook(s).
|
|
458
|
+
market: Filter by market type(s).
|
|
459
|
+
max_hold: Maximum hold/vig percentage (default: 5.0).
|
|
460
|
+
live: Filter live/prematch.
|
|
461
|
+
state: US state for deep links.
|
|
462
|
+
sort: Sort field ("hold" default, "market", "sport").
|
|
463
|
+
limit: Max results (1-500, default 50).
|
|
464
|
+
offset: Pagination offset.
|
|
465
|
+
"""
|
|
466
|
+
data = self._client._get("/opportunities/low_hold", {
|
|
467
|
+
"sport": sport,
|
|
468
|
+
"league": league,
|
|
469
|
+
"sportsbook": sportsbook,
|
|
470
|
+
"market": market,
|
|
471
|
+
"max_hold": max_hold,
|
|
472
|
+
"live": live,
|
|
473
|
+
"state": state,
|
|
474
|
+
"sort": sort,
|
|
475
|
+
"limit": limit,
|
|
476
|
+
"offset": offset,
|
|
477
|
+
})
|
|
478
|
+
return _parse_response(data, LowHoldOpportunity)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class _SportsResource:
|
|
482
|
+
def __init__(self, client: SharpAPI):
|
|
483
|
+
self._client = client
|
|
484
|
+
|
|
485
|
+
def list(self) -> APIResponse[list[Sport]]:
|
|
486
|
+
"""List all available sports."""
|
|
487
|
+
data = self._client._get("/sports")
|
|
488
|
+
return _parse_response(data, Sport)
|
|
489
|
+
|
|
490
|
+
def get(self, sport_id: str) -> Sport:
|
|
491
|
+
"""Get a specific sport."""
|
|
492
|
+
data = self._client._get(f"/sports/{sport_id}")
|
|
493
|
+
raw = data.get("data", data)
|
|
494
|
+
return Sport.model_validate(raw)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class _LeaguesResource:
|
|
498
|
+
def __init__(self, client: SharpAPI):
|
|
499
|
+
self._client = client
|
|
500
|
+
|
|
501
|
+
def list(self, *, sport: Optional[str] = None) -> APIResponse[list[League]]:
|
|
502
|
+
"""List all leagues, optionally filtered by sport."""
|
|
503
|
+
data = self._client._get("/leagues", {"sport": sport})
|
|
504
|
+
return _parse_response(data, League)
|
|
505
|
+
|
|
506
|
+
def get(self, league_id: str) -> League:
|
|
507
|
+
"""Get a specific league."""
|
|
508
|
+
data = self._client._get(f"/leagues/{league_id}")
|
|
509
|
+
raw = data.get("data", data)
|
|
510
|
+
return League.model_validate(raw)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class _SportsbooksResource:
|
|
514
|
+
def __init__(self, client: SharpAPI):
|
|
515
|
+
self._client = client
|
|
516
|
+
|
|
517
|
+
def list(self) -> APIResponse[list[Sportsbook]]:
|
|
518
|
+
"""List all active sportsbooks."""
|
|
519
|
+
data = self._client._get("/sportsbooks")
|
|
520
|
+
return _parse_response(data, Sportsbook)
|
|
521
|
+
|
|
522
|
+
def get(self, book_id: str) -> Sportsbook:
|
|
523
|
+
"""Get a specific sportsbook."""
|
|
524
|
+
data = self._client._get(f"/sportsbooks/{book_id}")
|
|
525
|
+
raw = data.get("data", data)
|
|
526
|
+
return Sportsbook.model_validate(raw)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class _EventsResource:
|
|
530
|
+
def __init__(self, client: SharpAPI):
|
|
531
|
+
self._client = client
|
|
532
|
+
|
|
533
|
+
def list(
|
|
534
|
+
self,
|
|
535
|
+
*,
|
|
536
|
+
sport: Optional[str] = None,
|
|
537
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
538
|
+
live: Optional[bool] = None,
|
|
539
|
+
limit: Optional[int] = None,
|
|
540
|
+
offset: Optional[int] = None,
|
|
541
|
+
) -> APIResponse[list[Event]]:
|
|
542
|
+
"""List events."""
|
|
543
|
+
data = self._client._get("/events", {
|
|
544
|
+
"sport": sport,
|
|
545
|
+
"league": league,
|
|
546
|
+
"live": live,
|
|
547
|
+
"limit": limit,
|
|
548
|
+
"offset": offset,
|
|
549
|
+
})
|
|
550
|
+
return _parse_response(data, Event)
|
|
551
|
+
|
|
552
|
+
def get(self, event_id: str) -> Event:
|
|
553
|
+
"""Get a specific event."""
|
|
554
|
+
data = self._client._get(f"/events/{event_id}")
|
|
555
|
+
raw = data.get("data", data)
|
|
556
|
+
return Event.model_validate(raw)
|
|
557
|
+
|
|
558
|
+
def search(self, query: str) -> APIResponse[list[Event]]:
|
|
559
|
+
"""Search events by name."""
|
|
560
|
+
data = self._client._get("/events/search", {"q": query})
|
|
561
|
+
return _parse_response(data, Event)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class _AccountResource:
|
|
565
|
+
def __init__(self, client: SharpAPI):
|
|
566
|
+
self._client = client
|
|
567
|
+
|
|
568
|
+
def me(self) -> AccountInfo:
|
|
569
|
+
"""Get current account info (tier, limits, features)."""
|
|
570
|
+
data = self._client._get("/account")
|
|
571
|
+
raw = data.get("data", data)
|
|
572
|
+
return AccountInfo.model_validate(raw)
|
|
573
|
+
|
|
574
|
+
def usage(self) -> dict:
|
|
575
|
+
"""Get current usage stats."""
|
|
576
|
+
data = self._client._get("/account/usage")
|
|
577
|
+
return data.get("data", data)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class _StreamResource:
|
|
581
|
+
"""Build SSE stream connections."""
|
|
582
|
+
|
|
583
|
+
def __init__(self, client: SharpAPI):
|
|
584
|
+
self._client = client
|
|
585
|
+
|
|
586
|
+
def _build_stream(self, path: str, params: dict | None = None) -> EventStream:
|
|
587
|
+
cleaned = _clean_params(params or {})
|
|
588
|
+
cleaned["api_key"] = self._client._api_key
|
|
589
|
+
query = "&".join(f"{k}={v}" for k, v in cleaned.items())
|
|
590
|
+
url = f"{self._client._base_url}/api/v1{path}?{query}"
|
|
591
|
+
return EventStream(
|
|
592
|
+
url=url,
|
|
593
|
+
headers={"X-API-Key": self._client._api_key},
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
def odds(
|
|
597
|
+
self,
|
|
598
|
+
*,
|
|
599
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
600
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
601
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
602
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
603
|
+
) -> EventStream:
|
|
604
|
+
"""Stream real-time odds updates.
|
|
605
|
+
|
|
606
|
+
Requires WebSocket add-on or Enterprise tier.
|
|
607
|
+
|
|
608
|
+
Returns an EventStream. Use .connect() to block or .iter_events()
|
|
609
|
+
to iterate.
|
|
610
|
+
"""
|
|
611
|
+
return self._build_stream("/stream", {
|
|
612
|
+
"channel": "odds",
|
|
613
|
+
"sportsbook": sportsbook,
|
|
614
|
+
"league": league,
|
|
615
|
+
"sport": sport,
|
|
616
|
+
"market": market,
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
def opportunities(
|
|
620
|
+
self,
|
|
621
|
+
*,
|
|
622
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
623
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
624
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
625
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
626
|
+
min_ev: Optional[float] = None,
|
|
627
|
+
min_profit: Optional[float] = None,
|
|
628
|
+
) -> EventStream:
|
|
629
|
+
"""Stream real-time opportunity alerts (EV, arb, middles).
|
|
630
|
+
|
|
631
|
+
Requires WebSocket add-on or Enterprise tier.
|
|
632
|
+
"""
|
|
633
|
+
return self._build_stream("/stream", {
|
|
634
|
+
"channel": "opportunities",
|
|
635
|
+
"sportsbook": sportsbook,
|
|
636
|
+
"league": league,
|
|
637
|
+
"sport": sport,
|
|
638
|
+
"market": market,
|
|
639
|
+
"min_ev": min_ev,
|
|
640
|
+
"min_profit": min_profit,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
def all(
|
|
644
|
+
self,
|
|
645
|
+
*,
|
|
646
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
647
|
+
league: Optional[Union[str, list[str]]] = None,
|
|
648
|
+
sport: Optional[Union[str, list[str]]] = None,
|
|
649
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
650
|
+
) -> EventStream:
|
|
651
|
+
"""Stream all data (odds + opportunities).
|
|
652
|
+
|
|
653
|
+
Requires WebSocket add-on or Enterprise tier.
|
|
654
|
+
"""
|
|
655
|
+
return self._build_stream("/stream", {
|
|
656
|
+
"channel": "all",
|
|
657
|
+
"sportsbook": sportsbook,
|
|
658
|
+
"league": league,
|
|
659
|
+
"sport": sport,
|
|
660
|
+
"market": market,
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
def event(
|
|
664
|
+
self,
|
|
665
|
+
event_id: str,
|
|
666
|
+
*,
|
|
667
|
+
sportsbook: Optional[Union[str, list[str]]] = None,
|
|
668
|
+
market: Optional[Union[str, list[str]]] = None,
|
|
669
|
+
) -> EventStream:
|
|
670
|
+
"""Stream updates for a single event."""
|
|
671
|
+
return self._build_stream(f"/stream/events/{event_id}", {
|
|
672
|
+
"sportsbook": sportsbook,
|
|
673
|
+
"market": market,
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# =============================================================================
|
|
678
|
+
# Helpers
|
|
679
|
+
# =============================================================================
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
_parse_response = parse_response
|