feat(backend): rewrite mealplanner with simple api (#683)

* feat(backend):  new meal-planner feature

* feat(frontend):  new meal plan feature

* refactor(backend): ♻️ refactor base services classes and add mixins for crud

* feat(frontend):  add UI/API for mealplanner

* feat(backend):  add get_today and get_slice options for mealplanner

* test(backend):  add and update group mealplanner tests

* fix(backend): 🐛 Fix recipe_id column type for PG

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-09-12 11:05:09 -08:00
committed by GitHub
parent bdaf758712
commit b542583303
46 changed files with 869 additions and 255 deletions

View File

@@ -3,14 +3,14 @@ from logging import getLogger
from sqlalchemy.orm.session import Session
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
from mealie.db.data_access_layer.meal_access_model import MealDataAccessModel
from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group
from mealie.db.models.group import Group, GroupMealPlan
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.invite_tokens import GroupInviteToken
from mealie.db.models.group.preferences import GroupPreferencesModel
from mealie.db.models.group.shopping_list import ShoppingList
from mealie.db.models.group.webhooks import GroupWebhooksModel
from mealie.db.models.mealplan import MealPlan
from mealie.db.models.recipe.category import Category
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
@@ -25,7 +25,8 @@ from mealie.schema.events import EventNotificationIn
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.invite_token import ReadInviteToken
from mealie.schema.group.webhook import ReadWebhook
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
from mealie.schema.meal_plan import ShoppingListOut
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from mealie.schema.recipe import (
CommentOut,
IngredientFood,
@@ -90,7 +91,7 @@ class DatabaseAccessLayer:
# Group Data
self.groups = GroupDataAccessModel(pk_id, Group, GroupInDB)
self.group_tokens = BaseAccessModel("token", GroupInviteToken, ReadInviteToken)
self.meals = BaseAccessModel(pk_id, MealPlan, MealPlanOut)
self.meals = MealDataAccessModel(pk_id, GroupMealPlan, ReadPlanEntry)
self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook)
self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut)
self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook)

View File

