odds-api-io 1.0.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.
odds_api/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ Odds-API.io Python SDK
3
+ Official Python client for the Odds-API.io sports betting odds API.
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+
8
+ from .client import OddsAPIClient
9
+ from .async_client import AsyncOddsAPIClient
10
+ from .exceptions import (
11
+ OddsAPIError,
12
+ InvalidAPIKeyError,
13
+ RateLimitExceededError,
14
+ NotFoundError,
15
+ ValidationError,
16
+ )
17
+
18
+ __all__ = [
19
+ "OddsAPIClient",
20
+ "AsyncOddsAPIClient",
21
+ "OddsAPIError",
22
+ "InvalidAPIKeyError",
23
+ "RateLimitExceededError",
24
+ "NotFoundError",
25
+ "ValidationError",
26
+ "__version__",
27
+ ]
@@ -0,0 +1,502 @@
1
+ """Asynchronous client for the Odds-API.io API."""
2
+
3
+ from typing import Any, Dict, Optional, List
4
+ import aiohttp
5
+
6
+ from .constants import BASE_API_URL, DEFAULT_TIMEOUT, Endpoints
7
+ from .exceptions import (
8
+ InvalidAPIKeyError,
9
+ NotFoundError,
10
+ OddsAPIError,
11
+ RateLimitExceededError,
12
+ ValidationError,
13
+ )
14
+
15
+
16
+ class AsyncOddsAPIClient:
17
+ """
18
+ Asynchronous client for the Odds-API.io API.
19
+
20
+ Args:
21
+ api_key: Your Odds-API.io API key
22
+ timeout: Request timeout in seconds (default: 10)
23
+ base_url: Base API URL (default: https://api2.odds-api.io/v3)
24
+
25
+ Example:
26
+ >>> async with AsyncOddsAPIClient(api_key="your_api_key") as client:
27
+ ... sports = await client.get_sports()
28
+ ... events = await client.get_events(sport="basketball", league="usa-nba")
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ api_key: str,
34
+ timeout: int = DEFAULT_TIMEOUT,
35
+ base_url: str = BASE_API_URL,
36
+ ):
37
+ """Initialize the async Odds API client."""
38
+ if not api_key:
39
+ raise ValueError("API key is required")
40
+
41
+ self.api_key = api_key
42
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
43
+ self.base_url = base_url
44
+ self._session: Optional[aiohttp.ClientSession] = None
45
+
46
+ @property
47
+ def session(self) -> aiohttp.ClientSession:
48
+ """Get or create the aiohttp session."""
49
+ if self._session is None or self._session.closed:
50
+ self._session = aiohttp.ClientSession(timeout=self.timeout)
51
+ return self._session
52
+
53
+ async def _handle_response(self, response: aiohttp.ClientResponse) -> Any:
54
+ """Handle API response and raise appropriate exceptions."""
55
+ if response.status == 200:
56
+ return await response.json()
57
+
58
+ text = await response.text()
59
+ status = response.status
60
+
61
+ if status == 400:
62
+ raise ValidationError(f"Invalid request: {text}")
63
+ elif status == 401:
64
+ raise InvalidAPIKeyError("Invalid API key")
65
+ elif status == 404:
66
+ raise NotFoundError("Resource not found")
67
+ elif status == 429:
68
+ raise RateLimitExceededError(
69
+ "Rate limit exceeded - please wait before retrying"
70
+ )
71
+ else:
72
+ raise OddsAPIError(f"API error {status}: {text}")
73
+
74
+ async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
75
+ """Make a GET request to the API."""
76
+ url = f"{self.base_url}/{path}"
77
+ params = params or {}
78
+ params["apiKey"] = self.api_key
79
+
80
+ try:
81
+ async with self.session.get(url, params=params) as response:
82
+ return await self._handle_response(response)
83
+ except aiohttp.ClientError as e:
84
+ raise OddsAPIError(f"Request failed: {e}") from e
85
+
86
+ async def _put(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
87
+ """Make a PUT request to the API."""
88
+ url = f"{self.base_url}/{path}"
89
+ params = params or {}
90
+ params["apiKey"] = self.api_key
91
+
92
+ try:
93
+ async with self.session.put(url, params=params) as response:
94
+ return await self._handle_response(response)
95
+ except aiohttp.ClientError as e:
96
+ raise OddsAPIError(f"Request failed: {e}") from e
97
+
98
+ @staticmethod
99
+ def _build_params(**kwargs) -> Dict[str, Any]:
100
+ """Build parameter dictionary, excluding None values."""
101
+ return {k: v for k, v in kwargs.items() if v is not None}
102
+
103
+ # Sports & Leagues
104
+
105
+ async def get_sports(self) -> List[Dict[str, Any]]:
106
+ """
107
+ Get all available sports.
108
+
109
+ Returns:
110
+ List of sports with their details
111
+
112
+ Example:
113
+ >>> sports = await client.get_sports()
114
+ >>> for sport in sports:
115
+ ... print(sport['name'])
116
+ """
117
+ return await self._get(Endpoints.GET_SPORTS)
118
+
119
+ async def get_leagues(self, sport: str) -> List[Dict[str, Any]]:
120
+ """
121
+ Get all leagues for a specific sport.
122
+
123
+ Args:
124
+ sport: Sport identifier (e.g., "basketball", "football")
125
+
126
+ Returns:
127
+ List of leagues for the specified sport
128
+
129
+ Example:
130
+ >>> leagues = await client.get_leagues(sport="basketball")
131
+ """
132
+ params = self._build_params(sport=sport)
133
+ return await self._get(Endpoints.GET_LEAGUES, params)
134
+
135
+ # Events
136
+
137
+ async def get_events(
138
+ self,
139
+ sport: str,
140
+ league: Optional[str] = None,
141
+ participant_id: Optional[int] = None,
142
+ status: Optional[str] = None,
143
+ start: Optional[str] = None,
144
+ end: Optional[str] = None,
145
+ bookmaker: Optional[str] = None,
146
+ ) -> List[Dict[str, Any]]:
147
+ """
148
+ Get events with optional filters.
149
+
150
+ Args:
151
+ sport: Sport identifier (required)
152
+ league: League identifier
153
+ participant_id: Filter by participant ID
154
+ status: Event status (e.g., "upcoming", "live", "finished")
155
+ start: Start date/time filter (ISO 8601 format)
156
+ end: End date/time filter (ISO 8601 format)
157
+ bookmaker: Filter by bookmaker
158
+
159
+ Returns:
160
+ List of events matching the filters
161
+
162
+ Example:
163
+ >>> events = await client.get_events(
164
+ ... sport="basketball",
165
+ ... league="usa-nba",
166
+ ... status="upcoming"
167
+ ... )
168
+ """
169
+ params = self._build_params(
170
+ sport=sport,
171
+ league=league,
172
+ participantId=participant_id,
173
+ status=status,
174
+ bookmaker=bookmaker,
175
+ )
176
+
177
+ # Map start/end to from/to
178
+ if start:
179
+ params["from"] = start
180
+ if end:
181
+ params["to"] = end
182
+
183
+ return await self._get(Endpoints.GET_EVENTS, params)
184
+
185
+ async def get_event_by_id(self, event_id: int) -> Dict[str, Any]:
186
+ """
187
+ Get a specific event by ID.
188
+
189
+ Args:
190
+ event_id: The event ID
191
+
192
+ Returns:
193
+ Event details
194
+
195
+ Example:
196
+ >>> event = await client.get_event_by_id(event_id=12345)
197
+ """
198
+ path = Endpoints.GET_EVENT_BY_ID.format(id=event_id)
199
+ return await self._get(path)
200
+
201
+ async def get_live_events(self, sport: str) -> List[Dict[str, Any]]:
202
+ """
203
+ Get currently live events for a sport.
204
+
205
+ Args:
206
+ sport: Sport identifier
207
+
208
+ Returns:
209
+ List of live events
210
+
211
+ Example:
212
+ >>> live_events = await client.get_live_events(sport="basketball")
213
+ """
214
+ params = self._build_params(sport=sport)
215
+ return await self._get(Endpoints.GET_LIVE_EVENTS, params)
216
+
217
+ async def search_events(self, query: str) -> List[Dict[str, Any]]:
218
+ """
219
+ Search for events by keyword.
220
+
221
+ Args:
222
+ query: Search query
223
+
224
+ Returns:
225
+ List of matching events
226
+
227
+ Example:
228
+ >>> events = await client.search_events(query="Lakers")
229
+ """
230
+ params = self._build_params(query=query)
231
+ return await self._get(Endpoints.SEARCH_EVENTS, params)
232
+
233
+ # Odds
234
+
235
+ async def get_event_odds(
236
+ self, event_id: str, bookmakers: str
237
+ ) -> Dict[str, Any]:
238
+ """
239
+ Get odds for a specific event.
240
+
241
+ Args:
242
+ event_id: Event ID
243
+ bookmakers: Comma-separated bookmaker slugs
244
+
245
+ Returns:
246
+ Odds data for the event
247
+
248
+ Example:
249
+ >>> odds = await client.get_event_odds(
250
+ ... event_id="12345",
251
+ ... bookmakers="pinnacle,bet365"
252
+ ... )
253
+ """
254
+ params = self._build_params(eventId=event_id, bookmakers=bookmakers)
255
+ return await self._get(Endpoints.GET_EVENT_ODDS, params)
256
+
257
+ async def get_odds_movement(
258
+ self,
259
+ event_id: str,
260
+ bookmaker: str,
261
+ market: str,
262
+ market_line: Optional[str] = None,
263
+ ) -> List[Dict[str, Any]]:
264
+ """
265
+ Track odds changes over time for an event.
266
+
267
+ Args:
268
+ event_id: Event ID
269
+ bookmaker: Bookmaker slug
270
+ market: Market type (e.g., "moneyline", "spread")
271
+ market_line: Market line (optional, for handicap markets)
272
+
273
+ Returns:
274
+ List of odds movements
275
+
276
+ Example:
277
+ >>> movements = await client.get_odds_movement(
278
+ ... event_id="12345",
279
+ ... bookmaker="pinnacle",
280
+ ... market="moneyline"
281
+ ... )
282
+ """
283
+ params = self._build_params(
284
+ eventId=event_id,
285
+ bookmaker=bookmaker,
286
+ market=market,
287
+ marketLine=market_line,
288
+ )
289
+ return await self._get(Endpoints.GET_ODDS_MOVEMENT, params)
290
+
291
+ async def get_odds_for_multiple_events(
292
+ self, event_ids: str, bookmakers: str
293
+ ) -> List[Dict[str, Any]]:
294
+ """
295
+ Get odds for multiple events at once.
296
+
297
+ Args:
298
+ event_ids: Comma-separated event IDs
299
+ bookmakers: Comma-separated bookmaker slugs
300
+
301
+ Returns:
302
+ Odds data for multiple events
303
+
304
+ Example:
305
+ >>> odds = await client.get_odds_for_multiple_events(
306
+ ... event_ids="12345,67890",
307
+ ... bookmakers="pinnacle,bet365"
308
+ ... )
309
+ """
310
+ params = self._build_params(eventIds=event_ids, bookmakers=bookmakers)
311
+ return await self._get(Endpoints.GET_ODDS_FOR_MULTIPLE_EVENTS, params)
312
+
313
+ async def get_updated_odds_since_timestamp(
314
+ self, since: int, bookmaker: str, sport: str
315
+ ) -> List[Dict[str, Any]]:
316
+ """
317
+ Get odds updated since a given timestamp.
318
+
319
+ Args:
320
+ since: Unix timestamp
321
+ bookmaker: Bookmaker slug
322
+ sport: Sport identifier
323
+
324
+ Returns:
325
+ List of updated odds
326
+
327
+ Example:
328
+ >>> updated = await client.get_updated_odds_since_timestamp(
329
+ ... since=1640000000,
330
+ ... bookmaker="pinnacle",
331
+ ... sport="basketball"
332
+ ... )
333
+ """
334
+ params = self._build_params(since=since, bookmaker=bookmaker, sport=sport)
335
+ return await self._get(Endpoints.GET_UPDATED_ODDS_SINCE_TIMESTAMP, params)
336
+
337
+ # Participants
338
+
339
+ async def get_participants(
340
+ self, sport: str, search: Optional[str] = None
341
+ ) -> List[Dict[str, Any]]:
342
+ """
343
+ Get participants (teams/players) for a sport.
344
+
345
+ Args:
346
+ sport: Sport identifier
347
+ search: Optional search query
348
+
349
+ Returns:
350
+ List of participants
351
+
352
+ Example:
353
+ >>> participants = await client.get_participants(
354
+ ... sport="basketball",
355
+ ... search="Warriors"
356
+ ... )
357
+ """
358
+ params = self._build_params(sport=sport, search=search)
359
+ return await self._get(Endpoints.GET_PARTICIPANTS, params)
360
+
361
+ async def get_participant_by_id(self, participant_id: int) -> Dict[str, Any]:
362
+ """
363
+ Get a specific participant by ID.
364
+
365
+ Args:
366
+ participant_id: Participant ID
367
+
368
+ Returns:
369
+ Participant details
370
+
371
+ Example:
372
+ >>> participant = await client.get_participant_by_id(participant_id=3428)
373
+ """
374
+ path = Endpoints.GET_PARTICIPANT_BY_ID.format(id=participant_id)
375
+ return await self._get(path)
376
+
377
+ # Bookmakers
378
+
379
+ async def get_bookmakers(self) -> List[Dict[str, Any]]:
380
+ """
381
+ Get all available bookmakers.
382
+
383
+ Returns:
384
+ List of bookmakers with their details
385
+
386
+ Example:
387
+ >>> bookmakers = await client.get_bookmakers()
388
+ """
389
+ return await self._get(Endpoints.GET_BOOKMAKERS)
390
+
391
+ async def get_selected_bookmakers(self) -> Dict[str, Any]:
392
+ """
393
+ Get bookmakers currently selected for your account.
394
+
395
+ Returns:
396
+ Selected bookmakers configuration
397
+
398
+ Example:
399
+ >>> selected = await client.get_selected_bookmakers()
400
+ """
401
+ return await self._get(Endpoints.GET_SELECTED_BOOKMAKERS)
402
+
403
+ async def select_bookmakers(self, bookmakers: str) -> Dict[str, Any]:
404
+ """
405
+ Select bookmakers for your account.
406
+
407
+ Args:
408
+ bookmakers: Comma-separated bookmaker slugs
409
+
410
+ Returns:
411
+ Updated bookmaker selection
412
+
413
+ Example:
414
+ >>> await client.select_bookmakers(bookmakers="pinnacle,bet365")
415
+ """
416
+ params = self._build_params(bookmakers=bookmakers)
417
+ return await self._put(Endpoints.SELECT_BOOKMAKERS, params)
418
+
419
+ async def clear_selected_bookmakers(self) -> Dict[str, Any]:
420
+ """
421
+ Clear all selected bookmakers.
422
+
423
+ Returns:
424
+ Confirmation response
425
+
426
+ Example:
427
+ >>> await client.clear_selected_bookmakers()
428
+ """
429
+ return await self._put(Endpoints.CLEAR_SELECTED_BOOKMAKERS)
430
+
431
+ # Betting Analysis
432
+
433
+ async def get_arbitrage_bets(
434
+ self,
435
+ bookmakers: str,
436
+ limit: Optional[int] = None,
437
+ include_event_details: Optional[bool] = None,
438
+ ) -> List[Dict[str, Any]]:
439
+ """
440
+ Find arbitrage betting opportunities.
441
+
442
+ Args:
443
+ bookmakers: Comma-separated bookmaker slugs
444
+ limit: Maximum number of results
445
+ include_event_details: Include full event details
446
+
447
+ Returns:
448
+ List of arbitrage opportunities
449
+
450
+ Example:
451
+ >>> arb_bets = await client.get_arbitrage_bets(
452
+ ... bookmakers="pinnacle,bet365",
453
+ ... limit=10,
454
+ ... include_event_details=True
455
+ ... )
456
+ """
457
+ params = self._build_params(
458
+ bookmakers=bookmakers,
459
+ limit=limit,
460
+ includeEventDetails=include_event_details,
461
+ )
462
+ return await self._get(Endpoints.GET_ARBITRAGE_BETS, params)
463
+
464
+ async def get_value_bets(
465
+ self,
466
+ bookmaker: str,
467
+ include_event_details: Optional[bool] = None,
468
+ ) -> List[Dict[str, Any]]:
469
+ """
470
+ Find value betting opportunities.
471
+
472
+ Args:
473
+ bookmaker: Bookmaker slug
474
+ include_event_details: Include full event details
475
+
476
+ Returns:
477
+ List of value bets
478
+
479
+ Example:
480
+ >>> value_bets = await client.get_value_bets(
481
+ ... bookmaker="pinnacle",
482
+ ... include_event_details=True
483
+ ... )
484
+ """
485
+ params = self._build_params(
486
+ bookmaker=bookmaker,
487
+ includeEventDetails=include_event_details,
488
+ )
489
+ return await self._get(Endpoints.GET_VALUE_BETS, params)
490
+
491
+ async def close(self) -> None:
492
+ """Close the HTTP session."""
493
+ if self._session and not self._session.closed:
494
+ await self._session.close()
495
+
496
+ async def __aenter__(self):
497
+ """Async context manager entry."""
498
+ return self
499
+
500
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
501
+ """Async context manager exit."""
502
+ await self.close()