From 4e638f37bd33f83194ffebbc32884d09065feea9 Mon Sep 17 00:00:00 2001 From: Etienne Boileau Date: Wed, 10 Apr 2024 17:12:11 +0200 Subject: [PATCH] WIP revisit docstring, types --- server/src/scimodom/api/public.py | 16 ++---- server/src/scimodom/api/user.py | 4 +- server/src/scimodom/database/queries.py | 5 +- server/src/scimodom/services/mail.py | 39 ++++++++++++-- server/src/scimodom/services/public.py | 6 +++ server/src/scimodom/services/user.py | 70 ++++++++++++++++++------- 6 files changed, 100 insertions(+), 40 deletions(-) diff --git a/server/src/scimodom/api/public.py b/server/src/scimodom/api/public.py index 414d7f83..fc4ad078 100644 --- a/server/src/scimodom/api/public.py +++ b/server/src/scimodom/api/public.py @@ -12,8 +12,6 @@ @api.route("/selection", methods=["GET"]) @cross_origin(supports_credentials=True) def get_selection(): - """Get available selection = - (modification, organism, technology).""" public_service = get_public_service() return public_service.get_selection() @@ -21,8 +19,6 @@ def get_selection(): @api.route("/chrom/", methods=["GET"]) @cross_origin(supports_credentials=True) def get_chrom(taxid): - """Provides access to chrom.sizes for one - selected organism for current database version.""" public_service = get_public_service() return public_service.get_chrom(taxid) @@ -30,8 +26,7 @@ def get_chrom(taxid): @api.route("/search", methods=["GET"]) @cross_origin(supports_credentials=True) def get_search(): - """Get Data records for conditional selection, add - filters and sort.""" + """Search view API.""" selection_ids = request.args.getlist("selection", type=int) taxa_id = request.args.get("taxid", type=int) chrom = request.args.get("chrom", type=str) @@ -60,16 +55,15 @@ def get_search(): @api.route("/browse", methods=["GET"]) @cross_origin(supports_credentials=True) def get_browse(): - """Retrieve all dataset/projects.""" - # 11.2023 no lazy loading, all data is returned - # filtering is done in Vue.js + """Browse view API.""" public_service = get_public_service() return public_service.get_dataset() @api.route("/compare/", methods=["GET"]) @cross_origin(supports_credentials=True) -def get_compare_step(step): +def get_compare(step): + """Compare view API.""" dataset_ids_a = request.args.getlist("datasetIdsA", type=str) dataset_ids_b = request.args.getlist("datasetIdsB", type=str) dataset_upload = request.args.get("datasetUpload", type=str) @@ -85,8 +79,6 @@ def get_compare_step(step): @api.route("/upload", methods=["POST"]) @cross_origin(supports_credentials=True) def upload_file(): - """Upload ...""" - # TODO: define app.config['UPLOAD_PATH'] = UPLOAD_FOLDER # ALLOWED_EXTENSIONS are dealt with PrimeVue FileUpload # PEP8 import diff --git a/server/src/scimodom/api/user.py b/server/src/scimodom/api/user.py index 06f5eb9a..37f85ad4 100644 --- a/server/src/scimodom/api/user.py +++ b/server/src/scimodom/api/user.py @@ -5,8 +5,6 @@ from flask import Blueprint, request, jsonify from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity -ACCESS_TOKEN_EXPIRATION_TIME = timedelta(hours=2) - from scimodom.services.user import ( get_user_service, UserExists, @@ -18,6 +16,8 @@ user_api = Blueprint("user_api", __name__) +ACCESS_TOKEN_EXPIRATION_TIME = timedelta(hours=2) + @user_api.route("/register_user", methods=["POST"]) def register_user(): diff --git a/server/src/scimodom/database/queries.py b/server/src/scimodom/database/queries.py index c64a4f39..82cf6957 100644 --- a/server/src/scimodom/database/queries.py +++ b/server/src/scimodom/database/queries.py @@ -26,10 +26,9 @@ def query_column_where( :param model: Model (or model name) :type model: ... :columns: Column or list of columns - :type columns: String | list + :type columns: str | list :param filters: Query filters in the form of {column: value} - :type filters: Dict - + :type filters: dict """ columns_list = utils.to_list(columns) diff --git a/server/src/scimodom/services/mail.py b/server/src/scimodom/services/mail.py index ed7aab9d..7e6250e6 100644 --- a/server/src/scimodom/services/mail.py +++ b/server/src/scimodom/services/mail.py @@ -1,6 +1,7 @@ from email.mime.text import MIMEText from smtplib import SMTP from typing import Optional + from scimodom.config import Config from scimodom.utils.url_routes import ( get_user_registration_link, @@ -12,9 +13,11 @@ class MailService: """Server to handle email notifications. For now all email templates are hardcoded in English. - :smtp_server: A server willing to relay unauthenticated emails for us. - :from_address: The email address used for the sender. It must be also acceptable - for the SMTP server. + :param smtp_server: A server willing to relay unauthenticated emails for us. + :type smtp_server: str + :param from_address: The email address used for the sender. It must be also acceptable + for the SMTP server. + :type from_address: str """ def __init__(self, smtp_server: str, from_address: str): @@ -22,6 +25,16 @@ def __init__(self, smtp_server: str, from_address: str): self._from_address = from_address def _send(self, to_address, subject: str, text: str): + """ + Sends email template. + + :param to_address: Email address + :type to_address: str + :param subject: Subject + :type subject: str + :param text: Content + :type text: str + """ connection = SMTP(self._smtp_server) m = MIMEText(text, "plain") m["Subject"] = subject @@ -32,7 +45,14 @@ def _send(self, to_address, subject: str, text: str): connection.quit() def send_register_confirmation_token(self, email: str, token: str): - """Sends out a message to verify the email address during user registration.""" + """Sends out a message to verify the email address for + user registration. + + :param email: Email address + :type email: str + :param token: Token + :type token: str + """ link = get_user_registration_link(email, token) self._send( to_address=email, @@ -58,7 +78,13 @@ def send_register_confirmation_token(self, email: str, token: str): ) def send_password_reset_token(self, email: str, token: str): - """Sends out a message to allow the user to confirm a password reset.""" + """Sends out a message to allow the user to confirm a password reset. + + :param email: Email address + :type email: str + :param token: Token + :type token: str + """ link = get_password_reset_link(email, token) self._send( to_address=email, @@ -90,6 +116,9 @@ def send_password_reset_token(self, email: str, token: str): def get_mail_service() -> MailService: """ Helper function to create a MailService by validating and injecting the configuration. + + :returns: Mail service instance + :rtype: MailService """ global _cached_mail_service if _cached_mail_service is None: diff --git a/server/src/scimodom/services/public.py b/server/src/scimodom/services/public.py index 10465d4d..3e12b3a5 100644 --- a/server/src/scimodom/services/public.py +++ b/server/src/scimodom/services/public.py @@ -41,6 +41,12 @@ class PublicService: :param session: SQLAlchemy ORM session :type session: Session + :param FEATURES: List of features + :type FEATURES: list of str + :param BIOTYPES: Available biotypes + :type BIOTYPES: dict of {str, str} + :param MAPPED_BIOTYPES: List of biotypes to use + :type MAPPED_BIOTYPES: list of str """ FEATURES: ClassVar[list[str]] = sorted( diff --git a/server/src/scimodom/services/user.py b/server/src/scimodom/services/user.py index 75cff39d..ce220bff 100644 --- a/server/src/scimodom/services/user.py +++ b/server/src/scimodom/services/user.py @@ -37,9 +37,11 @@ class UserService: attacks are logged in detailed but reported to the outside as a WrongUserOrPassword exception with a generic message. - :session: SQLAlchemy session object - :mail_service: Service used to send tokens for registration and - password reset to the user + :param session: SQLAlchemy ORM session + :type session: Session + :param mail_service: Service used to send tokens for registration and + password reset to the user + :type mail_service: MailService """ TOKEN_CHARACTERS = string.ascii_letters + string.digits @@ -53,9 +55,11 @@ def register_user(self, email: str, password: str) -> None: a token to validate the email address. It may fail with a UserExists exception. - :email: A user is identified by the email address. There is no - separate name. - :password: Clear text password + :param email: A user is identified by the email address. There is no + separate name. + :type email: str + :param password: Clear text password + :type password: str """ try: self._get_user_by_email(email) @@ -76,11 +80,18 @@ def register_user(self, email: str, password: str) -> None: email, confirmation_token ) self._session.commit() - except Exception as e: + except Exception as exc: self._session.rollback() - raise e + raise exc - def _get_user_by_email(self, email) -> User: + def _get_user_by_email(self, email: str) -> User: + """Get user. + + :param email: User name (email address). + :type email: str + :returns: User instance + :rtype: User + """ stmt = select(User).where(User.email == email) users = list(self._session.scalars(stmt)) if len(users) > 1: @@ -100,6 +111,11 @@ def _send_email_with_confirmation_token(self, email, confirmation_token): def confirm_user(self, email: str, confirmation_token: str): """Activates a registered user with the token sent out before by email. If the user is active already just ignore it and count it as success. + + :param email: User name (email address). + :type email: str + :param confirmation_token: Token + :type confirmation_token: str """ try: try: @@ -120,8 +136,8 @@ def confirm_user(self, email: str, confirmation_token: str): user.confirmation_token = None self._session.commit() - except _DetailedWrongUserOrPassword as e: - logger.warning(f"WARNING: {str(e)}") + except _DetailedWrongUserOrPassword as exc: + logger.warning(f"WARNING: {str(exc)}") raise WrongUserOrPassword("Go away hacker!") def request_password_reset(self, email: str) -> None: @@ -130,14 +146,27 @@ def request_password_reset(self, email: str) -> None: unauthenticated hacker may abuse the workflow to trigger a state change of the account. The workflow can also be used to retry registration if the initial email with the token was lost. + + :param email: User name (email address). + :type email: str """ user = self._get_user_by_email(email) user.confirmation_token = self._get_random_token() self._session.commit() self._mail_service.send_password_reset_token(email, user.confirmation_token) - def do_password_reset(self, email, confirmation_token, new_password) -> None: - """Do a password reset with a token sent out before via email.""" + def do_password_reset( + self, email: str, confirmation_token: str, new_password: str + ) -> None: + """Do a password reset with a token sent out before via email. + + :param email: User name (email address). + :type email: str + :param confirmation_token: Token + :type confirmation_token: str + :param new_password: New password + :type new_password: str + """ try: try: user = self._get_user_by_email(email) @@ -156,13 +185,18 @@ def do_password_reset(self, email, confirmation_token, new_password) -> None: user.confirmation_token = None self._session.commit() - except _DetailedWrongUserOrPassword as e: - logger.warning(f"WARNING: {str(e)}") + except _DetailedWrongUserOrPassword as exc: + logger.warning(f"WARNING: {str(exc)}") raise WrongUserOrPassword("Go away hacker!") - def check_password(self, email, password) -> bool: + def check_password(self, email: str, password: str) -> bool: """Returns true if the password matches the stored password. Otherwise, false is returned - also in case of an unknown or inactive user. + + :param email: User name (email address). + :type email: str + :param password: Password + :type password: str """ try: user = self._get_user_by_email(email) @@ -186,8 +220,8 @@ def check_password(self, email, password) -> bool: def get_user_service(): """Helper function to set up a UserService object by injecting its dependencies. - :refresh: If true a new instance is created. Otherwise, a cached object may be - returned. + :returns: User service instance + :rtype: UserService """ global _cached_user_service if _cached_user_service is None: