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.0 · updated 2026-09-22

Which one

Hosted API from Pythonpyoxynet package
What runsThe production engine: every analysis on the catalogueThe package's own research models (intensity-domain inference, synthetic CPET generation)
Vendor filesRead as exported, format detectedYou prepare the input table yourself
Where data goesTo app.oxynet.net, see Data handlingNowhere; runs on your machine
AccessAPI keyOpen source (MIT), no key
StatusProductionAvailable

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 maintained separately.

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 it

Minimal

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.

python/quickstart.py
"""
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

Uploadcpet_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}

Field-by-field →

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

Installpip install pyoxynet
Version0.1.12 on PyPI
Python>=3.10
LicenceMIT
Sourcegithub.com/andreazignoli/pyoxynet
Documentationpyoxynet.readthedocs.io

Model inference needs an extra: pip install "pyoxynet[tflite]" (with the extra index URL given in the package README) or pip install "pyoxynet[full]" for TensorFlow. The README's own example:

from the pyoxynet README
import pyoxynet

# Load the TFLite model
tfl_model = pyoxynet.load_tf_model(n_inputs=5, past_points=40, model='CNN')

# Make inference on a random input
pyoxynet.test_pyoxynet(tfl_model)

For partners who need the production engine to run locally rather than through the hosted API, see local and embedded deployment.