1
0

refactor: refactor for modern layout

- split frontend and backend.
- update backend with modern Python dev strategies.
This commit is contained in:
2026-04-28 13:17:54 +08:00
parent 8e72e75a15
commit a0e3385670
65 changed files with 479 additions and 139 deletions

19
.gitignore vendored
View File

@@ -1,16 +1,3 @@
# ignore sqlite db ## ======== Personal ========
*.db # Ignore VSCode
.vscode/
# ignore my debug setting
*.cfg
# ignore py cache
src/__pycache__
# ignore any image first
*.png
*.jpg
*.gif
# elimate vscode
.vscode

19
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
## ======== 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

1
backend/.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

76
backend/coconut-leaf.py Normal file
View File

@@ -0,0 +1,76 @@
import sys
import logging
from argparse import ArgumentParser
from typing import cast
from pathlib import Path
import server
import config
import utils
import database
def GetUsernamePassword():
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__":
print("Coconut-leaf")
print("A self-host, multi-account calendar system.")
print("Project: https://github.com/yyc12345/coconut-leaf")
print("===================")
# Receive arguments
parser = ArgumentParser(description="Coconut-leaf")
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()
# Load config file
try:
config.setup_config(cast(Path, args.config))
except Exception as e:
print(f"Error loading config file: {e}")
sys.exit(1)
# Setup logging level
logging_level = logging.DEBUG if config.get_config().others.debug else logging.INFO
logging.basicConfig(format='[%(levelname)s] %(message)s', 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()
logging.info("Staring server...")
server.run()

View File

@@ -0,0 +1,22 @@
[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 = 8888
[others]
auto-token-clean-duration = 86400
debug = true

130
backend/config.py Normal file
View File

@@ -0,0 +1,130 @@
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

View File

@@ -1,13 +1,16 @@
import config import config
import sqlite3 import sqlite3
import json
import utils import utils
import threading import threading
import logging import logging
import dt import dt
from typing import cast
from pathlib import Path
def SafeDatabaseOperation(func): def SafeDatabaseOperation(func):
def wrapper(self, *args, **kwargs): def wrapper(self: 'CalendarDatabase', *args, **kwargs):
cfg = config.get_config()
with self.mutex: with self.mutex:
# check database and acquire cursor # check database and acquire cursor
try: try:
@@ -15,16 +18,16 @@ def SafeDatabaseOperation(func):
self.cursor = self.db.cursor() self.cursor = self.db.cursor()
except Exception as e: except Exception as e:
self.cursor = None self.cursor = None
if config.CustomConfig['debug']: if cfg.others.debug:
logging.exception(e) logging.exception(e)
return (False, str(e), None) return (False, str(e), None)
# do real data work # do real data work
try: try:
currentTime = utils.GetCurrentTimestamp() currentTime = utils.GetCurrentTimestamp()
if currentTime - self.latestClean > config.CustomConfig['auto-token-clean-duration']: if currentTime - self.latestClean > cfg.others.auto_token_clean_duration:
self.latestClean = currentTime self.latestClean = currentTime
print('Cleaning outdated token...') logging.info('Cleaning outdated token...')
self.tokenOper_clean() self.tokenOper_clean()
result = (True, '', func(self, *args, **kwargs)) result = (True, '', func(self, *args, **kwargs))
@@ -36,13 +39,19 @@ def SafeDatabaseOperation(func):
self.cursor.close() self.cursor.close()
self.cursor = None self.cursor = None
self.db.rollback() self.db.rollback()
if config.CustomConfig['debug']: if cfg.others.debug:
logging.exception(e) logging.exception(e)
return (False, str(e), None) return (False, str(e), None)
return wrapper return wrapper
class CalendarDatabase(object): class CalendarDatabase:
db: sqlite3.Connection
cursor: sqlite3.Cursor
mutex: threading.Lock
latestClean: int
def __init__(self): def __init__(self):
self.db = None self.db = None
self.cursor = None self.cursor = None
@@ -53,13 +62,15 @@ class CalendarDatabase(object):
if (self.is_database_valid()): if (self.is_database_valid()):
raise Exception('Databade is opened') raise Exception('Databade is opened')
if config.CustomConfig['database-type'] == 'sqlite': cfg = config.get_config()
self.db = sqlite3.connect(config.CustomConfig['database-config']['url'], check_same_thread = False) 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 encoding = "UTF-8";')
self.db.execute('PRAGMA foreign_keys = ON;') self.db.execute('PRAGMA foreign_keys = ON;')
elif config.CustomConfig['database-type'] == 'mysql': case config.DatabaseDriver.MYSQL:
raise Exception('Not implemented database') raise Exception('Not implemented database')
else: case _:
raise Exception('Unknow database type') raise Exception('Unknow database type')
def init(self, username, password): def init(self, username, password):
@@ -67,9 +78,20 @@ class CalendarDatabase(object):
raise Exception('Database is opened') raise Exception('Database is opened')
# establish tables # 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 Exception('Not implemented database')
case _:
raise Exception('Unknow database type')
self.open() self.open()
cursor = self.db.cursor() cursor = self.db.cursor()
with open('sql/sqlite.sql', 'r', encoding='utf-8') as fsql: with open(sql_file, 'r', encoding='utf-8') as fsql:
cursor.executescript(fsql.read()) cursor.executescript(fsql.read())
# finish init # finish init
@@ -272,11 +294,11 @@ class CalendarDatabase(object):
sqlList.append('[ccn_loopDateTimeStart] = ?') sqlList.append('[ccn_loopDateTimeStart] = ?')
argumentsList.append(analyseData[5]) argumentsList.append(analyseData[5])
sqlList.append('[ccn_loopDateTimeEnd] = ?') sqlList.append('[ccn_loopDateTimeEnd] = ?')
argumentsList.append(dt.ResolveLoopStr( argumentsList.append(str(dt.ResolveLoopStr(
analyseData[8], analyseData[8],
analyseData[5], analyseData[5],
analyseData[7] analyseData[7]
)) )))
# execute # execute
argumentsList.append(uuid) argumentsList.append(uuid)
@@ -531,8 +553,8 @@ class CalendarDatabase(object):
argumentsList.append(_username) argumentsList.append(_username)
self.cursor.execute('UPDATE user SET {} WHERE [ccn_name] = ?;'.format(', '.join(sqlList)), self.cursor.execute('UPDATE user SET {} WHERE [ccn_name] = ?;'.format(', '.join(sqlList)),
tuple(argumentsList)) tuple(argumentsList))
print(cache) logging.debug(cache)
print(tuple(argumentsList)) logging.debug(tuple(argumentsList))
if self.cursor.rowcount != 1: if self.cursor.rowcount != 1:
raise Exception('Fail to update due to no matched rows or too much rows.') raise Exception('Fail to update due to no matched rows or too much rows.')
return True return True

View File

@@ -1,6 +1,7 @@
import datetime import datetime
import time
import re import re
import logging
import typing
from functools import reduce from functools import reduce
import utils import utils
@@ -13,7 +14,9 @@ MAX_TIMESTAMP = int(MAX_DATETIME.timestamp() / 60)
DAY1_SPAN = 60 * 24 DAY1_SPAN = 60 * 24
DAY7_SPAN = 7 * DAY1_SPAN DAY7_SPAN = 7 * DAY1_SPAN
def ResolveLoopStr(strl, starttime, tzoffset): LoopHandle = typing.Callable[[re.Match, int, int, int], int]
def ResolveLoopStr(strl: str, starttime: int, tzoffset: int) -> int:
# check no loop # check no loop
if strl == '': if strl == '':
return starttime return starttime
@@ -40,7 +43,7 @@ def ResolveLoopStr(strl, starttime, tzoffset):
raise Exception('Invalid loopRules') raise Exception('Invalid loopRules')
def LoopHandle_Year(searchResult, starttime, times, tzoffset): def LoopHandle_Year(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset)) clientDate = datetime.datetime.fromtimestamp(starttime * 60, UTCTimezone(tzoffset))
isStrict = searchResult.group(1) == 'S' isStrict = searchResult.group(1) == 'S'
yearSpan = int(searchResult.group(2)) yearSpan = int(searchResult.group(2))
@@ -52,7 +55,7 @@ def LoopHandle_Year(searchResult, starttime, times, tzoffset):
if clientMonth == 2 and clientDay == 29: if clientMonth == 2 and clientDay == 29:
if isStrict: if isStrict:
realSpan = utils.LCM(yearSpan, 4) realSpan = utils.LCM(yearSpan, 4)
print(realSpan) logging.debug(realSpan)
valCache = starttime valCache = starttime
while valCache < MAX_TIMESTAMP and times > 0: while valCache < MAX_TIMESTAMP and times > 0:
newYear += realSpan newYear += realSpan
@@ -71,7 +74,7 @@ def LoopHandle_Year(searchResult, starttime, times, tzoffset):
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
def LoopHandle_Month(searchResult, starttime, times, tzoffset): def LoopHandle_Month(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
isStrict = searchResult.group(1) == 'S' isStrict = searchResult.group(1) == 'S'
loopType = searchResult.group(2) loopType = searchResult.group(2)
monthSpan = int(searchResult.group(3)) monthSpan = int(searchResult.group(3))
@@ -144,7 +147,7 @@ def LoopHandle_Month(searchResult, starttime, times, tzoffset):
val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay)) val = starttime + DAY1_SPAN * (DaysCount(newYear, newMonth, newDay) - DaysCount(clientYear, clientMonth, clientDay))
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
def LoopHandle_Week(searchResult, starttime, times, tzoffset): def LoopHandle_Week(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1))) weekOccupied = tuple(map(lambda x: x == 'T', searchResult.group(1)))
weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0) weekEventCount = reduce(lambda x, y: x + (1 if y else 0), weekOccupied, 0)
if weekEventCount == 0: if weekEventCount == 0:
@@ -170,25 +173,25 @@ def LoopHandle_Week(searchResult, starttime, times, tzoffset):
val -= 1 val -= 1
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
def LoopHandle_Day(searchResult, starttime, times, tzoffset): def LoopHandle_Day(searchResult: re.Match, starttime: int, times: int, tzoffset: int) -> int:
val = starttime + DAY1_SPAN * times * int(searchResult.group(1)) val = starttime + DAY1_SPAN * times * int(searchResult.group(1))
val -= 1 val -= 1
return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP return val if val < MAX_TIMESTAMP else MAX_TIMESTAMP
precompiledLoopRules = ( precompiledLoopRules: tuple[tuple[re.Pattern, LoopHandle], ...] = (
(re.compile(r'^Y([SR]{1})([1-9]\d*)$'), LoopHandle_Year), (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'^M([SR]{1})([ABCD]{1})([1-9]\d*)$'), LoopHandle_Month),
(re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week), (re.compile(r'^W([TF]{7})([1-9]\d*)$'), LoopHandle_Week),
(re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day) (re.compile(r'^D([1-9]\d*)$'), LoopHandle_Day)
) )
precompiledLoopStopRules = { precompiledLoopStopRules: dict[str, re.Pattern] = {
'infinity': re.compile(r'^F$'), 'infinity': re.compile(r'^F$'),
'datetime': re.compile(r'^D([1-9]\d*|0)$'), 'datetime': re.compile(r'^D([1-9]\d*|0)$'),
'times': re.compile(r'^T([1-9]\d*)$') 'times': re.compile(r'^T([1-9]\d*)$')
} }
def LeapYearCountEx(endYear, includeThis = False, baseYear = 1, includeBase = True): def LeapYearCountEx(endYear: int, includeThis: bool = False, baseYear: int = 1, includeBase: bool = True):
if not includeThis: if not includeThis:
endYear -= 1 endYear -= 1
if includeBase: if includeBase:
@@ -204,10 +207,10 @@ def LeapYearCountEx(endYear, includeThis = False, baseYear = 1, includeBase = Tr
return (endly - basely) return (endly - basely)
def LeapYearCount(year): def LeapYearCount(year: int):
return LeapYearCountEx(year, False, 1, True) return LeapYearCountEx(year, False, 1, True)
def IsLeapYear(year): def IsLeapYear(year: int):
isLeap = False isLeap = False
if year % 4 == 0: if year % 4 == 0:
isLeap = True isLeap = True
@@ -217,7 +220,7 @@ def IsLeapYear(year):
isLeap = True isLeap = True
return isLeap return isLeap
def DaysCount(year, month, day): def DaysCount(year: int, month: int, day: int):
ly = LeapYearCountEx(year, False, 1, True) ly = LeapYearCountEx(year, False, 1, True)
days = 365 * (year - 1) days = 365 * (year - 1)
days += ly days += ly
@@ -231,7 +234,7 @@ def DaysCount(year, month, day):
days += day - 1 days += day - 1
return days return days
def DayOfWeek(year, month, day): def DayOfWeek(year: int, month: int, day: int):
# as we know, 1/1/1900 is Monday. # as we know, 1/1/1900 is Monday.
# via this method, we can got 1/1/1 is Monday # via this method, we can got 1/1/1 is Monday
# compute day span # compute day span
@@ -240,7 +243,7 @@ def DayOfWeek(year, month, day):
# return day of week (from 0 - 6, corresponding with python) # return day of week (from 0 - 6, corresponding with python)
return days % 7 return days % 7
def GetDayInMonth(year, month, day): def GetDayInMonth(year: int, month: int, day: int):
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
firstDayOfWeek = DayOfWeek(year, month, 1) firstDayOfWeek = DayOfWeek(year, month, 1)
dayOfWeek = (firstDayOfWeek + day - 1) % 7 dayOfWeek = (firstDayOfWeek + day - 1) % 7
@@ -253,7 +256,7 @@ def GetDayInMonth(year, month, day):
return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek) return (dayForwards, dayBackwards, weeksForward, dayOfWeek, weeksBackwards, dayOfWeek)
def GetMonthWeekStatistics(year, month): def GetMonthWeekStatistics(year: int, month: int):
days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0) days = MonthDayCount[month - 1] + (1 if (month == 2 and IsLeapYear(year)) else 0)
firstDayOfWeek = DayOfWeek(year, month, 1) firstDayOfWeek = DayOfWeek(year, month, 1)
lastDayOfWeek = (firstDayOfWeek + days - 1) % 7 lastDayOfWeek = (firstDayOfWeek + days - 1) % 7
@@ -269,14 +272,18 @@ def GetMonthWeekStatistics(year, month):
return tuple(result) return tuple(result)
class UTCTimezone(datetime.tzinfo): class UTCTimezone(datetime.tzinfo):
def __init__(self, offset = 0):
self._offset = offset __offset: int
def __init__(self, offset: int = 0):
self.__offset = offset
def utcoffset(self, dt): def utcoffset(self, dt):
return datetime.timedelta(minutes=self._offset) return datetime.timedelta(minutes=self.__offset)
def tzname(self, dt): def tzname(self, dt):
return 'UTC {}'.format(self._offset) return 'UTC {}'.format(self.__offset)
def dst(self, dt): def dst(self, dt):
return datetime.timedelta(0) return datetime.timedelta(0)

