sharpapi 0.1.0__tar.gz → 0.2.1__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.1/.github/workflows/publish.yml +27 -0
- sharpapi-0.2.1/.github/workflows/test.yml +28 -0
- sharpapi-0.2.1/.gitignore +12 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/PKG-INFO +7 -8
- {sharpapi-0.1.0 → sharpapi-0.2.1}/README.md +4 -5
- {sharpapi-0.1.0 → sharpapi-0.2.1}/pyproject.toml +5 -3
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/__init__.py +2 -2
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/_base.py +1 -3
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/async_client.py +0 -5
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/client.py +1 -6
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/models.py +13 -5
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/conftest.py +4 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/test_async_client.py +1 -1
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/test_client.py +2 -11
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/test_dataframe.py +5 -5
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/test_models.py +4 -4
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/_utils.py +0 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/exceptions.py +0 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/py.typed +0 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/src/sharpapi/streaming.py +0 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/__init__.py +0 -0
- {sharpapi-0.1.0 → sharpapi-0.2.1}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install build tools
|
|
21
|
+
run: pip install hatch
|
|
22
|
+
|
|
23
|
+
- name: Build
|
|
24
|
+
run: hatch build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,28 @@
|
|
|
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.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
pip install -e ".[test,pandas]"
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: pytest -v
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sharpapi
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
7
|
-
Project-URL: Repository, https://github.com/
|
|
8
|
-
Project-URL: Changelog, https://github.com/
|
|
7
|
+
Project-URL: Repository, https://github.com/Sharp-API/sharpapi-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/Sharp-API/sharpapi-python/releases
|
|
9
9
|
Author-email: SharpAPI <support@sharpapi.io>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
Keywords: api,arbitrage,ev,odds,pinnacle,real-time,sports-betting
|
|
@@ -60,9 +60,9 @@ for arb in arbs.data:
|
|
|
60
60
|
# --- +EV opportunities ---
|
|
61
61
|
evs = client.ev.get(min_ev=3.0, sport="basketball")
|
|
62
62
|
for opp in evs.data:
|
|
63
|
-
print(f"+{opp.
|
|
64
|
-
if opp.
|
|
65
|
-
print(f" Kelly: {opp.
|
|
63
|
+
print(f"+{opp.ev_percentage:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
|
|
64
|
+
if opp.kelly_percent:
|
|
65
|
+
print(f" Kelly: {opp.kelly_percent:.1%} of bankroll")
|
|
66
66
|
|
|
67
67
|
# --- Best odds across books ---
|
|
68
68
|
odds = client.odds.best(league="nba", market="moneyline")
|
|
@@ -80,7 +80,7 @@ stream = client.stream.opportunities(league="nba")
|
|
|
80
80
|
@stream.on("ev:detected")
|
|
81
81
|
def on_ev(data):
|
|
82
82
|
for opp in data:
|
|
83
|
-
print(f"+EV: {opp['selection']} {opp['
|
|
83
|
+
print(f"+EV: {opp['selection']} {opp['ev_percentage']}% @ {opp['sportsbook']}")
|
|
84
84
|
|
|
85
85
|
@stream.on("arb:detected")
|
|
86
86
|
def on_arb(data):
|
|
@@ -118,7 +118,6 @@ client.sports.list()
|
|
|
118
118
|
client.leagues.list(sport="basketball")
|
|
119
119
|
client.sportsbooks.list()
|
|
120
120
|
client.events.list(league="nba", live=True)
|
|
121
|
-
client.events.search("Lakers")
|
|
122
121
|
|
|
123
122
|
# Account
|
|
124
123
|
client.account.me() # Tier, limits, features
|
|
@@ -27,9 +27,9 @@ for arb in arbs.data:
|
|
|
27
27
|
# --- +EV opportunities ---
|
|
28
28
|
evs = client.ev.get(min_ev=3.0, sport="basketball")
|
|
29
29
|
for opp in evs.data:
|
|
30
|
-
print(f"+{opp.
|
|
31
|
-
if opp.
|
|
32
|
-
print(f" Kelly: {opp.
|
|
30
|
+
print(f"+{opp.ev_percentage:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
|
|
31
|
+
if opp.kelly_percent:
|
|
32
|
+
print(f" Kelly: {opp.kelly_percent:.1%} of bankroll")
|
|
33
33
|
|
|
34
34
|
# --- Best odds across books ---
|
|
35
35
|
odds = client.odds.best(league="nba", market="moneyline")
|
|
@@ -47,7 +47,7 @@ stream = client.stream.opportunities(league="nba")
|
|
|
47
47
|
@stream.on("ev:detected")
|
|
48
48
|
def on_ev(data):
|
|
49
49
|
for opp in data:
|
|
50
|
-
print(f"+EV: {opp['selection']} {opp['
|
|
50
|
+
print(f"+EV: {opp['selection']} {opp['ev_percentage']}% @ {opp['sportsbook']}")
|
|
51
51
|
|
|
52
52
|
@stream.on("arb:detected")
|
|
53
53
|
def on_arb(data):
|
|
@@ -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,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sharpapi"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -36,8 +36,8 @@ test = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
|
|
|
36
36
|
[project.urls]
|
|
37
37
|
Homepage = "https://sharpapi.io"
|
|
38
38
|
Documentation = "https://docs.sharpapi.io/sdks/python"
|
|
39
|
-
Repository = "https://github.com/
|
|
40
|
-
Changelog = "https://github.com/
|
|
39
|
+
Repository = "https://github.com/Sharp-API/sharpapi-python"
|
|
40
|
+
Changelog = "https://github.com/Sharp-API/sharpapi-python/releases"
|
|
41
41
|
|
|
42
42
|
[tool.hatch.build.targets.wheel]
|
|
43
43
|
packages = ["src/sharpapi"]
|
|
@@ -56,3 +56,5 @@ testpaths = ["tests"]
|
|
|
56
56
|
[tool.pyright]
|
|
57
57
|
pythonVersion = "3.9"
|
|
58
58
|
typeCheckingMode = "standard"
|
|
59
|
+
venvPath = "."
|
|
60
|
+
venv = ".venv"
|
|
@@ -14,7 +14,7 @@ Example::
|
|
|
14
14
|
# +EV opportunities
|
|
15
15
|
evs = client.ev.get(min_ev=3.0, league="nba")
|
|
16
16
|
for opp in evs.data:
|
|
17
|
-
print(f"+{opp.
|
|
17
|
+
print(f"+{opp.ev_percentage}% on {opp.selection} @ {opp.sportsbook}")
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
from .async_client import AsyncSharpAPI
|
|
@@ -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.1
|
|
54
|
+
__version__ = "0.2.1"
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
# Clients
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
5
|
import httpx
|
|
8
6
|
|
|
9
7
|
from .exceptions import (
|
|
@@ -17,7 +15,7 @@ from .models import APIResponse, RateLimitInfo, ResponseMeta
|
|
|
17
15
|
|
|
18
16
|
DEFAULT_BASE_URL = "https://api.sharpapi.io"
|
|
19
17
|
DEFAULT_TIMEOUT = 30.0
|
|
20
|
-
USER_AGENT = "sharpapi-python/0.
|
|
18
|
+
USER_AGENT = "sharpapi-python/0.2.0"
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
def parse_response(raw: dict, model_class: type) -> APIResponse:
|
|
@@ -442,11 +442,6 @@ class _AsyncEventsResource:
|
|
|
442
442
|
raw = data.get("data", data)
|
|
443
443
|
return Event.model_validate(raw)
|
|
444
444
|
|
|
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
445
|
|
|
451
446
|
class _AsyncAccountResource:
|
|
452
447
|
def __init__(self, client: AsyncSharpAPI):
|
|
@@ -52,7 +52,7 @@ class SharpAPI:
|
|
|
52
52
|
# Get +EV opportunities
|
|
53
53
|
evs = client.ev.get(min_ev=3.0, league="nba")
|
|
54
54
|
for opp in evs.data:
|
|
55
|
-
print(f"+{opp.
|
|
55
|
+
print(f"+{opp.ev_percentage}% on {opp.selection} @ {opp.sportsbook}")
|
|
56
56
|
|
|
57
57
|
# Stream real-time updates
|
|
58
58
|
stream = client.stream.opportunities(league="nba")
|
|
@@ -555,11 +555,6 @@ class _EventsResource:
|
|
|
555
555
|
raw = data.get("data", data)
|
|
556
556
|
return Event.model_validate(raw)
|
|
557
557
|
|
|
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
558
|
|
|
564
559
|
class _AccountResource:
|
|
565
560
|
def __init__(self, client: SharpAPI):
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any, Generic, List, Optional, TypeVar
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import AliasChoices, BaseModel, Field
|
|
8
8
|
|
|
9
9
|
T = TypeVar("T")
|
|
10
10
|
|
|
@@ -171,14 +171,22 @@ class EVOpportunity(BaseModel):
|
|
|
171
171
|
odds_american: int | float
|
|
172
172
|
odds_decimal: float
|
|
173
173
|
no_vig_odds: Optional[float] = None
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
fair_probability: Optional[float] = Field(
|
|
175
|
+
None, validation_alias=AliasChoices("fair_probability", "true_probability")
|
|
176
|
+
)
|
|
177
|
+
ev_percentage: float = Field(
|
|
178
|
+
validation_alias=AliasChoices("ev_percentage", "ev_percent")
|
|
179
|
+
)
|
|
180
|
+
kelly_percent: Optional[float] = Field(
|
|
181
|
+
None, validation_alias=AliasChoices("kelly_percent", "kelly_fraction")
|
|
182
|
+
)
|
|
177
183
|
confidence_score: Optional[float] = None
|
|
178
184
|
book_count: Optional[int] = None
|
|
179
185
|
market_width: Optional[float] = None
|
|
180
186
|
devig_method: Optional[str] = None
|
|
181
|
-
|
|
187
|
+
sharp_book: Optional[str] = Field(
|
|
188
|
+
None, validation_alias=AliasChoices("sharp_book", "devig_book")
|
|
189
|
+
)
|
|
182
190
|
sharp_odds_american: Optional[int | float] = None
|
|
183
191
|
sharp_odds_decimal: Optional[float] = None
|
|
184
192
|
line: Optional[float] = None
|
|
@@ -91,13 +91,17 @@ EV_RESPONSE = {
|
|
|
91
91
|
"odds_american": -105,
|
|
92
92
|
"odds_decimal": 1.952,
|
|
93
93
|
"no_vig_odds": 1.912,
|
|
94
|
+
"fair_probability": 0.523,
|
|
94
95
|
"true_probability": 0.523,
|
|
96
|
+
"ev_percentage": 4.2,
|
|
95
97
|
"ev_percent": 4.2,
|
|
98
|
+
"kelly_percent": 0.021,
|
|
96
99
|
"kelly_fraction": 0.021,
|
|
97
100
|
"confidence_score": 87,
|
|
98
101
|
"book_count": 8,
|
|
99
102
|
"market_width": 0.043,
|
|
100
103
|
"devig_method": "power",
|
|
104
|
+
"sharp_book": "pinnacle",
|
|
101
105
|
"devig_book": "pinnacle",
|
|
102
106
|
"is_live": False,
|
|
103
107
|
"possibly_stale": False,
|
|
@@ -174,7 +174,7 @@ class TestAsyncOpportunities:
|
|
|
174
174
|
result = await client.ev.get(min_ev=3.0)
|
|
175
175
|
assert len(result.data) == 1
|
|
176
176
|
assert isinstance(result.data[0], EVOpportunity)
|
|
177
|
-
assert result.data[0].
|
|
177
|
+
assert result.data[0].ev_percentage == 4.2
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
# =============================================================================
|
|
@@ -260,8 +260,8 @@ class TestEVResource:
|
|
|
260
260
|
assert len(result.data) == 1
|
|
261
261
|
ev = result.data[0]
|
|
262
262
|
assert isinstance(ev, EVOpportunity)
|
|
263
|
-
assert ev.
|
|
264
|
-
assert ev.
|
|
263
|
+
assert ev.ev_percentage == 4.2
|
|
264
|
+
assert ev.kelly_percent == 0.021
|
|
265
265
|
|
|
266
266
|
|
|
267
267
|
class TestMiddlesResource:
|
|
@@ -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(
|
|
@@ -22,7 +22,7 @@ class TestToDataFrame:
|
|
|
22
22
|
result = parse_response(EV_RESPONSE, EVOpportunity)
|
|
23
23
|
df = result.to_dataframe()
|
|
24
24
|
assert len(df) == 1
|
|
25
|
-
assert df.iloc[0]["
|
|
25
|
+
assert df.iloc[0]["ev_percentage"] == 4.2
|
|
26
26
|
assert df.iloc[0]["sportsbook"] == "draftkings"
|
|
27
27
|
|
|
28
28
|
def test_flattens_nested_dicts(self):
|
|
@@ -60,24 +60,24 @@ class TestToDataFrame:
|
|
|
60
60
|
"id": "ev_1", "sport": "basketball", "league": "nba",
|
|
61
61
|
"selection": "A", "sportsbook": "dk",
|
|
62
62
|
"odds_american": -110, "odds_decimal": 1.909,
|
|
63
|
-
"
|
|
63
|
+
"ev_percentage": 4.2, "possibly_stale": False, "warnings": [],
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"id": "ev_2", "sport": "basketball", "league": "nba",
|
|
67
67
|
"selection": "B", "sportsbook": "fd",
|
|
68
68
|
"odds_american": 130, "odds_decimal": 2.3,
|
|
69
|
-
"
|
|
69
|
+
"ev_percentage": 2.8, "possibly_stale": False, "warnings": [],
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
72
|
}
|
|
73
73
|
result = parse_response(multi, EVOpportunity)
|
|
74
74
|
df = result.to_dataframe()
|
|
75
75
|
assert len(df) == 2
|
|
76
|
-
assert list(df["
|
|
76
|
+
assert list(df["ev_percentage"]) == [4.2, 2.8]
|
|
77
77
|
assert list(df["sportsbook"]) == ["dk", "fd"]
|
|
78
78
|
|
|
79
79
|
def test_dataframe_dtypes(self):
|
|
80
80
|
result = parse_response(EV_RESPONSE, EVOpportunity)
|
|
81
81
|
df = result.to_dataframe()
|
|
82
|
-
assert df["
|
|
82
|
+
assert df["ev_percentage"].dtype == "float64"
|
|
83
83
|
assert "str" in str(df["sportsbook"].dtype).lower() or df["sportsbook"].dtype == "object"
|
|
@@ -74,7 +74,7 @@ class TestEVModel:
|
|
|
74
74
|
result = parse_response(EV_RESPONSE, EVOpportunity)
|
|
75
75
|
assert len(result.data) == 1
|
|
76
76
|
ev = result.data[0]
|
|
77
|
-
assert ev.
|
|
77
|
+
assert ev.ev_percentage == 4.2
|
|
78
78
|
assert ev.selection == "PHO Suns"
|
|
79
79
|
assert ev.sportsbook == "draftkings"
|
|
80
80
|
assert ev.odds_american == -105
|
|
@@ -84,15 +84,15 @@ class TestEVModel:
|
|
|
84
84
|
result = parse_response(EV_RESPONSE, EVOpportunity)
|
|
85
85
|
ev = result.data[0]
|
|
86
86
|
assert ev.devig_method == "power"
|
|
87
|
-
assert ev.
|
|
87
|
+
assert ev.sharp_book == "pinnacle"
|
|
88
88
|
assert ev.no_vig_odds == 1.912
|
|
89
|
-
assert ev.
|
|
89
|
+
assert ev.fair_probability == 0.523
|
|
90
90
|
|
|
91
91
|
def test_scoring(self):
|
|
92
92
|
result = parse_response(EV_RESPONSE, EVOpportunity)
|
|
93
93
|
ev = result.data[0]
|
|
94
94
|
assert ev.confidence_score == 87
|
|
95
|
-
assert ev.
|
|
95
|
+
assert ev.kelly_percent == 0.021
|
|
96
96
|
assert ev.book_count == 8
|
|
97
97
|
|
|
98
98
|
def test_aliased_fields(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|