sharpapi 0.2.2__py3-none-any.whl → 0.2.5__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 +17 -4
- sharpapi/_base.py +52 -5
- sharpapi/async_client.py +159 -82
- sharpapi/client.py +195 -101
- sharpapi/exceptions.py +134 -4
- sharpapi/models.py +212 -151
- sharpapi/streaming.py +45 -6
- {sharpapi-0.2.2.dist-info → sharpapi-0.2.5.dist-info}/METADATA +1 -1
- sharpapi-0.2.5.dist-info/RECORD +12 -0
- sharpapi-0.2.2.dist-info/RECORD +0 -12
- {sharpapi-0.2.2.dist-info → sharpapi-0.2.5.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,20 @@ 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,
|
|
36
41
|
Event,
|
|
42
|
+
EVOpportunity,
|
|
37
43
|
GameState,
|
|
38
44
|
League,
|
|
39
45
|
LowHoldOpportunity,
|
|
40
46
|
LowHoldSide,
|
|
47
|
+
Market,
|
|
41
48
|
MiddleOpportunity,
|
|
42
49
|
MiddleSide,
|
|
43
50
|
OddsLine,
|
|
@@ -49,25 +56,28 @@ from .models import (
|
|
|
49
56
|
Sportsbook,
|
|
50
57
|
)
|
|
51
58
|
from .streaming import EventStream
|
|
52
|
-
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
53
59
|
|
|
54
|
-
__version__ = "0.2.
|
|
60
|
+
__version__ = "0.2.5"
|
|
55
61
|
|
|
56
62
|
__all__ = [
|
|
57
63
|
# Clients
|
|
58
64
|
"SharpAPI",
|
|
59
65
|
"AsyncSharpAPI",
|
|
60
66
|
# Models
|
|
67
|
+
"APIKey",
|
|
61
68
|
"APIResponse",
|
|
62
69
|
"AccountInfo",
|
|
63
70
|
"ArbitrageLeg",
|
|
64
71
|
"ArbitrageOpportunity",
|
|
72
|
+
"ClosingOddsLine",
|
|
73
|
+
"ClosingSnapshot",
|
|
65
74
|
"EVOpportunity",
|
|
66
75
|
"Event",
|
|
67
76
|
"GameState",
|
|
68
77
|
"League",
|
|
69
78
|
"LowHoldOpportunity",
|
|
70
79
|
"LowHoldSide",
|
|
80
|
+
"Market",
|
|
71
81
|
"MiddleOpportunity",
|
|
72
82
|
"MiddleSide",
|
|
73
83
|
"OddsLine",
|
|
@@ -86,6 +96,9 @@ __all__ = [
|
|
|
86
96
|
"StreamError",
|
|
87
97
|
"TierRestrictedError",
|
|
88
98
|
"ValidationError",
|
|
99
|
+
# Error-code registry
|
|
100
|
+
"ERROR_CODE_DESCRIPTIONS",
|
|
101
|
+
"ERROR_CODE_TO_EXCEPTION",
|
|
89
102
|
# Utilities
|
|
90
103
|
"american_to_decimal",
|
|
91
104
|
"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,21 @@ 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,
|
|
28
32
|
League,
|
|
29
33
|
LowHoldOpportunity,
|
|
34
|
+
Market,
|
|
30
35
|
MiddleOpportunity,
|
|
31
36
|
OddsLine,
|
|
32
37
|
RateLimitInfo,
|
|
33
|
-
Sportsbook,
|
|
34
38
|
Sport,
|
|
39
|
+
Sportsbook,
|
|
35
40
|
)
|
|
36
41
|
|
|
37
42
|
|
|
@@ -41,6 +46,17 @@ class AsyncSharpAPI:
|
|
|
41
46
|
Provides typed access to odds, +EV, arbitrage, middles, and streaming
|
|
42
47
|
endpoints using ``async``/``await``.
|
|
43
48
|
|
|
49
|
+
Args:
|
|
50
|
+
api_key: Your SharpAPI key (e.g. ``sk_live_...``).
|
|
51
|
+
base_url: Override the API base URL (defaults to production).
|
|
52
|
+
timeout: HTTP timeout in seconds.
|
|
53
|
+
auth_method: How to send the API key on REST requests. ``"x-api-key"``
|
|
54
|
+
(default) sends the ``X-API-Key`` header. ``"bearer"`` sends
|
|
55
|
+
``Authorization: Bearer <key>`` instead — useful when running
|
|
56
|
+
behind IAM layers, SSO, or API gateways that strip custom
|
|
57
|
+
headers. SSE streams (sync client only) always authenticate via
|
|
58
|
+
``?api_key=`` query and are unaffected.
|
|
59
|
+
|
|
44
60
|
Example::
|
|
45
61
|
|
|
46
62
|
import asyncio
|
|
@@ -52,6 +68,10 @@ class AsyncSharpAPI:
|
|
|
52
68
|
for arb in arbs.data:
|
|
53
69
|
print(f"{arb.profit_percent}% — {arb.event_name}")
|
|
54
70
|
|
|
71
|
+
# Or, behind a proxy that requires standard Bearer auth:
|
|
72
|
+
async with AsyncSharpAPI("sk_live_xxx", auth_method="bearer") as c:
|
|
73
|
+
...
|
|
74
|
+
|
|
55
75
|
asyncio.run(main())
|
|
56
76
|
"""
|
|
57
77
|
|
|
@@ -61,16 +81,18 @@ class AsyncSharpAPI:
|
|
|
61
81
|
*,
|
|
62
82
|
base_url: str = DEFAULT_BASE_URL,
|
|
63
83
|
timeout: float = DEFAULT_TIMEOUT,
|
|
84
|
+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
|
|
64
85
|
):
|
|
65
86
|
if not api_key:
|
|
66
87
|
raise ValueError("api_key is required")
|
|
67
88
|
|
|
68
89
|
self._api_key = api_key
|
|
90
|
+
self._auth_method: AuthMethod = auth_method
|
|
69
91
|
self._base_url = base_url.rstrip("/")
|
|
70
92
|
self._timeout = timeout
|
|
71
93
|
self._http = httpx.AsyncClient(
|
|
72
94
|
base_url=f"{self._base_url}/api/v1",
|
|
73
|
-
headers=make_headers(api_key),
|
|
95
|
+
headers=make_headers(api_key, auth_method),
|
|
74
96
|
timeout=timeout,
|
|
75
97
|
)
|
|
76
98
|
self._last_rate_limit = RateLimitInfo()
|
|
@@ -86,6 +108,7 @@ class AsyncSharpAPI:
|
|
|
86
108
|
self.sportsbooks = _AsyncSportsbooksResource(self)
|
|
87
109
|
self.events = _AsyncEventsResource(self)
|
|
88
110
|
self.account = _AsyncAccountResource(self)
|
|
111
|
+
self.keys = _AsyncKeysResource(self)
|
|
89
112
|
|
|
90
113
|
@property
|
|
91
114
|
def rate_limit(self) -> RateLimitInfo:
|
|
@@ -93,7 +116,7 @@ class AsyncSharpAPI:
|
|
|
93
116
|
return self._last_rate_limit
|
|
94
117
|
|
|
95
118
|
async def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
|
|
96
|
-
"""Make an async
|
|
119
|
+
"""Make an async request, return parsed JSON. Retries 502/503/504 with jittered backoff."""
|
|
97
120
|
if params:
|
|
98
121
|
params = _clean_params(params)
|
|
99
122
|
|
|
@@ -148,18 +171,18 @@ class _AsyncOddsResource:
|
|
|
148
171
|
async def get(
|
|
149
172
|
self,
|
|
150
173
|
*,
|
|
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:
|
|
174
|
+
sportsbook: str | list[str] | None = None,
|
|
175
|
+
add_sportsbook: str | list[str] | None = None,
|
|
176
|
+
sport: str | list[str] | None = None,
|
|
177
|
+
league: str | list[str] | None = None,
|
|
178
|
+
market: str | list[str] | None = None,
|
|
179
|
+
event: str | list[str] | None = None,
|
|
180
|
+
live: bool | None = None,
|
|
181
|
+
sort: str | None = None,
|
|
182
|
+
group_by: str | None = None,
|
|
183
|
+
fields: str | list[str] | None = None,
|
|
184
|
+
limit: int | None = None,
|
|
185
|
+
offset: int | None = None,
|
|
163
186
|
) -> APIResponse[list[OddsLine]]:
|
|
164
187
|
"""Get current odds snapshot."""
|
|
165
188
|
data = await self._client._get("/odds", {
|
|
@@ -181,15 +204,15 @@ class _AsyncOddsResource:
|
|
|
181
204
|
async def best(
|
|
182
205
|
self,
|
|
183
206
|
*,
|
|
184
|
-
sport:
|
|
185
|
-
league:
|
|
186
|
-
market:
|
|
187
|
-
event:
|
|
188
|
-
live:
|
|
189
|
-
sportsbook:
|
|
190
|
-
add_sportsbook:
|
|
191
|
-
limit:
|
|
192
|
-
offset:
|
|
207
|
+
sport: str | list[str] | None = None,
|
|
208
|
+
league: str | list[str] | None = None,
|
|
209
|
+
market: str | list[str] | None = None,
|
|
210
|
+
event: str | list[str] | None = None,
|
|
211
|
+
live: bool | None = None,
|
|
212
|
+
sportsbook: str | list[str] | None = None,
|
|
213
|
+
add_sportsbook: str | list[str] | None = None,
|
|
214
|
+
limit: int | None = None,
|
|
215
|
+
offset: int | None = None,
|
|
193
216
|
) -> APIResponse[list[OddsLine]]:
|
|
194
217
|
"""Get best odds per selection across all sportsbooks."""
|
|
195
218
|
data = await self._client._get("/odds/best", {
|
|
@@ -209,7 +232,7 @@ class _AsyncOddsResource:
|
|
|
209
232
|
self,
|
|
210
233
|
event_id: str,
|
|
211
234
|
*,
|
|
212
|
-
market:
|
|
235
|
+
market: str | None = None,
|
|
213
236
|
) -> APIResponse[list[OddsLine]]:
|
|
214
237
|
"""Get side-by-side odds comparison for an event."""
|
|
215
238
|
data = await self._client._get("/odds/comparison", {
|
|
@@ -223,6 +246,25 @@ class _AsyncOddsResource:
|
|
|
223
246
|
data = await self._client._post("/odds/batch", {"event_ids": event_ids})
|
|
224
247
|
return parse_response(data, OddsLine)
|
|
225
248
|
|
|
249
|
+
async def closing(
|
|
250
|
+
self,
|
|
251
|
+
event_id: str,
|
|
252
|
+
*,
|
|
253
|
+
sportsbook: str | None = None,
|
|
254
|
+
) -> ClosingSnapshot:
|
|
255
|
+
"""Get closing-line snapshot for an event.
|
|
256
|
+
|
|
257
|
+
Returns the captured closing odds grouped by sportsbook. If no
|
|
258
|
+
closing data has been captured for the event, the returned
|
|
259
|
+
``ClosingSnapshot.books`` mapping will be empty.
|
|
260
|
+
"""
|
|
261
|
+
data = await self._client._get("/odds/closing", {
|
|
262
|
+
"event_id": event_id,
|
|
263
|
+
"sportsbook": sportsbook or None,
|
|
264
|
+
})
|
|
265
|
+
raw = data.get("data", data)
|
|
266
|
+
return ClosingSnapshot.model_validate(raw)
|
|
267
|
+
|
|
226
268
|
|
|
227
269
|
class _AsyncEVResource:
|
|
228
270
|
"""Async access to +EV opportunities."""
|
|
@@ -233,21 +275,21 @@ class _AsyncEVResource:
|
|
|
233
275
|
async def get(
|
|
234
276
|
self,
|
|
235
277
|
*,
|
|
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:
|
|
278
|
+
sport: str | list[str] | None = None,
|
|
279
|
+
league: str | list[str] | None = None,
|
|
280
|
+
sportsbook: str | list[str] | None = None,
|
|
281
|
+
add_sportsbook: str | list[str] | None = None,
|
|
282
|
+
market: str | list[str] | None = None,
|
|
283
|
+
min_ev: float | None = None,
|
|
284
|
+
max_ev: float | None = None,
|
|
285
|
+
min_market_width: float | None = None,
|
|
286
|
+
max_market_width: float | None = None,
|
|
287
|
+
max_odds_age: int | None = None,
|
|
288
|
+
date_range: str | None = None,
|
|
289
|
+
live: bool | None = None,
|
|
290
|
+
sort: str | None = None,
|
|
291
|
+
limit: int | None = None,
|
|
292
|
+
offset: int | None = None,
|
|
251
293
|
) -> APIResponse[list[EVOpportunity]]:
|
|
252
294
|
"""Get +EV opportunities. Requires Pro tier or higher."""
|
|
253
295
|
data = await self._client._get("/opportunities/ev", {
|
|
@@ -279,18 +321,18 @@ class _AsyncArbitrageResource:
|
|
|
279
321
|
async def get(
|
|
280
322
|
self,
|
|
281
323
|
*,
|
|
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:
|
|
324
|
+
sport: str | list[str] | None = None,
|
|
325
|
+
league: str | list[str] | None = None,
|
|
326
|
+
sportsbook: str | list[str] | None = None,
|
|
327
|
+
add_sportsbook: str | list[str] | None = None,
|
|
328
|
+
market: str | list[str] | None = None,
|
|
329
|
+
min_profit: float | None = None,
|
|
330
|
+
max_odds_age: int | None = None,
|
|
331
|
+
live: bool | None = None,
|
|
332
|
+
sort: str | None = None,
|
|
333
|
+
group: str | None = None,
|
|
334
|
+
limit: int | None = None,
|
|
335
|
+
offset: int | None = None,
|
|
294
336
|
) -> APIResponse[list[ArbitrageOpportunity]]:
|
|
295
337
|
"""Get arbitrage opportunities. Requires Hobby tier or higher."""
|
|
296
338
|
data = await self._client._get("/opportunities/arbitrage", {
|
|
@@ -319,17 +361,17 @@ class _AsyncMiddlesResource:
|
|
|
319
361
|
async def get(
|
|
320
362
|
self,
|
|
321
363
|
*,
|
|
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:
|
|
364
|
+
sport: str | list[str] | None = None,
|
|
365
|
+
league: str | list[str] | None = None,
|
|
366
|
+
sportsbook: str | list[str] | None = None,
|
|
367
|
+
market: str | list[str] | None = None,
|
|
368
|
+
min_size: float | None = None,
|
|
369
|
+
max_odds_age: int | None = None,
|
|
370
|
+
live: bool | None = None,
|
|
371
|
+
state: str | None = None,
|
|
372
|
+
sort: str | None = None,
|
|
373
|
+
limit: int | None = None,
|
|
374
|
+
offset: int | None = None,
|
|
333
375
|
) -> APIResponse[list[MiddleOpportunity]]:
|
|
334
376
|
"""Get middle opportunities. Requires Pro tier or higher."""
|
|
335
377
|
data = await self._client._get("/opportunities/middles", {
|
|
@@ -357,16 +399,16 @@ class _AsyncLowHoldResource:
|
|
|
357
399
|
async def get(
|
|
358
400
|
self,
|
|
359
401
|
*,
|
|
360
|
-
sport:
|
|
361
|
-
league:
|
|
362
|
-
sportsbook:
|
|
363
|
-
market:
|
|
364
|
-
max_hold:
|
|
365
|
-
live:
|
|
366
|
-
state:
|
|
367
|
-
sort:
|
|
368
|
-
limit:
|
|
369
|
-
offset:
|
|
402
|
+
sport: str | list[str] | None = None,
|
|
403
|
+
league: str | list[str] | None = None,
|
|
404
|
+
sportsbook: str | list[str] | None = None,
|
|
405
|
+
market: str | list[str] | None = None,
|
|
406
|
+
max_hold: float | None = None,
|
|
407
|
+
live: bool | None = None,
|
|
408
|
+
state: str | None = None,
|
|
409
|
+
sort: str | None = None,
|
|
410
|
+
limit: int | None = None,
|
|
411
|
+
offset: int | None = None,
|
|
370
412
|
) -> APIResponse[list[LowHoldOpportunity]]:
|
|
371
413
|
"""Get low-hold opportunities."""
|
|
372
414
|
data = await self._client._get("/opportunities/low_hold", {
|
|
@@ -404,7 +446,7 @@ class _AsyncLeaguesResource:
|
|
|
404
446
|
def __init__(self, client: AsyncSharpAPI):
|
|
405
447
|
self._client = client
|
|
406
448
|
|
|
407
|
-
async def list(self, *, sport:
|
|
449
|
+
async def list(self, *, sport: str | None = None) -> APIResponse[list[League]]:
|
|
408
450
|
"""List all leagues, optionally filtered by sport."""
|
|
409
451
|
data = await self._client._get("/leagues", {"sport": sport})
|
|
410
452
|
return parse_response(data, League)
|
|
@@ -439,11 +481,11 @@ class _AsyncEventsResource:
|
|
|
439
481
|
async def list(
|
|
440
482
|
self,
|
|
441
483
|
*,
|
|
442
|
-
sport:
|
|
443
|
-
league:
|
|
444
|
-
live:
|
|
445
|
-
limit:
|
|
446
|
-
offset:
|
|
484
|
+
sport: str | None = None,
|
|
485
|
+
league: str | list[str] | None = None,
|
|
486
|
+
live: bool | None = None,
|
|
487
|
+
limit: int | None = None,
|
|
488
|
+
offset: int | None = None,
|
|
447
489
|
) -> APIResponse[list[Event]]:
|
|
448
490
|
"""List events."""
|
|
449
491
|
data = await self._client._get("/events", {
|
|
@@ -461,6 +503,11 @@ class _AsyncEventsResource:
|
|
|
461
503
|
raw = data.get("data", data)
|
|
462
504
|
return Event.model_validate(raw)
|
|
463
505
|
|
|
506
|
+
async def markets(self, event_id: str) -> APIResponse[list[Market]]:
|
|
507
|
+
"""List the markets available on a specific event."""
|
|
508
|
+
data = await self._client._get(f"/events/{event_id}/markets")
|
|
509
|
+
return parse_response(data, Market)
|
|
510
|
+
|
|
464
511
|
|
|
465
512
|
class _AsyncAccountResource:
|
|
466
513
|
def __init__(self, client: AsyncSharpAPI):
|
|
@@ -476,3 +523,33 @@ class _AsyncAccountResource:
|
|
|
476
523
|
"""Get current usage stats."""
|
|
477
524
|
data = await self._client._get("/account/usage")
|
|
478
525
|
return data.get("data", data)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class _AsyncKeysResource:
|
|
529
|
+
"""Async access to API key CRUD on the current account."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, client: AsyncSharpAPI):
|
|
532
|
+
self._client = client
|
|
533
|
+
|
|
534
|
+
async def list(self) -> APIResponse[list[APIKey]]:
|
|
535
|
+
"""List all API keys on the account."""
|
|
536
|
+
data = await self._client._get("/account/keys")
|
|
537
|
+
return parse_response(data, APIKey)
|
|
538
|
+
|
|
539
|
+
async def create(self, name: str) -> APIKey:
|
|
540
|
+
"""Create a new API key. Returned ``APIKey.key`` is shown only once."""
|
|
541
|
+
data = await self._client._post("/account/keys", {"name": name})
|
|
542
|
+
raw = data.get("data", data)
|
|
543
|
+
return APIKey.model_validate(raw)
|
|
544
|
+
|
|
545
|
+
async def revoke(self, key_id: str) -> None:
|
|
546
|
+
"""Revoke (delete) an API key by ID."""
|
|
547
|
+
await self._client._request("DELETE", f"/account/keys/{key_id}")
|
|
548
|
+
|
|
549
|
+
async def rotate(self, key_id: str) -> APIKey:
|
|
550
|
+
"""Rotate an API key — issues a new key and revokes the old one."""
|
|
551
|
+
data = await self._client._post(f"/account/keys/{key_id}/rotate")
|
|
552
|
+
raw = data.get("data", data)
|
|
553
|
+
if isinstance(raw, dict) and "new_key" in raw:
|
|
554
|
+
return APIKey.model_validate(raw["new_key"])
|
|
555
|
+
return APIKey.model_validate(raw)
|