This repository contains fit-tool, a Python library and command-line tool for reading, writing, and converting Garmin FIT files. The package supports Python 3.9 and newer (requires-python = ">=3.9"); keep handwritten code compatible with 3.9 even when developing on a newer interpreter.
The project uses setuptools through pyproject.toml, uv for dependency and lock-file management, pytest for tests, and Towncrier news fragments for release notes.
fit_tool/: handwritten library code and CLI implementation.fit_tool/cli.py: implementation of thefit-toolconsole command.fit_tool/tests/: pytest suite. Most tests useunittest.TestCasestyle and committed fixtures underfit_tool/tests/data/.fit_tool/tests/out/: generated test/example output; it is ignored by Git and must not be committed.fit_tool/examples/: runnable examples. Some examples require optional third-party packages documented inREADME.md.fit_tool/gen/: Garmin profile parser, source generator, Jinja templates, and the source SDK profile spreadsheet.fit_tool/profile/profile_type.pyandfit_tool/profile/messages/: generated profile code.news/: Towncrier fragments named<issue>.<type>..github/workflows/: CI and PyPI release workflows.docs/RELEASING.md: maintainer checklist for tagging and publishing a new version.
Run commands from the repository root. Prefer uv so that commands use the locked environment.
uv sync --group dev
uv run pytest
uv run pytest fit_tool/tests/test_fit_file.py
uv run pytest fit_tool/tests/test_fit_file.py::TestFitFile::test_conversion_simple
uv run pytest --cov=fit_tool --cov-report=term
uv buildRuntime install of fit-tool has no third-party dependencies. Profile generation
uses the optional [gen] extra (openpyxl, inflection, jinja2). The dev
dependency group already includes those packages so uv sync --group dev supports
the full suite and uv run gen-profile. For a published package, install with
pip install 'fit-tool[gen]' or uv add 'fit-tool[gen]'.
The CI baseline is a package build plus the full test suite on Python 3.9 through 3.14. Before finishing a normal code change, run the narrowest relevant tests and then the full suite when practical. Run uv build for packaging, entry-point, dependency, or metadata changes.
Ruff is a CI gate on the Python 3.12 job (ruff check fit_tool). Mypy is configured in pyproject.toml and runs in a limited form in CI; core modules are not all under strict mypy yet. Locally:
uv run ruff check fit_tool
uv run mypy fit_toolDo not update uv.lock unless dependencies or dependency groups intentionally changed. Use uv lock or an appropriate uv add/uv remove command for intentional dependency changes; never edit the lock file by hand.
Files beginning with # Autogenerated. Do not modify. must not be edited directly. In particular, do not hand-edit:
fit_tool/profile/profile_type.py- anything under
fit_tool/profile/messages/
Change the generator inputs or templates under fit_tool/gen/, update fit_tool/SDK_VERSION in fit_tool/__init__.py when upgrading the Garmin SDK profile, and regenerate with:
uv run gen-profilegen-profile requires the gen extra (or an equivalent uv sync --group dev).
Without those packages it exits with a clear install hint rather than a raw
import traceback.
The generator deletes and rebuilds the entire fit_tool/profile/ directory. Before running it, make sure the intended spreadsheet is present in fit_tool/gen/ and the working tree contains no uncommitted manual work in the generated directory. Review the complete generated diff and run at least fit_tool/tests/test_profile.py followed by the full test suite.
Keep generator changes and their generated output in the same change. Do not apply broad formatting or lint fixes to generated files; adjust the Jinja templates instead.
Generated profile messages have two explicit construction modes. Do not
reintroduce a dual-mode __init__(definition_message=None, ...) that switches
growable/size behavior from a nullable definition argument.
| Path | Call | Purpose |
|---|---|---|
| Create (authoring) | RecordMessage() / RecordMessage(local_id=..., endian=..., developer_fields=...) |
Blank growable fields for writing new data |
| Project (decode) | RecordMessage.from_definition(definition, developer_fields=...) |
Wire definition projection; fixed field sizes, not growable |
Decode and factory entry points:
MessageFactory.from_definition(definition, developer_fields)— polymorphic (typed class orGenericMessage)DataMessage.from_definition(...)— delegates to MessageFactoryMessageClass.from_bytes(definition, developer_fields, bytes, offset)— usesfrom_definitionthen reads payload
GenericMessage has no blank create path (unknown global IDs only exist as
definition projections). Prefer GenericMessage.from_definition(...); its
__init__(definition_message, ...) remains an alias.
When changing construction, edit fit_tool/gen/templates/template_message.jinja
and template_message_factory.jinja, then regenerate. Document public usage in
README.md if the authoring/decode story changes for application callers.
- Preserve the public behavior of FIT parsing, serialization, CRC validation, field scaling, developer fields, and local message definitions unless the change explicitly alters that behavior.
- Treat FIT byte layout and round-trip fidelity as core invariants. For parser or writer changes, add tests that cover both decoding and encoding, and compare serialized bytes where appropriate.
- Use existing abstractions such as
FitFile,FitFileBuilder,Record,DefinitionMessage,DataMessage, and the generated message classes instead of duplicating protocol logic. - Match the style of the surrounding handwritten module. New code should satisfy the repository's Ruff configuration (120-character line limit and Python 3.9 target) without sweeping unrelated cleanup.
- Add type annotations to new or substantially changed handwritten functions. Avoid syntax or standard-library APIs unavailable on Python 3.9.
- Use
pathlib.Pathfor new filesystem code where it makes the code clearer, but do not rewrite stable code solely to change path style. - Keep optional example-only dependencies out of the core dependency list unless the library itself starts importing them.
- Do not commit generated artifacts from
dist/, coverage reports, egg metadata, virtual environments, orfit_tool/tests/out/.
- Put tests in
fit_tool/tests/test_*.pyso pytest discovers them through the configuration inpyproject.toml. - Follow the surrounding test style; both pytest assertions and existing
unittest.TestCasepatterns are acceptable. - Reuse small committed FIT/CSV fixtures when they exercise the required behavior. Add binary fixtures only when constructing the case in a test would be less clear or would fail to reproduce a real interoperability issue.
- Resolve fixture paths relative to the test module, not the caller's current directory.
- Write temporary output to pytest temporary directories,
tempfile, orfit_tool/tests/out/; never overwrite committed fixtures underfit_tool/tests/data/. - For bug fixes, add a regression test that fails before the fix and passes afterward.
- Keep CRC checking enabled in tests unless the test specifically exercises corrupt, truncated, or deliberately mismatched data.
The installed entry points are fit-tool = fit_tool.cli:main and gen-profile = fit_tool.gen.gen_profile:main. For CLI changes, test argument parsing, exit behavior, and produced files through fit_tool/tests/test_cli.py. Keep the usage text in README.md synchronized when user-facing options change.
The basic read, modify, and workout examples are intended to run from the repository root. The activity/course examples have additional dependencies; consult README.md before using them as validation.
User-visible fixes, features, deprecations, removals, and dependency changes should include a short Towncrier fragment in news/. Use the issue number and one of the configured suffixes:
.feature.bugfix.break.doc.dep.removal.misc
Write fragments as concise, user-facing descriptions. Do not edit a released section of CHANGELOG.md for an unreleased change, and do not build/finalize the Towncrier changelog unless preparing a release.
Update README.md when installation, CLI usage, supported workflows, or runnable examples change. Keep packaging metadata and console entry points in pyproject.toml aligned with the code and documentation.
Package version is static in pyproject.toml ([project].version). Git tags use a v prefix (v0.9.15) and must match that version (without v). Pushing a matching v* tag runs the Release workflow (PyPI via Trusted Publishing + GitHub Release from CHANGELOG.md).
Full checklist, tag rules, and failure notes: docs/RELEASING.md.
- Keep changes focused and preserve unrelated user modifications in the working tree.
- Inspect
git diffbefore finishing, especially after profile generation or formatter runs. - Report the exact checks run and any checks that could not be run.
- Never publish to PyPI, create a GitHub release, push tags, or otherwise invoke the release workflow unless the user explicitly requests it.