mirror of
https://github.com/Casvt/MIND.git
synced 2026-04-03 03:00:22 -04:00
Added support for cron schedules in backend (#83)
Fully functioning backend (and API) support for cron schedules. Frontend is coming later.
This commit is contained in:
@@ -7,6 +7,7 @@ repos:
|
||||
additional_dependencies: [
|
||||
apprise ~= 1.4,
|
||||
python-dateutil ~= 2.8,
|
||||
cron-converter ~= 1.2,
|
||||
Flask ~= 3.0,
|
||||
waitress ~= 2.1
|
||||
]
|
||||
@@ -22,6 +23,7 @@ repos:
|
||||
|
||||
apprise ~= 1.4,
|
||||
python-dateutil ~= 2.8,
|
||||
cron-converter ~= 1.2,
|
||||
Flask ~= 3.0,
|
||||
waitress ~= 2.1
|
||||
]
|
||||
@@ -34,6 +36,7 @@ repos:
|
||||
additional_dependencies: [
|
||||
apprise ~= 1.4,
|
||||
python-dateutil ~= 2.8,
|
||||
cron-converter ~= 1.2,
|
||||
Flask ~= 3.0,
|
||||
waitress ~= 2.1
|
||||
]
|
||||
@@ -47,6 +50,7 @@ repos:
|
||||
additional_dependencies: [
|
||||
apprise ~= 1.4,
|
||||
python-dateutil ~= 2.8,
|
||||
cron-converter ~= 1.2,
|
||||
Flask ~= 3.0,
|
||||
waitress ~= 2.1
|
||||
]
|
||||
|
||||
@@ -248,6 +248,7 @@ class ReminderData(GeneralReminderData):
|
||||
repeat_quantity: Union[str, None]
|
||||
repeat_interval: Union[int, None]
|
||||
_weekdays: Union[str, None]
|
||||
cron_schedule: Union[str, None]
|
||||
enabled: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import (Any, Callable, Generator, Iterable,
|
||||
List, Sequence, Set, Tuple, Union, cast)
|
||||
|
||||
from apprise import Apprise, LogCapture
|
||||
from cron_converter import Cron
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from backend.base.definitions import (WEEKDAY_NUMBER, GeneralReminderData,
|
||||
@@ -240,7 +241,8 @@ def find_next_time(
|
||||
original_time: int,
|
||||
repeat_quantity: Union[RepeatQuantity, None],
|
||||
repeat_interval: Union[int, None],
|
||||
weekdays: Union[List[WEEKDAY_NUMBER], None]
|
||||
weekdays: Union[List[WEEKDAY_NUMBER], None],
|
||||
cron_schedule: Union[str, None]
|
||||
) -> int:
|
||||
"""Calculate the next timestep based on original time and repeat/interval
|
||||
values.
|
||||
@@ -256,6 +258,8 @@ def find_next_time(
|
||||
weekdays (Union[List[WEEKDAY_NUMBER], None]): If set, on which days the
|
||||
time can continue. Monday is 0, Sunday is 6.
|
||||
|
||||
cron_schedule (Union[str, None]): If set, the cron schedule to follow.
|
||||
|
||||
Returns:
|
||||
int: The next timestamp in the future.
|
||||
"""
|
||||
@@ -266,7 +270,14 @@ def find_next_time(
|
||||
original_datetime = datetime.fromtimestamp(original_time)
|
||||
new_time = datetime.fromtimestamp(original_time)
|
||||
|
||||
if (
|
||||
if cron_schedule is not None:
|
||||
cron_instance = Cron(cron_schedule)
|
||||
schedule = cron_instance.schedule(current_time)
|
||||
new_time = schedule.next()
|
||||
while new_time <= current_time:
|
||||
new_time = schedule.next()
|
||||
|
||||
elif (
|
||||
repeat_quantity is not None
|
||||
and repeat_interval is not None
|
||||
):
|
||||
|
||||
@@ -61,8 +61,9 @@ class ReminderHandler(metaclass=Singleton):
|
||||
|
||||
elif (
|
||||
reminder.repeat_quantity,
|
||||
reminder.weekdays
|
||||
) == (None, None):
|
||||
reminder.weekdays,
|
||||
reminder.cron_schedule
|
||||
) == (None, None, None):
|
||||
# Delete the reminder from the database
|
||||
self.reminder_db.delete(reminder.id)
|
||||
|
||||
@@ -75,7 +76,8 @@ class ReminderHandler(metaclass=Singleton):
|
||||
lambda q: RepeatQuantity(q)
|
||||
),
|
||||
reminder.repeat_interval,
|
||||
reminder.weekdays
|
||||
reminder.weekdays,
|
||||
reminder.cron_schedule
|
||||
)
|
||||
|
||||
self.reminder_db.update(reminder.id, new_time)
|
||||
|
||||
@@ -57,6 +57,7 @@ class Reminder:
|
||||
repeat_quantity: Union[None, RepeatQuantity] = None,
|
||||
repeat_interval: Union[None, int] = None,
|
||||
weekdays: Union[None, List[WEEKDAY_NUMBER]] = None,
|
||||
cron_schedule: Union[None, str] = None,
|
||||
color: Union[None, str] = None,
|
||||
enabled: Union[None, bool] = None
|
||||
) -> ReminderData:
|
||||
@@ -89,6 +90,10 @@ class Reminder:
|
||||
indexes of the days of the week that the reminder should run.
|
||||
Defaults to None.
|
||||
|
||||
cron_schedule (Union[None, str], optional): The new cron schedule
|
||||
that the reminder should run on.
|
||||
Defaults to None.
|
||||
|
||||
color (Union[None, str], optional): The new hex code of the color
|
||||
of the reminder, which is shown in the web-ui.
|
||||
Defaults to None.
|
||||
@@ -112,11 +117,18 @@ class Reminder:
|
||||
LOGGER.info(
|
||||
f'Updating notification service {self.id}: '
|
||||
+ f'{title=}, {time=}, {notification_services=}, {text=}, '
|
||||
+ f'{repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}, '
|
||||
+ f"{enabled=}"
|
||||
+ f'{repeat_quantity=}, {repeat_interval=}, '
|
||||
+ f'{weekdays=}, {cron_schedule=}, '
|
||||
+ f'{color=}, {enabled=}'
|
||||
)
|
||||
|
||||
# Validate data
|
||||
if cron_schedule is not None and (
|
||||
repeat_quantity is not None
|
||||
or repeat_interval is not None
|
||||
or weekdays is not None
|
||||
):
|
||||
raise InvalidKeyValue('cron_schedule', cron_schedule)
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
@@ -127,6 +139,7 @@ class Reminder:
|
||||
repeated_reminder = (
|
||||
(repeat_quantity is not None and repeat_interval is not None)
|
||||
or weekdays is not None
|
||||
or cron_schedule is not None
|
||||
)
|
||||
|
||||
if time is not None:
|
||||
@@ -156,13 +169,18 @@ class Reminder:
|
||||
weekdays,
|
||||
lambda w: ",".join(map(str, sorted(w)))
|
||||
),
|
||||
'cron_schedule': cron_schedule,
|
||||
'color': color,
|
||||
'notification_services': notification_services,
|
||||
'enabled': enabled
|
||||
}
|
||||
for k, v in new_values.items():
|
||||
if (
|
||||
k in ('repeat_quantity', 'repeat_interval', 'weekdays', 'color')
|
||||
k in (
|
||||
'repeat_quantity', 'repeat_interval',
|
||||
'weekdays', 'cron_schedule',
|
||||
'color'
|
||||
)
|
||||
or v is not None
|
||||
):
|
||||
data[k] = v
|
||||
@@ -172,7 +190,8 @@ class Reminder:
|
||||
data["time"],
|
||||
repeat_quantity,
|
||||
data["repeat_interval"],
|
||||
weekdays
|
||||
weekdays,
|
||||
cron_schedule
|
||||
)
|
||||
self.reminder_db.update(
|
||||
self.id,
|
||||
@@ -182,6 +201,7 @@ class Reminder:
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["weekdays"],
|
||||
data["cron_schedule"],
|
||||
data["time"],
|
||||
data["color"],
|
||||
data["notification_services"],
|
||||
@@ -198,6 +218,7 @@ class Reminder:
|
||||
data["repeat_quantity"],
|
||||
data["repeat_interval"],
|
||||
data["weekdays"],
|
||||
data["cron_schedule"],
|
||||
data["original_time"],
|
||||
data["color"],
|
||||
data["notification_services"],
|
||||
@@ -290,6 +311,7 @@ class Reminders:
|
||||
repeat_quantity: Union[None, RepeatQuantity] = None,
|
||||
repeat_interval: Union[None, int] = None,
|
||||
weekdays: Union[None, List[WEEKDAY_NUMBER]] = None,
|
||||
cron_schedule: Union[None, str] = None,
|
||||
color: Union[None, str] = None,
|
||||
enabled: bool = True
|
||||
) -> Reminder:
|
||||
@@ -318,6 +340,10 @@ class Reminders:
|
||||
of the days of the week that the reminder should run.
|
||||
Defaults to None.
|
||||
|
||||
cron_schedule (Union[None, str], optional): The cron schedule that
|
||||
the reminder should run on.
|
||||
Defaults to None.
|
||||
|
||||
color (Union[None, str], optional): The hex code of the color of the
|
||||
reminder, which is shown in the web-ui.
|
||||
Defaults to None.
|
||||
@@ -328,7 +354,7 @@ class Reminders:
|
||||
|
||||
Note about args:
|
||||
Either repeat_quantity and repeat_interval are given,
|
||||
weekdays is given or neither, but not both.
|
||||
weekdays is given, cron_schedule is given or none.
|
||||
|
||||
Raises:
|
||||
NotificationServiceNotFound: One of the notification services was
|
||||
@@ -341,14 +367,20 @@ class Reminders:
|
||||
"""
|
||||
LOGGER.info(
|
||||
f'Adding reminder with {title=}, {time=}, {notification_services=}, ' +
|
||||
f'{text=}, {repeat_quantity=}, {repeat_interval=}, {weekdays=}, {color=}' +
|
||||
f'{enabled=}')
|
||||
f'{text=}, {repeat_quantity=}, {repeat_interval=}, {weekdays=}, ' +
|
||||
f'{cron_schedule=}, {color=}, {enabled=}')
|
||||
|
||||
# Validate data
|
||||
if time < datetime.utcnow().timestamp():
|
||||
raise InvalidTime(time)
|
||||
time = round(time)
|
||||
|
||||
if cron_schedule is not None and (
|
||||
repeat_quantity is not None
|
||||
or repeat_interval is not None
|
||||
or weekdays is not None
|
||||
):
|
||||
raise InvalidKeyValue('cron_schedule', cron_schedule)
|
||||
if repeat_quantity is None and repeat_interval is not None:
|
||||
raise InvalidKeyValue('repeat_quantity', repeat_quantity)
|
||||
elif repeat_quantity is not None and repeat_interval is None:
|
||||
@@ -365,13 +397,14 @@ class Reminders:
|
||||
NotificationService(self.user_id, ns)
|
||||
|
||||
# Prepare args
|
||||
if any((repeat_quantity, weekdays)):
|
||||
if any((repeat_quantity, weekdays, cron_schedule)):
|
||||
original_time = time
|
||||
time = find_next_time(
|
||||
original_time,
|
||||
repeat_quantity,
|
||||
repeat_interval,
|
||||
weekdays
|
||||
weekdays,
|
||||
cron_schedule
|
||||
)
|
||||
else:
|
||||
original_time = None
|
||||
@@ -390,6 +423,7 @@ class Reminders:
|
||||
time, repeat_quantity_str,
|
||||
repeat_interval,
|
||||
weekdays_str,
|
||||
cron_schedule,
|
||||
original_time,
|
||||
color,
|
||||
notification_services,
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Union
|
||||
from backend.base.definitions import Constants
|
||||
from backend.base.helpers import Singleton
|
||||
from backend.base.logging import LOGGER
|
||||
from backend.features.reminder_handler import ReminderHandler
|
||||
from backend.internals.db_models import UserlessRemindersDB
|
||||
from backend.internals.server import Server
|
||||
from backend.internals.settings import Settings
|
||||
@@ -72,6 +73,7 @@ class TimezoneChangeHandler(metaclass=Singleton):
|
||||
offset=shift_delta
|
||||
)
|
||||
settings.update({"measured_timezone": current_timezone})
|
||||
ReminderHandler().find_next_reminder()
|
||||
LOGGER.info(
|
||||
"Detected timezone/DST change (%s to %s), shifted reminders",
|
||||
measured_timezone, current_timezone
|
||||
|
||||
@@ -311,6 +311,7 @@ def setup_db() -> None:
|
||||
repeat_interval INTEGER,
|
||||
original_time INTEGER,
|
||||
weekdays VARCHAR(13),
|
||||
cron_schedule VARCHAR(255),
|
||||
|
||||
color VARCHAR(7),
|
||||
enabled BOOL NOT NULL DEFAULT 1,
|
||||
|
||||
@@ -334,3 +334,52 @@ class MigrateSetDBBackupFolder(DBMigrator):
|
||||
s.update({"db_backup_folder": SettingsValues.db_backup_folder})
|
||||
|
||||
return
|
||||
|
||||
|
||||
class MigrateAddCronScheduleColumn(DBMigrator):
|
||||
start_version = 12
|
||||
|
||||
def run(self) -> None:
|
||||
# V12 -> V13
|
||||
|
||||
from backend.internals.db import get_db
|
||||
|
||||
get_db().executescript("""
|
||||
BEGIN TRANSACTION;
|
||||
PRAGMA defer_foreign_keys = ON;
|
||||
|
||||
CREATE TEMPORARY TABLE temp_reminders_13 AS
|
||||
SELECT * FROM reminders;
|
||||
DROP TABLE reminders;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reminders(
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT,
|
||||
time INTEGER NOT NULL,
|
||||
|
||||
repeat_quantity VARCHAR(15),
|
||||
repeat_interval INTEGER,
|
||||
original_time INTEGER,
|
||||
weekdays VARCHAR(13),
|
||||
cron_schedule VARCHAR(255),
|
||||
|
||||
color VARCHAR(7),
|
||||
enabled BOOL NOT NULL DEFAULT 1,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
INSERT INTO reminders
|
||||
SELECT
|
||||
id, user_id,
|
||||
title, text, time,
|
||||
repeat_quantity, repeat_interval,
|
||||
original_time, weekdays,
|
||||
NULL AS cron_schedule,
|
||||
color, enabled
|
||||
FROM temp_reminders_13;
|
||||
|
||||
COMMIT;
|
||||
""")
|
||||
|
||||
@@ -547,7 +547,8 @@ class RemindersDB:
|
||||
id, title, text, color,
|
||||
time, original_time,
|
||||
repeat_quantity, repeat_interval,
|
||||
weekdays AS _weekdays, enabled
|
||||
weekdays AS _weekdays, cron_schedule,
|
||||
enabled
|
||||
FROM reminders
|
||||
WHERE user_id = :user_id
|
||||
{id_filter};
|
||||
@@ -574,6 +575,7 @@ class RemindersDB:
|
||||
repeat_quantity: Union[str, None],
|
||||
repeat_interval: Union[int, None],
|
||||
weekdays: Union[str, None],
|
||||
cron_schedule: Union[str, None],
|
||||
original_time: Union[int, None],
|
||||
color: Union[str, None],
|
||||
notification_services: List[int],
|
||||
@@ -585,7 +587,7 @@ class RemindersDB:
|
||||
title, text,
|
||||
time,
|
||||
repeat_quantity, repeat_interval,
|
||||
weekdays,
|
||||
weekdays, cron_schedule,
|
||||
original_time,
|
||||
color,
|
||||
enabled
|
||||
@@ -595,7 +597,7 @@ class RemindersDB:
|
||||
:title, :text,
|
||||
:time,
|
||||
:rq, :ri,
|
||||
:wd,
|
||||
:wd, :cs,
|
||||
:ot,
|
||||
:color,
|
||||
:enabled
|
||||
@@ -609,6 +611,7 @@ class RemindersDB:
|
||||
"rq": repeat_quantity,
|
||||
"ri": repeat_interval,
|
||||
"wd": weekdays,
|
||||
"cs": cron_schedule,
|
||||
"ot": original_time,
|
||||
"color": color,
|
||||
"enabled": enabled
|
||||
@@ -630,6 +633,7 @@ class RemindersDB:
|
||||
repeat_quantity: Union[str, None],
|
||||
repeat_interval: Union[int, None],
|
||||
weekdays: Union[str, None],
|
||||
cron_schedule: Union[str, None],
|
||||
original_time: Union[int, None],
|
||||
color: Union[str, None],
|
||||
notification_services: List[int],
|
||||
@@ -644,6 +648,7 @@ class RemindersDB:
|
||||
repeat_quantity = :rq,
|
||||
repeat_interval = :ri,
|
||||
weekdays = :wd,
|
||||
cron_schedule = :cs,
|
||||
original_time = :ot,
|
||||
color = :color,
|
||||
enabled = :enabled
|
||||
@@ -656,6 +661,7 @@ class RemindersDB:
|
||||
"rq": repeat_quantity,
|
||||
"ri": repeat_interval,
|
||||
"wd": weekdays,
|
||||
"cs": cron_schedule,
|
||||
"ot": original_time,
|
||||
"color": color,
|
||||
"enabled": enabled,
|
||||
@@ -731,7 +737,8 @@ class UserlessRemindersDB:
|
||||
title, text, color,
|
||||
time, original_time,
|
||||
repeat_quantity, repeat_interval,
|
||||
weekdays AS _weekdays, enabled
|
||||
weekdays AS _weekdays, cron_schedule,
|
||||
enabled
|
||||
FROM reminders
|
||||
{time_filter};
|
||||
""",
|
||||
@@ -757,6 +764,7 @@ class UserlessRemindersDB:
|
||||
repeat_quantity: Union[str, None],
|
||||
repeat_interval: Union[int, None],
|
||||
weekdays: Union[str, None],
|
||||
cron_schedule: Union[str, None],
|
||||
original_time: Union[int, None],
|
||||
color: Union[str, None],
|
||||
notification_services: List[int],
|
||||
@@ -769,6 +777,7 @@ class UserlessRemindersDB:
|
||||
time,
|
||||
repeat_quantity, repeat_interval,
|
||||
weekdays,
|
||||
cron_schedule,
|
||||
original_time,
|
||||
color,
|
||||
enabled
|
||||
@@ -779,6 +788,7 @@ class UserlessRemindersDB:
|
||||
:time,
|
||||
:rq, :ri,
|
||||
:wd,
|
||||
:cs,
|
||||
:ot,
|
||||
:color,
|
||||
:enabled
|
||||
@@ -792,6 +802,7 @@ class UserlessRemindersDB:
|
||||
"rq": repeat_quantity,
|
||||
"ri": repeat_interval,
|
||||
"wd": weekdays,
|
||||
"cs": cron_schedule,
|
||||
"ot": original_time,
|
||||
"color": color,
|
||||
"enabled": enabled
|
||||
|
||||
@@ -308,6 +308,7 @@ def api_reminders_list(inputs: Dict[str, Any]):
|
||||
repeat_quantity=inputs['repeat_quantity'],
|
||||
repeat_interval=inputs['repeat_interval'],
|
||||
weekdays=inputs['weekdays'],
|
||||
cron_schedule=inputs['cron_schedule'],
|
||||
color=inputs['color'],
|
||||
enabled=inputs['enabled']
|
||||
)
|
||||
@@ -347,7 +348,6 @@ def api_get_reminder(inputs: Dict[str, Any], r_id: int):
|
||||
return return_api(result.todict())
|
||||
|
||||
elif request.method == 'PUT':
|
||||
print(inputs)
|
||||
result = reminders.fetchone(r_id).update(
|
||||
title=inputs['title'],
|
||||
time=inputs['time'],
|
||||
@@ -356,6 +356,7 @@ def api_get_reminder(inputs: Dict[str, Any], r_id: int):
|
||||
repeat_quantity=inputs['repeat_quantity'],
|
||||
repeat_interval=inputs['repeat_interval'],
|
||||
weekdays=inputs['weekdays'],
|
||||
cron_schedule=inputs['cron_schedule'],
|
||||
color=inputs['color'],
|
||||
enabled=inputs['enabled']
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ from re import compile
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Type, Union
|
||||
|
||||
from apprise import Apprise
|
||||
from cron_converter import Cron
|
||||
from flask import Blueprint, Request, request
|
||||
|
||||
from backend.base.custom_exceptions import (AccessUnauthorized,
|
||||
@@ -295,6 +296,26 @@ class WeekDaysVariable(NonRequiredInputVariable):
|
||||
)
|
||||
|
||||
|
||||
class CronScheduleVariable(NonRequiredInputVariable):
|
||||
name = "cron_schedule"
|
||||
description = "The cron schedule that the reminder should run on"
|
||||
|
||||
def validate(self) -> bool:
|
||||
if self.value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(self.value, str):
|
||||
return False
|
||||
|
||||
try:
|
||||
Cron(self.value)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ColorVariable(NonRequiredInputVariable):
|
||||
name = "color"
|
||||
description = "The hex code of the color of the entry, which is shown in the web-UI"
|
||||
@@ -588,6 +609,7 @@ class RemindersData(EndpointData):
|
||||
RepeatQuantityVariable,
|
||||
RepeatIntervalVariable,
|
||||
WeekDaysVariable,
|
||||
CronScheduleVariable,
|
||||
ColorVariable,
|
||||
EnabledVariable
|
||||
]
|
||||
@@ -621,6 +643,7 @@ class ReminderData(EndpointData):
|
||||
RepeatQuantityVariable,
|
||||
RepeatIntervalVariable,
|
||||
WeekDaysVariable,
|
||||
CronScheduleVariable,
|
||||
ColorVariable,
|
||||
EditEnabledVariable
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
apprise ~= 1.4
|
||||
python-dateutil ~= 2.8
|
||||
cron-converter ~= 1.2
|
||||
Flask ~= 3.0
|
||||
waitress ~= 2.1
|
||||
|
||||
Reference in New Issue
Block a user