Skip to content

Latest commit

 

History

History
154 lines (109 loc) · 9.54 KB

AGENTS.md

File metadata and controls

154 lines (109 loc) · 9.54 KB

AGENTS.md

Repository overview

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.

Repository layout

  • fit_tool/: handwritten library code and CLI implementation.
  • fit_tool/cli.py: implementation of the fit-tool console command.
  • fit_tool/tests/: pytest suite. Most tests use unittest.TestCase style and committed fixtures under fit_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 in README.md.
  • fit_tool/gen/: Garmin profile parser, source generator, Jinja templates, and the source SDK profile spreadsheet.
  • fit_tool/profile/profile_type.py and fit_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.

Setup and common commands

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 build

Runtime 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_tool

Do 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.

Generated profile code

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-profile

gen-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.

Message construction paths

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 or GenericMessage)
  • DataMessage.from_definition(...) — delegates to MessageFactory
  • MessageClass.from_bytes(definition, developer_fields, bytes, offset) — uses from_definition then 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.

Implementation guidelines

  • 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.Path for 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, or fit_tool/tests/out/.

Tests

  • Put tests in fit_tool/tests/test_*.py so pytest discovers them through the configuration in pyproject.toml.
  • Follow the surrounding test style; both pytest assertions and existing unittest.TestCase patterns 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, or fit_tool/tests/out/; never overwrite committed fixtures under fit_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.

CLI and examples

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.

Changelog and documentation

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.

Releasing

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.

Change discipline

  • Keep changes focused and preserve unrelated user modifications in the working tree.
  • Inspect git diff before 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.