sharpapi 0.2.2__py3-none-any.whl → 0.3.1__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 +25 -4
- sharpapi/_base.py +52 -5
- sharpapi/async_client.py +196 -82
- sharpapi/client.py +244 -101
- sharpapi/exceptions.py +143 -4
- sharpapi/models.py +359 -155
- sharpapi/streaming.py +45 -6
- {sharpapi-0.2.2.dist-info → sharpapi-0.3.1.dist-info}/METADATA +3 -2
- sharpapi-0.3.1.dist-info/RECORD +13 -0
- sharpapi-0.3.1.dist-info/licenses/LICENSE +21 -0
- sharpapi-0.2.2.dist-info/RECORD +0 -12
- {sharpapi-0.2.2.dist-info → sharpapi-0.3.1.dist-info}/WHEEL +0 -0
sharpapi/__init__.py
CHANGED
|
@@ -17,9 +17,12 @@ Example::
|
|
|
17
17
|
print(f"+{opp.ev_percentage}% on {opp.selection} @ {opp.sportsbook}")
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
20
21
|
from .async_client import AsyncSharpAPI
|
|
21
22
|
from .client import SharpAPI
|
|
22
23
|
from .exceptions import (
|
|
24
|
+
ERROR_CODE_DESCRIPTIONS,
|
|
25
|
+
ERROR_CODE_TO_EXCEPTION,
|
|
23
26
|
AuthenticationError,
|
|
24
27
|
RateLimitedError,
|
|
25
28
|
SharpAPIError,
|
|
@@ -28,16 +31,21 @@ from .exceptions import (
|
|
|
28
31
|
ValidationError,
|
|
29
32
|
)
|
|
30
33
|
from .models import (
|
|
31
|
-
APIResponse,
|
|
32
34
|
AccountInfo,
|
|
35
|
+
APIKey,
|
|
36
|
+
APIResponse,
|
|
33
37
|
ArbitrageLeg,
|
|
34
38
|
ArbitrageOpportunity,
|
|
35
|
-
|
|
39
|
+
ClosingOddsLine,
|
|
40
|
+
ClosingSnapshot,
|
|
41
|
+
EntityRef,
|
|
36
42
|
Event,
|
|
43
|
+
EVOpportunity,
|
|
37
44
|
GameState,
|
|
38
45
|
League,
|
|
39
46
|
LowHoldOpportunity,
|
|
40
47
|
LowHoldSide,
|
|
48
|
+
Market,
|
|
41
49
|
MiddleOpportunity,
|
|
42
50
|
MiddleSide,
|
|
43
51
|
OddsLine,
|
|
@@ -46,28 +54,35 @@ from .models import (
|
|
|
46
54
|
RateLimitInfo,
|
|
47
55
|
ResponseMeta,
|
|
48
56
|
Sport,
|
|
57
|
+
SportRef,
|
|
49
58
|
Sportsbook,
|
|
59
|
+
Team,
|
|
60
|
+
TeamRef,
|
|
50
61
|
)
|
|
51
62
|
from .streaming import EventStream
|
|
52
|
-
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
53
63
|
|
|
54
|
-
__version__ = "0.
|
|
64
|
+
__version__ = "0.3.1"
|
|
55
65
|
|
|
56
66
|
__all__ = [
|
|
57
67
|
# Clients
|
|
58
68
|
"SharpAPI",
|
|
59
69
|
"AsyncSharpAPI",
|
|
60
70
|
# Models
|
|
71
|
+
"APIKey",
|
|
61
72
|
"APIResponse",
|
|
62
73
|
"AccountInfo",
|
|
63
74
|
"ArbitrageLeg",
|
|
64
75
|
"ArbitrageOpportunity",
|
|
76
|
+
"ClosingOddsLine",
|
|
77
|
+
"ClosingSnapshot",
|
|
78
|
+
"EntityRef",
|
|
65
79
|
"EVOpportunity",
|
|
66
80
|
"Event",
|
|
67
81
|
"GameState",
|
|
68
82
|
"League",
|
|
69
83
|
"LowHoldOpportunity",
|
|
70
84
|
"LowHoldSide",
|
|
85
|
+
"Market",
|
|
71
86
|
"MiddleOpportunity",
|
|
72
87
|
"MiddleSide",
|
|
73
88
|
"OddsLine",
|
|
@@ -76,7 +91,10 @@ __all__ = [
|
|
|
76
91
|
"RateLimitInfo",
|
|
77
92
|
"ResponseMeta",
|
|
78
93
|
"Sport",
|
|
94
|
+
"SportRef",
|
|
79
95
|
"Sportsbook",
|
|
96
|
+
"Team",
|
|
97
|
+
"TeamRef",
|
|
80
98
|
# Streaming
|
|
81
99
|
"EventStream",
|
|
82
100
|
# Exceptions
|
|
@@ -86,6 +104,9 @@ __all__ = [
|
|
|
86
104
|
"StreamError",
|
|
87
105
|
"TierRestrictedError",
|
|
88
106
|
"ValidationError",
|
|
107
|
+
# Error-code registry
|
|
108
|
+
"ERROR_CODE_DESCRIPTIONS",
|
|
109
|
+
"ERROR_CODE_TO_EXCEPTION",
|
|
89
110
|
# Utilities
|
|
90
111
|
"american_to_decimal",
|
|
91
112
|
"american_to_probability",
|
sharpapi/_base.py
CHANGED
|
@@ -3,21 +3,29 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import random
|
|
6
|
+
from typing import Literal
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
|
|
9
10
|
from .exceptions import (
|
|
11
|
+
ERROR_CODE_TO_EXCEPTION,
|
|
10
12
|
AuthenticationError,
|
|
11
13
|
RateLimitedError,
|
|
12
14
|
SharpAPIError,
|
|
13
15
|
TierRestrictedError,
|
|
14
16
|
ValidationError,
|
|
17
|
+
canonical_code,
|
|
15
18
|
)
|
|
16
19
|
from .models import APIResponse, RateLimitInfo, ResponseMeta
|
|
17
20
|
|
|
18
21
|
DEFAULT_BASE_URL = "https://api.sharpapi.io"
|
|
19
22
|
DEFAULT_TIMEOUT = 30.0
|
|
20
|
-
USER_AGENT = "sharpapi-python/0.2.
|
|
23
|
+
USER_AGENT = "sharpapi-python/0.2.5"
|
|
24
|
+
|
|
25
|
+
# Supported REST authentication methods. SSE always uses ``?api_key=`` query
|
|
26
|
+
# regardless of this setting because EventSource cannot set custom headers.
|
|
27
|
+
AuthMethod = Literal["x-api-key", "bearer"]
|
|
28
|
+
DEFAULT_AUTH_METHOD: AuthMethod = "x-api-key"
|
|
21
29
|
|
|
22
30
|
RETRY_STATUSES = frozenset({502, 503, 504})
|
|
23
31
|
RETRY_MAX_ATTEMPTS = 3
|
|
@@ -90,6 +98,30 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
90
98
|
code = body.get("code", "unknown_error")
|
|
91
99
|
status = response.status_code
|
|
92
100
|
|
|
101
|
+
# Resolve deprecated code aliases (bad_request, invalid_request → validation_error).
|
|
102
|
+
code = canonical_code(code)
|
|
103
|
+
|
|
104
|
+
# Prefer the canonical code→exception mapping for well-known codes; fall back
|
|
105
|
+
# to HTTP-status-based routing for responses that omit an error code.
|
|
106
|
+
exc_class = ERROR_CODE_TO_EXCEPTION.get(code or "")
|
|
107
|
+
if exc_class is TierRestrictedError:
|
|
108
|
+
raise TierRestrictedError(
|
|
109
|
+
error_msg,
|
|
110
|
+
code=code,
|
|
111
|
+
status=status,
|
|
112
|
+
required_tier=body.get("required_tier"),
|
|
113
|
+
)
|
|
114
|
+
if exc_class is RateLimitedError:
|
|
115
|
+
raise RateLimitedError(
|
|
116
|
+
error_msg,
|
|
117
|
+
code=code,
|
|
118
|
+
status=status,
|
|
119
|
+
retry_after=body.get("retry_after"),
|
|
120
|
+
)
|
|
121
|
+
if exc_class is not None and exc_class is not SharpAPIError:
|
|
122
|
+
raise exc_class(error_msg, code=code, status=status)
|
|
123
|
+
|
|
124
|
+
# No canonical code match — route by HTTP status.
|
|
93
125
|
if status == 401:
|
|
94
126
|
raise AuthenticationError(error_msg, code=code, status=status)
|
|
95
127
|
elif status == 403:
|
|
@@ -112,13 +144,28 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
112
144
|
raise SharpAPIError(error_msg, code=code, status=status)
|
|
113
145
|
|
|
114
146
|
|
|
115
|
-
def make_headers(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
def make_headers(
|
|
148
|
+
api_key: str,
|
|
149
|
+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
|
|
150
|
+
) -> dict[str, str]:
|
|
151
|
+
"""Build default request headers.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
api_key: The SharpAPI key (e.g. ``sk_live_...``).
|
|
155
|
+
auth_method: Either ``"x-api-key"`` (default — sends an
|
|
156
|
+
``X-API-Key`` header) or ``"bearer"`` (sends
|
|
157
|
+
``Authorization: Bearer <key>``). Useful when proxies, IAM
|
|
158
|
+
layers, or SSO gateways strip non-standard custom headers.
|
|
159
|
+
"""
|
|
160
|
+
headers: dict[str, str] = {
|
|
119
161
|
"Content-Type": "application/json",
|
|
120
162
|
"User-Agent": USER_AGENT,
|
|
121
163
|
}
|
|
164
|
+
if auth_method == "bearer":
|
|
165
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
166
|
+
else:
|
|
167
|
+
headers["X-API-Key"] = api_key
|
|
168
|
+
return headers
|
|
122
169
|
|
|
123
170
|
|
|
124
171
|
def _int_or_none(value: str | None) -> int | None:
|
sharpapi/async_client.py
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
10
10
|
from ._base import (
|
|
11
|
+
DEFAULT_AUTH_METHOD,
|
|
11
12
|
DEFAULT_BASE_URL,
|
|
12
13
|
DEFAULT_TIMEOUT,
|
|
13
14
|
RETRY_MAX_ATTEMPTS,
|
|
15
|
+
AuthMethod,
|
|
14
16
|
handle_errors,
|
|
15
17
|
make_headers,
|
|
16
18
|
parse_rate_limit,
|
|
@@ -20,18 +22,22 @@ from ._base import (
|
|
|
20
22
|
)
|
|
21
23
|
from ._utils import _clean_params
|
|
22
24
|
from .models import (
|
|
23
|
-
APIResponse,
|
|
24
25
|
AccountInfo,
|
|
26
|
+
APIKey,
|
|
27
|
+
APIResponse,
|
|
25
28
|
ArbitrageOpportunity,
|
|
26
|
-
|
|
29
|
+
ClosingSnapshot,
|
|
27
30
|
Event,
|
|
31
|
+
EVOpportunity,
|
|
32
|
+
GameState,
|
|
28
33
|
League,
|
|
29
34
|
LowHoldOpportunity,
|
|
35
|
+
Market,
|
|
30
36
|
MiddleOpportunity,
|
|
31
37
|
OddsLine,
|
|
32
38
|
RateLimitInfo,
|
|
33
|
-
Sportsbook,
|
|
34
39
|
Sport,
|
|
40
|
+
Sportsbook,
|
|
35
41
|
)
|
|
36
42
|
|
|
37
43
|
|
|
@@ -41,6 +47,17 @@ class AsyncSharpAPI:
|
|
|
41
47
|
Provides typed access to odds, +EV, arbitrage, middles, and streaming
|
|
42
48
|
endpoints using ``async``/``await``.
|
|
43
49
|
|
|
50
|
+
Args:
|
|
51
|
+
api_key: Your SharpAPI key (e.g. ``sk_live_...``).
|
|
52
|
+
base_url: Override the API base URL (defaults to production).
|
|
53
|
+
timeout: HTTP timeout in seconds.
|
|
54
|
+
auth_method: How to send the API key on REST requests. ``"x-api-key"``
|
|
55
|
+
(default) sends the ``X-API-Key`` header. ``"bearer"`` sends
|
|
56
|
+
``Authorization: Bearer <key>`` instead — useful when running
|
|
57
|
+
behind IAM layers, SSO, or API gateways that strip custom
|
|
58
|
+
headers. SSE streams (sync client only) always authenticate via
|
|
59
|
+
``?api_key=`` query and are unaffected.
|
|
60
|
+
|
|
44
61
|
Example::
|
|
45
62
|
|
|
46
63
|
import asyncio
|
|
@@ -52,6 +69,10 @@ class AsyncSharpAPI:
|
|
|
52
69
|
for arb in arbs.data:
|
|
53
70
|
print(f"{arb.profit_percent}% — {arb.event_name}")
|
|
54
71
|
|
|
72
|
+
# Or, behind a proxy that requires standard Bearer auth:
|
|
73
|
+
async with AsyncSharpAPI("sk_live_xxx", auth_method="bearer") as c:
|
|
74
|
+
...
|
|
75
|
+
|
|
55
76
|
asyncio.run(main())
|
|
56
77
|
"""
|
|
57
78
|
|
|
@@ -61,16 +82,18 @@ class AsyncSharpAPI:
|
|
|
61
82
|
*,
|
|
62
83
|
base_url: str = DEFAULT_BASE_URL,
|
|
63
84
|
timeout: float = DEFAULT_TIMEOUT,
|
|
85
|
+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
|
|
64
86
|
):
|
|
65
87
|
if not api_key:
|
|
66
88
|
raise ValueError("api_key is required")
|
|
67
89
|
|
|
68
90
|
self._api_key = api_key
|
|
91
|
+
self._auth_method: AuthMethod = auth_method
|
|
69
92
|
self._base_url = base_url.rstrip("/")
|
|
70
93
|
self._timeout = timeout
|
|
71
94
|
self._http = httpx.AsyncClient(
|
|
72
95
|
base_url=f"{self._base_url}/api/v1",
|
|
73
|
-
headers=make_headers(api_key),
|
|
96
|
+
headers=make_headers(api_key, auth_method),
|
|
74
97
|
timeout=timeout,
|
|
75
98
|
)
|
|
76
99
|
self._last_rate_limit = RateLimitInfo()
|
|
@@ -81,11 +104,13 @@ class AsyncSharpAPI:
|
|
|
81
104
|
self.arbitrage = _AsyncArbitrageResource(self)
|
|
82
105
|
self.middles = _AsyncMiddlesResource(self)
|
|
83
106
|
self.low_hold = _AsyncLowHoldResource(self)
|
|
107
|
+
self.gamestate = _AsyncGameStateResource(self)
|
|
84
108
|
self.sports = _AsyncSportsResource(self)
|
|
85
109
|
self.leagues = _AsyncLeaguesResource(self)
|
|
86
110
|
self.sportsbooks = _AsyncSportsbooksResource(self)
|
|
87
111
|
self.events = _AsyncEventsResource(self)
|
|
88
112
|
self.account = _AsyncAccountResource(self)
|
|
113
|
+
self.keys = _AsyncKeysResource(self)
|
|
89
114
|
|
|
90
115
|
@property
|
|
91
116
|
def rate_limit(self) -> RateLimitInfo:
|
|
@@ -93,7 +118,7 @@ class AsyncSharpAPI:
|
|
|
93
118
|
return self._last_rate_limit
|
|
94
119
|
|
|
95
120
|
async def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
|
|
96
|
-
"""Make an async
|
|
121
|
+
"""Make an async request, return parsed JSON. Retries 502/503/504 with jittered backoff."""
|
|
97
122
|
if params:
|
|
98
123
|
params = _clean_params(params)
|
|
99
124
|
|
|
@@ -148,18 +173,18 @@ class _AsyncOddsResource:
|
|
|
148
173
|
async def get(
|
|
149
174
|
self,
|
|
150
175
|
*,
|
|
151
|
-
sportsbook:
|
|
152
|
-
add_sportsbook:
|
|
153
|
-
sport:
|
|
154
|
-
league:
|
|
155
|
-
market:
|
|
156
|
-
event:
|
|
157
|
-
live:
|
|
158
|
-
sort:
|
|
159
|
-
group_by:
|
|
160
|
-
fields:
|
|
161
|
-
limit:
|
|
162
|
-
offset:
|
|
176
|
+
sportsbook: str | list[str] | None = None,
|
|
177
|
+
add_sportsbook: str | list[str] | None = None,
|
|
178
|
+
sport: str | list[str] | None = None,
|
|
179
|
+
league: str | list[str] | None = None,
|
|
180
|
+
market: str | list[str] | None = None,
|
|
181
|
+
event: str | list[str] | None = None,
|
|
182
|
+
live: bool | None = None,
|
|
183
|
+
sort: str | None = None,
|
|
184
|
+
group_by: str | None = None,
|
|
185
|
+
fields: str | list[str] | None = None,
|
|
186
|
+
limit: int | None = None,
|
|
187
|
+
offset: int | None = None,
|
|
163
188
|
) -> APIResponse[list[OddsLine]]:
|
|
164
189
|
"""Get current odds snapshot."""
|
|
165
190
|
data = await self._client._get("/odds", {
|
|
@@ -181,15 +206,15 @@ class _AsyncOddsResource:
|
|
|
181
206
|
async def best(
|
|
182
207
|
self,
|
|
183
208
|
*,
|
|
184
|
-
sport:
|
|
185
|
-
league:
|
|
186
|
-
market:
|
|
187
|
-
event:
|
|
188
|
-
live:
|
|
189
|
-
sportsbook:
|
|
190
|
-
add_sportsbook:
|
|
191
|
-
limit:
|
|
192
|
-
offset:
|
|
209
|
+
sport: str | list[str] | None = None,
|
|
210
|
+
league: str | list[str] | None = None,
|
|
211
|
+
market: str | list[str] | None = None,
|
|
212
|
+
event: str | list[str] | None = None,
|
|
213
|
+
live: bool | None = None,
|
|
214
|
+
sportsbook: str | list[str] | None = None,
|
|
215
|
+
add_sportsbook: str | list[str] | None = None,
|
|
216
|
+
limit: int | None = None,
|
|
217
|
+
offset: int | None = None,
|
|
193
218
|
) -> APIResponse[list[OddsLine]]:
|
|
194
219
|
"""Get best odds per selection across all sportsbooks."""
|
|
195
220
|
data = await self._client._get("/odds/best", {
|
|
@@ -209,7 +234,7 @@ class _AsyncOddsResource:
|
|
|
209
234
|
self,
|
|
210
235
|
event_id: str,
|
|
211
236
|
*,
|
|
212
|
-
market:
|
|
237
|
+
market: str | None = None,
|
|
213
238
|
) -> APIResponse[list[OddsLine]]:
|
|
214
239
|
"""Get side-by-side odds comparison for an event."""
|
|
215
240
|
data = await self._client._get("/odds/comparison", {
|
|
@@ -223,6 +248,25 @@ class _AsyncOddsResource:
|
|
|
223
248
|
data = await self._client._post("/odds/batch", {"event_ids": event_ids})
|
|
224
249
|
return parse_response(data, OddsLine)
|
|
225
250
|
|
|
251
|
+
async def closing(
|
|
252
|
+
self,
|
|
253
|
+
event_id: str,
|
|
254
|
+
*,
|
|
255
|
+
sportsbook: str | None = None,
|
|
256
|
+
) -> ClosingSnapshot:
|
|
257
|
+
"""Get closing-line snapshot for an event.
|
|
258
|
+
|
|
259
|
+
Returns the captured closing odds grouped by sportsbook. If no
|
|
260
|
+
closing data has been captured for the event, the returned
|
|
261
|
+
``ClosingSnapshot.books`` mapping will be empty.
|
|
262
|
+
"""
|
|
263
|
+
data = await self._client._get("/odds/closing", {
|
|
264
|
+
"event_id": event_id,
|
|
265
|
+
"sportsbook": sportsbook or None,
|
|
266
|
+
})
|
|
267
|
+
raw = data.get("data", data)
|
|
268
|
+
return ClosingSnapshot.model_validate(raw)
|
|
269
|
+
|
|
226
270
|
|
|
227
271
|
class _AsyncEVResource:
|
|
228
272
|
"""Async access to +EV opportunities."""
|
|
@@ -233,21 +277,21 @@ class _AsyncEVResource:
|
|
|
233
277
|
async def get(
|
|
234
278
|
self,
|
|
235
279
|
*,
|
|
236
|
-
sport:
|
|
237
|
-
league:
|
|
238
|
-
sportsbook:
|
|
239
|
-
add_sportsbook:
|
|
240
|
-
market:
|
|
241
|
-
min_ev:
|
|
242
|
-
max_ev:
|
|
243
|
-
min_market_width:
|
|
244
|
-
max_market_width:
|
|
245
|
-
max_odds_age:
|
|
246
|
-
date_range:
|
|
247
|
-
live:
|
|
248
|
-
sort:
|
|
249
|
-
limit:
|
|
250
|
-
offset:
|
|
280
|
+
sport: str | list[str] | None = None,
|
|
281
|
+
league: str | list[str] | None = None,
|
|
282
|
+
sportsbook: str | list[str] | None = None,
|
|
283
|
+
add_sportsbook: str | list[str] | None = None,
|
|
284
|
+
market: str | list[str] | None = None,
|
|
285
|
+
min_ev: float | None = None,
|
|
286
|
+
max_ev: float | None = None,
|
|
287
|
+
min_market_width: float | None = None,
|
|
288
|
+
max_market_width: float | None = None,
|
|
289
|
+
max_odds_age: int | None = None,
|
|
290
|
+
date_range: str | None = None,
|
|
291
|
+
live: bool | None = None,
|
|
292
|
+
sort: str | None = None,
|
|
293
|
+
limit: int | None = None,
|
|
294
|
+
offset: int | None = None,
|
|
251
295
|
) -> APIResponse[list[EVOpportunity]]:
|
|
252
296
|
"""Get +EV opportunities. Requires Pro tier or higher."""
|
|
253
297
|
data = await self._client._get("/opportunities/ev", {
|
|
@@ -279,18 +323,18 @@ class _AsyncArbitrageResource:
|
|
|
279
323
|
async def get(
|
|
280
324
|
self,
|
|
281
325
|
*,
|
|
282
|
-
sport:
|
|
283
|
-
league:
|
|
284
|
-
sportsbook:
|
|
285
|
-
add_sportsbook:
|
|
286
|
-
market:
|
|
287
|
-
min_profit:
|
|
288
|
-
max_odds_age:
|
|
289
|
-
live:
|
|
290
|
-
sort:
|
|
291
|
-
group:
|
|
292
|
-
limit:
|
|
293
|
-
offset:
|
|
326
|
+
sport: str | list[str] | None = None,
|
|
327
|
+
league: str | list[str] | None = None,
|
|
328
|
+
sportsbook: str | list[str] | None = None,
|
|
329
|
+
add_sportsbook: str | list[str] | None = None,
|
|
330
|
+
market: str | list[str] | None = None,
|
|
331
|
+
min_profit: float | None = None,
|
|
332
|
+
max_odds_age: int | None = None,
|
|
333
|
+
live: bool | None = None,
|
|
334
|
+
sort: str | None = None,
|
|
335
|
+
group: str | None = None,
|
|
336
|
+
limit: int | None = None,
|
|
337
|
+
offset: int | None = None,
|
|
294
338
|
) -> APIResponse[list[ArbitrageOpportunity]]:
|
|
295
339
|
"""Get arbitrage opportunities. Requires Hobby tier or higher."""
|
|
296
340
|
data = await self._client._get("/opportunities/arbitrage", {
|
|
@@ -319,17 +363,17 @@ class _AsyncMiddlesResource:
|
|
|
319
363
|
async def get(
|
|
320
364
|
self,
|
|
321
365
|
*,
|
|
322
|
-
sport:
|
|
323
|
-
league:
|
|
324
|
-
sportsbook:
|
|
325
|
-
market:
|
|
326
|
-
min_size:
|
|
327
|
-
max_odds_age:
|
|
328
|
-
live:
|
|
329
|
-
state:
|
|
330
|
-
sort:
|
|
331
|
-
limit:
|
|
332
|
-
offset:
|
|
366
|
+
sport: str | list[str] | None = None,
|
|
367
|
+
league: str | list[str] | None = None,
|
|
368
|
+
sportsbook: str | list[str] | None = None,
|
|
369
|
+
market: str | list[str] | None = None,
|
|
370
|
+
min_size: float | None = None,
|
|
371
|
+
max_odds_age: int | None = None,
|
|
372
|
+
live: bool | None = None,
|
|
373
|
+
state: str | None = None,
|
|
374
|
+
sort: str | None = None,
|
|
375
|
+
limit: int | None = None,
|
|
376
|
+
offset: int | None = None,
|
|
333
377
|
) -> APIResponse[list[MiddleOpportunity]]:
|
|
334
378
|
"""Get middle opportunities. Requires Pro tier or higher."""
|
|
335
379
|
data = await self._client._get("/opportunities/middles", {
|
|
@@ -357,16 +401,16 @@ class _AsyncLowHoldResource:
|
|
|
357
401
|
async def get(
|
|
358
402
|
self,
|
|
359
403
|
*,
|
|
360
|
-
sport:
|
|
361
|
-
league:
|
|
362
|
-
sportsbook:
|
|
363
|
-
market:
|
|
364
|
-
max_hold:
|
|
365
|
-
live:
|
|
366
|
-
state:
|
|
367
|
-
sort:
|
|
368
|
-
limit:
|
|
369
|
-
offset:
|
|
404
|
+
sport: str | list[str] | None = None,
|
|
405
|
+
league: str | list[str] | None = None,
|
|
406
|
+
sportsbook: str | list[str] | None = None,
|
|
407
|
+
market: str | list[str] | None = None,
|
|
408
|
+
max_hold: float | None = None,
|
|
409
|
+
live: bool | None = None,
|
|
410
|
+
state: str | None = None,
|
|
411
|
+
sort: str | None = None,
|
|
412
|
+
limit: int | None = None,
|
|
413
|
+
offset: int | None = None,
|
|
370
414
|
) -> APIResponse[list[LowHoldOpportunity]]:
|
|
371
415
|
"""Get low-hold opportunities."""
|
|
372
416
|
data = await self._client._get("/opportunities/low_hold", {
|
|
@@ -384,6 +428,41 @@ class _AsyncLowHoldResource:
|
|
|
384
428
|
return parse_response(data, LowHoldOpportunity)
|
|
385
429
|
|
|
386
430
|
|
|
431
|
+
class _AsyncGameStateResource:
|
|
432
|
+
"""Async access to live game state — scores, period, clock —
|
|
433
|
+
merged across sportsbooks.
|
|
434
|
+
|
|
435
|
+
Requires the Game State add-on ($79/mo) or Enterprise tier.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
def __init__(self, client: AsyncSharpAPI):
|
|
439
|
+
self._client = client
|
|
440
|
+
|
|
441
|
+
async def get(self, sport: str | None = None) -> dict[str, dict[str, GameState]]:
|
|
442
|
+
"""Fetch the current game state.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
sport: Limit to a single sport (e.g. ``"basketball"``).
|
|
446
|
+
Omit to fetch every sport at once.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Nested mapping ``{sport: {event_id: GameState}}``.
|
|
450
|
+
"""
|
|
451
|
+
path = f"/gamestate/{sport}" if sport else "/gamestate"
|
|
452
|
+
data = await self._client._get(path)
|
|
453
|
+
raw = data.get("data", {}) or {}
|
|
454
|
+
result: dict[str, dict[str, GameState]] = {}
|
|
455
|
+
for sport_key, events in raw.items():
|
|
456
|
+
if not isinstance(events, dict):
|
|
457
|
+
continue
|
|
458
|
+
result[sport_key] = {
|
|
459
|
+
eid: GameState.model_validate(state)
|
|
460
|
+
for eid, state in events.items()
|
|
461
|
+
if isinstance(state, dict)
|
|
462
|
+
}
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
|
|
387
466
|
class _AsyncSportsResource:
|
|
388
467
|
def __init__(self, client: AsyncSharpAPI):
|
|
389
468
|
self._client = client
|
|
@@ -404,7 +483,7 @@ class _AsyncLeaguesResource:
|
|
|
404
483
|
def __init__(self, client: AsyncSharpAPI):
|
|
405
484
|
self._client = client
|
|
406
485
|
|
|
407
|
-
async def list(self, *, sport:
|
|
486
|
+
async def list(self, *, sport: str | None = None) -> APIResponse[list[League]]:
|
|
408
487
|
"""List all leagues, optionally filtered by sport."""
|
|
409
488
|
data = await self._client._get("/leagues", {"sport": sport})
|
|
410
489
|
return parse_response(data, League)
|
|
@@ -439,11 +518,11 @@ class _AsyncEventsResource:
|
|
|
439
518
|
async def list(
|
|
440
519
|
self,
|
|
441
520
|
*,
|
|
442
|
-
sport:
|
|
443
|
-
league:
|
|
444
|
-
live:
|
|
445
|
-
limit:
|
|
446
|
-
offset:
|
|
521
|
+
sport: str | None = None,
|
|
522
|
+
league: str | list[str] | None = None,
|
|
523
|
+
live: bool | None = None,
|
|
524
|
+
limit: int | None = None,
|
|
525
|
+
offset: int | None = None,
|
|
447
526
|
) -> APIResponse[list[Event]]:
|
|
448
527
|
"""List events."""
|
|
449
528
|
data = await self._client._get("/events", {
|
|
@@ -461,6 +540,11 @@ class _AsyncEventsResource:
|
|
|
461
540
|
raw = data.get("data", data)
|
|
462
541
|
return Event.model_validate(raw)
|
|
463
542
|
|
|
543
|
+
async def markets(self, event_id: str) -> APIResponse[list[Market]]:
|
|
544
|
+
"""List the markets available on a specific event."""
|
|
545
|
+
data = await self._client._get(f"/events/{event_id}/markets")
|
|
546
|
+
return parse_response(data, Market)
|
|
547
|
+
|
|
464
548
|
|
|
465
549
|
class _AsyncAccountResource:
|
|
466
550
|
def __init__(self, client: AsyncSharpAPI):
|
|
@@ -476,3 +560,33 @@ class _AsyncAccountResource:
|
|
|
476
560
|
"""Get current usage stats."""
|
|
477
561
|
data = await self._client._get("/account/usage")
|
|
478
562
|
return data.get("data", data)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class _AsyncKeysResource:
|
|
566
|
+
"""Async access to API key CRUD on the current account."""
|
|
567
|
+
|
|
568
|
+
def __init__(self, client: AsyncSharpAPI):
|
|
569
|
+
self._client = client
|
|
570
|
+
|
|
571
|
+
async def list(self) -> APIResponse[list[APIKey]]:
|
|
572
|
+
"""List all API keys on the account."""
|
|
573
|
+
data = await self._client._get("/account/keys")
|
|
574
|
+
return parse_response(data, APIKey)
|
|
575
|
+
|
|
576
|
+
async def create(self, name: str) -> APIKey:
|
|
577
|
+
"""Create a new API key. Returned ``APIKey.key`` is shown only once."""
|
|
578
|
+
data = await self._client._post("/account/keys", {"name": name})
|
|
579
|
+
raw = data.get("data", data)
|
|
580
|
+
return APIKey.model_validate(raw)
|
|
581
|
+
|
|
582
|
+
async def revoke(self, key_id: str) -> None:
|
|
583
|
+
"""Revoke (delete) an API key by ID."""
|
|
584
|
+
await self._client._request("DELETE", f"/account/keys/{key_id}")
|
|
585
|
+
|
|
586
|
+
async def rotate(self, key_id: str) -> APIKey:
|
|
587
|
+
"""Rotate an API key — issues a new key and revokes the old one."""
|
|
588
|
+
data = await self._client._post(f"/account/keys/{key_id}/rotate")
|
|
589
|
+
raw = data.get("data", data)
|
|
590
|
+
if isinstance(raw, dict) and "new_key" in raw:
|
|
591
|
+
return APIKey.model_validate(raw["new_key"])
|
|
592
|
+
return APIKey.model_validate(raw)
|