API - add typing
This commit is contained in:
		@@ -2,12 +2,14 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, Dict, List, Optional, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from fittrackee import appLog, db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    DataInvalidPayloadErrorResponse,
 | 
			
		||||
    DataNotFoundErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InternalServerErrorResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    NotFoundErrorResponse,
 | 
			
		||||
@@ -46,7 +48,7 @@ ACTIVITIES_PER_PAGE = 5
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities(auth_user_id):
 | 
			
		||||
def get_activities(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities for the authenticated user.
 | 
			
		||||
 | 
			
		||||
@@ -275,7 +277,9 @@ def get_activities(auth_user_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get an activity
 | 
			
		||||
 | 
			
		||||
@@ -375,8 +379,11 @@ def get_activity(auth_user_id, activity_short_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_activity_data(
 | 
			
		||||
    auth_user_id, activity_short_id, data_type, segment_id=None
 | 
			
		||||
):
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_short_id: str,
 | 
			
		||||
    data_type: str,
 | 
			
		||||
    segment_id: Optional[int] = None,
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """Get data from an activity gpx file"""
 | 
			
		||||
    activity_uuid = decode_short_id(activity_short_id)
 | 
			
		||||
    activity = Activity.query.filter_by(uuid=activity_uuid).first()
 | 
			
		||||
@@ -396,14 +403,17 @@ def get_activity_data(
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        absolute_gpx_filepath = get_absolute_file_path(activity.gpx)
 | 
			
		||||
        chart_data_content: Optional[List] = []
 | 
			
		||||
        if data_type == 'chart_data':
 | 
			
		||||
            content = get_chart_data(absolute_gpx_filepath, segment_id)
 | 
			
		||||
            chart_data_content = get_chart_data(
 | 
			
		||||
                absolute_gpx_filepath, segment_id
 | 
			
		||||
            )
 | 
			
		||||
        else:  # data_type == 'gpx'
 | 
			
		||||
            with open(absolute_gpx_filepath, encoding='utf-8') as f:
 | 
			
		||||
                content = f.read()
 | 
			
		||||
                gpx_content = f.read()
 | 
			
		||||
                if segment_id is not None:
 | 
			
		||||
                    content = extract_segment_from_gpx_file(
 | 
			
		||||
                        content, segment_id
 | 
			
		||||
                    gpx_segment_content = extract_segment_from_gpx_file(
 | 
			
		||||
                        gpx_content, segment_id
 | 
			
		||||
                    )
 | 
			
		||||
    except ActivityGPXException as e:
 | 
			
		||||
        appLog.error(e.message)
 | 
			
		||||
@@ -416,7 +426,15 @@ def get_activity_data(
 | 
			
		||||
    return {
 | 
			
		||||
        'status': 'success',
 | 
			
		||||
        'message': '',
 | 
			
		||||
        'data': ({data_type: content}),
 | 
			
		||||
        'data': (
 | 
			
		||||
            {
 | 
			
		||||
                data_type: chart_data_content
 | 
			
		||||
                if data_type == 'chart_data'
 | 
			
		||||
                else gpx_content
 | 
			
		||||
                if segment_id is None
 | 
			
		||||
                else gpx_segment_content
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -424,7 +442,9 @@ def get_activity_data(
 | 
			
		||||
    '/activities/<string:activity_short_id>/gpx', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity_gpx(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity_gpx(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for an activity displayed on map with Leaflet
 | 
			
		||||
 | 
			
		||||
@@ -473,7 +493,9 @@ def get_activity_gpx(auth_user_id, activity_short_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>/chart_data', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity_chart_data(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity_chart_data(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from an activity gpx file, to display it with Recharts
 | 
			
		||||
 | 
			
		||||
@@ -542,7 +564,9 @@ def get_activity_chart_data(auth_user_id, activity_short_id):
 | 
			
		||||
    methods=['GET'],
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
def get_segment_gpx(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str, segment_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for an activity segment displayed on map with Leaflet
 | 
			
		||||
 | 
			
		||||
@@ -595,7 +619,9 @@ def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
    methods=['GET'],
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
def get_segment_chart_data(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str, segment_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from an activity gpx file, to display it with Recharts
 | 
			
		||||
 | 
			
		||||
@@ -662,7 +688,7 @@ def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities/map/<map_id>', methods=['GET'])
 | 
			
		||||
def get_map(map_id):
 | 
			
		||||
def get_map(map_id: int) -> Any:
 | 
			
		||||
    """
 | 
			
		||||
    Get map image for activities with gpx
 | 
			
		||||
 | 
			
		||||
@@ -704,7 +730,7 @@ def get_map(map_id):
 | 
			
		||||
@activities_blueprint.route(
 | 
			
		||||
    '/activities/map_tile/<s>/<z>/<x>/<y>.png', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
def get_map_tile(s, z, x, y):
 | 
			
		||||
def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
 | 
			
		||||
    """
 | 
			
		||||
    Get map tile from tile server.
 | 
			
		||||
 | 
			
		||||
@@ -743,7 +769,7 @@ def get_map_tile(s, z, x, y):
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def post_activity(auth_user_id):
 | 
			
		||||
def post_activity(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post an activity with a gpx file
 | 
			
		||||
 | 
			
		||||
@@ -904,7 +930,9 @@ def post_activity(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities/no_gpx', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def post_activity_no_gpx(auth_user_id):
 | 
			
		||||
def post_activity_no_gpx(
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post an activity without gpx file
 | 
			
		||||
 | 
			
		||||
@@ -1053,7 +1081,9 @@ def post_activity_no_gpx(auth_user_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['PATCH']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def update_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def update_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update an activity
 | 
			
		||||
 | 
			
		||||
@@ -1199,7 +1229,9 @@ def update_activity(auth_user_id, activity_short_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['DELETE']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def delete_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def delete_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete an activity
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,23 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
from typing import Any, Dict, Optional, Union
 | 
			
		||||
from uuid import UUID, uuid4
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from sqlalchemy.dialects import postgresql
 | 
			
		||||
from sqlalchemy.engine.base import Connection
 | 
			
		||||
from sqlalchemy.event import listens_for
 | 
			
		||||
from sqlalchemy.ext.declarative import DeclarativeMeta
 | 
			
		||||
from sqlalchemy.ext.hybrid import hybrid_property
 | 
			
		||||
from sqlalchemy.orm.session import object_session
 | 
			
		||||
from sqlalchemy.orm.mapper import Mapper
 | 
			
		||||
from sqlalchemy.orm.session import Session, object_session
 | 
			
		||||
from sqlalchemy.types import JSON, Enum
 | 
			
		||||
 | 
			
		||||
from .utils_files import get_absolute_file_path
 | 
			
		||||
from .utils_format import convert_in_duration, convert_value_to_integer
 | 
			
		||||
from .utils_id import encode_uuid
 | 
			
		||||
 | 
			
		||||
BaseModel: DeclarativeMeta = db.Model
 | 
			
		||||
record_types = [
 | 
			
		||||
    'AS',  # 'Best Average Speed'
 | 
			
		||||
    'FD',  # 'Farthest Distance'
 | 
			
		||||
@@ -21,7 +26,9 @@ record_types = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
def update_records(
 | 
			
		||||
    user_id: int, sport_id: int, connection: Connection, session: Session
 | 
			
		||||
) -> None:
 | 
			
		||||
    record_table = Record.__table__
 | 
			
		||||
    new_records = Activity.get_user_activity_records(user_id, sport_id)
 | 
			
		||||
    for record_type, record_data in new_records.items():
 | 
			
		||||
@@ -47,7 +54,7 @@ def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
                new_record = Record(
 | 
			
		||||
                    activity=record_data['activity'], record_type=record_type
 | 
			
		||||
                )
 | 
			
		||||
                new_record.value = record_data['record_value']
 | 
			
		||||
                new_record.value = record_data['record_value']  # type: ignore
 | 
			
		||||
                session.add(new_record)
 | 
			
		||||
        else:
 | 
			
		||||
            connection.execute(
 | 
			
		||||
@@ -58,7 +65,7 @@ def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sport(db.Model):
 | 
			
		||||
class Sport(BaseModel):
 | 
			
		||||
    __tablename__ = "sports"
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    label = db.Column(db.String(50), unique=True, nullable=False)
 | 
			
		||||
@@ -71,13 +78,13 @@ class Sport(db.Model):
 | 
			
		||||
        'Record', lazy=True, backref=db.backref('sports', lazy='joined')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
    def __repr__(self) -> str:
 | 
			
		||||
        return f'<Sport {self.label!r}>'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, label):
 | 
			
		||||
    def __init__(self, label: str) -> None:
 | 
			
		||||
        self.label = label
 | 
			
		||||
 | 
			
		||||
    def serialize(self, is_admin=False):
 | 
			
		||||
    def serialize(self, is_admin: Optional[bool] = False) -> Dict:
 | 
			
		||||
        serialized_sport = {
 | 
			
		||||
            'id': self.id,
 | 
			
		||||
            'label': self.label,
 | 
			
		||||
@@ -89,7 +96,7 @@ class Sport(db.Model):
 | 
			
		||||
        return serialized_sport
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Activity(db.Model):
 | 
			
		||||
class Activity(BaseModel):
 | 
			
		||||
    __tablename__ = "activities"
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    uuid = db.Column(
 | 
			
		||||
@@ -138,10 +145,17 @@ class Activity(db.Model):
 | 
			
		||||
        backref=db.backref('activities', lazy='joined', single_parent=True),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, user_id, sport_id, activity_date, distance, duration):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        user_id: int,
 | 
			
		||||
        sport_id: int,
 | 
			
		||||
        activity_date: datetime.datetime,
 | 
			
		||||
        distance: float,
 | 
			
		||||
        duration: datetime.timedelta,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.user_id = user_id
 | 
			
		||||
        self.sport_id = sport_id
 | 
			
		||||
        self.activity_date = activity_date
 | 
			
		||||
@@ -149,10 +163,10 @@ class Activity(db.Model):
 | 
			
		||||
        self.duration = duration
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def short_id(self):
 | 
			
		||||
    def short_id(self) -> str:
 | 
			
		||||
        return encode_uuid(self.uuid)
 | 
			
		||||
 | 
			
		||||
    def serialize(self, params=None):
 | 
			
		||||
    def serialize(self, params: Optional[Dict] = None) -> Dict:
 | 
			
		||||
        date_from = params.get('from') if params else None
 | 
			
		||||
        date_to = params.get('to') if params else None
 | 
			
		||||
        distance_from = params.get('distance_from') if params else None
 | 
			
		||||
@@ -239,41 +253,43 @@ class Activity(db.Model):
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.short_id,  # WARNING: client use uuid as id
 | 
			
		||||
            "user": self.user.username,
 | 
			
		||||
            "sport_id": self.sport_id,
 | 
			
		||||
            "title": self.title,
 | 
			
		||||
            "creation_date": self.creation_date,
 | 
			
		||||
            "modification_date": self.modification_date,
 | 
			
		||||
            "activity_date": self.activity_date,
 | 
			
		||||
            "duration": str(self.duration) if self.duration else None,
 | 
			
		||||
            "pauses": str(self.pauses) if self.pauses else None,
 | 
			
		||||
            "moving": str(self.moving) if self.moving else None,
 | 
			
		||||
            "distance": float(self.distance) if self.distance else None,
 | 
			
		||||
            "min_alt": float(self.min_alt) if self.min_alt else None,
 | 
			
		||||
            "max_alt": float(self.max_alt) if self.max_alt else None,
 | 
			
		||||
            "descent": float(self.descent) if self.descent else None,
 | 
			
		||||
            "ascent": float(self.ascent) if self.ascent else None,
 | 
			
		||||
            "max_speed": float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            "ave_speed": float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
            "with_gpx": self.gpx is not None,
 | 
			
		||||
            "bounds": [float(bound) for bound in self.bounds]
 | 
			
		||||
            'id': self.short_id,  # WARNING: client use uuid as id
 | 
			
		||||
            'user': self.user.username,
 | 
			
		||||
            'sport_id': self.sport_id,
 | 
			
		||||
            'title': self.title,
 | 
			
		||||
            'creation_date': self.creation_date,
 | 
			
		||||
            'modification_date': self.modification_date,
 | 
			
		||||
            'activity_date': self.activity_date,
 | 
			
		||||
            'duration': str(self.duration) if self.duration else None,
 | 
			
		||||
            'pauses': str(self.pauses) if self.pauses else None,
 | 
			
		||||
            'moving': str(self.moving) if self.moving else None,
 | 
			
		||||
            'distance': float(self.distance) if self.distance else None,
 | 
			
		||||
            'min_alt': float(self.min_alt) if self.min_alt else None,
 | 
			
		||||
            'max_alt': float(self.max_alt) if self.max_alt else None,
 | 
			
		||||
            'descent': float(self.descent) if self.descent else None,
 | 
			
		||||
            'ascent': float(self.ascent) if self.ascent else None,
 | 
			
		||||
            'max_speed': float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            'ave_speed': float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
            'with_gpx': self.gpx is not None,
 | 
			
		||||
            'bounds': [float(bound) for bound in self.bounds]
 | 
			
		||||
            if self.bounds
 | 
			
		||||
            else [],  # noqa
 | 
			
		||||
            "previous_activity": previous_activity.short_id
 | 
			
		||||
            'previous_activity': previous_activity.short_id
 | 
			
		||||
            if previous_activity
 | 
			
		||||
            else None,  # noqa
 | 
			
		||||
            "next_activity": next_activity.short_id if next_activity else None,
 | 
			
		||||
            "segments": [segment.serialize() for segment in self.segments],
 | 
			
		||||
            "records": [record.serialize() for record in self.records],
 | 
			
		||||
            "map": self.map_id if self.map else None,
 | 
			
		||||
            "weather_start": self.weather_start,
 | 
			
		||||
            "weather_end": self.weather_end,
 | 
			
		||||
            "notes": self.notes,
 | 
			
		||||
            'next_activity': next_activity.short_id if next_activity else None,
 | 
			
		||||
            'segments': [segment.serialize() for segment in self.segments],
 | 
			
		||||
            'records': [record.serialize() for record in self.records],
 | 
			
		||||
            'map': self.map_id if self.map else None,
 | 
			
		||||
            'weather_start': self.weather_start,
 | 
			
		||||
            'weather_end': self.weather_end,
 | 
			
		||||
            'notes': self.notes,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_user_activity_records(cls, user_id, sport_id, as_integer=False):
 | 
			
		||||
    def get_user_activity_records(
 | 
			
		||||
        cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        record_types_columns = {
 | 
			
		||||
            'AS': 'ave_speed',  # 'Average speed'
 | 
			
		||||
            'FD': 'distance',  # 'Farthest Distance'
 | 
			
		||||
@@ -300,22 +316,26 @@ class Activity(db.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_insert')
 | 
			
		||||
def on_activity_insert(mapper, connection, activity):
 | 
			
		||||
def on_activity_insert(
 | 
			
		||||
    mapper: Mapper, connection: Connection, activity: Activity
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        update_records(
 | 
			
		||||
            activity.user_id, activity.sport_id, connection, session
 | 
			
		||||
        )  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_update')
 | 
			
		||||
def on_activity_update(mapper, connection, activity):
 | 
			
		||||
def on_activity_update(
 | 
			
		||||
    mapper: Mapper, connection: Connection, activity: Activity
 | 
			
		||||
) -> None:
 | 
			
		||||
    if object_session(activity).is_modified(
 | 
			
		||||
        activity, include_collections=True
 | 
			
		||||
    ):  # noqa
 | 
			
		||||
 | 
			
		||||
        @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
        def receive_after_flush(session, context):
 | 
			
		||||
        def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
            sports_list = [activity.sport_id]
 | 
			
		||||
            records = Record.query.filter_by(activity_id=activity.id).all()
 | 
			
		||||
            for rec in records:
 | 
			
		||||
@@ -326,16 +346,18 @@ def on_activity_update(mapper, connection, activity):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_delete')
 | 
			
		||||
def on_activity_delete(mapper, connection, old_record):
 | 
			
		||||
def on_activity_delete(
 | 
			
		||||
    mapper: Mapper, connection: Connection, old_record: 'Record'
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        if old_record.map:
 | 
			
		||||
            os.remove(get_absolute_file_path(old_record.map))
 | 
			
		||||
        if old_record.gpx:
 | 
			
		||||
            os.remove(get_absolute_file_path(old_record.gpx))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivitySegment(db.Model):
 | 
			
		||||
class ActivitySegment(BaseModel):
 | 
			
		||||
    __tablename__ = "activity_segments"
 | 
			
		||||
    activity_id = db.Column(
 | 
			
		||||
        db.Integer, db.ForeignKey('activities.id'), primary_key=True
 | 
			
		||||
@@ -353,35 +375,37 @@ class ActivitySegment(db.Model):
 | 
			
		||||
    max_speed = db.Column(db.Numeric(6, 2), nullable=True)  # km/h
 | 
			
		||||
    ave_speed = db.Column(db.Numeric(6, 2), nullable=True)  # km/h
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return (
 | 
			
		||||
            f'<Segment \'{self.segment_id}\' '
 | 
			
		||||
            f'for activity \'{encode_uuid(self.activity_uuid)}\'>'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, segment_id, activity_id, activity_uuid):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, segment_id: int, activity_id: int, activity_uuid: UUID
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.segment_id = segment_id
 | 
			
		||||
        self.activity_id = activity_id
 | 
			
		||||
        self.activity_uuid = activity_uuid
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        return {
 | 
			
		||||
            "activity_id": encode_uuid(self.activity_uuid),
 | 
			
		||||
            "segment_id": self.segment_id,
 | 
			
		||||
            "duration": str(self.duration) if self.duration else None,
 | 
			
		||||
            "pauses": str(self.pauses) if self.pauses else None,
 | 
			
		||||
            "moving": str(self.moving) if self.moving else None,
 | 
			
		||||
            "distance": float(self.distance) if self.distance else None,
 | 
			
		||||
            "min_alt": float(self.min_alt) if self.min_alt else None,
 | 
			
		||||
            "max_alt": float(self.max_alt) if self.max_alt else None,
 | 
			
		||||
            "descent": float(self.descent) if self.descent else None,
 | 
			
		||||
            "ascent": float(self.ascent) if self.ascent else None,
 | 
			
		||||
            "max_speed": float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            "ave_speed": float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
            'activity_id': encode_uuid(self.activity_uuid),
 | 
			
		||||
            'segment_id': self.segment_id,
 | 
			
		||||
            'duration': str(self.duration) if self.duration else None,
 | 
			
		||||
            'pauses': str(self.pauses) if self.pauses else None,
 | 
			
		||||
            'moving': str(self.moving) if self.moving else None,
 | 
			
		||||
            'distance': float(self.distance) if self.distance else None,
 | 
			
		||||
            'min_alt': float(self.min_alt) if self.min_alt else None,
 | 
			
		||||
            'max_alt': float(self.max_alt) if self.max_alt else None,
 | 
			
		||||
            'descent': float(self.descent) if self.descent else None,
 | 
			
		||||
            'ascent': float(self.ascent) if self.ascent else None,
 | 
			
		||||
            'max_speed': float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            'ave_speed': float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Record(db.Model):
 | 
			
		||||
class Record(BaseModel):
 | 
			
		||||
    __tablename__ = "records"
 | 
			
		||||
    __table_args__ = (
 | 
			
		||||
        db.UniqueConstraint(
 | 
			
		||||
@@ -401,14 +425,14 @@ class Record(db.Model):
 | 
			
		||||
    activity_date = db.Column(db.DateTime, nullable=False)
 | 
			
		||||
    _value = db.Column("value", db.Integer, nullable=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return (
 | 
			
		||||
            f'<Record {self.sports.label} - '
 | 
			
		||||
            f'{self.record_type} - '
 | 
			
		||||
            f"{self.activity_date.strftime('%Y-%m-%d')}>"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, activity, record_type):
 | 
			
		||||
    def __init__(self, activity: Activity, record_type: str) -> None:
 | 
			
		||||
        self.user_id = activity.user_id
 | 
			
		||||
        self.sport_id = activity.sport_id
 | 
			
		||||
        self.activity_id = activity.id
 | 
			
		||||
@@ -417,7 +441,7 @@ class Record(db.Model):
 | 
			
		||||
        self.activity_date = activity.activity_date
 | 
			
		||||
 | 
			
		||||
    @hybrid_property
 | 
			
		||||
    def value(self):
 | 
			
		||||
    def value(self) -> Optional[Union[datetime.timedelta, float]]:
 | 
			
		||||
        if self._value is None:
 | 
			
		||||
            return None
 | 
			
		||||
        if self.record_type == 'LD':
 | 
			
		||||
@@ -427,33 +451,35 @@ class Record(db.Model):
 | 
			
		||||
        else:  # 'FD'
 | 
			
		||||
            return float(self._value / 1000)
 | 
			
		||||
 | 
			
		||||
    @value.setter
 | 
			
		||||
    def value(self, val):
 | 
			
		||||
    @value.setter  # type: ignore
 | 
			
		||||
    def value(self, val: Union[str, float]) -> None:
 | 
			
		||||
        self._value = convert_value_to_integer(self.record_type, val)
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        if self.value is None:
 | 
			
		||||
            value = None
 | 
			
		||||
        elif self.record_type in ['AS', 'FD', 'MS']:
 | 
			
		||||
            value = float(self.value)
 | 
			
		||||
            value = float(self.value)  # type: ignore
 | 
			
		||||
        else:  # 'LD'
 | 
			
		||||
            value = str(self.value)
 | 
			
		||||
            value = str(self.value)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "user": self.user.username,
 | 
			
		||||
            "sport_id": self.sport_id,
 | 
			
		||||
            "activity_id": encode_uuid(self.activity_uuid),
 | 
			
		||||
            "record_type": self.record_type,
 | 
			
		||||
            "activity_date": self.activity_date,
 | 
			
		||||
            "value": value,
 | 
			
		||||
            'id': self.id,
 | 
			
		||||
            'user': self.user.username,
 | 
			
		||||
            'sport_id': self.sport_id,
 | 
			
		||||
            'activity_id': encode_uuid(self.activity_uuid),
 | 
			
		||||
            'record_type': self.record_type,
 | 
			
		||||
            'activity_date': self.activity_date,
 | 
			
		||||
            'value': value,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Record, 'after_delete')
 | 
			
		||||
def on_record_delete(mapper, connection, old_record):
 | 
			
		||||
def on_record_delete(
 | 
			
		||||
    mapper: Mapper, connection: Connection, old_record: Record
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        activity = old_record.activities
 | 
			
		||||
        new_records = Activity.get_user_activity_records(
 | 
			
		||||
            activity.user_id, activity.sport_id
 | 
			
		||||
@@ -466,5 +492,5 @@ def on_record_delete(mapper, connection, old_record):
 | 
			
		||||
                new_record = Record(
 | 
			
		||||
                    activity=record_data['activity'], record_type=record_type
 | 
			
		||||
                )
 | 
			
		||||
                new_record.value = record_data['record_value']
 | 
			
		||||
                new_record.value = record_data['record_value']  # type: ignore
 | 
			
		||||
                session.add(new_record)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
 | 
			
		||||
from ..users.utils import authenticate
 | 
			
		||||
@@ -8,7 +10,7 @@ records_blueprint = Blueprint('records', __name__)
 | 
			
		||||
 | 
			
		||||
@records_blueprint.route('/records', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_records(auth_user_id):
 | 
			
		||||
def get_records(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all records for authenticated user.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
from typing import Dict, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    DataNotFoundErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    handle_error_and_return_response,
 | 
			
		||||
)
 | 
			
		||||
@@ -16,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_sports(auth_user_id):
 | 
			
		||||
def get_sports(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all sports
 | 
			
		||||
 | 
			
		||||
@@ -158,7 +161,7 @@ def get_sports(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_sport(auth_user_id, sport_id):
 | 
			
		||||
def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get a sport
 | 
			
		||||
 | 
			
		||||
@@ -253,7 +256,9 @@ def get_sport(auth_user_id, sport_id):
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def update_sport(auth_user_id, sport_id):
 | 
			
		||||
def update_sport(
 | 
			
		||||
    auth_user_id: int, sport_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update a sport
 | 
			
		||||
    Authenticated user must be an admin
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Dict, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    NotFoundErrorResponse,
 | 
			
		||||
    UserNotFoundErrorResponse,
 | 
			
		||||
@@ -19,7 +21,12 @@ from .utils_format import convert_timedelta_to_integer
 | 
			
		||||
stats_blueprint = Blueprint('stats', __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_activities(user_name, filter_type):
 | 
			
		||||
def get_activities(
 | 
			
		||||
    user_name: str, filter_type: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Return user activities by sport or by time
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        user = User.query.filter_by(username=user_name).first()
 | 
			
		||||
        if not user:
 | 
			
		||||
@@ -40,7 +47,6 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
        time = params.get('time')
 | 
			
		||||
 | 
			
		||||
        if filter_type == 'by_sport':
 | 
			
		||||
            sport_id = params.get('sport_id')
 | 
			
		||||
            if sport_id:
 | 
			
		||||
                sport = Sport.query.filter_by(id=sport_id).first()
 | 
			
		||||
                if not sport:
 | 
			
		||||
@@ -59,24 +65,26 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
            .all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        activities_list = {}
 | 
			
		||||
        activities_list_by_sport = {}
 | 
			
		||||
        activities_list_by_time = {}  # type: ignore
 | 
			
		||||
        for activity in activities:
 | 
			
		||||
            if filter_type == 'by_sport':
 | 
			
		||||
                sport_id = activity.sport_id
 | 
			
		||||
                if sport_id not in activities_list:
 | 
			
		||||
                    activities_list[sport_id] = {
 | 
			
		||||
                if sport_id not in activities_list_by_sport:
 | 
			
		||||
                    activities_list_by_sport[sport_id] = {
 | 
			
		||||
                        'nb_activities': 0,
 | 
			
		||||
                        'total_distance': 0.0,
 | 
			
		||||
                        'total_duration': 0,
 | 
			
		||||
                    }
 | 
			
		||||
                activities_list[sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list[sport_id]['total_distance'] += float(
 | 
			
		||||
                activities_list_by_sport[sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list_by_sport[sport_id]['total_distance'] += float(
 | 
			
		||||
                    activity.distance
 | 
			
		||||
                )
 | 
			
		||||
                activities_list[sport_id][
 | 
			
		||||
                activities_list_by_sport[sport_id][
 | 
			
		||||
                    'total_duration'
 | 
			
		||||
                ] += convert_timedelta_to_integer(activity.moving)
 | 
			
		||||
 | 
			
		||||
            # filter_type == 'by_time'
 | 
			
		||||
            else:
 | 
			
		||||
                if time == 'week':
 | 
			
		||||
                    activity_date = activity.activity_date - timedelta(
 | 
			
		||||
@@ -105,25 +113,31 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
                        'Invalid time period.', 'fail'
 | 
			
		||||
                    )
 | 
			
		||||
                sport_id = activity.sport_id
 | 
			
		||||
                if time_period not in activities_list:
 | 
			
		||||
                    activities_list[time_period] = {}
 | 
			
		||||
                if sport_id not in activities_list[time_period]:
 | 
			
		||||
                    activities_list[time_period][sport_id] = {
 | 
			
		||||
                if time_period not in activities_list_by_time:
 | 
			
		||||
                    activities_list_by_time[time_period] = {}
 | 
			
		||||
                if sport_id not in activities_list_by_time[time_period]:
 | 
			
		||||
                    activities_list_by_time[time_period][sport_id] = {
 | 
			
		||||
                        'nb_activities': 0,
 | 
			
		||||
                        'total_distance': 0.0,
 | 
			
		||||
                        'total_duration': 0,
 | 
			
		||||
                    }
 | 
			
		||||
                activities_list[time_period][sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list[time_period][sport_id][
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'nb_activities'
 | 
			
		||||
                ] += 1
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'total_distance'
 | 
			
		||||
                ] += float(activity.distance)
 | 
			
		||||
                activities_list[time_period][sport_id][
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'total_duration'
 | 
			
		||||
                ] += convert_timedelta_to_integer(activity.moving)
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            'status': 'success',
 | 
			
		||||
            'data': {'statistics': activities_list},
 | 
			
		||||
            'data': {
 | 
			
		||||
                'statistics': activities_list_by_sport
 | 
			
		||||
                if filter_type == 'by_sport'
 | 
			
		||||
                else activities_list_by_time
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return handle_error_and_return_response(e)
 | 
			
		||||
@@ -131,7 +145,9 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities_by_time(auth_user_id, user_name):
 | 
			
		||||
def get_activities_by_time(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities statistics for a user by time
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +243,9 @@ def get_activities_by_time(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities_by_sport(auth_user_id, user_name):
 | 
			
		||||
def get_activities_by_sport(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities statistics for a user by sport
 | 
			
		||||
 | 
			
		||||
@@ -313,7 +331,7 @@ def get_activities_by_sport(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/all', methods=['GET'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def get_application_stats(auth_user_id):
 | 
			
		||||
def get_application_stats(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all application statistics
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import os
 | 
			
		||||
import tempfile
 | 
			
		||||
import zipfile
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Dict, List, Optional, Tuple, Union
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
import gpxpy.gpx
 | 
			
		||||
import pytz
 | 
			
		||||
@@ -10,6 +12,7 @@ from fittrackee import appLog, db
 | 
			
		||||
from flask import current_app
 | 
			
		||||
from sqlalchemy import exc
 | 
			
		||||
from staticmap import Line, StaticMap
 | 
			
		||||
from werkzeug.datastructures import FileStorage
 | 
			
		||||
from werkzeug.utils import secure_filename
 | 
			
		||||
 | 
			
		||||
from ..users.models import User
 | 
			
		||||
@@ -19,13 +22,20 @@ from .utils_gpx import get_gpx_info
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityException(Exception):
 | 
			
		||||
    def __init__(self, status, message, e):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, status: str, message: str, e: Optional[Exception] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.status = status
 | 
			
		||||
        self.message = message
 | 
			
		||||
        self.e = e
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
 | 
			
		||||
def get_datetime_with_tz(
 | 
			
		||||
    timezone: str, activity_date: datetime, gpx_data: Optional[Dict] = None
 | 
			
		||||
) -> Tuple[Optional[datetime], datetime]:
 | 
			
		||||
    """
 | 
			
		||||
    Return naive datetime and datetime with user timezone
 | 
			
		||||
    """
 | 
			
		||||
    activity_date_tz = None
 | 
			
		||||
    if timezone:
 | 
			
		||||
        user_tz = pytz.timezone(timezone)
 | 
			
		||||
@@ -47,8 +57,12 @@ def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
 | 
			
		||||
    return activity_date_tz, activity_date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_activity_data(activity, gpx_data):
 | 
			
		||||
    """activity could be a complete activity or an activity segment"""
 | 
			
		||||
def update_activity_data(
 | 
			
		||||
    activity: Union[Activity, ActivitySegment], gpx_data: Dict
 | 
			
		||||
) -> Union[Activity, ActivitySegment]:
 | 
			
		||||
    """
 | 
			
		||||
    Update activity or activity segment with data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    activity.pauses = gpx_data['stop_time']
 | 
			
		||||
    activity.moving = gpx_data['moving_time']
 | 
			
		||||
    activity.min_alt = gpx_data['elevation_min']
 | 
			
		||||
@@ -60,12 +74,18 @@ def update_activity_data(activity, gpx_data):
 | 
			
		||||
    return activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
def create_activity(
 | 
			
		||||
    user: User, activity_data: Dict, gpx_data: Optional[Dict] = None
 | 
			
		||||
) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Create Activity from data entered by user and from gpx if a gpx file is
 | 
			
		||||
    provided
 | 
			
		||||
    """
 | 
			
		||||
    activity_date = (
 | 
			
		||||
        gpx_data['start']
 | 
			
		||||
        if gpx_data
 | 
			
		||||
        else datetime.strptime(
 | 
			
		||||
            activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
 | 
			
		||||
            activity_data['activity_date'], '%Y-%m-%d %H:%M'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    activity_date_tz, activity_date = get_datetime_with_tz(
 | 
			
		||||
@@ -75,16 +95,14 @@ def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
    duration = (
 | 
			
		||||
        gpx_data['duration']
 | 
			
		||||
        if gpx_data
 | 
			
		||||
        else timedelta(seconds=activity_data.get('duration'))
 | 
			
		||||
        else timedelta(seconds=activity_data['duration'])
 | 
			
		||||
    )
 | 
			
		||||
    distance = (
 | 
			
		||||
        gpx_data['distance'] if gpx_data else activity_data.get('distance')
 | 
			
		||||
    )
 | 
			
		||||
    title = gpx_data['name'] if gpx_data else activity_data.get('title')
 | 
			
		||||
    distance = gpx_data['distance'] if gpx_data else activity_data['distance']
 | 
			
		||||
    title = gpx_data['name'] if gpx_data else activity_data.get('title', '')
 | 
			
		||||
 | 
			
		||||
    new_activity = Activity(
 | 
			
		||||
        user_id=user.id,
 | 
			
		||||
        sport_id=activity_data.get('sport_id'),
 | 
			
		||||
        sport_id=activity_data['sport_id'],
 | 
			
		||||
        activity_date=activity_date,
 | 
			
		||||
        distance=distance,
 | 
			
		||||
        duration=duration,
 | 
			
		||||
@@ -118,7 +136,12 @@ def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
    return new_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_segment(activity_id, activity_uuid, segment_data):
 | 
			
		||||
def create_segment(
 | 
			
		||||
    activity_id: int, activity_uuid: UUID, segment_data: Dict
 | 
			
		||||
) -> ActivitySegment:
 | 
			
		||||
    """
 | 
			
		||||
    Create Activity Segment from gpx data
 | 
			
		||||
    """
 | 
			
		||||
    new_segment = ActivitySegment(
 | 
			
		||||
        activity_id=activity_id,
 | 
			
		||||
        activity_uuid=activity_uuid,
 | 
			
		||||
@@ -130,12 +153,9 @@ def create_segment(activity_id, activity_uuid, segment_data):
 | 
			
		||||
    return new_segment
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_activity(activity):
 | 
			
		||||
def update_activity(activity: Activity) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Note: only gpx_data is be updated for now (the gpx file is NOT modified)
 | 
			
		||||
 | 
			
		||||
    In a next version, map_data and weather_data will be updated
 | 
			
		||||
    (case of a modified gpx file, see issue #7)
 | 
			
		||||
    Update activity data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx_data, _, _ = get_gpx_info(
 | 
			
		||||
        get_absolute_file_path(activity.gpx), False, False
 | 
			
		||||
@@ -155,7 +175,16 @@ def update_activity(activity):
 | 
			
		||||
    return updated_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
def edit_activity(
 | 
			
		||||
    activity: Activity, activity_data: Dict, auth_user_id: int
 | 
			
		||||
) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Edit an activity
 | 
			
		||||
    Note: the gpx file is NOT modified
 | 
			
		||||
 | 
			
		||||
    In a next version, map_data and weather_data will be updated
 | 
			
		||||
    (case of a modified gpx file, see issue #7)
 | 
			
		||||
    """
 | 
			
		||||
    user = User.query.filter_by(id=auth_user_id).first()
 | 
			
		||||
    if activity_data.get('refresh'):
 | 
			
		||||
        activity = update_activity(activity)
 | 
			
		||||
@@ -168,20 +197,18 @@ def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
    if not activity.gpx:
 | 
			
		||||
        if activity_data.get('activity_date'):
 | 
			
		||||
            activity_date = datetime.strptime(
 | 
			
		||||
                activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
 | 
			
		||||
                activity_data['activity_date'], '%Y-%m-%d %H:%M'
 | 
			
		||||
            )
 | 
			
		||||
            _, activity.activity_date = get_datetime_with_tz(
 | 
			
		||||
                user.timezone, activity_date
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if activity_data.get('duration'):
 | 
			
		||||
            activity.duration = timedelta(
 | 
			
		||||
                seconds=activity_data.get('duration')
 | 
			
		||||
            )
 | 
			
		||||
            activity.duration = timedelta(seconds=activity_data['duration'])
 | 
			
		||||
            activity.moving = activity.duration
 | 
			
		||||
 | 
			
		||||
        if activity_data.get('distance'):
 | 
			
		||||
            activity.distance = activity_data.get('distance')
 | 
			
		||||
            activity.distance = activity_data['distance']
 | 
			
		||||
 | 
			
		||||
        activity.ave_speed = (
 | 
			
		||||
            None
 | 
			
		||||
@@ -192,7 +219,10 @@ def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
    return activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_file_path(dir_path, filename):
 | 
			
		||||
def get_file_path(dir_path: str, filename: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Get full path for a file
 | 
			
		||||
    """
 | 
			
		||||
    if not os.path.exists(dir_path):
 | 
			
		||||
        os.makedirs(dir_path)
 | 
			
		||||
    file_path = os.path.join(dir_path, filename)
 | 
			
		||||
@@ -200,9 +230,16 @@ def get_file_path(dir_path, filename):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_new_file_path(
 | 
			
		||||
    auth_user_id, activity_date, sport, old_filename=None, extension=None
 | 
			
		||||
):
 | 
			
		||||
    if not extension:
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_date: str,
 | 
			
		||||
    sport: str,
 | 
			
		||||
    old_filename: Optional[str] = None,
 | 
			
		||||
    extension: Optional[str] = None,
 | 
			
		||||
) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Generate a file path from user and activity data
 | 
			
		||||
    """
 | 
			
		||||
    if not extension and old_filename:
 | 
			
		||||
        extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
 | 
			
		||||
    _, new_filename = tempfile.mkstemp(
 | 
			
		||||
        prefix=f'{activity_date}_{sport}_', suffix=extension
 | 
			
		||||
@@ -214,7 +251,10 @@ def get_new_file_path(
 | 
			
		||||
    return file_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_map(map_filepath, map_data):
 | 
			
		||||
def generate_map(map_filepath: str, map_data: List) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Generate and save map image from map data
 | 
			
		||||
    """
 | 
			
		||||
    m = StaticMap(400, 225, 10)
 | 
			
		||||
    line = Line(map_data, '#3388FF', 4)
 | 
			
		||||
    m.add_line(line)
 | 
			
		||||
@@ -222,10 +262,10 @@ def generate_map(map_filepath, map_data):
 | 
			
		||||
    image.save(map_filepath)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_map_hash(map_filepath):
 | 
			
		||||
def get_map_hash(map_filepath: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    md5 hash is used as id instead of activity id, to retrieve map image
 | 
			
		||||
    (maps are sensitive data)
 | 
			
		||||
    Generate a md5 hash used as id instead of activity id, to retrieve map
 | 
			
		||||
    image (maps are sensitive data)
 | 
			
		||||
    """
 | 
			
		||||
    md5 = hashlib.md5()
 | 
			
		||||
    absolute_map_filepath = get_absolute_file_path(map_filepath)
 | 
			
		||||
@@ -235,7 +275,10 @@ def get_map_hash(map_filepath):
 | 
			
		||||
    return md5.hexdigest()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_one_gpx_file(params, filename):
 | 
			
		||||
def process_one_gpx_file(params: Dict, filename: str) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Get all data from a gpx file to create an activity with map image
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
 | 
			
		||||
        auth_user_id = params['user'].id
 | 
			
		||||
@@ -284,23 +327,33 @@ def process_one_gpx_file(params, filename):
 | 
			
		||||
        raise ActivityException('fail', 'Error during activity save.', e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_zip_archive(common_params, extract_dir):
 | 
			
		||||
def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Get files from a zip archive and create activities, if number of files
 | 
			
		||||
    does not exceed defined limit.
 | 
			
		||||
    """
 | 
			
		||||
    with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
 | 
			
		||||
        zip_ref.extractall(extract_dir)
 | 
			
		||||
 | 
			
		||||
    new_activities = []
 | 
			
		||||
    gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', '10')
 | 
			
		||||
    if gpx_files_limit and gpx_files_limit.isdigit():
 | 
			
		||||
    gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
 | 
			
		||||
    if (
 | 
			
		||||
        gpx_files_limit
 | 
			
		||||
        and isinstance(gpx_files_limit, str)
 | 
			
		||||
        and gpx_files_limit.isdigit()
 | 
			
		||||
    ):
 | 
			
		||||
        gpx_files_limit = int(gpx_files_limit)
 | 
			
		||||
    else:
 | 
			
		||||
        gpx_files_limit = 10
 | 
			
		||||
        appLog.error('GPX limit not configured, set to 10.')
 | 
			
		||||
        appLog.warning('GPX limit not configured, set to 10.')
 | 
			
		||||
    gpx_files_ok = 0
 | 
			
		||||
 | 
			
		||||
    for gpx_file in os.listdir(extract_dir):
 | 
			
		||||
        if '.' in gpx_file and gpx_file.rsplit('.', 1)[
 | 
			
		||||
            1
 | 
			
		||||
        ].lower() in current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS'):
 | 
			
		||||
        if (
 | 
			
		||||
            '.' in gpx_file
 | 
			
		||||
            and gpx_file.rsplit('.', 1)[1].lower()
 | 
			
		||||
            in current_app.config['ACTIVITY_ALLOWED_EXTENSIONS']
 | 
			
		||||
        ):
 | 
			
		||||
            gpx_files_ok += 1
 | 
			
		||||
            if gpx_files_ok > gpx_files_limit:
 | 
			
		||||
                break
 | 
			
		||||
@@ -313,7 +366,17 @@ def process_zip_archive(common_params, extract_dir):
 | 
			
		||||
    return new_activities
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
def process_files(
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_data: Dict,
 | 
			
		||||
    activity_file: FileStorage,
 | 
			
		||||
    folders: Dict,
 | 
			
		||||
) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Store gpx file or zip archive and create activities
 | 
			
		||||
    """
 | 
			
		||||
    if activity_file.filename is None:
 | 
			
		||||
        raise ActivityException('error', 'File has no filename.')
 | 
			
		||||
    filename = secure_filename(activity_file.filename)
 | 
			
		||||
    extension = f".{filename.rsplit('.', 1)[1].lower()}"
 | 
			
		||||
    file_path = get_file_path(folders['tmp_dir'], filename)
 | 
			
		||||
@@ -322,7 +385,6 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
        raise ActivityException(
 | 
			
		||||
            'error',
 | 
			
		||||
            f"Sport id: {activity_data.get('sport_id')} does not exist",
 | 
			
		||||
            None,
 | 
			
		||||
        )
 | 
			
		||||
    user = User.query.filter_by(id=auth_user_id).first()
 | 
			
		||||
 | 
			
		||||
@@ -344,7 +406,10 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
        return process_zip_archive(common_params, folders['extract_dir'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_upload_dir_size():
 | 
			
		||||
def get_upload_dir_size() -> int:
 | 
			
		||||
    """
 | 
			
		||||
    Return upload directory size
 | 
			
		||||
    """
 | 
			
		||||
    upload_path = get_absolute_file_path('')
 | 
			
		||||
    total_size = 0
 | 
			
		||||
    for dir_path, _, filenames in os.walk(upload_path):
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,5 @@ import os
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_absolute_file_path(relative_path):
 | 
			
		||||
def get_absolute_file_path(relative_path: str) -> str:
 | 
			
		||||
    return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,26 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Optional, Union
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_in_duration(value):
 | 
			
		||||
def convert_in_duration(value: str) -> timedelta:
 | 
			
		||||
    hours = int(value.split(':')[0])
 | 
			
		||||
    minutes = int(value.split(':')[1])
 | 
			
		||||
    return timedelta(seconds=(hours * 3600 + minutes * 60))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_timedelta_to_integer(value):
 | 
			
		||||
def convert_timedelta_to_integer(value: str) -> int:
 | 
			
		||||
    hours, minutes, seconds = str(value).split(':')
 | 
			
		||||
    return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_value_to_integer(record_type, val):
 | 
			
		||||
def convert_value_to_integer(
 | 
			
		||||
    record_type: str, val: Union[str, float]
 | 
			
		||||
) -> Optional[int]:
 | 
			
		||||
    if val is None:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    if record_type == 'LD':
 | 
			
		||||
        return convert_timedelta_to_integer(val)
 | 
			
		||||
        return convert_timedelta_to_integer(str(val))
 | 
			
		||||
    elif record_type in ['AS', 'MS']:
 | 
			
		||||
        return int(val * 100)
 | 
			
		||||
    else:  # 'FD'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Any, Dict, List, Optional, Tuple
 | 
			
		||||
 | 
			
		||||
import gpxpy.gpx
 | 
			
		||||
 | 
			
		||||
@@ -6,25 +7,40 @@ from .utils_weather import get_weather
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityGPXException(Exception):
 | 
			
		||||
    def __init__(self, status, message, e):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, status: str, message: str, e: Optional[Exception] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.status = status
 | 
			
		||||
        self.message = message
 | 
			
		||||
        self.e = e
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def open_gpx_file(gpx_file):
 | 
			
		||||
    gpx_file = open(gpx_file, 'r')
 | 
			
		||||
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
 | 
			
		||||
    gpx_file = open(gpx_file, 'r')  # type: ignore
 | 
			
		||||
    gpx = gpxpy.parse(gpx_file)
 | 
			
		||||
    if len(gpx.tracks) == 0:
 | 
			
		||||
        return None
 | 
			
		||||
    return gpx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    gpx_data = {'max_speed': (max_speed / 1000) * 3600, 'start': start}
 | 
			
		||||
def get_gpx_data(
 | 
			
		||||
    parsed_gpx: gpxpy.gpx,
 | 
			
		||||
    max_speed: float,
 | 
			
		||||
    start: int,
 | 
			
		||||
    stopped_time_between_seg: timedelta,
 | 
			
		||||
) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Returns data from parsed gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx_data: Dict[str, Any] = {
 | 
			
		||||
        'max_speed': (max_speed / 1000) * 3600,
 | 
			
		||||
        'start': start,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    duration = parsed_gpx.get_duration()
 | 
			
		||||
    gpx_data['duration'] = timedelta(seconds=duration) + stopped_time_btwn_seg
 | 
			
		||||
    gpx_data['duration'] = (
 | 
			
		||||
        timedelta(seconds=duration) + stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    ele = parsed_gpx.get_elevation_extremes()
 | 
			
		||||
    gpx_data['elevation_max'] = ele.maximum
 | 
			
		||||
@@ -37,7 +53,7 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    mv = parsed_gpx.get_moving_data()
 | 
			
		||||
    gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
 | 
			
		||||
    gpx_data['stop_time'] = (
 | 
			
		||||
        timedelta(seconds=mv.stopped_time) + stopped_time_btwn_seg
 | 
			
		||||
        timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
    distance = mv.moving_distance + mv.stopped_distance
 | 
			
		||||
    gpx_data['distance'] = distance / 1000
 | 
			
		||||
@@ -48,10 +64,17 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    return gpx_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
def get_gpx_info(
 | 
			
		||||
    gpx_file: str,
 | 
			
		||||
    update_map_data: Optional[bool] = True,
 | 
			
		||||
    update_weather_data: Optional[bool] = True,
 | 
			
		||||
) -> Tuple:
 | 
			
		||||
    """
 | 
			
		||||
    Parse and return gpx, map and weather data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx = open_gpx_file(gpx_file)
 | 
			
		||||
    if gpx is None:
 | 
			
		||||
        return None
 | 
			
		||||
        raise ActivityGPXException('not found', 'No gpx file')
 | 
			
		||||
 | 
			
		||||
    gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
 | 
			
		||||
    max_speed = 0
 | 
			
		||||
@@ -61,7 +84,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
    segments_nb = len(gpx.tracks[0].segments)
 | 
			
		||||
    prev_seg_last_point = None
 | 
			
		||||
    no_stopped_time = timedelta(seconds=0)
 | 
			
		||||
    stopped_time_btwn_seg = no_stopped_time
 | 
			
		||||
    stopped_time_between_seg = no_stopped_time
 | 
			
		||||
 | 
			
		||||
    for segment_idx, segment in enumerate(gpx.tracks[0].segments):
 | 
			
		||||
        segment_start = 0
 | 
			
		||||
@@ -77,7 +100,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
                # if a previous segment exists, calculate stopped time between
 | 
			
		||||
                # the two segments
 | 
			
		||||
                if prev_seg_last_point:
 | 
			
		||||
                    stopped_time_btwn_seg = point.time - prev_seg_last_point
 | 
			
		||||
                    stopped_time_between_seg = point.time - prev_seg_last_point
 | 
			
		||||
 | 
			
		||||
            # last segment point
 | 
			
		||||
            if point_idx == (segment_points_nb - 1):
 | 
			
		||||
@@ -104,7 +127,9 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
        segment_data['idx'] = segment_idx
 | 
			
		||||
        gpx_data['segments'].append(segment_data)
 | 
			
		||||
 | 
			
		||||
    full_gpx_data = get_gpx_data(gpx, max_speed, start, stopped_time_btwn_seg)
 | 
			
		||||
    full_gpx_data = get_gpx_data(
 | 
			
		||||
        gpx, max_speed, start, stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
    gpx_data = {**gpx_data, **full_gpx_data}
 | 
			
		||||
 | 
			
		||||
    if update_map_data:
 | 
			
		||||
@@ -119,7 +144,12 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
    return gpx_data, map_data, weather_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_segments(track_segments, segment_id=None):
 | 
			
		||||
def get_gpx_segments(
 | 
			
		||||
    track_segments: List, segment_id: Optional[int] = None
 | 
			
		||||
) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Return list of segments, filtered on segment id if provided
 | 
			
		||||
    """
 | 
			
		||||
    if segment_id is not None:
 | 
			
		||||
        segment_index = segment_id - 1
 | 
			
		||||
        if segment_index > (len(track_segments) - 1):
 | 
			
		||||
@@ -135,7 +165,12 @@ def get_gpx_segments(track_segments, segment_id=None):
 | 
			
		||||
    return segments
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_chart_data(gpx_file, segment_id=None):
 | 
			
		||||
def get_chart_data(
 | 
			
		||||
    gpx_file: str, segment_id: Optional[int] = None
 | 
			
		||||
) -> Optional[List]:
 | 
			
		||||
    """
 | 
			
		||||
    Return data needed to generate chart with speed and elevation
 | 
			
		||||
    """
 | 
			
		||||
    gpx = open_gpx_file(gpx_file)
 | 
			
		||||
    if gpx is None:
 | 
			
		||||
        return None
 | 
			
		||||
@@ -193,7 +228,12 @@ def get_chart_data(gpx_file, segment_id=None):
 | 
			
		||||
    return chart_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract_segment_from_gpx_file(content, segment_id):
 | 
			
		||||
def extract_segment_from_gpx_file(
 | 
			
		||||
    content: str, segment_id: int
 | 
			
		||||
) -> Optional[str]:
 | 
			
		||||
    """
 | 
			
		||||
    Returns segments in xml format from a gpx file content
 | 
			
		||||
    """
 | 
			
		||||
    gpx_content = gpxpy.parse(content)
 | 
			
		||||
    if len(gpx_content.tracks) == 0:
 | 
			
		||||
        return None
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,17 @@
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
import shortuuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def encode_uuid(uuid_value):
 | 
			
		||||
def encode_uuid(uuid_value: UUID) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Return short id string from an UUID
 | 
			
		||||
    """
 | 
			
		||||
    return shortuuid.encode(uuid_value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def decode_short_id(short_id):
 | 
			
		||||
def decode_short_id(short_id: str) -> UUID:
 | 
			
		||||
    """
 | 
			
		||||
    Return UUID from a short id string
 | 
			
		||||
    """
 | 
			
		||||
    return shortuuid.decode(short_id)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
import os
 | 
			
		||||
from typing import Dict, Optional
 | 
			
		||||
 | 
			
		||||
import forecastio
 | 
			
		||||
import pytz
 | 
			
		||||
from fittrackee import appLog
 | 
			
		||||
from gpxpy.gpx import GPXRoutePoint
 | 
			
		||||
 | 
			
		||||
API_KEY = os.getenv('WEATHER_API_KEY')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_weather(point):
 | 
			
		||||
def get_weather(point: GPXRoutePoint) -> Optional[Dict]:
 | 
			
		||||
    if not API_KEY or API_KEY == '':
 | 
			
		||||
        return None
 | 
			
		||||
    try:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user