Skip to content

Make TCX/GPX parsing schema-aware - #2

Merged
tabishm52 merged 11 commits into
mainfrom
schema-aware-tcx-gpx-parsing
Jul 5, 2026
Merged

tabishm52 merged 11 commits into
mainfrom
schema-aware-tcx-gpx-parsing

Conversation

@tabishm52

@tabishm52 tabishm52 commented Jul 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rewrites TCX/GPX parsing (parse_tcx_gpx.py) to be schema-derived. Elements/attributes are matched against declarative field tables (xml_fields.py) transcribed from the TCX v2, GPX 1.0/1.1, and known Garmin/Cluetrust extension schemas, rather than matched by bare localname substrings.

  • Namespace-aware matching replaces the old {*}-wildcard/localname-substring approach, eliminating cross-namespace collision risk.
  • Unrecognized elements/attributes are still collected (not dropped), now namespace-qualified to avoid the collisions the old approach was prone to.
  • Column-name collisions (two different source paths mapping to the same name) no longer silently drop the loser — the second falls back to a full-path column name.
  • Mixed-content elements (text + children) no longer silently drop the children.
  • xsi:type skip is now namespace-aware, rather than matching any attribute merely named type.
  • A leaf reset to no namespace (a real TrainerRoad TCX export quirk) is retried under its immediate parent's namespace before falling back to unrecognized.
  • gpx_records_selector trimmed to drop GPS-fix-quality diagnostics and static descriptive labels that aren't fitness data (still available via low-level parse_gpx).

Test plan

  • uv run pytest — 66 passing
  • uv run ruff check / ruff format --check clean
  • uv run pyright clean
  • Verified against real Strava archive data

tabishm52 and others added 11 commits July 4, 2026 19:47
Replace the namespace-blind XML flatten + name-heuristic numeric coercion
with declarative field tables derived from the TCX/GPX/extension XSDs
(xml_fields.py). parse_tcx/parse_gpx now emit canonical column names
directly, unrecognized elements are kept as namespace-qualified string
columns instead of colliding or being dropped, and scripts/inspect_schemas.py
audits a file or directory for schema coverage and coercion failures.

This is Claude's implementation against the agreed plan, not yet reviewed
by a human. Expect follow-up commits addressing review feedback before
this branch is merged via PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
XmlField.convert now holds a callable (Series -> Series) instead of an
enum tag that build_dataframe branched on. NUMERIC/NUMERIC_M_TO_KM/
NUMERIC_MS_TO_KMH/to_datetime are the converters themselves, collapsing
build_dataframe's dispatch to a single call per column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trim walk_fields' docstring to its contract instead of restating
FieldPath's own type documentation and _walk's internals. Add short
comments/docstrings to _walk, _row_from_fields, and _index_by_time
covering the xsi:type skip, the leaf-vs-container assumption, and each
private helper's job.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The audit script adds review surface area beyond the core schema-aware
parsing change. Split it out to inspect-schemas-script (stacked on this
branch) as a follow-up PR; drop its now-unused NUMERIC_CONVERTERS export
and the pyright extraPaths entry it needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that inspect_schemas.py (its other consumer) lives on its own
branch, column_converts had exactly one caller within this branch;
fold its collapsing loop directly into build_dataframe instead of
keeping it as a separately named/tested function.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hdop/vdop/pdop/satellites/fix_type/dgps_*/magnetic_variation/
geoid_height and the static text labels (name/cmt/desc/src/sym/type/
url/urlname) are schema-known but aren't fitness data, so ActivityParser
no longer surfaces them by default. They're still available via the
low-level parse_gpx for anyone who needs them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… type attrs

Reviewing test coverage surfaced three ways parse_tcx/parse_gpx could silently
lose data despite the "unrecognized extensions aren't dropped" design goal:
unrelated elements sharing a leaf name collided into one column, elements with
both text and children lost the children, and any attribute merely named
"type" was skipped regardless of namespace. Colliding paths now fall back to a
full-path column name instead of overwriting, mixed content keeps both text
and children, and only genuine xsi:type is skipped.
…path

extract_xml_fields (used for the extra metadata dict) had the identical
namespace-blind "type" attribute check as _walk, just in a separate code path.
TrainerRoad's TCX export resets AvgSpeed to no namespace even though its LX
wrapper is correctly namespaced, so it fell through as an unrecognized
column. When a leaf has no namespace and its exact path doesn't match,
retry once using the immediate parent's namespace instead of guessing more
broadly; a leaf that still doesn't match a real field stays unrecognized.
Moves schema-version/extension support and unrecognized-column handling
out of scattered prose into a single "TCX & GPX files" table, and drops
per-fixture behavior descriptions from tests/files/README.md that
duplicated each fixture's own comment and its referencing test.
@tabishm52
tabishm52 merged commit 934bfb5 into main Jul 5, 2026
4 checks passed
@tabishm52
tabishm52 deleted the schema-aware-tcx-gpx-parsing branch July 5, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant