Skip to main content

trinolio

Tooling over the Nolio API: fetch your training data, keep it locally, analyse it yourself. Unofficial and unaffiliated with Nolio.

Nolio aggregates from Garmin, Coros, Suunto, Polar, Zwift, Strava, Oura, Whoop and more, and normalises across them, so reading from Nolio alone gives device-agnostic data.

Reading is the whole point. The exceptions are the two backfills, which exist because a Nolio account starts the day it is opened and an athlete's history starts years earlier: push-metrics uploads wellness from a local Garmin export, and push-trainings uploads the activity files themselves. Both are for onboarding, not for ongoing use, and neither writes a workout you have already recorded.

Requirements

Setup

Not on PyPI yet, so install from a checkout:

uv sync          # in this directory, for development
uv pip install . # or into an environment of your own

Create a .env with your credentials. It can live in this directory or in any parent, which is where the rotating tokens get written too:

NOLIO_CLIENT_ID=...
NOLIO_CLIENT_SECRET=...
NOLIO_REDIRECT_URI=...

Then authorise once:

trinolio auth-url        # open the URL, approve, copy the code back
trinolio exchange CODE

Getting started

trinolio sync            # pull workouts and wellness metrics into data/
from trinolio import data

runs = data.trainings().filter(discipline="run")
print(runs["duration"].sum() / 3600, "hours running")

Always read through trinolio.data rather than the parquet directly. Nolio writes 0, never null, for what it did not record, and that is the common case rather than the exception. On an archive where most rides have no power meter, raw avg_watt.mean() reads 3.4 W against a true 112 W.

Commands

trinolio <command>, or python -m trinolio <command> if you would rather not rely on the console script being on your PATH. Each one explains itself with --help.

Command
auth-url, exchange, token Authorise once, then let it refresh itself.
sync Pull workouts and wellness metrics into the local parquet files.
athletes List the athletes you coach, with their ids.
wellness Rebuild wellness.parquet from a Garmin export.
push-metrics Trickle archive wellness up into Nolio.
push-trainings Upload an athlete's pre-Nolio history from their export.

Onboarding an athlete who trained before Nolio

Their Garmin or Strava export holds the years Nolio never saw. push-trainings walks it and uploads each FIT or TCX, one per request, pacing itself against the hourly limit and logging what Nolio answered for so an interrupted run resumes:

trinolio athletes                                          # find their id
trinolio push-trainings --athlete-id 123 --dry-run export/ # what would go up
trinolio push-trainings --athlete-id 123 export/           # hours, resumable

Read --help before the real run. Nolio matches uploads on a key trinolio derives from the file, so re-running is safe, but that key does not exist on the sessions their own watch already synced: uploading across a range Nolio holds duplicates it, and there is no delete route. The command refuses that case unless you pass --overlap. GPX is reported and skipped, since the API takes only FIT and TCX even though the web UI accepts GPX by hand.

Reading the data

data.trainings()   # one row per workout, unrecorded values as null
data.wellness()    # your export and Nolio's connector as one series
data.zones()       # time in zone, long: nolio_id, date_start, sport_id, channel, min, max, duration, zone

Each has a docstring covering what it decodes and what to watch out for. Units are Nolio's own, unconverted: distance in kilometres, elevation_* in metres, duration in seconds. date_start and hour_start are your local wall clock rather than UTC.

The module holds facts about how Nolio encodes things, not analysis. A weekly load or a fitness curve is a question, and questions belong in a notebook. trinolio.store exposes the raw paths and schemas if you want the file untouched.

Notes

NOTES.md has the reference material: where trinolio looks for your files, the rate limits, and what the API actually does where that differs from the official wiki.

Contributing

See CONTRIBUTING.md.

Versioning

CalVer, YY.M, so 26.8 is August 2026 and a patch inside that month is 26.8.1. The version says when this was last checked against Nolio's live API, which is the thing most likely to break it, and says nothing about whether an upgrade breaks your code. The reading API is small and I do not plan to churn it, but treat a change in the month as something to read the release notes for rather than as a compatibility promise.

License

Apache License 2.0, see LICENSE.

Metadata

Release files for trinolio 26.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for trinolio 26.8
File Size Uploaded
trinolio-26.8.tar.gz 64.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for trinolio 26.8
File Interpreter ABI Platform
trinolio-26.8-py3-none-any.whl Python 3 none any Details

Total release size: 94.6 kB

Release files / trinolio-26.8.tar.gz

Download URL trinolio-26.8.tar.gz
Size 64.1 kB
Tags Source
SHA-256 checksum
How to use checksums
7ef1054d0a6e17509e6def280313fda6f102d6a8b1d81fd8f45d6dc30545bd04
BLAKE2b-256 checksum
How to use checksums
03f3d74d3e01a06bb582fe0f9c6377610097495bf38cd52fe480f8ef7cab9242
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Release files / trinolio-26.8-py3-none-any.whl

Download URL trinolio-26.8-py3-none-any.whl
Size 30.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
34786253e91b54b14bc933c57a4cf91c5303080bb276dcfd38555108554a4482
BLAKE2b-256 checksum
How to use checksums
0bcf2e31427c21d642eaccf76603613789280735c90ab9663612aeca7a3e3aed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

26.8 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page