From 3d12fe7b5e7ae6777fd9fc4d7a54ffc878922b3d Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 1 Aug 2024 17:25:04 +0200 Subject: [PATCH] create update schedule through view --- tubearchivist/task/src/config_schedule.py | 112 +++++++++++++++------- tubearchivist/task/urls.py | 2 +- tubearchivist/task/views.py | 38 ++++++-- 3 files changed, 111 insertions(+), 41 deletions(-) diff --git a/tubearchivist/task/src/config_schedule.py b/tubearchivist/task/src/config_schedule.py index 9c3adfcd..4111d41b 100644 --- a/tubearchivist/task/src/config_schedule.py +++ b/tubearchivist/task/src/config_schedule.py @@ -6,6 +6,7 @@ Functionality: from datetime import datetime from appsettings.src.config import AppConfig +from celery.schedules import crontab from common.src.env_settings import EnvironmentSettings from django.utils import dateformat from django_celery_beat.models import CrontabSchedule @@ -24,33 +25,24 @@ class ScheduleBuilder: "run_backup": "0 18 0", "version_check": "0 11 *", } - CONFIG = { - "check_reindex_days": "check_reindex", - "run_backup_rotate": "run_backup", - "update_subscribed_notify": "update_subscribed", - "download_pending_notify": "download_pending", - "check_reindex_notify": "check_reindex", - } MSG = "message:setting" def __init__(self): self.config = AppConfig().config - def update_schedule_conf(self, form_post): - """process form post, schedules need to be validated before""" - for key, value in form_post.items(): - if not value: - continue + def update_schedule( + self, task_name: str, cron_schedule: str, schedule_conf: dict | None + ) -> None: + """update schedule""" + if cron_schedule == "auto": + cron_schedule = self.SCHEDULES[task_name] - if key in self.SCHEDULES: - if value == "auto": - value = self.SCHEDULES.get(key) + if cron_schedule: + _ = self.get_set_task(task_name, cron_schedule) - _ = self.get_set_task(key, value) - continue - - if key in self.CONFIG: - self.set_config(key, value) + if schedule_conf: + for key, value in schedule_conf.items(): + self.set_config(task_name, key, value) def get_set_task(self, task_name, schedule=False): """get task""" @@ -73,21 +65,77 @@ class ScheduleBuilder: return task @staticmethod - def get_set_cron_tab(schedule): + def get_set_cron_tab(schedule: str) -> CrontabSchedule: """needs to be validated before""" kwargs = dict(zip(["minute", "hour", "day_of_week"], schedule.split())) kwargs.update({"timezone": EnvironmentSettings.TZ}) - crontab, _ = CrontabSchedule.objects.get_or_create(**kwargs) + task_crontab, _ = CrontabSchedule.objects.get_or_create(**kwargs) - return crontab + return task_crontab - def set_config(self, key, value): - """set task_config""" - task_name = self.CONFIG.get(key) - if not task_name: - raise ValueError("invalid config key") + def set_config(self, task_name: str, key: str, value) -> None: + """set task_config, validate before""" + try: + task = CustomPeriodicTask.objects.get(name=task_name) + task.task_config.update({key: value}) + task.save() + except CustomPeriodicTask.DoesNotExist: + pass - task = CustomPeriodicTask.objects.get(name=task_name) - config_key = key.split(f"{task_name}_")[-1] - task.task_config.update({config_key: value}) - task.save() + +class CrontabValidator: + """validate crontab""" + + CONFIG = { + "check_reindex": ["days"], + "run_backup": ["rotate"], + } + + @staticmethod + def validate_fields(cron_fields: str) -> None: + """expect 3 cron fields""" + if not len(cron_fields) == 3: + raise ValueError("expected three cron schedule fields") + + @staticmethod + def validate_minute(minute_field: str): + """expect minute int""" + if not minute_field.isdigit(): + raise ValueError("Invalid value for minutes. Must be an integer.") + + minutes = int(minute_field) + if not 0 <= minutes <= 59: + raise ValueError("Invalid minutes. Must be between 0 and 59.") + + @staticmethod + def validate_cron_tab(minute, hour, day_of_week): + """check if crontab can be created""" + try: + crontab(minute=minute, hour=hour, day_of_week=day_of_week) + except ValueError as err: + raise ValueError(f"invalid crontab: {err}") from err + + def validate_cron(self, cron_expression): + """create crontab schedule""" + if not cron_expression or cron_expression == "auto": + return + + cron_fields = cron_expression.split() + self.validate_fields(cron_fields) + + minute, hour, day_of_week = cron_fields + self.validate_minute(minute) + self.validate_cron_tab(minute, hour, day_of_week) + + def validate_config(self, task_name: str, schedule_config: dict): + """validate config for given task""" + if not schedule_config: + return + + config_keys = self.CONFIG.get(task_name) + if not config_keys: + raise ValueError(f"task '{task_name}' doesn't take config") + + for key in schedule_config: + if key not in config_keys: + raise ValueError(f"invalid config key for task '{task_name}'") diff --git a/tubearchivist/task/urls.py b/tubearchivist/task/urls.py index 71a73b11..e0c7f2f4 100644 --- a/tubearchivist/task/urls.py +++ b/tubearchivist/task/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ name="api-task-id", ), path( - "schedule/", + "schedule//", views.ScheduleView.as_view(), name="api-schedule", ), diff --git a/tubearchivist/task/views.py b/tubearchivist/task/views.py index 9aeb6bdb..8972b1bf 100644 --- a/tubearchivist/task/views.py +++ b/tubearchivist/task/views.py @@ -1,8 +1,10 @@ """all task API views""" from common.views_base import AdminOnly, ApiBaseView +from django.shortcuts import get_object_or_404 from rest_framework.response import Response from task.models import CustomPeriodicTask +from task.src.config_schedule import CrontabValidator, ScheduleBuilder from task.src.notify import Notifications, get_all_notifications from task.src.task_config import TASK_CONFIG from task.src.task_manager import TaskCommand, TaskManager @@ -118,20 +120,40 @@ class TaskIDView(ApiBaseView): class ScheduleView(ApiBaseView): """resolves to /api/task/schedule// + POST: create/update schedule for task with config + - example: {"schedule": "0 0 *", "config": {"days": 90}} DEL: delete schedule for task """ permission_classes = [AdminOnly] - def delete(self, request): - """delete schedule by task_name query""" - task_name = request.data.get("task_name") - try: - task = CustomPeriodicTask.objects.get(name=task_name) - except CustomPeriodicTask.DoesNotExist: - message = {"message": "task_name not found"} - return Response(message, status=404) + def post(self, request, task_name): + """create/update schedule for task""" + cron_schedule = request.data.get("schedule") + schedule_config = request.data.get("config") + if not cron_schedule and not schedule_config: + message = {"message": "expected schedule or config key"} + return Response(message, status=400) + try: + validator = CrontabValidator() + validator.validate_cron(cron_schedule) + validator.validate_config(task_name, schedule_config) + except ValueError as err: + return Response({"message": str(err)}, status=400) + + ScheduleBuilder().update_schedule( + task_name, cron_schedule, schedule_config + ) + message = f"update schedule for task {task_name}" + if schedule_config: + message += f" with config {schedule_config}" + + return Response({"message": message}) + + def delete(self, request, task_name): + """delete schedule by task_name query""" + task = get_object_or_404(CustomPeriodicTask, name=task_name) _ = task.delete() return Response({"success": True})