sharpapi 0.2.1__tar.gz → 0.2.5__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.
- sharpapi-0.2.5/.github/dependabot.yml +22 -0
- sharpapi-0.2.5/.github/workflows/publish.yml +69 -0
- sharpapi-0.2.5/.github/workflows/test.yml +40 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/PKG-INFO +2 -3
- {sharpapi-0.2.1 → sharpapi-0.2.5}/pyproject.toml +4 -5
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/__init__.py +17 -4
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/_base.py +72 -5
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/async_client.py +179 -83
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/client.py +216 -103
- sharpapi-0.2.5/src/sharpapi/exceptions.py +182 -0
- sharpapi-0.2.5/src/sharpapi/models.py +502 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/streaming.py +45 -6
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/conftest.py +0 -2
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/test_async_client.py +17 -2
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/test_client.py +33 -9
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/test_dataframe.py +1 -1
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/test_models.py +2 -0
- sharpapi-0.2.1/.github/workflows/publish.yml +0 -27
- sharpapi-0.2.1/.github/workflows/test.yml +0 -28
- sharpapi-0.2.1/src/sharpapi/exceptions.py +0 -52
- sharpapi-0.2.1/src/sharpapi/models.py +0 -441
- {sharpapi-0.2.1 → sharpapi-0.2.5}/.gitignore +0 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/README.md +0 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/_utils.py +0 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/src/sharpapi/py.typed +0 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/__init__.py +0 -0
- {sharpapi-0.2.1 → sharpapi-0.2.5}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Dependabot keeps SHA-pinned GitHub Actions fresh.
|
|
2
|
+
#
|
|
3
|
+
# We pin third-party actions by full commit SHA (e.g. orhun/git-cliff-action
|
|
4
|
+
# @c93ef52f... # v4) so tag moves can't silently propagate into our runners.
|
|
5
|
+
# But frozen SHAs also freeze security fixes. This config opens a weekly PR
|
|
6
|
+
# to bump any action whose upstream tag has advanced since our last pin,
|
|
7
|
+
# with the changelog inlined so review is short. All action updates are
|
|
8
|
+
# grouped into a single PR per week to avoid a Monday flood.
|
|
9
|
+
#
|
|
10
|
+
# Scope: github-actions only. Adding gomod/pip/npm ecosystems is a separate
|
|
11
|
+
# decision — those PRs are much higher volume.
|
|
12
|
+
version: 2
|
|
13
|
+
updates:
|
|
14
|
+
- package-ecosystem: "github-actions"
|
|
15
|
+
directory: "/"
|
|
16
|
+
schedule:
|
|
17
|
+
interval: "weekly"
|
|
18
|
+
groups:
|
|
19
|
+
actions:
|
|
20
|
+
patterns: ["*"]
|
|
21
|
+
commit-message:
|
|
22
|
+
prefix: "chore(deps)"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
# workflow_dispatch lets operators dry-run the test matrix + gate
|
|
7
|
+
# wiring without cutting a real release (the actual PyPI upload step
|
|
8
|
+
# is guarded against non-release triggers, see below). Useful for
|
|
9
|
+
# verifying the gate after edits to test.yml or publish.yml.
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
# Re-run the full test matrix on the tagged commit before publish.
|
|
14
|
+
# Mirrors test.yml so a tagged release gets the same coverage as
|
|
15
|
+
# a push/PR. Previously publish.yml ran zero tests — a broken SDK
|
|
16
|
+
# could reach PyPI, and PyPI versions are immutable (only super-
|
|
17
|
+
# ceded). needs: test below gates the publish job on every matrix
|
|
18
|
+
# entry passing.
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
strategy:
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v5
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v6
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: |
|
|
34
|
+
pip install -e ".[test,pandas]"
|
|
35
|
+
pip install ruff pyright
|
|
36
|
+
|
|
37
|
+
- name: Ruff check
|
|
38
|
+
run: ruff check
|
|
39
|
+
|
|
40
|
+
- name: Pyright
|
|
41
|
+
run: pyright
|
|
42
|
+
|
|
43
|
+
- name: Run tests
|
|
44
|
+
run: pytest -v
|
|
45
|
+
|
|
46
|
+
publish:
|
|
47
|
+
needs: test
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
permissions:
|
|
50
|
+
id-token: write
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v5
|
|
54
|
+
|
|
55
|
+
- uses: actions/setup-python@v6
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.12"
|
|
58
|
+
|
|
59
|
+
- name: Install build tools
|
|
60
|
+
run: pip install hatch
|
|
61
|
+
|
|
62
|
+
- name: Build
|
|
63
|
+
run: hatch build
|
|
64
|
+
|
|
65
|
+
- name: Publish to PyPI
|
|
66
|
+
# Only publish on an actual release event. workflow_dispatch
|
|
67
|
+
# runs through test+build as a dry run but must not upload.
|
|
68
|
+
if: github.event_name == 'release'
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
pip install -e ".[test,pandas]"
|
|
26
|
+
pip install ruff pyright
|
|
27
|
+
|
|
28
|
+
- name: Ruff check
|
|
29
|
+
# [tool.ruff] config is already in pyproject.toml — rules E, F,
|
|
30
|
+
# I, UP. This step enforces them; before today they were only
|
|
31
|
+
# suggestions.
|
|
32
|
+
run: ruff check
|
|
33
|
+
|
|
34
|
+
- name: Pyright
|
|
35
|
+
# Same story — [tool.pyright] was set to standard mode in
|
|
36
|
+
# pyproject.toml but never invoked in CI.
|
|
37
|
+
run: pyright
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: pytest -v
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sharpapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
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.
|
|
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.
|
|
7
|
+
version = "0.2.5"
|
|
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.
|
|
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 = "
|
|
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.
|
|
56
|
+
pythonVersion = "3.10"
|
|
58
57
|
typeCheckingMode = "standard"
|
|
59
58
|
venvPath = "."
|
|
60
59
|
venv = ".venv"
|
|
@@ -17,9 +17,12 @@ Example::
|
|
|
17
17
|
print(f"+{opp.ev_percentage}% on {opp.selection} @ {opp.sportsbook}")
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
20
21
|
from .async_client import AsyncSharpAPI
|
|
21
22
|
from .client import SharpAPI
|
|
22
23
|
from .exceptions import (
|
|
24
|
+
ERROR_CODE_DESCRIPTIONS,
|
|
25
|
+
ERROR_CODE_TO_EXCEPTION,
|
|
23
26
|
AuthenticationError,
|
|
24
27
|
RateLimitedError,
|
|
25
28
|
SharpAPIError,
|
|
@@ -28,16 +31,20 @@ from .exceptions import (
|
|
|
28
31
|
ValidationError,
|
|
29
32
|
)
|
|
30
33
|
from .models import (
|
|
31
|
-
APIResponse,
|
|
32
34
|
AccountInfo,
|
|
35
|
+
APIKey,
|
|
36
|
+
APIResponse,
|
|
33
37
|
ArbitrageLeg,
|
|
34
38
|
ArbitrageOpportunity,
|
|
35
|
-
|
|
39
|
+
ClosingOddsLine,
|
|
40
|
+
ClosingSnapshot,
|
|
36
41
|
Event,
|
|
42
|
+
EVOpportunity,
|
|
37
43
|
GameState,
|
|
38
44
|
League,
|
|
39
45
|
LowHoldOpportunity,
|
|
40
46
|
LowHoldSide,
|
|
47
|
+
Market,
|
|
41
48
|
MiddleOpportunity,
|
|
42
49
|
MiddleSide,
|
|
43
50
|
OddsLine,
|
|
@@ -49,25 +56,28 @@ from .models import (
|
|
|
49
56
|
Sportsbook,
|
|
50
57
|
)
|
|
51
58
|
from .streaming import EventStream
|
|
52
|
-
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
53
59
|
|
|
54
|
-
__version__ = "0.2.
|
|
60
|
+
__version__ = "0.2.5"
|
|
55
61
|
|
|
56
62
|
__all__ = [
|
|
57
63
|
# Clients
|
|
58
64
|
"SharpAPI",
|
|
59
65
|
"AsyncSharpAPI",
|
|
60
66
|
# Models
|
|
67
|
+
"APIKey",
|
|
61
68
|
"APIResponse",
|
|
62
69
|
"AccountInfo",
|
|
63
70
|
"ArbitrageLeg",
|
|
64
71
|
"ArbitrageOpportunity",
|
|
72
|
+
"ClosingOddsLine",
|
|
73
|
+
"ClosingSnapshot",
|
|
65
74
|
"EVOpportunity",
|
|
66
75
|
"Event",
|
|
67
76
|
"GameState",
|
|
68
77
|
"League",
|
|
69
78
|
"LowHoldOpportunity",
|
|
70
79
|
"LowHoldSide",
|
|
80
|
+
"Market",
|
|
71
81
|
"MiddleOpportunity",
|
|
72
82
|
"MiddleSide",
|
|
73
83
|
"OddsLine",
|
|
@@ -86,6 +96,9 @@ __all__ = [
|
|
|
86
96
|
"StreamError",
|
|
87
97
|
"TierRestrictedError",
|
|
88
98
|
"ValidationError",
|
|
99
|
+
# Error-code registry
|
|
100
|
+
"ERROR_CODE_DESCRIPTIONS",
|
|
101
|
+
"ERROR_CODE_TO_EXCEPTION",
|
|
89
102
|
# Utilities
|
|
90
103
|
"american_to_decimal",
|
|
91
104
|
"american_to_probability",
|
|
@@ -2,20 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import random
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
5
8
|
import httpx
|
|
6
9
|
|
|
7
10
|
from .exceptions import (
|
|
11
|
+
ERROR_CODE_TO_EXCEPTION,
|
|
8
12
|
AuthenticationError,
|
|
9
13
|
RateLimitedError,
|
|
10
14
|
SharpAPIError,
|
|
11
15
|
TierRestrictedError,
|
|
12
16
|
ValidationError,
|
|
17
|
+
canonical_code,
|
|
13
18
|
)
|
|
14
19
|
from .models import APIResponse, RateLimitInfo, ResponseMeta
|
|
15
20
|
|
|
16
21
|
DEFAULT_BASE_URL = "https://api.sharpapi.io"
|
|
17
22
|
DEFAULT_TIMEOUT = 30.0
|
|
18
|
-
USER_AGENT = "sharpapi-python/0.2.
|
|
23
|
+
USER_AGENT = "sharpapi-python/0.2.5"
|
|
24
|
+
|
|
25
|
+
# Supported REST authentication methods. SSE always uses ``?api_key=`` query
|
|
26
|
+
# regardless of this setting because EventSource cannot set custom headers.
|
|
27
|
+
AuthMethod = Literal["x-api-key", "bearer"]
|
|
28
|
+
DEFAULT_AUTH_METHOD: AuthMethod = "x-api-key"
|
|
29
|
+
|
|
30
|
+
RETRY_STATUSES = frozenset({502, 503, 504})
|
|
31
|
+
RETRY_MAX_ATTEMPTS = 3
|
|
32
|
+
RETRY_BASE_DELAY = 0.5
|
|
33
|
+
RETRY_MAX_DELAY = 4.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def should_retry(response: httpx.Response | None, exc: Exception | None) -> bool:
|
|
37
|
+
"""True for transient upstream failures worth retrying."""
|
|
38
|
+
if exc is not None:
|
|
39
|
+
return isinstance(exc, (httpx.ConnectError, httpx.ReadError, httpx.RemoteProtocolError))
|
|
40
|
+
return response is not None and response.status_code in RETRY_STATUSES
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def retry_delay(attempt: int) -> float:
|
|
44
|
+
"""Exponential backoff with full jitter. attempt is 1-indexed."""
|
|
45
|
+
ceiling = min(RETRY_BASE_DELAY * (2 ** (attempt - 1)), RETRY_MAX_DELAY)
|
|
46
|
+
return random.uniform(0, ceiling)
|
|
19
47
|
|
|
20
48
|
|
|
21
49
|
def parse_response(raw: dict, model_class: type) -> APIResponse:
|
|
@@ -70,6 +98,30 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
70
98
|
code = body.get("code", "unknown_error")
|
|
71
99
|
status = response.status_code
|
|
72
100
|
|
|
101
|
+
# Resolve deprecated code aliases (bad_request, invalid_request → validation_error).
|
|
102
|
+
code = canonical_code(code)
|
|
103
|
+
|
|
104
|
+
# Prefer the canonical code→exception mapping for well-known codes; fall back
|
|
105
|
+
# to HTTP-status-based routing for responses that omit an error code.
|
|
106
|
+
exc_class = ERROR_CODE_TO_EXCEPTION.get(code or "")
|
|
107
|
+
if exc_class is TierRestrictedError:
|
|
108
|
+
raise TierRestrictedError(
|
|
109
|
+
error_msg,
|
|
110
|
+
code=code,
|
|
111
|
+
status=status,
|
|
112
|
+
required_tier=body.get("required_tier"),
|
|
113
|
+
)
|
|
114
|
+
if exc_class is RateLimitedError:
|
|
115
|
+
raise RateLimitedError(
|
|
116
|
+
error_msg,
|
|
117
|
+
code=code,
|
|
118
|
+
status=status,
|
|
119
|
+
retry_after=body.get("retry_after"),
|
|
120
|
+
)
|
|
121
|
+
if exc_class is not None and exc_class is not SharpAPIError:
|
|
122
|
+
raise exc_class(error_msg, code=code, status=status)
|
|
123
|
+
|
|
124
|
+
# No canonical code match — route by HTTP status.
|
|
73
125
|
if status == 401:
|
|
74
126
|
raise AuthenticationError(error_msg, code=code, status=status)
|
|
75
127
|
elif status == 403:
|
|
@@ -92,13 +144,28 @@ def handle_errors(response: httpx.Response) -> None:
|
|
|
92
144
|
raise SharpAPIError(error_msg, code=code, status=status)
|
|
93
145
|
|
|
94
146
|
|
|
95
|
-
def make_headers(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
def make_headers(
|
|
148
|
+
api_key: str,
|
|
149
|
+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
|
|
150
|
+
) -> dict[str, str]:
|
|
151
|
+
"""Build default request headers.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
api_key: The SharpAPI key (e.g. ``sk_live_...``).
|
|
155
|
+
auth_method: Either ``"x-api-key"`` (default — sends an
|
|
156
|
+
``X-API-Key`` header) or ``"bearer"`` (sends
|
|
157
|
+
``Authorization: Bearer <key>``). Useful when proxies, IAM
|
|
158
|
+
layers, or SSO gateways strip non-standard custom headers.
|
|
159
|
+
"""
|
|
160
|
+
headers: dict[str, str] = {
|
|
99
161
|
"Content-Type": "application/json",
|
|
100
162
|
"User-Agent": USER_AGENT,
|
|
101
163
|
}
|
|
164
|
+
if auth_method == "bearer":
|
|
165
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
166
|
+
else:
|
|
167
|
+
headers["X-API-Key"] = api_key
|
|
168
|
+
return headers
|
|
102
169
|
|
|
103
170
|
|
|
104
171
|
def _int_or_none(value: str | None) -> int | None:
|