Files
MIND/backend/base/definitions.py
CasVT b68284530b Changed generation and hashing of API keys
Moved from generating API keys using os.urandom to secrets.token_hex. Moved from hashing the keys using stdlib.hash to hashlib.sha256.
2025-08-26 00:50:46 +02:00

320 lines
7.0 KiB
Python

# -*- coding: utf-8 -*-
"""
Definitions of basic types, abstract classes, enums, etc.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Literal,
Sequence, Tuple, TypedDict, TypeVar, Union, cast)
from flask import Response
if TYPE_CHECKING:
from backend.implementations.users import User
# region Types
T = TypeVar('T')
U = TypeVar('U')
WEEKDAY_NUMBER = Literal[0, 1, 2, 3, 4, 5, 6]
BaseJSONSerialisable = Union[
int, float, bool, str, None, TypedDict
]
JSONSerialisable = Union[
BaseJSONSerialisable,
TypedDict,
Sequence["JSONSerialisable"],
Dict[str, "JSONSerialisable"]
]
EndpointResponse = Union[
Tuple[Dict[str, JSONSerialisable], int],
Tuple[Response, int],
None
]
EndpointHandler = Union[
Callable[[], EndpointResponse],
Callable[[int], EndpointResponse]
]
# region Constants
class Constants:
MIN_PYTHON_VERSION = (3, 8, 0)
SUB_PROCESS_TIMEOUT = 20.0 # seconds
HOSTING_THREADS = 10
HOSTING_REVERT_TIME = 60.0 # seconds
API_PREFIX = "/api"
ADMIN_API_EXTENSION = "/admin"
ADMIN_PREFIX = API_PREFIX + ADMIN_API_EXTENSION
API_KEY_LENGTH = 32 # hexadecimal characters
DB_FOLDER = ("db",)
DB_NAME = "MIND.db"
DB_ORIGINAL_NAME = 'MIND_original.db'
DB_TIMEOUT = 10.0 # seconds
DB_REVERT_TIME = 60.0 # seconds
TZ_CHANGE_CHECK_TIME = (3, 5) # (hour, minute)
LOGGER_NAME = "MIND"
LOGGER_FILENAME = "MIND.log"
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "admin"
INVALID_USERNAMES = ("", "reminders", "api")
USERNAME_CHARACTERS = 'abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.!@$'
CONNECTION_ERROR_TIMEOUT = 120 # seconds
APPRISE_TEST_TITLE = "MIND: Test title"
APPRISE_TEST_BODY = "MIND: Test body"
# region Enums
class BaseEnum(Enum):
def __eq__(self, other) -> bool:
return self.value == other
def __hash__(self) -> int:
return id(self.value)
class StartType(BaseEnum):
STARTUP = 130
RESTART = 131
RESTART_HOSTING_CHANGES = 132
RESTART_DB_CHANGES = 133
class Interval(BaseEnum):
"Time intervals where the value is in seconds"
ONE_MINUTE = 60
ONE_HOUR = ONE_MINUTE * 60
ONE_DAY = ONE_HOUR * 24
THIRTY_DAYS = ONE_DAY * 30
class InvalidUsernameReason(BaseEnum):
ONLY_NUMBERS = "Username can not only be numbers"
NOT_ALLOWED = "Username is not allowed"
INVALID_CHARACTER = "Username contains an invalid character"
class SendResult(BaseEnum):
SUCCESS = "Success"
CONNECTION_ERROR = "Connection error"
SYNTAX_INVALID_URL = "Syntax of URL invalid"
REJECTED_URL = "Values in URL rejected by service (e.g. invalid API token)"
class ReminderType(BaseEnum):
REMINDER = "Reminder"
STATIC_REMINDER = "Static Reminder"
TEMPLATE = "Template"
class RepeatQuantity(BaseEnum):
YEARS = "years"
MONTHS = "months"
WEEKS = "weeks"
DAYS = "days"
HOURS = "hours"
MINUTES = "minutes"
def sort_by_timeless_title(r: GeneralReminderData) -> Tuple[str, str, str]:
return (r.title, r.text or '', r.color or '')
def sort_by_time(r: ReminderData) -> Tuple[int, str, str, str]:
return (r.time, r.title, r.text or '', r.color or '')
def sort_by_timed_title(r: ReminderData) -> Tuple[str, int, str, str]:
return (r.title, r.time, r.text or '', r.color or '')
def sort_by_id(r: GeneralReminderData) -> int:
return r.id
class TimelessSortingMethod(BaseEnum):
TITLE = sort_by_timeless_title, False
TITLE_REVERSED = sort_by_timeless_title, True
DATE_ADDED = sort_by_id, False
DATE_ADDED_REVERSED = sort_by_id, True
class SortingMethod(BaseEnum):
TIME = sort_by_time, False
TIME_REVERSED = sort_by_time, True
TITLE = sort_by_timed_title, False
TITLE_REVERSED = sort_by_timed_title, True
DATE_ADDED = sort_by_id, False
DATE_ADDED_REVERSED = sort_by_id, True
class DataType(BaseEnum):
STR = 'string'
INT = 'number'
FLOAT = 'decimal number'
BOOL = 'bool'
INT_ARRAY = 'list of numbers'
STR_ARRAY = 'list of string'
NA = 'N/A'
class DataSource(BaseEnum):
DATA = 1
VALUES = 2
FILES = 3
# region TypedDicts
class ApiResponse(TypedDict):
result: Any
error: Union[str, None]
code: int
class DatabaseBackupEntry(TypedDict):
index: int
creation_date: int
filepath: str
filename: str
# region Abstract Classes
class DBMigrator(ABC):
start_version: int
@abstractmethod
def run(self) -> None:
...
class MindException(Exception, ABC):
"""An exception specific to MIND"""
@property
@abstractmethod
def api_response(self) -> ApiResponse:
...
class StartTypeHandler(ABC):
description: str
"""A short description of what the start type is for"""
timeout: float
"""The amount of time in seconds before reverting"""
restart_on_timeout: bool
""""Whether the application should restart once the timeout is reached"""
@abstractmethod
def on_timeout(self) -> None:
"""
Called when the timeout is reached. Generally reverts changes.
"""
...
@abstractmethod
def on_diffuse(self) -> None:
"""
Called when the timer is diffused. Generally finalises changes.
"""
...
# region Dataclasses
@dataclass
class ApiKeyEntry:
exp: int
user_data: User
@dataclass(frozen=True, order=True)
class NotificationServiceData:
id: int
title: str
url: str
def todict(self) -> Dict[str, Any]:
return self.__dict__
@dataclass(frozen=True, order=True)
class UserData:
id: int
username: str
admin: bool
salt: bytes
hash: bytes
def todict(self) -> Dict[str, Any]:
return {
k: v
for k, v in self.__dict__.items()
if k in ('id', 'username', 'admin')
}
@dataclass(order=True)
class GeneralReminderData:
id: int
title: str
text: Union[str, None]
color: Union[str, None]
notification_services: List[int]
def todict(self) -> Dict[str, Any]:
return self.__dict__
@dataclass(order=True)
class TemplateData(GeneralReminderData):
...
@dataclass(order=True)
class StaticReminderData(GeneralReminderData):
...
@dataclass(order=True)
class ReminderData(GeneralReminderData):
time: int
original_time: Union[int, None]
repeat_quantity: Union[str, None]
repeat_interval: Union[int, None]
_weekdays: Union[str, None]
cron_schedule: Union[str, None]
enabled: bool
def __post_init__(self) -> None:
if self._weekdays is not None:
self.weekdays: Union[List[WEEKDAY_NUMBER], None] = [
cast(WEEKDAY_NUMBER, int(n))
for n in self._weekdays.split(',')
if n
]
else:
self.weekdays = None
def todict(self) -> Dict[str, Any]:
return {
k: v
for k, v in self.__dict__.items()
if k != '_weekdays'
}