API - workouts refactoring

This commit is contained in:
Sam 2022-02-16 17:46:22 +01:00
parent 0b2e2ed5dd
commit 1b4a477544
20 changed files with 81 additions and 184 deletions

View File

@ -10,7 +10,7 @@ from werkzeug.datastructures import FileStorage
from fittrackee import db from fittrackee import db
from fittrackee.workouts.models import Sport, Workout, WorkoutSegment from fittrackee.workouts.models import Sport, Workout, WorkoutSegment
from fittrackee.workouts.utils import StaticMap from fittrackee.workouts.utils.maps import StaticMap
byte_io = BytesIO() byte_io = BytesIO()
Image.new('RGB', (256, 256)).save(byte_io, 'PNG') Image.new('RGB', (256, 256)).save(byte_io, 'PNG')

View File

@ -7,7 +7,7 @@ from werkzeug.datastructures import FileStorage
from fittrackee.users.models import User, UserSportPreference from fittrackee.users.models import User, UserSportPreference
from fittrackee.workouts.models import Sport from fittrackee.workouts.models import Sport
from fittrackee.workouts.utils import process_files from fittrackee.workouts.utils.workouts import process_files
folders = { folders = {
'extract_dir': '/tmp/fitTrackee/uploads', 'extract_dir': '/tmp/fitTrackee/uploads',
@ -38,7 +38,7 @@ class TestStoppedSpeedThreshold:
expected_threshold: float, expected_threshold: float,
) -> None: ) -> None:
with patch( with patch(
'fittrackee.workouts.utils.get_new_file_path', 'fittrackee.workouts.utils.workouts.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png', return_value='/tmp/fitTrackee/uploads/test.png',
), patch( ), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data', 'gpxpy.gpx.GPXTrackSegment.get_moving_data',
@ -68,7 +68,7 @@ class TestStoppedSpeedThreshold:
expected_threshold = 0.7 expected_threshold = 0.7
user_sport_1_preference.stopped_speed_threshold = expected_threshold user_sport_1_preference.stopped_speed_threshold = expected_threshold
with patch( with patch(
'fittrackee.workouts.utils.get_new_file_path', 'fittrackee.workouts.utils.workouts.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png', return_value='/tmp/fitTrackee/uploads/test.png',
), patch( ), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data', 'gpxpy.gpx.GPXTrackSegment.get_moving_data',

View File

@ -3,7 +3,7 @@ from typing import List
import pytest import pytest
from fittrackee.workouts.utils import get_average_speed from fittrackee.workouts.utils.workouts import get_average_speed
class TestWorkoutAverageSpeed: class TestWorkoutAverageSpeed:

View File

@ -11,7 +11,7 @@ from flask import Flask
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils_id import decode_short_id from fittrackee.workouts.utils.short_id import decode_short_id
from ..api_test_case import ApiTestCaseMixin, CallArgsMixin from ..api_test_case import ApiTestCaseMixin, CallArgsMixin

View File

@ -3,9 +3,9 @@ import os
from flask import Flask from flask import Flask
from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils import get_absolute_file_path
from ..api_test_case import ApiTestCaseMixin from ..api_test_case import ApiTestCaseMixin
from .utils import get_random_short_id, post_an_workout from .utils import get_random_short_id, post_an_workout

View File

@ -5,7 +5,7 @@ from flask import Flask
from fittrackee import db from fittrackee import db
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils_id import decode_short_id from fittrackee.workouts.utils.short_id import decode_short_id
class TestWorkoutModel: class TestWorkoutModel:

View File

@ -5,7 +5,7 @@ from uuid import uuid4
from flask import Flask from flask import Flask
from fittrackee.workouts.utils_id import encode_uuid from fittrackee.workouts.utils.short_id import encode_uuid
def get_random_short_id() -> str: def get_random_short_id() -> str:

View File

@ -1,109 +0,0 @@
import re
from typing import Optional, Tuple
from flask import Request
from fittrackee import db
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
UnauthorizedErrorResponse,
)
from .exceptions import UserNotFoundException
from .models import User
def is_valid_email(email: str) -> bool:
"""
Return if email format is valid
"""
mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
return re.match(mail_pattern, email) is not None
def check_passwords(password: str, password_conf: str) -> str:
"""
Verify if password and password confirmation are the same and have
more than 8 characters
If not, it returns not empty string
"""
ret = ''
if password_conf != password:
ret = 'password: password and password confirmation do not match\n'
if len(password) < 8:
ret += 'password: 8 characters required\n'
return ret
def check_username(username: str) -> str:
"""
Return if username is valid
"""
ret = ''
if not 2 < len(username) < 13:
ret += 'username: 3 to 12 characters required\n'
if not re.match(r'^[a-zA-Z0-9_]+$', username):
ret += (
'username: only alphanumeric characters and the '
'underscore character "_" allowed\n'
)
return ret
def register_controls(
username: str, email: str, password: str, password_conf: str
) -> str:
"""
Verify if username, email and passwords are valid
If not, it returns not empty string
"""
ret = check_username(username)
if not is_valid_email(email):
ret += 'email: valid email must be provided\n'
ret += check_passwords(password, password_conf)
return ret
def verify_user(
current_request: Request, verify_admin: bool
) -> Tuple[Optional[HttpResponse], Optional[User]]:
"""
Return authenticated user, if the provided token is valid and user has
admin rights if 'verify_admin' is True
"""
default_message = 'provide a valid auth token'
auth_header = current_request.headers.get('Authorization')
if not auth_header:
return UnauthorizedErrorResponse(default_message), None
auth_token = auth_header.split(' ')[1]
resp = User.decode_auth_token(auth_token)
if isinstance(resp, str):
return UnauthorizedErrorResponse(resp), None
user = User.query.filter_by(id=resp).first()
if not user:
return UnauthorizedErrorResponse(default_message), None
if verify_admin and not user.admin:
return ForbiddenErrorResponse(), None
return None, user
def can_view_workout(
auth_user_id: int, workout_user_id: int
) -> Optional[HttpResponse]:
"""
Return error response if user has no right to view workout
"""
if auth_user_id != workout_user_id:
return ForbiddenErrorResponse()
return None
def set_admin_rights(username: str) -> None:
user = User.query.filter_by(username=username).first()
if not user:
raise UserNotFoundException()
user.admin = True
db.session.commit()

View File

@ -15,8 +15,8 @@ from sqlalchemy.types import JSON, Enum
from fittrackee import db from fittrackee import db
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
from .utils_format import convert_in_duration, convert_value_to_integer from .utils.convert import convert_in_duration, convert_value_to_integer
from .utils_id import encode_uuid from .utils.short_id import encode_uuid
BaseModel: DeclarativeMeta = db.Model BaseModel: DeclarativeMeta = db.Model
record_types = [ record_types = [

View File

@ -16,12 +16,9 @@ from fittrackee.users.decorators import authenticate, authenticate_as_admin
from fittrackee.users.models import User from fittrackee.users.models import User
from .models import Sport, Workout from .models import Sport, Workout
from .utils import ( from .utils.convert import convert_timedelta_to_integer
get_average_speed, from .utils.uploads import get_upload_dir_size
get_datetime_from_request_args, from .utils.workouts import get_average_speed, get_datetime_from_request_args
get_upload_dir_size,
)
from .utils_format import convert_timedelta_to_integer
stats_blueprint = Blueprint('stats', __name__) stats_blueprint = Blueprint('stats', __name__)

View File

View File

@ -3,8 +3,8 @@ from typing import Any, Dict, List, Optional, Tuple
import gpxpy.gpx import gpxpy.gpx
from .exceptions import WorkoutGPXException from ..exceptions import WorkoutGPXException
from .utils_weather import get_weather from .weather import get_weather
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]: def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:

View File

@ -0,0 +1,35 @@
import hashlib
from typing import List
from flask import current_app
from staticmap import Line, StaticMap
from fittrackee.files import get_absolute_file_path
def generate_map(map_filepath: str, map_data: List) -> None:
"""
Generate and save map image from map data
"""
m = StaticMap(400, 225, 10)
if not current_app.config['TILE_SERVER']['DEFAULT_STATICMAP']:
m.url_template = current_app.config['TILE_SERVER']['URL'].replace(
'{s}.', ''
)
line = Line(map_data, '#3388FF', 4)
m.add_line(line)
image = m.render()
image.save(map_filepath)
def get_map_hash(map_filepath: str) -> str:
"""
Generate a md5 hash used as id instead of workout id, to retrieve map
image (maps are sensitive data)
"""
md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath)
with open(absolute_map_filepath, 'rb') as f:
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
md5.update(chunk)
return md5.hexdigest()

View File

@ -5,7 +5,7 @@ import shortuuid
def encode_uuid(uuid_value: UUID) -> str: def encode_uuid(uuid_value: UUID) -> str:
""" """
Return short id string from an UUID Return short id string from a UUID
""" """
return shortuuid.encode(uuid_value) return shortuuid.encode(uuid_value)

View File

@ -0,0 +1,16 @@
import os
from fittrackee.files import get_absolute_file_path
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):
for f in filenames:
fp = os.path.join(dir_path, f)
total_size += os.path.getsize(fp)
return total_size

