sharpapi 0.1.0__py3-none-any.whl

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