draftkingsapi 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.
- draftkingsapi-0.1.0/LICENSE +21 -0
- draftkingsapi-0.1.0/PKG-INFO +152 -0
- draftkingsapi-0.1.0/README.md +129 -0
- draftkingsapi-0.1.0/pyproject.toml +35 -0
- draftkingsapi-0.1.0/setup.cfg +4 -0
- draftkingsapi-0.1.0/src/draftkingsapi/__init__.py +31 -0
- draftkingsapi-0.1.0/src/draftkingsapi/_version.py +1 -0
- draftkingsapi-0.1.0/src/draftkingsapi/client.py +334 -0
- draftkingsapi-0.1.0/src/draftkingsapi/errors.py +88 -0
- draftkingsapi-0.1.0/src/draftkingsapi/py.typed +0 -0
- draftkingsapi-0.1.0/src/draftkingsapi.egg-info/PKG-INFO +152 -0
- draftkingsapi-0.1.0/src/draftkingsapi.egg-info/SOURCES.txt +14 -0
- draftkingsapi-0.1.0/src/draftkingsapi.egg-info/dependency_links.txt +1 -0
- draftkingsapi-0.1.0/src/draftkingsapi.egg-info/top_level.txt +1 -0
- draftkingsapi-0.1.0/tests/test_client.py +169 -0
- draftkingsapi-0.1.0/tests/test_live.py +50 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 draftkingsapi.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: draftkingsapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for draftkingsapi.com: DraftKings Pick6 lines and line history, DFS salaries, sportsbook odds and box scores. Independent, not affiliated with DraftKings.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/draftkingsapi/draftkingsapi-python
|
|
7
|
+
Project-URL: Issues, https://github.com/draftkingsapi/draftkingsapi-python/issues
|
|
8
|
+
Project-URL: Homepage, https://draftkingsapi.com
|
|
9
|
+
Project-URL: Documentation, https://draftkingsapi.com/docs
|
|
10
|
+
Project-URL: OpenAPI, https://draftkingsapi.com/openapi.json
|
|
11
|
+
Project-URL: Status, https://draftkingsapi.com/status
|
|
12
|
+
Keywords: draftkings,pick6,dfs,sportsbook,odds,sports-data,api-client
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# draftkingsapi (Python)
|
|
25
|
+
|
|
26
|
+
Python client for [draftkingsapi.com](https://draftkingsapi.com): DraftKings Pick6 lines and line history,
|
|
27
|
+
DFS salaries, contests and slates, sportsbook odds and price history, and box scores. Standard library only,
|
|
28
|
+
no dependencies, typed.
|
|
29
|
+
|
|
30
|
+
draftkingsapi.com is an independent API. It is not affiliated with or endorsed by DraftKings. "DraftKings" is a
|
|
31
|
+
trademark of its owner and is used here only to describe the data.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install draftkingsapi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## No key needed
|
|
38
|
+
|
|
39
|
+
The keyless feeds are delayed and capped, cost no credits, and need no account:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from draftkingsapi import Client
|
|
43
|
+
|
|
44
|
+
dk = Client() # reads DRAFTKINGS_API_KEY if set; keyless calls never send it
|
|
45
|
+
|
|
46
|
+
board = dk.public_board(league="nba", limit=3)
|
|
47
|
+
for prop in board["props"]:
|
|
48
|
+
print(prop["player"], prop["stat"], prop["line"], prop["more_multiplier"])
|
|
49
|
+
|
|
50
|
+
games = dk.public_sportsbook(league="nfl", limit=2)
|
|
51
|
+
for game in games["games"]:
|
|
52
|
+
print(game["away"], "at", game["home"], [(m["market"], m["side"], m["line"], m["american"]) for m in game["markets"]])
|
|
53
|
+
|
|
54
|
+
moves = dk.public_line_moves(market="pick6", limit=5)
|
|
55
|
+
costs = dk.credit_costs() # credits per endpoint
|
|
56
|
+
status = dk.status() # measured collection status
|
|
57
|
+
sample = dk.demo("odds", limit=1) # frozen sample rows, for the response shape only
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Output of the first loop, run on 15 Nov 2026 (live data, so yours will differ; a prop with no current line prints None):
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Giannis Antetokounmpo Points 32.5 0.8
|
|
64
|
+
Cade Cunningham Points 30.5 1
|
|
65
|
+
Bam Adebayo Points None None
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## With a key
|
|
69
|
+
|
|
70
|
+
Get a free key (100 credits a month, no card) at https://draftkingsapi.com/sign-up, then:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export DRAFTKINGS_API_KEY=dk_...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from draftkingsapi import Client
|
|
78
|
+
|
|
79
|
+
dk = Client(user_request="Build a prop screen from current Pick6 lines")
|
|
80
|
+
|
|
81
|
+
lines = dk.pick6_projections(sport="NBA", limit=100)
|
|
82
|
+
history = dk.pick6_line_history(lines["data"][0]["pickable_id"], change_type="line_move")
|
|
83
|
+
odds = dk.odds(sport="NFL", market="moneyline")
|
|
84
|
+
salaries = dk.dfs_players(sport="NFL", format="csv") # CSV text
|
|
85
|
+
box = dk.stats_player_games("NBA", 2026, limit=50)
|
|
86
|
+
edges = dk.ev(league="nba", min_edge=2)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`user_request` is optional: one plain sentence about the task, sent as `X-User-Request`. Never put personal data in it.
|
|
90
|
+
|
|
91
|
+
### Pagination
|
|
92
|
+
|
|
93
|
+
List routes return `pagination.has_more` with a cursor. `paginate()` follows it with a page budget, so a loop cannot
|
|
94
|
+
spend your whole allowance:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
for row in dk.paginate(dk.odds, sport="NBA", max_pages=5):
|
|
98
|
+
...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It follows `next_cursor` (sent back as `cursor`), `next` on the stats routes and `next_after` on the change feeds
|
|
102
|
+
(both sent back as `after`).
|
|
103
|
+
|
|
104
|
+
### Errors
|
|
105
|
+
|
|
106
|
+
Every non-2xx response raises a subclass of `DraftKingsAPIError` with the API's own message:
|
|
107
|
+
|
|
108
|
+
| Status | Exception | What to do |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 400 | `InvalidRequestError` | `e.error["query_help"]` often has a corrected example |
|
|
111
|
+
| 401 | `AuthenticationError` | missing or wrong key; raised before any request when a keyed method has no key |
|
|
112
|
+
| 402 | `PaymentRequiredError` | `e.error["upgrade_url"]` |
|
|
113
|
+
| 404 | `NotFoundError` | `e.error["closest_endpoints"]` |
|
|
114
|
+
| 429 | `RateLimitError` | wait `e.retry_after` seconds; if `e.error["limit"] == "month"`, stop retrying |
|
|
115
|
+
| 503 | `DatasetUnavailableError` | dataset temporarily unavailable; not charged, retry later |
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from draftkingsapi import Client, RateLimitError
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
Client().odds(sport="NFL")
|
|
122
|
+
except RateLimitError as e:
|
|
123
|
+
print("retry in", e.retry_after, "seconds:", e.message)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Methods
|
|
127
|
+
|
|
128
|
+
Keyless: `public_board`, `public_sportsbook`, `public_line_moves`, `demo(dataset)`, `credit_costs`, `directory`,
|
|
129
|
+
`status`, `history_coverage`.
|
|
130
|
+
|
|
131
|
+
Keyed: `coverage`, `events`, `odds`, `history`, `pick6_projections`, `pick6_history`, `pick6_line_history`,
|
|
132
|
+
`projections`, `changes`, `dfs_sports`, `dfs_contests`, `dfs_slates`, `dfs_players`, `dfs_changes`, `compare`,
|
|
133
|
+
`compare_books`, `ev`, `arbitrage`, `stats_games`, `stats_player_games`, `stats_team_games`, `context_games`,
|
|
134
|
+
`context_player_stats`, `context_team_stats`, `sportsbook_scopes`, `sportsbook_snapshot`, `stream_snapshot`.
|
|
135
|
+
|
|
136
|
+
Optional filters are keyword arguments, passed through unchanged. Every route's parameters are in the
|
|
137
|
+
[OpenAPI document](https://draftkingsapi.com/openapi.json) and the [docs](https://draftkingsapi.com/docs).
|
|
138
|
+
Not wrapped: the server-sent event stream, the native sportsbook WebSocket, and account, billing and sign-up routes.
|
|
139
|
+
|
|
140
|
+
## Plans
|
|
141
|
+
|
|
142
|
+
Free 100 credits a month, Developer $49, Production $149, Scale $499 a month. Credits per endpoint:
|
|
143
|
+
`Client().credit_costs()` or https://draftkingsapi.com/api/v1/credit-costs.
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python -m unittest discover -s tests # mocked, no network
|
|
149
|
+
DRAFTKINGS_LIVE=1 python -m unittest tests.test_live -v # live keyless endpoints, no key, no credits
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
MIT License.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# draftkingsapi (Python)
|
|
2
|
+
|
|
3
|
+
Python client for [draftkingsapi.com](https://draftkingsapi.com): DraftKings Pick6 lines and line history,
|
|
4
|
+
DFS salaries, contests and slates, sportsbook odds and price history, and box scores. Standard library only,
|
|
5
|
+
no dependencies, typed.
|
|
6
|
+
|
|
7
|
+
draftkingsapi.com is an independent API. It is not affiliated with or endorsed by DraftKings. "DraftKings" is a
|
|
8
|
+
trademark of its owner and is used here only to describe the data.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install draftkingsapi
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## No key needed
|
|
15
|
+
|
|
16
|
+
The keyless feeds are delayed and capped, cost no credits, and need no account:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from draftkingsapi import Client
|
|
20
|
+
|
|
21
|
+
dk = Client() # reads DRAFTKINGS_API_KEY if set; keyless calls never send it
|
|
22
|
+
|
|
23
|
+
board = dk.public_board(league="nba", limit=3)
|
|
24
|
+
for prop in board["props"]:
|
|
25
|
+
print(prop["player"], prop["stat"], prop["line"], prop["more_multiplier"])
|
|
26
|
+
|
|
27
|
+
games = dk.public_sportsbook(league="nfl", limit=2)
|
|
28
|
+
for game in games["games"]:
|
|
29
|
+
print(game["away"], "at", game["home"], [(m["market"], m["side"], m["line"], m["american"]) for m in game["markets"]])
|
|
30
|
+
|
|
31
|
+
moves = dk.public_line_moves(market="pick6", limit=5)
|
|
32
|
+
costs = dk.credit_costs() # credits per endpoint
|
|
33
|
+
status = dk.status() # measured collection status
|
|
34
|
+
sample = dk.demo("odds", limit=1) # frozen sample rows, for the response shape only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output of the first loop, run on 15 Nov 2026 (live data, so yours will differ; a prop with no current line prints None):
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Giannis Antetokounmpo Points 32.5 0.8
|
|
41
|
+
Cade Cunningham Points 30.5 1
|
|
42
|
+
Bam Adebayo Points None None
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## With a key
|
|
46
|
+
|
|
47
|
+
Get a free key (100 credits a month, no card) at https://draftkingsapi.com/sign-up, then:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export DRAFTKINGS_API_KEY=dk_...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from draftkingsapi import Client
|
|
55
|
+
|
|
56
|
+
dk = Client(user_request="Build a prop screen from current Pick6 lines")
|
|
57
|
+
|
|
58
|
+
lines = dk.pick6_projections(sport="NBA", limit=100)
|
|
59
|
+
history = dk.pick6_line_history(lines["data"][0]["pickable_id"], change_type="line_move")
|
|
60
|
+
odds = dk.odds(sport="NFL", market="moneyline")
|
|
61
|
+
salaries = dk.dfs_players(sport="NFL", format="csv") # CSV text
|
|
62
|
+
box = dk.stats_player_games("NBA", 2026, limit=50)
|
|
63
|
+
edges = dk.ev(league="nba", min_edge=2)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`user_request` is optional: one plain sentence about the task, sent as `X-User-Request`. Never put personal data in it.
|
|
67
|
+
|
|
68
|
+
### Pagination
|
|
69
|
+
|
|
70
|
+
List routes return `pagination.has_more` with a cursor. `paginate()` follows it with a page budget, so a loop cannot
|
|
71
|
+
spend your whole allowance:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
for row in dk.paginate(dk.odds, sport="NBA", max_pages=5):
|
|
75
|
+
...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It follows `next_cursor` (sent back as `cursor`), `next` on the stats routes and `next_after` on the change feeds
|
|
79
|
+
(both sent back as `after`).
|
|
80
|
+
|
|
81
|
+
### Errors
|
|
82
|
+
|
|
83
|
+
Every non-2xx response raises a subclass of `DraftKingsAPIError` with the API's own message:
|
|
84
|
+
|
|
85
|
+
| Status | Exception | What to do |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| 400 | `InvalidRequestError` | `e.error["query_help"]` often has a corrected example |
|
|
88
|
+
| 401 | `AuthenticationError` | missing or wrong key; raised before any request when a keyed method has no key |
|
|
89
|
+
| 402 | `PaymentRequiredError` | `e.error["upgrade_url"]` |
|
|
90
|
+
| 404 | `NotFoundError` | `e.error["closest_endpoints"]` |
|
|
91
|
+
| 429 | `RateLimitError` | wait `e.retry_after` seconds; if `e.error["limit"] == "month"`, stop retrying |
|
|
92
|
+
| 503 | `DatasetUnavailableError` | dataset temporarily unavailable; not charged, retry later |
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from draftkingsapi import Client, RateLimitError
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
Client().odds(sport="NFL")
|
|
99
|
+
except RateLimitError as e:
|
|
100
|
+
print("retry in", e.retry_after, "seconds:", e.message)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Methods
|
|
104
|
+
|
|
105
|
+
Keyless: `public_board`, `public_sportsbook`, `public_line_moves`, `demo(dataset)`, `credit_costs`, `directory`,
|
|
106
|
+
`status`, `history_coverage`.
|
|
107
|
+
|
|
108
|
+
Keyed: `coverage`, `events`, `odds`, `history`, `pick6_projections`, `pick6_history`, `pick6_line_history`,
|
|
109
|
+
`projections`, `changes`, `dfs_sports`, `dfs_contests`, `dfs_slates`, `dfs_players`, `dfs_changes`, `compare`,
|
|
110
|
+
`compare_books`, `ev`, `arbitrage`, `stats_games`, `stats_player_games`, `stats_team_games`, `context_games`,
|
|
111
|
+
`context_player_stats`, `context_team_stats`, `sportsbook_scopes`, `sportsbook_snapshot`, `stream_snapshot`.
|
|
112
|
+
|
|
113
|
+
Optional filters are keyword arguments, passed through unchanged. Every route's parameters are in the
|
|
114
|
+
[OpenAPI document](https://draftkingsapi.com/openapi.json) and the [docs](https://draftkingsapi.com/docs).
|
|
115
|
+
Not wrapped: the server-sent event stream, the native sportsbook WebSocket, and account, billing and sign-up routes.
|
|
116
|
+
|
|
117
|
+
## Plans
|
|
118
|
+
|
|
119
|
+
Free 100 credits a month, Developer $49, Production $149, Scale $499 a month. Credits per endpoint:
|
|
120
|
+
`Client().credit_costs()` or https://draftkingsapi.com/api/v1/credit-costs.
|
|
121
|
+
|
|
122
|
+
## Tests
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m unittest discover -s tests # mocked, no network
|
|
126
|
+
DRAFTKINGS_LIVE=1 python -m unittest tests.test_live -v # live keyless endpoints, no key, no credits
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
MIT License.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "draftkingsapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for draftkingsapi.com: DraftKings Pick6 lines and line history, DFS salaries, sportsbook odds and box scores. Independent, not affiliated with DraftKings."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["draftkings", "pick6", "dfs", "sportsbook", "odds", "sports-data", "api-client"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/draftkingsapi/draftkingsapi-python"
|
|
25
|
+
Issues = "https://github.com/draftkingsapi/draftkingsapi-python/issues"
|
|
26
|
+
Homepage = "https://draftkingsapi.com"
|
|
27
|
+
Documentation = "https://draftkingsapi.com/docs"
|
|
28
|
+
OpenAPI = "https://draftkingsapi.com/openapi.json"
|
|
29
|
+
Status = "https://draftkingsapi.com/status"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
draftkingsapi = ["py.typed"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Python client for draftkingsapi.com, an independent DraftKings data API (not affiliated with DraftKings)."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import DEFAULT_BASE_URL, DEMO_DATASETS, DraftKingsAPI, Response
|
|
5
|
+
from .errors import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
DatasetUnavailableError,
|
|
8
|
+
DraftKingsAPIError,
|
|
9
|
+
InvalidRequestError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
PaymentRequiredError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Client = DraftKingsAPI
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"Client",
|
|
20
|
+
"DraftKingsAPI",
|
|
21
|
+
"DEFAULT_BASE_URL",
|
|
22
|
+
"DEMO_DATASETS",
|
|
23
|
+
"Response",
|
|
24
|
+
"DraftKingsAPIError",
|
|
25
|
+
"InvalidRequestError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"PaymentRequiredError",
|
|
28
|
+
"NotFoundError",
|
|
29
|
+
"RateLimitError",
|
|
30
|
+
"DatasetUnavailableError",
|
|
31
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Typed client for the draftkingsapi.com REST API. Standard library only.
|
|
2
|
+
|
|
3
|
+
draftkingsapi.com is an independent API, not affiliated with or endorsed by DraftKings.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import Any, Callable, Dict, Iterator, Mapping, Optional, Union
|
|
12
|
+
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .errors import SIGN_UP_URL, AuthenticationError, DraftKingsAPIError, error_for
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://draftkingsapi.com"
|
|
17
|
+
|
|
18
|
+
JSON = Dict[str, Any]
|
|
19
|
+
Params = Mapping[str, Any]
|
|
20
|
+
|
|
21
|
+
DEMO_DATASETS = (
|
|
22
|
+
"pick6",
|
|
23
|
+
"dfs",
|
|
24
|
+
"events",
|
|
25
|
+
"odds",
|
|
26
|
+
"history",
|
|
27
|
+
"pick6-history",
|
|
28
|
+
"coverage",
|
|
29
|
+
"stats-games",
|
|
30
|
+
"stats-player-games",
|
|
31
|
+
"stats-team-games",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
Transport = Callable[[urllib.request.Request, float], "Response"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Response:
|
|
38
|
+
"""What a transport returns: status, headers (lower-case keys) and raw body bytes."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, status: int, headers: Mapping[str, str], body: bytes) -> None:
|
|
41
|
+
self.status = status
|
|
42
|
+
self.headers = {k.lower(): v for k, v in headers.items()}
|
|
43
|
+
self.body = body
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _urllib_transport(request: urllib.request.Request, timeout: float) -> Response:
|
|
47
|
+
try:
|
|
48
|
+
with urllib.request.urlopen(request, timeout=timeout) as res:
|
|
49
|
+
return Response(res.status, dict(res.headers.items()), res.read())
|
|
50
|
+
except urllib.error.HTTPError as e:
|
|
51
|
+
return Response(e.code, dict(e.headers.items()) if e.headers else {}, e.read() or b"")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _clean(params: Optional[Params]) -> Dict[str, str]:
|
|
55
|
+
out: Dict[str, str] = {}
|
|
56
|
+
for key, value in (params or {}).items():
|
|
57
|
+
if value is None:
|
|
58
|
+
continue
|
|
59
|
+
if isinstance(value, bool):
|
|
60
|
+
value = "true" if value else "false"
|
|
61
|
+
out[key] = str(value)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DraftKingsAPI:
|
|
66
|
+
"""Client for https://draftkingsapi.com/api/v1.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
api_key: your key (``dk_...``). Defaults to the ``DRAFTKINGS_API_KEY`` environment variable.
|
|
70
|
+
Keyless methods (``public_*``, ``demo``, ``credit_costs``, ``status``) work without one.
|
|
71
|
+
base_url: override for testing.
|
|
72
|
+
timeout: seconds per request.
|
|
73
|
+
user_request: optional one-sentence task sent as ``X-User-Request`` on every call, which
|
|
74
|
+
the API uses to improve its answers. Never put anything personal in it.
|
|
75
|
+
transport: injectable HTTP function, used by the tests.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
api_key: Optional[str] = None,
|
|
81
|
+
*,
|
|
82
|
+
base_url: Optional[str] = None,
|
|
83
|
+
timeout: float = 30.0,
|
|
84
|
+
user_request: Optional[str] = None,
|
|
85
|
+
transport: Optional[Transport] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
self.api_key = api_key if api_key is not None else os.environ.get("DRAFTKINGS_API_KEY") or None
|
|
88
|
+
self.base_url = (base_url or os.environ.get("DRAFTKINGS_API_BASE") or DEFAULT_BASE_URL).rstrip("/")
|
|
89
|
+
self.timeout = timeout
|
|
90
|
+
self.user_request = user_request
|
|
91
|
+
self._transport = transport or _urllib_transport
|
|
92
|
+
self.last_headers: Dict[str, str] = {}
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------ core
|
|
95
|
+
|
|
96
|
+
def request(self, path: str, params: Optional[Params] = None, *, keyed: bool = True) -> Union[JSON, str]:
|
|
97
|
+
"""GET ``path`` (``/api/v1/...``) and return decoded JSON, or text for ``format=csv``.
|
|
98
|
+
|
|
99
|
+
Raises a subclass of :class:`DraftKingsAPIError` on any non-2xx response.
|
|
100
|
+
"""
|
|
101
|
+
if keyed and not self.api_key:
|
|
102
|
+
raise AuthenticationError(
|
|
103
|
+
f"This endpoint needs an API key. Set DRAFTKINGS_API_KEY or pass api_key=. "
|
|
104
|
+
f"Free key, no card: {SIGN_UP_URL}",
|
|
105
|
+
status=401,
|
|
106
|
+
error_type="missing_api_key",
|
|
107
|
+
)
|
|
108
|
+
query = urllib.parse.urlencode(_clean(params))
|
|
109
|
+
url = f"{self.base_url}{path}" + (f"?{query}" if query else "")
|
|
110
|
+
headers = {"Accept": "application/json", "User-Agent": f"draftkingsapi-python/{__version__}"}
|
|
111
|
+
if keyed and self.api_key:
|
|
112
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
113
|
+
if self.user_request:
|
|
114
|
+
headers["X-User-Request"] = urllib.parse.quote(self.user_request[:500], safe=" ,.:;!?'()-")
|
|
115
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
116
|
+
try:
|
|
117
|
+
res = self._transport(req, self.timeout)
|
|
118
|
+
except (urllib.error.URLError, OSError) as e:
|
|
119
|
+
raise DraftKingsAPIError(f"Could not reach {self.base_url}: {e}") from e
|
|
120
|
+
self.last_headers = dict(res.headers)
|
|
121
|
+
text = res.body.decode("utf-8", errors="replace")
|
|
122
|
+
is_json = "json" in res.headers.get("content-type", "") or text[:1] in "{["
|
|
123
|
+
body: Any = text
|
|
124
|
+
if is_json and text:
|
|
125
|
+
try:
|
|
126
|
+
body = json.loads(text)
|
|
127
|
+
except ValueError:
|
|
128
|
+
body = text
|
|
129
|
+
if res.status >= 400:
|
|
130
|
+
retry = res.headers.get("retry-after")
|
|
131
|
+
raise error_for(res.status, body, int(retry) if retry and retry.isdigit() else None)
|
|
132
|
+
return body
|
|
133
|
+
|
|
134
|
+
def _get(self, path: str, params: Optional[Params] = None, keyed: bool = True) -> Any:
|
|
135
|
+
return self.request(path, params, keyed=keyed)
|
|
136
|
+
|
|
137
|
+
def paginate(
|
|
138
|
+
self,
|
|
139
|
+
method: Callable[..., Any],
|
|
140
|
+
*,
|
|
141
|
+
max_pages: int = 10,
|
|
142
|
+
**params: Any,
|
|
143
|
+
) -> Iterator[JSON]:
|
|
144
|
+
"""Yield rows across pages of any list method, with a page budget so a loop cannot drain your credits.
|
|
145
|
+
|
|
146
|
+
Follows ``pagination.next_cursor`` (sent back as ``cursor``), ``pagination.next`` (stats routes,
|
|
147
|
+
sent back as ``after``) or ``pagination.next_after`` (change feeds, sent back as ``after``).
|
|
148
|
+
|
|
149
|
+
for row in client.paginate(client.pick6_projections, sport="NBA", max_pages=3):
|
|
150
|
+
...
|
|
151
|
+
"""
|
|
152
|
+
params = dict(params)
|
|
153
|
+
params.pop("offset", None) # cursor and offset cannot be combined
|
|
154
|
+
for _ in range(max(1, max_pages)):
|
|
155
|
+
page = method(**params)
|
|
156
|
+
if not isinstance(page, dict):
|
|
157
|
+
raise TypeError("paginate() needs JSON pages; do not pass format='csv'")
|
|
158
|
+
rows = page.get("data")
|
|
159
|
+
if isinstance(rows, list):
|
|
160
|
+
yield from rows
|
|
161
|
+
elif rows is not None:
|
|
162
|
+
yield rows
|
|
163
|
+
pagination = page.get("pagination") or {}
|
|
164
|
+
if not pagination.get("has_more") or not rows:
|
|
165
|
+
return
|
|
166
|
+
if pagination.get("next_cursor"):
|
|
167
|
+
params["cursor"] = pagination["next_cursor"]
|
|
168
|
+
elif pagination.get("next"):
|
|
169
|
+
params["after"] = pagination["next"]
|
|
170
|
+
elif pagination.get("next_after") not in (None, ""):
|
|
171
|
+
params["after"] = pagination["next_after"]
|
|
172
|
+
else:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# -------------------------------------------------------- keyless routes
|
|
176
|
+
|
|
177
|
+
def public_board(self, league: Optional[str] = None, limit: Optional[int] = None) -> JSON:
|
|
178
|
+
"""Delayed, capped Pick6 board plus recent line moves. No key, no credits.
|
|
179
|
+
|
|
180
|
+
league: nfl, nba, nhl, cfb, cbb, soccer, mlb, ufc, cs or lol.
|
|
181
|
+
"""
|
|
182
|
+
return self._get("/api/v1/public/board", {"league": league, "limit": limit}, keyed=False)
|
|
183
|
+
|
|
184
|
+
def public_sportsbook(self, league: Optional[str] = None, limit: Optional[int] = None) -> JSON:
|
|
185
|
+
"""Sportsbook mainlines (moneyline, spread, total). No key, no credits.
|
|
186
|
+
|
|
187
|
+
league: nfl, nba, nhl, cfb, cbb, mlb, epl or mls.
|
|
188
|
+
"""
|
|
189
|
+
return self._get("/api/v1/public/sportsbook", {"league": league, "limit": limit}, keyed=False)
|
|
190
|
+
|
|
191
|
+
def public_line_moves(
|
|
192
|
+
self, league: Optional[str] = None, limit: Optional[int] = None, market: Optional[str] = None
|
|
193
|
+
) -> JSON:
|
|
194
|
+
"""Recent Pick6 and sportsbook line moves. market: all, pick6 or sportsbook. No key."""
|
|
195
|
+
return self._get(
|
|
196
|
+
"/api/v1/public/line-moves", {"league": league, "limit": limit, "market": market}, keyed=False
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def demo(self, dataset: str = "pick6", limit: Optional[int] = None, sport: Optional[str] = None) -> JSON:
|
|
200
|
+
"""Frozen sample rows to learn a response shape. No key. Never use them as data.
|
|
201
|
+
|
|
202
|
+
dataset: one of :data:`DEMO_DATASETS`.
|
|
203
|
+
"""
|
|
204
|
+
if dataset not in DEMO_DATASETS:
|
|
205
|
+
raise ValueError(f"dataset must be one of {', '.join(DEMO_DATASETS)}")
|
|
206
|
+
return self._get(f"/api/v1/demo/{dataset}", {"limit": limit, "sport": sport}, keyed=False)
|
|
207
|
+
|
|
208
|
+
def credit_costs(self, path: Optional[str] = None, rows: Optional[int] = None) -> JSON:
|
|
209
|
+
"""Credits each endpoint costs. No key, no credits."""
|
|
210
|
+
return self._get("/api/v1/credit-costs", {"path": path, "rows": rows}, keyed=False)
|
|
211
|
+
|
|
212
|
+
def directory(self) -> JSON:
|
|
213
|
+
"""Every route, the OpenAPI URL and docs. No key."""
|
|
214
|
+
return self._get("/api/v1", keyed=False)
|
|
215
|
+
|
|
216
|
+
def status(self) -> JSON:
|
|
217
|
+
"""Measured collection status (jobs, workers, dataset freshness). No key."""
|
|
218
|
+
return self._get("/status.json", keyed=False)
|
|
219
|
+
|
|
220
|
+
def history_coverage(self) -> JSON:
|
|
221
|
+
"""Start dates and capture counts of stored history by league. No key."""
|
|
222
|
+
return self._get("/history-coverage.json", keyed=False)
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------- keyed routes
|
|
225
|
+
# Optional filters are passed through as keyword arguments; see https://draftkingsapi.com/docs
|
|
226
|
+
# or https://draftkingsapi.com/openapi.json for each route's parameters.
|
|
227
|
+
|
|
228
|
+
def coverage(self) -> JSON:
|
|
229
|
+
"""Live coverage of every dataset for your key."""
|
|
230
|
+
return self._get("/api/v1/coverage")
|
|
231
|
+
|
|
232
|
+
def events(self, **params: Any) -> Any:
|
|
233
|
+
"""Sportsbook events. Filters: sport, event_id, status, search, start_after, start_before, limit, cursor, format."""
|
|
234
|
+
return self._get("/api/v1/events", params)
|
|
235
|
+
|
|
236
|
+
def odds(self, **params: Any) -> Any:
|
|
237
|
+
"""Current sportsbook odds. Filters: sport, event_id, market, status, view, fresh_only, limit, cursor, format."""
|
|
238
|
+
return self._get("/api/v1/odds", params)
|
|
239
|
+
|
|
240
|
+
def history(self, **params: Any) -> Any:
|
|
241
|
+
"""Stored sportsbook price history. Filters: sport, event_id, offer_id, market, limit, cursor, format."""
|
|
242
|
+
return self._get("/api/v1/history", params)
|
|
243
|
+
|
|
244
|
+
def pick6_projections(self, **params: Any) -> Any:
|
|
245
|
+
"""Current Pick6 lines. Filters: sport, stat, team, search, status, pickable_id, view, limit, cursor, format."""
|
|
246
|
+
return self._get("/api/v1/pick6/projections", params)
|
|
247
|
+
|
|
248
|
+
def pick6_history(self, **params: Any) -> Any:
|
|
249
|
+
"""Pick6 change feed. Filters: sport, stat, pickable_id, after, limit, cursor, format."""
|
|
250
|
+
return self._get("/api/v1/pick6/history", params)
|
|
251
|
+
|
|
252
|
+
def pick6_line_history(self, pickable_id: str, **params: Any) -> JSON:
|
|
253
|
+
"""Recorded moves for one Pick6 prop. Filters: stat, pick_group_id, change_type, limit, cursor."""
|
|
254
|
+
return self._get("/api/v1/pick6/line-history", {"pickable_id": pickable_id, **params})
|
|
255
|
+
|
|
256
|
+
def projections(self, **params: Any) -> Any:
|
|
257
|
+
"""Alias route of :meth:`pick6_projections`."""
|
|
258
|
+
return self._get("/api/v1/projections", params)
|
|
259
|
+
|
|
260
|
+
def changes(self, **params: Any) -> Any:
|
|
261
|
+
"""Every Pick6 line change after your cursor. Filters: after, sport, stat, limit, cursor."""
|
|
262
|
+
return self._get("/api/v1/changes", params)
|
|
263
|
+
|
|
264
|
+
def dfs_sports(self, **params: Any) -> Any:
|
|
265
|
+
"""DFS sports in the lobby."""
|
|
266
|
+
return self._get("/api/v1/dfs/sports", params)
|
|
267
|
+
|
|
268
|
+
def dfs_contests(self, **params: Any) -> Any:
|
|
269
|
+
"""DFS contests. Filters: sport, slate_id, limit, cursor, format."""
|
|
270
|
+
return self._get("/api/v1/dfs/contests", params)
|
|
271
|
+
|
|
272
|
+
def dfs_slates(self, **params: Any) -> Any:
|
|
273
|
+
"""DFS slates. Filters: sport, slate_id, limit, cursor, format."""
|
|
274
|
+
return self._get("/api/v1/dfs/slates", params)
|
|
275
|
+
|
|
276
|
+
def dfs_players(self, **params: Any) -> Any:
|
|
277
|
+
"""DFS players with salaries. Filters: sport, slate_id, team, search, view, limit, cursor, format."""
|
|
278
|
+
return self._get("/api/v1/dfs/players", params)
|
|
279
|
+
|
|
280
|
+
def dfs_changes(self, **params: Any) -> JSON:
|
|
281
|
+
"""DFS lobby changes. Filters: after, sport, kind, change, slate_id, limit."""
|
|
282
|
+
return self._get("/api/v1/dfs/changes", params)
|
|
283
|
+
|
|
284
|
+
def compare(self, **params: Any) -> JSON:
|
|
285
|
+
"""Pick6 lines against DraftKings Sportsbook lines. Filters: sport, stat, player, min_gap, sort, limit, offset."""
|
|
286
|
+
return self._get("/api/v1/compare", params)
|
|
287
|
+
|
|
288
|
+
def compare_books(self, **params: Any) -> JSON:
|
|
289
|
+
"""Lines across books. Filters: league, market_type, player, team, stat, with_fair, min_ev, sort, limit, offset."""
|
|
290
|
+
return self._get("/api/v1/compare/books", params)
|
|
291
|
+
|
|
292
|
+
def ev(self, **params: Any) -> JSON:
|
|
293
|
+
"""Positive expected value prices. Filters: league, market_type, book, player, min_edge, min_prob, sort, limit."""
|
|
294
|
+
return self._get("/api/v1/ev", params)
|
|
295
|
+
|
|
296
|
+
def arbitrage(self, **params: Any) -> JSON:
|
|
297
|
+
"""Arbitrage and middles. Filters: type, league, market_type, book, min_profit, limit, offset."""
|
|
298
|
+
return self._get("/api/v1/arbitrage", params)
|
|
299
|
+
|
|
300
|
+
def stats_games(self, sport: str, season: int, **params: Any) -> JSON:
|
|
301
|
+
"""Game results. sport: NBA, NFL, NHL, NCAAF, NCAAB, MLB or WNBA. Filters: event_id, completed, from, to, limit, after."""
|
|
302
|
+
return self._get("/api/v1/stats/games", {"sport": sport, "season": season, **params})
|
|
303
|
+
|
|
304
|
+
def stats_player_games(self, sport: str, season: int, **params: Any) -> JSON:
|
|
305
|
+
"""Player box scores. Filters: athlete_id, team_id, event_id, completed, from, to, limit, after."""
|
|
306
|
+
return self._get("/api/v1/stats/player-games", {"sport": sport, "season": season, **params})
|
|
307
|
+
|
|
308
|
+
def stats_team_games(self, sport: str, season: int, **params: Any) -> JSON:
|
|
309
|
+
"""Team box scores. Filters: team_id, event_id, completed, from, to, limit, after."""
|
|
310
|
+
return self._get("/api/v1/stats/team-games", {"sport": sport, "season": season, **params})
|
|
311
|
+
|
|
312
|
+
def context_games(self, **params: Any) -> Any:
|
|
313
|
+
"""Game context joined to lines. Filters: sport, event_id, athlete_id, limit, cursor, format."""
|
|
314
|
+
return self._get("/api/v1/context/games", params)
|
|
315
|
+
|
|
316
|
+
def context_player_stats(self, **params: Any) -> Any:
|
|
317
|
+
"""Player stat context. Filters: sport, event_id, athlete_id, limit, cursor, format."""
|
|
318
|
+
return self._get("/api/v1/context/player-stats", params)
|
|
319
|
+
|
|
320
|
+
def context_team_stats(self, **params: Any) -> Any:
|
|
321
|
+
"""Team stat context. Filters: sport, event_id, limit, cursor, format."""
|
|
322
|
+
return self._get("/api/v1/context/team-stats", params)
|
|
323
|
+
|
|
324
|
+
def sportsbook_scopes(self) -> JSON:
|
|
325
|
+
"""Native sportsbook scopes you can snapshot."""
|
|
326
|
+
return self._get("/api/v1/sportsbook/scopes")
|
|
327
|
+
|
|
328
|
+
def sportsbook_snapshot(self, scope: str) -> JSON:
|
|
329
|
+
"""Snapshot of one native sportsbook scope (from :meth:`sportsbook_scopes`)."""
|
|
330
|
+
return self._get("/api/v1/sportsbook/snapshot", {"scope": scope})
|
|
331
|
+
|
|
332
|
+
def stream_snapshot(self, sport: Optional[str] = None) -> JSON:
|
|
333
|
+
"""Current state that the server-sent event stream continues from."""
|
|
334
|
+
return self._get("/api/v1/stream/snapshot", {"sport": sport})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Exceptions raised by the draftkingsapi client.
|
|
2
|
+
|
|
3
|
+
Every API error carries the HTTP status and the API's own error JSON, which looks like
|
|
4
|
+
``{"error": {"code": 401, "type": "missing_api_key", "message": "..."}}``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
SIGN_UP_URL = "https://draftkingsapi.com/sign-up"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DraftKingsAPIError(Exception):
|
|
13
|
+
"""Any non-2xx response, or a transport failure (status 0)."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
status: int = 0,
|
|
19
|
+
error_type: Optional[str] = None,
|
|
20
|
+
body: Optional[Dict[str, Any]] = None,
|
|
21
|
+
retry_after: Optional[int] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.status = status
|
|
26
|
+
self.error_type = error_type
|
|
27
|
+
self.body = body or {}
|
|
28
|
+
self.retry_after = retry_after
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def error(self) -> Dict[str, Any]:
|
|
32
|
+
"""The ``error`` object from the response body, or an empty dict."""
|
|
33
|
+
value = self.body.get("error") if isinstance(self.body, dict) else None
|
|
34
|
+
return value if isinstance(value, dict) else {}
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
prefix = f"HTTP {self.status}" if self.status else "Request failed"
|
|
38
|
+
kind = f" {self.error_type}" if self.error_type else ""
|
|
39
|
+
return f"{prefix}{kind}: {self.message}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvalidRequestError(DraftKingsAPIError):
|
|
43
|
+
"""400: a query parameter was rejected. ``error['query_help']`` often has a corrected example."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AuthenticationError(DraftKingsAPIError):
|
|
47
|
+
"""401: the API key is missing or invalid. Free keys: https://draftkingsapi.com/sign-up"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PaymentRequiredError(DraftKingsAPIError):
|
|
51
|
+
"""402: the request needs a paid plan. ``error['upgrade_url']`` opens checkout."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NotFoundError(DraftKingsAPIError):
|
|
55
|
+
"""404: unknown route or record. ``error['closest_endpoints']`` lists real routes."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RateLimitError(DraftKingsAPIError):
|
|
59
|
+
"""429: a minute, day or month limit was reached. Not charged.
|
|
60
|
+
|
|
61
|
+
``retry_after`` is the Retry-After header in seconds. On a monthly limit, stop retrying:
|
|
62
|
+
``error['limit'] == 'month'``.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DatasetUnavailableError(DraftKingsAPIError):
|
|
67
|
+
"""503: the dataset is temporarily unavailable. Not charged; retry after ``retry_after``."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_BY_STATUS = {
|
|
71
|
+
400: InvalidRequestError,
|
|
72
|
+
401: AuthenticationError,
|
|
73
|
+
402: PaymentRequiredError,
|
|
74
|
+
404: NotFoundError,
|
|
75
|
+
429: RateLimitError,
|
|
76
|
+
503: DatasetUnavailableError,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def error_for(status: int, body: Any, retry_after: Optional[int]) -> DraftKingsAPIError:
|
|
81
|
+
"""Build the matching exception from a status code and decoded body (dict or text)."""
|
|
82
|
+
data: Dict[str, Any] = body if isinstance(body, dict) else {}
|
|
83
|
+
err = data.get("error") if isinstance(data.get("error"), dict) else {}
|
|
84
|
+
message = err.get("message") or (body if isinstance(body, str) and body else f"HTTP {status}")
|
|
85
|
+
if retry_after is None and isinstance(err.get("retry_after_seconds"), int):
|
|
86
|
+
retry_after = err["retry_after_seconds"]
|
|
87
|
+
cls = _BY_STATUS.get(status, DraftKingsAPIError)
|
|
88
|
+
return cls(str(message)[:2000], status=status, error_type=err.get("type"), body=data, retry_after=retry_after)
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: draftkingsapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for draftkingsapi.com: DraftKings Pick6 lines and line history, DFS salaries, sportsbook odds and box scores. Independent, not affiliated with DraftKings.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/draftkingsapi/draftkingsapi-python
|
|
7
|
+
Project-URL: Issues, https://github.com/draftkingsapi/draftkingsapi-python/issues
|
|
8
|
+
Project-URL: Homepage, https://draftkingsapi.com
|
|
9
|
+
Project-URL: Documentation, https://draftkingsapi.com/docs
|
|
10
|
+
Project-URL: OpenAPI, https://draftkingsapi.com/openapi.json
|
|
11
|
+
Project-URL: Status, https://draftkingsapi.com/status
|
|
12
|
+
Keywords: draftkings,pick6,dfs,sportsbook,odds,sports-data,api-client
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# draftkingsapi (Python)
|
|
25
|
+
|
|
26
|
+
Python client for [draftkingsapi.com](https://draftkingsapi.com): DraftKings Pick6 lines and line history,
|
|
27
|
+
DFS salaries, contests and slates, sportsbook odds and price history, and box scores. Standard library only,
|
|
28
|
+
no dependencies, typed.
|
|
29
|
+
|
|
30
|
+
draftkingsapi.com is an independent API. It is not affiliated with or endorsed by DraftKings. "DraftKings" is a
|
|
31
|
+
trademark of its owner and is used here only to describe the data.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install draftkingsapi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## No key needed
|
|
38
|
+
|
|
39
|
+
The keyless feeds are delayed and capped, cost no credits, and need no account:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from draftkingsapi import Client
|
|
43
|
+
|
|
44
|
+
dk = Client() # reads DRAFTKINGS_API_KEY if set; keyless calls never send it
|
|
45
|
+
|
|
46
|
+
board = dk.public_board(league="nba", limit=3)
|
|
47
|
+
for prop in board["props"]:
|
|
48
|
+
print(prop["player"], prop["stat"], prop["line"], prop["more_multiplier"])
|
|
49
|
+
|
|
50
|
+
games = dk.public_sportsbook(league="nfl", limit=2)
|
|
51
|
+
for game in games["games"]:
|
|
52
|
+
print(game["away"], "at", game["home"], [(m["market"], m["side"], m["line"], m["american"]) for m in game["markets"]])
|
|
53
|
+
|
|
54
|
+
moves = dk.public_line_moves(market="pick6", limit=5)
|
|
55
|
+
costs = dk.credit_costs() # credits per endpoint
|
|
56
|
+
status = dk.status() # measured collection status
|
|
57
|
+
sample = dk.demo("odds", limit=1) # frozen sample rows, for the response shape only
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Output of the first loop, run on 15 Nov 2026 (live data, so yours will differ; a prop with no current line prints None):
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Giannis Antetokounmpo Points 32.5 0.8
|
|
64
|
+
Cade Cunningham Points 30.5 1
|
|
65
|
+
Bam Adebayo Points None None
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## With a key
|
|
69
|
+
|
|
70
|
+
Get a free key (100 credits a month, no card) at https://draftkingsapi.com/sign-up, then:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export DRAFTKINGS_API_KEY=dk_...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from draftkingsapi import Client
|
|
78
|
+
|
|
79
|
+
dk = Client(user_request="Build a prop screen from current Pick6 lines")
|
|
80
|
+
|
|
81
|
+
lines = dk.pick6_projections(sport="NBA", limit=100)
|
|
82
|
+
history = dk.pick6_line_history(lines["data"][0]["pickable_id"], change_type="line_move")
|
|
83
|
+
odds = dk.odds(sport="NFL", market="moneyline")
|
|
84
|
+
salaries = dk.dfs_players(sport="NFL", format="csv") # CSV text
|
|
85
|
+
box = dk.stats_player_games("NBA", 2026, limit=50)
|
|
86
|
+
edges = dk.ev(league="nba", min_edge=2)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`user_request` is optional: one plain sentence about the task, sent as `X-User-Request`. Never put personal data in it.
|
|
90
|
+
|
|
91
|
+
### Pagination
|
|
92
|
+
|
|
93
|
+
List routes return `pagination.has_more` with a cursor. `paginate()` follows it with a page budget, so a loop cannot
|
|
94
|
+
spend your whole allowance:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
for row in dk.paginate(dk.odds, sport="NBA", max_pages=5):
|
|
98
|
+
...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It follows `next_cursor` (sent back as `cursor`), `next` on the stats routes and `next_after` on the change feeds
|
|
102
|
+
(both sent back as `after`).
|
|
103
|
+
|
|
104
|
+
### Errors
|
|
105
|
+
|
|
106
|
+
Every non-2xx response raises a subclass of `DraftKingsAPIError` with the API's own message:
|
|
107
|
+
|
|
108
|
+
| Status | Exception | What to do |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 400 | `InvalidRequestError` | `e.error["query_help"]` often has a corrected example |
|
|
111
|
+
| 401 | `AuthenticationError` | missing or wrong key; raised before any request when a keyed method has no key |
|
|
112
|
+
| 402 | `PaymentRequiredError` | `e.error["upgrade_url"]` |
|
|
113
|
+
| 404 | `NotFoundError` | `e.error["closest_endpoints"]` |
|
|
114
|
+
| 429 | `RateLimitError` | wait `e.retry_after` seconds; if `e.error["limit"] == "month"`, stop retrying |
|
|
115
|
+
| 503 | `DatasetUnavailableError` | dataset temporarily unavailable; not charged, retry later |
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from draftkingsapi import Client, RateLimitError
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
Client().odds(sport="NFL")
|
|
122
|
+
except RateLimitError as e:
|
|
123
|
+
print("retry in", e.retry_after, "seconds:", e.message)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Methods
|
|
127
|
+
|
|
128
|
+
Keyless: `public_board`, `public_sportsbook`, `public_line_moves`, `demo(dataset)`, `credit_costs`, `directory`,
|
|
129
|
+
`status`, `history_coverage`.
|
|
130
|
+
|
|
131
|
+
Keyed: `coverage`, `events`, `odds`, `history`, `pick6_projections`, `pick6_history`, `pick6_line_history`,
|
|
132
|
+
`projections`, `changes`, `dfs_sports`, `dfs_contests`, `dfs_slates`, `dfs_players`, `dfs_changes`, `compare`,
|
|
133
|
+
`compare_books`, `ev`, `arbitrage`, `stats_games`, `stats_player_games`, `stats_team_games`, `context_games`,
|
|
134
|
+
`context_player_stats`, `context_team_stats`, `sportsbook_scopes`, `sportsbook_snapshot`, `stream_snapshot`.
|
|
135
|
+
|
|
136
|
+
Optional filters are keyword arguments, passed through unchanged. Every route's parameters are in the
|
|
137
|
+
[OpenAPI document](https://draftkingsapi.com/openapi.json) and the [docs](https://draftkingsapi.com/docs).
|
|
138
|
+
Not wrapped: the server-sent event stream, the native sportsbook WebSocket, and account, billing and sign-up routes.
|
|
139
|
+
|
|
140
|
+
## Plans
|
|
141
|
+
|
|
142
|
+
Free 100 credits a month, Developer $49, Production $149, Scale $499 a month. Credits per endpoint:
|
|
143
|
+
`Client().credit_costs()` or https://draftkingsapi.com/api/v1/credit-costs.
|
|
144
|
+
|
|
145
|
+
## Tests
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python -m unittest discover -s tests # mocked, no network
|
|
149
|
+
DRAFTKINGS_LIVE=1 python -m unittest tests.test_live -v # live keyless endpoints, no key, no credits
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
MIT License.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/draftkingsapi/__init__.py
|
|
5
|
+
src/draftkingsapi/_version.py
|
|
6
|
+
src/draftkingsapi/client.py
|
|
7
|
+
src/draftkingsapi/errors.py
|
|
8
|
+
src/draftkingsapi/py.typed
|
|
9
|
+
src/draftkingsapi.egg-info/PKG-INFO
|
|
10
|
+
src/draftkingsapi.egg-info/SOURCES.txt
|
|
11
|
+
src/draftkingsapi.egg-info/dependency_links.txt
|
|
12
|
+
src/draftkingsapi.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
14
|
+
tests/test_live.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
draftkingsapi
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Mocked tests: no network. Run with `python -m unittest discover -s tests` from packages/python."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
10
|
+
|
|
11
|
+
from draftkingsapi import ( # noqa: E402
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
DatasetUnavailableError,
|
|
14
|
+
DraftKingsAPI,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
Response,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FakeTransport:
|
|
22
|
+
def __init__(self, responses):
|
|
23
|
+
self.responses = list(responses)
|
|
24
|
+
self.requests = []
|
|
25
|
+
|
|
26
|
+
def __call__(self, request, timeout):
|
|
27
|
+
self.requests.append(request)
|
|
28
|
+
status, body, headers = self.responses.pop(0)
|
|
29
|
+
raw = body if isinstance(body, bytes) else json.dumps(body).encode()
|
|
30
|
+
base = {"Content-Type": "application/json"} if not isinstance(body, bytes) else {"Content-Type": "text/csv"}
|
|
31
|
+
base.update(headers or {})
|
|
32
|
+
return Response(status, base, raw)
|
|
33
|
+
|
|
34
|
+
def query(self, i=-1):
|
|
35
|
+
url = urllib.parse.urlparse(self.requests[i].full_url)
|
|
36
|
+
return url.path, dict(urllib.parse.parse_qsl(url.query))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def client(responses, key="dk_test", **kw):
|
|
40
|
+
t = FakeTransport(responses)
|
|
41
|
+
return DraftKingsAPI(api_key=key, base_url="https://example.test", transport=t, **kw), t
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class KeylessTests(unittest.TestCase):
|
|
45
|
+
def test_public_board_sends_no_key(self):
|
|
46
|
+
c, t = client([(200, {"feed": "pick6-board", "props": []}, None)], key="dk_secret")
|
|
47
|
+
out = c.public_board(league="nba", limit=2)
|
|
48
|
+
self.assertEqual(out["feed"], "pick6-board")
|
|
49
|
+
path, q = t.query()
|
|
50
|
+
self.assertEqual(path, "/api/v1/public/board")
|
|
51
|
+
self.assertEqual(q, {"league": "nba", "limit": "2"})
|
|
52
|
+
self.assertIsNone(t.requests[0].get_header("Authorization"))
|
|
53
|
+
|
|
54
|
+
def test_keyless_without_key(self):
|
|
55
|
+
c, t = client([(200, {"data": []}, None)], key=None)
|
|
56
|
+
os.environ.pop("DRAFTKINGS_API_KEY", None)
|
|
57
|
+
c.api_key = None
|
|
58
|
+
c.demo("odds", limit=1)
|
|
59
|
+
self.assertEqual(t.query()[0], "/api/v1/demo/odds")
|
|
60
|
+
|
|
61
|
+
def test_demo_rejects_unknown_dataset(self):
|
|
62
|
+
c, _ = client([])
|
|
63
|
+
with self.assertRaises(ValueError):
|
|
64
|
+
c.demo("nope")
|
|
65
|
+
|
|
66
|
+
def test_status_and_coverage_paths(self):
|
|
67
|
+
c, t = client([(200, {"status": "ok"}, None), (200, {"page": "x"}, None), (200, {"data": []}, None)])
|
|
68
|
+
c.status()
|
|
69
|
+
self.assertEqual(t.query()[0], "/status.json")
|
|
70
|
+
c.history_coverage()
|
|
71
|
+
self.assertEqual(t.query()[0], "/history-coverage.json")
|
|
72
|
+
c.credit_costs(path="/api/v1/odds", rows=500)
|
|
73
|
+
self.assertEqual(t.query(), ("/api/v1/credit-costs", {"path": "/api/v1/odds", "rows": "500"}))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class KeyedTests(unittest.TestCase):
|
|
77
|
+
def test_missing_key_raises_before_any_request(self):
|
|
78
|
+
c, t = client([], key=None)
|
|
79
|
+
c.api_key = None
|
|
80
|
+
with self.assertRaises(AuthenticationError) as ctx:
|
|
81
|
+
c.odds(sport="NFL")
|
|
82
|
+
self.assertIn("draftkingsapi.com/sign-up", str(ctx.exception))
|
|
83
|
+
self.assertEqual(t.requests, [])
|
|
84
|
+
|
|
85
|
+
def test_bearer_and_task_header(self):
|
|
86
|
+
c, t = client([(200, {"data": [], "pagination": {"has_more": False}}, None)], user_request="Build a prop screen")
|
|
87
|
+
c.pick6_projections(sport="NBA", fresh_only=True, view="compact")
|
|
88
|
+
req = t.requests[0]
|
|
89
|
+
self.assertEqual(req.get_header("Authorization"), "Bearer dk_test")
|
|
90
|
+
self.assertEqual(urllib.parse.unquote(req.get_header("X-user-request")), "Build a prop screen")
|
|
91
|
+
self.assertEqual(t.query()[1], {"sport": "NBA", "fresh_only": "true", "view": "compact"})
|
|
92
|
+
|
|
93
|
+
def test_required_args(self):
|
|
94
|
+
c, t = client([(200, {"data": []}, None), (200, {"data": []}, None), (200, {"data": {}}, None)])
|
|
95
|
+
c.pick6_line_history("123", change_type="line_move")
|
|
96
|
+
self.assertEqual(t.query(), ("/api/v1/pick6/line-history", {"pickable_id": "123", "change_type": "line_move"}))
|
|
97
|
+
c.stats_player_games("NBA", 2026, athlete_id="9")
|
|
98
|
+
self.assertEqual(t.query()[1], {"sport": "NBA", "season": "2026", "athlete_id": "9"})
|
|
99
|
+
c.sportsbook_snapshot("nba-props")
|
|
100
|
+
self.assertEqual(t.query(), ("/api/v1/sportsbook/snapshot", {"scope": "nba-props"}))
|
|
101
|
+
|
|
102
|
+
def test_csv_returns_text(self):
|
|
103
|
+
c, _ = client([(200, b"id,line\n1,20.5\n", None)])
|
|
104
|
+
out = c.dfs_players(sport="NFL", format="csv")
|
|
105
|
+
self.assertTrue(out.startswith("id,line"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ErrorTests(unittest.TestCase):
|
|
109
|
+
def test_401_message(self):
|
|
110
|
+
body = {"error": {"code": 401, "type": "invalid_api_key", "message": "Invalid API key."}}
|
|
111
|
+
c, _ = client([(401, body, None)])
|
|
112
|
+
with self.assertRaises(AuthenticationError) as ctx:
|
|
113
|
+
c.odds()
|
|
114
|
+
self.assertEqual(ctx.exception.status, 401)
|
|
115
|
+
self.assertEqual(ctx.exception.error_type, "invalid_api_key")
|
|
116
|
+
self.assertEqual(ctx.exception.message, "Invalid API key.")
|
|
117
|
+
|
|
118
|
+
def test_429_retry_after(self):
|
|
119
|
+
body = {"error": {"code": 429, "type": "minute_rate_limited", "message": "Slow down.", "limit": "minute"}}
|
|
120
|
+
c, _ = client([(429, body, {"Retry-After": "12"})])
|
|
121
|
+
with self.assertRaises(RateLimitError) as ctx:
|
|
122
|
+
c.ev()
|
|
123
|
+
self.assertEqual(ctx.exception.retry_after, 12)
|
|
124
|
+
self.assertEqual(ctx.exception.error["limit"], "minute")
|
|
125
|
+
|
|
126
|
+
def test_503(self):
|
|
127
|
+
body = {"error": {"code": 503, "type": "service_unavailable", "message": "Dataset unavailable."}}
|
|
128
|
+
c, _ = client([(503, body, None)])
|
|
129
|
+
with self.assertRaises(DatasetUnavailableError):
|
|
130
|
+
c.public_board()
|
|
131
|
+
|
|
132
|
+
def test_400_query_help(self):
|
|
133
|
+
body = {"error": {"code": 400, "type": "invalid_query", "message": "Invalid input", "query_help": {"x": 1}}}
|
|
134
|
+
c, _ = client([(400, body, None)])
|
|
135
|
+
with self.assertRaises(InvalidRequestError) as ctx:
|
|
136
|
+
c.odds(limit=99999)
|
|
137
|
+
self.assertEqual(ctx.exception.error["query_help"], {"x": 1})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PaginationTests(unittest.TestCase):
|
|
141
|
+
def test_cursor_pages(self):
|
|
142
|
+
pages = [
|
|
143
|
+
(200, {"data": [1, 2], "pagination": {"has_more": True, "next_cursor": "c2"}}, None),
|
|
144
|
+
(200, {"data": [3], "pagination": {"has_more": False, "next_cursor": None}}, None),
|
|
145
|
+
]
|
|
146
|
+
c, t = client(pages)
|
|
147
|
+
rows = list(c.paginate(c.odds, sport="NFL", offset=50))
|
|
148
|
+
self.assertEqual(rows, [1, 2, 3])
|
|
149
|
+
self.assertNotIn("offset", t.query(0)[1])
|
|
150
|
+
self.assertEqual(t.query(1)[1], {"sport": "NFL", "cursor": "c2"})
|
|
151
|
+
|
|
152
|
+
def test_stats_next(self):
|
|
153
|
+
pages = [
|
|
154
|
+
(200, {"data": ["a"], "pagination": {"has_more": True, "next": "k:1"}}, None),
|
|
155
|
+
(200, {"data": ["b"], "pagination": {"has_more": False, "next": None}}, None),
|
|
156
|
+
]
|
|
157
|
+
c, t = client(pages)
|
|
158
|
+
self.assertEqual(list(c.paginate(c.stats_games, sport="NBA", season=2026)), ["a", "b"])
|
|
159
|
+
self.assertEqual(t.query(1)[1]["after"], "k:1")
|
|
160
|
+
|
|
161
|
+
def test_page_budget(self):
|
|
162
|
+
page = (200, {"data": [0], "pagination": {"has_more": True, "next_cursor": "x"}}, None)
|
|
163
|
+
c, t = client([page] * 5)
|
|
164
|
+
self.assertEqual(len(list(c.paginate(c.odds, max_pages=3))), 3)
|
|
165
|
+
self.assertEqual(len(t.requests), 3)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
unittest.main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Opt-in live test against the keyless endpoints. Spends no credits and needs no key.
|
|
2
|
+
|
|
3
|
+
DRAFTKINGS_LIVE=1 python -m unittest tests.test_live -v
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
11
|
+
|
|
12
|
+
from draftkingsapi import AuthenticationError, DraftKingsAPI # noqa: E402
|
|
13
|
+
|
|
14
|
+
LIVE = os.environ.get("DRAFTKINGS_LIVE") == "1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@unittest.skipUnless(LIVE, "set DRAFTKINGS_LIVE=1 to call the live keyless endpoints")
|
|
18
|
+
class LiveKeyless(unittest.TestCase):
|
|
19
|
+
@classmethod
|
|
20
|
+
def setUpClass(cls):
|
|
21
|
+
cls.c = DraftKingsAPI(api_key="", user_request="Run the SDK live keyless test")
|
|
22
|
+
cls.c.api_key = None
|
|
23
|
+
|
|
24
|
+
def test_public_board(self):
|
|
25
|
+
out = self.c.public_board(league="nba", limit=2)
|
|
26
|
+
self.assertEqual(out["feed"], "pick6-board")
|
|
27
|
+
self.assertTrue(out["keyless"])
|
|
28
|
+
self.assertIsInstance(out["props"], list)
|
|
29
|
+
|
|
30
|
+
def test_public_sportsbook(self):
|
|
31
|
+
out = self.c.public_sportsbook(league="nfl", limit=2)
|
|
32
|
+
self.assertEqual(out["feed"], "sportsbook-mainlines")
|
|
33
|
+
self.assertIsInstance(out["games"], list)
|
|
34
|
+
|
|
35
|
+
def test_line_moves(self):
|
|
36
|
+
out = self.c.public_line_moves(limit=2)
|
|
37
|
+
self.assertEqual(out["feed"], "line-moves")
|
|
38
|
+
|
|
39
|
+
def test_demo_and_costs(self):
|
|
40
|
+
self.assertTrue(self.c.demo("pick6")["demo"]["sample"])
|
|
41
|
+
self.assertIsInstance(self.c.credit_costs()["data"], list)
|
|
42
|
+
self.assertIn("status", self.c.status())
|
|
43
|
+
|
|
44
|
+
def test_keyed_without_key_is_clear(self):
|
|
45
|
+
with self.assertRaises(AuthenticationError):
|
|
46
|
+
self.c.odds()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
unittest.main()
|