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.workouts.models import Sport, Workout, WorkoutSegment
from fittrackee.workouts.utils import StaticMap
from fittrackee.workouts.utils.maps import StaticMap
byte_io = BytesIO()
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.workouts.models import Sport
from fittrackee.workouts.utils import process_files
from fittrackee.workouts.utils.workouts import process_files
folders = {
'extract_dir': '/tmp/fitTrackee/uploads',
@ -38,7 +38,7 @@ class TestStoppedSpeedThreshold:
expected_threshold: float,
) -> None:
with patch(
'fittrackee.workouts.utils.get_new_file_path',
'fittrackee.workouts.utils.workouts.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png',
), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
@ -68,7 +68,7 @@ class TestStoppedSpeedThreshold:
expected_threshold = 0.7
user_sport_1_preference.stopped_speed_threshold = expected_threshold
with patch(
'fittrackee.workouts.utils.get_new_file_path',
'fittrackee.workouts.utils.workouts.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png',
), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data',

View File

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

View File

@ -11,7 +11,7 @@ from flask import Flask
from fittrackee.users.models import User
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

View File

@ -3,9 +3,9 @@ import os
from flask import Flask
from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils import get_absolute_file_path
from ..api_test_case import ApiTestCaseMixin
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.users.models import User
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:

View File

@ -5,7 +5,7 @@ from uuid import uuid4
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:

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.files import get_absolute_file_path
from .utils_format import convert_in_duration, convert_value_to_integer
from .utils_id import encode_uuid
from .utils.convert import convert_in_duration, convert_value_to_integer
from .utils.short_id import encode_uuid
BaseModel: DeclarativeMeta = db.Model
record_types = [

View File

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

View File

View File

@ -3,8 +3,8 @@ from typing import Any, Dict, List, Optional, Tuple
import gpxpy.gpx
from .exceptions import WorkoutGPXException
from .utils_weather import get_weather
from ..exceptions import WorkoutGPXException
from .weather import get_weather
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:
"""
Return short id string from an UUID
Return short id string from a UUID
"""
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 tempfile
import zipfile
@ -10,7 +9,6 @@ import gpxpy.gpx
import pytz
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
@ -18,9 +16,10 @@ from fittrackee import db
from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User, UserSportPreference
from .exceptions import WorkoutException
from .models import Sport, Workout, WorkoutSegment
from .utils_gpx import get_gpx_info
from ..exceptions import WorkoutException
from ..models import Sport, Workout, WorkoutSegment
from .gpx import get_gpx_info
from .maps import generate_map, get_map_hash
def get_datetime_with_tz(
@ -258,39 +257,11 @@ def get_new_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(
params: Dict, filename: str, stopped_speed_threshold: float
) -> 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:
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(
nb_workouts: int, total_average_speed: float, workout_average_speed: float
) -> float:

View File

@ -33,7 +33,14 @@ from fittrackee.users.models import User
from fittrackee.users.utils import can_view_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,
create_workout,
edit_workout,
@ -41,13 +48,6 @@ from .utils import (
get_datetime_from_request_args,
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__)