Metadata-Version: 2.3
Name: grafit
Version: 1.37.0
Summary: Garmin FIT file loader for SQLite + Grafana
Requires-Dist: python-fitparse>=2.3.0
Requires-Dist: pytest>=7.0 ; extra == 'dev'
Requires-Dist: black>=23.0 ; extra == 'dev'
Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/nbr23/grafit
Project-URL: Repository, https://github.com/nbr23/grafit
Project-URL: Issues, https://github.com/nbr23/grafit/issues
Provides-Extra: dev
Description-Content-Type: text/markdown

# GraFIT

Loads Garmin FIT files into a SQLite database and visualizes them in Grafana.

GraFIT parses `.fit` files (activities, sleep, GPS, etc.) using [python-fitparse](https://github.com/nbr23/python-fitparse), writes them to SQLite using a schema generated from the FIT profile, and ships with Grafana provisioning (datasource + dashboards) so you can browse the data immediately.

## Install

```
uv tool install grafit
```

Or run without installing:

```
uvx grafit <command> [options]
```

Requires Python 3.11+.

## Usage

```
uv run grafit <command> [options]
```

### Commands

- `init <db_path>` — create and initialize the SQLite database.
- `import <path> <db_path>` — import a FIT file or directory of FIT files into the database.
  - `--force` reimport files already present in the database.
  - `--no-recursive` do not recurse into subdirectories.
  - `--fetch-weather` look up historical hourly weather (temperature, apparent temperature, humidity) for each session's start coordinates and date from the [Open-Meteo](https://open-meteo.com) archive. Failures are logged and skipped — the FIT import itself never fails because of weather.
- `backfill-weather <db_path>` — fetch weather for sessions already imported without `--fetch-weather`. Safe to re-run: only sessions missing weather are queried; rate-limited or failed sessions are picked up on the next run.
  - `--force` re-fetch weather even for sessions that already have it.
- `stats <db_path>` — show row counts per table.
- `purge <db_path> --file-hash <sha256>` — remove all rows imported from a given file.
- `generate-schema <output>` — write the full DDL SQL to a file.
- `install-provisioning <dest>` — copy the bundled Grafana provisioning (datasource + dashboards) to a directory.
  - `--force` overwrite the destination if it exists.
- `demo-data <dest>` — generate a synthetic Garmin FIT tree (see [Demo data](#demo-data)).
  - `--days` days of history (default 365), `--end-date YYYY-MM-DD` last day (default yesterday), `--seed` random seed, `--force` write into a non-empty directory.
- `demo-weather <db_path>` — fill `weather_hourly` with modelled Los Angeles weather for demo data, without calling Open-Meteo.

Global flag: `-v` / `--verbose` for debug logging.

### Upgrading

The schema is generated from the FIT profile, so upgrading GraFIT (or its `python-fitparse` dependency) can add new fields. On the next `import`/`init`, missing columns are added to existing tables automatically (you'll see a `Schema: added N new column(s)` notice). New columns are NULL for already-imported rows — to backfill history, re-import the affected files with `--force` or rebuild the database from your FIT source.

## Grafana

A `docker-compose.yml` is provided to run Grafana with the SQLite datasource plugin against `./fit_data.db` and the bundled dashboards mounted from `./grafana-provisioning`.

> **Note:** the bundled compose file enables anonymous admin access and disables the login form. It is intended for local use only — do not expose it to a network.

Drop your FIT files into `./GARMIN` and bring it up:

```
docker compose up
```

A `grafit-init` container installs the bundled provisioning, runs `grafit init`, and runs `grafit import ./GARMIN` against shared volumes; Grafana waits for it to finish before starting. Re-running `docker compose up` reuses the database — already-imported files are skipped by hash.

Grafana is then available at http://localhost:3000.

## Demo data

Don't have a Garmin dump handy (or want to see the dashboards without publishing your own data)? GraFIT can generate a synthetic one for a fictional Los Angeles athlete — a year of activities on real LA routes, sleep, stress, body battery, HRV, SpO2, saved locations and the odd plane spotted overhead:

```
docker compose -f docker-compose.demo.yml up
```

That generates the FIT files into their own volume, imports them into a separate database and starts Grafana on http://localhost:3000 with everything populated. It uses its own volumes, so it never touches the data from `docker-compose.yml`. Set `GRAFIT_DEMO_DAYS=90` to generate less history — a year takes a couple of minutes and about 300 MB of database, 90 days about 77 MB.

Or generate the files yourself and import them like any other FIT directory:

```
uvx grafit demo-data ./demo-garmin --days 90
uvx grafit import ./demo-garmin demo.db
uvx grafit demo-weather demo.db
```

The data is deterministic: the same `--seed`, `--end-date` and `--days` always produce byte-identical files. `--end-date` defaults to yesterday, so pin it if you want a dataset that doesn't move when you regenerate it on another day. Weather is modelled offline from Los Angeles climate normals (`demo-weather`) rather than fetched, so no network is needed and it works for any date range.
