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.
Files changed (34) hide show
  1. lumify_sdk-0.1.0/.gitignore +15 -0
  2. lumify_sdk-0.1.0/LICENSE +21 -0
  3. lumify_sdk-0.1.0/PKG-INFO +247 -0
  4. lumify_sdk-0.1.0/README.md +218 -0
  5. lumify_sdk-0.1.0/lumify/__init__.py +65 -0
  6. lumify_sdk-0.1.0/lumify/_transport.py +286 -0
  7. lumify_sdk-0.1.0/lumify/client.py +58 -0
  8. lumify_sdk-0.1.0/lumify/errors.py +169 -0
  9. lumify_sdk-0.1.0/lumify/meta.py +135 -0
  10. lumify_sdk-0.1.0/lumify/models.py +412 -0
  11. lumify_sdk-0.1.0/lumify/pagination.py +52 -0
  12. lumify_sdk-0.1.0/lumify/py.typed +0 -0
  13. lumify_sdk-0.1.0/lumify/resources/__init__.py +22 -0
  14. lumify_sdk-0.1.0/lumify/resources/agent.py +69 -0
  15. lumify_sdk-0.1.0/lumify/resources/events.py +173 -0
  16. lumify_sdk-0.1.0/lumify/resources/players.py +94 -0
  17. lumify_sdk-0.1.0/lumify/resources/sports.py +32 -0
  18. lumify_sdk-0.1.0/lumify/resources/teams.py +64 -0
  19. lumify_sdk-0.1.0/lumify/resources/webhooks.py +64 -0
  20. lumify_sdk-0.1.0/lumify/sse.py +167 -0
  21. lumify_sdk-0.1.0/lumify/webhook_signature.py +81 -0
  22. lumify_sdk-0.1.0/pyproject.toml +57 -0
  23. lumify_sdk-0.1.0/scripts/gen_models.py +209 -0
  24. lumify_sdk-0.1.0/tests/__init__.py +0 -0
  25. lumify_sdk-0.1.0/tests/_helpers.py +77 -0
  26. lumify_sdk-0.1.0/tests/conftest.py +6 -0
  27. lumify_sdk-0.1.0/tests/test_client.py +126 -0
  28. lumify_sdk-0.1.0/tests/test_contract.py +105 -0
  29. lumify_sdk-0.1.0/tests/test_errors.py +68 -0
  30. lumify_sdk-0.1.0/tests/test_meta.py +50 -0
  31. lumify_sdk-0.1.0/tests/test_pagination.py +59 -0
  32. lumify_sdk-0.1.0/tests/test_resources.py +128 -0
  33. lumify_sdk-0.1.0/tests/test_sse.py +84 -0
  34. lumify_sdk-0.1.0/tests/test_webhook_signature.py +52 -0
@@ -0,0 +1,15 @@
1
+ # Python build/artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+
12
+ # Local virtualenvs
13
+ .venv/
14
+ venv/
15
+ env/
@@ -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
+ ]