# pyfitparsernative

Fast FIT file parser using Rust, exposed to Python via PyO3.

Parses Garmin FIT activity files using the [`fitparser`](https://crates.io/crates/fitparser) Rust crate. Approximately **8x faster** than `garmin-fit-sdk` (Python).

| Parser | Time (81km / 11,555 records) | Speedup |
|--------|------------------------------|---------|
| garmin-fit-sdk (Python) | ~2.0s | 1x |
| pyfitparsernative (Rust) | ~0.25s | ~8x |

## Installation

```bash
pip install pyfitparsernative
```

No Rust toolchain required — pre-built wheels are provided for Linux (glibc and musl), macOS (Intel and Apple Silicon), and Windows (x64). The macOS Intel (x86_64) wheel is cross-compiled and not tested in CI since GitHub no longer offers Intel macOS runners.

## Usage

```python
from pyfitparsernative import parse_fit_file, parse_fit_bytes

# Parse from a file path
messages = parse_fit_file("activity.fit")

# Parse from bytes
with open("activity.fit", "rb") as f:
    messages = parse_fit_bytes(f.read())
```

**Return type:** `list[dict[str, Any]]`

Returns a flat list of message dicts in original FIT file order. Each dict includes a `"message_type"` key (e.g. `"record"`, `"session"`, `"lap"`) alongside the field data. Timestamps are returned as naive ISO 8601 strings (`YYYY-MM-DDTHH:MM:SS`).

```python
# Example: get average power from session message
session = [m for m in messages if m["message_type"] == "session"][0]
print(session["avg_power"])    # 160
print(session["max_power"])    # 489

# Example: iterate record (data) messages
for msg in messages:
    if msg["message_type"] == "record":
        print(msg["timestamp"], msg.get("power"), msg.get("heart_rate"))
```

## Differences from garmin-fit-sdk

`pyfitparsernative` returns the same field names and numeric values as `garmin-fit-sdk`, with the following differences:

- **Timestamps** — returned as naive ISO 8601 strings instead of timezone-aware `datetime` objects.
- **Enum fields** — returned as raw integers instead of string labels.
- **Message types** — no `_mesgs` suffix (e.g. `"session"` not `"session_mesgs"`).
- **Developer fields** — garmin-fit-sdk uses integer dict keys for developer fields (e.g. `{61: 2554}`); pyfitparsernative uses string keys (`{"unknown_field_61": 2554}`).
- **Component field expansion** — garmin-fit-sdk derives `speed` and `altitude` from the more precise `enhanced_speed`/`enhanced_altitude` fields when the base fields are absent from the message definition. pyfitparsernative applies the same expansion, so `"speed"` and `"altitude"` are always present in `record` messages alongside their `enhanced_` counterparts.
- **Unknown message types** — fitparser may not recognise every global message number in its profile. Unknown types that are known to the FIT SDK are remapped to their standard names (e.g. message number 13 → `"training_settings"`).

## Building from source

The `docker/` directory contains scripts to build and test inside containers, avoiding the need for a local Rust toolchain. These are for local development only; CI/CD uses its own pipeline.

**Build** — compiles the wheel inside `rust:1.93.1-trixie` and writes it to `dist/`:

```bash
bash docker/docker-build.sh
```

**Test** — runs the test suite inside `python:3.14.3-trixie` against the wheel in `dist/`:

```bash
bash docker/docker-test.sh
```

On Windows, run these from Git Bash or WSL. Docker Desktop must be running with Linux containers enabled.

## Releasing

Push a tag to trigger the CI/CD pipeline, which builds wheels for all platforms and publishes to PyPI:

```bash
git tag v0.1.0 && git push --tags
```

See `.github/workflows/CI.yml` for the full build matrix.
