flopsindex-partner 0.2.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.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: flopsindex-partner
3
+ Version: 0.2.0
4
+ Summary: Partner-tier write SDK for the FLOPS Compute Intelligence Platform — submit fleet / SMPI / CLRI data. Companion to the public-read SDK at https://pypi.org/project/flopsindex/.
5
+ Author-email: Ash Chary <ash@flopsindex.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://flopsindex.com
8
+ Project-URL: ReadSDK, https://pypi.org/project/flopsindex/
9
+ Project-URL: MCPServer, https://pypi.org/project/flopsindex-mcp/
10
+ Keywords: flops,compute,partner,submission,fleet,smpi,clri
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: httpx>=0.26.0
14
+ Provides-Extra: metrics
15
+ Requires-Dist: prometheus-client>=0.20.0; extra == "metrics"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
19
+ Requires-Dist: respx>=0.21.0; extra == "dev"
20
+
21
+ # flopsindex-partner — partner write SDK
22
+
23
+ [![PyPI version](https://img.shields.io/pypi/v/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
24
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
25
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
26
+
27
+ ```bash
28
+ pip install flopsindex-partner
29
+ ```
30
+
31
+ Authenticated **write-side** SDK for contributing partners (Modular, the
32
+ lender-wave anchors, future fleet operators) submitting fleet / SMPI / CLRI
33
+ data into the FLOPS Compute Intelligence Platform.
34
+
35
+ For the **read-side** (price / verify / catalog / methodology / timeseries
36
+ / compute_margin / spread) install the companion package
37
+ [`flopsindex`](https://pypi.org/project/flopsindex/) — different audience,
38
+ different brand, no API key required for the public surface.
39
+
40
+ ## 30-second example
41
+
42
+ ```python
43
+ from flopsindex_partner import FLOPSClient
44
+
45
+ c = FLOPSClient(api_key="flops_xxxxxxxxx")
46
+
47
+ # Weekly fleet snapshot
48
+ c.submit_weekly({
49
+ "partner_id": "modular",
50
+ "as_of": "2026-05-19T00:00:00Z",
51
+ "gpus": [{"sku": "h100_sxm5", "region": "us_east", "count": 128}, ...],
52
+ })
53
+
54
+ # Single-machine pricing index event
55
+ c.submit_smpi({
56
+ "partner_id": "modular",
57
+ "sku": "h100_sxm5",
58
+ "region": "us_east",
59
+ "price_usd": 2.42,
60
+ "tier": "on_demand",
61
+ "ts": "2026-05-19T22:00:00Z",
62
+ })
63
+
64
+ # CLRI lease-rate submission
65
+ c.submit_clri({
66
+ "partner_id": "modular",
67
+ "sku": "h100_sxm5",
68
+ "tenor": "P36M",
69
+ "implied_rate_pct": 11.4,
70
+ "as_of": "2026-05-19T00:00:00Z",
71
+ })
72
+ ```
73
+
74
+ ## Async surface
75
+
76
+ Every method has an `a`-prefixed async sibling:
77
+
78
+ ```python
79
+ import asyncio
80
+ from flopsindex_partner import FLOPSClient
81
+
82
+ async def main():
83
+ async with FLOPSClient(api_key="...") as c:
84
+ await c.asubmit_smpi({...})
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Renamed from `flops-client` (2026-05-19)
90
+
91
+ This package was previously published as `flops-client`. Old imports
92
+ continue to work but emit `DeprecationWarning`:
93
+
94
+ ```python
95
+ # OLD — deprecated, still works
96
+ from flops_client import FLOPSClient
97
+
98
+ # NEW — canonical
99
+ from flopsindex_partner import FLOPSClient
100
+ ```
101
+
102
+ The PyPI distribution name also changed (`flops-client` →
103
+ `flopsindex-partner`). Update your `requirements.txt`:
104
+
105
+ ```diff
106
+ - flops-client==0.1.0
107
+ + flopsindex-partner>=0.2.0
108
+ ```
109
+
110
+ The legacy `flops-client` distribution on PyPI will be marked deprecated
111
+ in a follow-up release; it will continue to install but won't receive
112
+ updates. The recommended deadline for migration is **2026-12-31**.
113
+
114
+ ## Authentication
115
+
116
+ API keys are issued by FLOPS partner ops. Email `partners@flopsindex.com`
117
+ to onboard. Once issued:
118
+
119
+ ```bash
120
+ export FLOPS_API_KEY="flops_xxxxxxxxx"
121
+ ```
122
+
123
+ ```python
124
+ import os
125
+ from flopsindex_partner import FLOPSClient
126
+
127
+ c = FLOPSClient(api_key=os.environ["FLOPS_API_KEY"])
128
+ ```
129
+
130
+ ## Submission contracts
131
+
132
+ The schemas for `submit_weekly` / `submit_smpi` / `submit_clri` live in
133
+ the Submission Guide (latest at
134
+ `https://app.flopsindex.com/v1/methodology/submission-guide`).
135
+ Each method returns the server's receipt envelope:
136
+
137
+ ```python
138
+ result = c.submit_smpi({...})
139
+ # {'receipt_id': '...', 'received_at': '...', 'methodology_version': '...',
140
+ # 'k_anon_floor_met': True, 'inputs_hash': 'sha256:...'}
141
+ ```
142
+
143
+ Hold onto `receipt_id` + `inputs_hash` — they're the audit trail.
144
+
145
+ ## Errors
146
+
147
+ `FLOPSClientError` is raised on non-retryable 4xx + exhausted 5xx
148
+ retries. The SDK retries 429/500/502/503/504 up to 3 times with
149
+ exponential backoff before surfacing.
150
+
151
+ ```python
152
+ from flopsindex_partner import FLOPSClient, FLOPSClientError
153
+
154
+ try:
155
+ c.submit_smpi({...})
156
+ except FLOPSClientError as e:
157
+ print(f"HTTP {e.status_code}: {e.detail}")
158
+ ```
159
+
160
+ ## Optional metrics
161
+
162
+ If `prometheus-client` is installed (`pip install flopsindex-partner[metrics]`),
163
+ the SDK emits:
164
+
165
+ | Metric | Labels |
166
+ |--------|--------|
167
+ | `flops_sdk_submissions_total` | `endpoint`, `status` (`ok` / `error` / `exhausted`) |
168
+ | `flops_sdk_request_seconds` | `endpoint` |
169
+
170
+ Scrape via the standard Prometheus exporter.
171
+
172
+ ## Related
173
+
174
+ - **Read SDK:** `pip install flopsindex` ([PyPI](https://pypi.org/project/flopsindex/))
175
+ - **MCP server:** `pip install flopsindex-mcp` ([PyPI](https://pypi.org/project/flopsindex-mcp/))
176
+ - **Schema (JSON-LD):** [`schema.flopsindex.com/compute-index-spec/v0.1/`](https://schema.flopsindex.com/compute-index-spec/v0.1/)
177
+ - **Verify endpoint:** `GET /v1/verify?index_id=<ID>&value=<v>`
178
+ - **Methodology library:** [`/v1/methodology`](https://app.flopsindex.com/v1/methodology)
@@ -0,0 +1,158 @@
1
+ # flopsindex-partner — partner write SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
5
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
6
+
7
+ ```bash
8
+ pip install flopsindex-partner
9
+ ```
10
+
11
+ Authenticated **write-side** SDK for contributing partners (Modular, the
12
+ lender-wave anchors, future fleet operators) submitting fleet / SMPI / CLRI
13
+ data into the FLOPS Compute Intelligence Platform.
14
+
15
+ For the **read-side** (price / verify / catalog / methodology / timeseries
16
+ / compute_margin / spread) install the companion package
17
+ [`flopsindex`](https://pypi.org/project/flopsindex/) — different audience,
18
+ different brand, no API key required for the public surface.
19
+
20
+ ## 30-second example
21
+
22
+ ```python
23
+ from flopsindex_partner import FLOPSClient
24
+
25
+ c = FLOPSClient(api_key="flops_xxxxxxxxx")
26
+
27
+ # Weekly fleet snapshot
28
+ c.submit_weekly({
29
+ "partner_id": "modular",
30
+ "as_of": "2026-05-19T00:00:00Z",
31
+ "gpus": [{"sku": "h100_sxm5", "region": "us_east", "count": 128}, ...],
32
+ })
33
+
34
+ # Single-machine pricing index event
35
+ c.submit_smpi({
36
+ "partner_id": "modular",
37
+ "sku": "h100_sxm5",
38
+ "region": "us_east",
39
+ "price_usd": 2.42,
40
+ "tier": "on_demand",
41
+ "ts": "2026-05-19T22:00:00Z",
42
+ })
43
+
44
+ # CLRI lease-rate submission
45
+ c.submit_clri({
46
+ "partner_id": "modular",
47
+ "sku": "h100_sxm5",
48
+ "tenor": "P36M",
49
+ "implied_rate_pct": 11.4,
50
+ "as_of": "2026-05-19T00:00:00Z",
51
+ })
52
+ ```
53
+
54
+ ## Async surface
55
+
56
+ Every method has an `a`-prefixed async sibling:
57
+
58
+ ```python
59
+ import asyncio
60
+ from flopsindex_partner import FLOPSClient
61
+
62
+ async def main():
63
+ async with FLOPSClient(api_key="...") as c:
64
+ await c.asubmit_smpi({...})
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ## Renamed from `flops-client` (2026-05-19)
70
+
71
+ This package was previously published as `flops-client`. Old imports
72
+ continue to work but emit `DeprecationWarning`:
73
+
74
+ ```python
75
+ # OLD — deprecated, still works
76
+ from flops_client import FLOPSClient
77
+
78
+ # NEW — canonical
79
+ from flopsindex_partner import FLOPSClient
80
+ ```
81
+
82
+ The PyPI distribution name also changed (`flops-client` →
83
+ `flopsindex-partner`). Update your `requirements.txt`:
84
+
85
+ ```diff
86
+ - flops-client==0.1.0
87
+ + flopsindex-partner>=0.2.0
88
+ ```
89
+
90
+ The legacy `flops-client` distribution on PyPI will be marked deprecated
91
+ in a follow-up release; it will continue to install but won't receive
92
+ updates. The recommended deadline for migration is **2026-12-31**.
93
+
94
+ ## Authentication
95
+
96
+ API keys are issued by FLOPS partner ops. Email `partners@flopsindex.com`
97
+ to onboard. Once issued:
98
+
99
+ ```bash
100
+ export FLOPS_API_KEY="flops_xxxxxxxxx"
101
+ ```
102
+
103
+ ```python
104
+ import os
105
+ from flopsindex_partner import FLOPSClient
106
+
107
+ c = FLOPSClient(api_key=os.environ["FLOPS_API_KEY"])
108
+ ```
109
+
110
+ ## Submission contracts
111
+
112
+ The schemas for `submit_weekly` / `submit_smpi` / `submit_clri` live in
113
+ the Submission Guide (latest at
114
+ `https://app.flopsindex.com/v1/methodology/submission-guide`).
115
+ Each method returns the server's receipt envelope:
116
+
117
+ ```python
118
+ result = c.submit_smpi({...})
119
+ # {'receipt_id': '...', 'received_at': '...', 'methodology_version': '...',
120
+ # 'k_anon_floor_met': True, 'inputs_hash': 'sha256:...'}
121
+ ```
122
+
123
+ Hold onto `receipt_id` + `inputs_hash` — they're the audit trail.
124
+
125
+ ## Errors
126
+
127
+ `FLOPSClientError` is raised on non-retryable 4xx + exhausted 5xx
128
+ retries. The SDK retries 429/500/502/503/504 up to 3 times with
129
+ exponential backoff before surfacing.
130
+
131
+ ```python
132
+ from flopsindex_partner import FLOPSClient, FLOPSClientError
133
+
134
+ try:
135
+ c.submit_smpi({...})
136
+ except FLOPSClientError as e:
137
+ print(f"HTTP {e.status_code}: {e.detail}")
138
+ ```
139
+
140
+ ## Optional metrics
141
+
142
+ If `prometheus-client` is installed (`pip install flopsindex-partner[metrics]`),
143
+ the SDK emits:
144
+
145
+ | Metric | Labels |
146
+ |--------|--------|
147
+ | `flops_sdk_submissions_total` | `endpoint`, `status` (`ok` / `error` / `exhausted`) |
148
+ | `flops_sdk_request_seconds` | `endpoint` |
149
+
150
+ Scrape via the standard Prometheus exporter.
151
+
152
+ ## Related
153
+
154
+ - **Read SDK:** `pip install flopsindex` ([PyPI](https://pypi.org/project/flopsindex/))
155
+ - **MCP server:** `pip install flopsindex-mcp` ([PyPI](https://pypi.org/project/flopsindex-mcp/))
156
+ - **Schema (JSON-LD):** [`schema.flopsindex.com/compute-index-spec/v0.1/`](https://schema.flopsindex.com/compute-index-spec/v0.1/)
157
+ - **Verify endpoint:** `GET /v1/verify?index_id=<ID>&value=<v>`
158
+ - **Methodology library:** [`/v1/methodology`](https://app.flopsindex.com/v1/methodology)
@@ -0,0 +1,21 @@
1
+ """DEPRECATED — renamed to ``flopsindex-partner`` 2026-05-19.
2
+
3
+ Imports continue to work via this shim but emit DeprecationWarning. Move
4
+ pinned imports to ``from flopsindex_partner import FLOPSClient`` for the
5
+ long-term contract. The PyPI distribution name also changed
6
+ (``flops-client`` → ``flopsindex-partner``); the legacy ``flops-client``
7
+ package on PyPI will be marked deprecated in a follow-up release.
8
+ """
9
+ import warnings as _warnings
10
+
11
+ _warnings.warn(
12
+ "flops_client has been renamed to flopsindex_partner. "
13
+ "Use `from flopsindex_partner import FLOPSClient` instead. "
14
+ "The old import path will continue to work but is deprecated.",
15
+ DeprecationWarning,
16
+ stacklevel=2,
17
+ )
18
+
19
+ from flopsindex_partner import FLOPSClient, FLOPSClientError # noqa: E402,F401
20
+
21
+ __all__ = ["FLOPSClient", "FLOPSClientError"]
@@ -0,0 +1,24 @@
1
+ """DEPRECATED — see ``flopsindex_partner.client``.
2
+
3
+ This module re-exports the canonical implementation so any caller pinned to
4
+ ``from flops_client.client import FLOPSClient`` (or
5
+ ``from sdk.flops_client.client import FLOPSClient``) keeps working. Single
6
+ source of truth lives at ``flopsindex_partner/client.py``; this file is
7
+ deliberately a thin shim to avoid drift between the two paths.
8
+ """
9
+ import warnings as _warnings
10
+
11
+ _warnings.warn(
12
+ "flops_client.client has been renamed to flopsindex_partner.client. "
13
+ "Use `from flopsindex_partner.client import FLOPSClient` instead. "
14
+ "The old import path will continue to work but is deprecated.",
15
+ DeprecationWarning,
16
+ stacklevel=2,
17
+ )
18
+
19
+ from flopsindex_partner.client import ( # noqa: E402,F401
20
+ FLOPSClient,
21
+ FLOPSClientError,
22
+ )
23
+
24
+ __all__ = ["FLOPSClient", "FLOPSClientError"]
@@ -0,0 +1,26 @@
1
+ """flopsindex-partner — partner-tier write SDK for the FLOPS Compute
2
+ Intelligence Platform.
3
+
4
+ ```python
5
+ from flopsindex_partner import FLOPSClient
6
+
7
+ c = FLOPSClient(api_key="flops_xxx")
8
+ c.submit_weekly({...})
9
+ c.submit_smpi({...})
10
+ c.submit_clri({...})
11
+ ```
12
+
13
+ For the public read-side surface (price / verify / catalog / methodology /
14
+ timeseries / spread / compute_margin) install the sibling ``flopsindex``
15
+ package — different audience, different brand, same publisher.
16
+
17
+ Renamed from ``flops-client`` 2026-05-19. The old import path
18
+ ``from flops_client import FLOPSClient`` continues to work via a
19
+ deprecation shim — but emits ``DeprecationWarning``. Move pinned imports
20
+ to ``flopsindex_partner`` for the long-term contract.
21
+ """
22
+
23
+ from flopsindex_partner.client import FLOPSClient, FLOPSClientError
24
+
25
+ __version__ = "0.2.0"
26
+ __all__ = ["FLOPSClient", "FLOPSClientError", "__version__"]
@@ -0,0 +1,288 @@
1
+ """FLOPS partner submission SDK — write-side client.
2
+
3
+ Canonical home for the partner-tier write surface. Use when ingesting
4
+ fleet / SMPI / CLRI data into FLOPS as a contributing partner (Modular,
5
+ and the wave that follows it). For the public read-side API see the
6
+ sibling ``flopsindex`` package.
7
+
8
+ Naming:
9
+ - PyPI package: ``flopsindex-partner``
10
+ - Python import: ``from flopsindex_partner import FLOPSClient``
11
+ - Legacy import (deprecated, kept for Modular's pinned imports):
12
+ ``from flops_client import FLOPSClient`` — emits DeprecationWarning
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import time
19
+ from typing import Any, Optional
20
+
21
+ import httpx
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ try:
26
+ from prometheus_client import Counter, Histogram
27
+
28
+ _prom_available = True
29
+ except ImportError:
30
+ _prom_available = False
31
+
32
+ _DEFAULT_BASE_URL = "https://api.flopsindex.com/v2"
33
+ _DEFAULT_TIMEOUT = 30
34
+ _MAX_RETRIES = 3
35
+ _BACKOFF_BASE = 0.5
36
+ _RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
37
+ _USER_AGENT = "flopsindex-partner/0.2.0"
38
+
39
+
40
+ def _init_metrics() -> tuple:
41
+ """Register Prometheus collectors idempotently.
42
+
43
+ Counter/Histogram raise ValueError on re-registration with the same name,
44
+ which crashes any caller that reloads this module (live-reload servers,
45
+ test suites that purge sys.modules). The double-registration only
46
+ happens when the registry already has the collectors — at which point
47
+ we can safely return (None, None); the original module instance still
48
+ owns the live counters and the reloaded copy doesn't need to re-emit.
49
+ """
50
+ if not _prom_available:
51
+ return None, None
52
+ try:
53
+ submissions = Counter(
54
+ "flops_sdk_submissions_total",
55
+ "Total SDK submissions",
56
+ ["endpoint", "status"],
57
+ )
58
+ latency = Histogram(
59
+ "flops_sdk_request_seconds",
60
+ "SDK request latency",
61
+ ["endpoint"],
62
+ )
63
+ return submissions, latency
64
+ except ValueError:
65
+ # Already registered — module is being reloaded.
66
+ return None, None
67
+
68
+
69
+ _submissions_counter, _latency_histogram = _init_metrics()
70
+
71
+
72
+ class FLOPSClientError(Exception):
73
+ def __init__(self, status_code: int, detail: Any):
74
+ self.status_code = status_code
75
+ self.detail = detail
76
+ super().__init__(f"HTTP {status_code}: {detail}")
77
+
78
+
79
+ class FLOPSClient:
80
+ """Synchronous + async write-side client for partner submissions."""
81
+
82
+ def __init__(
83
+ self,
84
+ api_key: str,
85
+ base_url: str = _DEFAULT_BASE_URL,
86
+ timeout: int = _DEFAULT_TIMEOUT,
87
+ ):
88
+ self._api_key = api_key
89
+ self._base_url = base_url.rstrip("/")
90
+ self._timeout = timeout
91
+ self._client: Optional[httpx.Client] = None
92
+ self._async_client: Optional[httpx.AsyncClient] = None
93
+
94
+ def _get_headers(self) -> dict[str, str]:
95
+ return {
96
+ "X-API-Key": self._api_key,
97
+ "Content-Type": "application/json",
98
+ "User-Agent": _USER_AGENT,
99
+ }
100
+
101
+ def _ensure_sync_client(self) -> httpx.Client:
102
+ if self._client is None:
103
+ self._client = httpx.Client(
104
+ base_url=self._base_url,
105
+ headers=self._get_headers(),
106
+ timeout=self._timeout,
107
+ )
108
+ return self._client
109
+
110
+ def _request(
111
+ self,
112
+ method: str,
113
+ path: str,
114
+ *,
115
+ json: Optional[dict] = None,
116
+ params: Optional[dict] = None,
117
+ ) -> dict:
118
+ client = self._ensure_sync_client()
119
+ last_exc: Optional[Exception] = None
120
+
121
+ for attempt in range(_MAX_RETRIES):
122
+ start = time.monotonic()
123
+ try:
124
+ response = client.request(method, path, json=json, params=params)
125
+ elapsed = time.monotonic() - start
126
+
127
+ if _latency_histogram:
128
+ _latency_histogram.labels(endpoint=path).observe(elapsed)
129
+
130
+ if response.status_code < 400:
131
+ if _submissions_counter and method == "POST":
132
+ _submissions_counter.labels(endpoint=path, status="ok").inc()
133
+ return response.json()
134
+
135
+ if response.status_code not in _RETRYABLE_STATUS_CODES:
136
+ if _submissions_counter and method == "POST":
137
+ _submissions_counter.labels(endpoint=path, status="error").inc()
138
+ raise FLOPSClientError(response.status_code, response.text)
139
+
140
+ last_exc = FLOPSClientError(response.status_code, response.text)
141
+
142
+ except httpx.TransportError as exc:
143
+ last_exc = exc
144
+
145
+ backoff = _BACKOFF_BASE * (2 ** attempt)
146
+ logger.warning("Retry %d/%d for %s %s (backoff %.1fs)",
147
+ attempt + 1, _MAX_RETRIES, method, path, backoff)
148
+ time.sleep(backoff)
149
+
150
+ if _submissions_counter and method == "POST":
151
+ _submissions_counter.labels(endpoint=path, status="exhausted").inc()
152
+ raise last_exc # type: ignore[misc]
153
+
154
+ def submit_weekly(self, submission: dict) -> dict:
155
+ return self._request("POST", "/submit/fleet", json=submission)
156
+
157
+ def submit_smpi(self, transaction: dict) -> dict:
158
+ return self._request("POST", "/submit/smpi", json=transaction)
159
+
160
+ def submit_clri(self, submission: dict) -> dict:
161
+ return self._request("POST", "/submit/clri", json=submission)
162
+
163
+ def get_index(
164
+ self,
165
+ index_id: str,
166
+ start: Optional[str] = None,
167
+ end: Optional[str] = None,
168
+ ) -> dict:
169
+ params: dict[str, str] = {}
170
+ if start:
171
+ params["start"] = start
172
+ if end:
173
+ params["end"] = end
174
+ return self._request("GET", f"/indices/{index_id}", params=params)
175
+
176
+ def get_health(self) -> dict:
177
+ return self._request("GET", "/health")
178
+
179
+ def close(self) -> None:
180
+ if self._client:
181
+ self._client.close()
182
+ self._client = None
183
+ if self._async_client:
184
+ pass # must be closed with aclose()
185
+
186
+ def __enter__(self) -> "FLOPSClient":
187
+ self._ensure_sync_client()
188
+ return self
189
+
190
+ def __exit__(self, *exc: Any) -> None:
191
+ self.close()
192
+
193
+ # --- async interface ---
194
+
195
+ def _ensure_async_client(self) -> httpx.AsyncClient:
196
+ if self._async_client is None:
197
+ self._async_client = httpx.AsyncClient(
198
+ base_url=self._base_url,
199
+ headers=self._get_headers(),
200
+ timeout=self._timeout,
201
+ )
202
+ return self._async_client
203
+
204
+ async def _arequest(
205
+ self,
206
+ method: str,
207
+ path: str,
208
+ *,
209
+ json: Optional[dict] = None,
210
+ params: Optional[dict] = None,
211
+ ) -> dict:
212
+ import asyncio
213
+
214
+ client = self._ensure_async_client()
215
+ last_exc: Optional[Exception] = None
216
+
217
+ for attempt in range(_MAX_RETRIES):
218
+ start = time.monotonic()
219
+ try:
220
+ response = await client.request(method, path, json=json, params=params)
221
+ elapsed = time.monotonic() - start
222
+
223
+ if _latency_histogram:
224
+ _latency_histogram.labels(endpoint=path).observe(elapsed)
225
+
226
+ if response.status_code < 400:
227
+ if _submissions_counter and method == "POST":
228
+ _submissions_counter.labels(endpoint=path, status="ok").inc()
229
+ return response.json()
230
+
231
+ if response.status_code not in _RETRYABLE_STATUS_CODES:
232
+ if _submissions_counter and method == "POST":
233
+ _submissions_counter.labels(endpoint=path, status="error").inc()
234
+ raise FLOPSClientError(response.status_code, response.text)
235
+
236
+ last_exc = FLOPSClientError(response.status_code, response.text)
237
+
238
+ except httpx.TransportError as exc:
239
+ last_exc = exc
240
+
241
+ backoff = _BACKOFF_BASE * (2 ** attempt)
242
+ logger.warning("Retry %d/%d for %s %s (backoff %.1fs)",
243
+ attempt + 1, _MAX_RETRIES, method, path, backoff)
244
+ await asyncio.sleep(backoff)
245
+
246
+ if _submissions_counter and method == "POST":
247
+ _submissions_counter.labels(endpoint=path, status="exhausted").inc()
248
+ raise last_exc # type: ignore[misc]
249
+
250
+ async def asubmit_weekly(self, submission: dict) -> dict:
251
+ return await self._arequest("POST", "/submit/fleet", json=submission)
252
+
253
+ async def asubmit_smpi(self, transaction: dict) -> dict:
254
+ return await self._arequest("POST", "/submit/smpi", json=transaction)
255
+
256
+ async def asubmit_clri(self, submission: dict) -> dict:
257
+ return await self._arequest("POST", "/submit/clri", json=submission)
258
+
259
+ async def aget_index(
260
+ self,
261
+ index_id: str,
262
+ start: Optional[str] = None,
263
+ end: Optional[str] = None,
264
+ ) -> dict:
265
+ params: dict[str, str] = {}
266
+ if start:
267
+ params["start"] = start
268
+ if end:
269
+ params["end"] = end
270
+ return await self._arequest("GET", f"/indices/{index_id}", params=params)
271
+
272
+ async def aget_health(self) -> dict:
273
+ return await self._arequest("GET", "/health")
274
+
275
+ async def aclose(self) -> None:
276
+ if self._async_client:
277
+ await self._async_client.aclose()
278
+ self._async_client = None
279
+
280
+ async def __aenter__(self) -> "FLOPSClient":
281
+ self._ensure_async_client()
282
+ return self
283
+
284
+ async def __aexit__(self, *exc: Any) -> None:
285
+ await self.aclose()
286
+
287
+
288
+ __all__ = ["FLOPSClient", "FLOPSClientError"]
@@ -0,0 +1,136 @@
1
+ """Deprecation-shim tests.
2
+
3
+ Pins two halves of the rebrand contract:
4
+
5
+ 1. `import flops_client` (the legacy distribution name) MUST emit a
6
+ DeprecationWarning at import time so partners discover the rename
7
+ when they re-pip from a clean environment.
8
+ 2. The shim MUST still re-export every write method (submit_weekly,
9
+ submit_smpi, submit_clri) so Modular's already-deployed integration
10
+ keeps working through the deprecation window. Silently breaking
11
+ the WRITE surface is the failure mode this test exists to catch.
12
+
13
+ Tests use importlib.reload + warnings.catch_warnings so the warning
14
+ fires inside the test scope even if another test imported the modules
15
+ earlier in the session.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import importlib
20
+ import sys
21
+ import warnings
22
+ from pathlib import Path
23
+
24
+ import pytest
25
+
26
+ # Make the sibling packages importable without `pip install -e .`
27
+ _SDK_ROOT = Path(__file__).resolve().parents[2]
28
+ if str(_SDK_ROOT) not in sys.path:
29
+ sys.path.insert(0, str(_SDK_ROOT))
30
+
31
+
32
+ def _purge(*module_prefixes: str) -> None:
33
+ """Drop cached modules so the next import re-fires the warning."""
34
+ for key in list(sys.modules):
35
+ for prefix in module_prefixes:
36
+ if key == prefix or key.startswith(prefix + "."):
37
+ del sys.modules[key]
38
+ break
39
+
40
+
41
+ def test_flops_client_import_emits_deprecation_warning():
42
+ """`from flops_client import FLOPSClient` must warn."""
43
+ _purge("flops_client", "flopsindex_partner")
44
+ with warnings.catch_warnings(record=True) as caught:
45
+ warnings.simplefilter("always")
46
+ import flops_client # noqa: F401
47
+ deprecation_warnings = [w for w in caught
48
+ if issubclass(w.category, DeprecationWarning)
49
+ and "flops_client" in str(w.message)]
50
+ assert deprecation_warnings, (
51
+ "Importing flops_client must emit a DeprecationWarning pointing at "
52
+ "flopsindex_partner; none found."
53
+ )
54
+
55
+
56
+ def test_flops_client_client_module_import_emits_warning():
57
+ """`from flops_client.client import FLOPSClient` must also warn —
58
+ Modular may have pinned the deeper import path."""
59
+ _purge("flops_client", "flopsindex_partner")
60
+ with warnings.catch_warnings(record=True) as caught:
61
+ warnings.simplefilter("always")
62
+ import flops_client.client # noqa: F401
63
+ deprecation_warnings = [w for w in caught
64
+ if issubclass(w.category, DeprecationWarning)]
65
+ assert deprecation_warnings, (
66
+ "Importing flops_client.client must emit a DeprecationWarning; "
67
+ "none found."
68
+ )
69
+
70
+
71
+ def test_shim_reexports_FLOPSClient_with_write_methods():
72
+ """The shim MUST re-export FLOPSClient with submit_weekly /
73
+ submit_smpi / submit_clri intact. Silently breaking the WRITE
74
+ surface is the failure mode this test pins."""
75
+ _purge("flops_client", "flopsindex_partner")
76
+ with warnings.catch_warnings():
77
+ warnings.simplefilter("ignore", DeprecationWarning)
78
+ from flops_client import FLOPSClient # noqa
79
+ # Class-level method discovery — no network call.
80
+ for method_name in ("submit_weekly", "submit_smpi", "submit_clri",
81
+ "asubmit_weekly", "asubmit_smpi", "asubmit_clri",
82
+ "get_index", "aget_index",
83
+ "get_health", "aget_health"):
84
+ assert hasattr(FLOPSClient, method_name), (
85
+ f"FLOPSClient lost {method_name}() during rebrand — the shim "
86
+ "did not re-export the full write surface."
87
+ )
88
+
89
+
90
+ def test_shim_FLOPSClient_is_canonical_class():
91
+ """The class re-exported through the shim MUST be the same identity
92
+ as the canonical class — no shadow class that drifts."""
93
+ _purge("flops_client", "flopsindex_partner")
94
+ with warnings.catch_warnings():
95
+ warnings.simplefilter("ignore", DeprecationWarning)
96
+ from flops_client import FLOPSClient as ShimClass
97
+ from flopsindex_partner import FLOPSClient as CanonClass
98
+ assert ShimClass is CanonClass, (
99
+ "flops_client.FLOPSClient and flopsindex_partner.FLOPSClient must "
100
+ "be the SAME class object. If they diverge, partners pinning the "
101
+ "old import path will silently miss new methods + fixes."
102
+ )
103
+
104
+
105
+ def test_canonical_user_agent_rebranded():
106
+ """The User-Agent string must reflect the rebrand so traffic auditing
107
+ can distinguish v0.2.0+ callers from legacy flops-client 0.1.0."""
108
+ _purge("flops_client", "flopsindex_partner")
109
+ from flopsindex_partner.client import _USER_AGENT
110
+ assert _USER_AGENT.startswith("flopsindex-partner/"), (
111
+ f"USER_AGENT not rebranded: {_USER_AGENT!r}"
112
+ )
113
+ assert "0.2" in _USER_AGENT # tolerates patch bumps
114
+
115
+
116
+ def test_version_pinned_to_0_2():
117
+ _purge("flopsindex_partner")
118
+ import flopsindex_partner
119
+ importlib.reload(flopsindex_partner)
120
+ assert flopsindex_partner.__version__.startswith("0.2"), (
121
+ f"flopsindex_partner.__version__ should start with 0.2, got "
122
+ f"{flopsindex_partner.__version__!r}"
123
+ )
124
+
125
+
126
+ def test_flops_client_exports_FLOPSClientError():
127
+ """FLOPSClientError MUST be re-exported through both shim levels —
128
+ partners catch this exception by name."""
129
+ _purge("flops_client", "flopsindex_partner")
130
+ with warnings.catch_warnings():
131
+ warnings.simplefilter("ignore", DeprecationWarning)
132
+ from flops_client import FLOPSClientError as ShimErr
133
+ from flops_client.client import FLOPSClientError as DeepShimErr
134
+ from flopsindex_partner import FLOPSClientError as CanonErr
135
+ assert ShimErr is CanonErr
136
+ assert DeepShimErr is CanonErr
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: flopsindex-partner
3
+ Version: 0.2.0
4
+ Summary: Partner-tier write SDK for the FLOPS Compute Intelligence Platform — submit fleet / SMPI / CLRI data. Companion to the public-read SDK at https://pypi.org/project/flopsindex/.
5
+ Author-email: Ash Chary <ash@flopsindex.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://flopsindex.com
8
+ Project-URL: ReadSDK, https://pypi.org/project/flopsindex/
9
+ Project-URL: MCPServer, https://pypi.org/project/flopsindex-mcp/
10
+ Keywords: flops,compute,partner,submission,fleet,smpi,clri
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: httpx>=0.26.0
14
+ Provides-Extra: metrics
15
+ Requires-Dist: prometheus-client>=0.20.0; extra == "metrics"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
19
+ Requires-Dist: respx>=0.21.0; extra == "dev"
20
+
21
+ # flopsindex-partner — partner write SDK
22
+
23
+ [![PyPI version](https://img.shields.io/pypi/v/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
24
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
25
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/flopsindex-partner.svg)](https://pypi.org/project/flopsindex-partner/)
26
+
27
+ ```bash
28
+ pip install flopsindex-partner
29
+ ```
30
+
31
+ Authenticated **write-side** SDK for contributing partners (Modular, the
32
+ lender-wave anchors, future fleet operators) submitting fleet / SMPI / CLRI
33
+ data into the FLOPS Compute Intelligence Platform.
34
+
35
+ For the **read-side** (price / verify / catalog / methodology / timeseries
36
+ / compute_margin / spread) install the companion package
37
+ [`flopsindex`](https://pypi.org/project/flopsindex/) — different audience,
38
+ different brand, no API key required for the public surface.
39
+
40
+ ## 30-second example
41
+
42
+ ```python
43
+ from flopsindex_partner import FLOPSClient
44
+
45
+ c = FLOPSClient(api_key="flops_xxxxxxxxx")
46
+
47
+ # Weekly fleet snapshot
48
+ c.submit_weekly({
49
+ "partner_id": "modular",
50
+ "as_of": "2026-05-19T00:00:00Z",
51
+ "gpus": [{"sku": "h100_sxm5", "region": "us_east", "count": 128}, ...],
52
+ })
53
+
54
+ # Single-machine pricing index event
55
+ c.submit_smpi({
56
+ "partner_id": "modular",
57
+ "sku": "h100_sxm5",
58
+ "region": "us_east",
59
+ "price_usd": 2.42,
60
+ "tier": "on_demand",
61
+ "ts": "2026-05-19T22:00:00Z",
62
+ })
63
+
64
+ # CLRI lease-rate submission
65
+ c.submit_clri({
66
+ "partner_id": "modular",
67
+ "sku": "h100_sxm5",
68
+ "tenor": "P36M",
69
+ "implied_rate_pct": 11.4,
70
+ "as_of": "2026-05-19T00:00:00Z",
71
+ })
72
+ ```
73
+
74
+ ## Async surface
75
+
76
+ Every method has an `a`-prefixed async sibling:
77
+
78
+ ```python
79
+ import asyncio
80
+ from flopsindex_partner import FLOPSClient
81
+
82
+ async def main():
83
+ async with FLOPSClient(api_key="...") as c:
84
+ await c.asubmit_smpi({...})
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ## Renamed from `flops-client` (2026-05-19)
90
+
91
+ This package was previously published as `flops-client`. Old imports
92
+ continue to work but emit `DeprecationWarning`:
93
+
94
+ ```python
95
+ # OLD — deprecated, still works
96
+ from flops_client import FLOPSClient
97
+
98
+ # NEW — canonical
99
+ from flopsindex_partner import FLOPSClient
100
+ ```
101
+
102
+ The PyPI distribution name also changed (`flops-client` →
103
+ `flopsindex-partner`). Update your `requirements.txt`:
104
+
105
+ ```diff
106
+ - flops-client==0.1.0
107
+ + flopsindex-partner>=0.2.0
108
+ ```
109
+
110
+ The legacy `flops-client` distribution on PyPI will be marked deprecated
111
+ in a follow-up release; it will continue to install but won't receive
112
+ updates. The recommended deadline for migration is **2026-12-31**.
113
+
114
+ ## Authentication
115
+
116
+ API keys are issued by FLOPS partner ops. Email `partners@flopsindex.com`
117
+ to onboard. Once issued:
118
+
119
+ ```bash
120
+ export FLOPS_API_KEY="flops_xxxxxxxxx"
121
+ ```
122
+
123
+ ```python
124
+ import os
125
+ from flopsindex_partner import FLOPSClient
126
+
127
+ c = FLOPSClient(api_key=os.environ["FLOPS_API_KEY"])
128
+ ```
129
+
130
+ ## Submission contracts
131
+
132
+ The schemas for `submit_weekly` / `submit_smpi` / `submit_clri` live in
133
+ the Submission Guide (latest at
134
+ `https://app.flopsindex.com/v1/methodology/submission-guide`).
135
+ Each method returns the server's receipt envelope:
136
+
137
+ ```python
138
+ result = c.submit_smpi({...})
139
+ # {'receipt_id': '...', 'received_at': '...', 'methodology_version': '...',
140
+ # 'k_anon_floor_met': True, 'inputs_hash': 'sha256:...'}
141
+ ```
142
+
143
+ Hold onto `receipt_id` + `inputs_hash` — they're the audit trail.
144
+
145
+ ## Errors
146
+
147
+ `FLOPSClientError` is raised on non-retryable 4xx + exhausted 5xx
148
+ retries. The SDK retries 429/500/502/503/504 up to 3 times with
149
+ exponential backoff before surfacing.
150
+
151
+ ```python
152
+ from flopsindex_partner import FLOPSClient, FLOPSClientError
153
+
154
+ try:
155
+ c.submit_smpi({...})
156
+ except FLOPSClientError as e:
157
+ print(f"HTTP {e.status_code}: {e.detail}")
158
+ ```
159
+
160
+ ## Optional metrics
161
+
162
+ If `prometheus-client` is installed (`pip install flopsindex-partner[metrics]`),
163
+ the SDK emits:
164
+
165
+ | Metric | Labels |
166
+ |--------|--------|
167
+ | `flops_sdk_submissions_total` | `endpoint`, `status` (`ok` / `error` / `exhausted`) |
168
+ | `flops_sdk_request_seconds` | `endpoint` |
169
+
170
+ Scrape via the standard Prometheus exporter.
171
+
172
+ ## Related
173
+
174
+ - **Read SDK:** `pip install flopsindex` ([PyPI](https://pypi.org/project/flopsindex/))
175
+ - **MCP server:** `pip install flopsindex-mcp` ([PyPI](https://pypi.org/project/flopsindex-mcp/))
176
+ - **Schema (JSON-LD):** [`schema.flopsindex.com/compute-index-spec/v0.1/`](https://schema.flopsindex.com/compute-index-spec/v0.1/)
177
+ - **Verify endpoint:** `GET /v1/verify?index_id=<ID>&value=<v>`
178
+ - **Methodology library:** [`/v1/methodology`](https://app.flopsindex.com/v1/methodology)
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ flops_client/__init__.py
4
+ flops_client/client.py
5
+ flopsindex_partner/__init__.py
6
+ flopsindex_partner/client.py
7
+ flopsindex_partner.egg-info/PKG-INFO
8
+ flopsindex_partner.egg-info/SOURCES.txt
9
+ flopsindex_partner.egg-info/dependency_links.txt
10
+ flopsindex_partner.egg-info/requires.txt
11
+ flopsindex_partner.egg-info/top_level.txt
12
+ flopsindex_partner/tests/test_deprecation.py
@@ -0,0 +1,9 @@
1
+ httpx>=0.26.0
2
+
3
+ [dev]
4
+ pytest>=8.0.0
5
+ pytest-asyncio>=0.23.0
6
+ respx>=0.21.0
7
+
8
+ [metrics]
9
+ prometheus-client>=0.20.0
@@ -0,0 +1,2 @@
1
+ flops_client
2
+ flopsindex_partner
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flopsindex-partner"
7
+ version = "0.2.0"
8
+ description = "Partner-tier write SDK for the FLOPS Compute Intelligence Platform — submit fleet / SMPI / CLRI data. Companion to the public-read SDK at https://pypi.org/project/flopsindex/."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "Proprietary"}
12
+ authors = [{name = "Ash Chary", email = "ash@flopsindex.com"}]
13
+ keywords = ["flops", "compute", "partner", "submission", "fleet", "smpi", "clri"]
14
+
15
+ dependencies = [
16
+ "httpx>=0.26.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ metrics = [
21
+ "prometheus-client>=0.20.0",
22
+ ]
23
+ dev = [
24
+ "pytest>=8.0.0",
25
+ "pytest-asyncio>=0.23.0",
26
+ "respx>=0.21.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://flopsindex.com"
31
+ ReadSDK = "https://pypi.org/project/flopsindex/"
32
+ MCPServer = "https://pypi.org/project/flopsindex-mcp/"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["."]
36
+ # Ship BOTH packages so legacy `from flops_client import ...` keeps working
37
+ # via the deprecation shim alongside the canonical `flopsindex_partner`.
38
+ include = ["flopsindex_partner*", "flops_client*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+