flopsindex 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.
- flopsindex/__init__.py +39 -0
- flopsindex/client.py +390 -0
- flopsindex/exceptions.py +39 -0
- flopsindex-0.1.0.dist-info/METADATA +158 -0
- flopsindex-0.1.0.dist-info/RECORD +7 -0
- flopsindex-0.1.0.dist-info/WHEEL +5 -0
- flopsindex-0.1.0.dist-info/top_level.txt +1 -0
flopsindex/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""flopsindex — Python SDK for the FLOPS Compute Intelligence API.
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from flopsindex import Client
|
|
5
|
+
|
|
6
|
+
c = Client() # auth from FLOPSINDEX_API_KEY env var
|
|
7
|
+
tick = c.price("FLCI-H100") # current price
|
|
8
|
+
hist = c.timeseries("FLCI-H100", "7d") # 7-day decimated timeseries
|
|
9
|
+
matches = c.search("h100 spot") # NL → index_id
|
|
10
|
+
catalog = c.catalog() # full catalog (partner-tier)
|
|
11
|
+
doc = c.methodology("flci-h100", "v0.9") # raw methodology markdown
|
|
12
|
+
margin = c.compute_margin(sku="h100_sxm5", region="us_west")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The public surface is auth-free (price / timeseries / search /
|
|
16
|
+
methodology) — those work without an API key. catalog +
|
|
17
|
+
compute_margin + recompute_audit need a key.
|
|
18
|
+
|
|
19
|
+
Set the key via:
|
|
20
|
+
- `FLOPSINDEX_API_KEY` env var (recommended)
|
|
21
|
+
- `Client(api_key="flops_xxx")` constructor
|
|
22
|
+
"""
|
|
23
|
+
from flopsindex.client import Client
|
|
24
|
+
from flopsindex.exceptions import (
|
|
25
|
+
FlopsError,
|
|
26
|
+
FlopsAuthError,
|
|
27
|
+
FlopsNotFoundError,
|
|
28
|
+
FlopsRateLimitError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Client",
|
|
34
|
+
"FlopsError",
|
|
35
|
+
"FlopsAuthError",
|
|
36
|
+
"FlopsNotFoundError",
|
|
37
|
+
"FlopsRateLimitError",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|
flopsindex/client.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""flopsindex consumer SDK — read-side client for the FLOPS API.
|
|
2
|
+
|
|
3
|
+
Architecture:
|
|
4
|
+
- All methods are synchronous (most consumers are scripts /
|
|
5
|
+
notebooks / finance pipelines, not async services). An async
|
|
6
|
+
wrapper can be added in v0.2 without breaking the sync surface.
|
|
7
|
+
- HTTP via stdlib `urllib` so the SDK has ZERO third-party
|
|
8
|
+
dependencies for the basic read path. `httpx` would be nicer
|
|
9
|
+
but locks consumers into a specific async runtime.
|
|
10
|
+
- API key resolved from constructor → FLOPSINDEX_API_KEY env var.
|
|
11
|
+
- Public-surface methods (price / timeseries / search / methodology)
|
|
12
|
+
work without an API key.
|
|
13
|
+
- Partner-tier methods (catalog / compute_margin / recompute_audit /
|
|
14
|
+
indices) require an API key; raise FlopsAuthError if missing.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import urllib.request
|
|
24
|
+
import urllib.error
|
|
25
|
+
from typing import Any, Dict, List, Optional, Union
|
|
26
|
+
|
|
27
|
+
from flopsindex.exceptions import (
|
|
28
|
+
FlopsAuthError,
|
|
29
|
+
FlopsError,
|
|
30
|
+
FlopsNotFoundError,
|
|
31
|
+
FlopsRateLimitError,
|
|
32
|
+
FlopsServerError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("flopsindex")
|
|
36
|
+
|
|
37
|
+
_DEFAULT_BASE_URL = "https://app.flopsindex.com"
|
|
38
|
+
_DEFAULT_TIMEOUT = 30
|
|
39
|
+
_USER_AGENT_TEMPLATE = "flopsindex/{version} (+https://flopsindex.com)"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Client:
|
|
43
|
+
"""The FLOPS API consumer client.
|
|
44
|
+
|
|
45
|
+
Most users want the no-arg form::
|
|
46
|
+
|
|
47
|
+
from flopsindex import Client
|
|
48
|
+
c = Client() # FLOPSINDEX_API_KEY from env
|
|
49
|
+
|
|
50
|
+
Override base_url to hit a staging instance::
|
|
51
|
+
|
|
52
|
+
c = Client(base_url="https://staging.flopsindex.com")
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: Optional[str] = None,
|
|
58
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
59
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
60
|
+
user_agent: Optional[str] = None,
|
|
61
|
+
):
|
|
62
|
+
self._api_key = api_key or os.environ.get("FLOPSINDEX_API_KEY")
|
|
63
|
+
self._base_url = base_url.rstrip("/")
|
|
64
|
+
self._timeout = timeout
|
|
65
|
+
# Lazy import to avoid circular ref
|
|
66
|
+
from flopsindex import __version__ as _v
|
|
67
|
+
self._user_agent = user_agent or _USER_AGENT_TEMPLATE.format(version=_v)
|
|
68
|
+
|
|
69
|
+
# -----------------------------------------------------------------
|
|
70
|
+
# PUBLIC (auth-free) — works without an API key
|
|
71
|
+
# -----------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def price(self, index_id: str) -> Dict[str, Any]:
|
|
74
|
+
"""Latest published price for one SPOT index. Auth-free.
|
|
75
|
+
|
|
76
|
+
Returns ``{index_id, value, unit, ts, tier, confidence,
|
|
77
|
+
verify_url, citation_url}``.
|
|
78
|
+
|
|
79
|
+
Raises FlopsNotFoundError if the slug is unknown or is not on
|
|
80
|
+
the public spot surface (forwards / derived stay partner-tier).
|
|
81
|
+
"""
|
|
82
|
+
return self._get(f"/v1/price/{urllib.parse.quote(index_id, safe='')}",
|
|
83
|
+
require_auth=False)
|
|
84
|
+
|
|
85
|
+
def timeseries(self, index_id: str, range: str = "7d") -> Dict[str, Any]:
|
|
86
|
+
"""Decimated timeseries (≤200 points). Auth-free.
|
|
87
|
+
|
|
88
|
+
``range`` ∈ {"24h", "7d", "30d", "90d", "1y"}.
|
|
89
|
+
"""
|
|
90
|
+
return self._get(
|
|
91
|
+
f"/v1/ts/{urllib.parse.quote(index_id, safe='')}",
|
|
92
|
+
params={"range": range}, require_auth=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def search(self, q: str, limit: int = 10) -> Dict[str, Any]:
|
|
96
|
+
"""Natural-language → canonical index_id. Auth-free.
|
|
97
|
+
|
|
98
|
+
Returns ``{q, count, results: [{index_id, family,
|
|
99
|
+
citation_url}, ...]}``.
|
|
100
|
+
"""
|
|
101
|
+
return self._get("/v1/search",
|
|
102
|
+
params={"q": q, "limit": str(limit)},
|
|
103
|
+
require_auth=False)
|
|
104
|
+
|
|
105
|
+
def methodology(
|
|
106
|
+
self, slug: str, version: Optional[str] = None,
|
|
107
|
+
) -> Dict[str, Any]:
|
|
108
|
+
"""Versioned methodology document (with parsed frontmatter).
|
|
109
|
+
|
|
110
|
+
Auth-free. If ``version`` is None, returns the version history
|
|
111
|
+
list. Otherwise returns the doc with body_md + frontmatter.
|
|
112
|
+
|
|
113
|
+
``slug`` is the lower-kebab methodology_id (e.g. "flci-h100",
|
|
114
|
+
"itpi-canonical", "flops-h100-od").
|
|
115
|
+
"""
|
|
116
|
+
if version is None:
|
|
117
|
+
return self._get(
|
|
118
|
+
f"/v1/methodology/{urllib.parse.quote(slug)}/versions",
|
|
119
|
+
require_auth=False)
|
|
120
|
+
return self._get(
|
|
121
|
+
f"/v1/methodology/{urllib.parse.quote(slug)}/"
|
|
122
|
+
f"{urllib.parse.quote(version)}.json",
|
|
123
|
+
require_auth=False)
|
|
124
|
+
|
|
125
|
+
def methodologies(self) -> Dict[str, Any]:
|
|
126
|
+
"""List every published methodology + active version. Auth-free."""
|
|
127
|
+
return self._get("/v1/methodology", require_auth=False)
|
|
128
|
+
|
|
129
|
+
def verify(self, index_id: str, value: float) -> Dict[str, Any]:
|
|
130
|
+
"""Verify whether the given value matches our latest published
|
|
131
|
+
tick for the index_id (within tolerance). Auth-free citation
|
|
132
|
+
check. Raises on non-2xx upstream — use ``verify_handshake`` if
|
|
133
|
+
you want a defensive envelope instead."""
|
|
134
|
+
return self._get(
|
|
135
|
+
"/v1/verify",
|
|
136
|
+
params={"index_id": index_id, "value": str(value)},
|
|
137
|
+
require_auth=False)
|
|
138
|
+
|
|
139
|
+
def verify_handshake(
|
|
140
|
+
self,
|
|
141
|
+
index_id: str,
|
|
142
|
+
value: Optional[float] = None,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""Defensive verify — returns either the canonical record OR a
|
|
145
|
+
structured ``{ok: false, reason, upstream_status}`` envelope on
|
|
146
|
+
any non-2xx response. Never raises for HTTP errors.
|
|
147
|
+
|
|
148
|
+
This is the citation handshake idiom Track D promotes::
|
|
149
|
+
|
|
150
|
+
tick = client.price("FLCI-H100")
|
|
151
|
+
check = client.verify_handshake("FLCI-H100", tick["value"])
|
|
152
|
+
if check.get("ok") is False:
|
|
153
|
+
# upstream broken / endpoint pending — caller decides
|
|
154
|
+
...
|
|
155
|
+
elif check.get("verified"):
|
|
156
|
+
cite(tick, source_url=check["source_url"])
|
|
157
|
+
|
|
158
|
+
Mirrors the MCP server's ``_defensive_get`` shape so an agent
|
|
159
|
+
switching between MCP and direct SDK gets the same error envelope.
|
|
160
|
+
|
|
161
|
+
``value`` is optional — omit to ask "what's the latest tick"
|
|
162
|
+
without committing a value to verify against.
|
|
163
|
+
"""
|
|
164
|
+
params: Dict[str, str] = {"index_id": index_id}
|
|
165
|
+
if value is not None:
|
|
166
|
+
params["value"] = str(value)
|
|
167
|
+
url = self._base_url + "/v1/verify?" + urllib.parse.urlencode(params)
|
|
168
|
+
headers = {"User-Agent": self._user_agent, "Accept": "application/json"}
|
|
169
|
+
try:
|
|
170
|
+
req = urllib.request.Request(url, headers=headers)
|
|
171
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
172
|
+
body = resp.read().decode("utf-8")
|
|
173
|
+
try:
|
|
174
|
+
return json.loads(body)
|
|
175
|
+
except ValueError:
|
|
176
|
+
return {"ok": False, "reason": "invalid_json",
|
|
177
|
+
"upstream_status": resp.status, "url": url}
|
|
178
|
+
except urllib.error.HTTPError as e:
|
|
179
|
+
code = e.code
|
|
180
|
+
if code in (401, 403):
|
|
181
|
+
return {"ok": False, "reason": "auth_required",
|
|
182
|
+
"upstream_status": code, "url": url}
|
|
183
|
+
if code == 404:
|
|
184
|
+
return {"ok": False, "reason": "endpoint_pending",
|
|
185
|
+
"upstream_status": code, "url": url}
|
|
186
|
+
if code >= 500:
|
|
187
|
+
return {"ok": False, "reason": "upstream_http_error",
|
|
188
|
+
"upstream_status": code, "url": url}
|
|
189
|
+
return {"ok": False, "reason": "client_error",
|
|
190
|
+
"upstream_status": code, "url": url}
|
|
191
|
+
except urllib.error.URLError as exc:
|
|
192
|
+
return {"ok": False, "reason": "network_error",
|
|
193
|
+
"url": url, "detail": str(exc.reason)[:300]}
|
|
194
|
+
except Exception as exc: # noqa: BLE001
|
|
195
|
+
return {"ok": False, "reason": "network_error",
|
|
196
|
+
"url": url, "detail": str(exc)[:300]}
|
|
197
|
+
|
|
198
|
+
# -----------------------------------------------------------------
|
|
199
|
+
# PARTNER-TIER (X-FLOPS-Api-Key required)
|
|
200
|
+
# -----------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def catalog(self) -> Dict[str, Any]:
|
|
203
|
+
"""Full index catalog with methodology_version stamps + tier
|
|
204
|
+
labels. Partner-tier — requires API key.
|
|
205
|
+
|
|
206
|
+
For an auth-free subset filtered to spot only, use the public
|
|
207
|
+
catalog mirror at GET /v2/catalog/public (no SDK method —
|
|
208
|
+
agents discover via /llms.txt).
|
|
209
|
+
"""
|
|
210
|
+
return self._get("/v1/catalog", require_auth=True)
|
|
211
|
+
|
|
212
|
+
def index_current(self, index_id: str) -> Dict[str, Any]:
|
|
213
|
+
"""Partner-tier per-index lookup with full envelope (value,
|
|
214
|
+
as_of, num_sources, data_tier, methodology_version,
|
|
215
|
+
verify_url). Returns 404 for unknown index_ids; differs from
|
|
216
|
+
``price()`` in that the partner-tier surface knows about
|
|
217
|
+
non-spot families (forwards, derived, etc.) too."""
|
|
218
|
+
return self._get(
|
|
219
|
+
f"/v1/indices/{urllib.parse.quote(index_id, safe='')}/current",
|
|
220
|
+
require_auth=True)
|
|
221
|
+
|
|
222
|
+
def index_history(
|
|
223
|
+
self, index_id: str, limit: int = 24,
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""Partner-tier per-index history. ``limit`` capped at 720."""
|
|
226
|
+
return self._get(
|
|
227
|
+
f"/v1/indices/{urllib.parse.quote(index_id, safe='')}/history",
|
|
228
|
+
params={"limit": str(min(limit, 720))},
|
|
229
|
+
require_auth=True)
|
|
230
|
+
|
|
231
|
+
def compute_margin(
|
|
232
|
+
self, sku: str, region: str = "us_east",
|
|
233
|
+
pue: float = 1.3, kwh_source: str = "live_lmp",
|
|
234
|
+
kwh_override: Optional[float] = None,
|
|
235
|
+
rack_amortization: Optional[float] = None,
|
|
236
|
+
) -> Dict[str, Any]:
|
|
237
|
+
"""Compute-margin endpoint (Tier 2 #5). Partner-tier.
|
|
238
|
+
|
|
239
|
+
Returns the full margin decomposition:
|
|
240
|
+
{price, power_cost, rack_amortization, margin, margin_pct,
|
|
241
|
+
inputs: {chip_power_kw, pue, kwh_source, kwh_usd, ...}}
|
|
242
|
+
"""
|
|
243
|
+
params = {"sku": sku, "region": region,
|
|
244
|
+
"pue": str(pue), "kwh_source": kwh_source}
|
|
245
|
+
if kwh_override is not None:
|
|
246
|
+
params["kwh_override"] = str(kwh_override)
|
|
247
|
+
if rack_amortization is not None:
|
|
248
|
+
params["rack_amortization"] = str(rack_amortization)
|
|
249
|
+
return self._get("/v1/derived/compute-margin",
|
|
250
|
+
params=params, require_auth=True)
|
|
251
|
+
|
|
252
|
+
def recompute_audit(
|
|
253
|
+
self,
|
|
254
|
+
methodology_id: Optional[str] = None,
|
|
255
|
+
index_id: Optional[str] = None,
|
|
256
|
+
status: Optional[str] = None,
|
|
257
|
+
since_hours: int = 168,
|
|
258
|
+
limit: int = 100,
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
"""Recompute audit receipts (Tier 2 #4). Partner-tier.
|
|
261
|
+
|
|
262
|
+
Returns ``{count, receipts: [...]}`` with each receipt
|
|
263
|
+
carrying methodology_version, window, shipped vs recomputed
|
|
264
|
+
values, variance_bps, inputs_hash + receipt_hash for
|
|
265
|
+
external citation.
|
|
266
|
+
"""
|
|
267
|
+
params: Dict[str, str] = {"since_hours": str(since_hours),
|
|
268
|
+
"limit": str(limit)}
|
|
269
|
+
if methodology_id:
|
|
270
|
+
params["methodology_id"] = methodology_id
|
|
271
|
+
if index_id:
|
|
272
|
+
params["index_id"] = index_id
|
|
273
|
+
if status:
|
|
274
|
+
params["status"] = status
|
|
275
|
+
return self._get("/v1/audit/recompute",
|
|
276
|
+
params=params, require_auth=True)
|
|
277
|
+
|
|
278
|
+
def gpu_capex(self, sku: Optional[str] = None) -> Dict[str, Any]:
|
|
279
|
+
"""Per-SKU GPU module reference price — 3-tier LIVE cascade.
|
|
280
|
+
|
|
281
|
+
Cascade per SKU:
|
|
282
|
+
T2 LIVE_USASPENDING — federal procurement median (real new),
|
|
283
|
+
preferred when >=3 awards in 180d
|
|
284
|
+
T1 LIVE_EBAY_X1.15 — eBay completed-listings trimmed-median × 1.15
|
|
285
|
+
T3 SEED — quarterly reference fallback
|
|
286
|
+
|
|
287
|
+
Returns the single-SKU envelope (price_usd, tier, source,
|
|
288
|
+
effective, secondary_market_range_usd, plus live_observations +
|
|
289
|
+
also_seed when LIVE). With ``sku=None`` returns the full seed
|
|
290
|
+
map with meta.live_api_status.
|
|
291
|
+
|
|
292
|
+
No auth required. Methodology:
|
|
293
|
+
https://app.flopsindex.com/methodology/GPU_CAPEX_LIVE_METHODOLOGY.md
|
|
294
|
+
|
|
295
|
+
NOTE: there is no public LIVE MSRP API for datacenter GPUs —
|
|
296
|
+
NVIDIA/AMD/Huawei are channel-priced through OEMs. The LIVE
|
|
297
|
+
tiers here are federal-procurement (T2) or secondary-market ×
|
|
298
|
+
markup (T1), not chip-new MSRP.
|
|
299
|
+
"""
|
|
300
|
+
if sku:
|
|
301
|
+
return self._get(f"/v1/refdata/gpu-capex/{sku}", require_auth=False)
|
|
302
|
+
return self._get("/v1/refdata/gpu-capex", require_auth=False)
|
|
303
|
+
|
|
304
|
+
def gpu_capex_observations(
|
|
305
|
+
self,
|
|
306
|
+
sku: str,
|
|
307
|
+
limit: int = 50,
|
|
308
|
+
) -> Dict[str, Any]:
|
|
309
|
+
"""Audit-trail surface — raw gpu_capex_observations rows for a SKU.
|
|
310
|
+
|
|
311
|
+
Returns the rows that feed the LIVE-tier cascade — partners +
|
|
312
|
+
auditors can drill from a published LIVE value back to the
|
|
313
|
+
underlying federal-contract / eBay-listing records.
|
|
314
|
+
|
|
315
|
+
Returns ``{sku, count, limit, rows, by_source, methodology}``.
|
|
316
|
+
Limit clamped server-side to [1, 500]. No auth required;
|
|
317
|
+
503 if migration 012 hasn't been applied to this Neon branch.
|
|
318
|
+
"""
|
|
319
|
+
return self._get(
|
|
320
|
+
f"/v1/refdata/gpu-capex/{sku}/observations",
|
|
321
|
+
params={"limit": str(int(limit))},
|
|
322
|
+
require_auth=False,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# -----------------------------------------------------------------
|
|
326
|
+
# HTTP plumbing
|
|
327
|
+
# -----------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def _get(
|
|
330
|
+
self,
|
|
331
|
+
path: str,
|
|
332
|
+
*,
|
|
333
|
+
params: Optional[Dict[str, str]] = None,
|
|
334
|
+
require_auth: bool = False,
|
|
335
|
+
) -> Dict[str, Any]:
|
|
336
|
+
url = self._base_url + path
|
|
337
|
+
if params:
|
|
338
|
+
url += "?" + urllib.parse.urlencode(params)
|
|
339
|
+
|
|
340
|
+
headers = {"User-Agent": self._user_agent, "Accept": "application/json"}
|
|
341
|
+
if require_auth:
|
|
342
|
+
if not self._api_key:
|
|
343
|
+
raise FlopsAuthError(
|
|
344
|
+
f"{path} requires an API key. Set FLOPSINDEX_API_KEY "
|
|
345
|
+
f"or pass api_key= to Client()."
|
|
346
|
+
)
|
|
347
|
+
headers["X-FLOPS-Api-Key"] = self._api_key
|
|
348
|
+
elif self._api_key:
|
|
349
|
+
# Send the key on public paths too — gives the server
|
|
350
|
+
# higher rate-limit allowance for keyed callers.
|
|
351
|
+
headers["X-FLOPS-Api-Key"] = self._api_key
|
|
352
|
+
|
|
353
|
+
req = urllib.request.Request(url, headers=headers)
|
|
354
|
+
try:
|
|
355
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
356
|
+
body = resp.read().decode("utf-8")
|
|
357
|
+
return json.loads(body)
|
|
358
|
+
except urllib.error.HTTPError as e:
|
|
359
|
+
self._raise_for_status(e.code, e.read().decode("utf-8", "replace"),
|
|
360
|
+
e.headers)
|
|
361
|
+
except urllib.error.URLError as e:
|
|
362
|
+
raise FlopsError(f"network error: {e.reason}")
|
|
363
|
+
# unreachable
|
|
364
|
+
raise FlopsError(f"unexpected: {path}")
|
|
365
|
+
|
|
366
|
+
def _raise_for_status(
|
|
367
|
+
self, status_code: int, body: str, headers,
|
|
368
|
+
) -> None:
|
|
369
|
+
try:
|
|
370
|
+
detail = json.loads(body)
|
|
371
|
+
except Exception:
|
|
372
|
+
detail = body
|
|
373
|
+
msg = f"HTTP {status_code}"
|
|
374
|
+
if isinstance(detail, dict) and "detail" in detail:
|
|
375
|
+
msg += f": {detail['detail']}"
|
|
376
|
+
if status_code in (401, 403):
|
|
377
|
+
raise FlopsAuthError(msg, status_code=status_code, detail=detail)
|
|
378
|
+
if status_code == 404:
|
|
379
|
+
raise FlopsNotFoundError(msg, status_code=status_code, detail=detail)
|
|
380
|
+
if status_code == 429:
|
|
381
|
+
retry_after = 60
|
|
382
|
+
try:
|
|
383
|
+
retry_after = int(headers.get("Retry-After", "60"))
|
|
384
|
+
except (ValueError, TypeError, AttributeError):
|
|
385
|
+
pass
|
|
386
|
+
raise FlopsRateLimitError(msg, retry_after_seconds=retry_after,
|
|
387
|
+
status_code=status_code, detail=detail)
|
|
388
|
+
if 500 <= status_code < 600:
|
|
389
|
+
raise FlopsServerError(msg, status_code=status_code, detail=detail)
|
|
390
|
+
raise FlopsError(msg, status_code=status_code, detail=detail)
|
flopsindex/exceptions.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""flopsindex SDK exception hierarchy.
|
|
2
|
+
|
|
3
|
+
All SDK errors descend from `FlopsError` so callers can catch one
|
|
4
|
+
thing if they don't care about the distinction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FlopsError(Exception):
|
|
9
|
+
"""Base SDK error. Carries (status_code, detail) when raised from
|
|
10
|
+
an HTTP response."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str, status_code: int = 0, detail=None):
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.detail = detail
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FlopsAuthError(FlopsError):
|
|
19
|
+
"""401 / 403 — bad or missing API key, or scope insufficient."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FlopsNotFoundError(FlopsError):
|
|
23
|
+
"""404 — index_id / methodology slug doesn't exist OR is not on
|
|
24
|
+
the public surface (forwards/term-rates/derived) and you're
|
|
25
|
+
calling an unauthenticated method."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FlopsRateLimitError(FlopsError):
|
|
29
|
+
"""429 — rate limit exceeded. `retry_after_seconds` populated
|
|
30
|
+
when the response carries the standard header."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, retry_after_seconds: int = 60,
|
|
33
|
+
status_code: int = 429, detail=None):
|
|
34
|
+
self.retry_after_seconds = retry_after_seconds
|
|
35
|
+
super().__init__(message, status_code, detail)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FlopsServerError(FlopsError):
|
|
39
|
+
"""5xx — server-side fault. Usually transient; retry with backoff."""
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flopsindex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for FLOPS Index — live GPU compute and inference token pricing reference rates
|
|
5
|
+
Author-email: FLOPS Index <support@flopsindex.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://app.flopsindex.com
|
|
8
|
+
Project-URL: Documentation, https://app.flopsindex.com/llms.txt
|
|
9
|
+
Project-URL: Source, https://github.com/zeroatflops/flopsindex
|
|
10
|
+
Project-URL: Methodology, https://app.flopsindex.com/v1/methodology
|
|
11
|
+
Keywords: flops,gpu,pricing,compute,inference,benchmark,fintech
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# flopsindex — Python SDK
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/flopsindex/)
|
|
32
|
+
[](https://pypi.org/project/flopsindex/)
|
|
33
|
+
[](https://pypi.org/project/flopsindex/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install flopsindex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Live GPU compute pricing + LLM inference token pricing reference rates
|
|
41
|
+
from the FLOPS Index. Single dependency-free SDK over the FLOPS HTTP
|
|
42
|
+
API.
|
|
43
|
+
|
|
44
|
+
## 30-second example
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from flopsindex import Client
|
|
48
|
+
|
|
49
|
+
c = Client() # picks up FLOPSINDEX_API_KEY if set; works auth-free for public methods
|
|
50
|
+
|
|
51
|
+
# Public, auth-free — works with no API key
|
|
52
|
+
print(c.price("FLCI-H100"))
|
|
53
|
+
# {'index_id': 'FLCI-H100', 'value': 2.45, 'unit': 'USD/GPU-hr',
|
|
54
|
+
# 'ts': '2026-05-17T14:00:00+00:00', 'tier': 'LIVE',
|
|
55
|
+
# 'confidence': 'HIGH', 'verify_url': '...', 'citation_url': '...'}
|
|
56
|
+
|
|
57
|
+
print(c.timeseries("FLCI-H100", "7d")) # ≤200 decimated points
|
|
58
|
+
print(c.search("h100 spot")) # NL → canonical slugs
|
|
59
|
+
print(c.methodology("flci-h100", "v0.9")) # versioned methodology + frontmatter
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Authentication
|
|
63
|
+
|
|
64
|
+
The SDK reads an API key from `FLOPSINDEX_API_KEY`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export FLOPSINDEX_API_KEY="flops_xxxxxxxxx"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or pass directly:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
c = Client(api_key="flops_xxxxxxxxx")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Auth-free methods** (work without a key): `price`, `timeseries`,
|
|
77
|
+
`search`, `methodology`, `methodologies`, `verify`.
|
|
78
|
+
|
|
79
|
+
**Partner-tier methods** (require a key): `catalog`, `index_current`,
|
|
80
|
+
`index_history`, `compute_margin`, `recompute_audit`.
|
|
81
|
+
|
|
82
|
+
If you call a partner-tier method without a key, you'll get
|
|
83
|
+
`FlopsAuthError`. Public methods always work — the API key just
|
|
84
|
+
unlocks higher rate-limit tiers when you have one.
|
|
85
|
+
|
|
86
|
+
## Citation in code
|
|
87
|
+
|
|
88
|
+
When citing a FLOPS price in code, contracts, or research, ALWAYS
|
|
89
|
+
include the methodology version (every response carries it under
|
|
90
|
+
`methodology_version`):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
tick = c.index_current("FLCI-H100")
|
|
94
|
+
print(f"Settled at ${tick['current_value']:.2f}/GPU-hr per "
|
|
95
|
+
f"{tick['methodology_version']}")
|
|
96
|
+
# Settled at $2.45/GPU-hr per flci-h100@v0.9
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The version is the contract anchor — partner replays + recompute
|
|
100
|
+
audits pin against it.
|
|
101
|
+
|
|
102
|
+
## Methods
|
|
103
|
+
|
|
104
|
+
| Method | Auth | Returns |
|
|
105
|
+
|-----------------------|-----------|------------------------------------|
|
|
106
|
+
| `price(index_id)` | none | latest tick |
|
|
107
|
+
| `timeseries(id, range)` | none | decimated points (≤200) |
|
|
108
|
+
| `search(q)` | none | NL → slug results |
|
|
109
|
+
| `methodologies()` | none | list of all methodologies |
|
|
110
|
+
| `methodology(slug, version)` | none | one methodology doc (markdown + JSON) |
|
|
111
|
+
| `verify(id, value)` | none | does the value match our tick? |
|
|
112
|
+
| `catalog()` | partner | full envelope w/ methodology_version |
|
|
113
|
+
| `index_current(id)` | partner | partner-tier per-index lookup |
|
|
114
|
+
| `index_history(id)` | partner | up to 720 historical ticks |
|
|
115
|
+
| `compute_margin(...)` | partner | derived: price − power − rack |
|
|
116
|
+
| `recompute_audit(...)`| partner | audit receipts (IOSCO substrate) |
|
|
117
|
+
|
|
118
|
+
## Naming conventions
|
|
119
|
+
|
|
120
|
+
- `FLCI-{model}` — composite spot+OD+DePIN
|
|
121
|
+
- `FLOPS-{model}-{OD|SPOT|DEPIN}` — single-tier specific
|
|
122
|
+
- `ITPI-{model_id}-{INPUT|OUTPUT}` — inference token pricing
|
|
123
|
+
- `CLRI-{model}-{tenor}` — forward / term rates (partner-tier only)
|
|
124
|
+
|
|
125
|
+
Use `c.search()` if you don't know the exact slug.
|
|
126
|
+
|
|
127
|
+
## Errors
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from flopsindex import Client, FlopsNotFoundError, FlopsAuthError
|
|
131
|
+
|
|
132
|
+
c = Client()
|
|
133
|
+
try:
|
|
134
|
+
tick = c.price("FLCI-UNKNOWN")
|
|
135
|
+
except FlopsNotFoundError as e:
|
|
136
|
+
print(f"index not found: {e.detail}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Hierarchy: `FlopsError` → `FlopsAuthError` / `FlopsNotFoundError` /
|
|
140
|
+
`FlopsRateLimitError` / `FlopsServerError`. All errors carry
|
|
141
|
+
`.status_code` and `.detail`.
|
|
142
|
+
|
|
143
|
+
## Not the MCP server
|
|
144
|
+
|
|
145
|
+
`flopsindex` (this package) is a HTTP client SDK. The MCP server for
|
|
146
|
+
AI agents is a separate package: `pip install flopsindex-mcp`. See
|
|
147
|
+
https://github.com/zeroatflops/flopsindex-mcp for that one.
|
|
148
|
+
|
|
149
|
+
## Methodology
|
|
150
|
+
|
|
151
|
+
Every published price is backed by a versioned methodology document.
|
|
152
|
+
Citation hooks live at:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
https://app.flopsindex.com/v1/methodology/{slug}/{version}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For example, `https://app.flopsindex.com/v1/methodology/flci-h100/v0.9`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
flopsindex/__init__.py,sha256=vjn_j_7ifKTTe21IUOonlsgSzFU2RvnXlyB7XpUH620,1158
|
|
2
|
+
flopsindex/client.py,sha256=gdBjRjSE7Zr2XE2HKnZDnus1HZlezpnb0IFcbova4kM,15939
|
|
3
|
+
flopsindex/exceptions.py,sha256=lTsIP3FEPy7uyQcyKwZ6ZrzGjy_eqS4Tl9UDhPGLXCQ,1294
|
|
4
|
+
flopsindex-0.1.0.dist-info/METADATA,sha256=1U-YNgY0tjJ1rEIO8JRGi51HbqhrKld3elhehwl8LS8,5980
|
|
5
|
+
flopsindex-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
flopsindex-0.1.0.dist-info/top_level.txt,sha256=SQ6eC8esddT9NW5BnuL1-8d3G8o4KuZiFSqo50dV008,11
|
|
7
|
+
flopsindex-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flopsindex
|