lumify-sdk 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.
- lumify_sdk-0.1.0/.gitignore +15 -0
- lumify_sdk-0.1.0/LICENSE +21 -0
- lumify_sdk-0.1.0/PKG-INFO +247 -0
- lumify_sdk-0.1.0/README.md +218 -0
- lumify_sdk-0.1.0/lumify/__init__.py +65 -0
- lumify_sdk-0.1.0/lumify/_transport.py +286 -0
- lumify_sdk-0.1.0/lumify/client.py +58 -0
- lumify_sdk-0.1.0/lumify/errors.py +169 -0
- lumify_sdk-0.1.0/lumify/meta.py +135 -0
- lumify_sdk-0.1.0/lumify/models.py +412 -0
- lumify_sdk-0.1.0/lumify/pagination.py +52 -0
- lumify_sdk-0.1.0/lumify/py.typed +0 -0
- lumify_sdk-0.1.0/lumify/resources/__init__.py +22 -0
- lumify_sdk-0.1.0/lumify/resources/agent.py +69 -0
- lumify_sdk-0.1.0/lumify/resources/events.py +173 -0
- lumify_sdk-0.1.0/lumify/resources/players.py +94 -0
- lumify_sdk-0.1.0/lumify/resources/sports.py +32 -0
- lumify_sdk-0.1.0/lumify/resources/teams.py +64 -0
- lumify_sdk-0.1.0/lumify/resources/webhooks.py +64 -0
- lumify_sdk-0.1.0/lumify/sse.py +167 -0
- lumify_sdk-0.1.0/lumify/webhook_signature.py +81 -0
- lumify_sdk-0.1.0/pyproject.toml +57 -0
- lumify_sdk-0.1.0/scripts/gen_models.py +209 -0
- lumify_sdk-0.1.0/tests/__init__.py +0 -0
- lumify_sdk-0.1.0/tests/_helpers.py +77 -0
- lumify_sdk-0.1.0/tests/conftest.py +6 -0
- lumify_sdk-0.1.0/tests/test_client.py +126 -0
- lumify_sdk-0.1.0/tests/test_contract.py +105 -0
- lumify_sdk-0.1.0/tests/test_errors.py +68 -0
- lumify_sdk-0.1.0/tests/test_meta.py +50 -0
- lumify_sdk-0.1.0/tests/test_pagination.py +59 -0
- lumify_sdk-0.1.0/tests/test_resources.py +128 -0
- lumify_sdk-0.1.0/tests/test_sse.py +84 -0
- lumify_sdk-0.1.0/tests/test_webhook_signature.py +52 -0
lumify_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lumify AI
|
|
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,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lumify-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Lumify agent-ready sports intelligence API (schedules, live scores, odds, betting splits, and AI bet intelligence).
|
|
5
|
+
Project-URL: Homepage, https://lumify.ai/docs
|
|
6
|
+
Project-URL: Documentation, https://lumify.ai/docs/reference
|
|
7
|
+
Project-URL: Repository, https://github.com/lumifyai/lumify
|
|
8
|
+
Project-URL: Issues, https://github.com/lumifyai/lumify/issues
|
|
9
|
+
Author-email: Lumify AI <dev@lumify.ai>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,betting,lumify,odds,sdk,sports,sports-api,sports-data
|
|
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: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# lumify-sdk
|
|
31
|
+
|
|
32
|
+
Official Python client for [Lumify](https://lumify.ai), the agent-ready sports
|
|
33
|
+
intelligence API — schedules, live scores, odds, line movement, public betting
|
|
34
|
+
splits, and AI bet intelligence across MLB, NFL, NCAAF, NBA, NHL, tennis, and
|
|
35
|
+
soccer.
|
|
36
|
+
|
|
37
|
+
> **Status:** v0.1.0 — the data-plane slice (sports/seasons/events/teams/players,
|
|
38
|
+
> SSE streaming, webhooks, agent onboarding). Same data as the REST API, the
|
|
39
|
+
> [TypeScript SDK](https://www.npmjs.com/package/@lumifyai/sdk), and the
|
|
40
|
+
> [MCP server](https://www.npmjs.com/package/@lumifyai/mcp) — this SDK *is* the
|
|
41
|
+
> typed REST path, not a third implementation. Synchronous today; an async
|
|
42
|
+
> client (`AsyncLumify`) is the planned 0.2.0 follow-up.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install lumify-sdk
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Python 3.8+. **Zero runtime dependencies** — the transport is stdlib
|
|
51
|
+
`urllib` and signatures use stdlib `hmac`/`hashlib`.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import os
|
|
57
|
+
from lumify import Lumify
|
|
58
|
+
|
|
59
|
+
client = Lumify(api_key=os.environ["LUMIFY_API_KEY"])
|
|
60
|
+
|
|
61
|
+
sports = client.sports.list()
|
|
62
|
+
|
|
63
|
+
event = client.events.get(12345, include_odds=True, include_intelligence=True)
|
|
64
|
+
print(event["status"], event.get("intelligence"))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Create a key at <https://lumify.ai/api-keys> or programmatically via
|
|
68
|
+
`client.agent.keys.create()`.
|
|
69
|
+
|
|
70
|
+
## Resources
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
client.sports.list() # client.seasons.list()
|
|
74
|
+
client.events.list(**filters) # .get(id, include_odds=?, include_intelligence=?, bookmaker=?)
|
|
75
|
+
client.events.batch_get(event_ids) # up to 25 ids in one round-trip; see below
|
|
76
|
+
client.events.query(text, limit=None) # natural-language search, e.g. "live nfl games today"
|
|
77
|
+
client.events.odds(id) # .odds_history(id) / .score(id) / .intelligence(id) / .splits(id)
|
|
78
|
+
client.events.stream(id) # SSE iterator of live score updates
|
|
79
|
+
client.events.paginate(**filters) # .iterate(**filters) — cursor pagination helpers
|
|
80
|
+
client.teams.list(**filters) # .get(id)
|
|
81
|
+
client.players.list(**filters) # .get(id) / .events(id, **filters)
|
|
82
|
+
client.webhooks.create(url=...) # .list() / .delete(id) / .verify(secret, header, raw_body)
|
|
83
|
+
client.agent.keys.create() # .list() / .revoke(id)
|
|
84
|
+
client.agent.credits.get() # .list_packs() / .topup(pack_id)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every method maps 1:1 to a REST endpoint and returns the same JSON shape you'd
|
|
88
|
+
get from `curl` (a plain `dict`, typed as the matching model in
|
|
89
|
+
`lumify.models`) — see <https://lumify.ai/docs/reference> for full field docs.
|
|
90
|
+
|
|
91
|
+
## Pagination
|
|
92
|
+
|
|
93
|
+
List endpoints are cursor-paginated (`after_id`/`limit`, max 100). Use the
|
|
94
|
+
iterator helpers instead of tracking cursors by hand:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
for event in client.events.iterate(sport="nfl", status="scheduled"):
|
|
98
|
+
print(event["id"], event["starts_at"])
|
|
99
|
+
|
|
100
|
+
# Or page-by-page (events pages use "events", not "data"):
|
|
101
|
+
for page in client.events.paginate(sport="nfl", limit=50):
|
|
102
|
+
print(len(page.get("events", [])), "events, next cursor:", page.get("next_after_id"))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Standalone helpers `paginate()` / `iterate_items()` are also exported for
|
|
106
|
+
custom fetchers.
|
|
107
|
+
|
|
108
|
+
## Batch event lookup
|
|
109
|
+
|
|
110
|
+
Already have a list of event ids (e.g. from `client.events.list()`) and want
|
|
111
|
+
full detail for each? `batch_get` fetches up to 25 in a single round-trip
|
|
112
|
+
instead of one `GET` per event:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = client.events.batch_get([101, 102, 999999999], include_odds=True)
|
|
116
|
+
print(result["total"], "found;", result["not_found"], "missing")
|
|
117
|
+
for event in result["events"]:
|
|
118
|
+
print(event["id"], event["status"])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Duplicate ids are billed once; ids that don't exist are returned under
|
|
122
|
+
`not_found` and cost nothing — credits are the sum of each event's normal
|
|
123
|
+
`GET /v1/events/{id}` cost, with the same billing-fairness rules (unavailable
|
|
124
|
+
odds/intelligence stay free).
|
|
125
|
+
|
|
126
|
+
## Natural-language search
|
|
127
|
+
|
|
128
|
+
`query` maps free text to the same filters `client.events.list()` accepts —
|
|
129
|
+
sport, status, and date/date-range — using a small, deterministic, rule-based
|
|
130
|
+
mapper (not an LLM call). Costs 1 credit, same as `list()`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
result = client.events.query("live nfl games today", limit=5)
|
|
134
|
+
print(result["interpreted"]) # {"sport": "nfl", "status": "inprogress", "date": "2026-07-15", ...}
|
|
135
|
+
print(result["equivalent_request"]) # "GET /v1/events?sport=nfl&status=inprogress&date=2026-07-15"
|
|
136
|
+
print(result["unrecognized_terms"]) # words that didn't map to a filter
|
|
137
|
+
for event in result["events"]:
|
|
138
|
+
print(event["id"], event["status"])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Live score streaming (SSE)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
for evt in client.events.stream(event_id):
|
|
145
|
+
if evt.event == "score":
|
|
146
|
+
print(evt.data["status"], evt.data.get("clock"))
|
|
147
|
+
if evt.event == "done":
|
|
148
|
+
break
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Cheaper than polling `client.events.score(id)` — the server only emits on
|
|
152
|
+
change, plus periodic keep-alives, and closes when the event finishes.
|
|
153
|
+
|
|
154
|
+
## Webhooks
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
sub = client.webhooks.create(url="https://you.example.com/hooks/lumify")
|
|
158
|
+
# sub["signing_secret"] ("whsec_...") is returned once — store it.
|
|
159
|
+
|
|
160
|
+
# In your webhook handler, verify against the *raw* request body:
|
|
161
|
+
client.webhooks.verify(signing_secret, request.headers["Lumify-Signature"], raw_body)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`verify()` raises `WebhookSignatureError` (bad format, signature mismatch, or a
|
|
165
|
+
stale/replayed timestamp) — treat that as "reject with 4xx", not a crash.
|
|
166
|
+
|
|
167
|
+
## Errors
|
|
168
|
+
|
|
169
|
+
Every non-2xx response raises a typed subclass of `LumifyError` — switch on
|
|
170
|
+
`err.code` (the stable machine-readable slug), not `str(err)`:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from lumify import NotFoundError, RateLimitError, ValidationError
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
client.events.get(999999999)
|
|
177
|
+
except NotFoundError:
|
|
178
|
+
...
|
|
179
|
+
except RateLimitError as err:
|
|
180
|
+
print("retry after", err.retry_after, "s")
|
|
181
|
+
except ValidationError as err:
|
|
182
|
+
print(err.field_errors)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`AuthenticationError` (401), `PaymentError` (402), `PermissionError` (403, has
|
|
186
|
+
`.upgrade_url` on sport-scope denials), `NotFoundError` (404), `ValidationError`
|
|
187
|
+
(422, has `.field_errors`), `RateLimitError` (429, has `.retry_after` from the
|
|
188
|
+
envelope or `Retry-After` header), `APIError` (5xx), and `ConnectionError`
|
|
189
|
+
(network/timeout, never reached the server) all extend `LumifyError`
|
|
190
|
+
(`.code`, `.status`, `.doc_url`, `.request_id`).
|
|
191
|
+
|
|
192
|
+
GET requests are automatically retried (default: 2 attempts) with exponential
|
|
193
|
+
backoff on `429`/`5xx`/network failures, honoring `Retry-After`. Non-idempotent
|
|
194
|
+
requests (`POST`/webhook & key creation) are never auto-retried. Configure with
|
|
195
|
+
`max_retries` / `timeout` on the `Lumify` constructor.
|
|
196
|
+
|
|
197
|
+
## Credits and rate limits
|
|
198
|
+
|
|
199
|
+
Every successful response carries `X-Credits-Used`, `X-Credits-Remaining`, and
|
|
200
|
+
`X-RateLimit-*` headers. Read them via `get_meta()` (attached out-of-band on the
|
|
201
|
+
returned object, so it never pollutes `json.dumps` or iteration):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from lumify import get_meta
|
|
205
|
+
|
|
206
|
+
odds = client.events.odds(event_id)
|
|
207
|
+
meta = get_meta(odds)
|
|
208
|
+
print(meta.credits_used, meta.rate_limit_remaining)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Queries for data that isn't available yet (e.g. odds not yet posted) return
|
|
212
|
+
`credits_used == 0` — you're never charged for a "not available" read.
|
|
213
|
+
|
|
214
|
+
## Sync with REST, MCP, and the OpenAPI contract
|
|
215
|
+
|
|
216
|
+
This SDK's response types (`lumify/models.py`) are generated from the *same*
|
|
217
|
+
filtered OpenAPI slice the TypeScript SDK uses
|
|
218
|
+
(`clients/lumify-sdk/openapi/openapi.sdk.json`, produced by
|
|
219
|
+
`scripts/export_openapi_sdk.py` at the repo root), not hand-maintained. CI fails
|
|
220
|
+
if the models are stale, so the two SDKs can't disagree with each other or drift
|
|
221
|
+
from what the API returns. A shared cross-surface probe matrix
|
|
222
|
+
(`tests/fixtures/agent_contract.json`) additionally asserts this SDK builds the
|
|
223
|
+
exact REST request that REST and MCP agree on. The client ergonomics (this
|
|
224
|
+
README's resource shape, pagination/SSE/webhook helpers) are hand-written.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
python scripts/export_openapi_sdk.py # regenerate the schema slice
|
|
228
|
+
python clients/lumify-sdk-python/scripts/gen_models.py # regenerate the Python models
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
python -m venv .venv && . .venv/bin/activate
|
|
235
|
+
pip install -e ".[dev]"
|
|
236
|
+
pytest
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
[MIT](./LICENSE) © 2026 Lumify AI
|
|
242
|
+
|
|
243
|
+
## Related
|
|
244
|
+
|
|
245
|
+
- Docs: <https://lumify.ai/docs>
|
|
246
|
+
- TypeScript SDK: [`@lumifyai/sdk`](https://www.npmjs.com/package/@lumifyai/sdk)
|
|
247
|
+
- MCP server (for AI agent runtimes): [`@lumifyai/mcp`](https://www.npmjs.com/package/@lumifyai/mcp)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# lumify-sdk
|
|
2
|
+
|
|
3
|
+
Official Python client for [Lumify](https://lumify.ai), the agent-ready sports
|
|
4
|
+
intelligence API — schedules, live scores, odds, line movement, public betting
|
|
5
|
+
splits, and AI bet intelligence across MLB, NFL, NCAAF, NBA, NHL, tennis, and
|
|
6
|
+
soccer.
|
|
7
|
+
|
|
8
|
+
> **Status:** v0.1.0 — the data-plane slice (sports/seasons/events/teams/players,
|
|
9
|
+
> SSE streaming, webhooks, agent onboarding). Same data as the REST API, the
|
|
10
|
+
> [TypeScript SDK](https://www.npmjs.com/package/@lumifyai/sdk), and the
|
|
11
|
+
> [MCP server](https://www.npmjs.com/package/@lumifyai/mcp) — this SDK *is* the
|
|
12
|
+
> typed REST path, not a third implementation. Synchronous today; an async
|
|
13
|
+
> client (`AsyncLumify`) is the planned 0.2.0 follow-up.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install lumify-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.8+. **Zero runtime dependencies** — the transport is stdlib
|
|
22
|
+
`urllib` and signatures use stdlib `hmac`/`hashlib`.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import os
|
|
28
|
+
from lumify import Lumify
|
|
29
|
+
|
|
30
|
+
client = Lumify(api_key=os.environ["LUMIFY_API_KEY"])
|
|
31
|
+
|
|
32
|
+
sports = client.sports.list()
|
|
33
|
+
|
|
34
|
+
event = client.events.get(12345, include_odds=True, include_intelligence=True)
|
|
35
|
+
print(event["status"], event.get("intelligence"))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create a key at <https://lumify.ai/api-keys> or programmatically via
|
|
39
|
+
`client.agent.keys.create()`.
|
|
40
|
+
|
|
41
|
+
## Resources
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
client.sports.list() # client.seasons.list()
|
|
45
|
+
client.events.list(**filters) # .get(id, include_odds=?, include_intelligence=?, bookmaker=?)
|
|
46
|
+
client.events.batch_get(event_ids) # up to 25 ids in one round-trip; see below
|
|
47
|
+
client.events.query(text, limit=None) # natural-language search, e.g. "live nfl games today"
|
|
48
|
+
client.events.odds(id) # .odds_history(id) / .score(id) / .intelligence(id) / .splits(id)
|
|
49
|
+
client.events.stream(id) # SSE iterator of live score updates
|
|
50
|
+
client.events.paginate(**filters) # .iterate(**filters) — cursor pagination helpers
|
|
51
|
+
client.teams.list(**filters) # .get(id)
|
|
52
|
+
client.players.list(**filters) # .get(id) / .events(id, **filters)
|
|
53
|
+
client.webhooks.create(url=...) # .list() / .delete(id) / .verify(secret, header, raw_body)
|
|
54
|
+
client.agent.keys.create() # .list() / .revoke(id)
|
|
55
|
+
client.agent.credits.get() # .list_packs() / .topup(pack_id)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Every method maps 1:1 to a REST endpoint and returns the same JSON shape you'd
|
|
59
|
+
get from `curl` (a plain `dict`, typed as the matching model in
|
|
60
|
+
`lumify.models`) — see <https://lumify.ai/docs/reference> for full field docs.
|
|
61
|
+
|
|
62
|
+
## Pagination
|
|
63
|
+
|
|
64
|
+
List endpoints are cursor-paginated (`after_id`/`limit`, max 100). Use the
|
|
65
|
+
iterator helpers instead of tracking cursors by hand:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
for event in client.events.iterate(sport="nfl", status="scheduled"):
|
|
69
|
+
print(event["id"], event["starts_at"])
|
|
70
|
+
|
|
71
|
+
# Or page-by-page (events pages use "events", not "data"):
|
|
72
|
+
for page in client.events.paginate(sport="nfl", limit=50):
|
|
73
|
+
print(len(page.get("events", [])), "events, next cursor:", page.get("next_after_id"))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Standalone helpers `paginate()` / `iterate_items()` are also exported for
|
|
77
|
+
custom fetchers.
|
|
78
|
+
|
|
79
|
+
## Batch event lookup
|
|
80
|
+
|
|
81
|
+
Already have a list of event ids (e.g. from `client.events.list()`) and want
|
|
82
|
+
full detail for each? `batch_get` fetches up to 25 in a single round-trip
|
|
83
|
+
instead of one `GET` per event:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
result = client.events.batch_get([101, 102, 999999999], include_odds=True)
|
|
87
|
+
print(result["total"], "found;", result["not_found"], "missing")
|
|
88
|
+
for event in result["events"]:
|
|
89
|
+
print(event["id"], event["status"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Duplicate ids are billed once; ids that don't exist are returned under
|
|
93
|
+
`not_found` and cost nothing — credits are the sum of each event's normal
|
|
94
|
+
`GET /v1/events/{id}` cost, with the same billing-fairness rules (unavailable
|
|
95
|
+
odds/intelligence stay free).
|
|
96
|
+
|
|
97
|
+
## Natural-language search
|
|
98
|
+
|
|
99
|
+
`query` maps free text to the same filters `client.events.list()` accepts —
|
|
100
|
+
sport, status, and date/date-range — using a small, deterministic, rule-based
|
|
101
|
+
mapper (not an LLM call). Costs 1 credit, same as `list()`:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
result = client.events.query("live nfl games today", limit=5)
|
|
105
|
+
print(result["interpreted"]) # {"sport": "nfl", "status": "inprogress", "date": "2026-07-15", ...}
|
|
106
|
+
print(result["equivalent_request"]) # "GET /v1/events?sport=nfl&status=inprogress&date=2026-07-15"
|
|
107
|
+
print(result["unrecognized_terms"]) # words that didn't map to a filter
|
|
108
|
+
for event in result["events"]:
|
|
109
|
+
print(event["id"], event["status"])
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Live score streaming (SSE)
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
for evt in client.events.stream(event_id):
|
|
116
|
+
if evt.event == "score":
|
|
117
|
+
print(evt.data["status"], evt.data.get("clock"))
|
|
118
|
+
if evt.event == "done":
|
|
119
|
+
break
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Cheaper than polling `client.events.score(id)` — the server only emits on
|
|
123
|
+
change, plus periodic keep-alives, and closes when the event finishes.
|
|
124
|
+
|
|
125
|
+
## Webhooks
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
sub = client.webhooks.create(url="https://you.example.com/hooks/lumify")
|
|
129
|
+
# sub["signing_secret"] ("whsec_...") is returned once — store it.
|
|
130
|
+
|
|
131
|
+
# In your webhook handler, verify against the *raw* request body:
|
|
132
|
+
client.webhooks.verify(signing_secret, request.headers["Lumify-Signature"], raw_body)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`verify()` raises `WebhookSignatureError` (bad format, signature mismatch, or a
|
|
136
|
+
stale/replayed timestamp) — treat that as "reject with 4xx", not a crash.
|
|
137
|
+
|
|
138
|
+
## Errors
|
|
139
|
+
|
|
140
|
+
Every non-2xx response raises a typed subclass of `LumifyError` — switch on
|
|
141
|
+
`err.code` (the stable machine-readable slug), not `str(err)`:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from lumify import NotFoundError, RateLimitError, ValidationError
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
client.events.get(999999999)
|
|
148
|
+
except NotFoundError:
|
|
149
|
+
...
|
|
150
|
+
except RateLimitError as err:
|
|
151
|
+
print("retry after", err.retry_after, "s")
|
|
152
|
+
except ValidationError as err:
|
|
153
|
+
print(err.field_errors)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`AuthenticationError` (401), `PaymentError` (402), `PermissionError` (403, has
|
|
157
|
+
`.upgrade_url` on sport-scope denials), `NotFoundError` (404), `ValidationError`
|
|
158
|
+
(422, has `.field_errors`), `RateLimitError` (429, has `.retry_after` from the
|
|
159
|
+
envelope or `Retry-After` header), `APIError` (5xx), and `ConnectionError`
|
|
160
|
+
(network/timeout, never reached the server) all extend `LumifyError`
|
|
161
|
+
(`.code`, `.status`, `.doc_url`, `.request_id`).
|
|
162
|
+
|
|
163
|
+
GET requests are automatically retried (default: 2 attempts) with exponential
|
|
164
|
+
backoff on `429`/`5xx`/network failures, honoring `Retry-After`. Non-idempotent
|
|
165
|
+
requests (`POST`/webhook & key creation) are never auto-retried. Configure with
|
|
166
|
+
`max_retries` / `timeout` on the `Lumify` constructor.
|
|
167
|
+
|
|
168
|
+
## Credits and rate limits
|
|
169
|
+
|
|
170
|
+
Every successful response carries `X-Credits-Used`, `X-Credits-Remaining`, and
|
|
171
|
+
`X-RateLimit-*` headers. Read them via `get_meta()` (attached out-of-band on the
|
|
172
|
+
returned object, so it never pollutes `json.dumps` or iteration):
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from lumify import get_meta
|
|
176
|
+
|
|
177
|
+
odds = client.events.odds(event_id)
|
|
178
|
+
meta = get_meta(odds)
|
|
179
|
+
print(meta.credits_used, meta.rate_limit_remaining)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Queries for data that isn't available yet (e.g. odds not yet posted) return
|
|
183
|
+
`credits_used == 0` — you're never charged for a "not available" read.
|
|
184
|
+
|
|
185
|
+
## Sync with REST, MCP, and the OpenAPI contract
|
|
186
|
+
|
|
187
|
+
This SDK's response types (`lumify/models.py`) are generated from the *same*
|
|
188
|
+
filtered OpenAPI slice the TypeScript SDK uses
|
|
189
|
+
(`clients/lumify-sdk/openapi/openapi.sdk.json`, produced by
|
|
190
|
+
`scripts/export_openapi_sdk.py` at the repo root), not hand-maintained. CI fails
|
|
191
|
+
if the models are stale, so the two SDKs can't disagree with each other or drift
|
|
192
|
+
from what the API returns. A shared cross-surface probe matrix
|
|
193
|
+
(`tests/fixtures/agent_contract.json`) additionally asserts this SDK builds the
|
|
194
|
+
exact REST request that REST and MCP agree on. The client ergonomics (this
|
|
195
|
+
README's resource shape, pagination/SSE/webhook helpers) are hand-written.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
python scripts/export_openapi_sdk.py # regenerate the schema slice
|
|
199
|
+
python clients/lumify-sdk-python/scripts/gen_models.py # regenerate the Python models
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python -m venv .venv && . .venv/bin/activate
|
|
206
|
+
pip install -e ".[dev]"
|
|
207
|
+
pytest
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
[MIT](./LICENSE) © 2026 Lumify AI
|
|
213
|
+
|
|
214
|
+
## Related
|
|
215
|
+
|
|
216
|
+
- Docs: <https://lumify.ai/docs>
|
|
217
|
+
- TypeScript SDK: [`@lumifyai/sdk`](https://www.npmjs.com/package/@lumifyai/sdk)
|
|
218
|
+
- MCP server (for AI agent runtimes): [`@lumifyai/mcp`](https://www.npmjs.com/package/@lumifyai/mcp)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
lumify — official Python client for the Lumify agent-ready sports intelligence
|
|
3
|
+
API (schedules, live scores, odds, line movement, betting splits, and AI bet
|
|
4
|
+
intelligence). See https://lumify.ai/docs.
|
|
5
|
+
|
|
6
|
+
This SDK *is* the typed REST path — the same data as the REST API and the
|
|
7
|
+
``@lumifyai/mcp`` server, not a third implementation. Response models
|
|
8
|
+
(``lumify.models``) are generated from Lumify's live OpenAPI schema, so they
|
|
9
|
+
can't silently drift from what the API returns.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ._transport import DEFAULT_BASE_URL, LumifyClient
|
|
15
|
+
from .client import Lumify
|
|
16
|
+
from .errors import (
|
|
17
|
+
APIError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
ConnectionError,
|
|
20
|
+
FieldError,
|
|
21
|
+
LumifyError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
PaymentError,
|
|
24
|
+
PermissionError,
|
|
25
|
+
RateLimitError,
|
|
26
|
+
ValidationError,
|
|
27
|
+
)
|
|
28
|
+
from .meta import ResponseMeta, get_meta
|
|
29
|
+
from .pagination import iterate_items, paginate
|
|
30
|
+
from .sse import ScoreStreamEvent, SSEEvent, parse_sse_stream, stream_scores
|
|
31
|
+
from .webhook_signature import WebhookSignatureError, verify_webhook
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Lumify",
|
|
37
|
+
"LumifyClient",
|
|
38
|
+
"DEFAULT_BASE_URL",
|
|
39
|
+
# errors
|
|
40
|
+
"LumifyError",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"PermissionError",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"RateLimitError",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
"PaymentError",
|
|
47
|
+
"APIError",
|
|
48
|
+
"ConnectionError",
|
|
49
|
+
"FieldError",
|
|
50
|
+
# meta
|
|
51
|
+
"ResponseMeta",
|
|
52
|
+
"get_meta",
|
|
53
|
+
# pagination
|
|
54
|
+
"paginate",
|
|
55
|
+
"iterate_items",
|
|
56
|
+
# sse
|
|
57
|
+
"stream_scores",
|
|
58
|
+
"parse_sse_stream",
|
|
59
|
+
"SSEEvent",
|
|
60
|
+
"ScoreStreamEvent",
|
|
61
|
+
# webhooks
|
|
62
|
+
"verify_webhook",
|
|
63
|
+
"WebhookSignatureError",
|
|
64
|
+
"__version__",
|
|
65
|
+
]
|