API - add typing

This commit is contained in:
Sam 2021-01-02 19:28:03 +01:00
parent 4705393a08
commit 634d06b05a
53 changed files with 1884 additions and 1075 deletions

View File

@ -33,6 +33,11 @@ lint:
script:
- pytest --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
type-check:
extends: .python
script:
- mypy fittrackee --disallow-untyped-defs --ignore-missing-imports
python-3.7:
extends: .python
image: python:3.7

View File

@ -9,10 +9,13 @@ make-p:
build-client: lint-client
cd fittrackee_client && $(NPM) build
check-all: lint-all type-check test-python
clean-install:
rm -fr $(NODE_MODULES)
rm -fr $(VENV)
rm -rf *.egg-info
rm -rf .mypy_cache
rm -rf .pytest_cache
rm -rf dist/
@ -61,18 +64,18 @@ lint-all: lint-python lint-client
lint-all-fix: lint-python-fix lint-client-fix
lint-python:
$(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
lint-python-fix:
$(BLACK) fittrackee e2e
lint-client:
cd fittrackee_client && $(NPM) lint
lint-client-fix:
cd fittrackee_client && $(NPM) lint-fix
lint-python:
$(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
lint-python-fix:
$(BLACK) fittrackee e2e
mail:
docker run -d -e "MH_STORAGE=maildir" -v /tmp/maildir:/maildir -p 1025:1025 -p 8025:8025 mailhog/mailhog
@ -91,21 +94,21 @@ run-server:
run-workers:
$(FLASK) worker --processes=$(WORKERS_PROCESSES) >> dramatiq.log 2>&1
serve-python:
$(FLASK) run --with-threads -h $(HOST) -p $(PORT)
serve-python-dev:
$(FLASK) run --with-threads -h $(HOST) -p $(PORT) --cert=adhoc
serve-client:
cd fittrackee_client && $(NPM) start
serve:
$(MAKE) P="serve-client serve-python" make-p
serve-dev:
$(MAKE) P="serve-client serve-python-dev" make-p
serve-client:
cd fittrackee_client && $(NPM) start
serve-python:
$(FLASK) run --with-threads -h $(HOST) -p $(PORT)
serve-python-dev:
$(FLASK) run --with-threads -h $(HOST) -p $(PORT) --cert=adhoc
test-e2e: init-db
$(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
@ -115,5 +118,9 @@ test-e2e-client: init-db
test-python:
$(PYTEST) fittrackee --cov-config .coveragerc --cov=fittrackee --cov-report term-missing $(PYTEST_ARGS)
type-check:
echo 'Running mypy...'
$(MYPY) fittrackee --disallow-untyped-defs --ignore-missing-imports
upgrade-db:
$(FLASK) db upgrade --directory $(MIGRATIONS)

View File

@ -25,6 +25,7 @@ FLASK = $(VENV)/bin/flask
PYTEST = $(VENV)/bin/py.test -c pyproject.toml -W ignore::DeprecationWarning
GUNICORN = $(VENV)/bin/gunicorn
BLACK = $(VENV)/bin/black
MYPY = $(VENV)/bin/mypy
# Node env
NODE_MODULES = $(PWD)/fittrackee_client/node_modules

View File

@ -1,8 +1,9 @@
import logging
import os
from importlib import import_module, reload
from typing import Any
from flask import Flask, render_template, send_file
from flask import Flask, Response, render_template, send_file
from flask_bcrypt import Bcrypt
from flask_dramatiq import Dramatiq
from flask_migrate import Migrate
@ -24,7 +25,7 @@ logging.basicConfig(
appLog = logging.getLogger('fittrackee')
def create_app():
def create_app() -> Flask:
# instantiate the app
app = Flask(__name__, static_folder='dist/static', template_folder='dist')
@ -88,7 +89,7 @@ def create_app():
# Enable CORS
@app.after_request
def after_request(response):
def after_request(response: Response) -> Response:
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add(
'Access-Control-Allow-Headers', 'Content-Type,Authorization'
@ -100,15 +101,23 @@ def create_app():
return response
@app.route('/favicon.ico')
def favicon():
return send_file(os.path.join(app.root_path, 'dist/favicon.ico'))
def favicon() -> Any:
return send_file(
os.path.join(app.root_path, 'dist/favicon.ico') # type: ignore
)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
def catch_all(path: str) -> Any:
# workaround to serve images (not in static directory)
if path.startswith('img/'):
return send_file(os.path.join(app.root_path, 'dist', path))
return send_file(
os.path.join(
app.root_path, # type: ignore
'dist',
path,
)
)
else:
return render_template('index.html')

View File

@ -2,6 +2,7 @@
# http://docs.gunicorn.org/en/stable/custom.html
import os
import shutil
from typing import Dict, Optional
import gunicorn.app.base
from fittrackee import create_app, db
@ -9,6 +10,7 @@ from fittrackee.activities.models import Activity
from fittrackee.activities.utils import update_activity
from fittrackee.application.utils import init_config
from fittrackee.database_utils import init_database
from flask import Flask
from flask_dramatiq import worker
from flask_migrate import upgrade
from tqdm import tqdm
@ -22,12 +24,14 @@ dramatiq_worker = worker
class StandaloneApplication(gunicorn.app.base.BaseApplication):
def __init__(self, current_app, options=None):
def __init__(
self, current_app: Flask, options: Optional[Dict] = None
) -> None:
self.options = options or {}
self.application = current_app
super().__init__()
def load_config(self):
def load_config(self) -> None:
config = {
key: value
for key, value in self.options.items()
@ -36,17 +40,17 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
for key, value in config.items():
self.cfg.set(key.lower(), value)
def load(self):
def load(self) -> Flask:
return self.application
def upgrade_db():
def upgrade_db() -> None:
with app.app_context():
upgrade(directory=BASEDIR + '/migrations')
@app.cli.command('drop-db')
def drop_db():
def drop_db() -> None:
"""Empty database for dev environments."""
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
db.drop_all()
@ -57,13 +61,13 @@ def drop_db():
@app.cli.command('init-data')
def init_data():
def init_data() -> None:
"""Init the database and application config."""
init_database(app)
@app.cli.command()
def recalculate():
def recalculate() -> None:
print("Starting activities data refresh")
activities = (
Activity.query.filter(Activity.gpx != None) # noqa
@ -81,7 +85,7 @@ def recalculate():
@app.cli.command('init-app-config')
def init_app_config():
def init_app_config() -> None:
"""Init application configuration."""
print("Init application configuration")
config_created, _ = init_config()
@ -94,7 +98,7 @@ def init_app_config():
)
def main():
def main() -> None:
options = {'bind': f'{HOST}:{PORT}', 'workers': WORKERS}
StandaloneApplication(app, options).run()

View File

@ -2,12 +2,14 @@ import json
import os
import shutil
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple, Union
import requests
from fittrackee import appLog, db
from fittrackee.responses import (
DataInvalidPayloadErrorResponse,
DataNotFoundErrorResponse,
HttpResponse,
InternalServerErrorResponse,
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
@ -46,7 +48,7 @@ ACTIVITIES_PER_PAGE = 5
@activities_blueprint.route('/activities', methods=['GET'])
@authenticate
def get_activities(auth_user_id):
def get_activities(auth_user_id: int) -> Union[Dict, HttpResponse]:
"""
Get activities for the authenticated user.
@ -275,7 +277,9 @@ def get_activities(auth_user_id):
'/activities/<string:activity_short_id>', methods=['GET']
)
@authenticate
def get_activity(auth_user_id, activity_short_id):
def get_activity(
auth_user_id: int, activity_short_id: str
) -> Union[Dict, HttpResponse]:
"""
Get an activity
@ -375,8 +379,11 @@ def get_activity(auth_user_id, activity_short_id):
def get_activity_data(
auth_user_id, activity_short_id, data_type, segment_id=None
):
auth_user_id: int,
activity_short_id: str,
data_type: str,
segment_id: Optional[int] = None,
) -> Union[Dict, HttpResponse]:
"""Get data from an activity gpx file"""
activity_uuid = decode_short_id(activity_short_id)
activity = Activity.query.filter_by(uuid=activity_uuid).first()
@ -396,14 +403,17 @@ def get_activity_data(
try:
absolute_gpx_filepath = get_absolute_file_path(activity.gpx)
chart_data_content: Optional[List] = []
if data_type == 'chart_data':
content = get_chart_data(absolute_gpx_filepath, segment_id)
chart_data_content = get_chart_data(
absolute_gpx_filepath, segment_id
)
else: # data_type == 'gpx'
with open(absolute_gpx_filepath, encoding='utf-8') as f:
content = f.read()
gpx_content = f.read()
if segment_id is not None:
content = extract_segment_from_gpx_file(
content, segment_id
gpx_segment_content = extract_segment_from_gpx_file(
gpx_content, segment_id
)
except ActivityGPXException as e:
appLog.error(e.message)
@ -416,7 +426,15 @@ def get_activity_data(
return {
'status': 'success',
'message': '',
'data': ({data_type: content}),
'data': (
{
data_type: chart_data_content
if data_type == 'chart_data'
else gpx_content
if segment_id is None
else gpx_segment_content
}
),
}
@ -424,7 +442,9 @@ def get_activity_data(
'/activities/<string:activity_short_id>/gpx', methods=['GET']
)
@authenticate
def get_activity_gpx(auth_user_id, activity_short_id):
def get_activity_gpx(
auth_user_id: int, activity_short_id: str
) -> Union[Dict, HttpResponse]:
"""
Get gpx file for an activity displayed on map with Leaflet
@ -473,7 +493,9 @@ def get_activity_gpx(auth_user_id, activity_short_id):
'/activities/<string:activity_short_id>/chart_data', methods=['GET']
)
@authenticate
def get_activity_chart_data(auth_user_id, activity_short_id):
def get_activity_chart_data(
auth_user_id: int, activity_short_id: str
) -> Union[Dict, HttpResponse]:
"""
Get chart data from an activity gpx file, to display it with Recharts
@ -542,7 +564,9 @@ def get_activity_chart_data(auth_user_id, activity_short_id):
methods=['GET'],
)
@authenticate
def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
def get_segment_gpx(
auth_user_id: int, activity_short_id: str, segment_id: int
) -> Union[Dict, HttpResponse]:
"""
Get gpx file for an activity segment displayed on map with Leaflet
@ -595,7 +619,9 @@ def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
methods=['GET'],
)
@authenticate
def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
def get_segment_chart_data(
auth_user_id: int, activity_short_id: str, segment_id: int
) -> Union[Dict, HttpResponse]:
"""
Get chart data from an activity gpx file, to display it with Recharts
@ -662,7 +688,7 @@ def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
@activities_blueprint.route('/activities/map/<map_id>', methods=['GET'])
def get_map(map_id):
def get_map(map_id: int) -> Any:
"""
Get map image for activities with gpx
@ -704,7 +730,7 @@ def get_map(map_id):
@activities_blueprint.route(
'/activities/map_tile/<s>/<z>/<x>/<y>.png', methods=['GET']
)
def get_map_tile(s, z, x, y):
def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
"""
Get map tile from tile server.
@ -743,7 +769,7 @@ def get_map_tile(s, z, x, y):
@activities_blueprint.route('/activities', methods=['POST'])
@authenticate
def post_activity(auth_user_id):
def post_activity(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
"""
Post an activity with a gpx file
@ -904,7 +930,9 @@ def post_activity(auth_user_id):
@activities_blueprint.route('/activities/no_gpx', methods=['POST'])
@authenticate
def post_activity_no_gpx(auth_user_id):
def post_activity_no_gpx(
auth_user_id: int,
) -> Union[Tuple[Dict, int], HttpResponse]:
"""
Post an activity without gpx file
@ -1053,7 +1081,9 @@ def post_activity_no_gpx(auth_user_id):
'/activities/<string:activity_short_id>', methods=['PATCH']
)
@authenticate
def update_activity(auth_user_id, activity_short_id):
def update_activity(
auth_user_id: int, activity_short_id: str
) -> Union[Dict, HttpResponse]:
"""
Update an activity
@ -1199,7 +1229,9 @@ def update_activity(auth_user_id, activity_short_id):
'/activities/<string:activity_short_id>', methods=['DELETE']
)
@authenticate
def delete_activity(auth_user_id, activity_short_id):
def delete_activity(
auth_user_id: int, activity_short_id: str
) -> Union[Tuple[Dict, int], HttpResponse]:
"""
Delete an activity

View File

@ -1,18 +1,23 @@
import datetime
import os
from uuid import uuid4
from typing import Any, Dict, Optional, Union
from uuid import UUID, uuid4
from fittrackee import db
from sqlalchemy.dialects import postgresql
from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm.session import object_session
from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session, object_session
from sqlalchemy.types import JSON, Enum
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
BaseModel: DeclarativeMeta = db.Model
record_types = [
'AS', # 'Best Average Speed'
'FD', # 'Farthest Distance'
@ -21,7 +26,9 @@ record_types = [
]
def update_records(user_id, sport_id, connection, session):
def update_records(
user_id: int, sport_id: int, connection: Connection, session: Session
) -> None:
record_table = Record.__table__
new_records = Activity.get_user_activity_records(user_id, sport_id)
for record_type, record_data in new_records.items():
@ -47,7 +54,7 @@ def update_records(user_id, sport_id, connection, session):
new_record = Record(
activity=record_data['activity'], record_type=record_type
)
new_record.value = record_data['record_value']
new_record.value = record_data['record_value'] # type: ignore
session.add(new_record)
else:
connection.execute(
@ -58,7 +65,7 @@ def update_records(user_id, sport_id, connection, session):
)
class Sport(db.Model):
class Sport(BaseModel):
__tablename__ = "sports"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
label = db.Column(db.String(50), unique=True, nullable=False)
@ -71,13 +78,13 @@ class Sport(db.Model):
'Record', lazy=True, backref=db.backref('sports', lazy='joined')
)
def __repr__(self):
def __repr__(self) -> str:
return f'<Sport {self.label!r}>'
def __init__(self, label):
def __init__(self, label: str) -> None:
self.label = label
def serialize(self, is_admin=False):
def serialize(self, is_admin: Optional[bool] = False) -> Dict:
serialized_sport = {
'id': self.id,
'label': self.label,
@ -89,7 +96,7 @@ class Sport(db.Model):
return serialized_sport
class Activity(db.Model):
class Activity(BaseModel):
__tablename__ = "activities"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column(
@ -138,10 +145,17 @@ class Activity(db.Model):
backref=db.backref('activities', lazy='joined', single_parent=True),
)
def __str__(self):
def __str__(self) -> str:
return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
def __init__(self, user_id, sport_id, activity_date, distance, duration):
def __init__(
self,
user_id: int,
sport_id: int,
activity_date: datetime.datetime,
distance: float,
duration: datetime.timedelta,
) -> None:
self.user_id = user_id
self.sport_id = sport_id
self.activity_date = activity_date
@ -149,10 +163,10 @@ class Activity(db.Model):
self.duration = duration
@property
def short_id(self):
def short_id(self) -> str:
return encode_uuid(self.uuid)
def serialize(self, params=None):
def serialize(self, params: Optional[Dict] = None) -> Dict:
date_from = params.get('from') if params else None
date_to = params.get('to') if params else None
distance_from = params.get('distance_from') if params else None
@ -239,41 +253,43 @@ class Activity(db.Model):
.first()
)
return {
"id": self.short_id, # WARNING: client use uuid as id
"user": self.user.username,
"sport_id": self.sport_id,
"title": self.title,
"creation_date": self.creation_date,
"modification_date": self.modification_date,
"activity_date": self.activity_date,
"duration": str(self.duration) if self.duration else None,
"pauses": str(self.pauses) if self.pauses else None,
"moving": str(self.moving) if self.moving else None,
"distance": float(self.distance) if self.distance else None,
"min_alt": float(self.min_alt) if self.min_alt else None,
"max_alt": float(self.max_alt) if self.max_alt else None,
"descent": float(self.descent) if self.descent else None,
"ascent": float(self.ascent) if self.ascent else None,
"max_speed": float(self.max_speed) if self.max_speed else None,
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
"with_gpx": self.gpx is not None,
"bounds": [float(bound) for bound in self.bounds]
'id': self.short_id, # WARNING: client use uuid as id
'user': self.user.username,
'sport_id': self.sport_id,
'title': self.title,
'creation_date': self.creation_date,
'modification_date': self.modification_date,
'activity_date': self.activity_date,
'duration': str(self.duration) if self.duration else None,
'pauses': str(self.pauses) if self.pauses else None,
'moving': str(self.moving) if self.moving else None,
'distance': float(self.distance) if self.distance else None,
'min_alt': float(self.min_alt) if self.min_alt else None,
'max_alt': float(self.max_alt) if self.max_alt else None,
'descent': float(self.descent) if self.descent else None,
'ascent': float(self.ascent) if self.ascent else None,
'max_speed': float(self.max_speed) if self.max_speed else None,
'ave_speed': float(self.ave_speed) if self.ave_speed else None,
'with_gpx': self.gpx is not None,
'bounds': [float(bound) for bound in self.bounds]
if self.bounds
else [], # noqa
"previous_activity": previous_activity.short_id
'previous_activity': previous_activity.short_id
if previous_activity
else None, # noqa
"next_activity": next_activity.short_id if next_activity else None,
"segments": [segment.serialize() for segment in self.segments],
"records": [record.serialize() for record in self.records],
"map": self.map_id if self.map else None,
"weather_start": self.weather_start,
"weather_end": self.weather_end,
"notes": self.notes,
'next_activity': next_activity.short_id if next_activity else None,
'segments': [segment.serialize() for segment in self.segments],
'records': [record.serialize() for record in self.records],
'map': self.map_id if self.map else None,
'weather_start': self.weather_start,
'weather_end': self.weather_end,
'notes': self.notes,
}
@classmethod
def get_user_activity_records(cls, user_id, sport_id, as_integer=False):
def get_user_activity_records(
cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
) -> Dict:
record_types_columns = {
'AS': 'ave_speed', # 'Average speed'
'FD': 'distance', # 'Farthest Distance'
@ -300,22 +316,26 @@ class Activity(db.Model):
@listens_for(Activity, 'after_insert')
def on_activity_insert(mapper, connection, activity):
def on_activity_insert(
mapper: Mapper, connection: Connection, activity: Activity
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Any) -> None:
update_records(
activity.user_id, activity.sport_id, connection, session
) # noqa
@listens_for(Activity, 'after_update')
def on_activity_update(mapper, connection, activity):
def on_activity_update(
mapper: Mapper, connection: Connection, activity: Activity
) -> None:
if object_session(activity).is_modified(
activity, include_collections=True
): # noqa
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Any) -> None:
sports_list = [activity.sport_id]
records = Record.query.filter_by(activity_id=activity.id).all()
for rec in records:
@ -326,16 +346,18 @@ def on_activity_update(mapper, connection, activity):
@listens_for(Activity, 'after_delete')
def on_activity_delete(mapper, connection, old_record):
def on_activity_delete(
mapper: Mapper, connection: Connection, old_record: 'Record'
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Any) -> None:
if old_record.map:
os.remove(get_absolute_file_path(old_record.map))
if old_record.gpx:
os.remove(get_absolute_file_path(old_record.gpx))
class ActivitySegment(db.Model):
class ActivitySegment(BaseModel):
__tablename__ = "activity_segments"
activity_id = db.Column(
db.Integer, db.ForeignKey('activities.id'), primary_key=True
@ -353,35 +375,37 @@ class ActivitySegment(db.Model):
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
def __str__(self):
def __str__(self) -> str:
return (
f'<Segment \'{self.segment_id}\' '
f'for activity \'{encode_uuid(self.activity_uuid)}\'>'
)
def __init__(self, segment_id, activity_id, activity_uuid):
def __init__(
self, segment_id: int, activity_id: int, activity_uuid: UUID
) -> None:
self.segment_id = segment_id
self.activity_id = activity_id
self.activity_uuid = activity_uuid
def serialize(self):
def serialize(self) -> Dict:
return {
"activity_id": encode_uuid(self.activity_uuid),
"segment_id": self.segment_id,
"duration": str(self.duration) if self.duration else None,
"pauses": str(self.pauses) if self.pauses else None,
"moving": str(self.moving) if self.moving else None,
"distance": float(self.distance) if self.distance else None,
"min_alt": float(self.min_alt) if self.min_alt else None,
"max_alt": float(self.max_alt) if self.max_alt else None,
"descent": float(self.descent) if self.descent else None,
"ascent": float(self.ascent) if self.ascent else None,
"max_speed": float(self.max_speed) if self.max_speed else None,
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
'activity_id': encode_uuid(self.activity_uuid),
'segment_id': self.segment_id,
'duration': str(self.duration) if self.duration else None,
'pauses': str(self.pauses) if self.pauses else None,
'moving': str(self.moving) if self.moving else None,
'distance': float(self.distance) if self.distance else None,
'min_alt': float(self.min_alt) if self.min_alt else None,
'max_alt': float(self.max_alt) if self.max_alt else None,
'descent': float(self.descent) if self.descent else None,
'ascent': float(self.ascent) if self.ascent else None,
'max_speed': float(self.max_speed) if self.max_speed else None,
'ave_speed': float(self.ave_speed) if self.ave_speed else None,
}
class Record(db.Model):
class Record(BaseModel):
__tablename__ = "records"
__table_args__ = (
db.UniqueConstraint(
@ -401,14 +425,14 @@ class Record(db.Model):
activity_date = db.Column(db.DateTime, nullable=False)
_value = db.Column("value", db.Integer, nullable=True)
def __str__(self):
def __str__(self) -> str:
return (
f'<Record {self.sports.label} - '
f'{self.record_type} - '
f"{self.activity_date.strftime('%Y-%m-%d')}>"
)
def __init__(self, activity, record_type):
def __init__(self, activity: Activity, record_type: str) -> None:
self.user_id = activity.user_id
self.sport_id = activity.sport_id
self.activity_id = activity.id
@ -417,7 +441,7 @@ class Record(db.Model):
self.activity_date = activity.activity_date
@hybrid_property
def value(self):
def value(self) -> Optional[Union[datetime.timedelta, float]]:
if self._value is None:
return None
if self.record_type == 'LD':
@ -427,33 +451,35 @@ class Record(db.Model):
else: # 'FD'
return float(self._value / 1000)
@value.setter
def value(self, val):
@value.setter # type: ignore
def value(self, val: Union[str, float]) -> None:
self._value = convert_value_to_integer(self.record_type, val)
def serialize(self):
def serialize(self) -> Dict:
if self.value is None:
value = None
elif self.record_type in ['AS', 'FD', 'MS']:
value = float(self.value)
value = float(self.value) # type: ignore
else: # 'LD'
value = str(self.value)
value = str(self.value) # type: ignore
return {
"id": self.id,
"user": self.user.username,
"sport_id": self.sport_id,
"activity_id": encode_uuid(self.activity_uuid),
"record_type": self.record_type,
"activity_date": self.activity_date,
"value": value,
'id': self.id,
'user': self.user.username,
'sport_id': self.sport_id,
'activity_id': encode_uuid(self.activity_uuid),
'record_type': self.record_type,
'activity_date': self.activity_date,
'value': value,
}
@listens_for(Record, 'after_delete')
def on_record_delete(mapper, connection, old_record):
def on_record_delete(
mapper: Mapper, connection: Connection, old_record: Record
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Any) -> None:
activity = old_record.activities
new_records = Activity.get_user_activity_records(
activity.user_id, activity.sport_id
@ -466,5 +492,5 @@ def on_record_delete(mapper, connection, old_record):
new_record = Record(
activity=record_data['activity'], record_type=record_type
)
new_record.value = record_data['record_value']
new_record.value = record_data['record_value'] # type: ignore
session.add(new_record)

View File

@ -1,3 +1,5 @@
from typing import Dict
from flask import Blueprint
from ..users.utils import authenticate
@ -8,7 +10,7 @@ records_blueprint = Blueprint('records', __name__)
@records_blueprint.route('/records', methods=['GET'])
@authenticate
def get_records(auth_user_id):
def get_records(auth_user_id: int) -> Dict:
"""
Get all records for authenticated user.

View File

@ -1,6 +1,9 @@
from typing import Dict, Union
from fittrackee import db
from fittrackee.responses import (
DataNotFoundErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse,
handle_error_and_return_response,
)
@ -16,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
@sports_blueprint.route('/sports', methods=['GET'])
@authenticate
def get_sports(auth_user_id):
def get_sports(auth_user_id: int) -> Dict:
"""
Get all sports
@ -158,7 +161,7 @@ def get_sports(auth_user_id):
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
@authenticate
def get_sport(auth_user_id, sport_id):
def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
"""
Get a sport
@ -253,7 +256,9 @@ def get_sport(auth_user_id, sport_id):
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
@authenticate_as_admin
def update_sport(auth_user_id, sport_id):
def update_sport(
auth_user_id: int, sport_id: int
) -> Union[Dict, HttpResponse]:
"""
Update a sport
Authenticated user must be an admin

View File

@ -1,7 +1,9 @@
from datetime import datetime, timedelta
from typing import Dict, Union
from fittrackee import db
from fittrackee.responses import (
HttpResponse,
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
UserNotFoundErrorResponse,
@ -19,7 +21,12 @@ from .utils_format import convert_timedelta_to_integer
stats_blueprint = Blueprint('stats', __name__)
def get_activities(user_name, filter_type):
def get_activities(
user_name: str, filter_type: str
) -> Union[Dict, HttpResponse]:
"""
Return user activities by sport or by time
"""
try:
user = User.query.filter_by(username=user_name).first()
if not user:
@ -40,7 +47,6 @@ def get_activities(user_name, filter_type):
time = params.get('time')
if filter_type == 'by_sport':
sport_id = params.get('sport_id')
if sport_id:
sport = Sport.query.filter_by(id=sport_id).first()
if not sport:
@ -59,24 +65,26 @@ def get_activities(user_name, filter_type):
.all()
)
activities_list = {}
activities_list_by_sport = {}
activities_list_by_time = {} # type: ignore
for activity in activities:
if filter_type == 'by_sport':
sport_id = activity.sport_id
if sport_id not in activities_list:
activities_list[sport_id] = {
if sport_id not in activities_list_by_sport:
activities_list_by_sport[sport_id] = {
'nb_activities': 0,
'total_distance': 0.0,
'total_duration': 0,
}
activities_list[sport_id]['nb_activities'] += 1
activities_list[sport_id]['total_distance'] += float(
activities_list_by_sport[sport_id]['nb_activities'] += 1
activities_list_by_sport[sport_id]['total_distance'] += float(
activity.distance
)
activities_list[sport_id][
activities_list_by_sport[sport_id][
'total_duration'
] += convert_timedelta_to_integer(activity.moving)
# filter_type == 'by_time'
else:
if time == 'week':
activity_date = activity.activity_date - timedelta(
@ -105,25 +113,31 @@ def get_activities(user_name, filter_type):
'Invalid time period.', 'fail'
)
sport_id = activity.sport_id
if time_period not in activities_list:
activities_list[time_period] = {}
if sport_id not in activities_list[time_period]:
activities_list[time_period][sport_id] = {
if time_period not in activities_list_by_time:
activities_list_by_time[time_period] = {}
if sport_id not in activities_list_by_time[time_period]:
activities_list_by_time[time_period][sport_id] = {
'nb_activities': 0,
'total_distance': 0.0,
'total_duration': 0,
}
activities_list[time_period][sport_id]['nb_activities'] += 1
activities_list[time_period][sport_id][
activities_list_by_time[time_period][sport_id][
'nb_activities'
] += 1
activities_list_by_time[time_period][sport_id][
'total_distance'
] += float(activity.distance)
activities_list[time_period][sport_id][
activities_list_by_time[time_period][sport_id][
'total_duration'
] += convert_timedelta_to_integer(activity.moving)
return {
'status': 'success',
'data': {'statistics': activities_list},
'data': {
'statistics': activities_list_by_sport
if filter_type == 'by_sport'
else activities_list_by_time
},
}
except Exception as e:
return handle_error_and_return_response(e)
@ -131,7 +145,9 @@ def get_activities(user_name, filter_type):
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
@authenticate
def get_activities_by_time(auth_user_id, user_name):
def get_activities_by_time(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get activities statistics for a user by time
@ -227,7 +243,9 @@ def get_activities_by_time(auth_user_id, user_name):
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
@authenticate
def get_activities_by_sport(auth_user_id, user_name):
def get_activities_by_sport(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get activities statistics for a user by sport
@ -313,7 +331,7 @@ def get_activities_by_sport(auth_user_id, user_name):
@stats_blueprint.route('/stats/all', methods=['GET'])
@authenticate_as_admin
def get_application_stats(auth_user_id):
def get_application_stats(auth_user_id: int) -> Dict:
"""
Get all application statistics

View File

@ -3,6 +3,8 @@ import os
import tempfile
import zipfile
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Union
from uuid import UUID
import gpxpy.gpx
import pytz
@ -10,6 +12,7 @@ from fittrackee import appLog, db
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 ..users.models import User
@ -19,13 +22,20 @@ from .utils_gpx import get_gpx_info
class ActivityException(Exception):
def __init__(self, status, message, e):
def __init__(
self, status: str, message: str, e: Optional[Exception] = None
) -> None:
self.status = status
self.message = message
self.e = e
def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
def get_datetime_with_tz(
timezone: str, activity_date: datetime, gpx_data: Optional[Dict] = None
) -> Tuple[Optional[datetime], datetime]:
"""
Return naive datetime and datetime with user timezone
"""
activity_date_tz = None
if timezone:
user_tz = pytz.timezone(timezone)
@ -47,8 +57,12 @@ def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
return activity_date_tz, activity_date
def update_activity_data(activity, gpx_data):
"""activity could be a complete activity or an activity segment"""
def update_activity_data(
activity: Union[Activity, ActivitySegment], gpx_data: Dict
) -> Union[Activity, ActivitySegment]:
"""
Update activity or activity segment with data from gpx file
"""
activity.pauses = gpx_data['stop_time']
activity.moving = gpx_data['moving_time']
activity.min_alt = gpx_data['elevation_min']
@ -60,12 +74,18 @@ def update_activity_data(activity, gpx_data):
return activity
def create_activity(user, activity_data, gpx_data=None):
def create_activity(
user: User, activity_data: Dict, gpx_data: Optional[Dict] = None
) -> Activity:
"""
Create Activity from data entered by user and from gpx if a gpx file is
provided
"""
activity_date = (
gpx_data['start']
if gpx_data
else datetime.strptime(
activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
activity_data['activity_date'], '%Y-%m-%d %H:%M'
)
)
activity_date_tz, activity_date = get_datetime_with_tz(
@ -75,16 +95,14 @@ def create_activity(user, activity_data, gpx_data=None):
duration = (
gpx_data['duration']
if gpx_data
else timedelta(seconds=activity_data.get('duration'))
else timedelta(seconds=activity_data['duration'])
)
distance = (
gpx_data['distance'] if gpx_data else activity_data.get('distance')
)
title = gpx_data['name'] if gpx_data else activity_data.get('title')
distance = gpx_data['distance'] if gpx_data else activity_data['distance']
title = gpx_data['name'] if gpx_data else activity_data.get('title', '')
new_activity = Activity(
user_id=user.id,
sport_id=activity_data.get('sport_id'),
sport_id=activity_data['sport_id'],
activity_date=activity_date,
distance=distance,
duration=duration,
@ -118,7 +136,12 @@ def create_activity(user, activity_data, gpx_data=None):
return new_activity
def create_segment(activity_id, activity_uuid, segment_data):
def create_segment(
activity_id: int, activity_uuid: UUID, segment_data: Dict
) -> ActivitySegment:
"""
Create Activity Segment from gpx data
"""
new_segment = ActivitySegment(
activity_id=activity_id,
activity_uuid=activity_uuid,
@ -130,12 +153,9 @@ def create_segment(activity_id, activity_uuid, segment_data):
return new_segment
def update_activity(activity):
def update_activity(activity: Activity) -> Activity:
"""
Note: only gpx_data is be updated for now (the gpx file is NOT modified)
In a next version, map_data and weather_data will be updated
(case of a modified gpx file, see issue #7)
Update activity data from gpx file
"""
gpx_data, _, _ = get_gpx_info(
get_absolute_file_path(activity.gpx), False, False
@ -155,7 +175,16 @@ def update_activity(activity):
return updated_activity
def edit_activity(activity, activity_data, auth_user_id):
def edit_activity(
activity: Activity, activity_data: Dict, auth_user_id: int
) -> Activity:
"""
Edit an activity
Note: the gpx file is NOT modified
In a next version, map_data and weather_data will be updated
(case of a modified gpx file, see issue #7)
"""
user = User.query.filter_by(id=auth_user_id).first()
if activity_data.get('refresh'):
activity = update_activity(activity)
@ -168,20 +197,18 @@ def edit_activity(activity, activity_data, auth_user_id):
if not activity.gpx:
if activity_data.get('activity_date'):
activity_date = datetime.strptime(
activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
activity_data['activity_date'], '%Y-%m-%d %H:%M'
)
_, activity.activity_date = get_datetime_with_tz(
user.timezone, activity_date
)
if activity_data.get('duration'):
activity.duration = timedelta(
seconds=activity_data.get('duration')
)
activity.duration = timedelta(seconds=activity_data['duration'])
activity.moving = activity.duration
if activity_data.get('distance'):
activity.distance = activity_data.get('distance')
activity.distance = activity_data['distance']
activity.ave_speed = (
None
@ -192,7 +219,10 @@ def edit_activity(activity, activity_data, auth_user_id):
return activity
def get_file_path(dir_path, filename):
def get_file_path(dir_path: str, filename: str) -> str:
"""
Get full path for a file
"""
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, filename)
@ -200,9 +230,16 @@ def get_file_path(dir_path, filename):
def get_new_file_path(
auth_user_id, activity_date, sport, old_filename=None, extension=None
):
if not extension:
auth_user_id: int,
activity_date: str,
sport: str,
old_filename: Optional[str] = None,
extension: Optional[str] = None,
) -> str:
"""
Generate a file path from user and activity data
"""
if not extension and old_filename:
extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
_, new_filename = tempfile.mkstemp(
prefix=f'{activity_date}_{sport}_', suffix=extension
@ -214,7 +251,10 @@ def get_new_file_path(
return file_path
def generate_map(map_filepath, map_data):
def generate_map(map_filepath: str, map_data: List) -> None:
"""
Generate and save map image from map data
"""
m = StaticMap(400, 225, 10)
line = Line(map_data, '#3388FF', 4)
m.add_line(line)
@ -222,10 +262,10 @@ def generate_map(map_filepath, map_data):
image.save(map_filepath)
def get_map_hash(map_filepath):
def get_map_hash(map_filepath: str) -> str:
"""
md5 hash is used as id instead of activity id, to retrieve map image
(maps are sensitive data)
Generate a md5 hash used as id instead of activity id, to retrieve map
image (maps are sensitive data)
"""
md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath)
@ -235,7 +275,10 @@ def get_map_hash(map_filepath):
return md5.hexdigest()
def process_one_gpx_file(params, filename):
def process_one_gpx_file(params: Dict, filename: str) -> Activity:
"""
Get all data from a gpx file to create an activity with map image
"""
try:
gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
auth_user_id = params['user'].id
@ -284,23 +327,33 @@ def process_one_gpx_file(params, filename):
raise ActivityException('fail', 'Error during activity save.', e)
def process_zip_archive(common_params, extract_dir):
def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
"""
Get files from a zip archive and create activities, if number of files
does not exceed defined limit.
"""
with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
zip_ref.extractall(extract_dir)
new_activities = []
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', '10')
if gpx_files_limit and gpx_files_limit.isdigit():
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
if (
gpx_files_limit
and isinstance(gpx_files_limit, str)
and gpx_files_limit.isdigit()
):
gpx_files_limit = int(gpx_files_limit)
else:
gpx_files_limit = 10
appLog.error('GPX limit not configured, set to 10.')
appLog.warning('GPX limit not configured, set to 10.')
gpx_files_ok = 0
for gpx_file in os.listdir(extract_dir):
if '.' in gpx_file and gpx_file.rsplit('.', 1)[
1
].lower() in current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS'):
if (
'.' in gpx_file
and gpx_file.rsplit('.', 1)[1].lower()
in current_app.config['ACTIVITY_ALLOWED_EXTENSIONS']
):
gpx_files_ok += 1
if gpx_files_ok > gpx_files_limit:
break
@ -313,7 +366,17 @@ def process_zip_archive(common_params, extract_dir):
return new_activities
def process_files(auth_user_id, activity_data, activity_file, folders):
def process_files(
auth_user_id: int,
activity_data: Dict,
activity_file: FileStorage,
folders: Dict,
) -> List:
"""
Store gpx file or zip archive and create activities
"""
if activity_file.filename is None:
raise ActivityException('error', 'File has no filename.')
filename = secure_filename(activity_file.filename)
extension = f".{filename.rsplit('.', 1)[1].lower()}"
file_path = get_file_path(folders['tmp_dir'], filename)
@ -322,7 +385,6 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
raise ActivityException(
'error',
f"Sport id: {activity_data.get('sport_id')} does not exist",
None,
)
user = User.query.filter_by(id=auth_user_id).first()
@ -344,7 +406,10 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
return process_zip_archive(common_params, folders['extract_dir'])
def get_upload_dir_size():
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):

View File

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

View File

@ -1,23 +1,26 @@
from datetime import timedelta
from typing import Optional, Union
def convert_in_duration(value):
def convert_in_duration(value: str) -> timedelta:
hours = int(value.split(':')[0])
minutes = int(value.split(':')[1])
return timedelta(seconds=(hours * 3600 + minutes * 60))
def convert_timedelta_to_integer(value):
def convert_timedelta_to_integer(value: str) -> int:
hours, minutes, seconds = str(value).split(':')
return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
def convert_value_to_integer(record_type, val):
def convert_value_to_integer(
record_type: str, val: Union[str, float]
) -> Optional[int]:
if val is None:
return None
if record_type == 'LD':
return convert_timedelta_to_integer(val)
return convert_timedelta_to_integer(str(val))
elif record_type in ['AS', 'MS']:
return int(val * 100)
else: # 'FD'

View File

@ -1,4 +1,5 @@
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple
import gpxpy.gpx
@ -6,25 +7,40 @@ from .utils_weather import get_weather
class ActivityGPXException(Exception):
def __init__(self, status, message, e):
def __init__(
self, status: str, message: str, e: Optional[Exception] = None
) -> None:
self.status = status
self.message = message
self.e = e
def open_gpx_file(gpx_file):
gpx_file = open(gpx_file, 'r')
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
gpx_file = open(gpx_file, 'r') # type: ignore
gpx = gpxpy.parse(gpx_file)
if len(gpx.tracks) == 0:
return None
return gpx
def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
gpx_data = {'max_speed': (max_speed / 1000) * 3600, 'start': start}
def get_gpx_data(
parsed_gpx: gpxpy.gpx,
max_speed: float,
start: int,
stopped_time_between_seg: timedelta,
) -> Dict:
"""
Returns data from parsed gpx file
"""
gpx_data: Dict[str, Any] = {
'max_speed': (max_speed / 1000) * 3600,
'start': start,
}
duration = parsed_gpx.get_duration()
gpx_data['duration'] = timedelta(seconds=duration) + stopped_time_btwn_seg
gpx_data['duration'] = (
timedelta(seconds=duration) + stopped_time_between_seg
)
ele = parsed_gpx.get_elevation_extremes()
gpx_data['elevation_max'] = ele.maximum
@ -37,7 +53,7 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
mv = parsed_gpx.get_moving_data()
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
gpx_data['stop_time'] = (
timedelta(seconds=mv.stopped_time) + stopped_time_btwn_seg
timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
)
distance = mv.moving_distance + mv.stopped_distance
gpx_data['distance'] = distance / 1000
@ -48,10 +64,17 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
return gpx_data
def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
def get_gpx_info(
gpx_file: str,
update_map_data: Optional[bool] = True,
update_weather_data: Optional[bool] = True,
) -> Tuple:
"""
Parse and return gpx, map and weather data from gpx file
"""
gpx = open_gpx_file(gpx_file)
if gpx is None:
return None
raise ActivityGPXException('not found', 'No gpx file')
gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
max_speed = 0
@ -61,7 +84,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
segments_nb = len(gpx.tracks[0].segments)
prev_seg_last_point = None
no_stopped_time = timedelta(seconds=0)
stopped_time_btwn_seg = no_stopped_time
stopped_time_between_seg = no_stopped_time
for segment_idx, segment in enumerate(gpx.tracks[0].segments):
segment_start = 0
@ -77,7 +100,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
# if a previous segment exists, calculate stopped time between
# the two segments
if prev_seg_last_point:
stopped_time_btwn_seg = point.time - prev_seg_last_point
stopped_time_between_seg = point.time - prev_seg_last_point
# last segment point
if point_idx == (segment_points_nb - 1):
@ -104,7 +127,9 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
segment_data['idx'] = segment_idx
gpx_data['segments'].append(segment_data)
full_gpx_data = get_gpx_data(gpx, max_speed, start, stopped_time_btwn_seg)
full_gpx_data = get_gpx_data(
gpx, max_speed, start, stopped_time_between_seg
)
gpx_data = {**gpx_data, **full_gpx_data}
if update_map_data:
@ -119,7 +144,12 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
return gpx_data, map_data, weather_data
def get_gpx_segments(track_segments, segment_id=None):
def get_gpx_segments(
track_segments: List, segment_id: Optional[int] = None
) -> List:
"""
Return list of segments, filtered on segment id if provided
"""
if segment_id is not None:
segment_index = segment_id - 1
if segment_index > (len(track_segments) - 1):
@ -135,7 +165,12 @@ def get_gpx_segments(track_segments, segment_id=None):
return segments
def get_chart_data(gpx_file, segment_id=None):
def get_chart_data(
gpx_file: str, segment_id: Optional[int] = None
) -> Optional[List]:
"""
Return data needed to generate chart with speed and elevation
"""
gpx = open_gpx_file(gpx_file)
if gpx is None:
return None
@ -193,7 +228,12 @@ def get_chart_data(gpx_file, segment_id=None):
return chart_data
def extract_segment_from_gpx_file(content, segment_id):
def extract_segment_from_gpx_file(
content: str, segment_id: int
) -> Optional[str]:
"""
Returns segments in xml format from a gpx file content
"""
gpx_content = gpxpy.parse(content)
if len(gpx_content.tracks) == 0:
return None

View File

@ -1,9 +1,17 @@
from uuid import UUID
import shortuuid
def encode_uuid(uuid_value):
def encode_uuid(uuid_value: UUID) -> str:
"""
Return short id string from an UUID
"""
return shortuuid.encode(uuid_value)
def decode_short_id(short_id):
def decode_short_id(short_id: str) -> UUID:
"""
Return UUID from a short id string
"""
return shortuuid.decode(short_id)

View File

@ -1,13 +1,15 @@
import os
from typing import Dict, Optional
import forecastio
import pytz
from fittrackee import appLog
from gpxpy.gpx import GPXRoutePoint
API_KEY = os.getenv('WEATHER_API_KEY')
def get_weather(point):
def get_weather(point: GPXRoutePoint) -> Optional[Dict]:
if not API_KEY or API_KEY == '':
return None
try:

View File

@ -1,5 +1,8 @@
from typing import Dict, Union
from fittrackee import db
from fittrackee.responses import (
HttpResponse,
InvalidPayloadErrorResponse,
handle_error_and_return_response,
)
@ -14,7 +17,7 @@ config_blueprint = Blueprint('config', __name__)
@config_blueprint.route('/config', methods=['GET'])
def get_application_config():
def get_application_config() -> Union[Dict, HttpResponse]:
"""
Get Application config
@ -59,7 +62,7 @@ def get_application_config():
@config_blueprint.route('/config', methods=['PATCH'])
@authenticate_as_admin
def update_application_config(auth_user_id):
def update_application_config(auth_user_id: int) -> Union[Dict, HttpResponse]:
"""
Update Application config
@ -137,7 +140,7 @@ def update_application_config(auth_user_id):
@config_blueprint.route('/ping', methods=['GET'])
def health_check():
def health_check() -> Union[Dict, HttpResponse]:
"""health check endpoint
**Example request**:

View File

@ -1,11 +1,19 @@
from typing import Dict
from fittrackee import db
from flask import current_app
from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session
from ..users.models import User
BaseModel: DeclarativeMeta = db.Model
class AppConfig(db.Model):
class AppConfig(BaseModel):
__tablename__ = 'app_config'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
max_users = db.Column(db.Integer, default=0, nullable=False)
@ -16,26 +24,26 @@ class AppConfig(db.Model):
max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
@property
def is_registration_enabled(self):
def is_registration_enabled(self) -> bool:
nb_users = User.query.count()
return self.max_users == 0 or nb_users < self.max_users
@property
def map_attribution(self):
def map_attribution(self) -> str:
return current_app.config['TILE_SERVER']['ATTRIBUTION']
def serialize(self):
def serialize(self) -> Dict:
return {
"gpx_limit_import": self.gpx_limit_import,
"is_registration_enabled": self.is_registration_enabled,
"max_single_file_size": self.max_single_file_size,
"max_zip_file_size": self.max_zip_file_size,
"max_users": self.max_users,
"map_attribution": self.map_attribution,
'gpx_limit_import': self.gpx_limit_import,
'is_registration_enabled': self.is_registration_enabled,
'max_single_file_size': self.max_single_file_size,
'max_zip_file_size': self.max_zip_file_size,
'max_users': self.max_users,
'map_attribution': self.map_attribution,
}
def update_app_config():
def update_app_config() -> None:
config = AppConfig.query.first()
if config:
current_app.config[
@ -44,14 +52,16 @@ def update_app_config():
@listens_for(User, 'after_insert')
def on_user_insert(mapper, connection, user):
def on_user_insert(mapper: Mapper, connection: Connection, user: User) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Connection) -> None:
update_app_config()
@listens_for(User, 'after_delete')
def on_user_delete(mapper, connection, old_user):
def on_user_delete(
mapper: Mapper, connection: Connection, old_user: User
) -> None:
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session, context):
def receive_after_flush(session: Session, context: Connection) -> None:
update_app_config()

View File

@ -1,14 +1,16 @@
import os
from typing import Tuple
from fittrackee import db
from fittrackee.users.models import User
from flask import Flask
from .models import AppConfig
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
def init_config():
def init_config() -> Tuple[bool, AppConfig]:
"""
init application configuration if not existing in database
@ -36,7 +38,9 @@ def init_config():
return False, existing_config
def update_app_config_from_database(current_app, db_config):
def update_app_config_from_database(
current_app: Flask, db_config: AppConfig
) -> None:
current_app.config['gpx_limit_import'] = db_config.gpx_limit_import
current_app.config['max_single_file_size'] = db_config.max_single_file_size
current_app.config['MAX_CONTENT_LENGTH'] = db_config.max_zip_file_size

View File

@ -5,9 +5,10 @@ from fittrackee.application.utils import (
update_app_config_from_database,
)
from fittrackee.users.models import User
from flask import Flask
def init_database(app):
def init_database(app: Flask) -> None:
"""Init the database."""
admin = User(
username='admin', email='admin@example.com', password='mpwoadmin'

View File

@ -3,7 +3,9 @@ import smtplib
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Dict, Optional, Type, Union
from flask import Flask
from jinja2 import Environment, FileSystemLoader
from .utils_email import parse_email_url
@ -13,14 +15,16 @@ email_log.setLevel(logging.DEBUG)
class EmailMessage:
def __init__(self, sender, recipient, subject, html, text):
def __init__(
self, sender: str, recipient: str, subject: str, html: str, text: str
) -> None:
self.sender = sender
self.recipient = recipient
self.subject = subject
self.html = html
self.text = text
def generate_message(self):
def generate_message(self) -> MIMEMultipart:
message = MIMEMultipart('alternative')
message['Subject'] = self.subject
message['From'] = self.sender
@ -33,20 +37,24 @@ class EmailMessage:
class EmailTemplate:
def __init__(self, template_directory):
def __init__(self, template_directory: str) -> None:
self._env = Environment(loader=FileSystemLoader(template_directory))
def get_content(self, template, lang, part, data):
template = self._env.get_template(f'{template}/{lang}/{part}')
def get_content(
self, template_name: str, lang: str, part: str, data: Dict
) -> str:
template = self._env.get_template(f'{template_name}/{lang}/{part}')
return template.render(data)
def get_all_contents(self, template, lang, data):
def get_all_contents(self, template: str, lang: str, data: Dict) -> Dict:
output = {}
for part in ['subject.txt', 'body.txt', 'body.html']:
output[part] = self.get_content(template, lang, part, data)
return output
def get_message(self, template, lang, sender, recipient, data):
def get_message(
self, template: str, lang: str, sender: str, recipient: str, data: Dict
) -> MIMEMultipart:
output = self.get_all_contents(template, lang, data)
message = EmailMessage(
sender,
@ -59,7 +67,7 @@ class EmailTemplate:
class Email:
def __init__(self, app=None):
def __init__(self, app: Optional[Flask] = None) -> None:
self.host = 'localhost'
self.port = 1025
self.use_tls = False
@ -67,26 +75,30 @@ class Email:
self.username = None
self.password = None
self.sender_email = 'no-reply@example.com'
self.email_template = None
self.email_template: Optional[EmailTemplate] = None
if app is not None:
self.init_email(app)
def init_email(self, app):
parsed_url = parse_email_url(app.config.get('EMAIL_URL'))
def init_email(self, app: Flask) -> None:
parsed_url = parse_email_url(app.config['EMAIL_URL'])
self.host = parsed_url['host']
self.port = parsed_url['port']
self.use_tls = parsed_url['use_tls']
self.use_ssl = parsed_url['use_ssl']
self.username = parsed_url['username']
self.password = parsed_url['password']
self.sender_email = app.config.get('SENDER_EMAIL')
self.email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
self.sender_email = app.config['SENDER_EMAIL']
self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
@property
def smtp(self):
def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]:
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
def send(self, template, lang, recipient, data):
def send(
self, template: str, lang: str, recipient: str, data: Dict
) -> None:
if not self.email_template:
raise Exception('No email template defined.')
message = self.email_template.get_message(
template, lang, self.sender_email, recipient, data
)
@ -95,8 +107,10 @@ class Email:
context = ssl.create_default_context()
if self.use_ssl:
connection_params.update({'context': context})
with self.smtp(self.host, self.port, **connection_params) as smtp:
smtp.login(self.username, self.password)
with self.smtp(
self.host, self.port, **connection_params # type: ignore
) as smtp:
smtp.login(self.username, self.password) # type: ignore
if self.use_tls:
smtp.starttls(context=context)
smtp.sendmail(self.sender_email, recipient, message.as_string())

View File

@ -1,3 +1,5 @@
from typing import Dict
from urllib3.util import parse_url
@ -5,7 +7,7 @@ class InvalidEmailUrlScheme(Exception):
...
def parse_email_url(email_url):
def parse_email_url(email_url: str) -> Dict:
parsed_url = parse_url(email_url)
if parsed_url.scheme != 'smtp':
raise InvalidEmailUrlScheme()

View File

@ -1,20 +1,22 @@
from json import dumps
from typing import Dict, List, Optional, Union
from fittrackee import appLog
from flask import Response
from flask_sqlalchemy import SQLAlchemy
def get_empty_data_for_datatype(data_type):
def get_empty_data_for_datatype(data_type: str) -> Union[str, List]:
return '' if data_type in ['gpx', 'chart_data'] else []
class HttpResponse(Response):
def __init__(
self,
response=None,
status_code=None,
content_type=None,
):
response: Optional[Union[str, Dict]] = None,
status_code: Optional[int] = None,
content_type: Optional[str] = None,
) -> None:
if isinstance(response, dict):
response = dumps(response)
content_type = (
@ -28,7 +30,9 @@ class HttpResponse(Response):
class GenericErrorResponse(HttpResponse):
def __init__(self, status_code, message, status=None):
def __init__(
self, status_code: int, message: str, status: Optional[str] = None
) -> None:
response = {
'status': 'error' if status is None else status,
'message': message,
@ -40,13 +44,15 @@ class GenericErrorResponse(HttpResponse):
class InvalidPayloadErrorResponse(GenericErrorResponse):
def __init__(self, message=None, status=None):
def __init__(
self, message: Optional[str] = None, status: Optional[str] = None
) -> None:
message = 'Invalid payload.' if message is None else message
super().__init__(status_code=400, message=message, status=status)
class DataInvalidPayloadErrorResponse(HttpResponse):
def __init__(self, data_type, status=None):
def __init__(self, data_type: str, status: Optional[str] = None) -> None:
response = {
'status': 'error' if status is None else status,
'data': {data_type: get_empty_data_for_datatype(data_type)},
@ -55,7 +61,7 @@ class DataInvalidPayloadErrorResponse(HttpResponse):
class UnauthorizedErrorResponse(GenericErrorResponse):
def __init__(self, message=None):
def __init__(self, message: Optional[str] = None) -> None:
message = (
'Invalid token. Please request a new token.'
if message is None
@ -65,7 +71,7 @@ class UnauthorizedErrorResponse(GenericErrorResponse):
class ForbiddenErrorResponse(GenericErrorResponse):
def __init__(self, message=None):
def __init__(self, message: Optional[str] = None) -> None:
message = (
'You do not have permissions.' if message is None else message
)
@ -73,17 +79,17 @@ class ForbiddenErrorResponse(GenericErrorResponse):
class NotFoundErrorResponse(GenericErrorResponse):
def __init__(self, message):
def __init__(self, message: str) -> None:
super().__init__(status_code=404, message=message, status='not found')
class UserNotFoundErrorResponse(NotFoundErrorResponse):
def __init__(self):
def __init__(self) -> None:
super().__init__(message='User does not exist.')
class DataNotFoundErrorResponse(HttpResponse):
def __init__(self, data_type, message=None):
def __init__(self, data_type: str, message: Optional[str] = None) -> None:
response = {
'status': 'not found',
'data': {data_type: get_empty_data_for_datatype(data_type)},
@ -94,12 +100,14 @@ class DataNotFoundErrorResponse(HttpResponse):
class PayloadTooLargeErrorResponse(GenericErrorResponse):
def __init__(self, message):
def __init__(self, message: str) -> None:
super().__init__(status_code=413, message=message, status='fail')
class InternalServerErrorResponse(GenericErrorResponse):
def __init__(self, message=None, status=None):
def __init__(
self, message: Optional[str] = None, status: Optional[str] = None
):
message = (
'Error. Please try again or contact the administrator.'
if message is None
@ -109,8 +117,11 @@ class InternalServerErrorResponse(GenericErrorResponse):
def handle_error_and_return_response(
error, message=None, status=None, db=None
):
error: Exception,
message: Optional[str] = None,
status: Optional[str] = None,
db: Optional[SQLAlchemy] = None,
) -> HttpResponse:
if db is not None:
db.session.rollback()
appLog.error(error)

View File

@ -1,8 +1,10 @@
from typing import Dict
from fittrackee import dramatiq, email_service
@dramatiq.actor(queue_name='fittrackee_emails')
def reset_password_email(user, email_data):
def reset_password_email(user: Dict, email_data: Dict) -> None:
email_service.send(
template='password_reset_request',
lang=user['language'],

View File

@ -1,21 +1,25 @@
import json
from uuid import uuid4
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
from .utils import get_random_short_id
class TestGetActivities:
def test_it_gets_all_activities_for_authenticated_user(
self,
app,
user_1,
user_2,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_cycling_user_2,
activity_running_user_1,
):
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -57,14 +61,14 @@ class TestGetActivities:
def test_it_gets_no_activities_for_authenticated_user_with_no_activities(
self,
app,
user_1,
user_2,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -85,7 +89,9 @@ class TestGetActivities:
assert 'success' in data['status']
assert len(data['data']['activities']) == 0
def test_it_returns_401_if_user_is_not_authenticated(self, app):
def test_it_returns_401_if_user_is_not_authenticated(
self, app: Flask
) -> None:
client = app.test_client()
response = client.get('/api/activities')
@ -98,8 +104,12 @@ class TestGetActivities:
class TestGetActivitiesWithPagination:
def test_it_gets_activities_with_default_pagination(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -133,8 +143,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][4]['duration']
def test_it_gets_first_page(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -168,8 +182,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][4]['duration']
def test_it_gets_second_page(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -203,8 +221,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][1]['duration']
def test_it_gets_empty_third_page(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -226,8 +248,12 @@ class TestGetActivitiesWithPagination:
assert len(data['data']['activities']) == 0
def test_it_returns_error_on_invalid_page_value(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -252,8 +278,12 @@ class TestGetActivitiesWithPagination:
)
def test_it_gets_5_activities_per_page(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -283,8 +313,12 @@ class TestGetActivitiesWithPagination:
)
def test_it_gets_3_activities_per_page(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -316,8 +350,12 @@ class TestGetActivitiesWithPagination:
class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_date_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -351,8 +389,12 @@ class TestGetActivitiesWithFilters:
assert '0:16:40' == data['data']['activities'][1]['duration']
def test_it_gets_no_activities_with_date_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -374,8 +416,12 @@ class TestGetActivitiesWithFilters:
assert len(data['data']['activities']) == 0
def test_if_gets_activities_with_date_filter_from(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -406,8 +452,12 @@ class TestGetActivitiesWithFilters:
)
def test_it_gets_activities_with_date_filter_to(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -437,8 +487,12 @@ class TestGetActivitiesWithFilters:
)
def test_it_gets_activities_with_ascending_order(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -468,8 +522,12 @@ class TestGetActivitiesWithFilters:
)
def test_it_gets_activities_with_distance_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -499,8 +557,12 @@ class TestGetActivitiesWithFilters:
)
def test_it_gets_activities_with_duration_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -526,8 +588,12 @@ class TestGetActivitiesWithFilters:
)
def test_it_gets_activities_with_average_speed_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -554,13 +620,13 @@ class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_max_speed_filter(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
activity_cycling_user_1.max_speed = 25
activity_running_user_1.max_speed = 11
client = app.test_client()
@ -589,13 +655,13 @@ class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_sport_filter(
self,
app,
user_1,
sport_1_cycling,
seven_activities_user_1,
sport_2_running,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
sport_2_running: Sport,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -623,8 +689,12 @@ class TestGetActivitiesWithFilters:
class TestGetActivitiesWithFiltersAndPagination:
def test_it_gets_page_2_with_date_filter(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -654,8 +724,12 @@ class TestGetActivitiesWithFiltersAndPagination:
)
def test_it_get_page_2_with_date_filter_and_ascending_order(
self, app, user_1, sport_1_cycling, seven_activities_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_activities_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -687,8 +761,12 @@ class TestGetActivitiesWithFiltersAndPagination:
class TestGetActivity:
def test_it_gets_an_activity(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -719,8 +797,13 @@ class TestGetActivity:
assert '1:00:00' == data['data']['activities'][0]['duration']
def test_it_returns_403_if_activity_belongs_to_a_different_user(
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -741,7 +824,9 @@ class TestGetActivity:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
def test_it_returns_404_if_activity_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -763,8 +848,8 @@ class TestGetActivity:
assert len(data['data']['activities']) == 0
def test_it_returns_404_on_getting_gpx_if_activity_does_not_exist(
self, app, user_1
):
self, app: Flask, user_1: User
) -> None:
random_short_id = get_random_short_id()
client = app.test_client()
resp_login = client.post(
@ -788,8 +873,8 @@ class TestGetActivity:
assert data['data']['gpx'] == ''
def test_it_returns_404_on_getting_chart_data_if_activity_does_not_exist(
self, app, user_1
):
self, app: Flask, user_1: User
) -> None:
random_short_id = get_random_short_id()
client = app.test_client()
resp_login = client.post(
@ -813,8 +898,12 @@ class TestGetActivity:
assert data['data']['chart_data'] == ''
def test_it_returns_404_on_getting_gpx_if_activity_have_no_gpx(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -840,8 +929,12 @@ class TestGetActivity:
)
def test_it_returns_404_if_activity_have_no_chart_data(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -867,8 +960,12 @@ class TestGetActivity:
)
def test_it_returns_500_on_getting_gpx_if_an_activity_has_invalid_gpx_pathname( # noqa
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_cycling_user_1.gpx = "some path"
client = app.test_client()
resp_login = client.post(
@ -895,8 +992,12 @@ class TestGetActivity:
assert 'data' not in data
def test_it_returns_500_on_getting_chart_data_if_an_activity_has_invalid_gpx_pathname( # noqa
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_cycling_user_1.gpx = 'some path'
client = app.test_client()
resp_login = client.post(
@ -922,7 +1023,9 @@ class TestGetActivity:
)
assert 'data' not in data
def test_it_returns_404_if_activity_has_no_map(self, app, user_1):
def test_it_returns_404_if_activity_has_no_map(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -2,12 +2,15 @@ import json
import os
from datetime import datetime
from io import BytesIO
from typing import Dict
from fittrackee.activities.models import Activity
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from flask import Flask
def assert_activity_data_with_gpx(data):
def assert_activity_data_with_gpx(data: Dict) -> None:
assert 'creation_date' in data['data']['activities'][0]
assert (
'Tue, 13 Mar 2018 12:44:45 GMT'
@ -70,7 +73,7 @@ def assert_activity_data_with_gpx(data):
assert records[3]['value'] == 4.61
def assert_activity_data_with_gpx_segments(data):
def assert_activity_data_with_gpx_segments(data: Dict) -> None:
assert 'creation_date' in data['data']['activities'][0]
assert (
'Tue, 13 Mar 2018 12:44:45 GMT'
@ -144,7 +147,7 @@ def assert_activity_data_with_gpx_segments(data):
assert records[2]['value'] == 4.59
def assert_activity_data_wo_gpx(data):
def assert_activity_data_wo_gpx(data: Dict) -> None:
assert 'creation_date' in data['data']['activities'][0]
assert (
data['data']['activities'][0]['activity_date']
@ -200,8 +203,8 @@ def assert_activity_data_wo_gpx(data):
class TestPostActivityWithGpx:
def test_it_adds_an_activity_with_gpx_file(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -230,8 +233,12 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data)
def test_it_adds_an_activity_with_gpx_without_name(
self, app, user_1, sport_1_cycling, gpx_file_wo_name
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
gpx_file_wo_name: str,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -263,8 +270,12 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data)
def test_it_adds_an_activity_with_gpx_without_name_timezone(
self, app, user_1, sport_1_cycling, gpx_file_wo_name
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
gpx_file_wo_name: str,
) -> None:
user_1.timezone = 'Europe/Paris'
client = app.test_client()
resp_login = client.post(
@ -297,8 +308,8 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data)
def test_it_adds_get_an_activity_with_gpx_notes(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -327,8 +338,12 @@ class TestPostActivityWithGpx:
assert 'test activity' == data['data']['activities'][0]['notes']
def test_it_returns_500_if_gpx_file_has_not_tracks(
self, app, user_1, sport_1_cycling, gpx_file_wo_track
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
gpx_file_wo_track: str,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -352,12 +367,16 @@ class TestPostActivityWithGpx:
data = json.loads(response.data.decode())
assert response.status_code == 500
assert 'error' in data['status']
assert 'Error during gpx file parsing.' in data['message']
assert 'Error during gpx processing.' in data['message']
assert 'data' not in data
def test_it_returns_500_if_gpx_has_invalid_xml(
self, app, user_1, sport_1_cycling, gpx_file_invalid_xml
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
gpx_file_invalid_xml: str,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -388,8 +407,8 @@ class TestPostActivityWithGpx:
assert 'data' not in data
def test_it_returns_400_if_activity_gpx_has_invalid_extension(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -416,8 +435,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'File extension not allowed.'
def test_it_returns_400_if_sport_id_is_not_provided(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -443,8 +462,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'Invalid payload.'
def test_it_returns_500_if_sport_id_does_not_exists(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -471,8 +490,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'Sport id: 2 does not exist'
def test_returns_400_if_no_gpx_file_is_provided(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -498,8 +517,8 @@ class TestPostActivityWithGpx:
class TestPostActivityWithoutGpx:
def test_it_adds_an_activity_without_gpx(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -531,8 +550,8 @@ class TestPostActivityWithoutGpx:
assert_activity_data_wo_gpx(data)
def test_it_returns_400_if_activity_date_is_missing(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -556,8 +575,8 @@ class TestPostActivityWithoutGpx:
assert 'Invalid payload.' in data['message']
def test_it_returns_500_if_activity_format_is_invalid(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -588,8 +607,12 @@ class TestPostActivityWithoutGpx:
assert 'Error during activity save.' in data['message']
def test_it_adds_activity_with_zero_value(
self, app, user_1, sport_1_cycling, sport_2_running
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -645,8 +668,8 @@ class TestPostActivityWithoutGpx:
class TestPostActivityWithZipArchive:
def test_it_adds_activities_with_zip_archive(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip')
# 'gpx_test.zip' contains 3 gpx files (same data) and 1 non-gpx file
with open(file_path, 'rb') as zip_file:
@ -679,8 +702,8 @@ class TestPostActivityWithZipArchive:
assert_activity_data_with_gpx(data)
def test_it_returns_400_if_folder_is_present_in_zpi_archive(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
file_path = os.path.join(
app.root_path, 'tests/files/gpx_test_folder.zip'
)
@ -715,8 +738,8 @@ class TestPostActivityWithZipArchive:
assert len(data['data']['activities']) == 0
def test_it_returns_500_if_one_fle_in_zip_archive_is_invalid(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
file_path = os.path.join(
app.root_path, 'tests/files/gpx_test_incorrect.zip'
)
@ -747,13 +770,15 @@ class TestPostActivityWithZipArchive:
data = json.loads(response.data.decode())
assert response.status_code == 500
assert 'error' in data['status']
assert 'Error during gpx file parsing.' in data['message']
assert 'Error during gpx processing.' in data['message']
assert 'data' not in data
class TestPostAndGetActivityWithGpx:
@staticmethod
def activity_assertion(app, gpx_file, with_segments):
def activity_assertion(
app: Flask, gpx_file: str, with_segments: bool
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -849,18 +874,22 @@ class TestPostAndGetActivityWithGpx:
)
def test_it_gets_an_activity_created_with_gpx(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
return self.activity_assertion(app, gpx_file, False)
def test_it_gets_an_activity_created_with_gpx_with_segments(
self, app, user_1, sport_1_cycling, gpx_file_with_segments
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
gpx_file_with_segments: str,
) -> None:
return self.activity_assertion(app, gpx_file_with_segments, True)
def test_it_gets_chart_data_for_an_activity_created_with_gpx(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -897,8 +926,8 @@ class TestPostAndGetActivityWithGpx:
assert data['data']['chart_data'] != ''
def test_it_gets_segment_chart_data_for_an_activity_created_with_gpx(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -935,8 +964,13 @@ class TestPostAndGetActivityWithGpx:
assert data['data']['chart_data'] != ''
def test_it_returns_403_on_getting_chart_data_if_activity_belongs_to_another_user( # noqa
self, app, user_1, user_2, sport_1_cycling, gpx_file
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
gpx_file: str,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -977,8 +1011,8 @@ class TestPostAndGetActivityWithGpx:
assert data['message'] == 'You do not have permissions.'
def test_it_returns_500_on_invalid_segment_id(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1015,8 +1049,8 @@ class TestPostAndGetActivityWithGpx:
assert 'data' not in data
def test_it_returns_404_if_segment_id_does_not_exist(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1055,8 +1089,8 @@ class TestPostAndGetActivityWithGpx:
class TestPostAndGetActivityWithoutGpx:
def test_it_add_and_gets_an_activity_wo_gpx(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1097,8 +1131,8 @@ class TestPostAndGetActivityWithoutGpx:
assert_activity_data_wo_gpx(data)
def test_it_adds_and_gets_an_activity_wo_gpx_notes(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1142,8 +1176,8 @@ class TestPostAndGetActivityWithoutGpx:
class TestPostAndGetActivityUsingTimezones:
def test_it_add_and_gets_an_activity_wo_gpx_with_timezone(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
user_1.timezone = 'Europe/Paris'
client = app.test_client()
resp_login = client.post(
@ -1192,8 +1226,8 @@ class TestPostAndGetActivityUsingTimezones:
)
def test_it_adds_and_gets_activities_date_filter_with_timezone_new_york(
self, app, user_1_full, sport_1_cycling
):
self, app: Flask, user_1_full: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1239,8 +1273,12 @@ class TestPostAndGetActivityUsingTimezones:
)
def test_it_adds_and_gets_activities_date_filter_with_timezone_paris(
self, app, user_1_paris, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1_paris: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,12 +1,15 @@
import json
from typing import Dict
from fittrackee.activities.models import Activity
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from flask import Flask
from .utils import get_random_short_id, post_an_activity
def assert_activity_data_with_gpx(data, sport_id):
def assert_activity_data_with_gpx(data: Dict, sport_id: int) -> None:
assert 'creation_date' in data['data']['activities'][0]
assert (
'Tue, 13 Mar 2018 12:44:45 GMT'
@ -51,8 +54,13 @@ def assert_activity_data_with_gpx(data, sport_id):
class TestEditActivityWithGpx:
def test_it_updates_title_for_an_activity_with_gpx(
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -72,8 +80,13 @@ class TestEditActivityWithGpx:
assert_activity_data_with_gpx(data, sport_2_running.id)
def test_it_adds_notes_for_an_activity_with_gpx(
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -92,8 +105,14 @@ class TestEditActivityWithGpx:
assert data['data']['activities'][0]['notes'] == 'test notes'
def test_it_raises_403_when_editing_an_activity_from_different_user(
self, app, user_1, user_2, sport_1_cycling, sport_2_running, gpx_file
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
gpx_file: str,
) -> None:
_, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
resp_login = client.post(
@ -118,8 +137,13 @@ class TestEditActivityWithGpx:
assert 'You do not have permissions.' in data['message']
def test_it_updates_sport(
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -139,8 +163,8 @@ class TestEditActivityWithGpx:
assert_activity_data_with_gpx(data, sport_2_running.id)
def test_it_returns_400_if_payload_is_empty(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -157,8 +181,8 @@ class TestEditActivityWithGpx:
assert 'Invalid payload.' in data['message']
def test_it_raises_500_if_sport_does_not_exists(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -181,12 +205,12 @@ class TestEditActivityWithGpx:
class TestEditActivityWithoutGpx:
def test_it_updates_an_activity_wo_gpx(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -266,8 +290,12 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 8.0
def test_it_adds_notes_to_an_activity_wo_gpx(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -338,8 +366,13 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 10.0
def test_returns_403_when_editing_an_activity_wo_gpx_from_different_user(
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -372,12 +405,12 @@ class TestEditActivityWithoutGpx:
def test_it_updates_an_activity_wo_gpx_with_timezone(
self,
app,
user_1_paris,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
):
app: Flask,
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -453,12 +486,12 @@ class TestEditActivityWithoutGpx:
def test_it_updates_only_sport_and_distance_an_activity_wo_gpx(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
) -> None:
activity_short_id = activity_cycling_user_1.short_id
client = app.test_client()
resp_login = client.post(
@ -525,8 +558,12 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 20.0
def test_it_returns_400_if_payload_is_empty(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -550,8 +587,12 @@ class TestEditActivityWithoutGpx:
assert 'Invalid payload.' in data['message']
def test_it_returns_500_if_date_format_is_invalid(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -585,8 +626,8 @@ class TestEditActivityWithoutGpx:
)
def test_it_returns_404_if_edited_activity_does_not_exists(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -618,8 +659,13 @@ class TestEditActivityWithoutGpx:
class TestRefreshActivityWithGpx:
def test_refresh_an_activity_with_gpx(
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
gpx_file: str,
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
activity_uuid = decode_short_id(activity_short_id)
client = app.test_client()

View File

@ -1,21 +1,23 @@
import json
import os
from fittrackee.activities.models import Activity
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils import get_absolute_file_path
from fittrackee.users.models import User
from flask import Flask
from .utils import get_random_short_id, post_an_activity
def get_gpx_filepath(activity_id):
def get_gpx_filepath(activity_id: int) -> str:
activity = Activity.query.filter_by(id=activity_id).first()
return activity.gpx
class TestDeleteActivityWithGpx:
def test_it_deletes_an_activity_with_gpx(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
@ -27,8 +29,13 @@ class TestDeleteActivityWithGpx:
assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user(
self, app, user_1, user_2, sport_1_cycling, gpx_file
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
gpx_file: str,
) -> None:
_, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
resp_login = client.post(
@ -51,7 +58,9 @@ class TestDeleteActivityWithGpx:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
def test_it_returns_404_if_activity_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -70,8 +79,8 @@ class TestDeleteActivityWithGpx:
assert 'not found' in data['status']
def test_it_returns_500_when_deleting_an_activity_with_gpx_invalid_file(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client()
gpx_filepath = get_gpx_filepath(1)
@ -95,8 +104,12 @@ class TestDeleteActivityWithGpx:
class TestDeleteActivityWithoutGpx:
def test_it_deletes_an_activity_wo_gpx(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -113,8 +126,13 @@ class TestDeleteActivityWithoutGpx:
assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user(
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,12 +1,19 @@
from uuid import UUID
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from flask import Flask
class TestActivityModel:
def test_activity_model(
self, app, sport_1_cycling, user_1, activity_cycling_user_1
):
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
) -> None:
activity_cycling_user_1.title = 'Test'
assert 1 == activity_cycling_user_1.id
@ -55,12 +62,12 @@ class TestActivityModel:
def test_activity_segment_model(
self,
app,
sport_1_cycling,
user_1,
activity_cycling_user_1,
activity_cycling_user_1_segment,
):
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
activity_cycling_user_1_segment: Activity,
) -> None:
assert (
f'<Segment \'{activity_cycling_user_1_segment.segment_id}\' '
f'for activity \'{activity_cycling_user_1.short_id}\'>'

View File

@ -1,17 +1,21 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetRecords:
def test_it_gets_records_for_authenticated_user(
self,
app,
user_1,
user_2,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -85,13 +89,13 @@ class TestGetRecords:
def test_it_gets_no_records_if_user_has_no_activity(
self,
app,
user_1,
user_2,
sport_1_cycling,
sport_2_running,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -112,8 +116,12 @@ class TestGetRecords:
assert len(data['data']['records']) == 0
def test_it_gets_no_records_if_activity_has_zero_value(
self, app, user_1, sport_1_cycling, sport_2_running
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -151,8 +159,8 @@ class TestGetRecords:
assert len(data['data']['records']) == 0
def test_it_gets_updated_records_after_activities_post_and_patch(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -696,8 +704,12 @@ class TestGetRecords:
assert len(data['data']['records']) == 0
def test_it_gets_updated_records_after_sport_change(
self, app, user_1, sport_1_cycling, sport_2_running
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,12 +1,18 @@
import datetime
from fittrackee.activities.models import Record
from fittrackee.activities.models import Activity, Record, Sport
from fittrackee.users.models import User
from flask import Flask
class TestRecordModel:
def test_record_model(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -29,8 +35,12 @@ class TestRecordModel:
assert 'value' in record_serialize
def test_record_model_with_none_value(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -49,8 +59,12 @@ class TestRecordModel:
assert record_serialize['value'] is None
def test_average_speed_records(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_as = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -66,8 +80,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), float)
def test_add_farest_distance_records(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_fd = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -83,8 +101,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), float)
def test_add_longest_duration_records(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -100,8 +122,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), str)
def test_add_longest_duration_records_with_zero(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,
@ -118,8 +144,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), str)
def test_max_speed_records_no_value(
self, app, user_1, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
record_ms = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id,

View File

@ -1,5 +1,9 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
expected_sport_1_cycling_result = {
'id': 1,
'label': 'Cycling',
@ -32,8 +36,12 @@ expected_sport_1_cycling_inactive_admin_result['has_activities'] = False
class TestGetSports:
def test_it_gets_all_sports(
self, app, user_1, sport_1_cycling, sport_2_running
):
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -57,8 +65,12 @@ class TestGetSports:
assert data['data']['sports'][1] == expected_sport_2_running_result
def test_it_gets_all_sports_with_inactive_one(
self, app, user_1, sport_1_cycling_inactive, sport_2_running
):
self,
app: Flask,
user_1: User,
sport_1_cycling_inactive: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -85,8 +97,12 @@ class TestGetSports:
assert data['data']['sports'][1] == expected_sport_2_running_result
def test_it_gets_all_sports_with_admin_rights(
self, app, user_1_admin, sport_1_cycling_inactive, sport_2_running
):
self,
app: Flask,
user_1_admin: User,
sport_1_cycling_inactive: Sport,
sport_2_running: Sport,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -118,7 +134,9 @@ class TestGetSports:
class TestGetSport:
def test_it_gets_a_sport(self, app, user_1, sport_1_cycling):
def test_it_gets_a_sport(
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -140,7 +158,9 @@ class TestGetSport:
assert len(data['data']['sports']) == 1
assert data['data']['sports'][0] == expected_sport_1_cycling_result
def test_it_returns_404_if_sport_does_not_exist(self, app, user_1):
def test_it_returns_404_if_sport_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -162,8 +182,8 @@ class TestGetSport:
assert len(data['data']['sports']) == 0
def test_it_gets_a_inactive_sport(
self, app, user_1, sport_1_cycling_inactive
):
self, app: Flask, user_1: User, sport_1_cycling_inactive: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -189,8 +209,8 @@ class TestGetSport:
)
def test_it_get_an_inactive_sport_with_admin_rights(
self, app, user_1_admin, sport_1_cycling_inactive
):
self, app: Flask, user_1_admin: User, sport_1_cycling_inactive: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -219,7 +239,9 @@ class TestGetSport:
class TestUpdateSport:
def test_it_disables_a_sport(self, app, user_1_admin, sport_1_cycling):
def test_it_disables_a_sport(
self, app: Flask, user_1_admin: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -246,7 +268,9 @@ class TestUpdateSport:
assert data['data']['sports'][0]['is_active'] is False
assert data['data']['sports'][0]['has_activities'] is False
def test_it_enables_a_sport(self, app, user_1_admin, sport_1_cycling):
def test_it_enables_a_sport(
self, app: Flask, user_1_admin: User, sport_1_cycling: Sport
) -> None:
sport_1_cycling.is_active = False
client = app.test_client()
resp_login = client.post(
@ -275,8 +299,12 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is False
def test_it_disables_a_sport_with_activities(
self, app, user_1_admin, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1_admin: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -304,8 +332,12 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is True
def test_it_enables_a_sport_with_activities(
self, app, user_1_admin, sport_1_cycling, activity_cycling_user_1
):
self,
app: Flask,
user_1_admin: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
) -> None:
sport_1_cycling.is_active = False
client = app.test_client()
resp_login = client.post(
@ -334,8 +366,8 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is True
def test_returns_error_if_user_has_no_admin_rights(
self, app, user_1, sport_1_cycling
):
self, app: Flask, user_1: User, sport_1_cycling: Sport
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -358,7 +390,9 @@ class TestUpdateSport:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_returns_error_if_payload_is_invalid(self, app, user_1_admin):
def test_returns_error_if_payload_is_invalid(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -383,7 +417,9 @@ class TestUpdateSport:
assert 'error' in data['status']
assert 'Invalid payload.' in data['message']
def test_it_returns_error_if_sport_does_not_exist(self, app, user_1_admin):
def test_it_returns_error_if_sport_does_not_exist(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,6 +1,15 @@
from typing import Dict, Optional
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestSportModel:
@staticmethod
def assert_sport_model(sport, is_admin=False):
def assert_sport_model(
sport: Sport, is_admin: Optional[bool] = False
) -> Dict:
assert 1 == sport.id
assert 'Cycling' == sport.label
assert '<Sport \'Cycling\'>' == str(sport)
@ -11,18 +20,26 @@ class TestSportModel:
assert serialized_sport['is_active'] is True
return serialized_sport
def test_sport_model(self, app, sport_1_cycling):
def test_sport_model(self, app: Flask, sport_1_cycling: Sport) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport
def test_sport_model_with_activity(
self, app, sport_1_cycling, user_1, activity_cycling_user_1
):
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport
def test_sport_model_with_activity_as_admin(
self, app, sport_1_cycling, user_1, activity_cycling_user_1
):
self,
app: Flask,
sport_1_cycling: Sport,
user_1: User,
activity_cycling_user_1: Activity,
) -> None:
serialized_sport = self.assert_sport_model(sport_1_cycling, True)
assert serialized_sport['has_activities'] is True

View File

@ -1,8 +1,14 @@
import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetStatsByTime:
def test_it_gets_no_stats_when_user_has_no_activities(self, app, user_1):
def test_it_gets_no_stats_when_user_has_no_activities(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -23,7 +29,9 @@ class TestGetStatsByTime:
assert 'success' in data['status']
assert data['data']['statistics'] == {}
def test_it_returns_error_when_user_does_not_exists(self, app, user_1):
def test_it_returns_error_when_user_does_not_exists(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -46,13 +54,13 @@ class TestGetStatsByTime:
def test_it_returns_error_if_date_format_is_invalid(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -78,13 +86,13 @@ class TestGetStatsByTime:
def test_it_returns_error_if_period_is_invalid(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -107,13 +115,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_time_all_activities(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -156,13 +164,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_for_april_2018(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -198,13 +206,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_for_april_2018_with_paris_timezone(
self,
app,
user_1_paris,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -241,13 +249,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -290,13 +298,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year_for_april_2018(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -332,13 +340,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year_for_april_2018_with_paris_timezone(
self,
app,
user_1_paris,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1_paris: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -374,13 +382,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -451,13 +459,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month_with_new_york_timezone(
self,
app,
user_1_full,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1_full: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -528,13 +536,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month_for_april_2018(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -570,13 +578,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week(
self,
app,
user_1_full,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1_full: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -647,13 +655,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week_for_week_13(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -689,13 +697,13 @@ class TestGetStatsByTime:
def test_if_get_stats_by_week_starting_with_monday(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -766,13 +774,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week_starting_with_monday_for_week_13(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -810,13 +818,13 @@ class TestGetStatsByTime:
class TestGetStatsBySport:
def test_it_gets_stats_by_sport(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -850,13 +858,13 @@ class TestGetStatsBySport:
def test_it_get_stats_for_sport_1(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -885,13 +893,13 @@ class TestGetStatsBySport:
def test_it_returns_errors_if_user_does_not_exist(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -914,13 +922,13 @@ class TestGetStatsBySport:
def test_it_returns_error_if_sport_does_not_exist(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -943,13 +951,13 @@ class TestGetStatsBySport:
def test_it_returns_error_if_sport_id_is_invalid(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
seven_activities_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
seven_activities_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -976,8 +984,8 @@ class TestGetStatsBySport:
class TestGetAllStats:
def test_it_returns_all_stats_when_users_have_no_activities(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1005,16 +1013,16 @@ class TestGetAllStats:
def test_it_gets_app_all_stats_with_activities(
self,
app,
user_1_admin,
user_2,
user_3,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_cycling_user_2,
activity_running_user_1,
):
app: Flask,
user_1_admin: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1042,16 +1050,16 @@ class TestGetAllStats:
def test_it_returns_error_if_user_has_no_admin_rights(
self,
app,
user_1,
user_2,
user_3,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_cycling_user_2,
activity_running_user_1,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_cycling_user_2: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,15 +1,17 @@
import json
from io import BytesIO
from typing import Tuple
from uuid import uuid4
from fittrackee.activities.utils_id import encode_uuid
from flask import Flask
def get_random_short_id():
def get_random_short_id() -> str:
return encode_uuid(uuid4())
def post_an_activity(app, gpx_file):
def post_an_activity(app: Flask, gpx_file: str) -> Tuple[str, str]:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,8 +1,13 @@
import json
from fittrackee.users.models import User
from flask import Flask
class TestGetConfig:
def test_it_gets_application_config(self, app, user_1):
def test_it_gets_application_config(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -33,8 +38,8 @@ class TestGetConfig:
)
def test_it_returns_error_if_application_has_no_config(
self, app_no_config, user_1_admin
):
self, app_no_config: Flask, user_1_admin: User
) -> None:
client = app_no_config.test_client()
resp_login = client.post(
'/api/auth/login',
@ -59,8 +64,8 @@ class TestGetConfig:
assert 'Error on getting configuration.' in data['message']
def test_it_returns_error_if_application_has_several_config(
self, app, app_config, user_1_admin
):
self, app: Flask, app_config: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -86,7 +91,9 @@ class TestGetConfig:
class TestUpdateConfig:
def test_it_updates_config_when_user_is_admin(self, app, user_1_admin):
def test_it_updates_config_when_user_is_admin(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -114,7 +121,9 @@ class TestUpdateConfig:
assert data['data']['max_zip_file_size'] == 10485760
assert data['data']['max_users'] == 10
def test_it_updates_all_config(self, app, user_1_admin):
def test_it_updates_all_config(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -150,7 +159,9 @@ class TestUpdateConfig:
assert data['data']['max_zip_file_size'] == 25000
assert data['data']['max_users'] == 50
def test_it_returns_403_when_user_is_not_an_admin(self, app, user_1):
def test_it_returns_403_when_user_is_not_an_admin(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -174,7 +185,9 @@ class TestUpdateConfig:
assert 'error' in data['status']
assert 'You do not have permissions.' in data['message']
def test_it_returns_400_if_invalid_is_payload(self, app, user_1_admin):
def test_it_returns_400_if_invalid_is_payload(
self, app: Flask, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -200,8 +213,8 @@ class TestUpdateConfig:
assert 'Invalid payload.' in data['message']
def test_it_returns_error_on_update_if_application_has_no_config(
self, app_no_config, user_1_admin
):
self, app_no_config: Flask, user_1_admin: User
) -> None:
client = app_no_config.test_client()
resp_login = client.post(
'/api/auth/login',

View File

@ -1,8 +1,9 @@
from fittrackee.application.models import AppConfig
from flask import Flask
class TestConfigModel:
def test_application_config(self, app):
def test_application_config(self, app: Flask) -> None:
app_config = AppConfig.query.first()
assert 1 == app_config.id

View File

@ -1,8 +1,10 @@
import os
from flask import Flask
class TestConfig:
def test_development_config(self, app):
def test_development_config(self, app: Flask) -> None:
app.config.from_object('fittrackee.config.DevelopmentConfig')
assert app.config['DEBUG']
assert not app.config['TESTING']
@ -10,7 +12,7 @@ class TestConfig:
'DATABASE_URL'
)
def test_testing_config(self, app):
def test_testing_config(self, app: Flask) -> None:
app.config.from_object('fittrackee.config.TestingConfig')
assert app.config['DEBUG']
assert app.config['TESTING']

View File

@ -1,8 +1,10 @@
import json
from flask import Flask
class TestHealthCheck:
def test_it_returns_pong_on_health_check(self, app):
def test_it_returns_pong_on_health_check(self, app: Flask) -> None:
""" => Ensure the /health_check route behaves correctly."""
client = app.test_client()
response = client.get('/api/ping')

View File

@ -1,5 +1,6 @@
import datetime
import os
from typing import Generator, Optional
import pytest
from fittrackee import create_app, db
@ -11,10 +12,10 @@ from fittrackee.users.models import User
os.environ['FLASK_ENV'] = 'testing'
os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig'
# to avoid resetting dev database during tests
os.environ['DATABASE_URL'] = os.getenv('DATABASE_TEST_URL')
os.environ['DATABASE_URL'] = os.environ['DATABASE_TEST_URL']
def get_app_config(with_config=False):
def get_app_config(with_config: Optional[bool] = False) -> Optional[AppConfig]:
if with_config:
config = AppConfig()
config.gpx_limit_import = 10
@ -28,7 +29,7 @@ def get_app_config(with_config=False):
return None
def get_app(with_config=False):
def get_app(with_config: Optional[bool] = False) -> Generator:
app = create_app()
with app.app_context():
db.create_all()
@ -46,7 +47,7 @@ def get_app(with_config=False):
@pytest.fixture
def app(monkeypatch):
def app(monkeypatch: pytest.MonkeyPatch) -> Generator:
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
if os.getenv('TILE_SERVER_URL'):
monkeypatch.delenv('TILE_SERVER_URL')
@ -56,24 +57,24 @@ def app(monkeypatch):
@pytest.fixture
def app_no_config():
def app_no_config() -> Generator:
yield from get_app(with_config=False)
@pytest.fixture
def app_ssl(monkeypatch):
def app_ssl(monkeypatch: pytest.MonkeyPatch) -> Generator:
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?ssl=True')
yield from get_app(with_config=True)
@pytest.fixture
def app_tls(monkeypatch):
def app_tls(monkeypatch: pytest.MonkeyPatch) -> Generator:
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?tls=True')
yield from get_app(with_config=True)
@pytest.fixture()
def app_config():
def app_config() -> AppConfig:
config = AppConfig()
config.gpx_limit_import = 10
config.max_single_file_size = 1048576
@ -85,7 +86,7 @@ def app_config():
@pytest.fixture()
def user_1():
def user_1() -> User:
user = User(username='test', email='test@test.com', password='12345678')
db.session.add(user)
db.session.commit()
@ -93,7 +94,7 @@ def user_1():
@pytest.fixture()
def user_1_admin():
def user_1_admin() -> User:
admin = User(
username='admin', email='admin@example.com', password='12345678'
)
@ -104,7 +105,7 @@ def user_1_admin():
@pytest.fixture()
def user_1_full():
def user_1_full() -> User:
user = User(username='test', email='test@test.com', password='12345678')
user.first_name = 'John'
user.last_name = 'Doe'
@ -119,7 +120,7 @@ def user_1_full():
@pytest.fixture()
def user_1_paris():
def user_1_paris() -> User:
user = User(username='test', email='test@test.com', password='12345678')
user.timezone = 'Europe/Paris'
db.session.add(user)
@ -128,7 +129,7 @@ def user_1_paris():
@pytest.fixture()
def user_2():
def user_2() -> User:
user = User(username='toto', email='toto@toto.com', password='87654321')
db.session.add(user)
db.session.commit()
@ -136,7 +137,7 @@ def user_2():
@pytest.fixture()
def user_2_admin():
def user_2_admin() -> User:
user = User(username='toto', email='toto@toto.com', password='87654321')
user.admin = True
db.session.add(user)
@ -145,7 +146,7 @@ def user_2_admin():
@pytest.fixture()
def user_3():
def user_3() -> User:
user = User(username='sam', email='sam@test.com', password='12345678')
user.weekm = True
db.session.add(user)
@ -154,7 +155,7 @@ def user_3():
@pytest.fixture()
def sport_1_cycling():
def sport_1_cycling() -> Sport:
sport = Sport(label='Cycling')
db.session.add(sport)
db.session.commit()
@ -162,7 +163,7 @@ def sport_1_cycling():
@pytest.fixture()
def sport_1_cycling_inactive():
def sport_1_cycling_inactive() -> Sport:
sport = Sport(label='Cycling')
sport.is_active = False
db.session.add(sport)
@ -171,7 +172,7 @@ def sport_1_cycling_inactive():
@pytest.fixture()
def sport_2_running():
def sport_2_running() -> Sport:
sport = Sport(label='Running')
db.session.add(sport)
db.session.commit()
@ -179,7 +180,7 @@ def sport_2_running():
@pytest.fixture()
def activity_cycling_user_1():
def activity_cycling_user_1() -> Activity:
activity = Activity(
user_id=1,
sport_id=1,
@ -196,7 +197,9 @@ def activity_cycling_user_1():
@pytest.fixture()
def activity_cycling_user_1_segment(activity_cycling_user_1):
def activity_cycling_user_1_segment(
activity_cycling_user_1: Activity,
) -> ActivitySegment:
activity_segment = ActivitySegment(
activity_id=activity_cycling_user_1.id,
activity_uuid=activity_cycling_user_1.uuid,
@ -211,7 +214,7 @@ def activity_cycling_user_1_segment(activity_cycling_user_1):
@pytest.fixture()
def activity_running_user_1():
def activity_running_user_1() -> Activity:
activity = Activity(
user_id=1,
sport_id=2,
@ -226,7 +229,7 @@ def activity_running_user_1():
@pytest.fixture()
def seven_activities_user_1():
def seven_activities_user_1() -> Activity:
activity = Activity(
user_id=1,
sport_id=1,
@ -308,7 +311,7 @@ def seven_activities_user_1():
@pytest.fixture()
def activity_cycling_user_2():
def activity_cycling_user_2() -> Activity:
activity = Activity(
user_id=2,
sport_id=1,
@ -323,7 +326,7 @@ def activity_cycling_user_2():
@pytest.fixture()
def gpx_file():
def gpx_file() -> str:
return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
@ -438,7 +441,7 @@ def gpx_file():
@pytest.fixture()
def gpx_file_wo_name():
def gpx_file_wo_name() -> str:
return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
@ -552,7 +555,7 @@ def gpx_file_wo_name():
@pytest.fixture()
def gpx_file_wo_track():
def gpx_file_wo_track() -> str:
return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
@ -562,7 +565,7 @@ def gpx_file_wo_track():
@pytest.fixture()
def gpx_file_invalid_xml():
def gpx_file_invalid_xml() -> str:
return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
@ -571,7 +574,7 @@ def gpx_file_invalid_xml():
@pytest.fixture()
def gpx_file_with_segments():
def gpx_file_with_segments() -> str:
return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa

View File

@ -1,13 +1,15 @@
from unittest.mock import patch
from typing import Any
from unittest.mock import Mock, patch
from fittrackee import email_service
from fittrackee.email.email import EmailMessage
from flask import Flask
from ..template_results.password_reset_request import expected_en_text_body
class TestEmailMessage:
def test_it_generate_email_data(self):
def test_it_generate_email_data(self) -> None:
message = EmailMessage(
sender='fittrackee@example.com',
recipient='test@test.com',
@ -40,14 +42,14 @@ class TestEmailSending:
}
@staticmethod
def get_args(call_args):
def get_args(call_args: Any) -> Any:
if len(call_args) == 2:
args, _ = call_args
else:
_, args, _ = call_args
return args
def assert_smtp(self, smtp):
def assert_smtp(self, smtp: Mock) -> None:
assert smtp.sendmail.call_count == 1
call_args = self.get_args(smtp.sendmail.call_args)
assert call_args[0] == 'fittrackee@example.com'
@ -56,7 +58,9 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP')
def test_it_sends_message(self, mock_smtp, mock_smtp_ssl, app):
def test_it_sends_message(
self, mock_smtp: Mock, mock_smtp_ssl: Mock, app: Flask
) -> None:
email_service.send(
template='password_reset_request',
@ -72,8 +76,8 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP')
def test_it_sends_message_with_ssl(
self, mock_smtp, mock_smtp_ssl, app_ssl
):
self, mock_smtp: Mock, mock_smtp_ssl: Mock, app_ssl: Flask
) -> None:
email_service.send(
template='password_reset_request',
lang='en',
@ -88,8 +92,8 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP')
def test_it_sends_message_with_tls(
self, mock_smtp, mock_smtp_ssl, app_tls
):
self, mock_smtp: Mock, mock_smtp_ssl: Mock, app_tls: Flask
) -> None:
email_service.send(
template='password_reset_request',
lang='en',

View File

@ -1,5 +1,6 @@
import pytest
from fittrackee.email.email import EmailTemplate
from flask import Flask
from ..template_results.password_reset_request import (
expected_en_html_body,
@ -17,8 +18,10 @@ class TestEmailTemplateForPasswordRequest:
('fr', 'FitTrackee - Réinitialiser votre mot de passe'),
],
)
def test_it_gets_subject(self, app, lang, expected_subject):
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
def test_it_gets_subject(
self, app: Flask, lang: str, expected_subject: str
) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
subject = email_template.get_content(
'password_reset_request', lang, 'subject.txt', {}
@ -30,8 +33,10 @@ class TestEmailTemplateForPasswordRequest:
'lang, expected_text_body',
[('en', expected_en_text_body), ('fr', expected_fr_text_body)],
)
def test_it_gets_text_body(self, app, lang, expected_text_body):
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
def test_it_gets_text_body(
self, app: Flask, lang: str, expected_text_body: str
) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = {
'expiration_delay': '3 seconds' if lang == 'en' else '3 secondes',
'username': 'test',
@ -46,8 +51,8 @@ class TestEmailTemplateForPasswordRequest:
assert text_body == expected_text_body
def test_it_gets_en_html_body(self, app):
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
def test_it_gets_en_html_body(self, app: Flask) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = {
'expiration_delay': '3 seconds',
'username': 'test',
@ -62,8 +67,8 @@ class TestEmailTemplateForPasswordRequest:
assert expected_en_html_body in text_body
def test_it_gets_fr_html_body(self, app):
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
def test_it_gets_fr_html_body(self, app: Flask) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = {
'expiration_delay': '3 secondes',
'username': 'test',

View File

@ -3,12 +3,12 @@ from fittrackee.email.utils_email import InvalidEmailUrlScheme, parse_email_url
class TestEmailUrlParser:
def test_it_raises_error_if_url_scheme_is_invalid(self):
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)
def test_it_parses_email_url(self):
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'
@ -18,7 +18,7 @@ class TestEmailUrlParser:
assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_tls(self):
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'
@ -28,7 +28,7 @@ class TestEmailUrlParser:
assert parsed_email['use_tls'] is True
assert parsed_email['use_ssl'] is False
def test_it_parses_email_url_with_ssl(self):
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'

View File

@ -1,14 +1,17 @@
import json
from datetime import datetime, timedelta
from io import BytesIO
from unittest.mock import patch
from unittest.mock import Mock, patch
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from fittrackee.users.utils_token import get_user_token
from flask import Flask
from freezegun import freeze_time
class TestUserRegistration:
def test_user_can_register(self, app):
def test_user_can_register(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -31,7 +34,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 201
def test_it_returns_error_if_user_already_exists(self, app, user_1):
def test_it_returns_error_if_user_already_exists(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
@ -51,7 +56,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_username_is_too_short(self, app):
def test_it_returns_error_if_username_is_too_short(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
@ -73,7 +80,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_username_is_too_long(self, app):
def test_it_returns_error_if_username_is_too_long(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
@ -93,7 +102,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_email_is_invalid(self, app):
def test_it_returns_error_if_email_is_invalid(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -115,7 +124,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_password_is_too_short(self, app):
def test_it_returns_error_if_password_is_too_short(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
@ -137,7 +148,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_passwords_mismatch(self, app):
def test_it_returns_error_if_passwords_mismatch(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -162,7 +173,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json'
assert response.status_code == 400
def test_it_returns_error_if_payload_is_invalid(self, app):
def test_it_returns_error_if_payload_is_invalid(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
@ -174,7 +185,7 @@ class TestUserRegistration:
assert 'Invalid payload.', data['message']
assert 'error', data['status']
def test_it_returns_error_if_username_is_missing(self, app):
def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -194,7 +205,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message']
assert 'error' in data['status']
def test_it_returns_error_if_email_is_missing(self, app):
def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -214,7 +225,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message']
assert 'error' in data['status']
def test_it_returns_error_if_password_is_missing(self, app):
def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -234,7 +245,9 @@ class TestUserRegistration:
assert 'Invalid payload.', data['message']
assert 'error', data['status']
def test_it_returns_error_if_password_confirmation_is_missing(self, app):
def test_it_returns_error_if_password_confirmation_is_missing(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
@ -250,7 +263,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message']
assert 'error' in data['status']
def test_it_returns_error_if_username_is_invalid(self, app):
def test_it_returns_error_if_username_is_invalid(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -276,7 +289,7 @@ class TestUserRegistration:
class TestUserLogin:
def test_user_can_register(self, app, user_1):
def test_user_can_register(self, app: Flask, user_1: User) -> None:
client = app.test_client()
response = client.post(
@ -292,7 +305,9 @@ class TestUserLogin:
assert data['message'] == 'Successfully logged in.'
assert data['auth_token']
def test_it_returns_error_if_user_does_not_exists(self, app):
def test_it_returns_error_if_user_does_not_exists(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
@ -307,7 +322,7 @@ class TestUserLogin:
assert data['status'] == 'error'
assert data['message'] == 'Invalid credentials.'
def test_it_returns_error_on_invalid_payload(self, app):
def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -322,7 +337,9 @@ class TestUserLogin:
assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.'
def test_it_returns_error_if_password_is_invalid(self, app, user_1):
def test_it_returns_error_if_password_is_invalid(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.post(
@ -339,7 +356,7 @@ class TestUserLogin:
class TestUserLogout:
def test_user_can_logout(self, app, user_1):
def test_user_can_logout(self, app: Flask, user_1: User) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -360,7 +377,9 @@ class TestUserLogout:
assert data['message'] == 'Successfully logged out.'
assert response.status_code == 200
def test_it_returns_error_with_expired_token(self, app, user_1):
def test_it_returns_error_with_expired_token(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
now = datetime.utcnow()
resp_login = client.post(
@ -381,7 +400,7 @@ class TestUserLogout:
assert data['message'] == 'Signature expired. Please log in again.'
assert response.status_code == 401
def test_it_returns_error_with_invalid_token(self, app):
def test_it_returns_error_with_invalid_token(self, app: Flask) -> None:
client = app.test_client()
response = client.get(
'/api/auth/logout', headers=dict(Authorization='Bearer invalid')
@ -391,7 +410,7 @@ class TestUserLogout:
assert data['message'] == 'Invalid token. Please log in again.'
assert response.status_code == 401
def test_it_returns_error_with_invalid_headers(self, app):
def test_it_returns_error_with_invalid_headers(self, app: Flask) -> None:
client = app.test_client()
response = client.get('/api/auth/logout', headers=dict())
data = json.loads(response.data.decode())
@ -401,7 +420,9 @@ class TestUserLogout:
class TestUserProfile:
def test_it_returns_user_minimal_profile(self, app, user_1):
def test_it_returns_user_minimal_profile(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -432,7 +453,9 @@ class TestUserProfile:
assert data['data']['total_duration'] == '0:00:00'
assert response.status_code == 200
def test_it_returns_user_full_profile(self, app, user_1_full):
def test_it_returns_user_full_profile(
self, app: Flask, user_1_full: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -470,13 +493,13 @@ class TestUserProfile:
def test_it_returns_user_profile_with_activities(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -505,7 +528,7 @@ class TestUserProfile:
assert data['data']['total_duration'] == '2:40:00'
assert response.status_code == 200
def test_it_returns_error_if_headers_are_invalid(self, app):
def test_it_returns_error_if_headers_are_invalid(self, app: Flask) -> None:
client = app.test_client()
response = client.get(
'/api/auth/profile', headers=dict(Authorization='Bearer invalid')
@ -517,7 +540,7 @@ class TestUserProfile:
class TestUserProfileUpdate:
def test_it_updates_user_profile(self, app, user_1):
def test_it_updates_user_profile(self, app: Flask, user_1: User) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -568,7 +591,9 @@ class TestUserProfileUpdate:
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'
def test_it_updates_user_profile_without_password(self, app, user_1):
def test_it_updates_user_profile_without_password(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -617,7 +642,9 @@ class TestUserProfileUpdate:
assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00'
def test_it_returns_error_if_fields_are_missing(self, app, user_1):
def test_it_returns_error_if_fields_are_missing(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -638,7 +665,9 @@ class TestUserProfileUpdate:
assert data['message'] == 'Invalid payload.'
assert response.status_code == 400
def test_it_returns_error_if_payload_is_empty(self, app, user_1):
def test_it_returns_error_if_payload_is_empty(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -659,7 +688,9 @@ class TestUserProfileUpdate:
assert 'Invalid payload.' in data['message']
assert 'error' in data['status']
def test_it_returns_error_if_passwords_mismatch(self, app, user_1):
def test_it_returns_error_if_passwords_mismatch(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -697,8 +728,8 @@ class TestUserProfileUpdate:
assert response.status_code == 400
def test_it_returns_error_if_password_confirmation_is_missing(
self, app, user_1
):
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -736,7 +767,7 @@ class TestUserProfileUpdate:
class TestUserPicture:
def test_it_updates_user_picture(self, app, user_1):
def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -774,7 +805,9 @@ class TestUserPicture:
assert 'avatar.png' not in user_1.picture
assert 'avatar2.png' in user_1.picture
def test_it_returns_error_if_file_is_missing(self, app, user_1):
def test_it_returns_error_if_file_is_missing(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -794,7 +827,9 @@ class TestUserPicture:
assert data['message'] == 'No file part.'
assert response.status_code == 400
def test_it_returns_error_if_file_is_invalid(self, app, user_1):
def test_it_returns_error_if_file_is_invalid(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -818,8 +853,8 @@ class TestUserPicture:
class TestRegistrationConfiguration:
def test_it_returns_error_if_it_exceeds_max_users(
self, app, user_1_admin, user_2, user_3
):
self, app: Flask, user_1_admin: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
@ -859,8 +894,12 @@ class TestRegistrationConfiguration:
assert data['message'] == 'Error. Registration is disabled.'
def test_it_disables_registration_on_user_registration(
self, app_no_config, app_config, user_1_admin, user_2
):
self,
app_no_config: Flask,
app_config: Flask,
user_1_admin: User,
user_2: User,
) -> None:
app_config.max_users = 3
client = app_no_config.test_client()
client.post(
@ -894,11 +933,11 @@ class TestRegistrationConfiguration:
def test_it_does_not_disable_registration_on_user_registration(
self,
app_no_config,
app_config,
user_1_admin,
user_2,
):
app_no_config: Flask,
app_config: Flask,
user_1_admin: User,
user_2: User,
) -> None:
app_config.max_users = 4
client = app_no_config.test_client()
client.post(
@ -932,8 +971,8 @@ class TestPasswordResetRequest:
@patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP')
def test_it_requests_password_reset_when_user_exists(
self, mock_smtp, mock_smtp_ssl, app, user_1
):
self, mock_smtp: Mock, mock_smtp_ssl: Mock, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/password/reset-request',
@ -946,7 +985,9 @@ class TestPasswordResetRequest:
assert data['status'] == 'success'
assert data['message'] == 'Password reset request processed.'
def test_it_does_not_return_error_when_user_does_not_exist(self, app):
def test_it_does_not_return_error_when_user_does_not_exist(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
@ -960,7 +1001,7 @@ class TestPasswordResetRequest:
assert data['status'] == 'success'
assert data['message'] == 'Password reset request processed.'
def test_it_returns_error_on_invalid_payload(self, app):
def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -974,7 +1015,7 @@ class TestPasswordResetRequest:
assert data['message'] == 'Invalid payload.'
assert data['status'] == 'error'
def test_it_returns_error_on_empty_payload(self, app):
def test_it_returns_error_on_empty_payload(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -990,7 +1031,7 @@ class TestPasswordResetRequest:
class TestPasswordUpdate:
def test_it_returns_error_if_payload_is_empty(self, app):
def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -1009,7 +1050,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.'
def test_it_returns_error_if_token_is_missing(self, app):
def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -1028,7 +1069,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.'
def test_it_returns_error_if_password_is_missing(self, app):
def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
client = app.test_client()
response = client.post(
@ -1047,7 +1088,9 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.'
def test_it_returns_error_if_password_confirmation_is_missing(self, app):
def test_it_returns_error_if_password_confirmation_is_missing(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
@ -1066,7 +1109,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.'
def test_it_returns_error_if_token_is_invalid(self, app):
def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
token = get_user_token(1)
client = app.test_client()
@ -1087,7 +1130,9 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Invalid token. Please request a new token.'
def test_it_returns_error_if_token_is_expired(self, app, user_1):
def test_it_returns_error_if_token_is_expired(
self, app: Flask, user_1: User
) -> None:
now = datetime.utcnow()
token = get_user_token(user_1.id, password_reset=True)
client = app.test_client()
@ -1112,7 +1157,9 @@ class TestPasswordUpdate:
data['message'] == 'Invalid token. Please request a new token.'
)
def test_it_returns_error_if_password_is_invalid(self, app, user_1):
def test_it_returns_error_if_password_is_invalid(
self, app: Flask, user_1: User
) -> None:
token = get_user_token(user_1.id, password_reset=True)
client = app.test_client()
@ -1133,7 +1180,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error'
assert data['message'] == 'Password: 8 characters required.\n'
def test_it_update_password(self, app, user_1):
def test_it_update_password(self, app: Flask, user_1: User) -> None:
token = get_user_token(user_1.id, password_reset=True)
client = app.test_client()

View File

@ -3,9 +3,15 @@ from datetime import datetime, timedelta
from io import BytesIO
from unittest.mock import patch
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetUser:
def test_it_gets_single_user_without_activities(self, app, user_1, user_2):
def test_it_gets_single_user_without_activities(
self, app: Flask, user_1: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -47,13 +53,13 @@ class TestGetUser:
def test_it_gets_single_user_with_activities(
self,
app,
user_1,
sport_1_cycling,
sport_2_running,
activity_cycling_user_1,
activity_running_user_1,
):
app: Flask,
user_1: User,
sport_1_cycling: Sport,
sport_2_running: Sport,
activity_cycling_user_1: Activity,
activity_running_user_1: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -93,7 +99,9 @@ class TestGetUser:
assert user['total_distance'] == 22
assert user['total_duration'] == '2:40:00'
def test_it_returns_error_if_user_does_not_exist(self, app, user_1):
def test_it_returns_error_if_user_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -116,7 +124,9 @@ class TestGetUser:
class TestGetUsers:
def test_it_get_users_list(self, app, user_1, user_2, user_3):
def test_it_get_users_list(
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -179,16 +189,16 @@ class TestGetUsers:
def test_it_gets_users_list_with_activities(
self,
app,
user_1,
user_2,
user_3,
sport_1_cycling,
activity_cycling_user_1,
sport_2_running,
activity_running_user_1,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_1: Activity,
sport_2_running: Sport,
activity_running_user_1: Activity,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -249,11 +259,11 @@ class TestGetUsers:
@patch('fittrackee.users.users.USER_PER_PAGE', 2)
def test_it_gets_first_page_on_users_list(
self,
app,
user_1,
user_2,
user_3,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -284,11 +294,11 @@ class TestGetUsers:
@patch('fittrackee.users.users.USER_PER_PAGE', 2)
def test_it_gets_next_page_on_users_list(
self,
app,
user_1,
user_2,
user_3,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -318,11 +328,11 @@ class TestGetUsers:
def test_it_gets_empty_next_page_on_users_list(
self,
app,
user_1,
user_2,
user_3,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -352,11 +362,11 @@ class TestGetUsers:
def test_it_gets_user_list_with_2_per_page(
self,
app,
user_1,
user_2,
user_3,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -386,11 +396,11 @@ class TestGetUsers:
def test_it_gets_next_page_on_user_list_with_2_per_page(
self,
app,
user_1,
user_2,
user_3,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -419,8 +429,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_username(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -451,8 +461,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_username_ascending(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -484,8 +494,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_username_descending(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -517,8 +527,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_creation_date(
self, app, user_2, user_3, user_1_admin
):
self, app: Flask, user_2: User, user_3: User, user_1_admin: User
) -> None:
user_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow()
@ -555,8 +565,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_creation_date_ascending(
self, app, user_2, user_3, user_1_admin
):
self, app: Flask, user_2: User, user_3: User, user_1_admin: User
) -> None:
user_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow()
@ -593,8 +603,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_creation_date_descending(
self, app, user_2, user_3, user_1_admin
):
self, app: Flask, user_2: User, user_3: User, user_1_admin: User
) -> None:
user_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow()
@ -631,8 +641,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_admin_rights(
self, app, user_2, user_1_admin, user_3
):
self, app: Flask, user_2: User, user_1_admin: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -666,8 +676,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_admin_rights_ascending(
self, app, user_2, user_1_admin, user_3
):
self, app: Flask, user_2: User, user_1_admin: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -701,8 +711,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_ordered_by_admin_rights_descending(
self, app, user_2, user_3, user_1_admin
):
self, app: Flask, user_2: User, user_3: User, user_1_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -737,13 +747,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count(
self,
app,
user_1,
user_2,
user_3,
sport_1_cycling,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -779,13 +789,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count_ascending(
self,
app,
user_1,
user_2,
user_3,
sport_1_cycling,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -821,13 +831,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count_descending(
self,
app,
user_1,
user_2,
user_3,
sport_1_cycling,
activity_cycling_user_2,
):
app: Flask,
user_1: User,
user_2: User,
user_3: User,
sport_1_cycling: Sport,
activity_cycling_user_2: Activity,
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -862,8 +872,8 @@ class TestGetUsers:
}
def test_it_gets_users_list_filtering_on_username(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -893,8 +903,8 @@ class TestGetUsers:
}
def test_it_returns_empty_users_list_filtering_on_username(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -923,8 +933,8 @@ class TestGetUsers:
}
def test_it_users_list_with_complex_query(
self, app, user_1, user_2, user_3
):
self, app: Flask, user_1: User, user_2: User, user_3: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -955,7 +965,9 @@ class TestGetUsers:
class TestGetUserPicture:
def test_it_return_error_if_user_has_no_picture(self, app, user_1):
def test_it_return_error_if_user_has_no_picture(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.get(f'/api/users/{user_1.username}/picture')
@ -965,7 +977,9 @@ class TestGetUserPicture:
assert 'not found' in data['status']
assert 'No picture.' in data['message']
def test_it_returns_error_if_user_does_not_exist(self, app, user_1):
def test_it_returns_error_if_user_does_not_exist(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.get('/api/users/not_existing/picture')
@ -977,7 +991,9 @@ class TestGetUserPicture:
class TestUpdateUser:
def test_it_adds_admin_rights_to_a_user(self, app, user_1_admin, user_2):
def test_it_adds_admin_rights_to_a_user(
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1006,8 +1022,8 @@ class TestUpdateUser:
assert user['admin'] is True
def test_it_removes_admin_rights_to_a_user(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1037,8 +1053,8 @@ class TestUpdateUser:
assert user['admin'] is False
def test_it_returns_error_if_payload_for_admin_rights_is_empty(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1064,8 +1080,8 @@ class TestUpdateUser:
assert 'Invalid payload.' in data['message']
def test_it_returns_error_if_payload_for_admin_rights_is_invalid(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1094,8 +1110,8 @@ class TestUpdateUser:
)
def test_it_returns_error_if_user_can_not_change_admin_rights(
self, app, user_1, user_2
):
self, app: Flask, user_1: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1120,7 +1136,9 @@ class TestUpdateUser:
class TestDeleteUser:
def test_user_can_delete_its_own_account(self, app, user_1):
def test_user_can_delete_its_own_account(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1139,8 +1157,8 @@ class TestDeleteUser:
assert response.status_code == 204
def test_user_with_activity_can_delete_its_own_account(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1171,8 +1189,8 @@ class TestDeleteUser:
assert response.status_code == 204
def test_user_with_picture_can_delete_its_own_account(
self, app, user_1, sport_1_cycling, gpx_file
):
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1200,8 +1218,8 @@ class TestDeleteUser:
assert response.status_code == 204
def test_user_can_not_delete_another_user_account(
self, app, user_1, user_2
):
self, app: Flask, user_1: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1223,8 +1241,8 @@ class TestDeleteUser:
assert 'You do not have permissions.' in data['message']
def test_it_returns_error_when_deleting_non_existing_user(
self, app, user_1
):
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1246,8 +1264,8 @@ class TestDeleteUser:
assert 'User does not exist.' in data['message']
def test_admin_can_delete_another_user_account(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1268,8 +1286,8 @@ class TestDeleteUser:
assert response.status_code == 204
def test_admin_can_delete_its_own_account(
self, app, user_1_admin, user_2_admin
):
self, app: Flask, user_1_admin: User, user_2_admin: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1290,8 +1308,8 @@ class TestDeleteUser:
assert response.status_code == 204
def test_admin_can_not_delete_its_own_account_if_no_other_admin(
self, app, user_1_admin, user_2
):
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
client = app.test_client()
resp_login = client.post(
'/api/auth/login',
@ -1317,8 +1335,13 @@ class TestDeleteUser:
)
def test_it_enables_registration_on_user_delete(
self, app_no_config, app_config, user_1_admin, user_2, user_3
):
self,
app_no_config: Flask,
app_config: Flask,
user_1_admin: User,
user_2: User,
user_3: User,
) -> None:
app_config.max_users = 3
client = app_no_config.test_client()
resp_login = client.post(
@ -1351,8 +1374,13 @@ class TestDeleteUser:
assert response.status_code == 201
def test_it_does_not_enable_registration_on_user_delete(
self, app_no_config, app_config, user_1_admin, user_2, user_3
):
self,
app_no_config: Flask,
app_config: Flask,
user_1_admin: User,
user_2: User,
user_3: User,
) -> None:
app_config.max_users = 2
client = app_no_config.test_client()
resp_login = client.post(

View File

@ -1,8 +1,9 @@
from fittrackee.users.models import User
from flask import Flask
class TestUserModel:
def test_user_model(self, app, user_1):
def test_user_model(self, app: Flask, user_1: User) -> None:
assert '<User \'test\'>' == str(user_1)
serialized_user = user_1.serialize()
@ -23,15 +24,15 @@ class TestUserModel:
assert serialized_user['total_distance'] == 0
assert serialized_user['total_duration'] == '0:00:00'
def test_encode_auth_token(self, app, user_1):
def test_encode_auth_token(self, app: Flask, user_1: User) -> None:
auth_token = user_1.encode_auth_token(user_1.id)
assert isinstance(auth_token, str)
def test_encode_password_token(self, app, user_1):
def test_encode_password_token(self, app: Flask, user_1: User) -> None:
password_token = user_1.encode_password_reset_token(user_1.id)
assert isinstance(password_token, str)
def test_decode_auth_token(self, app, user_1):
def test_decode_auth_token(self, app: Flask, user_1: User) -> None:
auth_token = user_1.encode_auth_token(user_1.id)
assert isinstance(auth_token, str)
assert User.decode_auth_token(auth_token) == user_1.id

View File

@ -1,10 +1,12 @@
import datetime
import os
from typing import Dict, Tuple, Union
import jwt
from fittrackee import appLog, bcrypt, db
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse,
PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse,
@ -32,7 +34,7 @@ auth_blueprint = Blueprint('auth', __name__)
@auth_blueprint.route('/auth/register', methods=['POST'])
def register_user():
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
"""
register a user
@ -144,7 +146,7 @@ def register_user():
@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
def login_user() -> Union[Dict, HttpResponse]:
"""
user login
@ -216,7 +218,7 @@ def login_user():
@auth_blueprint.route('/auth/logout', methods=['GET'])
@authenticate
def logout_user(auth_user_id):
def logout_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
"""
user logout
@ -277,7 +279,9 @@ def logout_user(auth_user_id):
@auth_blueprint.route('/auth/profile', methods=['GET'])
@authenticate
def get_authenticated_user_profile(auth_user_id):
def get_authenticated_user_profile(
auth_user_id: int,
) -> Union[Dict, HttpResponse]:
"""
get authenticated user info
@ -338,7 +342,7 @@ def get_authenticated_user_profile(auth_user_id):
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
@authenticate
def edit_user(auth_user_id):
def edit_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
"""
edit authenticated user
@ -474,7 +478,7 @@ def edit_user(auth_user_id):
@auth_blueprint.route('/auth/picture', methods=['POST'])
@authenticate
def edit_picture(auth_user_id):
def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
"""
update authenticated user picture
@ -561,7 +565,7 @@ def edit_picture(auth_user_id):
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
@authenticate
def del_picture(auth_user_id):
def del_picture(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
"""
delete authenticated user picture
@ -604,7 +608,7 @@ def del_picture(auth_user_id):
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
def request_password_reset():
def request_password_reset() -> Union[Dict, HttpResponse]:
"""
handle password reset request
@ -644,15 +648,15 @@ def request_password_reset():
ui_url = current_app.config['UI_URL']
email_data = {
'expiration_delay': get_readable_duration(
current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS'),
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
'en' if user.language is None else user.language,
),
'username': user.username,
'password_reset_url': (
f'{ui_url}/password-reset?token={password_reset_token}' # noqa
),
'operating_system': request.user_agent.platform,
'browser_name': request.user_agent.browser,
'operating_system': request.user_agent.platform, # type: ignore
'browser_name': request.user_agent.browser, # type: ignore
}
user_data = {
'language': user.language if user.language else 'en',
@ -666,7 +670,7 @@ def request_password_reset():
@auth_blueprint.route('/auth/password/update', methods=['POST'])
def update_password():
def update_password() -> Union[Dict, HttpResponse]:
"""
update user password

View File

@ -1,18 +1,22 @@
from datetime import datetime
from typing import Dict, Optional, Union
import jwt
from fittrackee import bcrypt, db
from flask import current_app
from sqlalchemy import func
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import select
from ..activities.models import Activity
from .utils_token import decode_user_token, get_user_token
BaseModel: DeclarativeMeta = db.Model
class User(db.Model):
__tablename__ = "users"
class User(BaseModel):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
@ -36,12 +40,16 @@ class User(db.Model):
)
language = db.Column(db.String(50), nullable=True)
def __repr__(self):
def __repr__(self) -> str:
return f'<User {self.username!r}>'
def __init__(
self, username, email, password, created_at=datetime.utcnow()
):
self,
username: str,
email: str,
password: str,
created_at: Optional[datetime] = datetime.utcnow(),
) -> None:
self.username = username
self.email = email
self.password = bcrypt.generate_password_hash(
@ -50,31 +58,25 @@ class User(db.Model):
self.created_at = created_at
@staticmethod
def encode_auth_token(user_id):
def encode_auth_token(user_id: int) -> str:
"""
Generates the auth token
:param user_id: -
:return: JWToken
"""
try:
return get_user_token(user_id)
except Exception as e:
return e
@staticmethod
def encode_password_reset_token(user_id):
def encode_password_reset_token(user_id: int) -> str:
"""
Generates the auth token
:param user_id: -
:return: JWToken
"""
try:
return get_user_token(user_id, password_reset=True)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
def decode_auth_token(auth_token: str) -> Union[int, str]:
"""
Decodes the auth token
:param auth_token: -
@ -88,21 +90,21 @@ class User(db.Model):
return 'Invalid token. Please log in again.'
@hybrid_property
def activities_count(self):
def activities_count(self) -> int:
return Activity.query.filter(Activity.user_id == self.id).count()
@activities_count.expression
def activities_count(self):
@activities_count.expression # type: ignore
def activities_count(self) -> int:
return (
select([func.count(Activity.id)])
.where(Activity.user_id == self.id)
.label("activities_count")
.label('activities_count')
)
def serialize(self):
def serialize(self) -> Dict:
sports = []
total = (None, None)
if self.activities_count > 0:
total = (0, '0:00:00')
if self.activities_count > 0: # type: ignore
sports = (
db.session.query(Activity.sport_id)
.filter(Activity.user_id == self.id)
@ -136,6 +138,6 @@ class User(db.Model):
'sports_list': [
sport for sportslist in sports for sport in sportslist
],
'total_distance': float(total[0]) if total[0] else 0,
'total_duration': str(total[1]) if total[1] else "0:00:00",
'total_distance': float(total[0]),
'total_duration': str(total[1]),
}

View File

@ -1,9 +1,11 @@
import os
import shutil
from typing import Any, Dict, Tuple, Union
from fittrackee import db
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
UserNotFoundErrorResponse,
@ -23,7 +25,7 @@ USER_PER_PAGE = 10
@users_blueprint.route('/users', methods=['GET'])
@authenticate
def get_users(auth_user_id):
def get_users(auth_user_id: int) -> Dict:
"""
Get all users
@ -135,10 +137,10 @@ def get_users(auth_user_id):
User.username.like('%' + query + '%') if query else True,
)
.order_by(
User.activities_count.asc()
User.activities_count.asc() # type: ignore
if order_by == 'activities_count' and order == 'asc'
else True,
User.activities_count.desc()
User.activities_count.desc() # type: ignore
if order_by == 'activities_count' and order == 'desc'
else True,
User.username.asc()
@ -178,7 +180,9 @@ def get_users(auth_user_id):
@users_blueprint.route('/users/<user_name>', methods=['GET'])
@authenticate
def get_single_user(auth_user_id, user_name):
def get_single_user(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get single user details
@ -251,7 +255,7 @@ def get_single_user(auth_user_id, user_name):
@users_blueprint.route('/users/<user_name>/picture', methods=['GET'])
def get_picture(user_name):
def get_picture(user_name: str) -> Any:
"""get user picture
**Example request**:
@ -290,7 +294,9 @@ def get_picture(user_name):
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
@authenticate_as_admin
def update_user(auth_user_id, user_name):
def update_user(
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Update user to add admin rights
@ -377,7 +383,9 @@ def update_user(auth_user_id, user_name):
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
@authenticate
def delete_user(auth_user_id, user_name):
def delete_user(
auth_user_id: int, user_name: str
) -> Union[Tuple[Dict, int], HttpResponse]:
"""
Delete a user account

View File

@ -1,30 +1,44 @@
import re
from datetime import timedelta
from functools import wraps
from typing import Any, Callable, Optional, Tuple, Union
import humanize
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse,
PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse,
)
from flask import current_app, request
from flask import Request, current_app, request
from .models import User
def is_admin(user_id):
def is_admin(user_id: int) -> bool:
"""
Return if user has admin rights
"""
user = User.query.filter_by(id=user_id).first()
return user.admin
def is_valid_email(email):
def is_valid_email(email: str) -> bool:
"""
Return if email format is valid
"""
mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
return re.match(mail_pattern, email) is not None
def check_passwords(password, password_conf):
def check_passwords(password: str, password_conf: str) -> str:
"""
Verify if password and password confirmation are the same and have
more than 8 characters
If not, it returns not empty string
"""
ret = ''
if password_conf != password:
ret = 'Password and password confirmation don\'t match.\n'
@ -33,7 +47,14 @@ def check_passwords(password, password_conf):
return ret
def register_controls(username, email, password, password_conf):
def register_controls(
username: str, email: str, password: str, password_conf: str
) -> str:
"""
Verify if user name, email and passwords are valid
If not, it returns not empty string
"""
ret = ''
if not 2 < len(username) < 13:
ret += 'Username: 3 to 12 characters required.\n'
@ -43,7 +64,12 @@ def register_controls(username, email, password, password_conf):
return ret
def verify_extension_and_size(file_type, req):
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')
@ -66,7 +92,7 @@ def verify_extension_and_size(file_type, req):
if not (
file_extension
and file_extension in current_app.config.get(allowed_extensions)
and file_extension in current_app.config[allowed_extensions]
):
return InvalidPayloadErrorResponse(
'File extension not allowed.', 'fail'
@ -81,7 +107,13 @@ def verify_extension_and_size(file_type, req):
return None
def verify_user(current_request, verify_admin):
def verify_user(
current_request: Request, verify_admin: bool
) -> Tuple[Optional[HttpResponse], Optional[int]]:
"""
Return user id, if the provided token is valid and if user has admin
rights if 'verify_admin' is True
"""
default_message = 'Provide a valid auth token.'
auth_header = current_request.headers.get('Authorization')
if not auth_header:
@ -98,9 +130,11 @@ def verify_user(current_request, verify_admin):
return None, resp
def authenticate(f):
def authenticate(f: Callable) -> Callable:
@wraps(f)
def decorated_function(*args, **kwargs):
def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = False
response_object, resp = verify_user(request, verify_admin)
if response_object:
@ -110,9 +144,11 @@ def authenticate(f):
return decorated_function
def authenticate_as_admin(f):
def authenticate_as_admin(f: Callable) -> Callable:
@wraps(f)
def decorated_function(*args, **kwargs):
def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = True
response_object, resp = verify_user(request, verify_admin)
if response_object:
@ -122,25 +158,36 @@ def authenticate_as_admin(f):
return decorated_function
def can_view_activity(auth_user_id, activity_user_id):
def can_view_activity(
auth_user_id: int, activity_user_id: int
) -> Optional[HttpResponse]:
"""
Return error response if user has no right to view activity
"""
if auth_user_id != activity_user_id:
return ForbiddenErrorResponse()
return None
def display_readable_file_size(size_in_bytes):
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}"
return f'{size_in_bytes:3.1f}{unit}'
size_in_bytes /= 1024.0
return f"{size_in_bytes} bytes"
return f'{size_in_bytes} bytes'
def get_readable_duration(duration, locale='en'):
def get_readable_duration(duration: int, locale: Optional[str] = 'en') -> str:
"""
Return readable and localized duration from duration in seconds
"""
if locale is not None and locale != 'en':
_t = humanize.i18n.activate(locale) # noqa
readable_duration = humanize.naturaldelta(timedelta(seconds=duration))

View File

@ -1,19 +1,25 @@
from datetime import datetime, timedelta
from typing import Optional
import jwt
from flask import current_app
def get_user_token(user_id, password_reset=False):
expiration_days = (
0
if password_reset
else current_app.config.get('TOKEN_EXPIRATION_DAYS')
def get_user_token(
user_id: int, password_reset: Optional[bool] = False
) -> str:
"""
Return authentication token for a given user.
Token expiration time depends on token type (authentication or password
reset)
"""
expiration_days: float = (
0.0 if password_reset else current_app.config['TOKEN_EXPIRATION_DAYS']
)
expiration_seconds = (
current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS')
expiration_seconds: float = (
current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS']
if password_reset
else current_app.config.get('TOKEN_EXPIRATION_SECONDS')
else current_app.config['TOKEN_EXPIRATION_SECONDS']
)
payload = {
'exp': datetime.utcnow()
@ -23,15 +29,18 @@ def get_user_token(user_id, password_reset=False):
}
return jwt.encode(
payload,
current_app.config.get('SECRET_KEY'),
current_app.config['SECRET_KEY'],
algorithm='HS256',
)
def decode_user_token(auth_token):
def decode_user_token(auth_token: str) -> int:
"""
Return user id from token
"""
payload = jwt.decode(
auth_token,
current_app.config.get('SECRET_KEY'),
current_app.config['SECRET_KEY'],
algorithms=['HS256'],
)
return payload['sub']

99
poetry.lock generated
View File

@ -396,7 +396,7 @@ python-versions = "*"
[[package]]
name = "isort"
version = "5.6.4"
version = "5.7.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
@ -460,6 +460,22 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "mypy"
version = "0.790"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
dmypy = ["psutil (>=4.0)"]
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
typed-ast = ">=1.4.0,<1.5.0"
typing-extensions = ">=3.7.4"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
@ -1095,7 +1111,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tqdm"
version = "4.55.0"
version = "4.55.1"
description = "Fast, Extensible Progress Meter"
category = "main"
optional = false
@ -1107,7 +1123,7 @@ telegram = ["requests"]
[[package]]
name = "typed-ast"
version = "1.4.1"
version = "1.4.2"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
@ -1162,7 +1178,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt
[metadata]
lock-version = "1.0"
python-versions = "^3.7"
content-hash = "b5c7cbb6e449c8c6cc94a7413ae2fc0b8889d738e3fc802566efc45cc5096287"
content-hash = "11c3a22ee800d71d1d9804e33e756be81c5b7c56bba85d7de03a0f27acb35148"
[metadata.files]
alabaster = [
@ -1388,8 +1404,8 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"},
{file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"},
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
@ -1442,6 +1458,22 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mypy = [
{file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
{file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
{file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
{file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
{file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
{file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
{file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
{file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
{file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
{file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
{file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
{file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
{file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
{file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
@ -1781,31 +1813,40 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tqdm = [
{file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"},
{file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"},
{file = "tqdm-4.55.1-py2.py3-none-any.whl", hash = "sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4"},
{file = "tqdm-4.55.1.tar.gz", hash = "sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},

View File

@ -42,6 +42,7 @@ tqdm = "^4.55"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
freezegun = "^1.0.0"
mypy = "^0.790"
pyopenssl = "^20.0"
pytest = "^6.2"
pytest-black = "^0.3.12"