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:
Hayden
2021-10-02 11:37:04 -08:00
committed by GitHub
parent 476aefeeb0
commit 4bdba9f3af
26 changed files with 714 additions and 50 deletions

View 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()

View File

@@ -0,0 +1 @@
from .backup_service import *

View 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

View File

@@ -0,0 +1,2 @@
class Exporter:
pass

View 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

View File

@@ -0,0 +1,2 @@
class Importer:
pass

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View 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