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 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
- EVOpportunity,
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.2.1"
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.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(api_key: str) -> dict[str, str]:
116
- """Build default request headers."""
117
- return {
118
- "X-API-Key": api_key,
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, Optional, Union
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
- EVOpportunity,
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 API request and return parsed JSON. Retries 502/503/504 with jittered backoff."""
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: Optional[Union[str, list[str]]] = None,
152
- add_sportsbook: Optional[Union[str, list[str]]] = None,
153
- sport: Optional[Union[str, list[str]]] = None,
154
- league: Optional[Union[str, list[str]]] = None,
155
- market: Optional[Union[str, list[str]]] = None,
156
- event: Optional[Union[str, list[str]]] = None,
157
- live: Optional[bool] = None,
158
- sort: Optional[str] = None,
159
- group_by: Optional[str] = None,
160
- fields: Optional[Union[str, list[str]]] = None,
161
- limit: Optional[int] = None,
162
- offset: Optional[int] = None,
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: Optional[Union[str, list[str]]] = None,
185
- league: Optional[Union[str, list[str]]] = None,
186
- market: Optional[Union[str, list[str]]] = None,
187
- event: Optional[Union[str, list[str]]] = None,
188
- live: Optional[bool] = None,
189
- sportsbook: Optional[Union[str, list[str]]] = None,
190
- add_sportsbook: Optional[Union[str, list[str]]] = None,
191
- limit: Optional[int] = None,
192
- offset: Optional[int] = None,
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: Optional[str] = None,
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: Optional[Union[str, list[str]]] = None,
237
- league: Optional[Union[str, list[str]]] = None,
238
- sportsbook: Optional[Union[str, list[str]]] = None,
239
- add_sportsbook: Optional[Union[str, list[str]]] = None,
240
- market: Optional[Union[str, list[str]]] = None,
241
- min_ev: Optional[float] = None,
242
- max_ev: Optional[float] = None,
243
- min_market_width: Optional[float] = None,
244
- max_market_width: Optional[float] = None,
245
- max_odds_age: Optional[int] = None,
246
- date_range: Optional[str] = None,
247
- live: Optional[bool] = None,
248
- sort: Optional[str] = None,
249
- limit: Optional[int] = None,
250
- offset: Optional[int] = None,
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: Optional[Union[str, list[str]]] = None,
283
- league: Optional[Union[str, list[str]]] = None,
284
- sportsbook: Optional[Union[str, list[str]]] = None,
285
- add_sportsbook: Optional[Union[str, list[str]]] = None,
286
- market: Optional[Union[str, list[str]]] = None,
287
- min_profit: Optional[float] = None,
288
- max_odds_age: Optional[int] = None,
289
- live: Optional[bool] = None,
290
- sort: Optional[str] = None,
291
- group: Optional[str] = None,
292
- limit: Optional[int] = None,
293
- offset: Optional[int] = None,
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: Optional[Union[str, list[str]]] = None,
323
- league: Optional[Union[str, list[str]]] = None,
324
- sportsbook: Optional[Union[str, list[str]]] = None,
325
- market: Optional[Union[str, list[str]]] = None,
326
- min_size: Optional[float] = None,
327
- max_odds_age: Optional[int] = None,
328
- live: Optional[bool] = None,
329
- state: Optional[str] = None,
330
- sort: Optional[str] = None,
331
- limit: Optional[int] = None,
332
- offset: Optional[int] = None,
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: Optional[Union[str, list[str]]] = None,
361
- league: Optional[Union[str, list[str]]] = None,
362
- sportsbook: Optional[Union[str, list[str]]] = None,
363
- market: Optional[Union[str, list[str]]] = None,
364
- max_hold: Optional[float] = None,
365
- live: Optional[bool] = None,
366
- state: Optional[str] = None,
367
- sort: Optional[str] = None,
368
- limit: Optional[int] = None,
369
- offset: Optional[int] = None,
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: Optional[str] = None) -> APIResponse[list[League]]:
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: Optional[str] = None,
443
- league: Optional[Union[str, list[str]]] = None,
444
- live: Optional[bool] = None,
445
- limit: Optional[int] = None,
446
- offset: Optional[int] = None,
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)