sharpapi 0.2.1__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 +72 -5
- sharpapi/async_client.py +179 -83
- sharpapi/client.py +216 -103
- sharpapi/exceptions.py +134 -4
- sharpapi/models.py +212 -151
- sharpapi/streaming.py +45 -6
- {sharpapi-0.2.1.dist-info → sharpapi-0.2.5.dist-info}/METADATA +2 -3
- sharpapi-0.2.5.dist-info/RECORD +12 -0
- sharpapi-0.2.1.dist-info/RECORD +0 -12
- {sharpapi-0.2.1.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
|
@@ -2,20 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import random
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
5
8
|
import httpx
|
|
6
9
|
|
|
7
10
|
from .exceptions import (
|
|
11
|
+
ERROR_CODE_TO_EXCEPTION,
|
|
8
12
|
AuthenticationError,
|
|
9
13
|
RateLimitedError,
|
|
10
14
|
SharpAPIError,
|
|
11
15
|
TierRestrictedError,
|
|
12
16
|
ValidationError,
|
|
17
|
+
canonical_code,
|
|
13
18
|
)
|
|
14
19
|
from .models import APIResponse, RateLimitInfo, ResponseMeta
|
|
15
20
|
|
|
16
21
|
DEFAULT_BASE_URL = "https://api.sharpapi.io"
|
|
17
22
|
DEFAULT_TIMEOUT = 30.0
|
|
18
|
-
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"
|
|
29
|
+
|
|
30
|
+
RETRY_STATUSES = frozenset({502, 503, 504})
|
|
31
|
+
RETRY_MAX_ATTEMPTS = 3
|
|
32
|
+
RETRY_BASE_DELAY = 0.5
|
|
33
|
+
RETRY_MAX_DELAY = 4.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def should_retry(response: httpx.Response | None, exc: Exception | None) -> bool:
|
|
37
|
+
"""True for transient upstream failures worth retrying."""
|
|
38
|
+
if exc is not None:
|
|
39
|
+
return isinstance(exc, (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError))
|
|
40
|
+
return response is not None and response.status_code in RETRY_STATUSES
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def retry_delay(attempt: int) -> float:
|
|
44
|
+
"""Exponential backoff with full jitter. attempt is 1-indexed."""
|
|
45
|
+
ceiling = min(RETRY_BASE_DELAY * (2 ** (attempt - 1)), RETRY_MAX_DELAY)
|
|
46
|
+
return random.uniform(0, ceiling)
|
|
19
47
|
|
|
20
48
|
|
|
21
49
|
def parse_response(raw: dict, model_class: type) -> APIResponse:
|
|
@@ -70,6 +98,30 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
70
98
|
code = body.get("code", "unknown_error")
|
|
71
99
|
status = response.status_code
|
|
72
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.
|
|
73
125
|
if status == 401:
|
|
74
126
|
raise AuthenticationError(error_msg, code=code, status=status)
|
|
75
127
|
elif status == 403:
|
|
@@ -92,13 +144,28 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
92
144
|
raise SharpAPIError(error_msg, code=code, status=status)
|
|
93
145
|
|
|
94
146
|
|
|
95
|
-
def make_headers(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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] = {
|
|
99
161
|
"Content-Type": "application/json",
|
|
100
162
|
"User-Agent": USER_AGENT,
|
|
101
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
|
|
102
169
|
|
|
103
170
|
|
|
104
171
|
def _int_or_none(value: str | None) -> int | None:
|
sharpapi/async_client.py
CHANGED
|
@@ -2,32 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
|
|
9
10
|
from ._base import (
|
|
11
|
+
DEFAULT_AUTH_METHOD,
|
|
10
12
|
DEFAULT_BASE_URL,
|
|
11
13
|
DEFAULT_TIMEOUT,
|
|
14
|
+
RETRY_MAX_ATTEMPTS,
|
|
15
|
+
AuthMethod,
|
|
12
16
|
handle_errors,
|
|
13
17
|
make_headers,
|
|
14
18
|
parse_rate_limit,
|
|
15
19
|
parse_response,
|
|
20
|
+
retry_delay,
|
|
21
|
+
should_retry,
|
|
16
22
|
)
|
|
17
23
|
from ._utils import _clean_params
|
|
18
24
|
from .models import (
|
|
19
|
-
APIResponse,
|
|
20
25
|
AccountInfo,
|
|
26
|
+
APIKey,
|
|
27
|
+
APIResponse,
|
|
21
28
|
ArbitrageOpportunity,
|
|
22
|
-
|
|
29
|
+
ClosingSnapshot,
|
|
23
30
|
Event,
|
|
31
|
+
EVOpportunity,
|
|
24
32
|
League,
|
|
25
33
|
LowHoldOpportunity,
|
|
34
|
+
Market,
|
|
26
35
|
MiddleOpportunity,
|
|
27
36
|
OddsLine,
|
|
28
37
|
RateLimitInfo,
|
|
29
|
-
Sportsbook,
|
|
30
38
|
Sport,
|
|
39
|
+
Sportsbook,
|
|
31
40
|
)
|
|
32
41
|
|
|
33
42
|
|
|
@@ -37,6 +46,17 @@ class AsyncSharpAPI:
|
|
|
37
46
|
Provides typed access to odds, +EV, arbitrage, middles, and streaming
|
|
38
47
|
endpoints using ``async``/``await``.
|
|
39
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
|
+
|
|
40
60
|
Example::
|
|
41
61
|
|
|
42
62
|
import asyncio
|
|
@@ -48,6 +68,10 @@ class AsyncSharpAPI:
|
|
|
48
68
|
for arb in arbs.data:
|
|
49
69
|
print(f"{arb.profit_percent}% — {arb.event_name}")
|
|
50
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
|
+
|
|
51
75
|
asyncio.run(main())
|
|
52
76
|
"""
|
|
53
77
|
|
|
@@ -57,16 +81,18 @@ class AsyncSharpAPI:
|
|
|
57
81
|
*,
|
|
58
82
|
base_url: str = DEFAULT_BASE_URL,
|
|
59
83
|
timeout: float = DEFAULT_TIMEOUT,
|
|
84
|
+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
|
|
60
85
|
):
|
|
61
86
|
if not api_key:
|
|
62
87
|
raise ValueError("api_key is required")
|
|
63
88
|
|
|
64
89
|
self._api_key = api_key
|
|
90
|
+
self._auth_method: AuthMethod = auth_method
|
|
65
91
|
self._base_url = base_url.rstrip("/")
|
|
66
92
|
self._timeout = timeout
|
|
67
93
|
self._http = httpx.AsyncClient(
|
|
68
94
|
base_url=f"{self._base_url}/api/v1",
|
|
69
|
-
headers=make_headers(api_key),
|
|
95
|
+
headers=make_headers(api_key, auth_method),
|
|
70
96
|
timeout=timeout,
|
|
71
97
|
)
|
|
72
98
|
self._last_rate_limit = RateLimitInfo()
|
|
@@ -82,6 +108,7 @@ class AsyncSharpAPI:
|
|
|
82
108
|
self.sportsbooks = _AsyncSportsbooksResource(self)
|
|
83
109
|
self.events = _AsyncEventsResource(self)
|
|
84
110
|
self.account = _AsyncAccountResource(self)
|
|
111
|
+
self.keys = _AsyncKeysResource(self)
|
|
85
112
|
|
|
86
113
|
@property
|
|
87
114
|
def rate_limit(self) -> RateLimitInfo:
|
|
@@ -89,11 +116,26 @@ class AsyncSharpAPI:
|
|
|
89
116
|
return self._last_rate_limit
|
|
90
117
|
|
|
91
118
|
async def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
|
|
92
|
-
"""Make an async
|
|
119
|
+
"""Make an async request, return parsed JSON. Retries 502/503/504 with jittered backoff."""
|
|
93
120
|
if params:
|
|
94
121
|
params = _clean_params(params)
|
|
95
122
|
|
|
96
|
-
response
|
|
123
|
+
response: httpx.Response | None = None
|
|
124
|
+
for attempt in range(1, RETRY_MAX_ATTEMPTS + 1):
|
|
125
|
+
exc: Exception | None = None
|
|
126
|
+
try:
|
|
127
|
+
response = await self._http.request(method, path, params=params, **kwargs)
|
|
128
|
+
except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError) as e:
|
|
129
|
+
exc = e
|
|
130
|
+
|
|
131
|
+
if attempt < RETRY_MAX_ATTEMPTS and should_retry(response, exc):
|
|
132
|
+
await asyncio.sleep(retry_delay(attempt))
|
|
133
|
+
continue
|
|
134
|
+
if exc is not None:
|
|
135
|
+
raise exc
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
assert response is not None
|
|
97
139
|
self._last_rate_limit = parse_rate_limit(response)
|
|
98
140
|
handle_errors(response)
|
|
99
141
|
return response.json()
|
|
@@ -129,18 +171,18 @@ class _AsyncOddsResource:
|
|
|
129
171
|
async def get(
|
|
130
172
|
self,
|
|
131
173
|
*,
|
|
132
|
-
sportsbook:
|
|
133
|
-
add_sportsbook:
|
|
134
|
-
sport:
|
|
135
|
-
league:
|
|
136
|
-
market:
|
|
137
|
-
event:
|
|
138
|
-
live:
|
|
139
|
-
sort:
|
|
140
|
-
group_by:
|
|
141
|
-
fields:
|
|
142
|
-
limit:
|
|
143
|
-
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,
|
|
144
186
|
) -> APIResponse[list[OddsLine]]:
|
|
145
187
|
"""Get current odds snapshot."""
|
|
146
188
|
data = await self._client._get("/odds", {
|
|
@@ -162,15 +204,15 @@ class _AsyncOddsResource:
|
|
|
162
204
|
async def best(
|
|
163
205
|
self,
|
|
164
206
|
*,
|
|
165
|
-
sport:
|
|
166
|
-
league:
|
|
167
|
-
market:
|
|
168
|
-
event:
|
|
169
|
-
live:
|
|
170
|
-
sportsbook:
|
|
171
|
-
add_sportsbook:
|
|
172
|
-
limit:
|
|
173
|
-
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,
|
|
174
216
|
) -> APIResponse[list[OddsLine]]:
|
|
175
217
|
"""Get best odds per selection across all sportsbooks."""
|
|
176
218
|
data = await self._client._get("/odds/best", {
|
|
@@ -190,7 +232,7 @@ class _AsyncOddsResource:
|
|
|
190
232
|
self,
|
|
191
233
|
event_id: str,
|
|
192
234
|
*,
|
|
193
|
-
market:
|
|
235
|
+
market: str | None = None,
|
|
194
236
|
) -> APIResponse[list[OddsLine]]:
|
|
195
237
|
"""Get side-by-side odds comparison for an event."""
|
|
196
238
|
data = await self._client._get("/odds/comparison", {
|
|
@@ -204,6 +246,25 @@ class _AsyncOddsResource:
|
|
|
204
246
|
data = await self._client._post("/odds/batch", {"event_ids": event_ids})
|
|
205
247
|
return parse_response(data, OddsLine)
|
|
206
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
|
+
|
|
207
268
|
|
|
208
269
|
class _AsyncEVResource:
|
|
209
270
|
"""Async access to +EV opportunities."""
|
|
@@ -214,21 +275,21 @@ class _AsyncEVResource:
|
|
|
214
275
|
async def get(
|
|
215
276
|
self,
|
|
216
277
|
*,
|
|
217
|
-
sport:
|
|
218
|
-
league:
|
|
219
|
-
sportsbook:
|
|
220
|
-
add_sportsbook:
|
|
221
|
-
market:
|
|
222
|
-
min_ev:
|
|
223
|
-
max_ev:
|
|
224
|
-
min_market_width:
|
|
225
|
-
max_market_width:
|
|
226
|
-
max_odds_age:
|
|
227
|
-
date_range:
|
|
228
|
-
live:
|
|
229
|
-
sort:
|
|
230
|
-
limit:
|
|
231
|
-
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,
|
|
232
293
|
) -> APIResponse[list[EVOpportunity]]:
|
|
233
294
|
"""Get +EV opportunities. Requires Pro tier or higher."""
|
|
234
295
|
data = await self._client._get("/opportunities/ev", {
|
|
@@ -260,18 +321,18 @@ class _AsyncArbitrageResource:
|
|
|
260
321
|
async def get(
|
|
261
322
|
self,
|
|
262
323
|
*,
|
|
263
|
-
sport:
|
|
264
|
-
league:
|
|
265
|
-
sportsbook:
|
|
266
|
-
add_sportsbook:
|
|
267
|
-
market:
|
|
268
|
-
min_profit:
|
|
269
|
-
max_odds_age:
|
|
270
|
-
live:
|
|
271
|
-
sort:
|
|
272
|
-
group:
|
|
273
|
-
limit:
|
|
274
|
-
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,
|
|
275
336
|
) -> APIResponse[list[ArbitrageOpportunity]]:
|
|
276
337
|
"""Get arbitrage opportunities. Requires Hobby tier or higher."""
|
|
277
338
|
data = await self._client._get("/opportunities/arbitrage", {
|
|
@@ -300,17 +361,17 @@ class _AsyncMiddlesResource:
|
|
|
300
361
|
async def get(
|
|
301
362
|
self,
|
|
302
363
|
*,
|
|
303
|
-
sport:
|
|
304
|
-
league:
|
|
305
|
-
sportsbook:
|
|
306
|
-
market:
|
|
307
|
-
min_size:
|
|
308
|
-
max_odds_age:
|
|
309
|
-
live:
|
|
310
|
-
state:
|
|
311
|
-
sort:
|
|
312
|
-
limit:
|
|
313
|
-
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,
|
|
314
375
|
) -> APIResponse[list[MiddleOpportunity]]:
|
|
315
376
|
"""Get middle opportunities. Requires Pro tier or higher."""
|
|
316
377
|
data = await self._client._get("/opportunities/middles", {
|
|
@@ -338,16 +399,16 @@ class _AsyncLowHoldResource:
|
|
|
338
399
|
async def get(
|
|
339
400
|
self,
|
|
340
401
|
*,
|
|
341
|
-
sport:
|
|
342
|
-
league:
|
|
343
|
-
sportsbook:
|
|
344
|
-
market:
|
|
345
|
-
max_hold:
|
|
346
|
-
live:
|
|
347
|
-
state:
|
|
348
|
-
sort:
|
|
349
|
-
limit:
|
|
350
|
-
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,
|
|
351
412
|
) -> APIResponse[list[LowHoldOpportunity]]:
|
|
352
413
|
"""Get low-hold opportunities."""
|
|
353
414
|
data = await self._client._get("/opportunities/low_hold", {
|
|
@@ -385,7 +446,7 @@ class _AsyncLeaguesResource:
|
|
|
385
446
|
def __init__(self, client: AsyncSharpAPI):
|
|
386
447
|
self._client = client
|
|
387
448
|
|
|
388
|
-
async def list(self, *, sport:
|
|
449
|
+
async def list(self, *, sport: str | None = None) -> APIResponse[list[League]]:
|
|
389
450
|
"""List all leagues, optionally filtered by sport."""
|
|
390
451
|
data = await self._client._get("/leagues", {"sport": sport})
|
|
391
452
|
return parse_response(data, League)
|
|
@@ -420,11 +481,11 @@ class _AsyncEventsResource:
|
|
|
420
481
|
async def list(
|
|
421
482
|
self,
|
|
422
483
|
*,
|
|
423
|
-
sport:
|
|
424
|
-
league:
|
|
425
|
-
live:
|
|
426
|
-
limit:
|
|
427
|
-
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,
|
|
428
489
|
) -> APIResponse[list[Event]]:
|
|
429
490
|
"""List events."""
|
|
430
491
|
data = await self._client._get("/events", {
|
|
@@ -442,6 +503,11 @@ class _AsyncEventsResource:
|
|
|
442
503
|
raw = data.get("data", data)
|
|
443
504
|
return Event.model_validate(raw)
|
|
444
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
|
+
|
|
445
511
|
|
|
446
512
|
class _AsyncAccountResource:
|
|
447
513
|
def __init__(self, client: AsyncSharpAPI):
|
|
@@ -457,3 +523,33 @@ class _AsyncAccountResource:
|
|
|
457
523
|
"""Get current usage stats."""
|
|
458
524
|
data = await self._client._get("/account/usage")
|
|
459
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)
|