@@ -0,0 +1,26 @@
from datetime import date
from sqlalchemy.orm.session import Session
from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
from ._base_access_model import BaseAccessModel
class MealDataAccessModel(BaseAccessModel[ReadPlanEntry, GroupMealPlan]):
def get_slice(self, session: Session, start: date, end: date, group_id: int) -> list[ReadPlanEntry]:
start = start.strftime("%Y-%m-%d")
end = end.strftime("%Y-%m-%d")
qry = session.query(GroupMealPlan).filter(
GroupMealPlan.date.between(start, end),
GroupMealPlan.group_id == group_id,
)
return [self.schema.from_orm(x) for x in qry.all()]
def get_today(self, session: Session, group_id: int) -> list[ReadPlanEntry]:
today = date.today()
qry = session.query(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
return [self.schema.from_orm(x) for x in qry.all()]

View File

@@ -10,7 +10,7 @@ from mealie.db.models._model_base import SqlAlchemyBase
from mealie.schema.admin import SiteSettings
from mealie.schema.user.user import GroupBase
from mealie.services.events import create_general_event
from mealie.services.group_services.group_mixins import create_new_group
from mealie.services.group_services.group_utils import create_new_group
logger = root_logger.get_logger("init_db")

View File

@@ -1,6 +1,5 @@
from .event import *
from .group import *
from .mealplan import *
from .recipe.recipe import *
from .settings import *
from .sign_up import *

View File

@@ -1,6 +1,7 @@
from .cookbook import *
from .group import *
from .invite_tokens import *
from .mealplan import *
from .preferences import *
from .shopping_list import *
from .webhooks import *

View File

@@ -10,6 +10,7 @@ from .._model_utils import auto_init
from ..group.webhooks import GroupWebhooksModel
from ..recipe.category import Category, group2categories
from .cookbook import CookBook
from .mealplan import GroupMealPlan
from .preferences import GroupPreferencesModel
@@ -34,12 +35,14 @@ class Group(SqlAlchemyBase, BaseMixins):
recipes = orm.relationship("RecipeModel", back_populates="group", uselist=True)
# CRUD From Others
mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
mealplans = orm.relationship(
GroupMealPlan, back_populates="group", single_parent=True, order_by="GroupMealPlan.date"
)
webhooks = orm.relationship(GroupWebhooksModel, uselist=True, cascade="all, delete-orphan")
cookbooks = orm.relationship(CookBook, 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"})
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens", "mealplans"})
def __init__(self, **_) -> None:
pass

View File

@@ -0,0 +1,24 @@
from sqlalchemy import Column, Date, ForeignKey, String, orm
from sqlalchemy.sql.sqltypes import Integer
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_meal_plans"
date = Column(Date, index=True, nullable=False)
entry_type = Column(String, index=True, nullable=False)
title = Column(String, index=True, nullable=False)
text = Column(String, nullable=False)
group_id = Column(Integer, ForeignKey("groups.id"), index=True)
group = orm.relationship("Group", back_populates="mealplans")
recipe_id = Column(Integer, ForeignKey("recipes.id"))
recipe = orm.relationship("RecipeModel", back_populates="meal_entries", uselist=False)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -1,82 +0,0 @@
import sqlalchemy.orm as orm
from sqlalchemy import Column, Date, ForeignKey, Integer, String
from sqlalchemy.ext.orderinglist import ordering_list
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.group import Group
from mealie.db.models.recipe.recipe import RecipeModel
from .group.shopping_list import ShoppingList
class Meal(SqlAlchemyBase):
__tablename__ = "meal"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("mealdays.id"))
position = Column(Integer)
name = Column(String)
slug = Column(String)
description = Column(String)
def __init__(self, slug, name="", description="", session=None) -> None:
if slug and slug != "":
recipe: RecipeModel = session.query(RecipeModel).filter(RecipeModel.slug == slug).one_or_none()
if recipe:
name = recipe.name
self.slug = recipe.slug
description = recipe.description
self.name = name
self.description = description
class MealDay(SqlAlchemyBase, BaseMixins):
__tablename__ = "mealdays"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("mealplan.id"))
date = Column(Date)
meals: list[Meal] = orm.relationship(
Meal,
cascade="all, delete, delete-orphan",
order_by="Meal.position",
collection_class=ordering_list("position"),
)
def __init__(self, date, meals: list, session=None):
self.date = date
self.meals = [Meal(**m, session=session) for m in meals]
class MealPlan(SqlAlchemyBase, BaseMixins):
__tablename__ = "mealplan"
# TODO: Migrate to use ID as PK
start_date = Column(Date)
end_date = Column(Date)
plan_days: list[MealDay] = orm.relationship(MealDay, cascade="all, delete, delete-orphan")
group_id = Column(Integer, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="mealplans")
shopping_list_id = Column(Integer, ForeignKey("shopping_lists.id"))
shopping_list: ShoppingList = orm.relationship("ShoppingList", single_parent=True)
def __init__(
self,
start_date,
end_date,
plan_days,
group: str,
shopping_list: int = None,
session=None,
**_,
) -> None:
self.start_date = start_date
self.end_date = end_date
self.group = Group.get_ref(session, group)
if shopping_list:
self.shopping_list = ShoppingList.get_ref(session, shopping_list)
self.plan_days = [MealDay(**day, session=session) for day in plan_days]

View File

@@ -34,7 +34,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
user_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
favorited_by: list = orm.relationship("User", secondary=users_to_favorites, back_populates="favorite_recipes")
meal_entries = orm.relationship("GroupMealPlan", back_populates="recipe")
favorited_by = orm.relationship("User", secondary=users_to_favorites, back_populates="favorite_recipes")
# General Recipe Properties
name = sa.Column(sa.String, nullable=False)

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter
from . import app_about, app_defaults
from . import app_about
router = APIRouter(prefix="/app")
router.include_router(app_about.router, tags=["App: About"])
router.include_router(app_defaults.router, tags=["App: Defaults"])

View File

