Merge branch 'api-refacto' into dev_bis

This commit is contained in:
Sam 2022-02-20 10:02:10 +01:00
commit 83f234d574
43 changed files with 319 additions and 320 deletions

View File

@ -16,13 +16,13 @@ from flask_dramatiq import Dramatiq
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from fittrackee.emails.email import Email
from fittrackee.emails.email import EmailService
VERSION = __version__ = '0.5.7'
db = SQLAlchemy()
bcrypt = Bcrypt()
migrate = Migrate()
email_service = Email()
email_service = EmailService()
dramatiq = Dramatiq()
log_file = os.getenv('APP_LOG')
logging.basicConfig(

View File

@ -7,8 +7,9 @@ from typing import Dict, Optional, Type, Union
from flask import Flask
from jinja2 import Environment, FileSystemLoader, select_autoescape
from urllib3.util import parse_url
from .utils_email import parse_email_url
from .exceptions import InvalidEmailUrlScheme
email_log = logging.getLogger('fittrackee_api_email')
email_log.setLevel(logging.DEBUG)
@ -69,7 +70,7 @@ class EmailTemplate:
return message.generate_message()
class Email:
class EmailService:
def __init__(self, app: Optional[Flask] = None) -> None:
self.host = 'localhost'
self.port = 25
@ -83,7 +84,7 @@ class Email:
self.init_email(app)
def init_email(self, app: Flask) -> None:
parsed_url = parse_email_url(app.config['EMAIL_URL'])
parsed_url = self.parse_email_url(app.config['EMAIL_URL'])
self.host = parsed_url['host']
self.port = parsed_url['port']
self.use_tls = parsed_url['use_tls']
@ -93,6 +94,23 @@ class Email:
self.sender_email = app.config['SENDER_EMAIL']
self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
@staticmethod
def parse_email_url(email_url: str) -> Dict:
parsed_url = parse_url(email_url)
if parsed_url.scheme != 'smtp':
raise InvalidEmailUrlScheme()
credentials = (
parsed_url.auth.split(':') if parsed_url.auth else [None, None]
)
return {
'host': parsed_url.host,
'port': 25 if parsed_url.port is None else parsed_url.port,
'use_tls': True if parsed_url.query == 'tls=True' else False,
'use_ssl': True if parsed_url.query == 'ssl=True' else False,
'username': credentials[0],
'password': credentials[1],
}
@property
def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]:
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP

View File

@ -1,22 +0,0 @@
from typing import Dict
from urllib3.util import parse_url
from .exceptions import InvalidEmailUrlScheme
def parse_email_url(email_url: str) -> Dict:
parsed_url = parse_url(email_url)
if parsed_url.scheme != 'smtp':
raise InvalidEmailUrlScheme()
credentials = (
parsed_url.auth.split(':') if parsed_url.auth else [None, None]
)
return {
'host': parsed_url.host,
'port': 25 if parsed_url.port is None else parsed_url.port,
'use_tls': True if parsed_url.query == 'tls=True' else False,
'use_ssl': True if parsed_url.query == 'ssl=True' else False,
'username': credentials[0],
'password': credentials[1],
}

23
fittrackee/files.py Normal file
View File

@ -0,0 +1,23 @@
import os
from typing import Union
from flask import current_app
def display_readable_file_size(size_in_bytes: Union[float, int]) -> str:
"""
Return readable file size from size in bytes
"""
if size_in_bytes == 0:
return '0 bytes'
if size_in_bytes == 1:
return '1 byte'
for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']:
if abs(size_in_bytes) < 1024.0:
return f'{size_in_bytes:3.1f}{unit}'
size_in_bytes /= 1024.0
return f'{size_in_bytes} bytes'
def get_absolute_file_path(relative_path: str) -> str:
return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)

View File

