"""This module declareds the pydantic ORM representation for users.""" from datetime import datetime from abc import ABC from typing import Optional from re import compile from pydantic import BaseModel, validator from todo.schemas.common import is_valid_id def is_valid_email_format(input_str: str) -> bool: """Checks whether the input string is a valid email address format. Uses a regular expression to perform the check. """ if not isinstance(input_str, str): raise TypeError(f"Expected a string but got {type(input_str)}.") regex = compile(r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') if regex.fullmatch(input_str): return True else: return False class AbstractUserValidator(BaseModel, ABC): """Base class for user validators shared with child classes.""" @validator('email', check_fields=False) def assert_email_is_valid(cls, email): if email is not None: if not is_valid_email_format(email): raise ValueError("Invalid email address format.") return email @validator('first_name', check_fields=False) def assert_first_name_is_valid(cls, first_name): if first_name is not None: if not len(first_name): raise ValueError("First Name must not be empty.") return first_name @validator('last_name', check_fields=False) def assert_last_name_is_valid(cls, last_name): if last_name is not None: if not len(last_name): raise ValueError("Last Name must not be empty.") return last_name class AbstractUser(AbstractUserValidator, ABC): """Base class for user attributes shared with child classes.""" email: str first_name: str last_name: str is_admin: bool class UserLogin(AbstractUserValidator, ABC): """Schema used during user login""" email: str password: str class UserCreate(AbstractUser): """Schema for user creation.""" password: str password_confirmation: str @validator('password_confirmation') def assert_passwords_match(cls, password_confirmation, values): if not password_confirmation == values['password']: raise ValueError("Passwords do not match.") if len(password_confirmation) < 1: # TODO use more robust password rules raise ValueError("Password must not be empty.") return password_confirmation class UserUpdate(AbstractUserValidator): """Schema for user info updates. All fields here are optional, but passwords must match if at least one was provided. """ email: Optional[str] first_name: Optional[str] last_name: Optional[str] is_admin: Optional[bool] password: Optional[str] = None password_confirmation: Optional[str] = None @validator('password_confirmation') def assert_passwords_match_or_are_both_none(cls, password_confirmation, values): password = values.get('password') if None not in [password, password_confirmation]: if not password == password_confirmation: raise ValueError("Passwords do not match.") if len(password_confirmation) < 1: # TODO use more robust password rules raise ValueError("Password must not be empty.") return password_confirmation class User(AbstractUser): """Schema for user info displaying.""" id: int created: datetime updated: datetime @validator('id') def assert_id_is_valid(cls, id): if not is_valid_id(id): raise ValueError("ID is invalid.") return id class Config: orm_mode = True