@@ -1,12 +0,0 @@
from fastapi import APIRouter
from mealie.schema.recipe.recipe_settings import RecipeSettings
router = APIRouter(prefix="/defaults")
@router.get("/recipe", response_model=RecipeSettings)
async def get_recipe_settings_defaults():
""" Returns the Default Settings for Recieps as set by ENV variables """
return RecipeSettings()

View File

@@ -1,16 +1,39 @@
from fastapi import APIRouter
from datetime import date, timedelta
from fastapi import APIRouter, Depends
from mealie.services._base_http_service import RouterFactory
from mealie.services.group_services import CookbookService, WebhookService
from mealie.services.group_services.meal_service import MealService
from . import categories, invitations, preferences, self_service
router = APIRouter()
router.include_router(self_service.user_router)
webhook_router = RouterFactory(service=WebhookService, prefix="/groups/webhooks", tags=["Groups: Webhooks"])
cookbook_router = RouterFactory(service=CookbookService, prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
router.include_router(self_service.user_router)
@router.get("/groups/mealplans/today", tags=["Groups: Mealplans"])
def get_todays_meals(m_service: MealService = Depends(MealService.private)):
return m_service.get_today()
meal_plan_router = RouterFactory(service=MealService, prefix="/groups/mealplans", tags=["Groups: Mealplans"])
@meal_plan_router.get("")
def get_all(start: date = None, limit: date = None, m_service: MealService = Depends(MealService.private)):
start = start or date.today() - timedelta(days=999)
limit = limit or date.today() + timedelta(days=999)
return m_service.get_slice(start, limit)
router.include_router(cookbook_router)
router.include_router(meal_plan_router)
router.include_router(categories.user_router)
router.include_router(webhook_router)
router.include_router(invitations.router, prefix="/groups/invitations", tags=["Groups: Invitations"])

View File

@@ -1,2 +1,3 @@
from .meal import *
from .new_meal import *
from .shopping_list import *

View File

@@ -3,9 +3,6 @@ from typing import Optional
from fastapi_camelcase import CamelModel
from pydantic import validator
from pydantic.utils import GetterDict
from mealie.db.models.mealplan import MealPlan
class MealIn(CamelModel):
@@ -54,18 +51,3 @@ class MealPlanOut(MealPlanIn):
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm: MealPlan):
try:
return {
**GetterDict(name_orm),
"group": name_orm.group.name,
"shopping_list": name_orm.shopping_list.id,
}
except Exception:
return {
**GetterDict(name_orm),
"group": name_orm.group.name,
"shopping_list": None,
}

View File

@@ -0,0 +1,51 @@
from datetime import date
from enum import Enum
from typing import Optional
from fastapi_camelcase import CamelModel
from pydantic import validator
from mealie.schema.recipe.recipe import RecipeSummary
class PlanEntryType(str, Enum):
breakfast = "breakfast"
lunch = "lunch"
dinner = "dinner"
snack = "snack"
class CreatePlanEntry(CamelModel):
date: date
entry_type: PlanEntryType = PlanEntryType.breakfast
title: str = ""
text: str = ""
recipe_id: Optional[int]
@validator("recipe_id", always=True)
@classmethod
def id_or_title(cls, value, values):
print(value, values)
if bool(value) is False and bool(values["title"]) is False:
raise ValueError(f"`recipe_id={value}` or `title={values['title']}` must be provided")
return value
class UpdatePlanEntry(CreatePlanEntry):
id: int
group_id: int
class SavePlanEntry(CreatePlanEntry):
group_id: int
class Config:
orm_mode = True
class ReadPlanEntry(UpdatePlanEntry):
recipe: Optional[RecipeSummary]
class Config:
orm_mode = True

View File

@@ -10,7 +10,7 @@ from mealie.db.models.users import User
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.recipe import RecipeSummary
from ..meal_plan import MealPlanOut, ShoppingListOut
from ..meal_plan import ShoppingListOut
from ..recipe import CategoryBase
@@ -129,7 +129,6 @@ class UpdateGroup(GroupBase):
class GroupInDB(UpdateGroup):
users: Optional[list[UserOut]]
mealplans: Optional[list[MealPlanOut]]
shopping_lists: Optional[list[ShoppingListOut]]
preferences: Optional[ReadGroupPreferences] = None

