clutchapi 0.1.0__py3-none-any.whl

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.
clutchapi/__init__.py ADDED
@@ -0,0 +1,184 @@
1
+ """Official Python client for the CLUTCH API (https://api.clutchbets.online).
2
+
3
+ Standard library only. Python 3.8+.
4
+
5
+ from clutchapi import Clutch
6
+ clutch = Clutch("ck_live_...") # or set CLUTCH_API_KEY
7
+ nfl = clutch.odds("nfl")
8
+ """
9
+
10
+ import hashlib
11
+ import hmac
12
+ import json
13
+ import os
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+
18
+ __version__ = "0.1.0"
19
+ __all__ = ["Clutch", "ClutchError", "verify_webhook"]
20
+
21
+ DEFAULT_BASE = "https://api.clutchbets.online"
22
+
23
+
24
+ class ClutchError(Exception):
25
+ def __init__(self, message, status=None, body=None):
26
+ super().__init__(message)
27
+ self.status = status
28
+ self.body = body
29
+
30
+
31
+ class _Alerts:
32
+ def __init__(self, client):
33
+ self._c = client
34
+
35
+ def list(self):
36
+ return self._c.get("/api/v1/alerts")["alerts"]
37
+
38
+ def create(self, **rule):
39
+ """channel='webhook'|'email', url=, book=, sport=, player=, market=,
40
+ min_move=1, min_odds_move=, event='line_move'|'sheet_drop', name=.
41
+ Returns {'rule': ..., 'secret': 'whsec_...'} — the secret is shown once."""
42
+ return self._c.request("POST", "/api/v1/alerts", body=rule)
43
+
44
+ def update(self, alert_id, **patch):
45
+ return self._c.request("PATCH", f"/api/v1/alerts/{alert_id}", body=patch)["alert"]
46
+
47
+ def delete(self, alert_id):
48
+ return self._c.request("DELETE", f"/api/v1/alerts/{alert_id}")
49
+
50
+ def test(self, alert_id):
51
+ return self._c.request("POST", f"/api/v1/alerts/{alert_id}/test")
52
+
53
+ def deliveries(self, alert_id):
54
+ return self._c.get(f"/api/v1/alerts/{alert_id}/deliveries")["deliveries"]
55
+
56
+
57
+ class Clutch:
58
+ def __init__(self, api_key=None, base_url=DEFAULT_BASE, timeout=30):
59
+ self.api_key = api_key or os.environ.get("CLUTCH_API_KEY")
60
+ self.base_url = base_url.rstrip("/")
61
+ self.timeout = timeout
62
+ #: rate-limit / quota headers from the most recent response
63
+ self.last_limits = None
64
+ self.alerts = _Alerts(self)
65
+
66
+ def request(self, method, path, params=None, body=None):
67
+ query = {}
68
+ for k, v in (params or {}).items():
69
+ if v is None or v == "":
70
+ continue
71
+ query[k] = ",".join(map(str, v)) if isinstance(v, (list, tuple)) else str(v)
72
+ url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
73
+ headers = {"Accept": "application/json", "User-Agent": f"clutchapi-python/{__version__}"}
74
+ if self.api_key:
75
+ headers["Authorization"] = f"Bearer {self.api_key}"
76
+ data = None
77
+ if body is not None:
78
+ data = json.dumps(body).encode()
79
+ headers["Content-Type"] = "application/json"
80
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
81
+ try:
82
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
83
+ self._limits(res.headers)
84
+ text = res.read().decode()
85
+ except urllib.error.HTTPError as e:
86
+ self._limits(e.headers)
87
+ raw = e.read().decode(errors="replace")
88
+ try:
89
+ parsed = json.loads(raw)
90
+ except ValueError:
91
+ parsed = {"raw": raw}
92
+ raise ClutchError(parsed.get("message") or parsed.get("error") or f"HTTP {e.code}", e.code, parsed) from None
93
+ return json.loads(text) if text else {}
94
+
95
+ def get(self, path, params=None):
96
+ return self.request("GET", path, params=params)
97
+
98
+ def _limits(self, h):
99
+ n = lambda k: int(h[k]) if h and h.get(k) not in (None, "") else None
100
+ self.last_limits = {
101
+ "limit": n("X-RateLimit-Limit"), "remaining": n("X-RateLimit-Remaining"),
102
+ "quota_limit": n("X-Quota-Limit"), "quota_remaining": n("X-Quota-Remaining"),
103
+ }
104
+
105
+ # ── catalog + health ──
106
+ def status(self): return self.get("/api/v1/status")
107
+ def book_status(self, days=90): return self.get("/api/v1/status/books", {"days": days})
108
+ def sports(self): return self.get("/api/v1/sports")
109
+
110
+ # ── odds ──
111
+ def odds(self, sport, **params):
112
+ """Game lines (moneyline / spread / total) for a sport, every book."""
113
+ return self.get(f"/api/v1/{sport}/odds", params)
114
+
115
+ def props(self, sport, **params):
116
+ """Player props for a sport, every book."""
117
+ return self.get(f"/api/v1/{sport}/props", params)
118
+
119
+ def board(self, **params): return self.get("/api/v1/board", params)
120
+ def ev(self, sport, **params): return self.get(f"/api/v1/{sport}/ev", params)
121
+ def arbitrage(self, sport=None, **params):
122
+ return self.get(f"/api/v1/{sport}/arbitrage" if sport else "/api/v1/arbitrage", params)
123
+ def signals(self, sport, **params): return self.get(f"/api/v1/{sport}/signals", params)
124
+
125
+ # ── movement + history ──
126
+ def movements(self, **params): return self.get("/api/v1/movements", params)
127
+ def steam(self, **params): return self.get("/api/v1/movements/steam", params)
128
+ def prop_movements(self, **params): return self.get("/api/v1/props/movements", params)
129
+ def history_games(self, **params): return self.get("/api/v1/history/games", params)
130
+ def history_odds(self, game_key, **params): return self.get("/api/v1/history/odds", {"game_key": game_key, **params})
131
+ def history_props(self, **params): return self.get("/api/v1/history/props", params)
132
+
133
+ def closing(self, **params):
134
+ """The closing line for a game market."""
135
+ return self.get("/api/v1/closing", params)
136
+
137
+ def clv(self, **params):
138
+ """Grade a bet's price against the close (closing-line value)."""
139
+ return self.get("/api/v1/clv", params)
140
+
141
+ # ── stats + context ──
142
+ def injuries(self, sport): return self.get(f"/api/v1/{sport}/injuries")
143
+ def results(self, sport, **params): return self.get(f"/api/v1/{sport}/results", params)
144
+ def averages(self, sport, **params): return self.get(f"/api/v1/{sport}/stats/averages", params)
145
+ def player_log(self, player_id, **params): return self.get(f"/api/v1/player/{player_id}/log", params)
146
+
147
+ # ── prediction markets ──
148
+ def kalshi(self, sport): return self.get(f"/api/v2/kalshi/{sport}/leagues")
149
+ def polymarket(self, sport="all"):
150
+ return self.get("/api/v2/polymarket/all/markets" if sport == "all" else f"/api/v2/polymarket/{sport}/leagues")
151
+
152
+ def stream(self, sport=None):
153
+ """Live odds over Server-Sent Events. Yields (event_type, data) forever."""
154
+ q = "?" + urllib.parse.urlencode({"sport": sport}) if sport else ""
155
+ headers = {"Accept": "text/event-stream"}
156
+ if self.api_key:
157
+ headers["Authorization"] = f"Bearer {self.api_key}"
158
+ req = urllib.request.Request(self.base_url + "/api/v1/stream/odds" + q, headers=headers)
159
+ with urllib.request.urlopen(req) as res:
160
+ kind, data = "message", ""
161
+ for raw in res:
162
+ line = raw.decode().rstrip("\n").rstrip("\r")
163
+ if not line:
164
+ if data:
165
+ try:
166
+ yield kind, json.loads(data)
167
+ except ValueError:
168
+ yield kind, data
169
+ kind, data = "message", ""
170
+ elif line.startswith("event:"):
171
+ kind = line[6:].strip()
172
+ elif line.startswith("data:"):
173
+ data += line[5:].strip()
174
+
175
+
176
+ def verify_webhook(raw_body, signature_header, secret):
177
+ """Check a CLUTCH webhook. Pass the RAW body (bytes or str), the
178
+ X-Clutch-Signature header, and the whsec_... secret from the dashboard."""
179
+ if not signature_header or not secret:
180
+ return False
181
+ if isinstance(raw_body, str):
182
+ raw_body = raw_body.encode()
183
+ expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
184
+ return hmac.compare_digest(expected, str(signature_header))
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: clutchapi
3
+ Version: 0.1.0
4
+ Summary: Official client for the CLUTCH sports odds API: live odds and player props from 30+ books, line movement, closing lines, CLV, arbitrage and webhooks.
5
+ Author: Cooper Tech Solutions LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://api.clutchbets.online
8
+ Project-URL: Documentation, https://api.clutchbets.online/docs
9
+ Keywords: sports betting,odds api,player props,sportsbook,nfl odds,nba odds,line movement,closing line value,arbitrage
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # clutchapi
17
+
18
+ The official Python client for the [CLUTCH API](https://api.clutchbets.online): live odds and player props from 30+ sportsbooks, line movement, closing lines, CLV grading, arbitrage and webhooks. Standard library only.
19
+
20
+ ```bash
21
+ pip install clutchapi
22
+ ```
23
+
24
+ ```python
25
+ from clutchapi import Clutch
26
+
27
+ clutch = Clutch("ck_live_...") # or set CLUTCH_API_KEY
28
+
29
+ nfl = clutch.odds("nfl")
30
+ props = clutch.props("nba", market="points")
31
+ steam = clutch.steam(sport="nfl")
32
+ close = clutch.closing(sport="nfl", game_key="...", market="spread")
33
+ ```
34
+
35
+ Get a key at [api.clutchbets.online/console](https://api.clutchbets.online/console).
36
+
37
+ ## Live stream
38
+
39
+ ```python
40
+ for event, data in clutch.stream(sport="nba"):
41
+ print(event, data)
42
+ ```
43
+
44
+ ## Line-move webhooks
45
+
46
+ ```python
47
+ out = clutch.alerts.create(channel="webhook", url="https://your.app/hooks/clutch",
48
+ book="pinnacle", sport="nfl", min_move=1)
49
+ secret = out["secret"] # shown once, save it
50
+ ```
51
+
52
+ Verify each delivery (Flask example, raw body):
53
+
54
+ ```python
55
+ from clutchapi import verify_webhook
56
+
57
+ @app.post("/hooks/clutch")
58
+ def hook():
59
+ if not verify_webhook(request.get_data(), request.headers.get("X-Clutch-Signature"), SECRET):
60
+ return "", 401
61
+ moves = request.get_json()["moves"]
62
+ return "", 200
63
+ ```
64
+
65
+ ## Errors and limits
66
+
67
+ Failed requests raise `ClutchError` with `.status` and `.body`. After every call, `clutch.last_limits` holds your rate-limit and monthly-quota headers.
68
+
69
+ Any endpoint is reachable directly with `clutch.get("/api/v1/...", {...})`. Full reference: [api.clutchbets.online/docs](https://api.clutchbets.online/docs).
70
+
71
+ MIT © Cooper Tech Solutions LLC
@@ -0,0 +1,5 @@
1
+ clutchapi/__init__.py,sha256=K4v6U2tWYXBHGF9vF-mgfe1jLl4tNjKvjuBxEmcJ2Yc,7900
2
+ clutchapi-0.1.0.dist-info/METADATA,sha256=jCjZ1ZVvDWccb6x8h4vWN2P3OCzc99yXPEtw8qg0xss,2431
3
+ clutchapi-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
4
+ clutchapi-0.1.0.dist-info/top_level.txt,sha256=IFNYX_334-30iFUBnGlmTjw0q9g5dCGEd5JTlwkKW68,10
5
+ clutchapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ clutchapi