sharpapi 0.1.0__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.1.0/PKG-INFO +203 -0
- sharpapi-0.1.0/README.md +170 -0
- sharpapi-0.1.0/pyproject.toml +58 -0
- sharpapi-0.1.0/src/sharpapi/__init__.py +93 -0
- sharpapi-0.1.0/src/sharpapi/_base.py +121 -0
- sharpapi-0.1.0/src/sharpapi/_utils.py +39 -0
- sharpapi-0.1.0/src/sharpapi/async_client.py +464 -0
- sharpapi-0.1.0/src/sharpapi/client.py +682 -0
- sharpapi-0.1.0/src/sharpapi/exceptions.py +52 -0
- sharpapi-0.1.0/src/sharpapi/models.py +433 -0
- sharpapi-0.1.0/src/sharpapi/py.typed +0 -0
- sharpapi-0.1.0/src/sharpapi/streaming.py +200 -0
- sharpapi-0.1.0/tests/__init__.py +0 -0
- sharpapi-0.1.0/tests/conftest.py +273 -0
- sharpapi-0.1.0/tests/test_async_client.py +223 -0
- sharpapi-0.1.0/tests/test_client.py +392 -0
- sharpapi-0.1.0/tests/test_dataframe.py +83 -0
- sharpapi-0.1.0/tests/test_models.py +191 -0
- sharpapi-0.1.0/tests/test_utils.py +75 -0
sharpapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sharpapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the SharpAPI real-time sports betting odds API
|
|
5
|
+
Project-URL: Homepage, https://sharpapi.io
|
|
6
|
+
Project-URL: Documentation, https://docs.sharpapi.io/sdks/python
|
|
7
|
+
Project-URL: Repository, https://github.com/sharpapi/sharpapi-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/sharpapi/sharpapi-python/releases
|
|
9
|
+
Author-email: SharpAPI <support@sharpapi.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api,arbitrage,ev,odds,pinnacle,real-time,sports-betting
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Provides-Extra: pandas
|
|
27
|
+
Requires-Dist: pandas>=1.5.0; extra == 'pandas'
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'test'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# SharpAPI Python SDK
|
|
35
|
+
|
|
36
|
+
Official Python client for the [SharpAPI](https://sharpapi.io) real-time sports betting odds API.
|
|
37
|
+
|
|
38
|
+
Get pre-computed +EV opportunities, arbitrage detection, middles, and live odds from 20+ sportsbooks — with Pinnacle as the sharp reference.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install sharpapi
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from sharpapi import SharpAPI
|
|
50
|
+
|
|
51
|
+
client = SharpAPI("sk_live_xxx")
|
|
52
|
+
|
|
53
|
+
# --- Arbitrage opportunities ---
|
|
54
|
+
arbs = client.arbitrage.get(min_profit=1.0, league="nba")
|
|
55
|
+
for arb in arbs.data:
|
|
56
|
+
print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}")
|
|
57
|
+
for leg in arb.legs:
|
|
58
|
+
print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)")
|
|
59
|
+
|
|
60
|
+
# --- +EV opportunities ---
|
|
61
|
+
evs = client.ev.get(min_ev=3.0, sport="basketball")
|
|
62
|
+
for opp in evs.data:
|
|
63
|
+
print(f"+{opp.ev_percent:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
|
|
64
|
+
if opp.kelly_fraction:
|
|
65
|
+
print(f" Kelly: {opp.kelly_fraction:.1%} of bankroll")
|
|
66
|
+
|
|
67
|
+
# --- Best odds across books ---
|
|
68
|
+
odds = client.odds.best(league="nba", market="moneyline")
|
|
69
|
+
for line in odds.data:
|
|
70
|
+
print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Streaming
|
|
74
|
+
|
|
75
|
+
Real-time SSE streaming for odds updates and opportunity alerts (requires WebSocket add-on):
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
stream = client.stream.opportunities(league="nba")
|
|
79
|
+
|
|
80
|
+
@stream.on("ev:detected")
|
|
81
|
+
def on_ev(data):
|
|
82
|
+
for opp in data:
|
|
83
|
+
print(f"+EV: {opp['selection']} {opp['ev_percent']}% @ {opp['sportsbook']}")
|
|
84
|
+
|
|
85
|
+
@stream.on("arb:detected")
|
|
86
|
+
def on_arb(data):
|
|
87
|
+
for arb in data:
|
|
88
|
+
print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
|
|
89
|
+
|
|
90
|
+
stream.connect() # Blocks, processing events
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or iterate over events:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
for event_type, data in stream.iter_events():
|
|
97
|
+
if event_type == "ev:detected":
|
|
98
|
+
print(data)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## All Resources
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Odds
|
|
105
|
+
client.odds.get(sport="basketball", league="nba")
|
|
106
|
+
client.odds.best(league="nfl", market="moneyline")
|
|
107
|
+
client.odds.comparison(event_id="abc123")
|
|
108
|
+
client.odds.batch(event_ids=["abc123", "def456"])
|
|
109
|
+
|
|
110
|
+
# Opportunities
|
|
111
|
+
client.ev.get(min_ev=2.0, sportsbook="draftkings")
|
|
112
|
+
client.arbitrage.get(min_profit=0.5, sport="football")
|
|
113
|
+
client.middles.get(sport="football", min_size=3.0)
|
|
114
|
+
client.low_hold.get(max_hold=2.0)
|
|
115
|
+
|
|
116
|
+
# Reference data
|
|
117
|
+
client.sports.list()
|
|
118
|
+
client.leagues.list(sport="basketball")
|
|
119
|
+
client.sportsbooks.list()
|
|
120
|
+
client.events.list(league="nba", live=True)
|
|
121
|
+
client.events.search("Lakers")
|
|
122
|
+
|
|
123
|
+
# Account
|
|
124
|
+
client.account.me() # Tier, limits, features
|
|
125
|
+
client.account.usage() # Request counts
|
|
126
|
+
|
|
127
|
+
# Streaming
|
|
128
|
+
client.stream.odds(league="nba")
|
|
129
|
+
client.stream.opportunities(min_ev=3.0)
|
|
130
|
+
client.stream.all(sport="basketball")
|
|
131
|
+
client.stream.event("event_id_here")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Data Quality
|
|
135
|
+
|
|
136
|
+
Every opportunity response includes staleness metadata to avoid acting on stale odds:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
arbs = client.arbitrage.get()
|
|
140
|
+
for arb in arbs.data:
|
|
141
|
+
if arb.possibly_stale:
|
|
142
|
+
print(f" Skipping — odds may be stale ({arb.oldest_odds_age_seconds}s old)")
|
|
143
|
+
continue
|
|
144
|
+
if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings:
|
|
145
|
+
print(f" Skipping — likely phantom arb")
|
|
146
|
+
continue
|
|
147
|
+
print(f"Actionable: {arb.profit_percent}%")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Rate Limits
|
|
151
|
+
|
|
152
|
+
Rate limit info is available after every request:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
response = client.odds.get()
|
|
156
|
+
print(f"Remaining: {client.rate_limit.remaining}/{client.rate_limit.limit}")
|
|
157
|
+
print(f"Tier: {client.rate_limit.tier}")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Error Handling
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from sharpapi import (
|
|
164
|
+
SharpAPI,
|
|
165
|
+
AuthenticationError,
|
|
166
|
+
TierRestrictedError,
|
|
167
|
+
RateLimitedError,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
client = SharpAPI("sk_live_xxx")
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
evs = client.ev.get()
|
|
174
|
+
except AuthenticationError:
|
|
175
|
+
print("Invalid API key")
|
|
176
|
+
except TierRestrictedError as e:
|
|
177
|
+
print(f"Upgrade to {e.required_tier} tier for this feature")
|
|
178
|
+
except RateLimitedError as e:
|
|
179
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Odds Conversion Utilities
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from sharpapi import american_to_decimal, american_to_probability, decimal_to_american
|
|
186
|
+
|
|
187
|
+
american_to_decimal(-110) # 1.909
|
|
188
|
+
american_to_decimal(150) # 2.5
|
|
189
|
+
american_to_probability(-110) # 0.524
|
|
190
|
+
decimal_to_american(2.5) # 150
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
- Python 3.9+
|
|
196
|
+
- httpx
|
|
197
|
+
- pydantic v2
|
|
198
|
+
|
|
199
|
+
## Links
|
|
200
|
+
|
|
201
|
+
- [API Docs](https://docs.sharpapi.io)
|
|
202
|
+
- [Dashboard](https://sharpapi.io/dashboard)
|
|
203
|
+
- [Discord](https://discord.gg/sharpapi)
|
sharpapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# SharpAPI Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [SharpAPI](https://sharpapi.io) real-time sports betting odds API.
|
|
4
|
+
|
|
5
|
+
Get pre-computed +EV opportunities, arbitrage detection, middles, and live odds from 20+ sportsbooks — with Pinnacle as the sharp reference.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install sharpapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from sharpapi import SharpAPI
|
|
17
|
+
|
|
18
|
+
client = SharpAPI("sk_live_xxx")
|
|
19
|
+
|
|
20
|
+
# --- Arbitrage opportunities ---
|
|
21
|
+
arbs = client.arbitrage.get(min_profit=1.0, league="nba")
|
|
22
|
+
for arb in arbs.data:
|
|
23
|
+
print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}")
|
|
24
|
+
for leg in arb.legs:
|
|
25
|
+
print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)")
|
|
26
|
+
|
|
27
|
+
# --- +EV opportunities ---
|
|
28
|
+
evs = client.ev.get(min_ev=3.0, sport="basketball")
|
|
29
|
+
for opp in evs.data:
|
|
30
|
+
print(f"+{opp.ev_percent:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
|
|
31
|
+
if opp.kelly_fraction:
|
|
32
|
+
print(f" Kelly: {opp.kelly_fraction:.1%} of bankroll")
|
|
33
|
+
|
|
34
|
+
# --- Best odds across books ---
|
|
35
|
+
odds = client.odds.best(league="nba", market="moneyline")
|
|
36
|
+
for line in odds.data:
|
|
37
|
+
print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Streaming
|
|
41
|
+
|
|
42
|
+
Real-time SSE streaming for odds updates and opportunity alerts (requires WebSocket add-on):
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
stream = client.stream.opportunities(league="nba")
|
|
46
|
+
|
|
47
|
+
@stream.on("ev:detected")
|
|
48
|
+
def on_ev(data):
|
|
49
|
+
for opp in data:
|
|
50
|
+
print(f"+EV: {opp['selection']} {opp['ev_percent']}% @ {opp['sportsbook']}")
|
|
51
|
+
|
|
52
|
+
@stream.on("arb:detected")
|
|
53
|
+
def on_arb(data):
|
|
54
|
+
for arb in data:
|
|
55
|
+
print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
|
|
56
|
+
|
|
57
|
+
stream.connect() # Blocks, processing events
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or iterate over events:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
for event_type, data in stream.iter_events():
|
|
64
|
+
if event_type == "ev:detected":
|
|
65
|
+
print(data)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## All Resources
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Odds
|
|
72
|
+
client.odds.get(sport="basketball", league="nba")
|
|
73
|
+
client.odds.best(league="nfl", market="moneyline")
|
|
74
|
+
client.odds.comparison(event_id="abc123")
|
|
75
|
+
client.odds.batch(event_ids=["abc123", "def456"])
|
|
76
|
+
|
|
77
|
+
# Opportunities
|
|
78
|
+
client.ev.get(min_ev=2.0, sportsbook="draftkings")
|
|
79
|
+
client.arbitrage.get(min_profit=0.5, sport="football")
|
|
80
|
+
client.middles.get(sport="football", min_size=3.0)
|
|
81
|
+
client.low_hold.get(max_hold=2.0)
|
|
82
|
+
|
|
83
|
+
# Reference data
|
|
84
|
+
client.sports.list()
|
|
85
|
+
client.leagues.list(sport="basketball")
|
|
86
|
+
client.sportsbooks.list()
|
|
87
|
+
client.events.list(league="nba", live=True)
|
|
88
|
+
client.events.search("Lakers")
|
|
89
|
+
|
|
90
|
+
# Account
|
|
91
|
+
client.account.me() # Tier, limits, features
|
|
92
|
+
client.account.usage() # Request counts
|
|
93
|
+
|
|
94
|
+
# Streaming
|
|
95
|
+
client.stream.odds(league="nba")
|
|
96
|
+
client.stream.opportunities(min_ev=3.0)
|
|
97
|
+
client.stream.all(sport="basketball")
|
|
98
|
+
client.stream.event("event_id_here")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Data Quality
|
|
102
|
+
|
|
103
|
+
Every opportunity response includes staleness metadata to avoid acting on stale odds:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
arbs = client.arbitrage.get()
|
|
107
|
+
for arb in arbs.data:
|
|
108
|
+
if arb.possibly_stale:
|
|
109
|
+
print(f" Skipping — odds may be stale ({arb.oldest_odds_age_seconds}s old)")
|
|
110
|
+
continue
|
|
111
|
+
if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings:
|
|
112
|
+
print(f" Skipping — likely phantom arb")
|
|
113
|
+
continue
|
|
114
|
+
print(f"Actionable: {arb.profit_percent}%")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Rate Limits
|
|
118
|
+
|
|
119
|
+
Rate limit info is available after every request:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
response = client.odds.get()
|
|
123
|
+
print(f"Remaining: {client.rate_limit.remaining}/{client.rate_limit.limit}")
|
|
124
|
+
print(f"Tier: {client.rate_limit.tier}")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Error Handling
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from sharpapi import (
|
|
131
|
+
SharpAPI,
|
|
132
|
+
AuthenticationError,
|
|
133
|
+
TierRestrictedError,
|
|
134
|
+
RateLimitedError,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
client = SharpAPI("sk_live_xxx")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
evs = client.ev.get()
|
|
141
|
+
except AuthenticationError:
|
|
142
|
+
print("Invalid API key")
|
|
143
|
+
except TierRestrictedError as e:
|
|
144
|
+
print(f"Upgrade to {e.required_tier} tier for this feature")
|
|
145
|
+
except RateLimitedError as e:
|
|
146
|
+
print(f"Rate limited — retry after {e.retry_after}s")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Odds Conversion Utilities
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from sharpapi import american_to_decimal, american_to_probability, decimal_to_american
|
|
153
|
+
|
|
154
|
+
american_to_decimal(-110) # 1.909
|
|
155
|
+
american_to_decimal(150) # 2.5
|
|
156
|
+
american_to_probability(-110) # 0.524
|
|
157
|
+
decimal_to_american(2.5) # 150
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
- Python 3.9+
|
|
163
|
+
- httpx
|
|
164
|
+
- pydantic v2
|
|
165
|
+
|
|
166
|
+
## Links
|
|
167
|
+
|
|
168
|
+
- [API Docs](https://docs.sharpapi.io)
|
|
169
|
+
- [Dashboard](https://sharpapi.io/dashboard)
|
|
170
|
+
- [Discord](https://discord.gg/sharpapi)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sharpapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "SharpAPI", email = "support@sharpapi.io" }]
|
|
13
|
+
keywords = ["sports-betting", "odds", "arbitrage", "ev", "api", "real-time", "pinnacle"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.25.0",
|
|
29
|
+
"pydantic>=2.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
pandas = ["pandas>=1.5.0"]
|
|
34
|
+
test = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://sharpapi.io"
|
|
38
|
+
Documentation = "https://docs.sharpapi.io/sdks/python"
|
|
39
|
+
Repository = "https://github.com/sharpapi/sharpapi-python"
|
|
40
|
+
Changelog = "https://github.com/sharpapi/sharpapi-python/releases"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/sharpapi"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py39"
|
|
47
|
+
line-length = 100
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "UP"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.pyright]
|
|
57
|
+
pythonVersion = "3.9"
|
|
58
|
+
typeCheckingMode = "standard"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""SharpAPI Python SDK — Real-time sports betting odds, +EV, and arbitrage detection.
|
|
2
|
+
|
|
3
|
+
Example::
|
|
4
|
+
|
|
5
|
+
from sharpapi import SharpAPI
|
|
6
|
+
|
|
7
|
+
client = SharpAPI("sk_live_xxx")
|
|
8
|
+
|
|
9
|
+
# Arbitrage opportunities
|
|
10
|
+
arbs = client.arbitrage.get(min_profit=1.0)
|
|
11
|
+
for arb in arbs.data:
|
|
12
|
+
print(f"{arb.profit_percent}% — {arb.event_name}")
|
|
13
|
+
|
|
14
|
+
# +EV opportunities
|
|
15
|
+
evs = client.ev.get(min_ev=3.0, league="nba")
|
|
16
|
+
for opp in evs.data:
|
|
17
|
+
print(f"+{opp.ev_percent}% on {opp.selection} @ {opp.sportsbook}")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .async_client import AsyncSharpAPI
|
|
21
|
+
from .client import SharpAPI
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
RateLimitedError,
|
|
25
|
+
SharpAPIError,
|
|
26
|
+
StreamError,
|
|
27
|
+
TierRestrictedError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
from .models import (
|
|
31
|
+
APIResponse,
|
|
32
|
+
AccountInfo,
|
|
33
|
+
ArbitrageLeg,
|
|
34
|
+
ArbitrageOpportunity,
|
|
35
|
+
EVOpportunity,
|
|
36
|
+
Event,
|
|
37
|
+
GameState,
|
|
38
|
+
League,
|
|
39
|
+
LowHoldOpportunity,
|
|
40
|
+
LowHoldSide,
|
|
41
|
+
MiddleOpportunity,
|
|
42
|
+
MiddleSide,
|
|
43
|
+
OddsLine,
|
|
44
|
+
OddsValue,
|
|
45
|
+
Pagination,
|
|
46
|
+
RateLimitInfo,
|
|
47
|
+
ResponseMeta,
|
|
48
|
+
Sport,
|
|
49
|
+
Sportsbook,
|
|
50
|
+
)
|
|
51
|
+
from .streaming import EventStream
|
|
52
|
+
from ._utils import american_to_decimal, american_to_probability, decimal_to_american
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Clients
|
|
58
|
+
"SharpAPI",
|
|
59
|
+
"AsyncSharpAPI",
|
|
60
|
+
# Models
|
|
61
|
+
"APIResponse",
|
|
62
|
+
"AccountInfo",
|
|
63
|
+
"ArbitrageLeg",
|
|
64
|
+
"ArbitrageOpportunity",
|
|
65
|
+
"EVOpportunity",
|
|
66
|
+
"Event",
|
|
67
|
+
"GameState",
|
|
68
|
+
"League",
|
|
69
|
+
"LowHoldOpportunity",
|
|
70
|
+
"LowHoldSide",
|
|
71
|
+
"MiddleOpportunity",
|
|
72
|
+
"MiddleSide",
|
|
73
|
+
"OddsLine",
|
|
74
|
+
"OddsValue",
|
|
75
|
+
"Pagination",
|
|
76
|
+
"RateLimitInfo",
|
|
77
|
+
"ResponseMeta",
|
|
78
|
+
"Sport",
|
|
79
|
+
"Sportsbook",
|
|
80
|
+
# Streaming
|
|
81
|
+
"EventStream",
|
|
82
|
+
# Exceptions
|
|
83
|
+
"AuthenticationError",
|
|
84
|
+
"RateLimitedError",
|
|
85
|
+
"SharpAPIError",
|
|
86
|
+
"StreamError",
|
|
87
|
+
"TierRestrictedError",
|
|
88
|
+
"ValidationError",
|
|
89
|
+
# Utilities
|
|
90
|
+
"american_to_decimal",
|
|
91
|
+
"american_to_probability",
|
|
92
|
+
"decimal_to_american",
|
|
93
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Shared logic for sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
RateLimitedError,
|
|
12
|
+
SharpAPIError,
|
|
13
|
+
TierRestrictedError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
from .models import APIResponse, RateLimitInfo, ResponseMeta
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE_URL = "https://api.sharpapi.io"
|
|
19
|
+
DEFAULT_TIMEOUT = 30.0
|
|
20
|
+
USER_AGENT = "sharpapi-python/0.1.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_response(raw: dict, model_class: type) -> APIResponse:
|
|
24
|
+
"""Parse raw API JSON into a typed APIResponse."""
|
|
25
|
+
data_raw = raw.get("data", [])
|
|
26
|
+
if isinstance(data_raw, list):
|
|
27
|
+
items = [model_class.model_validate(item) for item in data_raw]
|
|
28
|
+
else:
|
|
29
|
+
items = [model_class.model_validate(data_raw)]
|
|
30
|
+
|
|
31
|
+
meta = None
|
|
32
|
+
meta_raw = raw.get("meta")
|
|
33
|
+
if meta_raw:
|
|
34
|
+
meta = ResponseMeta.model_validate(meta_raw)
|
|
35
|
+
|
|
36
|
+
return APIResponse(
|
|
37
|
+
success=raw.get("success"),
|
|
38
|
+
data=items,
|
|
39
|
+
meta=meta,
|
|
40
|
+
timestamp=raw.get("timestamp"),
|
|
41
|
+
tier=raw.get("tier"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_rate_limit(response: httpx.Response) -> RateLimitInfo:
|
|
46
|
+
"""Extract rate limit info from response headers."""
|
|
47
|
+
headers = response.headers
|
|
48
|
+
return RateLimitInfo(
|
|
49
|
+
limit=_int_or_none(headers.get("x-ratelimit-limit")),
|
|
50
|
+
remaining=_int_or_none(headers.get("x-ratelimit-remaining")),
|
|
51
|
+
reset=_float_or_none(headers.get("x-ratelimit-reset")),
|
|
52
|
+
tier=headers.get("x-tier"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handle_errors(response: httpx.Response) -> None:
|
|
57
|
+
"""Raise typed exceptions for error responses."""
|
|
58
|
+
if response.is_success:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
body = response.json()
|
|
63
|
+
except Exception:
|
|
64
|
+
body = {}
|
|
65
|
+
|
|
66
|
+
error_obj = body.get("error", body)
|
|
67
|
+
if isinstance(error_obj, dict):
|
|
68
|
+
error_msg = error_obj.get("message", error_obj.get("error", f"HTTP {response.status_code}"))
|
|
69
|
+
code = error_obj.get("code", body.get("code", "unknown_error"))
|
|
70
|
+
else:
|
|
71
|
+
error_msg = str(error_obj) if error_obj else f"HTTP {response.status_code}"
|
|
72
|
+
code = body.get("code", "unknown_error")
|
|
73
|
+
status = response.status_code
|
|
74
|
+
|
|
75
|
+
if status == 401:
|
|
76
|
+
raise AuthenticationError(error_msg, code=code, status=status)
|
|
77
|
+
elif status == 403:
|
|
78
|
+
raise TierRestrictedError(
|
|
79
|
+
error_msg,
|
|
80
|
+
code=code,
|
|
81
|
+
status=status,
|
|
82
|
+
required_tier=body.get("required_tier"),
|
|
83
|
+
)
|
|
84
|
+
elif status == 429:
|
|
85
|
+
raise RateLimitedError(
|
|
86
|
+
error_msg,
|
|
87
|
+
code=code,
|
|
88
|
+
status=status,
|
|
89
|
+
retry_after=body.get("retry_after"),
|
|
90
|
+
)
|
|
91
|
+
elif status == 400:
|
|
92
|
+
raise ValidationError(error_msg, code=code, status=status)
|
|
93
|
+
else:
|
|
94
|
+
raise SharpAPIError(error_msg, code=code, status=status)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def make_headers(api_key: str) -> dict[str, str]:
|
|
98
|
+
"""Build default request headers."""
|
|
99
|
+
return {
|
|
100
|
+
"X-API-Key": api_key,
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"User-Agent": USER_AGENT,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _int_or_none(value: str | None) -> int | None:
|
|
107
|
+
if value is None:
|
|
108
|
+
return None
|
|
109
|
+
try:
|
|
110
|
+
return int(value)
|
|
111
|
+
except (ValueError, TypeError):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _float_or_none(value: str | None) -> float | None:
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
return float(value)
|
|
120
|
+
except (ValueError, TypeError):
|
|
121
|
+
return None
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Internal utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def american_to_decimal(american: int | float) -> float:
|
|
7
|
+
"""Convert American odds to decimal odds."""
|
|
8
|
+
if american > 0:
|
|
9
|
+
return american / 100 + 1
|
|
10
|
+
return 100 / abs(american) + 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def decimal_to_american(decimal: float) -> int:
|
|
14
|
+
"""Convert decimal odds to American odds."""
|
|
15
|
+
if decimal >= 2.0:
|
|
16
|
+
return round((decimal - 1) * 100)
|
|
17
|
+
return round(-100 / (decimal - 1))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def american_to_probability(american: int | float) -> float:
|
|
21
|
+
"""Convert American odds to implied probability (0-1)."""
|
|
22
|
+
if american > 0:
|
|
23
|
+
return 100 / (american + 100)
|
|
24
|
+
return abs(american) / (abs(american) + 100)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _clean_params(params: dict) -> dict:
|
|
28
|
+
"""Remove None values and join lists with commas."""
|
|
29
|
+
cleaned = {}
|
|
30
|
+
for key, value in params.items():
|
|
31
|
+
if value is None:
|
|
32
|
+
continue
|
|
33
|
+
if isinstance(value, bool):
|
|
34
|
+
cleaned[key] = str(value).lower()
|
|
35
|
+
elif isinstance(value, list):
|
|
36
|
+
cleaned[key] = ",".join(str(v) for v in value)
|
|
37
|
+
else:
|
|
38
|
+
cleaned[key] = value
|
|
39
|
+
return cleaned
|