View File

@ -1,4 +1,3 @@
import hashlib
import os import os
import tempfile import tempfile
import zipfile import zipfile
@ -10,7 +9,6 @@ import gpxpy.gpx
import pytz import pytz
from flask import current_app from flask import current_app
from sqlalchemy import exc from sqlalchemy import exc
from staticmap import Line, StaticMap
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -18,9 +16,10 @@ from fittrackee import db
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User, UserSportPreference from fittrackee.users.models import User, UserSportPreference
from .exceptions import WorkoutException from ..exceptions import WorkoutException
from .models import Sport, Workout, WorkoutSegment from ..models import Sport, Workout, WorkoutSegment
from .utils_gpx import get_gpx_info from .gpx import get_gpx_info
from .maps import generate_map, get_map_hash
def get_datetime_with_tz( def get_datetime_with_tz(
@ -258,39 +257,11 @@ def get_new_file_path(
return file_path return file_path
def generate_map(map_filepath: str, map_data: List) -> None:
"""
Generate and save map image from map data
"""
m = StaticMap(400, 225, 10)
if not current_app.config['TILE_SERVER']['DEFAULT_STATICMAP']:
m.url_template = current_app.config['TILE_SERVER']['URL'].replace(
'{s}.', ''
)
line = Line(map_data, '#3388FF', 4)
m.add_line(line)
image = m.render()
image.save(map_filepath)
def get_map_hash(map_filepath: str) -> str:
"""
Generate a md5 hash used as id instead of workout id, to retrieve map
image (maps are sensitive data)
"""
md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath)
with open(absolute_map_filepath, 'rb') as f:
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
md5.update(chunk)
return md5.hexdigest()
def process_one_gpx_file( def process_one_gpx_file(
params: Dict, filename: str, stopped_speed_threshold: float params: Dict, filename: str, stopped_speed_threshold: float
) -> Workout: ) -> Workout:
""" """
Get all data from a gpx file to create an workout with map image Get all data from a gpx file to create a workout with map image
""" """
try: try:
gpx_data, map_data, weather_data = get_gpx_info( gpx_data, map_data, weather_data = get_gpx_info(
@ -433,19 +404,6 @@ def process_files(
) )
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):
for f in filenames:
fp = os.path.join(dir_path, f)
total_size += os.path.getsize(fp)
return total_size
def get_average_speed( def get_average_speed(
nb_workouts: int, total_average_speed: float, workout_average_speed: float nb_workouts: int, total_average_speed: float, workout_average_speed: float
) -> float: ) -> float:

View File

@ -33,7 +33,14 @@ from fittrackee.users.models import User
from fittrackee.users.utils import can_view_workout from fittrackee.users.utils import can_view_workout
from .models import Workout from .models import Workout
from .utils import ( from .utils.convert import convert_in_duration
from .utils.gpx import (
WorkoutGPXException,
extract_segment_from_gpx_file,
get_chart_data,
)
from .utils.short_id import decode_short_id
from .utils.workouts import (
WorkoutException, WorkoutException,
create_workout, create_workout,
edit_workout, edit_workout,
@ -41,13 +48,6 @@ from .utils import (
get_datetime_from_request_args, get_datetime_from_request_args,
process_files, process_files,
) )
from .utils_format import convert_in_duration
from .utils_gpx import (
WorkoutGPXException,
extract_segment_from_gpx_file,
get_chart_data,
)
from .utils_id import decode_short_id
workouts_blueprint = Blueprint('workouts', __name__) workouts_blueprint = Blueprint('workouts', __name__)