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