sharpapi 0.2.1__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.1
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
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sharpapi"
7
- version = "0.2.1"
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"
@@ -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()
@@ -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()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes