puntersedge 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.
- puntersedge-0.1.0/PKG-INFO +134 -0
- puntersedge-0.1.0/README.md +116 -0
- puntersedge-0.1.0/puntersedge/__init__.py +3 -0
- puntersedge-0.1.0/puntersedge/client.py +162 -0
- puntersedge-0.1.0/puntersedge/exceptions.py +20 -0
- puntersedge-0.1.0/puntersedge/models.py +68 -0
- puntersedge-0.1.0/puntersedge.egg-info/PKG-INFO +134 -0
- puntersedge-0.1.0/puntersedge.egg-info/SOURCES.txt +12 -0
- puntersedge-0.1.0/puntersedge.egg-info/dependency_links.txt +1 -0
- puntersedge-0.1.0/puntersedge.egg-info/requires.txt +1 -0
- puntersedge-0.1.0/puntersedge.egg-info/top_level.txt +1 -0
- puntersedge-0.1.0/pyproject.toml +20 -0
- puntersedge-0.1.0/setup.cfg +4 -0
- puntersedge-0.1.0/setup.py +19 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: puntersedge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the PuntersEdge Australian Sports Odds API
|
|
5
|
+
Home-page: https://puntersedge.online/developers
|
|
6
|
+
Author: PuntersEdge
|
|
7
|
+
Author-email: PuntersEdge <hello@puntersedge.online>
|
|
8
|
+
Project-URL: Homepage, https://puntersedge.online
|
|
9
|
+
Project-URL: Documentation, https://puntersedge.online/developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: home-page
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
|
|
19
|
+
# PuntersEdge Python SDK
|
|
20
|
+
|
|
21
|
+
Official Python client for the PuntersEdge Australian Sports Odds API.
|
|
22
|
+
|
|
23
|
+
PuntersEdge gives developers simple REST access to Australian sports odds, best-price comparisons, next-to-go racing and arbitrage signals from local bookmakers.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install puntersedge
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For local development from this repository:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e sdk/python
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from puntersedge import PuntersEdgeClient
|
|
41
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
42
|
+
sports = client.get_sports()
|
|
43
|
+
odds = client.get_odds("nrl", markets=["h2h"])
|
|
44
|
+
print(odds[:1])
|
|
45
|
+
client.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
Pass your API key when creating the client. The SDK sends it as the `X-API-Key` header.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
client = PuntersEdgeClient(api_key="YOUR_API_KEY")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can override the API base URL for testing:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = PuntersEdgeClient("YOUR_API_KEY", base_url="http://localhost:8010")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Methods
|
|
63
|
+
|
|
64
|
+
| Method | Description | Returns | Endpoint |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `get_sports()` | List active sports and competitions | `list` | `GET /v1/sports` |
|
|
67
|
+
| `get_odds(sport_key, markets=None, bookmakers=None)` | Get bookmaker odds for a sport. Defaults to `h2h` market. | `list` | `GET /v1/sports/{sport_key}/odds` |
|
|
68
|
+
| `get_best_odds(sport_key)` | Get best available prices by outcome for a sport | `list` | `GET /v1/best-odds/{sport_key}` |
|
|
69
|
+
| `get_racing(categories=None, num_races=10)` | Get next-to-go racing markets | `list` | `GET /v1/racing/next-to-go` |
|
|
70
|
+
| `get_arb(sport_key=None, min_profit_pct=0.5)` | Scan for arbitrage opportunities | `dict` | `GET /v1/arb/sports` |
|
|
71
|
+
| `get_usage()` | View plan, credits, endpoint usage and recent activity | `dict` | `GET /v1/usage` |
|
|
72
|
+
| `get_health()` | Connector health and freshness | `dict` | `GET /v1/health` |
|
|
73
|
+
| `close()` | Close the underlying HTTP connection pool | `None` | - |
|
|
74
|
+
|
|
75
|
+
## Async usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
from puntersedge import AsyncPuntersEdgeClient
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
async with AsyncPuntersEdgeClient("YOUR_API_KEY") as client:
|
|
83
|
+
sports = await client.get_sports()
|
|
84
|
+
odds = await client.get_odds("afl", markets=["h2h"])
|
|
85
|
+
usage = await client.get_usage()
|
|
86
|
+
print(len(sports), len(odds), usage["credits_remaining"])
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Error handling
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from puntersedge import PuntersEdgeClient
|
|
95
|
+
from puntersedge.exceptions import AuthError, RateLimitError, NotFoundError, ServerError
|
|
96
|
+
|
|
97
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
98
|
+
try:
|
|
99
|
+
print(client.get_odds("nrl"))
|
|
100
|
+
except AuthError:
|
|
101
|
+
print("Invalid API key")
|
|
102
|
+
except RateLimitError as exc:
|
|
103
|
+
print(f"Credit limit reached. Resets at {exc.reset_at}")
|
|
104
|
+
except NotFoundError:
|
|
105
|
+
print("Unknown sport or endpoint")
|
|
106
|
+
except ServerError:
|
|
107
|
+
print("PuntersEdge API is temporarily unavailable")
|
|
108
|
+
finally:
|
|
109
|
+
client.close()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
All SDK exceptions inherit from `PuntersEdgeError`.
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
See the `examples/` directory:
|
|
117
|
+
|
|
118
|
+
- `basic_usage.py` — list sports, fetch NRL odds and check usage
|
|
119
|
+
- `arb_scanner.py` — scan all sports for arbitrage opportunities
|
|
120
|
+
- `racing_ntg.py` — print next-to-go racing runners and prices
|
|
121
|
+
|
|
122
|
+
Run an example with:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
PUNTERSEDGE_API_KEY=YOUR_API_KEY python sdk/python/examples/basic_usage.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Documentation
|
|
129
|
+
|
|
130
|
+
- API docs: https://puntersedge.online/api/docs
|
|
131
|
+
- Developer docs: https://puntersedge.online/developers
|
|
132
|
+
- Website: https://puntersedge.online
|
|
133
|
+
|
|
134
|
+
18+ only. Please gamble responsibly.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# PuntersEdge Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the PuntersEdge Australian Sports Odds API.
|
|
4
|
+
|
|
5
|
+
PuntersEdge gives developers simple REST access to Australian sports odds, best-price comparisons, next-to-go racing and arbitrage signals from local bookmakers.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install puntersedge
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For local development from this repository:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e sdk/python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from puntersedge import PuntersEdgeClient
|
|
23
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
24
|
+
sports = client.get_sports()
|
|
25
|
+
odds = client.get_odds("nrl", markets=["h2h"])
|
|
26
|
+
print(odds[:1])
|
|
27
|
+
client.close()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
Pass your API key when creating the client. The SDK sends it as the `X-API-Key` header.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
client = PuntersEdgeClient(api_key="YOUR_API_KEY")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can override the API base URL for testing:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
client = PuntersEdgeClient("YOUR_API_KEY", base_url="http://localhost:8010")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Methods
|
|
45
|
+
|
|
46
|
+
| Method | Description | Returns | Endpoint |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| `get_sports()` | List active sports and competitions | `list` | `GET /v1/sports` |
|
|
49
|
+
| `get_odds(sport_key, markets=None, bookmakers=None)` | Get bookmaker odds for a sport. Defaults to `h2h` market. | `list` | `GET /v1/sports/{sport_key}/odds` |
|
|
50
|
+
| `get_best_odds(sport_key)` | Get best available prices by outcome for a sport | `list` | `GET /v1/best-odds/{sport_key}` |
|
|
51
|
+
| `get_racing(categories=None, num_races=10)` | Get next-to-go racing markets | `list` | `GET /v1/racing/next-to-go` |
|
|
52
|
+
| `get_arb(sport_key=None, min_profit_pct=0.5)` | Scan for arbitrage opportunities | `dict` | `GET /v1/arb/sports` |
|
|
53
|
+
| `get_usage()` | View plan, credits, endpoint usage and recent activity | `dict` | `GET /v1/usage` |
|
|
54
|
+
| `get_health()` | Connector health and freshness | `dict` | `GET /v1/health` |
|
|
55
|
+
| `close()` | Close the underlying HTTP connection pool | `None` | - |
|
|
56
|
+
|
|
57
|
+
## Async usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from puntersedge import AsyncPuntersEdgeClient
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
async with AsyncPuntersEdgeClient("YOUR_API_KEY") as client:
|
|
65
|
+
sports = await client.get_sports()
|
|
66
|
+
odds = await client.get_odds("afl", markets=["h2h"])
|
|
67
|
+
usage = await client.get_usage()
|
|
68
|
+
print(len(sports), len(odds), usage["credits_remaining"])
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Error handling
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from puntersedge import PuntersEdgeClient
|
|
77
|
+
from puntersedge.exceptions import AuthError, RateLimitError, NotFoundError, ServerError
|
|
78
|
+
|
|
79
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
80
|
+
try:
|
|
81
|
+
print(client.get_odds("nrl"))
|
|
82
|
+
except AuthError:
|
|
83
|
+
print("Invalid API key")
|
|
84
|
+
except RateLimitError as exc:
|
|
85
|
+
print(f"Credit limit reached. Resets at {exc.reset_at}")
|
|
86
|
+
except NotFoundError:
|
|
87
|
+
print("Unknown sport or endpoint")
|
|
88
|
+
except ServerError:
|
|
89
|
+
print("PuntersEdge API is temporarily unavailable")
|
|
90
|
+
finally:
|
|
91
|
+
client.close()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
All SDK exceptions inherit from `PuntersEdgeError`.
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
|
|
98
|
+
See the `examples/` directory:
|
|
99
|
+
|
|
100
|
+
- `basic_usage.py` — list sports, fetch NRL odds and check usage
|
|
101
|
+
- `arb_scanner.py` — scan all sports for arbitrage opportunities
|
|
102
|
+
- `racing_ntg.py` — print next-to-go racing runners and prices
|
|
103
|
+
|
|
104
|
+
Run an example with:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
PUNTERSEDGE_API_KEY=YOUR_API_KEY python sdk/python/examples/basic_usage.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Documentation
|
|
111
|
+
|
|
112
|
+
- API docs: https://puntersedge.online/api/docs
|
|
113
|
+
- Developer docs: https://puntersedge.online/developers
|
|
114
|
+
- Website: https://puntersedge.online
|
|
115
|
+
|
|
116
|
+
18+ only. Please gamble responsibly.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from .exceptions import AuthError, RateLimitError, NotFoundError, ServerError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PuntersEdgeClient:
|
|
6
|
+
"""Synchronous PuntersEdge API client."""
|
|
7
|
+
BASE_URL = "https://puntersedge.online/api"
|
|
8
|
+
|
|
9
|
+
def __init__(self, api_key: str, timeout: int = 30, base_url: str = None):
|
|
10
|
+
self.api_key = api_key
|
|
11
|
+
self.timeout = timeout
|
|
12
|
+
if base_url:
|
|
13
|
+
self.BASE_URL = base_url
|
|
14
|
+
self._client = httpx.Client(
|
|
15
|
+
headers={"X-API-Key": api_key, "User-Agent": "puntersedge-python/0.1.0"},
|
|
16
|
+
timeout=timeout,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def _handle_response(self, resp):
|
|
20
|
+
if resp.status_code == 401:
|
|
21
|
+
raise AuthError("Invalid API key", status_code=401, response=resp)
|
|
22
|
+
if resp.status_code == 402:
|
|
23
|
+
data = resp.json()
|
|
24
|
+
raise RateLimitError(
|
|
25
|
+
data.get("detail", "Credit limit reached"),
|
|
26
|
+
credits_remaining=data.get("credits_remaining", 0),
|
|
27
|
+
reset_at=data.get("reset_at"),
|
|
28
|
+
status_code=402,
|
|
29
|
+
response=resp,
|
|
30
|
+
)
|
|
31
|
+
if resp.status_code == 404:
|
|
32
|
+
raise NotFoundError(f"Not found: {resp.url}", status_code=404, response=resp)
|
|
33
|
+
if resp.status_code >= 500:
|
|
34
|
+
raise ServerError(f"Server error: {resp.status_code}", status_code=resp.status_code, response=resp)
|
|
35
|
+
resp.raise_for_status()
|
|
36
|
+
return resp.json()
|
|
37
|
+
|
|
38
|
+
def get_sports(self) -> list:
|
|
39
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/sports")
|
|
40
|
+
return self._handle_response(resp)
|
|
41
|
+
|
|
42
|
+
def get_odds(self, sport_key: str, markets: list = None, bookmakers: list = None) -> list:
|
|
43
|
+
params = {"markets": ",".join(markets or ["h2h"])}
|
|
44
|
+
if bookmakers:
|
|
45
|
+
params["bookmakers"] = ",".join(bookmakers)
|
|
46
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/sports/{sport_key}/odds", params=params)
|
|
47
|
+
return self._handle_response(resp)
|
|
48
|
+
|
|
49
|
+
def get_best_odds(self, sport_key: str) -> list:
|
|
50
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/best-odds/{sport_key}")
|
|
51
|
+
return self._handle_response(resp)
|
|
52
|
+
|
|
53
|
+
def get_racing(self, categories: list = None, num_races: int = 10) -> list:
|
|
54
|
+
params = {"num_races": num_races}
|
|
55
|
+
if categories:
|
|
56
|
+
params["categories"] = ",".join(categories)
|
|
57
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/racing/next-to-go", params=params)
|
|
58
|
+
return self._handle_response(resp)
|
|
59
|
+
|
|
60
|
+
def get_arb(self, sport_key: str = None, min_profit_pct: float = 0.5) -> dict:
|
|
61
|
+
params = {"min_profit_pct": min_profit_pct}
|
|
62
|
+
if sport_key:
|
|
63
|
+
params["sport_key"] = sport_key
|
|
64
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/arb/sports", params=params)
|
|
65
|
+
return self._handle_response(resp)
|
|
66
|
+
|
|
67
|
+
def get_usage(self) -> dict:
|
|
68
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/usage")
|
|
69
|
+
return self._handle_response(resp)
|
|
70
|
+
|
|
71
|
+
def get_health(self) -> dict:
|
|
72
|
+
resp = self._client.get(f"{self.BASE_URL}/v1/health")
|
|
73
|
+
return self._handle_response(resp)
|
|
74
|
+
|
|
75
|
+
def close(self):
|
|
76
|
+
self._client.close()
|
|
77
|
+
|
|
78
|
+
def __enter__(self):
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __exit__(self, *args):
|
|
82
|
+
self.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncPuntersEdgeClient:
|
|
86
|
+
"""Async PuntersEdge API client."""
|
|
87
|
+
BASE_URL = "https://puntersedge.online/api"
|
|
88
|
+
|
|
89
|
+
def __init__(self, api_key: str, timeout: int = 30, base_url: str = None):
|
|
90
|
+
self.api_key = api_key
|
|
91
|
+
self.timeout = timeout
|
|
92
|
+
if base_url:
|
|
93
|
+
self.BASE_URL = base_url
|
|
94
|
+
self._client = httpx.AsyncClient(
|
|
95
|
+
headers={"X-API-Key": api_key, "User-Agent": "puntersedge-python/0.1.0"},
|
|
96
|
+
timeout=timeout,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _handle_response(self, resp):
|
|
100
|
+
if resp.status_code == 401:
|
|
101
|
+
raise AuthError("Invalid API key", status_code=401, response=resp)
|
|
102
|
+
if resp.status_code == 402:
|
|
103
|
+
data = resp.json()
|
|
104
|
+
raise RateLimitError(
|
|
105
|
+
data.get("detail", "Credit limit reached"),
|
|
106
|
+
credits_remaining=data.get("credits_remaining", 0),
|
|
107
|
+
reset_at=data.get("reset_at"),
|
|
108
|
+
status_code=402,
|
|
109
|
+
response=resp,
|
|
110
|
+
)
|
|
111
|
+
if resp.status_code == 404:
|
|
112
|
+
raise NotFoundError("Not found", status_code=404, response=resp)
|
|
113
|
+
if resp.status_code >= 500:
|
|
114
|
+
raise ServerError(f"Server error {resp.status_code}", status_code=resp.status_code, response=resp)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
return resp.json()
|
|
117
|
+
|
|
118
|
+
async def get_sports(self) -> list:
|
|
119
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/sports")
|
|
120
|
+
return self._handle_response(resp)
|
|
121
|
+
|
|
122
|
+
async def get_odds(self, sport_key: str, markets: list = None, bookmakers: list = None) -> list:
|
|
123
|
+
params = {"markets": ",".join(markets or ["h2h"])}
|
|
124
|
+
if bookmakers:
|
|
125
|
+
params["bookmakers"] = ",".join(bookmakers)
|
|
126
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/sports/{sport_key}/odds", params=params)
|
|
127
|
+
return self._handle_response(resp)
|
|
128
|
+
|
|
129
|
+
async def get_best_odds(self, sport_key: str) -> list:
|
|
130
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/best-odds/{sport_key}")
|
|
131
|
+
return self._handle_response(resp)
|
|
132
|
+
|
|
133
|
+
async def get_racing(self, categories: list = None, num_races: int = 10) -> list:
|
|
134
|
+
params = {"num_races": num_races}
|
|
135
|
+
if categories:
|
|
136
|
+
params["categories"] = ",".join(categories)
|
|
137
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/racing/next-to-go", params=params)
|
|
138
|
+
return self._handle_response(resp)
|
|
139
|
+
|
|
140
|
+
async def get_arb(self, sport_key: str = None, min_profit_pct: float = 0.5) -> dict:
|
|
141
|
+
params = {"min_profit_pct": min_profit_pct}
|
|
142
|
+
if sport_key:
|
|
143
|
+
params["sport_key"] = sport_key
|
|
144
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/arb/sports", params=params)
|
|
145
|
+
return self._handle_response(resp)
|
|
146
|
+
|
|
147
|
+
async def get_usage(self) -> dict:
|
|
148
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/usage")
|
|
149
|
+
return self._handle_response(resp)
|
|
150
|
+
|
|
151
|
+
async def get_health(self) -> dict:
|
|
152
|
+
resp = await self._client.get(f"{self.BASE_URL}/v1/health")
|
|
153
|
+
return self._handle_response(resp)
|
|
154
|
+
|
|
155
|
+
async def close(self):
|
|
156
|
+
await self._client.aclose()
|
|
157
|
+
|
|
158
|
+
async def __aenter__(self):
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
async def __aexit__(self, *args):
|
|
162
|
+
await self.close()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class PuntersEdgeError(Exception):
|
|
2
|
+
def __init__(self, message, status_code=None, response=None):
|
|
3
|
+
super().__init__(message)
|
|
4
|
+
self.status_code = status_code
|
|
5
|
+
self.response = response
|
|
6
|
+
|
|
7
|
+
class AuthError(PuntersEdgeError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class RateLimitError(PuntersEdgeError):
|
|
11
|
+
def __init__(self, message, credits_remaining=0, reset_at=None, **kwargs):
|
|
12
|
+
super().__init__(message, **kwargs)
|
|
13
|
+
self.credits_remaining = credits_remaining
|
|
14
|
+
self.reset_at = reset_at
|
|
15
|
+
|
|
16
|
+
class NotFoundError(PuntersEdgeError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class ServerError(PuntersEdgeError):
|
|
20
|
+
pass
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Outcome:
|
|
7
|
+
name: str
|
|
8
|
+
price: float
|
|
9
|
+
point: Optional[float] = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def from_dict(cls, d):
|
|
13
|
+
return cls(**{k: d.get(k) for k in ['name', 'price', 'point']})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class BookmakerOdds:
|
|
18
|
+
bookmaker_key: str
|
|
19
|
+
bookmaker_title: str
|
|
20
|
+
outcomes: List[Outcome]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_dict(cls, d):
|
|
24
|
+
return cls(
|
|
25
|
+
bookmaker_key=d.get('bookmaker_key', ''),
|
|
26
|
+
bookmaker_title=d.get('bookmaker_title', ''),
|
|
27
|
+
outcomes=[Outcome.from_dict(o) for o in d.get('outcomes', [])]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Event:
|
|
33
|
+
id: str
|
|
34
|
+
sport_key: str
|
|
35
|
+
home_team: str
|
|
36
|
+
away_team: str
|
|
37
|
+
commence_time: str
|
|
38
|
+
bookmakers: List[BookmakerOdds]
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, d):
|
|
42
|
+
return cls(
|
|
43
|
+
id=d.get('id', ''),
|
|
44
|
+
sport_key=d.get('sport_key', ''),
|
|
45
|
+
home_team=d.get('home_team', ''),
|
|
46
|
+
away_team=d.get('away_team', ''),
|
|
47
|
+
commence_time=d.get('commence_time', ''),
|
|
48
|
+
bookmakers=[BookmakerOdds.from_dict(b) for b in d.get('bookmakers', [])]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class UsageInfo:
|
|
54
|
+
plan: str
|
|
55
|
+
credits_used: int
|
|
56
|
+
credits_limit: int
|
|
57
|
+
credits_remaining: int
|
|
58
|
+
reset_at: Optional[str]
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, d):
|
|
62
|
+
return cls(
|
|
63
|
+
plan=d['plan'],
|
|
64
|
+
credits_used=d['credits_used'],
|
|
65
|
+
credits_limit=d['credits_limit'],
|
|
66
|
+
credits_remaining=d['credits_remaining'],
|
|
67
|
+
reset_at=d.get('reset_at'),
|
|
68
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: puntersedge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the PuntersEdge Australian Sports Odds API
|
|
5
|
+
Home-page: https://puntersedge.online/developers
|
|
6
|
+
Author: PuntersEdge
|
|
7
|
+
Author-email: PuntersEdge <hello@puntersedge.online>
|
|
8
|
+
Project-URL: Homepage, https://puntersedge.online
|
|
9
|
+
Project-URL: Documentation, https://puntersedge.online/developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: home-page
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
|
|
19
|
+
# PuntersEdge Python SDK
|
|
20
|
+
|
|
21
|
+
Official Python client for the PuntersEdge Australian Sports Odds API.
|
|
22
|
+
|
|
23
|
+
PuntersEdge gives developers simple REST access to Australian sports odds, best-price comparisons, next-to-go racing and arbitrage signals from local bookmakers.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install puntersedge
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For local development from this repository:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e sdk/python
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from puntersedge import PuntersEdgeClient
|
|
41
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
42
|
+
sports = client.get_sports()
|
|
43
|
+
odds = client.get_odds("nrl", markets=["h2h"])
|
|
44
|
+
print(odds[:1])
|
|
45
|
+
client.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
Pass your API key when creating the client. The SDK sends it as the `X-API-Key` header.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
client = PuntersEdgeClient(api_key="YOUR_API_KEY")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can override the API base URL for testing:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = PuntersEdgeClient("YOUR_API_KEY", base_url="http://localhost:8010")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Methods
|
|
63
|
+
|
|
64
|
+
| Method | Description | Returns | Endpoint |
|
|
65
|
+
| --- | --- | --- | --- |
|
|
66
|
+
| `get_sports()` | List active sports and competitions | `list` | `GET /v1/sports` |
|
|
67
|
+
| `get_odds(sport_key, markets=None, bookmakers=None)` | Get bookmaker odds for a sport. Defaults to `h2h` market. | `list` | `GET /v1/sports/{sport_key}/odds` |
|
|
68
|
+
| `get_best_odds(sport_key)` | Get best available prices by outcome for a sport | `list` | `GET /v1/best-odds/{sport_key}` |
|
|
69
|
+
| `get_racing(categories=None, num_races=10)` | Get next-to-go racing markets | `list` | `GET /v1/racing/next-to-go` |
|
|
70
|
+
| `get_arb(sport_key=None, min_profit_pct=0.5)` | Scan for arbitrage opportunities | `dict` | `GET /v1/arb/sports` |
|
|
71
|
+
| `get_usage()` | View plan, credits, endpoint usage and recent activity | `dict` | `GET /v1/usage` |
|
|
72
|
+
| `get_health()` | Connector health and freshness | `dict` | `GET /v1/health` |
|
|
73
|
+
| `close()` | Close the underlying HTTP connection pool | `None` | - |
|
|
74
|
+
|
|
75
|
+
## Async usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
from puntersedge import AsyncPuntersEdgeClient
|
|
80
|
+
|
|
81
|
+
async def main():
|
|
82
|
+
async with AsyncPuntersEdgeClient("YOUR_API_KEY") as client:
|
|
83
|
+
sports = await client.get_sports()
|
|
84
|
+
odds = await client.get_odds("afl", markets=["h2h"])
|
|
85
|
+
usage = await client.get_usage()
|
|
86
|
+
print(len(sports), len(odds), usage["credits_remaining"])
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Error handling
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from puntersedge import PuntersEdgeClient
|
|
95
|
+
from puntersedge.exceptions import AuthError, RateLimitError, NotFoundError, ServerError
|
|
96
|
+
|
|
97
|
+
client = PuntersEdgeClient("YOUR_API_KEY")
|
|
98
|
+
try:
|
|
99
|
+
print(client.get_odds("nrl"))
|
|
100
|
+
except AuthError:
|
|
101
|
+
print("Invalid API key")
|
|
102
|
+
except RateLimitError as exc:
|
|
103
|
+
print(f"Credit limit reached. Resets at {exc.reset_at}")
|
|
104
|
+
except NotFoundError:
|
|
105
|
+
print("Unknown sport or endpoint")
|
|
106
|
+
except ServerError:
|
|
107
|
+
print("PuntersEdge API is temporarily unavailable")
|
|
108
|
+
finally:
|
|
109
|
+
client.close()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
All SDK exceptions inherit from `PuntersEdgeError`.
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
See the `examples/` directory:
|
|
117
|
+
|
|
118
|
+
- `basic_usage.py` — list sports, fetch NRL odds and check usage
|
|
119
|
+
- `arb_scanner.py` — scan all sports for arbitrage opportunities
|
|
120
|
+
- `racing_ntg.py` — print next-to-go racing runners and prices
|
|
121
|
+
|
|
122
|
+
Run an example with:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
PUNTERSEDGE_API_KEY=YOUR_API_KEY python sdk/python/examples/basic_usage.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Documentation
|
|
129
|
+
|
|
130
|
+
- API docs: https://puntersedge.online/api/docs
|
|
131
|
+
- Developer docs: https://puntersedge.online/developers
|
|
132
|
+
- Website: https://puntersedge.online
|
|
133
|
+
|
|
134
|
+
18+ only. Please gamble responsibly.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
puntersedge/__init__.py
|
|
5
|
+
puntersedge/client.py
|
|
6
|
+
puntersedge/exceptions.py
|
|
7
|
+
puntersedge/models.py
|
|
8
|
+
puntersedge.egg-info/PKG-INFO
|
|
9
|
+
puntersedge.egg-info/SOURCES.txt
|
|
10
|
+
puntersedge.egg-info/dependency_links.txt
|
|
11
|
+
puntersedge.egg-info/requires.txt
|
|
12
|
+
puntersedge.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.27.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
puntersedge
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "puntersedge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the PuntersEdge Australian Sports Odds API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = ["httpx>=0.27.0"]
|
|
12
|
+
authors = [{name = "PuntersEdge", email = "hello@puntersedge.online"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://puntersedge.online"
|
|
20
|
+
Documentation = "https://puntersedge.online/developers"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="puntersedge",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Official Python SDK for the PuntersEdge Australian Sports Odds API",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="PuntersEdge",
|
|
10
|
+
author_email="hello@puntersedge.online",
|
|
11
|
+
url="https://puntersedge.online/developers",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=["httpx>=0.27.0"],
|
|
14
|
+
python_requires=">=3.8",
|
|
15
|
+
classifiers=[
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
],
|
|
19
|
+
)
|