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
- Python 3.13+
- uv, for development
- A Nolio account and a developer app from https://www.nolio.io/developers/, which gives you a
client_idandclient_secret.
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)
| File | Size | Uploaded | |
|---|---|---|---|
| trinolio-26.8.tar.gz | 64.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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