Merge branch 'api-refacto' into dev_bis
This commit is contained in:
commit
83f234d574
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
23
fittrackee/files.py
Normal 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)
|
@ -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
|
||||
|
@ -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',
|
@ -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
|
@ -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')
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
0
fittrackee/tests/workouts/test_utils/__init__.py
Normal file
0
fittrackee/tests/workouts/test_utils/__init__.py
Normal 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',
|
@ -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:
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
0
fittrackee/users/utils/__init__.py
Normal file
0
fittrackee/users/utils/__init__.py
Normal file
12
fittrackee/users/utils/admin.py
Normal file
12
fittrackee/users/utils/admin.py
Normal 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()
|
@ -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()
|
@ -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:
|
||||
|
@ -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 = [
|
||||
|
@ -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__)
|
||||
|
||||
|
0
fittrackee/workouts/utils/__init__.py
Normal file
0
fittrackee/workouts/utils/__init__.py
Normal 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]:
|
35
fittrackee/workouts/utils/maps.py
Normal file
35
fittrackee/workouts/utils/maps.py
Normal 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()
|
@ -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)
|
||||
|
16
fittrackee/workouts/utils/uploads.py
Normal file
16
fittrackee/workouts/utils/uploads.py
Normal 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
|
14
fittrackee/workouts/utils/visibility.py
Normal file
14
fittrackee/workouts/utils/visibility.py
Normal 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
|
@ -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:
|
@ -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)
|
@ -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
67
poetry.lock
generated
@ -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"},
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user