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_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from fittrackee.emails.email import Email from fittrackee.emails.email import EmailService
VERSION = __version__ = '0.5.7' VERSION = __version__ = '0.5.7'
db = SQLAlchemy() db = SQLAlchemy()
bcrypt = Bcrypt() bcrypt = Bcrypt()
migrate = Migrate() migrate = Migrate()
email_service = Email() email_service = EmailService()
dramatiq = Dramatiq() dramatiq = Dramatiq()
log_file = os.getenv('APP_LOG') log_file = os.getenv('APP_LOG')
logging.basicConfig( logging.basicConfig(

View File

@ -7,8 +7,9 @@ from typing import Dict, Optional, Type, Union
from flask import Flask from flask import Flask
from jinja2 import Environment, FileSystemLoader, select_autoescape 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 = logging.getLogger('fittrackee_api_email')
email_log.setLevel(logging.DEBUG) email_log.setLevel(logging.DEBUG)
@ -69,7 +70,7 @@ class EmailTemplate:
return message.generate_message() return message.generate_message()
class Email: class EmailService:
def __init__(self, app: Optional[Flask] = None) -> None: def __init__(self, app: Optional[Flask] = None) -> None:
self.host = 'localhost' self.host = 'localhost'
self.port = 25 self.port = 25
@ -83,7 +84,7 @@ class Email:
self.init_email(app) self.init_email(app)
def init_email(self, app: Flask) -> None: 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.host = parsed_url['host']
self.port = parsed_url['port'] self.port = parsed_url['port']
self.use_tls = parsed_url['use_tls'] self.use_tls = parsed_url['use_tls']
@ -93,6 +94,23 @@ class Email:
self.sender_email = app.config['SENDER_EMAIL'] self.sender_email = app.config['SENDER_EMAIL']
self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER']) 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 @property
def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]: def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]:
return smtplib.SMTP_SSL if self.use_ssl else 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 json import dumps
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from flask import Response from flask import Request, Response, current_app
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from fittrackee import appLog from fittrackee import appLog
from fittrackee.files import display_readable_file_size
def get_empty_data_for_datatype(data_type: str) -> Union[str, List]: def get_empty_data_for_datatype(data_type: str) -> Union[str, List]:
return '' if data_type in ['gpx', 'chart_data'] else [] 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): class HttpResponse(Response):
def __init__( def __init__(
self, self,
@ -157,3 +143,48 @@ def handle_error_and_return_response(
db.session.rollback() db.session.rollback()
appLog.error(error) appLog.error(error)
return InternalServerErrorResponse(message=message, status=status) 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 from unittest.mock import Mock, patch
import pytest
from flask import Flask from flask import Flask
from fittrackee import email_service from fittrackee import email_service
from fittrackee.emails.email import EmailMessage from fittrackee.emails.email import EmailMessage
from fittrackee.emails.exceptions import InvalidEmailUrlScheme
from ..api_test_case import CallArgsMixin from ..api_test_case import CallArgsMixin
from .template_results.password_reset_request import expected_en_text_body from .template_results.password_reset_request import expected_en_text_body
@ -32,7 +34,62 @@ class TestEmailMessage:
assert 'Hello !' in message_string 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 = { email_data = {
'expiration_delay': '3 seconds', '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 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

@ -3,7 +3,7 @@ from uuid import uuid4
import pytest 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 from fittrackee.utils import get_readable_duration

View File

@ -8,7 +8,7 @@ from flask import Flask
from freezegun import freeze_time from freezegun import freeze_time
from fittrackee.users.models import User, UserSportPreference 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 fittrackee.workouts.models import Sport, Workout
from ..api_test_case import ApiTestCaseMixin 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.exceptions import UserNotFoundException
from fittrackee.users.models import User 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_passwords,
check_username, check_username,
is_valid_email, is_valid_email,
register_controls, register_controls,
set_admin_rights,
) )
from ..utils import random_string from ..utils import random_string
@ -163,7 +163,7 @@ class TestIsUsernameValid:
class TestRegisterControls: class TestRegisterControls:
module_path = 'fittrackee.users.utils.' module_path = 'fittrackee.users.utils.controls.'
valid_username = random_string() valid_username = random_string()
valid_email = f'{random_string()}@example.com' valid_email = f'{random_string()}@example.com'
valid_password = random_string() valid_password = random_string()

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

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

View File

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

View File

@ -11,7 +11,7 @@ from sqlalchemy.sql.expression import select
from fittrackee import bcrypt, db from fittrackee import bcrypt, db
from fittrackee.workouts.models import Workout 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 BaseModel: DeclarativeMeta = db.Model

View File

@ -7,6 +7,7 @@ from flask import Blueprint, request, send_file
from sqlalchemy import exc from sqlalchemy import exc
from fittrackee import db from fittrackee import db
from fittrackee.files import get_absolute_file_path
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse, HttpResponse,
@ -16,12 +17,11 @@ from fittrackee.responses import (
handle_error_and_return_response, handle_error_and_return_response,
) )
from fittrackee.workouts.models import Record, Workout, WorkoutSegment 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 .decorators import authenticate, authenticate_as_admin
from .exceptions import UserNotFoundException from .exceptions import UserNotFoundException
from .models import User, UserSportPreference from .models import User, UserSportPreference
from .utils import set_admin_rights from .utils.admin import set_admin_rights
users_blueprint = Blueprint('users', __name__) 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 flask import Request
from fittrackee import db
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse, HttpResponse,
UnauthorizedErrorResponse, UnauthorizedErrorResponse,
) )
from .exceptions import UserNotFoundException from ..models import User
from .models import User
def is_valid_email(email: str) -> bool: def is_valid_email(email: str) -> bool:
@ -88,22 +86,3 @@ def verify_user(
if verify_admin and not user.admin: if verify_admin and not user.admin:
return ForbiddenErrorResponse(), None return ForbiddenErrorResponse(), None
return None, user 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 from typing import Optional
import humanize 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: 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 sqlalchemy.types import JSON, Enum
from fittrackee import db from fittrackee import db
from fittrackee.files import get_absolute_file_path
from .utils_files import get_absolute_file_path from .utils.convert import convert_in_duration, convert_value_to_integer
from .utils_format import convert_in_duration, convert_value_to_integer from .utils.short_id import encode_uuid
from .utils_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

@ -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 os
import tempfile import tempfile
import zipfile import zipfile
@ -10,17 +9,17 @@ 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
from fittrackee import db from fittrackee import db
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_files import get_absolute_file_path from .gpx import get_gpx_info
from .utils_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

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

67
poetry.lock generated
View File

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