sharpapi 0.2.0__tar.gz → 0.2.2__tar.gz

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.
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v4
@@ -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
@@ -85,7 +85,6 @@ client.sports.list()
85
85
  client.leagues.list(sport="basketball")
86
86
  client.sportsbooks.list()
87
87
  client.events.list(league="nba", live=True)
88
- client.events.search("Lakers")
89
88
 
90
89
  # Account
91
90
  client.account.me() # Tier, limits, features
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sharpapi"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
- requires-python = ">=3.9"
11
+ requires-python = ">=3.10"
12
12
  authors = [{ name = "SharpAPI", email = "support@sharpapi.io" }]
13
13
  keywords = ["sports-betting", "odds", "arbitrage", "ev", "api", "real-time", "pinnacle"]
14
14
  classifiers = [
@@ -16,7 +16,6 @@ classifiers = [
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.9",
20
19
  "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
22
21
  "Programming Language :: Python :: 3.12",
@@ -43,7 +42,7 @@ Changelog = "https://github.com/Sharp-API/sharpapi-python/releases"
43
42
  packages = ["src/sharpapi"]
44
43
 
45
44
  [tool.ruff]
46
- target-version = "py39"
45
+ target-version = "py310"
47
46
  line-length = 100
48
47
 
49
48
  [tool.ruff.lint]
@@ -54,7 +53,7 @@ asyncio_mode = "auto"
54
53
  testpaths = ["tests"]
55
54
 
56
55
  [tool.pyright]
57
- pythonVersion = "3.9"
56
+ pythonVersion = "3.10"
58
57
  typeCheckingMode = "standard"
59
58
  venvPath = "."
60
59
  venv = ".venv"
@@ -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
@@ -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:
@@ -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):
@@ -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):
@@ -346,15 +346,6 @@ class TestReferenceResources:
346
346
  result = client.events.list(league="nba")
347
347
  assert len(result.data) == 1
348
348
 
349
- @respx.mock
350
- def test_events_search(self):
351
- respx.get(f"{BASE_URL}/api/v1/events/search").mock(
352
- return_value=Response(200, json=EVENTS_RESPONSE)
353
- )
354
- with SharpAPI(API_KEY) as client:
355
- result = client.events.search("Lakers")
356
- assert len(result.data) == 1
357
-
358
349
  @respx.mock
359
350
  def test_account_me(self):
360
351
  respx.get(f"{BASE_URL}/api/v1/account").mock(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes