mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 21:45:25 +02:00
feat(backend): ✨ basic support for background tasks
This commit is contained in:
parent
7f99c3d113
commit
7055cb2c43
15 changed files with 226 additions and 19 deletions
|
@ -93,7 +93,7 @@ def main():
|
|||
reload_dirs=["mealie"],
|
||||
reload_delay=2,
|
||||
debug=True,
|
||||
log_level="debug",
|
||||
log_level="info",
|
||||
use_colors=True,
|
||||
log_config=None,
|
||||
workers=1,
|
||||
|
|
|
@ -41,22 +41,41 @@ class AccessModel(Generic[T, D]):
|
|||
def get_all(self, limit: int = None, order_by: str = None, start=0, override_schema=None) -> list[T]:
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
order_attr = None
|
||||
if order_by:
|
||||
order_attr = getattr(self.sql_model, str(order_by))
|
||||
order_attr = order_attr.desc()
|
||||
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in self.session.query(self.sql_model).order_by(order_attr.desc()).offset(start).limit(limit).all()
|
||||
for x in self.session.query(self.sql_model).order_by(order_attr).offset(start).limit(limit).all()
|
||||
]
|
||||
|
||||
return [eff_schema.from_orm(x) for x in self.session.query(self.sql_model).offset(start).limit(limit).all()]
|
||||
|
||||
def multi_query(self, query_by: dict[str, str], start=0, limit: int = None, override_schema=None) -> list[T]:
|
||||
def multi_query(
|
||||
self,
|
||||
query_by: dict[str, str],
|
||||
start=0,
|
||||
limit: int = None,
|
||||
override_schema=None,
|
||||
order_by: str = None,
|
||||
) -> list[T]:
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
order_attr = None
|
||||
if order_by:
|
||||
order_attr = getattr(self.sql_model, str(order_by))
|
||||
order_attr = order_attr.desc()
|
||||
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in self.session.query(self.sql_model).filter_by(**query_by).offset(start).limit(limit).all()
|
||||
for x in self.session.query(self.sql_model)
|
||||
.filter_by(**query_by)
|
||||
.order_by(order_attr)
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_all_limit_columns(self, fields: list[str], limit: int = None) -> list[D]:
|
||||
|
|
|
@ -13,6 +13,7 @@ from mealie.db.models.recipe.comment import RecipeComment
|
|||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.server.task import ServerTaskModel
|
||||
from mealie.db.models.settings import SiteSettings
|
||||
from mealie.db.models.sign_up import SignUp
|
||||
from mealie.db.models.users import LongLiveToken, User
|
||||
|
@ -27,6 +28,7 @@ from mealie.schema.group.webhook import ReadWebhook
|
|||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||
from mealie.schema.recipe import CommentOut, Recipe, RecipeCategoryResponse, RecipeTagResponse
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||
from mealie.schema.server import ServerTask
|
||||
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
||||
from mealie.schema.user.user_passwords import PrivatePasswordResetToken
|
||||
|
||||
|
@ -70,15 +72,15 @@ class Database:
|
|||
return RecipeDataAccessModel(self.session, pk_slug, RecipeModel, Recipe)
|
||||
|
||||
@cached_property
|
||||
def ingredient_foods(self) -> AccessModel:
|
||||
def ingredient_foods(self) -> AccessModel[IngredientFood, IngredientFoodModel]:
|
||||
return AccessModel(self.session, pk_id, IngredientFoodModel, IngredientFood)
|
||||
|
||||
@cached_property
|
||||
def ingredient_units(self) -> AccessModel:
|
||||
def ingredient_units(self) -> AccessModel[IngredientUnit, IngredientUnitModel]:
|
||||
return AccessModel(self.session, pk_id, IngredientUnitModel, IngredientUnit)
|
||||
|
||||
@cached_property
|
||||
def comments(self) -> AccessModel:
|
||||
def comments(self) -> AccessModel[CommentOut, RecipeComment]:
|
||||
return AccessModel(self.session, pk_id, RecipeComment, CommentOut)
|
||||
|
||||
@cached_property
|
||||
|
@ -93,19 +95,19 @@ class Database:
|
|||
# Site Items
|
||||
|
||||
@cached_property
|
||||
def settings(self) -> AccessModel:
|
||||
def settings(self) -> AccessModel[SiteSettingsSchema, SiteSettings]:
|
||||
return AccessModel(self.session, pk_id, SiteSettings, SiteSettingsSchema)
|
||||
|
||||
@cached_property
|
||||
def sign_up(self) -> AccessModel:
|
||||
def sign_up(self) -> AccessModel[SignUpOut, SignUp]:
|
||||
return AccessModel(self.session, pk_id, SignUp, SignUpOut)
|
||||
|
||||
@cached_property
|
||||
def event_notifications(self) -> AccessModel:
|
||||
def event_notifications(self) -> AccessModel[EventNotificationIn, EventNotification]:
|
||||
return AccessModel(self.session, pk_id, EventNotification, EventNotificationIn)
|
||||
|
||||
@cached_property
|
||||
def events(self) -> AccessModel:
|
||||
def events(self) -> AccessModel[EventSchema, Event]:
|
||||
return AccessModel(self.session, pk_id, Event, EventSchema)
|
||||
|
||||
# ================================================================
|
||||
|
@ -116,7 +118,7 @@ class Database:
|
|||
return UserDataAccessModel(self.session, pk_id, User, PrivateUser)
|
||||
|
||||
@cached_property
|
||||
def api_tokens(self) -> AccessModel:
|
||||
def api_tokens(self) -> AccessModel[LongLiveTokenInDB, LongLiveToken]:
|
||||
return AccessModel(self.session, pk_id, LongLiveToken, LongLiveTokenInDB)
|
||||
|
||||
@cached_property
|
||||
|
@ -126,16 +128,20 @@ class Database:
|
|||
# ================================================================
|
||||
# Group Items
|
||||
|
||||
@cached_property
|
||||
def server_tasks(self) -> AccessModel[ServerTask, ServerTaskModel]:
|
||||
return AccessModel(self.session, pk_id, ServerTaskModel, ServerTask)
|
||||
|
||||
@cached_property
|
||||
def groups(self) -> GroupDataAccessModel:
|
||||
return GroupDataAccessModel(self.session, pk_id, Group, GroupInDB)
|
||||
|
||||
@cached_property
|
||||
def group_invite_tokens(self) -> AccessModel:
|
||||
def group_invite_tokens(self) -> AccessModel[ReadInviteToken, GroupInviteToken]:
|
||||
return AccessModel(self.session, pk_token, GroupInviteToken, ReadInviteToken)
|
||||
|
||||
@cached_property
|
||||
def group_preferences(self) -> AccessModel:
|
||||
def group_preferences(self) -> AccessModel[ReadGroupPreferences, GroupPreferencesModel]:
|
||||
return AccessModel(self.session, "group_id", GroupPreferencesModel, ReadGroupPreferences)
|
||||
|
||||
@cached_property
|
||||
|
@ -143,9 +149,9 @@ class Database:
|
|||
return MealDataAccessModel(self.session, pk_id, GroupMealPlan, ReadPlanEntry)
|
||||
|
||||
@cached_property
|
||||
def cookbooks(self) -> AccessModel:
|
||||
def cookbooks(self) -> AccessModel[ReadCookBook, CookBook]:
|
||||
return AccessModel(self.session, pk_id, CookBook, ReadCookBook)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> AccessModel:
|
||||
def webhooks(self) -> AccessModel[ReadWebhook, GroupWebhooksModel]:
|
||||
return AccessModel(self.session, pk_id, GroupWebhooksModel, ReadWebhook)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from .event import *
|
||||
from .group import *
|
||||
from .recipe.recipe import *
|
||||
from .server import *
|
||||
from .settings import *
|
||||
from .sign_up import *
|
||||
from .users import *
|
||||
|
|
|
@ -9,8 +9,8 @@ from sqlalchemy.orm.session import Session
|
|||
@as_declarative()
|
||||
class Base:
|
||||
id = Column(Integer, primary_key=True)
|
||||
created_at = Column(DateTime, default=datetime.now())
|
||||
update_at = Column(DateTime, default=datetime.now(), onupdate=datetime.now())
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class BaseMixins:
|
||||
|
|
|
@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session
|
|||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||
from mealie.db.models.server.task import ServerTaskModel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
|
@ -42,6 +43,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||
)
|
||||
webhooks = orm.relationship(GroupWebhooksModel, uselist=True, cascade="all, delete-orphan")
|
||||
cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
|
||||
server_tasks = orm.relationship(ServerTaskModel, back_populates="group", single_parent=True)
|
||||
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||
|
||||
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens", "mealplans"})
|
||||
|
|
1
mealie/db/models/server/__init__.py
Normal file
1
mealie/db/models/server/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .task import *
|
20
mealie/db/models/server/task.py
Normal file
20
mealie/db/models/server/task.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, orm
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
from .._model_utils import auto_init
|
||||
|
||||
|
||||
class ServerTaskModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "server_tasks"
|
||||
name = Column(String, nullable=False)
|
||||
completed_date = Column(DateTime, nullable=True)
|
||||
status = Column(String, nullable=False)
|
||||
log = Column(String, nullable=True)
|
||||
|
||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="server_tasks")
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
|
@ -2,7 +2,7 @@ from fastapi import APIRouter
|
|||
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
|
||||
from . import admin_about, admin_email, admin_group, admin_log
|
||||
from . import admin_about, admin_email, admin_group, admin_log, admin_server_tasks
|
||||
|
||||
router = AdminAPIRouter(prefix="/admin")
|
||||
|
||||
|
@ -10,3 +10,4 @@ router.include_router(admin_about.router, tags=["Admin: About"])
|
|||
router.include_router(admin_log.router, tags=["Admin: Log"])
|
||||
router.include_router(admin_group.router, tags=["Admin: Group"])
|
||||
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
|
||||
|
|
18
mealie/routes/admin/admin_server_tasks.py
Normal file
18
mealie/routes/admin/admin_server_tasks.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.server.tasks import ServerTask, ServerTaskNames
|
||||
from mealie.services.server_tasks import BackgroundExecutor, test_executor_func
|
||||
from mealie.services.server_tasks.tasks_http_service import AdminServerTasks
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.get("/server-tasks", response_model=list[ServerTask])
|
||||
def get_all_tasks(tasks_service: AdminServerTasks = Depends(AdminServerTasks.private)):
|
||||
return tasks_service.get_all()
|
||||
|
||||
|
||||
@router.post("/server-tasks", response_model=ServerTask)
|
||||
def create_test_tasks(bg_executor: BackgroundExecutor = Depends(BackgroundExecutor.private)):
|
||||
return bg_executor.dispatch(ServerTaskNames.default, test_executor_func)
|
1
mealie/schema/server/__init__.py
Normal file
1
mealie/schema/server/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .tasks import *
|
44
mealie/schema/server/tasks.py
Normal file
44
mealie/schema/server/tasks.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import datetime
|
||||
import enum
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ServerTaskNames(str, enum.Enum):
|
||||
default = "Background Task"
|
||||
backup_task = "Database Backup"
|
||||
|
||||
|
||||
class ServerTaskStatus(str, enum.Enum):
|
||||
running = "running"
|
||||
finished = "finished"
|
||||
failed = "failed"
|
||||
|
||||
|
||||
class ServerTaskCreate(CamelModel):
|
||||
group_id: int
|
||||
name: ServerTaskNames = ServerTaskNames.default
|
||||
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||
status: ServerTaskStatus = ServerTaskStatus.running
|
||||
log: str = ""
|
||||
|
||||
def set_running(self) -> None:
|
||||
self.status = ServerTaskStatus.running
|
||||
|
||||
def set_finished(self) -> None:
|
||||
self.status = ServerTaskStatus.finished
|
||||
|
||||
def set_failed(self) -> None:
|
||||
self.status = ServerTaskStatus.failed
|
||||
|
||||
def append_log(self, message: str) -> None:
|
||||
# Prefix with Timestamp and append new line and join to log
|
||||
self.log += f"{datetime.datetime.now()}: {message}\n"
|
||||
|
||||
|
||||
class ServerTask(ServerTaskCreate):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
2
mealie/services/server_tasks/__init__.py
Normal file
2
mealie/services/server_tasks/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .background_executory import *
|
||||
from .tasks_http_service import *
|
56
mealie/services/server_tasks/background_executory.py
Normal file
56
mealie/services/server_tasks/background_executory.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from random import getrandbits
|
||||
from time import sleep
|
||||
from typing import Any, Callable
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.db.database import get_database
|
||||
from mealie.schema.server.tasks import ServerTask, ServerTaskCreate, ServerTaskNames
|
||||
|
||||
from .._base_http_service.http_services import UserHttpService
|
||||
|
||||
|
||||
class BackgroundExecutor(UserHttpService):
|
||||
def populate_item(self, _: int) -> ServerTask:
|
||||
pass
|
||||
|
||||
def dispatch(self, task_name: ServerTaskNames, func: Callable, *args: Any, **kwargs: Any) -> ServerTask:
|
||||
"""The dispatch function is a wrapper around the BackgroundTasks class in Starlett. It dirctly calls
|
||||
the add_task function and your task will be run in the background. This function all passes the id required
|
||||
to check on the server tasks in the database and provide updates.
|
||||
|
||||
Tasks that are dispachd by the Background executor should be designed to accept this key word argument
|
||||
and update the item in the database accordingly.
|
||||
"""
|
||||
|
||||
server_task = ServerTaskCreate(group_id=self.group_id, name=task_name)
|
||||
server_task = self.db.server_tasks.create(server_task)
|
||||
|
||||
self.background_tasks.add_task(func, *args, **kwargs, task_id=server_task.id, session=self.session)
|
||||
|
||||
return server_task
|
||||
|
||||
|
||||
def test_executor_func(task_id: int, session: Session) -> None:
|
||||
database = get_database(session)
|
||||
task = database.server_tasks.get_one(task_id)
|
||||
|
||||
task.append_log("test task has started")
|
||||
task.append_log("test task sleeping for 60 seconds")
|
||||
|
||||
sleep(60)
|
||||
|
||||
task.append_log("test task has finished sleep")
|
||||
|
||||
# Randomly Decide to set to failed or not
|
||||
|
||||
is_fail = bool(getrandbits(1))
|
||||
|
||||
if is_fail:
|
||||
task.append_log("test task has failed")
|
||||
task.set_failed()
|
||||
else:
|
||||
task.append_log("test task has succeeded")
|
||||
task.set_finished()
|
||||
|
||||
database.server_tasks.update(task.id, task)
|
36
mealie/services/server_tasks/tasks_http_service.py
Normal file
36
mealie/services/server_tasks/tasks_http_service.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from functools import cached_property
|
||||
|
||||
from mealie.schema.server import ServerTask
|
||||
from mealie.services._base_http_service.http_services import AdminHttpService, UserHttpService
|
||||
|
||||
|
||||
class ServerTasksHttpService(UserHttpService[int, ServerTask]):
|
||||
_restrict_by_group = True
|
||||
_schema = ServerTask
|
||||
|
||||
@cached_property
|
||||
def dal(self):
|
||||
return self.db.server_tasks
|
||||
|
||||
def populate_item(self, id: int) -> ServerTask:
|
||||
self.item = self.dal.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[ServerTask]:
|
||||
return self.dal.multi_query(query_by={"group_id": self.group_id}, order_by="created_at")
|
||||
|
||||
|
||||
class AdminServerTasks(AdminHttpService[int, ServerTask]):
|
||||
_restrict_by_group = True
|
||||
_schema = ServerTask
|
||||
|
||||
@cached_property
|
||||
def dal(self):
|
||||
return self.db.server_tasks
|
||||
|
||||
def populate_item(self, id: int) -> ServerTask:
|
||||
self.item = self.dal.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[ServerTask]:
|
||||
return self.dal.get_all(order_by="created_at")
|
Loading…
Add table
Add a link
Reference in a new issue