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 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)
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/flopsindex.svg)](https://pypi.org/project/flopsindex/)
32
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flopsindex.svg)](https://pypi.org/project/flopsindex/)
33
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/flopsindex.svg)](https://pypi.org/project/flopsindex/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flopsindex