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.
- flopsindex_partner-0.2.0/PKG-INFO +178 -0
- flopsindex_partner-0.2.0/README.md +158 -0
- flopsindex_partner-0.2.0/flops_client/__init__.py +21 -0
- flopsindex_partner-0.2.0/flops_client/client.py +24 -0
- flopsindex_partner-0.2.0/flopsindex_partner/__init__.py +26 -0
- flopsindex_partner-0.2.0/flopsindex_partner/client.py +288 -0
- flopsindex_partner-0.2.0/flopsindex_partner/tests/test_deprecation.py +136 -0
- flopsindex_partner-0.2.0/flopsindex_partner.egg-info/PKG-INFO +178 -0
- flopsindex_partner-0.2.0/flopsindex_partner.egg-info/SOURCES.txt +12 -0
- flopsindex_partner-0.2.0/flopsindex_partner.egg-info/dependency_links.txt +1 -0
- flopsindex_partner-0.2.0/flopsindex_partner.egg-info/requires.txt +9 -0
- flopsindex_partner-0.2.0/flopsindex_partner.egg-info/top_level.txt +2 -0
- flopsindex_partner-0.2.0/pyproject.toml +38 -0
- flopsindex_partner-0.2.0/setup.cfg +4 -0
|
@@ -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
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
24
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
25
|
+
[](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
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
4
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
5
|
+
[](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
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
24
|
+
[](https://pypi.org/project/flopsindex-partner/)
|
|
25
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|