prizepicksapi 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stunning Couches LLC
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,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: prizepicksapi
3
+ Version: 0.1.0
4
+ Summary: Python client for prizepicksapi.com: live PrizePicks lines, goblin and demon tiers, line history and graded results.
5
+ Author-email: Stunning Couches LLC <support@prizepicksapi.com>
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/prizepicksapi/prizepicksapi-python
8
+ Project-URL: Issues, https://github.com/prizepicksapi/prizepicksapi-python/issues
9
+ Project-URL: Homepage, https://prizepicksapi.com
10
+ Project-URL: Documentation, https://prizepicksapi.com/docs
11
+ Project-URL: API reference, https://prizepicksapi.com/openapi.json
12
+ Project-URL: Pricing, https://prizepicksapi.com/pricing
13
+ Keywords: prizepicks,player props,dfs,pick'em,sports data,api
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # prizepicksapi (Python)
25
+
26
+ Python client for [prizepicksapi.com](https://prizepicksapi.com): live PrizePicks player-prop lines, goblin and demon tiers on each selection, recorded line history, the change feed, graded results and game logs. Standard library only, Python 3.9+.
27
+
28
+ prizepicksapi.com is an independent service operated by Stunning Couches LLC. It is not affiliated with or endorsed by PrizePicks.
29
+
30
+ ```bash
31
+ pip install prizepicksapi
32
+ ```
33
+
34
+ ## Try it with no key
35
+
36
+ The `demo_*` methods call the keyless demo endpoints. They return fictional rows (`is_demo: true`) with the same fields as the live board, so you can write your parser first.
37
+
38
+ ```python
39
+ from prizepicksapi import PrizePicksAPI
40
+
41
+ api = PrizePicksAPI(api_key="")
42
+ for p in api.demo_projections(league="NBA", limit=3)["data"]:
43
+ print(p["player"]["name"], p["market"]["stat"], p["line"], p["selections"][0]["tier"], "opened", p["line_context"]["opening_line"])
44
+
45
+ for row in api.demo_payouts()["data"][:2]:
46
+ print(row["entry_type"], row["picks"], "break-even", row["derived"]["break_even_probability"])
47
+
48
+ print(api.credit_cost("/api/v1/projections?league=NBA&limit=200")["quoted_credits"])
49
+ ```
50
+
51
+ Output on 15 Nov 2026:
52
+
53
+ ```
54
+ Jordan Ellis Points 27.5 standard opened 26.5
55
+ Marcus Cole Rebounds 8.5 standard opened 7.5
56
+ Devin Brooks Assists 6.5 standard opened 5.5
57
+ power 2 break-even 0.57735
58
+ power 3 break-even 0.55032
59
+ 6
60
+ ```
61
+
62
+ ## Live data
63
+
64
+ Get a free key (100 credits a month, no card) at https://prizepicksapi.com/sign-up and export it:
65
+
66
+ ```bash
67
+ export PRIZEPICKS_API_KEY=...
68
+ ```
69
+
70
+ ```python
71
+ from prizepicksapi import PrizePicksAPI
72
+
73
+ api = PrizePicksAPI(user_request="NBA points model: pull today's board") # optional X-User-Request header
74
+
75
+ # Every current NBA points line, following cursors for you
76
+ for p in api.iter_projections(league="NBA", stat="Points", limit=200):
77
+ tiers = {s["side"]: s["tier"] for s in p["selections"]}
78
+ print(p["player"]["name"], p["line"], tiers)
79
+
80
+ # Only goblin lines, compact rows (up to 1,000 per page)
81
+ goblins = api.projections(league="NBA", tier="goblin", view="compact", limit=1000)
82
+
83
+ # Recorded moves for one projection
84
+ history = api.history(goblins["data"][0]["id"])
85
+
86
+ # Change feed: new, moved, pulled and restored lines. Store meta.next_cursor and resume from it.
87
+ feed = api.changes(league="NBA", limit=200)
88
+
89
+ # Graded results and hit rates
90
+ graded = api.results(league="NBA", date="2026-11-14")
91
+ rates = api.hit_rates(player="Victor Wembanyama", stat="Points")
92
+
93
+ # The board as it stood at a past moment
94
+ past = api.board(as_of="2026-11-10T23:00:00Z", league="NBA")
95
+
96
+ # CSV page for a spreadsheet
97
+ csv_text = api.export_csv(league="NBA", limit=200)
98
+ ```
99
+
100
+ ## Errors and limits
101
+
102
+ Non-2xx responses raise `APIError` with the API's stable `code` (for example `INVALID_API_KEY`). HTTP 429 raises `RateLimitError`. Per-minute limits (`RATE_LIMIT_EXCEEDED`) and 503s are retried up to `max_retries` times using the `Retry-After` header; a monthly quota (`MONTHLY_QUOTA_EXCEEDED`) is raised at once.
103
+
104
+ ```python
105
+ from prizepicksapi import APIError, RateLimitError
106
+
107
+ try:
108
+ api.projections(league="NBA")
109
+ except RateLimitError as e:
110
+ print("limited:", e.code, e.retry_after)
111
+ except APIError as e:
112
+ print(e.status, e.code, e.message)
113
+ ```
114
+
115
+ ## Methods
116
+
117
+ | Method | Endpoint | Key |
118
+ |---|---|---|
119
+ | `demo_projections`, `demo_history`, `demo_changes`, `demo_payouts`, `demo_coverage`, `demo_export_csv` | `/api/v1/demo/*` | no |
120
+ | `credit_cost(path)` | `/api/v1/credit-costs` | no |
121
+ | `projections`, `iter_projections` | `/api/v1/projections` | yes |
122
+ | `history(id)` | `/api/v1/projections/{id}/history` | yes |
123
+ | `trending` | `/api/v1/projections/trending` | yes |
124
+ | `changes` | `/api/v1/changes` | yes |
125
+ | `board(as_of)` | `/api/v1/board` | yes |
126
+ | `results`, `hit_rates`, `game_logs`, `training_set` | `/api/v1/results` etc. | yes |
127
+ | `coverage`, `payouts`, `compare`, `players`, `fixtures` | `/api/v1/*` | yes |
128
+ | `export_csv` | `/api/v1/export` | yes |
129
+ | `request(path, params)`, `paginate(path, params)` | any GET endpoint | |
130
+
131
+ Full reference: https://prizepicksapi.com/docs and https://prizepicksapi.com/openapi.json. Plans: https://prizepicksapi.com/pricing.
132
+
133
+ ## Tests
134
+
135
+ ```bash
136
+ python3 -m unittest discover -s tests # mocked transport
137
+ PRIZEPICKS_LIVE_TEST=1 python3 -m unittest discover -s tests # also calls the keyless demo endpoint
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT.
@@ -0,0 +1,119 @@
1
+ # prizepicksapi (Python)
2
+
3
+ Python client for [prizepicksapi.com](https://prizepicksapi.com): live PrizePicks player-prop lines, goblin and demon tiers on each selection, recorded line history, the change feed, graded results and game logs. Standard library only, Python 3.9+.
4
+
5
+ prizepicksapi.com is an independent service operated by Stunning Couches LLC. It is not affiliated with or endorsed by PrizePicks.
6
+
7
+ ```bash
8
+ pip install prizepicksapi
9
+ ```
10
+
11
+ ## Try it with no key
12
+
13
+ The `demo_*` methods call the keyless demo endpoints. They return fictional rows (`is_demo: true`) with the same fields as the live board, so you can write your parser first.
14
+
15
+ ```python
16
+ from prizepicksapi import PrizePicksAPI
17
+
18
+ api = PrizePicksAPI(api_key="")
19
+ for p in api.demo_projections(league="NBA", limit=3)["data"]:
20
+ print(p["player"]["name"], p["market"]["stat"], p["line"], p["selections"][0]["tier"], "opened", p["line_context"]["opening_line"])
21
+
22
+ for row in api.demo_payouts()["data"][:2]:
23
+ print(row["entry_type"], row["picks"], "break-even", row["derived"]["break_even_probability"])
24
+
25
+ print(api.credit_cost("/api/v1/projections?league=NBA&limit=200")["quoted_credits"])
26
+ ```
27
+
28
+ Output on 15 Nov 2026:
29
+
30
+ ```
31
+ Jordan Ellis Points 27.5 standard opened 26.5
32
+ Marcus Cole Rebounds 8.5 standard opened 7.5
33
+ Devin Brooks Assists 6.5 standard opened 5.5
34
+ power 2 break-even 0.57735
35
+ power 3 break-even 0.55032
36
+ 6
37
+ ```
38
+
39
+ ## Live data
40
+
41
+ Get a free key (100 credits a month, no card) at https://prizepicksapi.com/sign-up and export it:
42
+
43
+ ```bash
44
+ export PRIZEPICKS_API_KEY=...
45
+ ```
46
+
47
+ ```python
48
+ from prizepicksapi import PrizePicksAPI
49
+
50
+ api = PrizePicksAPI(user_request="NBA points model: pull today's board") # optional X-User-Request header
51
+
52
+ # Every current NBA points line, following cursors for you
53
+ for p in api.iter_projections(league="NBA", stat="Points", limit=200):
54
+ tiers = {s["side"]: s["tier"] for s in p["selections"]}
55
+ print(p["player"]["name"], p["line"], tiers)
56
+
57
+ # Only goblin lines, compact rows (up to 1,000 per page)
58
+ goblins = api.projections(league="NBA", tier="goblin", view="compact", limit=1000)
59
+
60
+ # Recorded moves for one projection
61
+ history = api.history(goblins["data"][0]["id"])
62
+
63
+ # Change feed: new, moved, pulled and restored lines. Store meta.next_cursor and resume from it.
64
+ feed = api.changes(league="NBA", limit=200)
65
+
66
+ # Graded results and hit rates
67
+ graded = api.results(league="NBA", date="2026-11-14")
68
+ rates = api.hit_rates(player="Victor Wembanyama", stat="Points")
69
+
70
+ # The board as it stood at a past moment
71
+ past = api.board(as_of="2026-11-10T23:00:00Z", league="NBA")
72
+
73
+ # CSV page for a spreadsheet
74
+ csv_text = api.export_csv(league="NBA", limit=200)
75
+ ```
76
+
77
+ ## Errors and limits
78
+
79
+ Non-2xx responses raise `APIError` with the API's stable `code` (for example `INVALID_API_KEY`). HTTP 429 raises `RateLimitError`. Per-minute limits (`RATE_LIMIT_EXCEEDED`) and 503s are retried up to `max_retries` times using the `Retry-After` header; a monthly quota (`MONTHLY_QUOTA_EXCEEDED`) is raised at once.
80
+
81
+ ```python
82
+ from prizepicksapi import APIError, RateLimitError
83
+
84
+ try:
85
+ api.projections(league="NBA")
86
+ except RateLimitError as e:
87
+ print("limited:", e.code, e.retry_after)
88
+ except APIError as e:
89
+ print(e.status, e.code, e.message)
90
+ ```
91
+
92
+ ## Methods
93
+
94
+ | Method | Endpoint | Key |
95
+ |---|---|---|
96
+ | `demo_projections`, `demo_history`, `demo_changes`, `demo_payouts`, `demo_coverage`, `demo_export_csv` | `/api/v1/demo/*` | no |
97
+ | `credit_cost(path)` | `/api/v1/credit-costs` | no |
98
+ | `projections`, `iter_projections` | `/api/v1/projections` | yes |
99
+ | `history(id)` | `/api/v1/projections/{id}/history` | yes |
100
+ | `trending` | `/api/v1/projections/trending` | yes |
101
+ | `changes` | `/api/v1/changes` | yes |
102
+ | `board(as_of)` | `/api/v1/board` | yes |
103
+ | `results`, `hit_rates`, `game_logs`, `training_set` | `/api/v1/results` etc. | yes |
104
+ | `coverage`, `payouts`, `compare`, `players`, `fixtures` | `/api/v1/*` | yes |
105
+ | `export_csv` | `/api/v1/export` | yes |
106
+ | `request(path, params)`, `paginate(path, params)` | any GET endpoint | |
107
+
108
+ Full reference: https://prizepicksapi.com/docs and https://prizepicksapi.com/openapi.json. Plans: https://prizepicksapi.com/pricing.
109
+
110
+ ## Tests
111
+
112
+ ```bash
113
+ python3 -m unittest discover -s tests # mocked transport
114
+ PRIZEPICKS_LIVE_TEST=1 python3 -m unittest discover -s tests # also calls the keyless demo endpoint
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "prizepicksapi"
7
+ version = "0.1.0"
8
+ description = "Python client for prizepicksapi.com: live PrizePicks lines, goblin and demon tiers, line history and graded results."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Stunning Couches LLC", email = "support@prizepicksapi.com" }]
13
+ keywords = ["prizepicks", "player props", "dfs", "pick'em", "sports data", "api"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/prizepicksapi/prizepicksapi-python"
25
+ Issues = "https://github.com/prizepicksapi/prizepicksapi-python/issues"
26
+ Homepage = "https://prizepicksapi.com"
27
+ Documentation = "https://prizepicksapi.com/docs"
28
+ "API reference" = "https://prizepicksapi.com/openapi.json"
29
+ Pricing = "https://prizepicksapi.com/pricing"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """Python client for prizepicksapi.com: live PrizePicks lines, goblin and demon tiers, line history and graded results.
2
+
3
+ Independent service operated by Stunning Couches LLC; not affiliated with PrizePicks.
4
+ """
5
+
6
+ from .client import DEFAULT_BASE_URL, PrizePicksAPI
7
+ from .errors import APIError, PrizePicksAPIError, RateLimitError, TransportError
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["PrizePicksAPI", "DEFAULT_BASE_URL", "APIError", "PrizePicksAPIError", "RateLimitError", "TransportError", "__version__"]
@@ -0,0 +1,220 @@
1
+ """HTTP client for the prizepicksapi.com REST API (https://prizepicksapi.com/docs).
2
+
3
+ Standard library only. Every method maps to one documented endpoint and returns the decoded JSON body
4
+ (a dict with ``data`` and ``meta``), except ``export_csv`` which returns CSV text.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ from typing import Any, Callable, Dict, Iterator, Mapping, Optional, Tuple
13
+ from urllib.error import HTTPError, URLError
14
+ from urllib.parse import quote, urlencode
15
+ from urllib.request import Request, urlopen
16
+
17
+ from .errors import APIError, RateLimitError, TransportError
18
+
19
+ __all__ = ["PrizePicksAPI", "DEFAULT_BASE_URL"]
20
+
21
+ DEFAULT_BASE_URL = "https://prizepicksapi.com"
22
+ USER_AGENT = "prizepicksapi-python/0.1.0"
23
+
24
+ # A transport takes (method, url, headers, timeout) and returns (status, headers, body_bytes).
25
+ Transport = Callable[[str, str, Dict[str, str], float], Tuple[int, Mapping[str, str], bytes]]
26
+
27
+
28
+ def _urllib_transport(method: str, url: str, headers: Dict[str, str], timeout: float):
29
+ req = Request(url, headers=headers, method=method)
30
+ try:
31
+ with urlopen(req, timeout=timeout) as res:
32
+ return res.status, dict(res.headers.items()), res.read()
33
+ except HTTPError as err:
34
+ return err.code, dict(err.headers.items()) if err.headers else {}, err.read() or b""
35
+ except URLError as err:
36
+ raise TransportError(str(err.reason)) from err
37
+
38
+
39
+ def _clean(params: Mapping[str, Any]) -> Dict[str, str]:
40
+ out: Dict[str, str] = {}
41
+ for key, value in params.items():
42
+ if value is None:
43
+ continue
44
+ if isinstance(value, bool):
45
+ out[key] = "true" if value else "false"
46
+ elif isinstance(value, (list, tuple)):
47
+ out[key] = ",".join(str(v) for v in value)
48
+ else:
49
+ out[key] = str(value)
50
+ return out
51
+
52
+
53
+ class PrizePicksAPI:
54
+ """Client for prizepicksapi.com.
55
+
56
+ >>> api = PrizePicksAPI() # reads PRIZEPICKS_API_KEY from the environment
57
+ >>> api.demo_projections(league="NBA", limit=2)["data"][0]["player"]["name"]
58
+
59
+ ``user_request`` is sent as the optional X-User-Request header: one sentence describing the task,
60
+ with no personal data. It never changes the data or the cost.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ api_key: Optional[str] = None,
66
+ *,
67
+ base_url: str = DEFAULT_BASE_URL,
68
+ user_request: Optional[str] = None,
69
+ timeout: float = 30.0,
70
+ max_retries: int = 2,
71
+ transport: Optional[Transport] = None,
72
+ sleep: Callable[[float], None] = time.sleep,
73
+ ) -> None:
74
+ self.api_key = api_key if api_key is not None else os.environ.get("PRIZEPICKS_API_KEY")
75
+ self.base_url = base_url.rstrip("/")
76
+ self.user_request = user_request
77
+ self.timeout = timeout
78
+ self.max_retries = max_retries
79
+ self._transport = transport or _urllib_transport
80
+ self._sleep = sleep
81
+
82
+ # ------------------------------------------------------------------ core
83
+ def request(self, path: str, params: Optional[Mapping[str, Any]] = None, *, auth: bool = True, raw: bool = False) -> Any:
84
+ """GET ``path`` with query ``params``. Retries 429 per-minute limits and 503s using Retry-After."""
85
+ query = urlencode(_clean(params or {}))
86
+ url = f"{self.base_url}{path}" + (f"?{query}" if query else "")
87
+ headers = {"Accept": "text/csv" if raw else "application/json", "User-Agent": USER_AGENT}
88
+ if auth:
89
+ if not self.api_key:
90
+ raise APIError(401, "INVALID_API_KEY", "Set PRIZEPICKS_API_KEY or pass api_key=. Free keys: https://prizepicksapi.com/sign-up. The demo_* methods need no key.")
91
+ headers["Authorization"] = f"Bearer {self.api_key}"
92
+ if self.user_request:
93
+ headers["X-User-Request"] = quote(self.user_request, safe=" ,.;:!?()'-/")
94
+ attempt = 0
95
+ while True:
96
+ status, resp_headers, body = self._transport("GET", url, headers, self.timeout)
97
+ lower = {k.lower(): v for k, v in resp_headers.items()}
98
+ if 200 <= status < 300 or status == 304:
99
+ if raw:
100
+ return body.decode("utf-8")
101
+ return json.loads(body.decode("utf-8")) if body else None
102
+ error = self._error(status, lower, body)
103
+ retryable = (isinstance(error, RateLimitError) and error.code != "MONTHLY_QUOTA_EXCEEDED") or status == 503
104
+ if retryable and attempt < self.max_retries:
105
+ attempt += 1
106
+ self._sleep(min(error.retry_after or 2 ** attempt, 60))
107
+ continue
108
+ raise error
109
+
110
+ @staticmethod
111
+ def _error(status: int, headers: Mapping[str, str], body: bytes) -> APIError:
112
+ code, message, payload = "HTTP_ERROR", f"HTTP {status}", None
113
+ try:
114
+ payload = json.loads(body.decode("utf-8"))
115
+ err = payload.get("error", {}) if isinstance(payload, dict) else {}
116
+ code = err.get("code", code)
117
+ message = err.get("message", message)
118
+ except (ValueError, UnicodeDecodeError):
119
+ pass
120
+ retry_after = None
121
+ if headers.get("retry-after"):
122
+ try:
123
+ retry_after = float(headers["retry-after"])
124
+ except ValueError:
125
+ retry_after = None
126
+ cls = RateLimitError if status == 429 else APIError
127
+ return cls(status, code, message, retry_after=retry_after, body=payload)
128
+
129
+ def paginate(self, path: str, params: Optional[Mapping[str, Any]] = None, *, auth: bool = True, max_pages: Optional[int] = None) -> Iterator[Dict[str, Any]]:
130
+ """Yield every row across pages by following meta.next_cursor. Keep filters unchanged while paging."""
131
+ params = dict(params or {})
132
+ pages = 0
133
+ while True:
134
+ page = self.request(path, params, auth=auth)
135
+ yield from page.get("data", [])
136
+ pages += 1
137
+ cursor = (page.get("meta") or {}).get("next_cursor")
138
+ if not cursor or (max_pages is not None and pages >= max_pages):
139
+ return
140
+ params["cursor"] = cursor
141
+
142
+ # ------------------------------------------------------------ keyless demo
143
+ def demo_projections(self, **params: Any) -> Dict[str, Any]:
144
+ """Fictional sample rows with the live schema. No key, no quota."""
145
+ return self.request("/api/v1/demo/projections", params, auth=False)
146
+
147
+ def demo_history(self, projection_id: str, **params: Any) -> Dict[str, Any]:
148
+ return self.request(f"/api/v1/demo/projections/{quote(projection_id, safe='')}/history", params, auth=False)
149
+
150
+ def demo_changes(self, **params: Any) -> Dict[str, Any]:
151
+ return self.request("/api/v1/demo/changes", params, auth=False)
152
+
153
+ def demo_payouts(self) -> Dict[str, Any]:
154
+ return self.request("/api/v1/demo/payouts", auth=False)
155
+
156
+ def demo_coverage(self) -> Dict[str, Any]:
157
+ return self.request("/api/v1/demo/coverage", auth=False)
158
+
159
+ def demo_export_csv(self, **params: Any) -> str:
160
+ return self.request("/api/v1/demo/export", params, auth=False, raw=True)
161
+
162
+ def credit_cost(self, path: str) -> Dict[str, Any]:
163
+ """Quote the credit cost of a request path, for example '/api/v1/projections?league=NBA&limit=200'. No key."""
164
+ return self.request("/api/v1/credit-costs", {"path": path}, auth=False)
165
+
166
+ # ------------------------------------------------------------- live (key)
167
+ def projections(self, **params: Any) -> Dict[str, Any]:
168
+ """Current board. Filters: league, search, stat, tier, status, start_after, start_before, limit, cursor, view."""
169
+ return self.request("/api/v1/projections", params)
170
+
171
+ def iter_projections(self, *, max_pages: Optional[int] = None, **params: Any) -> Iterator[Dict[str, Any]]:
172
+ return self.paginate("/api/v1/projections", params, max_pages=max_pages)
173
+
174
+ def history(self, projection_id: str, **params: Any) -> Dict[str, Any]:
175
+ """Every recorded change for one projection."""
176
+ return self.request(f"/api/v1/projections/{quote(projection_id, safe='')}/history", params)
177
+
178
+ def trending(self, **params: Any) -> Dict[str, Any]:
179
+ return self.request("/api/v1/projections/trending", params)
180
+
181
+ def changes(self, **params: Any) -> Dict[str, Any]:
182
+ """Change feed: new, moved, pulled and restored lines. Resume with cursor."""
183
+ return self.request("/api/v1/changes", params)
184
+
185
+ def board(self, as_of: str, **params: Any) -> Dict[str, Any]:
186
+ """The board as it stood at ``as_of`` (ISO 8601)."""
187
+ return self.request("/api/v1/board", {"as_of": as_of, **params})
188
+
189
+ def results(self, **params: Any) -> Dict[str, Any]:
190
+ """Graded lines: line at game time, actual stat and over, under, push or void."""
191
+ return self.request("/api/v1/results", params)
192
+
193
+ def hit_rates(self, **params: Any) -> Dict[str, Any]:
194
+ return self.request("/api/v1/hit-rates", params)
195
+
196
+ def game_logs(self, **params: Any) -> Dict[str, Any]:
197
+ return self.request("/api/v1/game-logs", params)
198
+
199
+ def training_set(self, **params: Any) -> Dict[str, Any]:
200
+ return self.request("/api/v1/training-set", params)
201
+
202
+ def coverage(self) -> Dict[str, Any]:
203
+ return self.request("/api/v1/coverage")
204
+
205
+ def payouts(self) -> Dict[str, Any]:
206
+ return self.request("/api/v1/payouts")
207
+
208
+ def compare(self, **params: Any) -> Dict[str, Any]:
209
+ """PrizePicks lines next to matched sportsbook and exchange quotes (NBA, NFL, NHL where matched)."""
210
+ return self.request("/api/v1/compare", params)
211
+
212
+ def players(self, **params: Any) -> Dict[str, Any]:
213
+ return self.request("/api/v1/players", params)
214
+
215
+ def fixtures(self, **params: Any) -> Dict[str, Any]:
216
+ return self.request("/api/v1/fixtures", params)
217
+
218
+ def export_csv(self, **params: Any) -> str:
219
+ """One CSV page (up to 200 rows) of current projections with the same filters as projections()."""
220
+ return self.request("/api/v1/export", params, raw=True)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class PrizePicksAPIError(Exception):
7
+ """Base class for every error raised by this package."""
8
+
9
+
10
+ class TransportError(PrizePicksAPIError):
11
+ """The request never got an HTTP response (DNS, TLS, timeout)."""
12
+
13
+
14
+ class APIError(PrizePicksAPIError):
15
+ """A non-2xx response. ``code`` is the API's stable error code, for example INVALID_API_KEY."""
16
+
17
+ def __init__(self, status: int, code: str, message: str, *, retry_after: Optional[float] = None, body: Any = None) -> None:
18
+ super().__init__(f"{status} {code}: {message}")
19
+ self.status = status
20
+ self.code = code
21
+ self.message = message
22
+ self.retry_after = retry_after
23
+ self.body = body
24
+
25
+
26
+ class RateLimitError(APIError):
27
+ """HTTP 429. Per-minute limits are retried automatically; a monthly quota is not."""
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: prizepicksapi
3
+ Version: 0.1.0
4
+ Summary: Python client for prizepicksapi.com: live PrizePicks lines, goblin and demon tiers, line history and graded results.
5
+ Author-email: Stunning Couches LLC <support@prizepicksapi.com>
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/prizepicksapi/prizepicksapi-python
8
+ Project-URL: Issues, https://github.com/prizepicksapi/prizepicksapi-python/issues
9
+ Project-URL: Homepage, https://prizepicksapi.com
10
+ Project-URL: Documentation, https://prizepicksapi.com/docs
11
+ Project-URL: API reference, https://prizepicksapi.com/openapi.json
12
+ Project-URL: Pricing, https://prizepicksapi.com/pricing
13
+ Keywords: prizepicks,player props,dfs,pick'em,sports data,api
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # prizepicksapi (Python)
25
+
26
+ Python client for [prizepicksapi.com](https://prizepicksapi.com): live PrizePicks player-prop lines, goblin and demon tiers on each selection, recorded line history, the change feed, graded results and game logs. Standard library only, Python 3.9+.
27
+
28
+ prizepicksapi.com is an independent service operated by Stunning Couches LLC. It is not affiliated with or endorsed by PrizePicks.
29
+
30
+ ```bash
31
+ pip install prizepicksapi
32
+ ```
33
+
34
+ ## Try it with no key
35
+
36
+ The `demo_*` methods call the keyless demo endpoints. They return fictional rows (`is_demo: true`) with the same fields as the live board, so you can write your parser first.
37
+
38
+ ```python
39
+ from prizepicksapi import PrizePicksAPI
40
+
41
+ api = PrizePicksAPI(api_key="")
42
+ for p in api.demo_projections(league="NBA", limit=3)["data"]:
43
+ print(p["player"]["name"], p["market"]["stat"], p["line"], p["selections"][0]["tier"], "opened", p["line_context"]["opening_line"])
44
+
45
+ for row in api.demo_payouts()["data"][:2]:
46
+ print(row["entry_type"], row["picks"], "break-even", row["derived"]["break_even_probability"])
47
+
48
+ print(api.credit_cost("/api/v1/projections?league=NBA&limit=200")["quoted_credits"])
49
+ ```
50
+
51
+ Output on 15 Nov 2026:
52
+
53
+ ```
54
+ Jordan Ellis Points 27.5 standard opened 26.5
55
+ Marcus Cole Rebounds 8.5 standard opened 7.5
56
+ Devin Brooks Assists 6.5 standard opened 5.5
57
+ power 2 break-even 0.57735
58
+ power 3 break-even 0.55032
59
+ 6
60
+ ```
61
+
62
+ ## Live data
63
+
64
+ Get a free key (100 credits a month, no card) at https://prizepicksapi.com/sign-up and export it:
65
+
66
+ ```bash
67
+ export PRIZEPICKS_API_KEY=...
68
+ ```
69
+
70
+ ```python
71
+ from prizepicksapi import PrizePicksAPI
72
+
73
+ api = PrizePicksAPI(user_request="NBA points model: pull today's board") # optional X-User-Request header
74
+
75
+ # Every current NBA points line, following cursors for you
76
+ for p in api.iter_projections(league="NBA", stat="Points", limit=200):
77
+ tiers = {s["side"]: s["tier"] for s in p["selections"]}
78
+ print(p["player"]["name"], p["line"], tiers)
79
+
80
+ # Only goblin lines, compact rows (up to 1,000 per page)
81
+ goblins = api.projections(league="NBA", tier="goblin", view="compact", limit=1000)
82
+
83
+ # Recorded moves for one projection
84
+ history = api.history(goblins["data"][0]["id"])
85
+
86
+ # Change feed: new, moved, pulled and restored lines. Store meta.next_cursor and resume from it.
87
+ feed = api.changes(league="NBA", limit=200)
88
+
89
+ # Graded results and hit rates
90
+ graded = api.results(league="NBA", date="2026-11-14")
91
+ rates = api.hit_rates(player="Victor Wembanyama", stat="Points")
92
+
93
+ # The board as it stood at a past moment
94
+ past = api.board(as_of="2026-11-10T23:00:00Z", league="NBA")
95
+
96
+ # CSV page for a spreadsheet
97
+ csv_text = api.export_csv(league="NBA", limit=200)
98
+ ```
99
+
100
+ ## Errors and limits
101
+
102
+ Non-2xx responses raise `APIError` with the API's stable `code` (for example `INVALID_API_KEY`). HTTP 429 raises `RateLimitError`. Per-minute limits (`RATE_LIMIT_EXCEEDED`) and 503s are retried up to `max_retries` times using the `Retry-After` header; a monthly quota (`MONTHLY_QUOTA_EXCEEDED`) is raised at once.
103
+
104
+ ```python
105
+ from prizepicksapi import APIError, RateLimitError
106
+
107
+ try:
108
+ api.projections(league="NBA")
109
+ except RateLimitError as e:
110
+ print("limited:", e.code, e.retry_after)
111
+ except APIError as e:
112
+ print(e.status, e.code, e.message)
113
+ ```
114
+
115
+ ## Methods
116
+
117
+ | Method | Endpoint | Key |
118
+ |---|---|---|
119
+ | `demo_projections`, `demo_history`, `demo_changes`, `demo_payouts`, `demo_coverage`, `demo_export_csv` | `/api/v1/demo/*` | no |
120
+ | `credit_cost(path)` | `/api/v1/credit-costs` | no |
121
+ | `projections`, `iter_projections` | `/api/v1/projections` | yes |
122
+ | `history(id)` | `/api/v1/projections/{id}/history` | yes |
123
+ | `trending` | `/api/v1/projections/trending` | yes |
124
+ | `changes` | `/api/v1/changes` | yes |
125
+ | `board(as_of)` | `/api/v1/board` | yes |
126
+ | `results`, `hit_rates`, `game_logs`, `training_set` | `/api/v1/results` etc. | yes |
127
+ | `coverage`, `payouts`, `compare`, `players`, `fixtures` | `/api/v1/*` | yes |
128
+ | `export_csv` | `/api/v1/export` | yes |
129
+ | `request(path, params)`, `paginate(path, params)` | any GET endpoint | |
130
+
131
+ Full reference: https://prizepicksapi.com/docs and https://prizepicksapi.com/openapi.json. Plans: https://prizepicksapi.com/pricing.
132
+
133
+ ## Tests
134
+
135
+ ```bash
136
+ python3 -m unittest discover -s tests # mocked transport
137
+ PRIZEPICKS_LIVE_TEST=1 python3 -m unittest discover -s tests # also calls the keyless demo endpoint
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/prizepicksapi/__init__.py
5
+ src/prizepicksapi/client.py
6
+ src/prizepicksapi/errors.py
7
+ src/prizepicksapi.egg-info/PKG-INFO
8
+ src/prizepicksapi.egg-info/SOURCES.txt
9
+ src/prizepicksapi.egg-info/dependency_links.txt
10
+ src/prizepicksapi.egg-info/top_level.txt
11
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ prizepicksapi
@@ -0,0 +1,110 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import unittest
5
+ from urllib.parse import parse_qs, urlparse
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
8
+
9
+ from prizepicksapi import APIError, PrizePicksAPI, RateLimitError # noqa: E402
10
+
11
+
12
+ class FakeTransport:
13
+ def __init__(self, responses):
14
+ self.responses = list(responses)
15
+ self.calls = []
16
+
17
+ def __call__(self, method, url, headers, timeout):
18
+ self.calls.append((method, url, headers))
19
+ status, body, headers_out = self.responses.pop(0)
20
+ raw = body.encode() if isinstance(body, str) else json.dumps(body).encode()
21
+ return status, headers_out, raw
22
+
23
+
24
+ def page(rows, cursor=None):
25
+ return {"data": rows, "meta": {"next_cursor": cursor}}
26
+
27
+
28
+ class ClientTests(unittest.TestCase):
29
+ def test_demo_needs_no_key(self):
30
+ t = FakeTransport([(200, page([{"id": "demo_nba_001"}]), {})])
31
+ api = PrizePicksAPI(api_key="", transport=t)
32
+ out = api.demo_projections(league="NBA", limit=2)
33
+ self.assertEqual(out["data"][0]["id"], "demo_nba_001")
34
+ _, url, headers = t.calls[0]
35
+ self.assertNotIn("Authorization", headers)
36
+ self.assertEqual(urlparse(url).path, "/api/v1/demo/projections")
37
+ self.assertEqual(parse_qs(urlparse(url).query), {"league": ["NBA"], "limit": ["2"]})
38
+
39
+ def test_live_sends_bearer_and_user_request(self):
40
+ t = FakeTransport([(200, page([]), {})])
41
+ api = PrizePicksAPI(api_key="pk_test", user_request="NBA dashboard check", transport=t)
42
+ api.projections(league=["NBA", "NFL"], view="compact", tier=None)
43
+ _, url, headers = t.calls[0]
44
+ self.assertEqual(headers["Authorization"], "Bearer pk_test")
45
+ self.assertEqual(headers["X-User-Request"], "NBA dashboard check")
46
+ self.assertEqual(parse_qs(urlparse(url).query), {"league": ["NBA,NFL"], "view": ["compact"]})
47
+
48
+ def test_missing_key_raises_before_request(self):
49
+ t = FakeTransport([])
50
+ api = PrizePicksAPI(api_key="", transport=t)
51
+ with self.assertRaises(APIError) as ctx:
52
+ api.projections()
53
+ self.assertEqual(ctx.exception.code, "INVALID_API_KEY")
54
+ self.assertEqual(t.calls, [])
55
+
56
+ def test_paginate_follows_cursor_and_keeps_filters(self):
57
+ t = FakeTransport([(200, page([{"id": 1}, {"id": 2}], "c1"), {}), (200, page([{"id": 3}]), {})])
58
+ api = PrizePicksAPI(api_key="k", transport=t)
59
+ rows = list(api.iter_projections(league="NBA", limit=2))
60
+ self.assertEqual([r["id"] for r in rows], [1, 2, 3])
61
+ q2 = parse_qs(urlparse(t.calls[1][1]).query)
62
+ self.assertEqual(q2, {"league": ["NBA"], "limit": ["2"], "cursor": ["c1"]})
63
+
64
+ def test_api_error_code(self):
65
+ body = {"error": {"code": "INVALID_API_KEY", "message": "Provide a valid API key"}}
66
+ t = FakeTransport([(401, body, {})])
67
+ api = PrizePicksAPI(api_key="bad", transport=t)
68
+ with self.assertRaises(APIError) as ctx:
69
+ api.coverage()
70
+ self.assertEqual((ctx.exception.status, ctx.exception.code), (401, "INVALID_API_KEY"))
71
+
72
+ def test_per_minute_429_is_retried_with_retry_after(self):
73
+ slept = []
74
+ limited = {"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "slow down"}}
75
+ t = FakeTransport([(429, limited, {"Retry-After": "7"}), (200, page([]), {})])
76
+ api = PrizePicksAPI(api_key="k", transport=t, sleep=slept.append)
77
+ api.changes(league="NBA")
78
+ self.assertEqual(slept, [7.0])
79
+ self.assertEqual(len(t.calls), 2)
80
+
81
+ def test_monthly_quota_is_not_retried(self):
82
+ quota = {"error": {"code": "MONTHLY_QUOTA_EXCEEDED", "message": "monthly limit"}}
83
+ t = FakeTransport([(429, quota, {"Retry-After": "60"})])
84
+ api = PrizePicksAPI(api_key="k", transport=t, sleep=lambda s: None)
85
+ with self.assertRaises(RateLimitError):
86
+ api.projections()
87
+ self.assertEqual(len(t.calls), 1)
88
+
89
+ def test_history_path_is_encoded(self):
90
+ t = FakeTransport([(200, page([]), {})])
91
+ PrizePicksAPI(api_key="k", transport=t).history("abc/1")
92
+ self.assertTrue(t.calls[0][1].startswith("https://prizepicksapi.com/api/v1/projections/abc%2F1/history"))
93
+
94
+ def test_export_returns_csv_text(self):
95
+ t = FakeTransport([(200, "id,player\n1,A\n", {})])
96
+ csv = PrizePicksAPI(api_key="", transport=t).demo_export_csv(league="NBA", limit=1)
97
+ self.assertTrue(csv.startswith("id,player"))
98
+ self.assertEqual(t.calls[0][2]["Accept"], "text/csv")
99
+
100
+
101
+ @unittest.skipUnless(os.environ.get("PRIZEPICKS_LIVE_TEST"), "set PRIZEPICKS_LIVE_TEST=1 to call the keyless demo endpoint")
102
+ class LiveDemoTest(unittest.TestCase):
103
+ def test_demo_projections_live(self):
104
+ out = PrizePicksAPI(api_key="").demo_projections(league="NBA", limit=2)
105
+ self.assertTrue(out["meta"]["is_demo"])
106
+ self.assertEqual(len(out["data"]), 2)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ unittest.main()