From 405036814794c7595c66b39945098d5869aee574 Mon Sep 17 00:00:00 2001 From: klemek Date: Fri, 3 Jul 2026 19:55:03 +0200 Subject: [PATCH] feat: task --- app/migrations/0002_auto_20260703_1716.py | 27 ++++++++++++ app/models/__init__.py | 1 + app/models/base.py | 4 +- app/models/comment.py | 2 +- app/models/task.py | 50 +++++++++++++++++++++++ app/server.py | 44 +++++++++++++++++++- pyproject.toml | 2 +- 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 app/migrations/0002_auto_20260703_1716.py create mode 100644 app/models/task.py diff --git a/app/migrations/0002_auto_20260703_1716.py b/app/migrations/0002_auto_20260703_1716.py new file mode 100644 index 0000000..1d49af4 --- /dev/null +++ b/app/migrations/0002_auto_20260703_1716.py @@ -0,0 +1,27 @@ +from tortoise import migrations +from tortoise.migrations import operations as ops +from uuid import uuid4 +from tortoise import fields + +class Migration(migrations.Migration): + dependencies = [('models', '0001_initial')] + + initial = False + + operations = [ + ops.CreateModel( + name='Task', + fields=[ + ('id', fields.UUIDField(primary_key=True, default=uuid4, unique=True, db_index=True)), + ('created_at', fields.DatetimeField(auto_now=False, auto_now_add=True)), + ('updated_at', fields.DatetimeField(auto_now=True, auto_now_add=False)), + ('list', fields.CharField(db_index=True, max_length=32)), + ('name', fields.TextField(unique=False)), + ('reset_cron', fields.CharField(null=True, max_length=32)), + ('check_date', fields.DatetimeField(null=True, auto_now=False, auto_now_add=False)), + ('previous_check_date', fields.DatetimeField(null=True, auto_now=False, auto_now_add=False)), + ], + options={'table': 'task', 'app': 'models', 'pk_attr': 'id'}, + bases=['AbstractModel'], + ), + ] diff --git a/app/models/__init__.py b/app/models/__init__.py index d2cdee0..7a9c600 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1 +1,2 @@ from .comment import Comment # noqa: F401 +from .task import Task # noqa: F401 diff --git a/app/models/base.py b/app/models/base.py index 80833f5..d1faf15 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -4,8 +4,8 @@ import tortoise.models class AbstractModel(tortoise.models.Model): id = tortoise.fields.UUIDField(primary_key=True) - created_at = tortoise.fields.DatetimeField(auto_now_add=True) - updated_at = tortoise.fields.DatetimeField(auto_now=True) + created_at = tortoise.fields.DatetimeField(auto_now_add=True, null=False) + updated_at = tortoise.fields.DatetimeField(auto_now=True, null=False) class Meta: abstract = True diff --git a/app/models/comment.py b/app/models/comment.py index 6e6df2c..e2759fb 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -4,4 +4,4 @@ from app.models.base import AbstractModel class Comment(AbstractModel): - content = tortoise.fields.TextField() + content = tortoise.fields.TextField(null=False) diff --git a/app/models/task.py b/app/models/task.py new file mode 100644 index 0000000..6f9f9fb --- /dev/null +++ b/app/models/task.py @@ -0,0 +1,50 @@ +import typing + +import tortoise + +from app.models.base import AbstractModel + +# https://stackoverflow.com/a/57639657 +CRONTAB_REGEX = r"/(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/" + + +class Task(AbstractModel): + list = tortoise.fields.CharField(max_length=32, db_index=True, null=False) + name = tortoise.fields.TextField(null=False) + reset_cron = tortoise.fields.CharField(max_length=64, null=True) + check_date = tortoise.fields.DatetimeField(null=True) + previous_check_date = tortoise.fields.DatetimeField(null=True) + + @classmethod + def validate_create(cls, data: dict[str, typing.Any]) -> dict | None: + out_data = {} + try: + for field in [ + "list", + "name", + "reset_cron", + "check_date", + "previous_check_date", + ]: + getattr(cls, field).validate(data.get(field)) + out_data[field] = data.get(field) + except tortoise.exceptions.ValidationError: + return None + return out_data + + @classmethod + def validate_update(cls, data: dict[str, typing.Any]) -> dict | None: + out_data = {} + try: + for field in [ + "name", + "reset_cron", + "check_date", + "previous_check_date", + ]: + if field in data: + getattr(cls, field).validate(data["field"]) + out_data[field] = data["field"] + except tortoise.exceptions.ValidationError: + return None + return out_data diff --git a/app/server.py b/app/server.py index b5cfe10..402515a 100644 --- a/app/server.py +++ b/app/server.py @@ -5,9 +5,10 @@ import flask import flask_limiter import flask_limiter.util import gunicorn.app.base +import tortoise from app import PKG_NAME -from app.models.comment import Comment +from app.models import Comment, Task if typing.TYPE_CHECKING: from app.params import Parameters @@ -50,6 +51,47 @@ class Server(gunicorn.app.base.BaseApplication): await Comment.create(content=data["content"]) return ("", 204) + @self.app.route("/api/lists//tasks", methods=["GET"]) + async def get_tasks(list_name: str) -> list: + return await Task.filter(list=list_name).values() + + @self.app.route("/api/lists//tasks", methods=["POST"]) + async def post_task(list_name: str) -> typing.Any: + data = flask.request.get_json() + data["list"] = list_name + parsed_data = Task.validate_create(data) + if parsed_data is None: + flask.abort(400) + return (await Task.create(**parsed_data)).get().values() + + @self.app.route("/api/lists//tasks/", methods=["PUT"]) + async def put_task(list_name: str, task_uuid: str) -> typing.Any: + data = flask.request.get_json() + data["list"] = list_name + parsed_data = Task.validate_update(data) + if parsed_data is None: + flask.abort(400) + try: + task = await Task.get(id=task_uuid) + await task.update_from_dict(parsed_data) + return task.get().values() + except tortoise.exceptions.ObjectDoesNotExistError: + flask.abort(404) + + @self.app.route("/api/lists//tasks/", methods=["DELETE"]) + async def delete_task(list_name: str, task_uuid: str) -> typing.Any: + data = flask.request.get_json() + data["list"] = list_name + parsed_data = Task.validate_update(data) + if parsed_data is None: + flask.abort(400) + try: + task = await Task.get(id=task_uuid) + await task.delete() + except tortoise.exceptions.ObjectDoesNotExistError: + flask.abort(404) + return "" + return self @typing.override diff --git a/pyproject.toml b/pyproject.toml index 2043d77..fd7fe08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ exclude = ["app/migrations"] [tool.ruff.lint] select = ["ALL"] -ignore = ["D", "E501", "ANN401", "BLE001", "COM812", "S101", "PT", "S104"] +ignore = ["D", "E501", "ANN401", "BLE001", "COM812", "S101", "PT", "S104", "C901"] [tool.ty.src] include = ["app"]