Add activity name support to file parsing functions

Introduces an optional activity_name parameter to GPX, TCX, and FIT file parsing utilities, allowing the activity name to be set from external sources (such as Garmin Connect). Updates related function signatures and usages throughout the codebase to propagate this parameter. Also adds windsurf to ActivityDistances schema and improves type annotations and error handling in activity utilities.
This commit is contained in:
João Vitória Silva
2025-11-16 22:48:37 +00:00
parent 68dbb92401
commit 31a851250d
6 changed files with 49 additions and 18 deletions

View File

@@ -71,6 +71,7 @@ class ActivityDistances(BaseModel):
rowing: float
snow_ski: float
snowboard: float
windsurf: float
class ActivityEdit(BaseModel):

View File

@@ -300,7 +300,9 @@ def serialize_activity(activity: activities_schema.Activity):
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
return dt.astimezone(timezone).strftime("%Y-%m-%dT%H:%M:%S")
def convert_to_datetime_if_string(dt):
def convert_to_datetime_if_string(dt: str | datetime | None) -> datetime:
if dt is None:
raise ValueError("Datetime cannot be None")
if isinstance(dt, str):
return datetime.fromisoformat(dt)
return dt
@@ -364,7 +366,8 @@ async def parse_and_store_activity_from_file(
websocket_manager: websocket_schema.WebSocketManager,
db: Session,
from_garmin: bool = False,
garminconnect_gear: dict = None,
garminconnect_gear: dict | None = None,
activity_name: str | None = None,
):
try:
core_logger.print_to_log_and_console(
@@ -403,6 +406,7 @@ async def parse_and_store_activity_from_file(
file_extension,
file_path,
db,
activity_name,
)
if parsed_info is not None:
@@ -430,8 +434,12 @@ async def parse_and_store_activity_from_file(
split_records_by_activity,
token_user_id,
user_privacy_settings,
int(garmin_connect_activity_id),
garminconnect_gear,
(
int(garmin_connect_activity_id)
if garmin_connect_activity_id
else None
),
garminconnect_gear if garminconnect_gear else None,
db,
)
else:
@@ -509,6 +517,12 @@ async def parse_and_store_activity_from_uploaded_file(
websocket_manager: websocket_schema.WebSocketManager,
db: Session,
):
# Validate filename exists
if file.filename is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Filename is required",
)
# Get file extension
_, file_extension = os.path.splitext(file.filename)
@@ -654,7 +668,8 @@ def parse_file(
file_extension: str,
filename: str,
db: Session,
) -> dict:
activity_name: str | None = None,
) -> dict | None:
try:
if filename.lower() != "bulk_import/__init__.py":
core_logger.print_to_log(f"Parsing file: {filename}")
@@ -666,6 +681,7 @@ def parse_file(
token_user_id,
user_privacy_settings,
db,
activity_name,
)
elif file_extension.lower() == ".tcx":
parsed_info = tcx_utils.parse_tcx_file(
@@ -673,17 +689,17 @@ def parse_file(
token_user_id,
user_privacy_settings,
db,
activity_name,
)
elif file_extension.lower() == ".fit":
# Parse the FIT file
parsed_info = fit_utils.parse_fit_file(filename, db)
parsed_info = fit_utils.parse_fit_file(filename, db, activity_name)
else:
# file extension not supported raise an HTTPException with a 406 Not Acceptable status code
raise HTTPException(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
detail="File extension not supported. Supported file extensions are .gpx, .fit and .tcx",
)
return None # Can't return parsed info if we haven't parsed anything
return parsed_info
else:
return None
@@ -708,7 +724,7 @@ async def store_activity(
)
# Check if created_activity is None
if created_activity is None:
if created_activity is None or created_activity.id is None:
# Log the error
core_logger.print_to_log(
"Error in store_activity - activity is None, error creating activity",

View File

@@ -32,8 +32,8 @@ def create_activity_objects(
sessions_records: dict,
user_id: int,
user_privacy_settings: users_privacy_settings_schema.UsersPrivacySettings,
garmin_activity_id: int = None,
garminconnect_gear: dict = None,
garmin_activity_id: int | None = None,
garminconnect_gear: dict | None = None,
db: Session = None,
) -> list:
try:
@@ -428,13 +428,15 @@ def split_records_by_activity(parsed_data: dict) -> dict:
return sessions_records
def parse_fit_file(file: str, db: Session) -> dict:
def parse_fit_file(
file: str, db: Session, activity_name_input: str | None = None
) -> dict:
try:
# Initialize default values for various variables
sessions = []
time_offset = 0
last_waypoint_time = None
activity_name = "Workout"
activity_name = activity_name_input if activity_name_input else "Workout"
# Arrays to store waypoint data
lat_lon_waypoints = []

View File

@@ -56,6 +56,7 @@ async def fetch_and_process_activities_by_dates(
for activity in garmin_activities:
# Get the activity ID
activity_id = activity["activityId"]
activity_name = activity["activityName"]
# Check if the activity is already stored in the database
activity_db = activities_crud.get_activity_by_garminconnect_id_from_user_id(
@@ -114,6 +115,7 @@ async def fetch_and_process_activities_by_dates(
db,
True,
activity_gear,
activity_name,
)
or []
)
@@ -205,8 +207,15 @@ async def get_user_garminconnect_activities_by_dates(
if garminconnect_client is not None:
# Fetch Garmin Connect activities for the specified date range
garminconnect_activities_processed = await fetch_and_process_activities_by_dates(
garminconnect_client, start_date, end_date, user_id, websocket_manager, db
garminconnect_activities_processed = (
await fetch_and_process_activities_by_dates(
garminconnect_client,
start_date,
end_date,
user_id,
websocket_manager,
db,
)
)
# Log the start of the activities processing

View File

@@ -22,6 +22,7 @@ def parse_gpx_file(
user_id: int,
user_privacy_settings: users_privacy_settings_schema.UsersPrivacySettings,
db: Session,
activity_name_input: str | None = None,
) -> dict:
try:
# Create an instance of TimezoneFinder
@@ -45,7 +46,7 @@ def parse_gpx_file(
np = None
avg_speed = None
max_speed = None
activity_name = "Workout"
activity_name = activity_name_input if activity_name_input else "Workout"
activity_description = None
process_one_time_fields = 0
gear_id = None
@@ -105,7 +106,7 @@ def parse_gpx_file(
# Extract elevation, time, and location details
elevation, time = point.elevation, point.time
# Skip trackpoints without time data (common in some OsmAnd exports)
if time is None:
continue

View File

@@ -12,7 +12,9 @@ import core.logger as core_logger
import core.config as core_config
def parse_tcx_file(file, user_id, user_privacy_settings, db):
def parse_tcx_file(
file, user_id, user_privacy_settings, db, activity_name_input: str | None = None
) -> dict:
tcx_file = tcxreader.TCXReader().read(file)
trackpoints = tcx_file.trackpoints_to_dict()
@@ -26,7 +28,7 @@ def parse_tcx_file(file, user_id, user_privacy_settings, db):
city = None
town = None
country = None
activity_name = "Workout"
activity_name = activity_name_input if activity_name_input else "Workout"
avg_power = None
max_power = None
np = None