diff --git a/src/activity_parser/__init__.py b/src/activity_parser/__init__.py index 4231a05..5bf3670 100755 --- a/src/activity_parser/__init__.py +++ b/src/activity_parser/__init__.py @@ -1,10 +1,10 @@ """Parser for loading FIT, TCX and GPX files into Pandas DataFrames.""" +from .activity import Activity from .exceptions import FitError, ParseError, XmlError -from .output import Activity -from .parse_activity import ActivityParser -from .parse_fit_file import parse_fit, parse_fit_raw +from .parse_fit import parse_fit, parse_fit_raw from .parse_tcx_gpx import parse_gpx, parse_tcx +from .parser import ActivityParser __all__ = [ "Activity", diff --git a/src/activity_parser/derive.py b/src/activity_parser/activity.py similarity index 71% rename from src/activity_parser/derive.py rename to src/activity_parser/activity.py index 0c3f378..4280f19 100644 --- a/src/activity_parser/derive.py +++ b/src/activity_parser/activity.py @@ -1,11 +1,43 @@ -"""Fills an ``Activity``'s missing fields by aggregating its own records/laps.""" +"""Defines ``Activity``, a file-level summary across FIT, TCX, and GPX files. + +Also defines the logic for filling missing ``Activity`` fields by calculating from the +file's records and laps. +""" import dataclasses +from dataclasses import dataclass from typing import Literal import pandas as pd -from .output import Activity + +@dataclass(frozen=True) +class Activity: + """File-level summary, present across FIT/TCX/GPX. + + Fields are populated from the source file's summary fields where reported, or + calculated from records and laps where practical. Fields are ``None`` when neither + is available. + """ + + sport: str | None = None + start_time: pd.Timestamp | None = None + total_elapsed_time: float | None = None # seconds + total_timer_time: float | None = None # seconds, excludes paused time + total_distance: float | None = None # km + total_ascent: float | None = None # m + total_descent: float | None = None # m + total_calories: float | None = None # kcal + avg_heart_rate: float | None = None # bpm + max_heart_rate: float | None = None # bpm + avg_power: float | None = None # W + max_power: float | None = None # W + avg_cadence: float | None = None # rpm + max_cadence: float | None = None # rpm + avg_speed: float | None = None # kph + max_speed: float | None = None # kph + creator: str | None = None # device/app that recorded or wrote the file + notes: str | None = None def _aggregate( diff --git a/src/activity_parser/output.py b/src/activity_parser/output.py deleted file mode 100644 index fae04ab..0000000 --- a/src/activity_parser/output.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Defines the shape of ``parse()``'s return values, shared across FIT/TCX/GPX.""" - -from dataclasses import dataclass - -import pandas as pd - -DEFAULT_RECORD_COLUMNS: tuple[str, ...] = ( - "latitude", - "longitude", - "altitude", - "distance", - "speed", - "cadence", - "heart_rate", - "power", - "left_balance", - "right_balance", - "accumulated_power", - "temperature", - "water_temperature", - "depth", - "course", - "bearing", -) - -DEFAULT_LAP_COLUMNS: tuple[str, ...] = ( - "start_time", - "total_elapsed_time", - "total_timer_time", - "start_position_lat", - "start_position_long", - "end_position_lat", - "end_position_long", - "total_distance", - "total_ascent", - "total_descent", - "avg_vam", - "avg_speed", - "max_speed", - "avg_cadence", - "max_cadence", - "total_strokes", - "steps", - "avg_heart_rate", - "max_heart_rate", - "time_in_hr_zone", - "avg_power", - "max_power", - "normalized_power", - "left_balance", - "right_balance", - "time_in_power_zone", - "total_work", - "avg_temperature", - "max_temperature", - "total_calories", - "total_fat_calories", -) - - -@dataclass(frozen=True) -class Activity: - """File-level summary, present across FIT/TCX/GPX. - - Fields are populated from the source file's summary fields where reported, or - calculated from records and laps where practical. Fields are ``None`` when neither - is available. - """ - - sport: str | None = None - start_time: pd.Timestamp | None = None - total_elapsed_time: float | None = None # seconds - total_timer_time: float | None = None # seconds, excludes paused time - total_distance: float | None = None # km - total_ascent: float | None = None # m - total_descent: float | None = None # m - total_calories: float | None = None # kcal - avg_heart_rate: float | None = None # bpm - max_heart_rate: float | None = None # bpm - avg_power: float | None = None # W - max_power: float | None = None # W - avg_cadence: float | None = None # rpm - max_cadence: float | None = None # rpm - avg_speed: float | None = None # kph - max_speed: float | None = None # kph - creator: str | None = None # device/app that recorded or wrote the file - notes: str | None = None diff --git a/src/activity_parser/parse_fit_file.py b/src/activity_parser/parse_fit.py similarity index 99% rename from src/activity_parser/parse_fit_file.py rename to src/activity_parser/parse_fit.py index 64b5e5a..9bd0d05 100644 --- a/src/activity_parser/parse_fit_file.py +++ b/src/activity_parser/parse_fit.py @@ -14,10 +14,9 @@ import pandas as pd from garmin_fit_sdk import Decoder, Profile, Stream -from .derive import fill_activity +from .activity import Activity, fill_activity from .exceptions import FitError from .fit_fields import LAP_UNITS, RECORD_UNITS, SESSION_UNITS, convert_units, convert_units_mapping -from .output import Activity from .postprocess import coerce_numeric_columns, index_by_time if TYPE_CHECKING: diff --git a/src/activity_parser/parse_tcx_gpx.py b/src/activity_parser/parse_tcx_gpx.py index 07de6a3..db00791 100644 --- a/src/activity_parser/parse_tcx_gpx.py +++ b/src/activity_parser/parse_tcx_gpx.py @@ -14,9 +14,8 @@ import pandas as pd from lxml import etree -from .derive import fill_activity +from .activity import Activity, fill_activity from .exceptions import XmlError -from .output import Activity from .postprocess import index_by_time from .sports import normalize_sport from .xml_fields import ( diff --git a/src/activity_parser/parse_activity.py b/src/activity_parser/parser.py similarity index 83% rename from src/activity_parser/parse_activity.py rename to src/activity_parser/parser.py index 908e93d..8651451 100644 --- a/src/activity_parser/parse_activity.py +++ b/src/activity_parser/parser.py @@ -7,10 +7,63 @@ import pandas as pd -from .output import DEFAULT_LAP_COLUMNS, DEFAULT_RECORD_COLUMNS, Activity -from .parse_fit_file import parse_fit +from .activity import Activity +from .parse_fit import parse_fit from .parse_tcx_gpx import parse_gpx, parse_tcx +DEFAULT_RECORD_COLUMNS: tuple[str, ...] = ( + "latitude", + "longitude", + "altitude", + "distance", + "speed", + "cadence", + "heart_rate", + "power", + "left_balance", + "right_balance", + "accumulated_power", + "temperature", + "water_temperature", + "depth", + "course", + "bearing", +) + +DEFAULT_LAP_COLUMNS: tuple[str, ...] = ( + "start_time", + "total_elapsed_time", + "total_timer_time", + "start_position_lat", + "start_position_long", + "end_position_lat", + "end_position_long", + "total_distance", + "total_ascent", + "total_descent", + "avg_vam", + "avg_speed", + "max_speed", + "avg_cadence", + "max_cadence", + "total_strokes", + "steps", + "avg_heart_rate", + "max_heart_rate", + "time_in_hr_zone", + "avg_power", + "max_power", + "normalized_power", + "left_balance", + "right_balance", + "time_in_power_zone", + "total_work", + "avg_temperature", + "max_temperature", + "total_calories", + "total_fat_calories", +) + def select_and_reorder_cols( df: pd.DataFrame, diff --git a/tests/test_derive.py b/tests/test_activity.py similarity index 96% rename from tests/test_derive.py rename to tests/test_activity.py index d0b9d7f..3c26c81 100644 --- a/tests/test_derive.py +++ b/tests/test_activity.py @@ -4,8 +4,7 @@ import pandas as pd -from activity_parser.derive import fill_activity -from activity_parser.output import Activity +from activity_parser.activity import Activity, fill_activity from activity_parser.parse_tcx_gpx import parse_tcx FILES = Path(__file__).parent / "files" diff --git a/tests/test_parse_fit.py b/tests/test_parse_fit.py index 8ee2263..15c1dc4 100644 --- a/tests/test_parse_fit.py +++ b/tests/test_parse_fit.py @@ -10,7 +10,7 @@ import synthetic_fit from activity_parser import ActivityParser, FitError -from activity_parser.parse_fit_file import ( +from activity_parser.parse_fit import ( LEFT_RIGHT_BALANCE_PERCENT_MASK, LEFT_RIGHT_BALANCE_PERCENT_MASK_100, LEFT_RIGHT_BALANCE_RIGHT_FLAG, diff --git a/tests/test_parse_activity.py b/tests/test_parser.py similarity index 98% rename from tests/test_parse_activity.py rename to tests/test_parser.py index 90a010f..ae1e115 100644 --- a/tests/test_parse_activity.py +++ b/tests/test_parser.py @@ -7,7 +7,7 @@ import pytest from activity_parser import ActivityParser -from activity_parser.parse_activity import ( +from activity_parser.parser import ( infer_extension, normalize_extension, select_and_reorder_cols, diff --git a/tests/test_readme.py b/tests/test_readme.py index a2b37d6..f4ed26a 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -4,7 +4,8 @@ from dataclasses import fields from pathlib import Path -from activity_parser.output import DEFAULT_LAP_COLUMNS, DEFAULT_RECORD_COLUMNS, Activity +from activity_parser.activity import Activity +from activity_parser.parser import DEFAULT_LAP_COLUMNS, DEFAULT_RECORD_COLUMNS README = Path(__file__).parent.parent / "README.md" COLUMN_NAME = re.compile(r"`(\w+)`")