View File

@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
from typing import Any, Callable, Generic, Type, TypeVar
from fastapi import BackgroundTasks, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs, get_settings
@@ -113,6 +114,25 @@ class BaseHttpService(Generic[T, D], ABC):
self._group_id_cache = group.id
return self._group_id_cache
def cast(self, item: BaseModel, dest, assign_owner=True) -> T:
"""cast a pydantic model to the destination type
Args:
item (BaseModel): A pydantic model containing data
dest ([type]): A type to cast the data to
assign_owner (bool, optional): If true, will assign the user_id and group_id to the dest type. Defaults to True.
Returns:
TypeVar(dest): Returns the destionation model type
"""
data = item.dict()
if assign_owner:
data["user_id"] = self.user.id
data["group_id"] = self.group_id
return dest(**data)
def assert_existing(self, id: T) -> None:
self.populate_item(id)
self._check_item()
@@ -135,30 +155,3 @@ class BaseHttpService(Generic[T, D], ABC):
raise NotImplementedError("`event_func` must be set by child class")
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
# Generic CRUD Functions
def _create_one(self, data: Any, exception_msg="generic-create-error") -> D:
try:
self.item = self.db_access.create(self.session, data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
return self.item
def _update_one(self, data: Any, id: int = None) -> D:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db_access.update(self.session, target_id, data)
return self.item
def _delete_one(self, id: int = None) -> D:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db_access.delete(self.session, target_id)
return self.item

View File

@@ -0,0 +1,49 @@
from typing import Generic, TypeVar
from fastapi import HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
C = TypeVar("C", bound=BaseModel)
R = TypeVar("R", bound=BaseModel)
U = TypeVar("U", bound=BaseModel)
DAL = TypeVar("DAL", bound=DatabaseAccessLayer)
logger = get_logger()
class CrudHttpMixins(Generic[C, R, U]):
item: C
session: Session
dal: DAL
def _create_one(self, data: C, exception_msg="generic-create-error") -> R:
try:
self.item = self.dal.create(self.session, data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
return self.item
def _update_one(self, data: U, item_id: int = None) -> R:
if not self.item:
return
target_id = item_id or self.item.id
self.item = self.dal.update(self.session, target_id, data)
return self.item
def _patch_one(self) -> None:
raise NotImplementedError
def _delete_one(self, item_id: int = None) -> None:
if not self.item:
return
target_id = item_id or self.item.id
self.item = self.dal.delete(self.session, target_id)
return self.item

View File

@@ -75,6 +75,7 @@ class RouterFactory(APIRouter):
methods=["POST"],
response_model=self.schema,
summary="Create One",
status_code=201,
description=inspect.cleandoc(self.service.create_one.__doc__ or ""),
)
@@ -162,7 +163,9 @@ class RouterFactory(APIRouter):
self.routes.remove(route)
def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
def route(service: S = Depends(self.service.private)) -> T: # type: ignore
service_dep = getattr(self.service, "get_all_dep", self.service.private)
def route(service: S = Depends(service_dep)) -> T: # type: ignore
return service.get_all()
return route

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_group_event
from mealie.utils.error_messages import ErrorMessages
@@ -10,13 +11,18 @@ from mealie.utils.error_messages import ErrorMessages
logger = get_logger(module=__name__)
class CookbookService(UserHttpService[int, ReadCookBook]):
class CookbookService(
UserHttpService[int, ReadCookBook],
CrudHttpMixins[CreateCookBook, ReadCookBook, UpdateCookBook],
):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadCookBook
db_access = get_database().cookbooks
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().cookbooks
def populate_item(self, item_id: int) -> RecipeCookBook:
try:
@@ -36,7 +42,7 @@ class CookbookService(UserHttpService[int, ReadCookBook]):
return items
def create_one(self, data: CreateCookBook) -> ReadCookBook:
data = SaveCookBook(group_id=self.group_id, **data.dict())
data = self.cast(data, SaveCookBook)
return self._create_one(data, ErrorMessages.cookbook_create_failure)
def update_one(self, data: UpdateCookBook, id: int = None) -> ReadCookBook:

View File

@@ -46,7 +46,6 @@ class GroupSelfService(UserHttpService[int, str]):
def update_categories(self, new_categories: list[CategoryBase]):
self.item.categories = new_categories
return self.db.groups.update(self.session, self.group_id, self.item)
def update_preferences(self, new_preferences: UpdateGroupPreferences):

View File

@@ -7,7 +7,7 @@ def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPrefe
db = get_database()
created_group = db.groups.create(session, g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
g_preferences.group_id = created_group.id

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from datetime import date
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from .._base_http_service.crud_http_mixins import CrudHttpMixins
from .._base_http_service.http_services import UserHttpService
from ..events import create_group_event
logger = get_logger(module=__name__)
class MealService(UserHttpService[int, ReadPlanEntry], CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadPlanEntry
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().meals
def populate_item(self, id: int) -> ReadPlanEntry:
self.item = self.db.meals.get_one(self.session, id)
return self.item
def get_slice(self, start: date = None, end: date = None) -> list[ReadPlanEntry]:
# 2 days ago
return self.db.meals.get_slice(self.session, start, end, group_id=self.group_id)
def get_today(self) -> list[ReadPlanEntry]:
return self.db.meals.get_today(self.session, group_id=self.group_id)
def create_one(self, data: CreatePlanEntry) -> ReadPlanEntry:
data = self.cast(data, SavePlanEntry)
return self._create_one(data)
def update_one(self, data: UpdatePlanEntry, id: int = None) -> ReadPlanEntry:
target_id = id or self.item.id
return self._update_one(data, target_id)
def delete_one(self, id: int = None) -> ReadPlanEntry:
target_id = id or self.item.id
return self._delete_one(target_id)

View File

@@ -1,23 +1,25 @@
from __future__ import annotations
from fastapi import HTTPException, status
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.group import ReadWebhook
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_group_event
logger = get_logger(module=__name__)
class WebhookService(UserHttpService[int, ReadWebhook]):
class WebhookService(UserHttpService[int, ReadWebhook], CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadWebhook
_create_schema = CreateWebhook
_update_schema = CreateWebhook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().webhooks
def populate_item(self, id: int) -> ReadWebhook:
self.item = self.db.webhooks.get_one(self.session, id)
@@ -27,29 +29,11 @@ class WebhookService(UserHttpService[int, ReadWebhook]):
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
def create_one(self, data: CreateWebhook) -> ReadWebhook:
try:
self.item = self.db.webhooks.create(self.session, SaveWebhook(group_id=self.group_id, **data.dict()))
except Exception as ex:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail={"message": "WEBHOOK_CREATION_ERROR", "exception": str(ex)}
)
data = self.cast(data, SaveWebhook)
return self._create_one(data)
return self.item
def update_one(self, data: CreateWebhook, id: int = None) -> ReadWebhook:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db.webhooks.update(self.session, target_id, data)
return self.item
def update_one(self, data: CreateWebhook, item_id: int = None) -> ReadWebhook:
return self._update_one(data, item_id)
def delete_one(self, id: int = None) -> ReadWebhook:
if not self.item:
return
target_id = id or self.item.id
self.db.webhooks.delete(self.session, target_id)
return self.item
return self._delete_one(id)

View File

@@ -7,7 +7,7 @@ from mealie.schema.user.registration import CreateUserRegistration
from mealie.schema.user.user import GroupBase, GroupInDB, PrivateUser, UserIn
from mealie.services._base_http_service.http_services import PublicHttpService
from mealie.services.events import create_user_event
from mealie.services.group_services.group_mixins import create_new_group
from mealie.services.group_services.group_utils import create_new_group
logger = get_logger(module=__name__)