Python
Oxynet from Python
Two different things, and it matters which one you want. The hosted engine is reached from Python over plain HTTP. The open-source pyoxynet package runs research models locally and is not a client for the hosted engine.
API v1 · docs 1.6 · updated 2026-10-29
Which one
| Hosted API from Python | pyoxynet package | |
|---|---|---|
| What runs | The production engine: every analysis on the catalogue | Two frozen research models from the papers (intensity domains, VT1 and VT2) and a synthetic CPET generator |
| Vendor files | Read as exported, format detected | You prepare the input table yourself |
| Where data goes | To app.oxynet.net, see Data handling | Nowhere; runs on your machine |
| Access | API key | Open source (MIT), no key |
| Status | Production | Available |
Results from the package are not guaranteed to match the hosted engine: the two are separate codebases, the package does not include the hosted parsers or signal conditioning, and its models are the frozen research models, not the ones the hosted engine runs.
The hosted engine from Python
Not yet available
An Oxynet client package. There is none today, and none is implied by the examples: they use requests against the documented endpoints. Generating a typed client from the OpenAPI schema (for example with openapi-python-client) also works. When an official client ships, it will be documented here.
Install and authenticate
pip install requests
export OXYNET_API_KEY=sk_live_... # never commit itMinimal
import os, requests
BASE = "https://app.oxynet.net"
H = {"X-API-Key": os.environ["OXYNET_API_KEY"]}
with open("oxynet_sample.json", "rb") as f:
rec = requests.post(f"{BASE}/v1/cpet", headers=H, files={"file": f}).json()
out = requests.post(f"{BASE}/v1/cpet/{rec['cpet_id']}/analyze",
headers=H, json={"analyses": ["vt"]}).json()
print(out["results"][0]["findings"])Complete
Capabilities, upload, every analysis the key holds, derived quantities, deletion.
"""
Oxynet quick start: upload a CPET export, analyse it, read the result.
pip install requests
export OXYNET_API_KEY=sk_live_...
python quickstart.py oxynet_sample.json
Works on any supported vendor export, sent unchanged: do not convert units or
rename columns first. Reference: https://www.oxynet.net/developers/api/v1
"""
import os
import sys
import requests
BASE = "https://app.oxynet.net"
session = requests.Session()
session.headers["X-API-Key"] = os.environ["OXYNET_API_KEY"]
def call(method, path, **kwargs):
r = session.request(method, BASE + path, timeout=120, **kwargs)
if not r.ok:
# Every error has the same shape: {"error": CODE, "message": ..., "context": {...}}
err = r.json()
sys.exit(f"{r.status_code} {err['error']}: {err['message']}\n{err.get('context')}")
return r.json()
def main(path):
# 1. What this key may do. Analyses are licensed separately, so ask first.
caps = call("GET", "/v1/capabilities")
print("analyses on this key:", caps["analyses"])
# 2. Upload the file exactly as exported. The format is detected from its content.
with open(path, "rb") as f:
rec = call("POST", "/v1/cpet", files={"file": (os.path.basename(path), f)})
cpet_id = rec["cpet_id"]
print("cpet_id:", cpet_id, "| format:", rec["source"]["format"])
for name, ch in rec["channels"].items():
if not ch.get("present"):
print(f" {name} absent: {ch.get('reason', 'not in this export')}")
# 3. Analyse. One envelope per analysis; one that cannot run does not stop the others.
wanted = [a for a in ("vt", "eov", "substrate") if a in caps["analyses"]]
out = call("POST", f"/v1/cpet/{cpet_id}/analyze", json={"analyses": wanted})
for env in out["results"]:
print(f"\n[{env['analysis']}] status: {env['status']}")
if env["status"] != "ok":
# A refusal is information: it says what was missing and why.
print(" refused:", env["error"], "-", env["message"])
continue
# `quality` describes the RECORDING. It is not a confidence in the answer.
print(" quality of the input:", env["quality"])
if env["analysis"] == "vt":
print(" findings:", env["findings"])
else:
print(" finding fields:", ", ".join(sorted(env["findings"])))
for note in env["notes"]:
print(" note:", note) # caveats that must travel with the numbers
print(" provenance:", env["provenance"])
# 4. Derived quantities and input checks, cheaper than a full analysis.
m = call("POST", f"/v1/cpet/{cpet_id}/compute",
json={"metrics": ["vo2max", "gas_quality", "sampling_adequacy"]})
for name, res in m["metrics"].items():
print(f"\n[{name}]", res["value"] if res["status"] == "ok" else res)
# 5. Delete now rather than waiting for the 24 h expiry.
call("DELETE", f"/v1/cpet/{cpet_id}")
print("\ndeleted", cpet_id)
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "oxynet_sample.json")What comes back
| Upload | cpet_id, source.format, channels (present, coverage, reason), sampling, load, flags, expires_at |
|---|---|
| Analyze | {cpet_id, results: [envelope, ...]} |
| Compute | {cpet_id, metrics: {name: {status, value | error}}, provenance} |
Errors and refusals
An HTTP error means the request failed. A not_analysable envelope inside a 200 means the request worked and the recording cannot support that analysis. Treat them differently.
r = requests.post(f"{BASE}/v1/cpet/{cpet_id}/analyze", headers=H,
json={"analyses": ["vt", "eov"]})
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", "5")) # back off, then retry
elif not r.ok:
err = r.json() # {"error": CODE, "message": ..., "context": {...}}
raise RuntimeError(f"{err['error']}: {err['message']}")
else:
for env in r.json()["results"]:
if env["status"] == "not_analysable":
# Not an exception: the recording cannot support this analysis.
# Keep the reason; it belongs in the dataset.
print(env["analysis"], "refused:", env["error"], env["message"])Versioning
Store provenance with every result. A cohort analysed across a model or analysis version change should be visible as such in your data, not discovered later.
pyoxynet: local research inference
| Install | pip install pyoxynet |
|---|---|
| Version | 1.0.2 on PyPI |
| Python | 3.10 to 3.14; Windows, Linux, macOS (Apple silicon) |
| Models | CNN (default, evaluated in Keir et al., Sports Medicine 2026) and LSTM; frozen |
| Licence | MIT |
| Source | github.com/oxynet-cpet/pyoxynet |
| Documentation | pyoxynet.readthedocs.io |
The models are LiteRT files and run without TensorFlow. The input is a table with the columns time, VO2_I, VCO2_I, VE_I, PetO2_I and PetCO2_I, which you prepare from your export. The README's own example:
import pyoxynet
# The bundled example test, through the default model
out_df, thresholds = pyoxynet.test_pyoxynet()
print(thresholds['VT1']['time'], round(thresholds['VT1']['VO2'])) # 372 2979
print(thresholds['VT2']['time'], round(thresholds['VT2']['VO2'])) # 550 3656For partners who need the production engine to run locally rather than through the hosted API, see local and embedded deployment.