From 06001b29f81db701e0d0f756b5d1a375fa55e9e3 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 17 Jul 2026 16:45:38 +0800 Subject: [PATCH] refactor: remove legacy backend --- backend-legacy/.gitignore | 19 - backend-legacy/.python-version | 1 - backend-legacy/README.md | 0 backend-legacy/coconut-leaf.py | 84 --- backend-legacy/coconut-leaf.template.toml | 22 - backend-legacy/config.py | 130 ---- backend-legacy/database.py | 708 ---------------------- backend-legacy/dt.py | 289 --------- backend-legacy/logger.py | 42 -- backend-legacy/pyproject.toml | 14 - backend-legacy/server.py | 513 ---------------- backend-legacy/sql/mysql.sql | 0 backend-legacy/sql/sqlite.sql | 67 -- backend-legacy/utils.py | 64 -- backend-legacy/uv.lock | 117 ---- 15 files changed, 2070 deletions(-) delete mode 100644 backend-legacy/.gitignore delete mode 100644 backend-legacy/.python-version delete mode 100644 backend-legacy/README.md delete mode 100644 backend-legacy/coconut-leaf.py delete mode 100644 backend-legacy/coconut-leaf.template.toml delete mode 100644 backend-legacy/config.py delete mode 100644 backend-legacy/database.py delete mode 100644 backend-legacy/dt.py delete mode 100644 backend-legacy/logger.py delete mode 100644 backend-legacy/pyproject.toml delete mode 100644 backend-legacy/server.py delete mode 100644 backend-legacy/sql/mysql.sql delete mode 100644 backend-legacy/sql/sqlite.sql delete mode 100644 backend-legacy/utils.py delete mode 100644 backend-legacy/uv.lock diff --git a/backend-legacy/.gitignore b/backend-legacy/.gitignore deleted file mode 100644 index 09347df..0000000 --- a/backend-legacy/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -## ======== Personal ======== -# Database file -*.db - -# Ignore setting file -coconut-leaf.toml - -## ======== Python ======== -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info - -# Virtual environments -.venv - diff --git a/backend-legacy/.python-version b/backend-legacy/.python-version deleted file mode 100644 index 2c07333..0000000 --- a/backend-legacy/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/backend-legacy/README.md b/backend-legacy/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/backend-legacy/coconut-leaf.py b/backend-legacy/coconut-leaf.py deleted file mode 100644 index 96d18b1..0000000 --- a/backend-legacy/coconut-leaf.py +++ /dev/null @@ -1,84 +0,0 @@ -import sys -from argparse import ArgumentParser -from typing import cast -from pathlib import Path - -import server -import config -import utils -import database -import logger -from logger import LOGGER, LoggerLevel - - -def GetUsernamePassword() -> tuple[str, str]: - print("What is the first username of this calendar system?") - cache = input() - while not utils.IsValidUsername(cache): - print("Sorry, invalid data. Please try again.") - cache = input() - username = cache - - print("Input this user password:") - cache = input() - while not utils.IsValidPassword(cache): - print("Sorry, invalid data. Please try again.") - cache = input() - password = cache - - return (username, password) - -if __name__ == "__main__": - # Set as INFO level in default first, - # and we will change it once we load the configuration file. - logger.set_level(LoggerLevel.INFO) - - # Receive arguments - parser = ArgumentParser( - description="The server of light, self-host and multi-account calendar system." - ) - parser.add_argument( - "-c", - "--config", - required=True, - type=Path, - action="store", - metavar="CONFIG_TOML", - dest="config", - help="The configuration file for coconut-leaf", - ) - parser.add_argument( - "-i", - "--init", - action="store_true", - dest="init", - help="Set for initialize the calendar system", - ) - args = parser.parse_args() - - # Show splash - LOGGER.info("Coconut-leaf") - LOGGER.info("A light, self-host and multi-account calendar system") - LOGGER.info("Project: https://github.com/yyc12345/coconut-leaf") - LOGGER.info("===================") - - # Load config file - try: - config.setup_config(cast(Path, args.config)) - except Exception as e: - LOGGER.critical(f"Error loading config file: {e}") - sys.exit(1) - - # Change logging level again according to whether enable debug mode - logging_level = LoggerLevel.DEBUG if config.get_config().others.debug else LoggerLevel.INFO - logger.set_level(logging_level) - - # Initialize the calendar system if needed - if cast(bool, args.init): - gotten_data = GetUsernamePassword() - calendar = database.CalendarDatabase() - calendar.init(*gotten_data) - calendar.close() - - LOGGER.info("Staring server...") - server.run() diff --git a/backend-legacy/coconut-leaf.template.toml b/backend-legacy/coconut-leaf.template.toml deleted file mode 100644 index 5ed2c96..0000000 --- a/backend-legacy/coconut-leaf.template.toml +++ /dev/null @@ -1,22 +0,0 @@ -[database] -driver = "sqlite" - -[database.config] -path = "coconut-leaf.db" - -# [database] -# driver = "mysql" -# -# [database.config] -# host = "localhost" -# port = 3306 -# user = "root" -# password = "password" -# database = "coconut_leaf" - -[web] -port = 8848 - -[others] -auto-token-clean-duration = 86400 -debug = true diff --git a/backend-legacy/config.py b/backend-legacy/config.py deleted file mode 100644 index 33eebcb..0000000 --- a/backend-legacy/config.py +++ /dev/null @@ -1,130 +0,0 @@ -import tomllib -from dataclasses import dataclass -from enum import StrEnum -from typing import Optional -from pathlib import Path - - -class DatabaseDriver(StrEnum): - SQLITE = "sqlite" - MYSQL = "mysql" - - @staticmethod - def from_raw(raw: dict): - return DatabaseDriver(raw["driver"]) - - -@dataclass(frozen=True) -class SqliteDatabaseConfig: - path: str - """Database path""" - - @staticmethod - def from_raw(raw: dict): - return SqliteDatabaseConfig(raw["path"]) - - -@dataclass(frozen=True) -class MysqlDatabaseConfig: - host: str - """Database host""" - port: int - """Database port""" - user: str - """Database user""" - password: str - """Database password""" - database: str - """Database name""" - - @staticmethod - def from_raw(raw: dict): - return MysqlDatabaseConfig( - raw["host"], raw["port"], raw["user"], raw["password"], raw["database"] - ) - - -@dataclass(frozen=True) -class DatabaseConfig: - driver: DatabaseDriver - """Database driver""" - config: SqliteDatabaseConfig | MysqlDatabaseConfig - """Database config""" - - @staticmethod - def from_raw(raw: dict): - if raw["driver"] == DatabaseDriver.SQLITE: - return DatabaseConfig( - DatabaseDriver.SQLITE, SqliteDatabaseConfig.from_raw(raw["config"]) - ) - elif raw["driver"] == DatabaseDriver.MYSQL: - return DatabaseConfig( - DatabaseDriver.MYSQL, MysqlDatabaseConfig.from_raw(raw["config"]) - ) - else: - raise ValueError("Invalid database driver") - - -@dataclass(frozen=True) -class WebConfig: - port: int - """Web server port""" - - @staticmethod - def from_raw(raw: dict): - return WebConfig(raw["port"]) - - -@dataclass(frozen=True) -class OthersConfig: - debug: bool - """Whether enable debug mode""" - auto_token_clean_duration: int - """Auto token clean duration""" - - @staticmethod - def from_raw(raw: dict): - return OthersConfig(raw["debug"], raw["auto-token-clean-duration"]) - - -@dataclass(frozen=True) -class Config: - database: DatabaseConfig - web: WebConfig - others: OthersConfig - - @staticmethod - def from_raw(raw: dict): - return Config( - database=DatabaseConfig.from_raw(raw["database"]), - web=WebConfig.from_raw(raw["web"]), - others=OthersConfig.from_raw(raw["others"]), - ) - - -_CONFIG: Optional[Config] = None - - -def setup_config(p: Path) -> None: - """ - Setup config by given path. - - Raise exception if config file is invalid. - """ - with open(p, "rb") as f: - raw = tomllib.load(f) - - global _CONFIG - _CONFIG = Config.from_raw(raw) - - -def get_config() -> Config: - """ - Get config instance. - - Raises RuntimeError if config is not loaded. - """ - if _CONFIG is None: - raise RuntimeError("Config is not loaded. Call setup_config() first.") - else: - return _CONFIG diff --git a/backend-legacy/database.py b/backend-legacy/database.py deleted file mode 100644 index 354e94b..0000000 --- a/backend-legacy/database.py +++ /dev/null @@ -1,708 +0,0 @@ -import sqlite3 -import threading -from typing import cast -from pathlib import Path -from dataclasses import dataclass -from typing import Callable, ParamSpec, TypeVar, Generic - -import dt -import utils -import config -from logger import LOGGER - - -T = TypeVar('T') -P = ParamSpec('P') -R = TypeVar('R') - - -@dataclass(frozen=True) -class ResponseBody(Generic[T]): - """The generic response body for API return.""" - - success: bool - """True if this operation is successful, otherwise false.""" - error: str - """The error message provided when operation failed.""" - data: T | None - """The payload provided when operation successed.""" - - -class DbException(Exception): - """Error occurs when manipulating with database.""" - pass - - -def SafeDatabaseOperation(inner: Callable[P, R]) -> Callable[P, ResponseBody[R]]: - def wrapper(*args, **kwargs) -> ResponseBody[R]: - # extract self from args - self: 'CalendarDatabase' = args[0] - # get config - cfg = config.get_config() - - with self.mutex: - # try to fetching database and allocate database cursor - try: - db = self._get_db() - self._allocate_cursor() - except Exception as e: - self._free_cursor() - if cfg.others.debug: - LOGGER.exception(e) - return ResponseBody(False, str(e), None) - - # do real data work - try: - currentTime = utils.GetCurrentTimestamp() - if currentTime - self.latestClean > cfg.others.auto_token_clean_duration: - self.latestClean = currentTime - LOGGER.info('Cleaning outdated token...') - self.tokenOper_clean() - - result = ResponseBody(True, '', inner(*args, **kwargs)) - self._free_cursor() - db.commit() - return result - except Exception as e: - self._free_cursor() - db.rollback() - if cfg.others.debug: - LOGGER.exception(e) - return ResponseBody(False, str(e), None) - - return wrapper - -class CalendarDatabase: - - db: sqlite3.Connection | None - cursor: sqlite3.Cursor | None - mutex: threading.Lock - latestClean: int - - def __init__(self): - self.db = None - self.cursor = None - self.mutex = threading.Lock() - self.latestClean = 0 - - def open(self): - if (self.db is not None): - raise DbException('Database is already opened') - - cfg = config.get_config() - match cfg.database.driver: - case config.DatabaseDriver.SQLITE: - self.db = sqlite3.connect(cast(config.SqliteDatabaseConfig, cfg.database.config).path, check_same_thread = False) - self.db.execute('PRAGMA encoding = "UTF-8";') - self.db.execute('PRAGMA foreign_keys = ON;') - case config.DatabaseDriver.MYSQL: - raise DbException('Not implemented database') - case _: - raise DbException('Unknow database type') - - def init(self, username: str, password: str): - if (self.db is not None): - raise DbException('Database is already opened') - - # establish tables - cfg = config.get_config() - backend_path = Path(__file__).resolve().parent - backend_sql_path = backend_path / 'sql' - match cfg.database.driver: - case config.DatabaseDriver.SQLITE: - sql_file = backend_sql_path / 'sqlite.sql' - case config.DatabaseDriver.MYSQL: - raise DbException('Not implemented database') - case _: - raise DbException('Unknow database type') - - self.open() - db = self._get_db() - - self._allocate_cursor() - cursor = self._get_cursor() - - # execute script for creating tables - with open(sql_file, 'r', encoding='utf-8') as fsql: - cursor.executescript(fsql.read()) - # add default user in user table - cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', ( - username, - utils.ComputePasswordHash(password), - 1, - utils.GenerateSalt() - )) - - self._free_cursor() - - # commit to database - db.commit() - - def close(self): - if (self.db is None): - LOGGER.warning('Try to close null database.') - else: - self._free_cursor() - self.db.close() - self.db = None - - def _get_db(self) -> sqlite3.Connection: - if (self.db is None): - raise DbException('There is no opened database') - else: - return self.db - - def _allocate_cursor(self) -> None: - if (self.cursor is not None): - raise DbException('There is already opened database cursor') - else: - self.cursor = self._get_db().cursor() - - def _get_cursor(self) -> sqlite3.Cursor: - if (self.cursor is None): - raise DbException('There is no opened database cursor') - else: - return self.cursor - - def _free_cursor(self) -> None: - if (self.cursor is None): - LOGGER.warning('Try to free null databse cursor.') - else: - self.cursor.close() - self.cursor = None - - # ======================= token related internal operation - def tokenOper_clean(self): - # remove outdated token - cursor = self._get_cursor() - cursor.execute('DELETE FROM token WHERE [token_expire_on] <= ?',(utils.GetCurrentTimestamp(), )) - - def tokenOper_postpone_expireOn(self, token): - cursor = self._get_cursor() - cursor.execute('UPDATE token SET [token_expire_on] = ? WHERE [token] = ?;', ( - utils.GetTokenExpireOn(), - token - )) - - def tokenOper_check_valid(self, token): - self.tokenOper_get_username(token) - - def tokenOper_is_admin(self, username): - cursor = self._get_cursor() - cursor.execute('SELECT [is_admin] FROM user WHERE [name] = ?;',(username, )) - cache = cursor.fetchone()[0] - return cache == 1 - - def tokenOper_get_username(self, token): - cursor = self._get_cursor() - cursor.execute('SELECT [user] FROM token WHERE [token] = ? AND [token_expire_on] > ?;',( - token, - utils.GetCurrentTimestamp() - )) - result = cursor.fetchone()[0] - # need postpone expire on time - self.tokenOper_postpone_expireOn(token) - return result - - # =============================== # =============================== operation function - # =============================== common - - @SafeDatabaseOperation - def common_salt(self, username): - cursor = self._get_cursor() - salt = utils.GenerateSalt() - cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', ( - salt, - username - )) - return salt - - @SafeDatabaseOperation - def common_login(self, username, password, clientUa, clientIp): - cursor = self._get_cursor() - cursor.execute('SELECT [password], [salt] FROM user WHERE [name] = ?;', (username, )) - (gotten_salt, gotten_password) = cursor.fetchone() - - if password == utils.ComputePasswordHashWithSalt(gotten_password, gotten_salt): - token = utils.GenerateToken(username) - cursor.execute('UPDATE user SET [salt] = ? WHERE [name] = ?;', ( - utils.GenerateSalt(), # regenerate a new slat to prevent re-login try - username - )) - cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', ( - username, - token, - utils.GetTokenExpireOn(), # add 2 day from now - clientUa, - clientIp, - )) - return token - else: - # throw a exception to indicate fail to login - raise DbException('Login authentication failed') - - @SafeDatabaseOperation - def common_webLogin(self, username, password, clientUa, clientIp): - cursor = self._get_cursor() - LOGGER.debug(f'WebLogin Username: {username}') - LOGGER.debug(f'WebLogin Password: {password}') - passwordHash = utils.ComputePasswordHash(password) - LOGGER.debug(f'WebLogin Password Hash: {passwordHash}') - - cursor.execute('SELECT [name] FROM user WHERE [name] = ? AND [password] = ?;', (username, passwordHash)) - - if len(cursor.fetchall()) != 0: - token = utils.GenerateToken(username) - cursor.execute('INSERT INTO token VALUES (?, ?, ?, ?, ?);', ( - username, - token, - utils.GetTokenExpireOn(), # add 2 day from now - clientUa, - clientIp, - )) - return token - else: - # throw a exception to indicate fail to login - raise DbException('Login authentication failed') - - @SafeDatabaseOperation - def common_logout(self, token): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - cursor.execute('DELETE FROM token WHERE [token] = ?;', (token, )) - return True - - @SafeDatabaseOperation - def common_tokenValid(self, token): - self.tokenOper_check_valid(token) - return True - - # =============================== calendar - @SafeDatabaseOperation - def calendar_getFull(self, token, startDateTime, endDateTime): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT calendar.* FROM calendar INNER JOIN collection \ - ON collection.uuid = calendar.belong_to \ - WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);', - (username, startDateTime, endDateTime)) - return cursor.fetchall() - - @SafeDatabaseOperation - def calendar_getList(self, token, startDateTime, endDateTime): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT calendar.uuid FROM calendar INNER JOIN collection \ - ON collection.uuid = calendar.belong_to \ - WHERE (collection.user = ? AND calendar.loop_date_time_end >= ? AND calendar.loop_date_time_start - (calendar.event_date_time_end - calendar.event_date_time_start) <= ?);', - (username, startDateTime, endDateTime)) - return tuple(map(lambda x: x[0], cursor.fetchall())) - - @SafeDatabaseOperation - def calendar_getDetail(self, token, uuid): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - cursor.execute('SELECT * FROM calendar WHERE [uuid] = ?;', (uuid, )) - return cursor.fetchone() - - @SafeDatabaseOperation - def calendar_update(self, token, uuid, lastChange, **optArgs): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - # get prev data - cursor.execute('SELECT * FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange)) - analyseData = list(cursor.fetchone()) - - # construct update data - lastupdate = utils.GenerateUUID() - sqlList = [ - '[last_change] = ?', - ] - argumentsList = [ - lastupdate, - ] - - # analyse opt arg - reAnalyseLoop = False - - cache = optArgs.get('belongTo', None) - if cache is not None: - sqlList.append('[belong_to] = ?') - argumentsList.append(cache) - cache = optArgs.get('title', None) - if cache is not None: - sqlList.append('[title] = ?') - argumentsList.append(cache) - cache = optArgs.get('description', None) - if cache is not None: - sqlList.append('[description] = ?') - argumentsList.append(cache) - cache = optArgs.get('eventDateTimeStart', None) - if cache is not None: - sqlList.append('[event_date_time_start] = ?') - argumentsList.append(cache) - reAnalyseLoop = True - analyseData[5] = cache - cache = optArgs.get('eventDateTimeEnd', None) - if cache is not None: - sqlList.append('[event_date_time_end] = ?') - argumentsList.append(cache) - cache = optArgs.get('loopRules', None) - if cache is not None: - sqlList.append('[loop_rules] = ?') - argumentsList.append(cache) - reAnalyseLoop = True - analyseData[8] = cache - cache = optArgs.get('timezoneOffset', None) - if cache is not None: - sqlList.append('[timezone_offset] = ?') - argumentsList.append(cache) - reAnalyseLoop = True - analyseData[7] = cache - - if reAnalyseLoop: - # re-compute loop data and upload it into list - sqlList.append('[loop_date_time_start] = ?') - argumentsList.append(analyseData[5]) - sqlList.append('[loop_date_time_end] = ?') - argumentsList.append(str(dt.ResolveLoopStr( - analyseData[8], - analyseData[5], - analyseData[7] - ))) - - # execute - argumentsList.append(uuid) - cursor.execute('UPDATE calendar SET {} WHERE [uuid] = ?;'.format(', '.join(sqlList)), - tuple(argumentsList)) - if cursor.rowcount != 1: - raise DbException('Fail to update due to no matched rows or too much rows.') - return lastupdate - - @SafeDatabaseOperation - def calendar_add(self, token, belongTo, title, description, eventDateTimeStart, eventDateTimeEnd, loopRules, timezoneOffset): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - newuuid = utils.GenerateUUID() - lastupdate = utils.GenerateUUID() - - # analyse loopRules and output following 2 fileds. - loopDateTimeStart = eventDateTimeStart - loopDateTimeEnd = dt.ResolveLoopStr(loopRules, eventDateTimeStart, timezoneOffset) - - cursor.execute('INSERT INTO calendar VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', - (newuuid, - belongTo, - title, - description, - lastupdate, - eventDateTimeStart, - eventDateTimeEnd, - timezoneOffset, - loopRules, - loopDateTimeStart, - loopDateTimeEnd)) - return newuuid - - @SafeDatabaseOperation - def calendar_delete(self, token, uuid, lastChange): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - cursor.execute('DELETE FROM calendar WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange)) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - return True - - # =============================== collection - @SafeDatabaseOperation - def collection_getFullOwn(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ?;', (username, )) - return cursor.fetchall() - - @SafeDatabaseOperation - def collection_getListOwn(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT [uuid] FROM collection WHERE [user] = ?;', (username, )) - return tuple(map(lambda x: x[0], cursor.fetchall())) - - @SafeDatabaseOperation - def collection_getDetailOwn(self, token, uuid): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT [uuid], [name], [last_change] FROM collection WHERE [user] = ? AND [uuid] = ?;', (username, uuid)) - return cursor.fetchone() - - @SafeDatabaseOperation - def collection_addOwn(self, token, newname): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - newuuid = utils.GenerateUUID() - lastupdate = utils.GenerateUUID() - cursor.execute('INSERT INTO collection VALUES (?, ?, ?, ?);', - (newuuid, newname, username, lastupdate)) - return newuuid - - @SafeDatabaseOperation - def collection_updateOwn(self, token, uuid, newname, lastChange): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - lastupdate = utils.GenerateUUID() - cursor.execute('UPDATE collection SET [name] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', ( - newname, - lastupdate, - uuid, - lastChange - )) - if cursor.rowcount != 1: - raise DbException('Fail to update due to no matched rows or too much rows.') - return lastupdate - - @SafeDatabaseOperation - def collection_deleteOwn(self, token, uuid, lastChange): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - cursor.execute('DELETE FROM collection WHERE [uuid] = ? AND [last_change] = ?;', ( - uuid, - lastChange - )) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - return True - - @SafeDatabaseOperation - def collection_getSharing(self, token, uuid): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - cursor.execute('SELECT [target] FROM share WHERE [uuid] = ?;', (uuid, )) - return tuple(map(lambda x: x[0], cursor.fetchall())) - - @SafeDatabaseOperation - def collection_deleteSharing(self, token, uuid, target, lastChange): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - lastupdate = utils.GenerateUUID() - cursor.execute('UPDATE collection SET [last_change] = ?, WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange)) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - - cursor.execute('DELETE FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target)) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - - return lastupdate - - @SafeDatabaseOperation - def collection_addSharing(self, token, uuid, target, lastChange): - cursor = self._get_cursor() - self.tokenOper_check_valid(token) - - lastupdate = utils.GenerateUUID() - cursor.execute('UPDATE collection SET [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', (lastupdate, uuid, lastChange)) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - - cursor.execute('SELECT * FROM share WHERE [uuid] = ? AND [target] = ?;', (uuid, target)) - if len(cursor.fetchall()) != 0: - raise DbException('Fail to insert duplicated item.') - cursor.execute('INSERT INTO share VALUES (?, ?);', (uuid, target)) - - return lastupdate - - @SafeDatabaseOperation - def collection_getShared(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT collection.uuid, collection.name, collection.user \ - FROM share INNER JOIN collection \ - ON share.uuid = collection.uuid \ - WHERE share.target = ?;', (username, )) - return cursor.fetchall() - - # =============================== todo - @SafeDatabaseOperation - def todo_getFull(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT * FROM todo WHERE [belong_to] = ?;', (username, )) - return cursor.fetchall() - - @SafeDatabaseOperation - def todo_getList(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT [uuid] FROM todo WHERE [belong_to] = ?;', (username, )) - return tuple(map(lambda x: x[0], cursor.fetchall())) - - @SafeDatabaseOperation - def todo_getDetail(self, token, uuid): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('SELECT * FROM todo WHERE [belong_to] = ? AND [uuid] = ?;', (username, uuid)) - return cursor.fetchone() - - @SafeDatabaseOperation - def todo_add(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - newuuid = utils.GenerateUUID() - lastupdate = utils.GenerateUUID() - returnedData = ( - newuuid, - username, - '', - lastupdate, - ) - cursor.execute('INSERT INTO todo VALUES (?, ?, ?, ?);', returnedData) - return returnedData - - @SafeDatabaseOperation - def todo_update(self, token, uuid, data, lastChange): - cursor = self._get_cursor() - # check valid token - self.tokenOper_check_valid(token) - - # update - newLastChange = utils.GenerateUUID() - cursor.execute('UPDATE todo SET [data] = ?, [last_change] = ? WHERE [uuid] = ? AND [last_change] = ?;', ( - data, - newLastChange, - uuid, - lastChange - )) - if cursor.rowcount != 1: - raise DbException('Fail to update due to no matched rows or too much rows.') - return newLastChange - - @SafeDatabaseOperation - def todo_delete(self, token, uuid, lastChange): - cursor = self._get_cursor() - # check valid token - self.tokenOper_check_valid(token) - - # delete - cursor.execute('DELETE FROM todo WHERE [uuid] = ? AND [last_change] = ?;', (uuid, lastChange)) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - return True - - - # =============================== admin - @SafeDatabaseOperation - def admin_get(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - if not self.tokenOper_is_admin(username): - raise DbException('Permission denied.') - - cursor.execute('SELECT [name], [is_admin] FROM user;') - return tuple(map(lambda x: (x[0], x[1] == 1), cursor.fetchall())) - - @SafeDatabaseOperation - def admin_add(self, token, newname): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - if not self.tokenOper_is_admin(username): - raise DbException('Permission denied.') - - newpassword = utils.ComputePasswordHash(utils.GenerateUUID()) - cursor.execute('INSERT INTO user VALUES (?, ?, ?, ?);', ( - newname, - newpassword, - 0, - utils.GenerateSalt() - )) - return (newname, False) - - @SafeDatabaseOperation - def admin_update(self, token, _username, **optArgs): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - if not self.tokenOper_is_admin(username): - raise DbException('Permission denied.') - - # construct data - sqlList = [] - argumentsList = [] - - # analyse opt arg - cache = optArgs.get('password', None) - if cache is not None: - sqlList.append('[password] = ?') - argumentsList.append(utils.ComputePasswordHash(cache)) - cache = optArgs.get('isAdmin', None) - if cache is not None: - sqlList.append('[is_admin] = ?') - argumentsList.append(1 if cache else 0) - - # execute - argumentsList.append(_username) - cursor.execute('UPDATE user SET {} WHERE [name] = ?;'.format(', '.join(sqlList)), - tuple(argumentsList)) - LOGGER.debug(cache) - LOGGER.debug(tuple(argumentsList)) - if cursor.rowcount != 1: - raise DbException('Fail to update due to no matched rows or too much rows.') - return True - - @SafeDatabaseOperation - def admin_delete(self, token, username): - cursor = self._get_cursor() - _username = self.tokenOper_get_username(token) - if not self.tokenOper_is_admin(_username): - raise DbException('Permission denied.') - - # delete - cursor.execute('DELETE FROM user WHERE [name] = ?;', (username, )) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - return True - - # =============================== profile - @SafeDatabaseOperation - def profile_isAdmin(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - return self.tokenOper_is_admin(username) - - @SafeDatabaseOperation - def profile_changePassword(self, token, newpassword): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - cursor.execute('UPDATE user SET [password] = ? WHERE [name] = ?;', ( - utils.ComputePasswordHash(newpassword), - username - )) - return True - - @SafeDatabaseOperation - def profile_getToken(self, token): - cursor = self._get_cursor() - username = self.tokenOper_get_username(token) - - cursor.execute('SELECT * FROM token WHERE [user] = ?;', ( - username, - )) - return cursor.fetchall() - - @SafeDatabaseOperation - def profile_deleteToken(self, token, deleteToken): - cursor = self._get_cursor() - _username = self.tokenOper_get_username(token) - - # delete - cursor.execute('DELETE FROM token WHERE [user] = ? AND [token] = ?;', ( - _username, - deleteToken - )) - if cursor.rowcount != 1: - raise DbException('Fail to delete due to no matched rows or too much rows.') - return True - diff --git a/backend-legacy/dt.py b/backend-legacy/dt.py deleted file mode 100644 index 2f9066b..0000000 --- a/backend-legacy/dt.py +++ /dev/null @@ -1,289 +0,0 @@ -import datetime -import re -import logging -import typing -from functools import reduce -import utils - -MonthDayCount = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - -MIN_DATETIME = datetime.datetime(1950, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) -MAX_DATETIME = datetime.datetime(2200, 1, 1, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) -MIN_TIMESTAMP = int(MIN_DATETIME.timestamp() / 60) -MAX_TIMESTAMP = int(MAX_DATETIME.timestamp() / 60) -DAY1_SPAN = 60 * 24 -DAY7_SPAN = 7 * DAY1_SPAN - -LoopHandle = typing.Callable[[re.Match, int, int, int], int] - -def ResolveLoopStr(strl: str, starttime: int, tzoffset: int) -> int: - # check no loop - if strl == '': - return starttime - - # try compute from loopStop - (loopRules, loopStopRules) = strl.split('-') - cache = precompiledLoopStopRules['infinity'].search(loopStopRules) - if cache is not None: - return MAX_TIMESTAMP - cache = precompiledLoopStopRules['datetime'].search(loopStopRules) - if cache is not None: - return int(cache.group(1)) # group 1 is datetime - cache = precompiledLoopStopRules['times'].search(loopStopRules) - if cache is not None: - loopTimes = int(cache.group(1)) # for follwing calc - else: - raise Exception('Invalid loopStopRules') # invalid rules - - for rules in precompiledLoopRules: - cache = rules[0].search(loopRules) - if cache is not None: - return rules[1](cache, starttime, loopTimes, tzoffset) - else: - raise Exception('Invalid loopRules') - - -def LoopHandle_Year(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int: - clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset)) - isStrict = searchResult.group(1) == 'S' - yearSpan = int(searchResult.group(2)) - - times -= 1 - newYear = clientYear = clientDate.year - newMonth = clientMonth = clientDate.month - newDay = clientDay = clientDate.day - if clientMonth == 2 and clientDay == 29: - if isStrict: - realSpan = utils.LCM(yearSpan, 4) - logging.debug(realSpan) - valCache = starttime - while valCache < MAX_TIMESTAMP and times > 0: - newYear += realSpan - if not IsLeapYear(newYear): - continue - valCache = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) - times -= 1 - else: - newYear += times * yearSpan - if not IsLeapYear(newYear): - newDay = 28 # migrate to 28 - else: - # if times == 1, no extra datetime need to be added - newYear += times * yearSpan - - val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) - return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP - -def LoopHandle_Month(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int: - isStrict = searchResult.group(1) == 'S' - loopType = searchResult.group(2) - monthSpan = int(searchResult.group(3)) - - # we should get original data in each method - times -= 1 - clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset)) - newYear = clientYear = clientDate.year - newMonth = clientMonth = clientDate.month - newDay = clientDay = clientDate.day - # data struct - # dayStatistics = - # (dayForwards || dayBackwards || weeksForward, dayOfWeek || weeksBackwards, dayOfWeek) - # ( A || B || C || D ) - dayStatistics = GetDayInMonth(clientYear, clientMonth, clientDay) - - if isStrict: - if loopType == 'A': - while times > 0: - newMonth += monthSpan - if newMonth > 12: - newYear += int((newMonth - 1) / 12) - newMonth = ((newMonth - 1) % 12) + 1 - if newYear > MAX_DATETIME.year: - break - maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) - if dayStatistics[0] <= maxDays: - times -= 1 - elif loopType == 'B': - while times > 0: - newMonth += monthSpan - if newMonth > 12: - newYear += int((newMonth - 1) / 12) - newMonth = ((newMonth - 1) % 12) + 1 - if newYear > MAX_DATETIME.year: - break - maxDays = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) - if dayStatistics[1] <= maxDays: - times -= 1 - elif loopType == 'C': - while times > 0: - newMonth += monthSpan - if newMonth > 12: - newYear += int((newMonth - 1) / 12) - newMonth = ((newMonth - 1) % 12) + 1 - if newYear > MAX_DATETIME.year: - break - monthStatistics = GetMonthWeekStatistics(newYear, newMonth) - if dayStatistics[2] <= monthStatistics[dayStatistics[3]]: - times -= 1 - elif loopType == 'D': - while times > 0: - newMonth += monthSpan - if newMonth > 12: - newYear += int((newMonth - 1) / 12) - newMonth = ((newMonth - 1) % 12) + 1 - if newYear > MAX_DATETIME.year: - break - monthStatistics = GetMonthWeekStatistics(newYear, newMonth) - if dayStatistics[4] <= monthStatistics[dayStatistics[5]]: - times -= 1 - else: - newMonth += times * monthSpan - newYear += int((newMonth - 1) / 12) - newMonth = ((newMonth - 1) % 12) + 1 - - # all method need calc newDay and it should be the last day of current selected month - # so calc it in there - newDay = MonthDayCount[newMonth - 1] + (1 if newMonth == 2 and IsLeapYear(newYear) else 0) - val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) - return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP - -def LoopHandle_Week(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int: - weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1))) - weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0) - if weekEventCount == 0: - raise Exception('Invalid week format') - - weekSpan = int(searchResult.group(2)) - nowDayOfWeek = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset)).weekday() - if not weekOccupied[nowDayOfWeek]: - times-=1 # if first event is not suit for week loop rules, minus one more event to suit it. - fullWeek = int(times / weekEventCount) - remainEvent = times % weekEventCount - - val = starttime + DAY7_SPAN * fullWeek * weekSpan - if val > MAX_TIMESTAMP: - return MAX_TIMESTAMP # return now, to reduce calc usage - - while remainEvent != 0: - val += DAY1_SPAN - if weekOccupied[nowDayOfWeek % 7]: - remainEvent -= 1 - nowDayOfWeek += 1 - - val -= 1 - return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP - -def LoopHandle_Day(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int: - val = starttime + DAY1_SPAN * times * int(searchResult.group(1)) - val -= 1 - return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP - -precompiledLoopRules: tuple[tuple[re.Pattern, LoopHandle], ...] = ( - (re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year), - (re.compile(r'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month), - (re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week), - (re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day) -) - -precompiledLoopStopRules: dict[str, re.Pattern] = { - 'infinity': re.compile(r'^F$'), - 'datetime': re.compile(r'^D([1-9]\d*|0)$'), - 'times': re.compile(r'^T([1-9]\d*)$') -} - -def LeapYearCountEx(endYear: int, includeThis: bool = False, baseYear: int = 1, includeBase: bool = True): - if not includeThis: - endYear -= 1 - if includeBase: - baseYear -= 1 - - endly = int(endYear / 4) - endly -= int(endYear / 100) - endly += int(endYear / 400) - - basely = int(baseYear / 4) - basely -= int(baseYear / 100) - basely += int(baseYear / 400) - - return (endly - basely) - -def LeapYearCount(year: int): - return LeapYearCountEx(year, False, 1, True) - -def IsLeapYear(year: int): - isLeap = False - if year % 4 == 0: - isLeap = True - if year % 100 == 0: - isLeap = False - if year % 400 == 0: - isLeap = True - return isLeap - -def DaysCount(year: int, month: int, day: int): - ly = LeapYearCountEx(year, False, 1, True) - days = 365 * (year - 1) - days += ly - - for index in range(1, month, 1): - days += MonthDayCount[index - 1] - - if (month > 2) and IsLeapYear(year): - days += 1 - - days += day - 1 - return days - -def DayOfWeek(year: int, month: int, day: int): - # as we know, 1/1/1900 is Monday. - # via this method, we can got 1/1/1 is Monday - # compute day span - days=DaysCount(year, month, day) - - # return day of week (from 0 - 6, corresponding with python) - return days % 7 - -def GetDayInMonth(year: int, month: int, day: int): - days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) - firstDayOfWeek = DayOfWeek(year, month, 1) - dayOfWeek = (firstDayOfWeek + day - 1) % 7 - - dayForwards = day - dayBackwards = days - day + 1 - - weeksForward = int((dayForwards - 1) / 7) + 1 - weeksBackwards = int((dayBackwards - 1) / 7) + 1 - - return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek) - -def GetMonthWeekStatistics(year: int, month: int): - days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) - firstDayOfWeek = DayOfWeek(year, month, 1) - lastDayOfWeek = (firstDayOfWeek + days - 1) % 7 - - result = [4, 4, 4, 4, 4, 4, 4] - remain = days % 7 - week = firstDayOfWeek - while remain > 0: - result[week % 7] += 1 - week += 1 - remain -= 1 - - return tuple(result) - -class UTCTimezone(datetime.tzinfo): - - __offset: int - - def __init__(self, offset: int = 0): - self.__offset = offset - - def utcoffset(self, dt): - return datetime.timedelta(minutes=self.__offset) - - def tzname(self, dt): - return 'UTC {}'.format(self.__offset) - - def dst(self, dt): - return datetime.timedelta(0) - \ No newline at end of file diff --git a/backend-legacy/logger.py b/backend-legacy/logger.py deleted file mode 100644 index a2ecd69..0000000 --- a/backend-legacy/logger.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -import enum - - -def _build_logger() -> tuple[logging.Logger, logging.Handler]: - # Create a new logger which is independent with Flask - logger = logging.getLogger("my_console_logger") - # Avoid message was propagated to root logger or captured by Flask logger. - logger.propagate = False - # Set initial level. - logger.setLevel(logging.INFO) - - # Create StreamHandler to output into stderr. - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - # Set format for it. - formatter = logging.Formatter("[%(levelname)s] %(message)s") - console_handler.setFormatter(formatter) - # Add handler - logger.addHandler(console_handler) - - return (logger, console_handler) - - -(LOGGER, CONSOLE_HANDLER) = _build_logger() - - -class LoggerLevel(enum.IntEnum): - DEBUG = enum.auto() - INFO = enum.auto() - - -def set_level(level: LoggerLevel) -> None: - logging_level: int = logging.INFO - match level: - case LoggerLevel.DEBUG: - logging_level = logging.DEBUG - case LoggerLevel.INFO: - logging_level = logging.INFO - - LOGGER.setLevel(logging_level) - CONSOLE_HANDLER.setLevel(logging_level) diff --git a/backend-legacy/pyproject.toml b/backend-legacy/pyproject.toml deleted file mode 100644 index 8e802f5..0000000 --- a/backend-legacy/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[project] -name = "coleaf-backend" -version = "1.1.0" -description = "The backend of coconut-leaf." -readme = "README.md" -requires-python = ">=3.11" -dependencies = [ - "flask==2.2.3", -] -[tool.uv] -constraint-dependencies = [ - "Werkzeug==2.2.2", - "MarkupSafe==2.1.5" -] \ No newline at end of file diff --git a/backend-legacy/server.py b/backend-legacy/server.py deleted file mode 100644 index 5084940..0000000 --- a/backend-legacy/server.py +++ /dev/null @@ -1,513 +0,0 @@ -from flask import Flask -from flask import request -from dataclasses import dataclass -from typing import Any, Callable, ParamSpec, TypeVar, Generic - -import config -import database -import utils -from logger import LOGGER -from database import ResponseBody - -app = Flask(__name__) -calendar_db = database.CalendarDatabase() - -# region: API Route - -# region: Common - - -@app.route("/common/salt", methods=["POST"]) -def api_common_saltHandle(): - return SmartDbCaller( - calendar_db.common_salt, (FormField("username", str, False),), None - ) - - -@app.route("/common/login", methods=["POST"]) -def api_common_loginHandle(): - clientInfo = FetchClientNetworkInfo() - - return SmartDbCaller( - calendar_db.common_login, - ( - FormField("username", str, False), - FormField("password", str, False), - FormField("clientUa", str, False), - FormField("clientIp", str, False), - ), - {"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr}, - ) - - -@app.route("/common/webLogin", methods=["POST"]) -def api_common_webLoginHandle(): - clientInfo = FetchClientNetworkInfo() - - return SmartDbCaller( - calendar_db.common_webLogin, - ( - FormField("username", str, False), - FormField("password", str, False), - FormField("clientUa", str, False), - FormField("clientIp", str, False), - ), - {"clientUa": clientInfo.user_agent, "clientIp": clientInfo.ip_addr}, - ) - - -@app.route("/common/logout", methods=["POST"]) -def api_common_logoutHandle(): - return SmartDbCaller( - calendar_db.common_logout, (FormField("token", str, False),), None - ) - - -@app.route("/common/tokenValid", methods=["POST"]) -def api_common_tokenValidHandle(): - return SmartDbCaller( - calendar_db.common_tokenValid, (FormField("token", str, False),), None - ) - - -# endregion - -# region: Calendar - - -@app.route("/calendar/getFull", methods=["POST"]) -def api_calendar_getFullHandle(): - return SmartDbCaller( - calendar_db.calendar_getFull, - ( - FormField("token", str, False), - FormField("startDateTime", int, False), - FormField("endDateTime", int, False), - ), - None, - ) - - -@app.route("/calendar/getList", methods=["POST"]) -def api_calendar_getListHandle(): - return SmartDbCaller( - calendar_db.calendar_getList, - ( - FormField("token", str, False), - FormField("startDateTime", int, False), - FormField("endDateTime", int, False), - ), - None, - ) - - -@app.route("/calendar/getDetail", methods=["POST"]) -def api_calendar_getDetailHandle(): - return SmartDbCaller( - calendar_db.calendar_getDetail, - (FormField("token", str, False), FormField("uuid", str, False)), - None, - ) - - -@app.route("/calendar/update", methods=["POST"]) -def api_calendar_updateHandle(): - return SmartDbCaller( - calendar_db.calendar_update, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("belongTo", str, True), - FormField("title", str, True), - FormField("description", str, True), - FormField("eventDateTimeStart", int, True), - FormField("eventDateTimeEnd", int, True), - FormField("loopRules", str, True), - FormField("timezoneOffset", int, True), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/calendar/add", methods=["POST"]) -def api_calendar_addHandle(): - return SmartDbCaller( - calendar_db.calendar_add, - ( - FormField("token", str, False), - FormField("belongTo", str, False), - FormField("title", str, False), - FormField("description", str, False), - FormField("eventDateTimeStart", int, False), - FormField("eventDateTimeEnd", int, False), - FormField("loopRules", str, False), - FormField("timezoneOffset", int, False), - ), - None, - ) - - -@app.route("/calendar/delete", methods=["POST"]) -def api_calendar_deleteHandle(): - return SmartDbCaller( - calendar_db.calendar_delete, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -# endregion - -# region: Collection - - -@app.route("/collection/getFullOwn", methods=["POST"]) -def api_collection_getFullOwnHandle(): - return SmartDbCaller( - calendar_db.collection_getFullOwn, (FormField("token", str, False),), None - ) - - -@app.route("/collection/getListOwn", methods=["POST"]) -def api_collection_getListOwnHandle(): - return SmartDbCaller( - calendar_db.collection_getListOwn, (FormField("token", str, False),), None - ) - - -@app.route("/collection/getDetailOwn", methods=["POST"]) -def api_collection_getDetailOwnHandle(): - return SmartDbCaller( - calendar_db.collection_getDetailOwn, - (FormField("token", str, False), FormField("uuid", str, False)), - None, - ) - - -@app.route("/collection/addOwn", methods=["POST"]) -def api_collection_addOwnHandle(): - return SmartDbCaller( - calendar_db.collection_addOwn, - (FormField("token", str, False), FormField("name", str, False)), - None, - ) - - -@app.route("/collection/updateOwn", methods=["POST"]) -def api_collection_updateOwnHandle(): - return SmartDbCaller( - calendar_db.collection_updateOwn, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("name", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/collection/deleteOwn", methods=["POST"]) -def api_collection_deleteOwnHandle(): - return SmartDbCaller( - calendar_db.collection_deleteOwn, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/collection/getSharing", methods=["POST"]) -def api_collection_getSharingHandle(): - return SmartDbCaller( - calendar_db.collection_getSharing, - (FormField("token", str, False), FormField("uuid", str, False)), - None, - ) - - -@app.route("/collection/deleteSharing", methods=["POST"]) -def api_collection_deleteSharingHandle(): - return SmartDbCaller( - calendar_db.collection_deleteSharing, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("target", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/collection/addSharing", methods=["POST"]) -def api_collection_addSharingHandle(): - return SmartDbCaller( - calendar_db.collection_addSharing, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("target", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/collection/getShared", methods=["POST"]) -def api_collection_getSharedHandle(): - return SmartDbCaller( - calendar_db.collection_getShared, (FormField("token", str, False),), None - ) - - -# endregion - -# region: Todo - - -@app.route("/todo/getFull", methods=["POST"]) -def api_todo_getFullHandle(): - return SmartDbCaller( - calendar_db.todo_getFull, (FormField("token", str, False),), None - ) - - -@app.route("/todo/getList", methods=["POST"]) -def api_todo_getListHandle(): - return SmartDbCaller( - calendar_db.todo_getList, (FormField("token", str, False),), None - ) - - -@app.route("/todo/getDetail", methods=["POST"]) -def api_todo_getDetailHandle(): - return SmartDbCaller( - calendar_db.todo_getDetail, - (FormField("token", str, False), FormField("uuid", str, False)), - None, - ) - - -@app.route("/todo/add", methods=["POST"]) -def api_todo_addHandle(): - return SmartDbCaller(calendar_db.todo_add, (FormField("token", str, False),), None) - - -@app.route("/todo/update", methods=["POST"]) -def api_todo_updateHandle(): - return SmartDbCaller( - calendar_db.todo_update, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("data", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -@app.route("/todo/delete", methods=["POST"]) -def api_todo_deleteHandle(): - return SmartDbCaller( - calendar_db.todo_delete, - ( - FormField("token", str, False), - FormField("uuid", str, False), - FormField("lastChange", str, False), - ), - None, - ) - - -# endregion - -# region: Admin - - -@app.route("/admin/get", methods=["POST"]) -def api_admin_getHandle(): - return SmartDbCaller(calendar_db.admin_get, (FormField("token", str, False),), None) - - -@app.route("/admin/add", methods=["POST"]) -def api_admin_addHandle(): - return SmartDbCaller( - calendar_db.admin_add, - (FormField("token", str, False), FormField("username", str, False)), - None, - ) - - -@app.route("/admin/update", methods=["POST"]) -def api_admin_updateHandle(): - return SmartDbCaller( - calendar_db.admin_update, - ( - FormField("token", str, False), - FormField("username", str, False), - FormField("password", str, True), - FormField("isAdmin", utils.Str2Bool, True), - ), - None, - ) - - -@app.route("/admin/delete", methods=["POST"]) -def api_admin_deleteHandle(): - return SmartDbCaller( - calendar_db.admin_delete, - (FormField("token", str, False), FormField("username", str, False)), - None, - ) - - -# endregion - -# region: Profile - - -@app.route("/profile/isAdmin", methods=["POST"]) -def api_profile_isAdminHandle(): - return SmartDbCaller( - calendar_db.profile_isAdmin, (FormField("token", str, False),), None - ) - - -@app.route("/profile/changePassword", methods=["POST"]) -def api_profile_changePasswordHandle(): - return SmartDbCaller( - calendar_db.profile_changePassword, - (FormField("token", str, False), FormField("password", str, False)), - None, - ) - - -@app.route("/profile/getToken", methods=["POST"]) -def api_profile_getTokenHandle(): - return SmartDbCaller( - calendar_db.profile_getToken, (FormField("token", str, False),), None - ) - - -@app.route("/profile/deleteToken", methods=["POST"]) -def api_profile_deleteTokenHandle(): - return SmartDbCaller( - calendar_db.profile_deleteToken, - (FormField("token", str, False), FormField("deleteToken", str, False)), - None, - ) - - -# endregion - -# endregion - -# region: Utilities - - -@dataclass(frozen=True) -class ClientNetworkInfo: - user_agent: str - """The user agent of client.""" - ip_addr: str - """The IP address of client.""" - - -def FetchClientNetworkInfo() -> ClientNetworkInfo: - clientUa = request.user_agent.string - forwardIpList = request.headers.getlist("X-Forwarded-For") - if forwardIpList: - clientIp = forwardIpList[0] - else: - directIp = request.remote_addr - if directIp is not None: - clientIp = directIp - else: - clientIp = "0.0.0.0" - - return ClientNetworkInfo(clientUa, clientIp) - - -@dataclass(frozen=True) -class FormField: - name: str - """The name of form field.""" - ty: Callable[[str], Any] - """The type of form field.""" - is_optional: bool - """True if this form field is optional, otherwise false.""" - - -def SmartDbCaller( - db_method: Callable[..., ResponseBody[Any]], - fields: tuple[FormField, ...], - padding_form: dict[str, str] | None, -) -> dict[str, Any]: - opt_param_counter = 0 - lost_required: bool = False - param_list: list[Any] = [] - opt_param_dict: dict[str, Any] = {} - - # fetch user passed form - user_form: dict[str, str] = request.form.to_dict() - LOGGER.debug(f"User Form: {user_form}") - # overwrite user form by our padding form - if padding_form is not None: - user_form.update(padding_form) - LOGGER.debug(f"Padded User Form: {user_form}") - - # check fields one by one - for field in fields: - value = user_form.get(field.name, None) - if value is not None: - value = field.ty(value) - - if field.is_optional: - # optional param - if value is not None: - opt_param_dict[field.name] = value - opt_param_counter += 1 - else: - # required param - if value is None: - lost_required = True - else: - param_list.append(value) - - # Only execute database function if there is no lost required fields. - # And fulfill one of following requirements: - # 1. There are all required fields (optional parameter count is zero). - # 1. Or, there is some optional parameter. - LOGGER.debug(f"Has Lost Required Parameter: {lost_required}") - LOGGER.debug(f"All Optional Parameter Count: {opt_param_counter}") - LOGGER.debug(f"Available Optional Parameter Count: {len(opt_param_dict)}") - result: ResponseBody[Any] - if lost_required == False and (opt_param_counter == 0 or len(opt_param_dict) != 0): - result = db_method(*param_list, **opt_param_dict) - else: - result = ResponseBody(False, "Invalid parameter", None) - - return ConstructResponseBody(result) - - -def ConstructResponseBody(body: ResponseBody[Any]) -> dict[str, Any]: - return {"success": body.success, "error": body.error, "data": body.data} - - -# endregion - - -def run(): - calendar_db.open() - app.run(port=config.get_config().web.port) - calendar_db.close() diff --git a/backend-legacy/sql/mysql.sql b/backend-legacy/sql/mysql.sql deleted file mode 100644 index e69de29..0000000 diff --git a/backend-legacy/sql/sqlite.sql b/backend-legacy/sql/sqlite.sql deleted file mode 100644 index e52b028..0000000 --- a/backend-legacy/sql/sqlite.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE TABLE user( - [name] TEXT NOT NULL, - [password] TEXT NOT NULL, - [is_admin] TINYINT NOT NULL CHECK(is_admin = 1 OR is_admin = 0), - [salt] INTEGER NOT NULL, - - PRIMARY KEY (name) -); - -CREATE TABLE token( - [user] TEXT NOT NULL, - [token] TEXT UNIQUE NOT NULL, - [token_expire_on] BIGINT NOT NULL, - [ua] TEXT NOT NULL, - [ip] TEXT NOT NULL, - - FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE -); - -CREATE TABLE collection( - [uuid] TEXT NOT NULL, - [name] TEXT NOT NULL, - [user] TEXT NOT NULL, - [last_change] TEXT NOT NULL, - - PRIMARY KEY (uuid), - FOREIGN KEY (user) REFERENCES user(name) ON DELETE CASCADE -); - -CREATE TABLE share( - [uuid] TEXT NOT NULL, - [target] TEXT NOT NULL, - - FOREIGN KEY (uuid) REFERENCES collection(uuid) ON DELETE CASCADE - FOREIGN KEY (target) REFERENCES user(name) ON DELETE CASCADE -); - -CREATE TABLE calendar( - [uuid] TEXT NOT NULL, - [belong_to] TEXT NOT NULL, - - [title] TEXT NOT NULL, - [description] TEXT NOT NULL, - [last_change] TEXT NOT NULL, - - [event_date_time_start] BIGINT NOT NULL, - [event_date_time_end] BIGINT NOT NULL, - [timezone_offset] INT NOT NULL, - - [loop_rules] TEXT NOT NULL, - [loop_date_time_start] BIGINT NOT NULL, - [loop_date_time_end] BIGINT NOT NULL, - - PRIMARY KEY (uuid), - FOREIGN KEY (belong_to) REFERENCES collection(uuid) ON DELETE CASCADE -); - -CREATE TABLE todo( - [uuid] TEXT NOT NULL, - [belong_to] TEXT NOT NULL, - - [data] TEXT NOT NULL, - [last_change] TEXT NOT NULL, - - PRIMARY KEY (uuid), - FOREIGN KEY (belong_to) REFERENCES user(name) ON DELETE CASCADE -); \ No newline at end of file diff --git a/backend-legacy/utils.py b/backend-legacy/utils.py deleted file mode 100644 index 1fc65c6..0000000 --- a/backend-legacy/utils.py +++ /dev/null @@ -1,64 +0,0 @@ -import hashlib -import random -import uuid -import time -import math -import re - -USERNAME_PATTERN: re.Pattern = re.compile("^[0-9A-Za-z]+$") -PASSWORD_PATTERN: re.Pattern = re.compile("^[!-~]+$") - - -def IsValidUsername(strl: str) -> bool: - return USERNAME_PATTERN.match(strl) is not None - - -def IsValidPassword(strl: str) -> bool: - return PASSWORD_PATTERN.match(strl) is not None - - -def ComputePasswordHash(password: str) -> str: - s = hashlib.sha256() - s.update(password.encode("utf-8")) - return s.hexdigest() - - -def GenerateUUID() -> str: - return str(uuid.uuid1()) - - -def GenerateToken(username: str) -> str: - s = hashlib.sha256() - s.update(username.encode("utf-8")) - s.update(GenerateUUID().encode("utf-8")) - return s.hexdigest() - - -def GenerateSalt() -> int: - return random.randint(0, 6172748) - - -def ComputePasswordHashWithSalt(passwordHashed: str, salt: int) -> str: - s = hashlib.sha256() - s.update((passwordHashed + str(salt)).encode("utf-8")) - return s.hexdigest() - - -def GetCurrentTimestamp() -> int: - return int(time.time()) - - -def GetTokenExpireOn() -> int: - return GetCurrentTimestamp() + 60 * 60 * 24 * 2 # add 2 day from now - - -def Str2Bool(strl: str) -> bool: - return strl.lower() == "true" - - -def GCD(a: int, b: int) -> int: - return math.gcd(a, b) - - -def LCM(a: int, b: int) -> int: - return (a * b) // GCD(a, b) diff --git a/backend-legacy/uv.lock b/backend-legacy/uv.lock deleted file mode 100644 index 6d824c3..0000000 --- a/backend-legacy/uv.lock +++ /dev/null @@ -1,117 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.11" - -[manifest] -constraints = [ - { name = "markupsafe", specifier = "==2.1.5" }, - { name = "werkzeug", specifier = "==2.2.2" }, -] - -[[package]] -name = "click" -version = "8.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, -] - -[[package]] -name = "coleaf-backend" -version = "1.1.0" -source = { virtual = "." } -dependencies = [ - { name = "flask" }, -] - -[package.metadata] -requires-dist = [{ name = "flask", specifier = "==2.2.3" }] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "flask" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e8/5c/ff9047989bd995b1098d14b03013f160225db2282925b517bb4a967752ee/Flask-2.2.3.tar.gz", hash = "sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d", size = 697599, upload-time = "2023-02-15T22:43:57.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl", hash = "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d", size = 101839, upload-time = "2023-02-15T22:43:55.501Z" }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markupsafe" -version = "2.1.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, - { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, - { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, - { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, - { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, - { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, - { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, - { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, - { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, - { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, - { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, - { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, -] - -[[package]] -name = "werkzeug" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/c1/1c8e539f040acd80f844c69a5ef8e2fccdf8b442dabb969e497b55d544e1/Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", size = 844378, upload-time = "2022-08-08T21:44:15.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/27/be6ddbcf60115305205de79c29004a0c6bc53cec814f733467b1bb89386d/Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5", size = 232700, upload-time = "2022-08-08T21:44:13.251Z" }, -]