sharpapi 0.2.0__py3-none-any.whl → 0.2.2__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
@@ -51,7 +51,7 @@ from .models import (
51
51
  from .streaming import EventStream
52
52
  from ._utils import american_to_decimal, american_to_probability, decimal_to_american
53
53
 
54
- __version__ = "0.2.0"
54
+ __version__ = "0.2.1"
55
55
 
56
56
  __all__ = [
57
57
  # Clients
sharpapi/_base.py CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import random
6
+
5
7
  import httpx
6
8
 
7
9
  from .exceptions import (
@@ -15,7 +17,25 @@ from .models import APIResponse, RateLimitInfo, ResponseMeta
15
17
 
16
18
  DEFAULT_BASE_URL = "https://api.sharpapi.io"
17
19
  DEFAULT_TIMEOUT = 30.0
18
- USER_AGENT = "sharpapi-python/0.2.0"
20
+ USER_AGENT = "sharpapi-python/0.2.2"
21
+
22
+ RETRY_STATUSES = frozenset({502, 503, 504})
23
+ RETRY_MAX_ATTEMPTS = 3
24
+ RETRY_BASE_DELAY = 0.5
25
+ RETRY_MAX_DELAY = 4.0
26
+
27
+
28
+ def should_retry(response: httpx.Response | None, exc: Exception | None) -> bool:
29
+ """True for transient upstream failures worth retrying."""
30
+ if exc is not None:
31
+ return isinstance(exc, (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError))
32
+ return response is not None and response.status_code in RETRY_STATUSES
33
+
34
+
35
+ def retry_delay(attempt: int) -> float:
36
+ """Exponential backoff with full jitter. attempt is 1-indexed."""
37
+ ceiling = min(RETRY_BASE_DELAY * (2 ** (attempt - 1)), RETRY_MAX_DELAY)
38
+ return random.uniform(0, ceiling)
19
39
 
20
40
 
21
41
  def parse_response(raw: dict, model_class: type) -> APIResponse:
sharpapi/async_client.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  from typing import Any, Optional, Union
6
7
 
7
8
  import httpx
@@ -9,10 +10,13 @@ import httpx
9
10
  from ._base import (
10
11
  DEFAULT_BASE_URL,
11
12
  DEFAULT_TIMEOUT,
13
+ RETRY_MAX_ATTEMPTS,
12
14
  handle_errors,
13
15
  make_headers,
14
16
  parse_rate_limit,
15
17
  parse_response,
18
+ retry_delay,
19
+ should_retry,
16
20
  )
17
21
  from ._utils import _clean_params
18
22
  from .models import (
@@ -89,11 +93,26 @@ class AsyncSharpAPI:
89
93
  return self._last_rate_limit
90
94
 
91
95
  async def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
92
- """Make an async API request and return parsed JSON."""
96
+ """Make an async API request and return parsed JSON. Retries 502/503/504 with jittered backoff."""
93
97
  if params:
94
98
  params = _clean_params(params)
95
99
 
96
- response = await self._http.request(method, path, params=params, **kwargs)
100
+ response: httpx.Response | None = None
101
+ for attempt in range(1, RETRY_MAX_ATTEMPTS + 1):
102
+ exc: Exception | None = None
103
+ try:
104
+ response = await self._http.request(method, path, params=params, **kwargs)
105
+ except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError) as e:
106
+ exc = e
107
+
108
+ if attempt < RETRY_MAX_ATTEMPTS and should_retry(response, exc):
109
+ await asyncio.sleep(retry_delay(attempt))
110
+ continue
111
+ if exc is not None:
112
+ raise exc
113
+ break
114
+
115
+ assert response is not None
97
116
  self._last_rate_limit = parse_rate_limit(response)
98
117
  handle_errors(response)
99
118
  return response.json()
@@ -442,11 +461,6 @@ class _AsyncEventsResource:
442
461
  raw = data.get("data", data)
443
462
  return Event.model_validate(raw)
444
463
 
445
- async def search(self, query: str) -> APIResponse[list[Event]]:
446
- """Search events by name."""
447
- data = await self._client._get("/events/search", {"q": query})
448
- return parse_response(data, Event)
449
-
450
464
 
451
465
  class _AsyncAccountResource:
452
466
  def __init__(self, client: AsyncSharpAPI):
sharpapi/client.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import time
5
6
  from typing import Any, Optional, Union
6
7
 
7
8
  import httpx
@@ -9,10 +10,13 @@ import httpx
9
10
  from ._base import (
10
11
  DEFAULT_BASE_URL,
11
12
  DEFAULT_TIMEOUT,
13
+ RETRY_MAX_ATTEMPTS,
12
14
  handle_errors,
13
15
  make_headers,
14
16
  parse_rate_limit,
15
17
  parse_response,
18
+ retry_delay,
19
+ should_retry,
16
20
  )
17
21
  from ._utils import _clean_params
18
22
  from .models import (
@@ -99,11 +103,26 @@ class SharpAPI:
99
103
  return self._last_rate_limit
100
104
 
101
105
  def _request(self, method: str, path: str, params: dict | None = None, **kwargs) -> Any:
102
- """Make an API request and return parsed JSON."""
106
+ """Make an API request and return parsed JSON. Retries 502/503/504 with jittered backoff."""
103
107
  if params:
104
108
  params = _clean_params(params)
105
109
 
106
- response = self._http.request(method, path, params=params, **kwargs)
110
+ response: httpx.Response | None = None
111
+ for attempt in range(1, RETRY_MAX_ATTEMPTS + 1):
112
+ exc: Exception | None = None
113
+ try:
114
+ response = self._http.request(method, path, params=params, **kwargs)
115
+ except (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError) as e:
116
+ exc = e
117
+
118
+ if attempt < RETRY_MAX_ATTEMPTS and should_retry(response, exc):
119
+ time.sleep(retry_delay(attempt))
120
+ continue
121
+ if exc is not None:
122
+ raise exc
123
+ break
124
+
125
+ assert response is not None
107
126
  self._last_rate_limit = parse_rate_limit(response)
108
127
  handle_errors(response)
109
128
  return response.json()
@@ -555,11 +574,6 @@ class _EventsResource:
555
574
  raw = data.get("data", data)
556
575
  return Event.model_validate(raw)
557
576
 
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
577
 
564
578
  class _AccountResource:
565
579
  def __init__(self, client: SharpAPI):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sharpapi
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Official Python SDK for the SharpAPI real-time sports betting odds API
5
5
  Project-URL: Homepage, https://sharpapi.io
6
6
  Project-URL: Documentation, https://docs.sharpapi.io/sdks/python
@@ -13,14 +13,13 @@ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
21
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.9
22
+ Requires-Python: >=3.10
24
23
  Requires-Dist: httpx>=0.25.0
25
24
  Requires-Dist: pydantic>=2.0.0
26
25
  Provides-Extra: pandas
@@ -118,7 +117,6 @@ client.sports.list()
118
117
  client.leagues.list(sport="basketball")
119
118
  client.sportsbooks.list()
120
119
  client.events.list(league="nba", live=True)
121
- client.events.search("Lakers")
122
120
 
123
121
  # Account
124
122
  client.account.me() # Tier, limits, features
@@ -0,0 +1,12 @@
1
+ sharpapi/__init__.py,sha256=dCb0vGKwVAMd8nCaLTfo_m5IyV_yymY4N9bjTborKLU,1974
2
+ sharpapi/_base.py,sha256=lSV5MiGI02rB_fMX3oXlbqrckwkFyRbwecX7NyAep7A,4045
3
+ sharpapi/_utils.py,sha256=nQU1gNkzepAIr93HDYX455aRO2yhc6BeBbaWDnpI5lw,1143
4
+ sharpapi/async_client.py,sha256=yT45nbCWMxkMba11AtLM8gbst55JDM99ooEGhYQkl10,15825
5
+ sharpapi/client.py,sha256=f_PTMUBy8bFs_Q-PKDRIe-CfmfHOMG9Ln4HdLmwExaU,22973
6
+ sharpapi/exceptions.py,sha256=nseJ4BboGjSWIfDtMYQpXPIaOUxbPAJLsxUPbERZqpY,1272
7
+ sharpapi/models.py,sha256=z9IIQd2dQKeEQTzZ8-qF8J5_L0GwT-8k2r3Y3ozXJ0U,12951
8
+ sharpapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ sharpapi/streaming.py,sha256=smQi9F-m7ET7s7V_psdg3S-butiuG_bIq3CnOE1zG8M,6971
10
+ sharpapi-0.2.2.dist-info/METADATA,sha256=YuRZC64xa9GMLLaE4-qL-QbqerLNwrAMD2aU78ODwpQ,5715
11
+ sharpapi-0.2.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ sharpapi-0.2.2.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- sharpapi/__init__.py,sha256=thLONzlOusuGL1M7NiE7PVzO_nQgI-ZGAIx59mxE5oE,1974
2
- sharpapi/_base.py,sha256=roQjRMVz0NxTZGc303tkQs5iWwcGyg-LUppdIC1p0eI,3348
3
- sharpapi/_utils.py,sha256=nQU1gNkzepAIr93HDYX455aRO2yhc6BeBbaWDnpI5lw,1143
4
- sharpapi/async_client.py,sha256=MrNM01ZTUcnSSoeJpw6Z_77-5iWi1kNHyIW58wJvPZc,15370
5
- sharpapi/client.py,sha256=V80lCdTQvN-yt-RdRfeT0VQGxtf9NqJpFCejLRksHp4,22519
6
- sharpapi/exceptions.py,sha256=nseJ4BboGjSWIfDtMYQpXPIaOUxbPAJLsxUPbERZqpY,1272
7
- sharpapi/models.py,sha256=z9IIQd2dQKeEQTzZ8-qF8J5_L0GwT-8k2r3Y3ozXJ0U,12951
8
- sharpapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- sharpapi/streaming.py,sha256=smQi9F-m7ET7s7V_psdg3S-butiuG_bIq3CnOE1zG8M,6971
10
- sharpapi-0.2.0.dist-info/METADATA,sha256=N_CF_re06FgeeqmNPFDtbcC9azO_iB9MOzD0XWo9-wE,5795
11
- sharpapi-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
- sharpapi-0.2.0.dist-info/RECORD,,