14
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[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"
]

View File

@@ -1,39 +1,45 @@
from flask import Flask from flask import Flask
from flask import g # from flask import g
from flask import render_template from flask import render_template
from flask import url_for from flask import url_for
from flask import request from flask import request
from flask import abort # from flask import abort
from flask import redirect from flask import redirect
from functools import reduce # from functools import reduce
import json # import json
import os # import os
import config import config
import database import database
import utils import utils
from pathlib import Path
app = Flask(__name__) _FRONTEND_PATH = Path(__file__).resolve().parent.parent / "frontend"
app = Flask(
__name__,
static_folder=_FRONTEND_PATH / "static",
template_folder=_FRONTEND_PATH / "templates",
)
calendar_db = database.CalendarDatabase() calendar_db = database.CalendarDatabase()
# render_static_resources = None # render_static_resources = None
# =============================================database # =============================================database
'''
def get_database():
db = getattr(g, '_database', None)
if db is None:
db = database.CalendarDatabase()
db.open()
return db
@app.teardown_appcontext # def get_database():
def close_database(exception): # db = getattr(g, '_database', None)
db = getattr(g, '_database', None) # if db is None:
if db is not None: # db = database.CalendarDatabase()
db.close() # db.open()
''' # return db
# @app.teardown_appcontext
# def close_database(exception):
# db = getattr(g, '_database', None)
# if db is not None:
# db.close()
# ============================================= static page route # ============================================= static page route
@@ -216,7 +222,7 @@ def api_collection_getFullOwnHandle():
@app.route('/api/collection/getListOwn', methods=['POST']) @app.route('/api/collection/getListOwn', methods=['POST'])
def api_collection_getListOwnHandle(): def api_collection_getListOwnHandle():
return SmartDbCaller(calendar_db.collection_getListlOwn, return SmartDbCaller(calendar_db.collection_getListOwn,
(('token', str, False), ), (('token', str, False), ),
None) None)
@@ -446,6 +452,6 @@ def ConstructResponseBody(returnedTuple):
def run(): def run():
calendar_db.open() calendar_db.open()
app.run(port=config.CustomConfig['web']['port']) app.run(port=config.get_config().web.port)
calendar_db.close() calendar_db.close()

0
backend/sql/mysql.sql Normal file
View File

117
backend/uv.lock generated Normal file
View File

@@ -0,0 +1,117 @@
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" },
]

View File

@@ -1,54 +0,0 @@
import os
import sys
import getopt
import server
import config
import utils
import database
def GetUsernamePassword():
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)
print('Coconut-leaf')
print('A self-host, multi-account calendar system.')
print('Project: https://github.com/yyc12345/coconut-leaf')
print('===================')
# process args
# preset init value
need_init = False
try:
opts, args = getopt.getopt(sys.argv[1:], "hi")
except getopt.GetoptError:
print('Wrong arguments!')
print('python coconut-leaf.py [-i] [-h]')
sys.exit(1)
for opt, arg in opts:
if opt == '-h':
print('python coconut-leaf.py [-i]')
sys.exit(0)
elif opt == '-i':
need_init = True
if need_init:
gotten_data = GetUsernamePassword()
calendar = database.CalendarDatabase()
calendar.init(*gotten_data)
calendar.close()
print('Staring server...')
server.run()

View File

@@ -1,7 +0,0 @@
import json
CustomConfig = None
# read cfg
with open('config.cfg', 'r', encoding='utf-8') as f:
CustomConfig = json.load(f)