mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-04 15:03:10 -05:00
feat(backend): 🚧 stub out new exporter service (WIP) (#715)
* chore(backend): 🎨 add isort path to vscode settings * style(frontend): 💄 remove fab and add general create button * feat(backend): 🚧 stub out new exporter service * comment out stub tests Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
7
mealie/services/_base_service/__init__.py
Normal file
7
mealie/services/_base_service/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self) -> None:
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
1
mealie/services/admin/__init__.py
Normal file
1
mealie/services/admin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .backup_service import *
|
||||
36
mealie/services/admin/backup_service.py
Normal file
36
mealie/services/admin/backup_service.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import operator
|
||||
|
||||
from mealie.schema.admin.backup import AllBackups, BackupFile, CreateBackup
|
||||
from mealie.services._base_http_service import AdminHttpService
|
||||
from mealie.services.events import create_backup_event
|
||||
|
||||
from .exporter import Exporter
|
||||
|
||||
|
||||
class BackupHttpService(AdminHttpService):
|
||||
event_func = create_backup_event
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exporter = Exporter()
|
||||
|
||||
def get_all(self) -> AllBackups:
|
||||
imports = []
|
||||
for archive in self.app_dirs.BACKUP_DIR.glob("*.zip"):
|
||||
backup = BackupFile(name=archive.name, date=archive.stat().st_ctime)
|
||||
imports.append(backup)
|
||||
|
||||
templates = [template.name for template in self.app_dirs.TEMPLATE_DIR.glob("*.*")]
|
||||
imports.sort(key=operator.attrgetter("date"), reverse=True)
|
||||
|
||||
return AllBackups(imports=imports, templates=templates)
|
||||
|
||||
def create_one(self, options: CreateBackup):
|
||||
pass
|
||||
|
||||
def delete_one(self):
|
||||
pass
|
||||
|
||||
|
||||
class BackupService:
|
||||
pass
|
||||
2
mealie/services/admin/exporter.py
Normal file
2
mealie/services/admin/exporter.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class Exporter:
|
||||
pass
|
||||
21
mealie/services/admin/import_service.py
Normal file
21
mealie/services/admin/import_service.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from mealie.services._base_http_service import AdminHttpService
|
||||
|
||||
from .importer import Importer
|
||||
|
||||
|
||||
class ImportHttpService(AdminHttpService):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exporter = Importer()
|
||||
|
||||
def get_all(self):
|
||||
pass
|
||||
|
||||
def get_one(self):
|
||||
pass
|
||||
|
||||
def create(self):
|
||||
pass
|
||||
|
||||
def delete_one(self):
|
||||
pass
|
||||
2
mealie/services/admin/importer.py
Normal file
2
mealie/services/admin/importer.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class Importer:
|
||||
pass
|
||||
@@ -23,7 +23,6 @@ class ExportDatabase:
|
||||
with any supported backend database platform. By default tags are timestamps, and no
|
||||
Jinja2 templates are rendered
|
||||
|
||||
|
||||
Args:
|
||||
tag ([str], optional): A str to be used as a file tag. Defaults to None.
|
||||
templates (list, optional): A list of template file names. Defaults to None.
|
||||
|
||||
@@ -35,17 +35,29 @@ class GroupSelfService(UserHttpService[int, str]):
|
||||
self.item = self.db.groups.get(self.group_id)
|
||||
return self.item
|
||||
|
||||
# ====================================================================
|
||||
# Meal Categories
|
||||
|
||||
def update_categories(self, new_categories: list[CategoryBase]):
|
||||
self.item.categories = new_categories
|
||||
return self.db.groups.update(self.group_id, self.item)
|
||||
|
||||
# ====================================================================
|
||||
# Preferences
|
||||
|
||||
def update_preferences(self, new_preferences: UpdateGroupPreferences):
|
||||
self.db.group_preferences.update(self.group_id, new_preferences)
|
||||
return self.populate_item()
|
||||
|
||||
# ====================================================================
|
||||
# Group Invites
|
||||
|
||||
def create_invite_token(self, uses: int = 1) -> None:
|
||||
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
|
||||
return self.db.group_invite_tokens.create(token)
|
||||
|
||||
def get_invite_tokens(self) -> list[ReadInviteToken]:
|
||||
return self.db.group_invite_tokens.multi_query({"group_id": self.group_id})
|
||||
|
||||
# ====================================================================
|
||||
# Export / Import Recipes
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import json
|
||||
import shutil
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from shutil import copytree, rmtree
|
||||
from typing import Union
|
||||
from zipfile import ZipFile
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, UploadFile, status
|
||||
|
||||
from mealie.core.dependencies.grouped import PublicDeps, UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
@@ -12,8 +15,11 @@ from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
|
||||
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_recipe_event
|
||||
from mealie.services.image.image import write_image
|
||||
from mealie.services.recipe.mixins import recipe_creation_factory
|
||||
|
||||
from .template_service import TemplateService
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
@@ -65,6 +71,33 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic
|
||||
)
|
||||
return self.item
|
||||
|
||||
def create_from_zip(self, archive: UploadFile, temp_path: Path) -> Recipe:
|
||||
"""
|
||||
`create_from_zip` creates a recipe in the database from a zip file exported from Mealie. This is NOT
|
||||
a generic import from a zip file.
|
||||
"""
|
||||
with temp_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(archive.file, buffer)
|
||||
|
||||
recipe_dict = None
|
||||
recipe_image = None
|
||||
|
||||
with ZipFile(temp_path) as myzip:
|
||||
for file in myzip.namelist():
|
||||
if file.endswith(".json"):
|
||||
with myzip.open(file) as myfile:
|
||||
recipe_dict = json.loads(myfile.read())
|
||||
elif file.endswith(".webp"):
|
||||
with myzip.open(file) as myfile:
|
||||
recipe_image = myfile.read()
|
||||
|
||||
self.create_one(Recipe(**recipe_dict))
|
||||
|
||||
if self.item:
|
||||
write_image(self.item.slug, recipe_image, "webp")
|
||||
|
||||
return self.item
|
||||
|
||||
def update_one(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
self._update_one(update_data, original_slug)
|
||||
@@ -107,3 +140,10 @@ class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpServic
|
||||
recipe_dir = self.item.directory
|
||||
rmtree(recipe_dir, ignore_errors=True)
|
||||
logger.info(f"Recipe Directory Removed: {self.item.slug}")
|
||||
|
||||
# =================================================================
|
||||
# Recipe Template Methods
|
||||
|
||||
def render_template(self, temp_dir: Path, template: str = None) -> Path:
|
||||
t_service = TemplateService(temp_dir)
|
||||
return t_service.render(self.item, template)
|
||||
|
||||
133
mealie/services/recipe/template_service.py
Normal file
133
mealie/services/recipe/template_service.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import enum
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.recipe.recipe_image_types import RecipeImageTypes
|
||||
from mealie.services._base_service import BaseService
|
||||
|
||||
|
||||
class TemplateType(str, enum.Enum):
|
||||
json = "json"
|
||||
jinja2 = "jinja2"
|
||||
zip = "zip"
|
||||
|
||||
|
||||
class TemplateService(BaseService):
|
||||
def __init__(self, temp: Path = None) -> None:
|
||||
"""Creates a template service that can be used for multiple template generations
|
||||
A temporary directory must be provided as a place holder for where to render all templates
|
||||
Args:
|
||||
temp (Path): [description]
|
||||
"""
|
||||
self.temp = temp
|
||||
self.types = TemplateType
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def templates(self) -> list:
|
||||
"""
|
||||
Returns a list of all templates available to render.
|
||||
"""
|
||||
return {
|
||||
TemplateType.jinja2.value: [x.name for x in self.app_dirs.TEMPLATE_DIR.iterdir() if x.is_file()],
|
||||
TemplateType.json.value: ["raw"],
|
||||
TemplateType.zip.value: ["zip"],
|
||||
}
|
||||
|
||||
def __check_temp(self, method) -> None:
|
||||
"""
|
||||
Checks if the temporary directory was provided on initialization
|
||||
"""
|
||||
|
||||
if self.temp is None:
|
||||
raise ValueError(f"Temporary directory must be provided for method {method.__name__}")
|
||||
|
||||
def template_type(self, template: str) -> TemplateType:
|
||||
# Determine Type:
|
||||
t_type = None
|
||||
for key, value in self.templates.items():
|
||||
if template in value:
|
||||
t_type = key
|
||||
break
|
||||
|
||||
if t_type is None:
|
||||
raise ValueError(f"Template '{template}' not found.")
|
||||
|
||||
return TemplateType(t_type)
|
||||
|
||||
def render(self, recipe: Recipe, template: str = None) -> Path:
|
||||
"""
|
||||
Renders a TemplateType in a temporary directory and returns the path to the file.
|
||||
|
||||
Args:
|
||||
t_type (TemplateType): The type of template to render
|
||||
recipe (Recipe): The recipe to render
|
||||
template (str): The template to render **Required for Jinja2 Templates**
|
||||
"""
|
||||
t_type = self.template_type(template)
|
||||
|
||||
if t_type == TemplateType.json:
|
||||
return self._render_json(recipe)
|
||||
|
||||
if t_type == TemplateType.jinja2:
|
||||
return self._render_jinja2(recipe, template)
|
||||
|
||||
if t_type == TemplateType.zip:
|
||||
return self._render_zip(recipe)
|
||||
|
||||
def _render_json(self, recipe: Recipe) -> Path:
|
||||
"""
|
||||
Renders a JSON file in a temporary directory and returns
|
||||
the path to the file.
|
||||
"""
|
||||
self.__check_temp(self._render_json)
|
||||
|
||||
save_path = self.temp.joinpath(f"{recipe.slug}.json")
|
||||
with open(save_path, "w") as f:
|
||||
f.write(recipe.json(indent=4, by_alias=True))
|
||||
|
||||
return save_path
|
||||
|
||||
def _render_jinja2(self, recipe: Recipe, j2_template: str = None) -> Path:
|
||||
"""
|
||||
Renders a Jinja2 Template in a temporary directory and returns
|
||||
the path to the file.
|
||||
"""
|
||||
self.__check_temp(self._render_jinja2)
|
||||
|
||||
j2_template: Path = self.app_dirs.TEMPLATE_DIR / j2_template
|
||||
|
||||
if not j2_template.is_file():
|
||||
raise FileNotFoundError(f"Template '{j2_template}' not found.")
|
||||
|
||||
with open(j2_template, "r") as f:
|
||||
template_text = f.read()
|
||||
|
||||
template = Template(template_text)
|
||||
rendered_text = template.render(recipe=recipe.dict(by_alias=True))
|
||||
|
||||
save_name = f"{recipe.slug}{j2_template.suffix}"
|
||||
|
||||
save_path = self.temp.joinpath(save_name)
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
f.write(rendered_text)
|
||||
|
||||
return save_path
|
||||
|
||||
def _render_zip(self, recipe: Recipe) -> Path:
|
||||
self.__check_temp(self._render_jinja2)
|
||||
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
zip_temp = self.temp.joinpath(f"{recipe.slug}.zip")
|
||||
|
||||
with ZipFile(zip_temp, "w") as myzip:
|
||||
myzip.writestr(f"{recipe.slug}.json", recipe.json())
|
||||
|
||||
if image_asset.is_file():
|
||||
myzip.write(image_asset, arcname=image_asset.name)
|
||||
|
||||
return zip_temp
|
||||
Reference in New Issue
Block a user