diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 61b46cb4..c327a1a7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/Makefile b/Makefile index ff4ff8ea..58471ea3 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/Makefile.config b/Makefile.config index ae3ecb9d..a14dfff2 100644 --- a/Makefile.config +++ b/Makefile.config @@ -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 diff --git a/fittrackee/__init__.py b/fittrackee/__init__.py index a1401827..fe77fbb9 100644 --- a/fittrackee/__init__.py +++ b/fittrackee/__init__.py @@ -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('/') - 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') diff --git a/fittrackee/__main__.py b/fittrackee/__main__.py index 30edc86e..c00aea1a 100644 --- a/fittrackee/__main__.py +++ b/fittrackee/__main__.py @@ -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() diff --git a/fittrackee/activities/activities.py b/fittrackee/activities/activities.py index f80953f7..6b9378a2 100644 --- a/fittrackee/activities/activities.py +++ b/fittrackee/activities/activities.py @@ -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/', 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//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//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/', 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////.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/', 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/', 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 diff --git a/fittrackee/activities/models.py b/fittrackee/activities/models.py index f66fbc44..4698557d 100644 --- a/fittrackee/activities/models.py +++ b/fittrackee/activities/models.py @@ -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'' - 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'' - 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'' ) - 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'" ) - 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) diff --git a/fittrackee/activities/records.py b/fittrackee/activities/records.py index c25320b5..eb1d1885 100644 --- a/fittrackee/activities/records.py +++ b/fittrackee/activities/records.py @@ -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. diff --git a/fittrackee/activities/sports.py b/fittrackee/activities/sports.py index c85ea7ed..f21613aa 100644 --- a/fittrackee/activities/sports.py +++ b/fittrackee/activities/sports.py @@ -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/', 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/', 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 diff --git a/fittrackee/activities/stats.py b/fittrackee/activities/stats.py index 4e8087fc..c0bf7574 100644 --- a/fittrackee/activities/stats.py +++ b/fittrackee/activities/stats.py @@ -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//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//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 diff --git a/fittrackee/activities/utils.py b/fittrackee/activities/utils.py index d6094b28..31cd01bb 100644 --- a/fittrackee/activities/utils.py +++ b/fittrackee/activities/utils.py @@ -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): diff --git a/fittrackee/activities/utils_files.py b/fittrackee/activities/utils_files.py index e2d176c5..64bb6c82 100644 --- a/fittrackee/activities/utils_files.py +++ b/fittrackee/activities/utils_files.py @@ -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) diff --git a/fittrackee/activities/utils_format.py b/fittrackee/activities/utils_format.py index acf7bb77..c1d93663 100644 --- a/fittrackee/activities/utils_format.py +++ b/fittrackee/activities/utils_format.py @@ -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' diff --git a/fittrackee/activities/utils_gpx.py b/fittrackee/activities/utils_gpx.py index db54fcfa..1fd781b6 100644 --- a/fittrackee/activities/utils_gpx.py +++ b/fittrackee/activities/utils_gpx.py @@ -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 diff --git a/fittrackee/activities/utils_id.py b/fittrackee/activities/utils_id.py index 5ea88bc7..bf81bab4 100644 --- a/fittrackee/activities/utils_id.py +++ b/fittrackee/activities/utils_id.py @@ -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) diff --git a/fittrackee/activities/utils_weather.py b/fittrackee/activities/utils_weather.py index cedb6d78..3f8c18dd 100644 --- a/fittrackee/activities/utils_weather.py +++ b/fittrackee/activities/utils_weather.py @@ -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: diff --git a/fittrackee/application/app_config.py b/fittrackee/application/app_config.py index 3cc97bce..8e41ede0 100644 --- a/fittrackee/application/app_config.py +++ b/fittrackee/application/app_config.py @@ -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**: diff --git a/fittrackee/application/models.py b/fittrackee/application/models.py index 77bb7a1b..11274ea6 100644 --- a/fittrackee/application/models.py +++ b/fittrackee/application/models.py @@ -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() diff --git a/fittrackee/application/utils.py b/fittrackee/application/utils.py index a50e6813..5c0401d3 100644 --- a/fittrackee/application/utils.py +++ b/fittrackee/application/utils.py @@ -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 diff --git a/fittrackee/database_utils.py b/fittrackee/database_utils.py index c58e377a..1c9094fe 100644 --- a/fittrackee/database_utils.py +++ b/fittrackee/database_utils.py @@ -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' diff --git a/fittrackee/email/email.py b/fittrackee/email/email.py index 271c7ca2..4e1df723 100644 --- a/fittrackee/email/email.py +++ b/fittrackee/email/email.py @@ -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()) diff --git a/fittrackee/email/utils_email.py b/fittrackee/email/utils_email.py index 57f8a5f7..1daa2ab3 100644 --- a/fittrackee/email/utils_email.py +++ b/fittrackee/email/utils_email.py @@ -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() diff --git a/fittrackee/responses.py b/fittrackee/responses.py index 9ff343b1..fef4161a 100644 --- a/fittrackee/responses.py +++ b/fittrackee/responses.py @@ -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) diff --git a/fittrackee/tasks.py b/fittrackee/tasks.py index e44d22f1..75c285a2 100644 --- a/fittrackee/tasks.py +++ b/fittrackee/tasks.py @@ -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'], diff --git a/fittrackee/tests/activities/test_activities_api_0_get.py b/fittrackee/tests/activities/test_activities_api_0_get.py index f327e09a..683b09d1 100644 --- a/fittrackee/tests/activities/test_activities_api_0_get.py +++ b/fittrackee/tests/activities/test_activities_api_0_get.py @@ -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', diff --git a/fittrackee/tests/activities/test_activities_api_1_post.py b/fittrackee/tests/activities/test_activities_api_1_post.py index b492c6bc..03a1a32f 100644 --- a/fittrackee/tests/activities/test_activities_api_1_post.py +++ b/fittrackee/tests/activities/test_activities_api_1_post.py @@ -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', diff --git a/fittrackee/tests/activities/test_activities_api_2_patch.py b/fittrackee/tests/activities/test_activities_api_2_patch.py index f1ffb81c..80c0742f 100644 --- a/fittrackee/tests/activities/test_activities_api_2_patch.py +++ b/fittrackee/tests/activities/test_activities_api_2_patch.py @@ -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() diff --git a/fittrackee/tests/activities/test_activities_api_3_delete.py b/fittrackee/tests/activities/test_activities_api_3_delete.py index 644def9c..5be0b5b5 100644 --- a/fittrackee/tests/activities/test_activities_api_3_delete.py +++ b/fittrackee/tests/activities/test_activities_api_3_delete.py @@ -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', diff --git a/fittrackee/tests/activities/test_activities_model.py b/fittrackee/tests/activities/test_activities_model.py index 8a85429f..2b4e2a62 100644 --- a/fittrackee/tests/activities/test_activities_model.py +++ b/fittrackee/tests/activities/test_activities_model.py @@ -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'' diff --git a/fittrackee/tests/activities/test_records_api.py b/fittrackee/tests/activities/test_records_api.py index 9b55859d..5a20fcae 100644 --- a/fittrackee/tests/activities/test_records_api.py +++ b/fittrackee/tests/activities/test_records_api.py @@ -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', diff --git a/fittrackee/tests/activities/test_records_model.py b/fittrackee/tests/activities/test_records_model.py index 1d8ac943..ef23d926 100644 --- a/fittrackee/tests/activities/test_records_model.py +++ b/fittrackee/tests/activities/test_records_model.py @@ -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, diff --git a/fittrackee/tests/activities/test_sports_api.py b/fittrackee/tests/activities/test_sports_api.py index f4bddf79..5eda476d 100644 --- a/fittrackee/tests/activities/test_sports_api.py +++ b/fittrackee/tests/activities/test_sports_api.py @@ -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', diff --git a/fittrackee/tests/activities/test_sports_model.py b/fittrackee/tests/activities/test_sports_model.py index c204a1e9..cde8db39 100644 --- a/fittrackee/tests/activities/test_sports_model.py +++ b/fittrackee/tests/activities/test_sports_model.py @@ -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 '' == 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 diff --git a/fittrackee/tests/activities/test_stats_api.py b/fittrackee/tests/activities/test_stats_api.py index 02fe52a6..4ca7ef65 100644 --- a/fittrackee/tests/activities/test_stats_api.py +++ b/fittrackee/tests/activities/test_stats_api.py @@ -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', diff --git a/fittrackee/tests/activities/utils.py b/fittrackee/tests/activities/utils.py index 906d2b46..70d8a15e 100644 --- a/fittrackee/tests/activities/utils.py +++ b/fittrackee/tests/activities/utils.py @@ -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', diff --git a/fittrackee/tests/application/test_app_config_api.py b/fittrackee/tests/application/test_app_config_api.py index d6f0ba2b..f09afa6f 100644 --- a/fittrackee/tests/application/test_app_config_api.py +++ b/fittrackee/tests/application/test_app_config_api.py @@ -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', diff --git a/fittrackee/tests/application/test_app_config_model.py b/fittrackee/tests/application/test_app_config_model.py index f58b7c77..a4537543 100644 --- a/fittrackee/tests/application/test_app_config_model.py +++ b/fittrackee/tests/application/test_app_config_model.py @@ -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 diff --git a/fittrackee/tests/application/test_config.py b/fittrackee/tests/application/test_config.py index 8503395c..6a6b1bb4 100644 --- a/fittrackee/tests/application/test_config.py +++ b/fittrackee/tests/application/test_config.py @@ -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'] diff --git a/fittrackee/tests/application/test_health_check_api.py b/fittrackee/tests/application/test_health_check_api.py index 8ab16aab..71dfb992 100644 --- a/fittrackee/tests/application/test_health_check_api.py +++ b/fittrackee/tests/application/test_health_check_api.py @@ -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') diff --git a/fittrackee/tests/conftest.py b/fittrackee/tests/conftest.py index cbf81c8f..139c985d 100644 --- a/fittrackee/tests/conftest.py +++ b/fittrackee/tests/conftest.py @@ -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 ( '' '' # noqa @@ -438,7 +441,7 @@ def gpx_file(): @pytest.fixture() -def gpx_file_wo_name(): +def gpx_file_wo_name() -> str: return ( '' '' # noqa @@ -552,7 +555,7 @@ def gpx_file_wo_name(): @pytest.fixture() -def gpx_file_wo_track(): +def gpx_file_wo_track() -> str: return ( '' '' # noqa @@ -562,7 +565,7 @@ def gpx_file_wo_track(): @pytest.fixture() -def gpx_file_invalid_xml(): +def gpx_file_invalid_xml() -> str: return ( '' '' # noqa @@ -571,7 +574,7 @@ def gpx_file_invalid_xml(): @pytest.fixture() -def gpx_file_with_segments(): +def gpx_file_with_segments() -> str: return ( '' '' # noqa diff --git a/fittrackee/tests/email/test_email.py b/fittrackee/tests/email/test_email.py index e451b55f..c2ed442e 100644 --- a/fittrackee/tests/email/test_email.py +++ b/fittrackee/tests/email/test_email.py @@ -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', diff --git a/fittrackee/tests/email/test_email_template_password_request.py b/fittrackee/tests/email/test_email_template_password_request.py index 597dc6b9..ed1f2d4b 100644 --- a/fittrackee/tests/email/test_email_template_password_request.py +++ b/fittrackee/tests/email/test_email_template_password_request.py @@ -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', diff --git a/fittrackee/tests/email/test_email_utils.py b/fittrackee/tests/email/test_email_utils.py index d6975879..e6c625ed 100644 --- a/fittrackee/tests/email/test_email_utils.py +++ b/fittrackee/tests/email/test_email_utils.py @@ -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' diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index 03a79935..c30cb9f0 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -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() diff --git a/fittrackee/tests/users/test_users_api.py b/fittrackee/tests/users/test_users_api.py index d06e1411..738f5dbb 100644 --- a/fittrackee/tests/users/test_users_api.py +++ b/fittrackee/tests/users/test_users_api.py @@ -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( diff --git a/fittrackee/tests/users/test_users_model.py b/fittrackee/tests/users/test_users_model.py index 7d48b847..c849d53c 100644 --- a/fittrackee/tests/users/test_users_model.py +++ b/fittrackee/tests/users/test_users_model.py @@ -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 '' == 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 diff --git a/fittrackee/users/auth.py b/fittrackee/users/auth.py index 9745b672..a97bb34b 100644 --- a/fittrackee/users/auth.py +++ b/fittrackee/users/auth.py @@ -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 diff --git a/fittrackee/users/models.py b/fittrackee/users/models.py index b40c5db3..2c18850e 100644 --- a/fittrackee/users/models.py +++ b/fittrackee/users/models.py @@ -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'' 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 + return get_user_token(user_id) @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 + return get_user_token(user_id, password_reset=True) @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]), } diff --git a/fittrackee/users/users.py b/fittrackee/users/users.py index 321c5722..bf21b41b 100644 --- a/fittrackee/users/users.py +++ b/fittrackee/users/users.py @@ -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/', 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//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/', 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/', 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 diff --git a/fittrackee/users/utils.py b/fittrackee/users/utils.py index 492e1ac8..eb57d38c 100644 --- a/fittrackee/users/utils.py +++ b/fittrackee/users/utils.py @@ -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)) diff --git a/fittrackee/users/utils_token.py b/fittrackee/users/utils_token.py index d9b61bd5..33ac1f41 100644 --- a/fittrackee/users/utils_token.py +++ b/fittrackee/users/utils_token.py @@ -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'] diff --git a/poetry.lock b/poetry.lock index 6d839168..61621984 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index b6ac3164..d4b66ce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"