@ -1,31 +1,17 @@
from json import dumps
from typing import Dict, List, Optional, Union
from flask import Response
from flask import Request, Response, current_app
from flask_sqlalchemy import SQLAlchemy
from fittrackee import appLog
from fittrackee.files import display_readable_file_size
def get_empty_data_for_datatype(data_type: str) -> Union[str, List]:
return '' if data_type in ['gpx', 'chart_data'] else []
def display_readable_file_size(size_in_bytes: Union[float, int]) -> str:
"""
Return readable file size from size in bytes
"""
if size_in_bytes == 0:
return '0 bytes'
if size_in_bytes == 1:
return '1 byte'
for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']:
if abs(size_in_bytes) < 1024.0:
return f'{size_in_bytes:3.1f}{unit}'
size_in_bytes /= 1024.0
return f'{size_in_bytes} bytes'
class HttpResponse(Response):
def __init__(
self,
@ -157,3 +143,48 @@ def handle_error_and_return_response(
db.session.rollback()
appLog.error(error)
return InternalServerErrorResponse(message=message, status=status)
def get_error_response_if_file_is_invalid(
file_type: str, req: Request
) -> Optional[HttpResponse]:
if 'file' not in req.files:
return InvalidPayloadErrorResponse('no file part', 'fail')
file = req.files['file']
if not file.filename or file.filename == '':
return InvalidPayloadErrorResponse('no selected file', 'fail')
allowed_extensions = (
'WORKOUT_ALLOWED_EXTENSIONS'
if file_type == 'workout'
else 'PICTURE_ALLOWED_EXTENSIONS'
)
file_extension = (
file.filename.rsplit('.', 1)[1].lower()
if '.' in file.filename
else None
)
max_file_size = current_app.config['max_single_file_size']
if not (
file_extension
and file_extension in current_app.config[allowed_extensions]
):
return InvalidPayloadErrorResponse(
'file extension not allowed', 'fail'
)
if (
file_extension != 'zip'
and req.content_length is not None
and req.content_length > max_file_size
):
return PayloadTooLargeErrorResponse(
file_type=file_type,
file_size=req.content_length,
max_size=max_file_size,
)
return None

View File

@ -1,9 +1,11 @@
from unittest.mock import Mock, patch
import pytest
from flask import Flask
from fittrackee import email_service
from fittrackee.emails.email import EmailMessage
from fittrackee.emails.exceptions import InvalidEmailUrlScheme
from ..api_test_case import CallArgsMixin
from .template_results.password_reset_request import expected_en_text_body
@ -32,7 +34,62 @@ class TestEmailMessage:
assert 'Hello !' in message_string
class TestEmailSending(CallArgsMixin):
class TestEmailServiceUrlParser(CallArgsMixin):
def test_it_raises_error_if_url_scheme_is_invalid(self) -> None:
url = 'stmp://username:password@localhost:587'
with pytest.raises(InvalidEmailUrlScheme):
email_service.parse_email_url(url)
@staticmethod
def assert_parsed_email(url: str) -> None:
parsed_email = email_service.parse_email_url(url)
assert parsed_email['username'] is None
assert parsed_email['password'] is None
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 25
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_without_port(self) -> None:
url = 'smtp://localhost'
self.assert_parsed_email(url)
def test_it_parses_email_url_without_authentication(self) -> None:
url = 'smtp://localhost:25'
self.assert_parsed_email(url)
def test_it_parses_email_url(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:25'
parsed_email = email_service.parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 25
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_tls(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:587?tls=True'
parsed_email = email_service.parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 587
assert parsed_email['use_tls'] is True
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_ssl(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:465?ssl=True'
parsed_email = email_service.parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 465
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is True
class TestEmailServiceSend(CallArgsMixin):
email_data = {
'expiration_delay': '3 seconds',

View File

@ -1,61 +0,0 @@
import pytest
from fittrackee.emails.utils_email import (
InvalidEmailUrlScheme,
parse_email_url,
)
class TestEmailUrlParser:
def test_it_raises_error_if_url_scheme_is_invalid(self) -> None:
url = 'stmp://username:password@localhost:587'
with pytest.raises(InvalidEmailUrlScheme):
parse_email_url(url)
@staticmethod
def assert_parsed_email(url: str) -> None:
parsed_email = parse_email_url(url)
assert parsed_email['username'] is None
assert parsed_email['password'] is None
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 25
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_without_port(self) -> None:
url = 'smtp://localhost'
self.assert_parsed_email(url)
def test_it_parses_email_url_without_authentication(self) -> None:
url = 'smtp://localhost:25'
self.assert_parsed_email(url)
def test_it_parses_email_url(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:25'
parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 25
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_tls(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:587?tls=True'
parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 587
assert parsed_email['use_tls'] is True
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_ssl(self) -> None:
url = 'smtp://test@example.com:12345678@localhost:465?ssl=True'
parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com'
assert parsed_email['password'] == '12345678'
assert parsed_email['host'] == 'localhost'
assert parsed_email['port'] == 465
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is True

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

@ -3,7 +3,7 @@ from uuid import uuid4
import pytest
from fittrackee.responses import display_readable_file_size
from fittrackee.files import display_readable_file_size
from fittrackee.utils import get_readable_duration

View File

@ -8,7 +8,7 @@ from flask import Flask
from freezegun import freeze_time
from fittrackee.users.models import User, UserSportPreference
from fittrackee.users.utils_token import get_user_token
from fittrackee.users.utils.token import get_user_token
from fittrackee.workouts.models import Sport, Workout
from ..api_test_case import ApiTestCaseMixin

View File

@ -5,12 +5,12 @@ from flask import Flask
from fittrackee.users.exceptions import UserNotFoundException
from fittrackee.users.models import User
from fittrackee.users.utils import (
from fittrackee.users.utils.admin import set_admin_rights
from fittrackee.users.utils.controls import (
check_passwords,
check_username,
is_valid_email,
register_controls,
set_admin_rights,
)
from ..utils import random_string
@ -163,7 +163,7 @@ class TestIsUsernameValid:
class TestRegisterControls:
module_path = 'fittrackee.users.utils.'
module_path = 'fittrackee.users.utils.controls.'
valid_username = random_string()
valid_email = f'{random_string()}@example.com'
valid_password = random_string()

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

@ -10,6 +10,8 @@ from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.utils import secure_filename
from fittrackee import appLog, bcrypt, db
from fittrackee.emails.tasks import reset_password_email
from fittrackee.files import get_absolute_file_path
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
@ -17,17 +19,16 @@ from fittrackee.responses import (
NotFoundErrorResponse,
PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse,
get_error_response_if_file_is_invalid,
handle_error_and_return_response,
)
from fittrackee.tasks import reset_password_email
from fittrackee.utils import get_readable_duration, verify_extension_and_size
from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Sport
from fittrackee.workouts.utils_files import get_absolute_file_path
from .decorators import authenticate
from .models import User, UserSportPreference
from .utils import check_passwords, register_controls
from .utils_token import decode_user_token
from .utils.controls import check_passwords, register_controls
from .utils.token import decode_user_token
auth_blueprint = Blueprint('auth', __name__)
@ -890,7 +891,9 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
"""
try:
response_object = verify_extension_and_size('picture', request)
response_object = get_error_response_if_file_is_invalid(
'picture', request
)
except RequestEntityTooLarge as e:
appLog.error(e)
return PayloadTooLargeErrorResponse(

View File

@ -5,7 +5,7 @@ from flask import request
from fittrackee.responses import HttpResponse
from .utils import verify_user
from .utils.controls import verify_user
def verify_auth_user(

View File

@ -11,7 +11,7 @@ from sqlalchemy.sql.expression import select
from fittrackee import bcrypt, db
from fittrackee.workouts.models import Workout
from .utils_token import decode_user_token, get_user_token
from .utils.token import decode_user_token, get_user_token
BaseModel: DeclarativeMeta = db.Model

View File

@ -7,6 +7,7 @@ from flask import Blueprint, request, send_file
from sqlalchemy import exc
from fittrackee import db
from fittrackee.files import get_absolute_file_path
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
@ -16,12 +17,11 @@ from fittrackee.responses import (
handle_error_and_return_response,
)
from fittrackee.workouts.models import Record, Workout, WorkoutSegment
from fittrackee.workouts.utils_files import get_absolute_file_path
from .decorators import authenticate, authenticate_as_admin
from .exceptions import UserNotFoundException
from .models import User, UserSportPreference
from .utils import set_admin_rights
from .utils.admin import set_admin_rights
users_blueprint = Blueprint('users', __name__)

View File

View File

@ -0,0 +1,12 @@
from fittrackee import db
from ..exceptions import UserNotFoundException
from ..models import User
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

@ -3,15 +3,13 @@ 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
from ..models import User
def is_valid_email(email: str) -> bool:
@ -88,22 +86,3 @@ def verify_user(
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

@ -2,61 +2,6 @@ from datetime import timedelta
from typing import Optional
import humanize
from flask import Request, current_app
from .responses import (
HttpResponse,
InvalidPayloadErrorResponse,
PayloadTooLargeErrorResponse,
)
def verify_extension_and_size(
file_type: str, req: Request
) -> Optional[HttpResponse]:
"""
Return error Response if file is invalid
"""
if 'file' not in req.files:
return InvalidPayloadErrorResponse('no file part', 'fail')
file = req.files['file']
if not file.filename or file.filename == '':
return InvalidPayloadErrorResponse('no selected file', 'fail')
allowed_extensions = (
'WORKOUT_ALLOWED_EXTENSIONS'
if file_type == 'workout'
else 'PICTURE_ALLOWED_EXTENSIONS'
)
file_extension = (
file.filename.rsplit('.', 1)[1].lower()
if '.' in file.filename
else None
)
max_file_size = current_app.config['max_single_file_size']
if not (
file_extension
and file_extension in current_app.config[allowed_extensions]
):
return InvalidPayloadErrorResponse(
'file extension not allowed', 'fail'
)
if (
file_extension != 'zip'
and req.content_length is not None
and req.content_length > max_file_size
):
return PayloadTooLargeErrorResponse(
file_type=file_type,
file_size=req.content_length,
max_size=max_file_size,
)
return None
def get_readable_duration(duration: int, locale: Optional[str] = None) -> str:

View File

@ -13,10 +13,10 @@ from sqlalchemy.orm.session import Session, object_session
from sqlalchemy.types import JSON, Enum
from fittrackee import db
from fittrackee.files import get_absolute_file_path
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
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

@ -0,0 +1,14 @@
from typing import Optional
from fittrackee.responses import ForbiddenErrorResponse, HttpResponse
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

View File

@ -1,4 +1,3 @@
import hashlib
import os
import tempfile
import zipfile
@ -10,17 +9,17 @@ 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
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_files import get_absolute_file_path
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

@ -1,7 +0,0 @@
import os
from flask import current_app
def get_absolute_file_path(relative_path: str) -> str:
return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)

View File

@ -25,15 +25,22 @@ from fittrackee.responses import (
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
PayloadTooLargeErrorResponse,
get_error_response_if_file_is_invalid,
handle_error_and_return_response,
)
from fittrackee.users.decorators import authenticate
from fittrackee.users.models import User
from fittrackee.users.utils import can_view_workout
from fittrackee.utils import verify_extension_and_size
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.visibility import can_view_workout
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__)
@ -961,7 +961,9 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
"""
try:
error_response = verify_extension_and_size('workout', request)
error_response = get_error_response_if_file_is_invalid(
'workout', request
)
except RequestEntityTooLarge as e:
appLog.error(e)
return PayloadTooLargeErrorResponse(

67
poetry.lock generated
View File

@ -124,7 +124,7 @@ pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.11"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
@ -256,7 +256,7 @@ pyflakes = ">=2.3.0,<2.4.0"
[[package]]
name = "flask"
version = "2.0.2"
version = "2.0.3"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
@ -377,11 +377,11 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "humanize"
version = "3.14.0"
version = "4.0.0"
description = "Python humanize utilities"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
@ -407,7 +407,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "4.11.0"
version = "4.11.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
@ -420,7 +420,7 @@ zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]]
name = "importlib-resources"
@ -790,17 +790,16 @@ pytest-metadata = "*"
[[package]]
name = "pytest-isort"
version = "2.0.0"
version = "3.0.0"
description = "py.test plugin to check import ordering using isort"
category = "dev"
optional = false
python-versions = "*"
python-versions = ">=3.6,<4"
[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
isort = ">=4.0"
[package.extras]
tests = ["mock"]
pytest = ">=5.0"
[[package]]
name = "pytest-metadata"
@ -907,7 +906,7 @@ sphinx = ">=1.3.1"
[[package]]
name = "redis"
version = "4.1.3"
version = "4.1.4"
description = "Python client for Redis database and key-value store"
category = "main"
optional = false
@ -1254,7 +1253,7 @@ python-versions = "*"
[[package]]
name = "types-pytz"
version = "2021.3.4"
version = "2021.3.5"
description = "Typing stubs for pytz"
category = "dev"
optional = false
@ -1262,7 +1261,7 @@ python-versions = "*"
[[package]]
name = "types-requests"
version = "2.27.9"
version = "2.27.10"
description = "Typing stubs for requests"
category = "dev"
optional = false
@ -1281,7 +1280,7 @@ python-versions = "*"
[[package]]
name = "typing-extensions"
version = "4.0.1"
version = "4.1.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "main"
optional = false
@ -1351,7 +1350,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "ec6da4aaa4cef6ee6c235ef4d2f101b533409097ee8b5169596373a5b4c60cdb"
content-hash = "42616189ff19a6e3c7c397a44a7622d6b97ca263924860f15cf0bc781e36e40d"
[metadata.files]
alabaster = [
@ -1469,8 +1468,8 @@ cffi = [
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"},
{file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"},
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
@ -1566,8 +1565,8 @@ flake8 = [
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
flask = [
{file = "Flask-2.0.2-py3-none-any.whl", hash = "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a"},
{file = "Flask-2.0.2.tar.gz", hash = "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2"},
{file = "Flask-2.0.3-py3-none-any.whl", hash = "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f"},
{file = "Flask-2.0.3.tar.gz", hash = "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d"},
]
flask-bcrypt = [
{file = "Flask-Bcrypt-0.7.1.tar.gz", hash = "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"},
@ -1651,8 +1650,8 @@ h11 = [
{file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"},
]
humanize = [
{file = "humanize-3.14.0-py3-none-any.whl", hash = "sha256:32bcf712ac98ff5e73627a9d31e1ba5650619008d6d13543b5d53b48e8ab8d43"},
{file = "humanize-3.14.0.tar.gz", hash = "sha256:60dd8c952b1df1ad83f0903844dec50a34ba7a04eea22a6b14204ffb62dbb0a4"},
{file = "humanize-4.0.0-py3-none-any.whl", hash = "sha256:8d86333b8557dacffd4dce1dbe09c81c189e2caf7bb17a970b2212f0f58f10f2"},
{file = "humanize-4.0.0.tar.gz", hash = "sha256:ee1f872fdfc7d2ef4a28d4f80ddde9f96d36955b5d6b0dac4bdeb99502bddb00"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
@ -1663,8 +1662,8 @@ imagesize = [
{file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"},
]
importlib-metadata = [
{file = "importlib_metadata-4.11.0-py3-none-any.whl", hash = "sha256:6affcdb3aec542dd98df8211e730bba6c5f2bec8288d47bacacde898f548c9ad"},
{file = "importlib_metadata-4.11.0.tar.gz", hash = "sha256:9e5e553bbba1843cb4a00823014b907616be46ee503d2b9ba001d214a8da218f"},
{file = "importlib_metadata-4.11.1-py3-none-any.whl", hash = "sha256:e0bc84ff355328a4adfc5240c4f211e0ab386f80aa640d1b11f0618a1d282094"},
{file = "importlib_metadata-4.11.1.tar.gz", hash = "sha256:175f4ee440a0317f6e8d81b7f8d4869f93316170a65ad2b007d2929186c8052c"},
]
importlib-resources = [
{file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"},
@ -1931,8 +1930,8 @@ pytest-html = [
{file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"},
]
pytest-isort = [
{file = "pytest-isort-2.0.0.tar.gz", hash = "sha256:821a8c5c9c4f3a3c52cfa9c541fbe89ac9e28728125125af53724c4c3f129117"},
{file = "pytest_isort-2.0.0-py3-none-any.whl", hash = "sha256:ab949c593213dad38ba75db32a0ce361fcddd11d4152be4a2c93b85104cc4376"},
{file = "pytest-isort-3.0.0.tar.gz", hash = "sha256:4fe4b26ead2af776730ec23f5870d7421f35aace22a41c4e938586ef4d8787cb"},
{file = "pytest_isort-3.0.0-py3-none-any.whl", hash = "sha256:2d96a25a135d6fd084ac36878e7d54f26f27c6987c2c65f0d12809bffade9cb9"},
]
pytest-metadata = [
{file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"},
@ -1966,8 +1965,8 @@ recommonmark = [
{file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"},
]
redis = [
{file = "redis-4.1.3-py3-none-any.whl", hash = "sha256:267e89e476eb684517584e8988f1e5d755f483a368c133020c4c40e8b676bc5d"},
{file = "redis-4.1.3.tar.gz", hash = "sha256:f2715caad9f0e8c6ff8df46d3c4c9022a3929001f530f66b62747554d3067068"},
{file = "redis-4.1.4-py3-none-any.whl", hash = "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a"},
{file = "redis-4.1.4.tar.gz", hash = "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"},
]
requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
@ -2128,20 +2127,20 @@ types-freezegun = [
{file = "types_freezegun-1.1.6-py3-none-any.whl", hash = "sha256:eaa4ccac7f4ff92762b6e5d34c3c4e41a7763b6d09a8595e0224ff1f24c9d4e1"},
]
types-pytz = [
{file = "types-pytz-2021.3.4.tar.gz", hash = "sha256:101da53091013bb07403468c20d36930d749d3918054ac46f9c1bfc607dadf7d"},
{file = "types_pytz-2021.3.4-py3-none-any.whl", hash = "sha256:ccfa2ed29f816e3de2f882541c06ad2791f808a79cfe38265411820190999f0f"},
{file = "types-pytz-2021.3.5.tar.gz", hash = "sha256:fef8de238ee95135952229a2a23bfb87bd63d5a6c8598106a46cfcf48f069ea8"},
{file = "types_pytz-2021.3.5-py3-none-any.whl", hash = "sha256:8831f689379ac9e2a62668157381379ed74b3702980e08e71f8673c179c4e3c7"},
]
types-requests = [
{file = "types-requests-2.27.9.tar.gz", hash = "sha256:7368974534d297939492efdfdab232930440b11e2203f6df1f0c40e3242a87ea"},
{file = "types_requests-2.27.9-py3-none-any.whl", hash = "sha256:74070045418faf710f3154403d6a16c9e67db50e5119906ca6955f1658d20f7b"},
{file = "types-requests-2.27.10.tar.gz", hash = "sha256:5dcb088fcaa778efeee6b7fc46967037e983fbfb9fec02594578bd33fd75e555"},
{file = "types_requests-2.27.10-py3-none-any.whl", hash = "sha256:6cb4fb0bbcbc585c57eeee6ffe5a47638dc89706b8d290ec89a77213fc5bad1a"},
]
types-urllib3 = [
{file = "types-urllib3-1.26.9.tar.gz", hash = "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c"},
{file = "types_urllib3-1.26.9-py3-none-any.whl", hash = "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9"},
]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]
urllib3 = [
{file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"},

View File

@ -31,7 +31,7 @@ flask-dramatiq = "^0.6.0"
flask-migrate = "^3.1"
gpxpy = "=1.3.4"
gunicorn = "^20.1"
humanize = "^3.14"
humanize = "^4.0"
psycopg2-binary = "^2.9"
pyjwt = "^2.3"
python-forecastio = "^1.4"
@ -49,7 +49,7 @@ pytest = "^7.0"
pytest-black = "^0.3.12"
pytest-cov = "^3.0"
pytest-flake8 = "^1.0"
pytest-isort = "^2.0"
pytest-isort = "^3.0"
pytest-runner = "^5.3"
pytest-selenium = "^2.0.1"
recommonmark = "^0.7"