Coverage for opt/mealie/lib/python3.12/site-packages/mealie/services/migrations/tandoor.py: 17%
86 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:32 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-25 15:32 +0000
1import json 1a
2import os 1a
3import tempfile 1a
4import zipfile 1a
5from pathlib import Path 1a
6from typing import Any 1a
8from mealie.schema.recipe.recipe_ingredient import RecipeIngredientBase 1a
9from mealie.schema.reports.reports import ReportEntryCreate 1a
11from ._migration_base import BaseMigrator 1a
12from .utils.migration_alias import MigrationAlias 1a
13from .utils.migration_helpers import format_time 1a
16def _build_ingredient_from_ingredient_data(ingredient_data: dict[str, Any], title: str | None = None) -> dict[str, Any]: 1a
17 quantity = ingredient_data.get("amount", "1")
18 if unit_data := ingredient_data.get("unit"):
19 unit = unit_data.get("plural_name") or unit_data.get("name")
20 else:
21 unit = None
23 if food_data := ingredient_data.get("food"):
24 food = food_data.get("plural_name") or food_data.get("name")
25 else:
26 food = None
28 base_ingredient = RecipeIngredientBase(quantity=quantity, unit=unit, food=food)
29 return {"title": title, "note": base_ingredient.display}
32def extract_instructions_and_ingredients(steps: list[dict[str, Any]]) -> tuple[list[str], list[dict[str, Any]]]: 1a
33 """Returns a list of instructions and ingredients for a recipe"""
35 instructions: list[str] = []
36 ingredients: list[dict[str, Any]] = []
37 for step in steps:
38 if instruction_text := step.get("instruction"):
39 instructions.append(instruction_text)
40 if ingredients_data := step.get("ingredients"):
41 for i, ingredient in enumerate(ingredients_data):
42 if not i and (title := step.get("name")):
43 ingredients.append(_build_ingredient_from_ingredient_data(ingredient, title))
44 else:
45 ingredients.append(_build_ingredient_from_ingredient_data(ingredient))
47 return instructions, ingredients
50def parse_times(working_time: int, waiting_time: int) -> tuple[str, str]: 1a
51 """Returns the performTime and totalTime"""
53 total_time = working_time + waiting_time
54 return format_time(working_time), format_time(total_time)
57class TandoorMigrator(BaseMigrator): 1a
58 def __init__(self, **kwargs): 1a
59 super().__init__(**kwargs)
61 self.name = "tandoor"
63 self.key_aliases = [
64 MigrationAlias(key="tags", alias="keywords", func=lambda kws: [kw["name"] for kw in kws if kw.get("name")]),
65 MigrationAlias(key="orgURL", alias="source_url", func=None),
66 ]
68 def _process_recipe_document(self, source_dir: Path, recipe_data: dict) -> dict: 1a
69 steps_data = recipe_data.pop("steps", [])
70 recipe_data["recipeInstructions"], recipe_data["recipeIngredient"] = extract_instructions_and_ingredients(
71 steps_data
72 )
73 recipe_data["performTime"], recipe_data["totalTime"] = parse_times(
74 recipe_data.pop("working_time", 0), recipe_data.pop("waiting_time", 0)
75 )
77 recipe_data["recipeYieldQuantity"] = recipe_data.pop("servings", 0)
78 recipe_data["recipeYield"] = recipe_data.pop("servings_text", "")
80 try:
81 recipe_image_path = next(source_dir.glob("image.*"))
82 recipe_data["image"] = str(recipe_image_path)
83 except StopIteration:
84 pass
85 return recipe_data
87 def _migrate(self) -> None: 1a
88 with tempfile.TemporaryDirectory() as tmpdir:
89 with zipfile.ZipFile(self.archive) as zip_file:
90 zip_file.extractall(tmpdir)
92 source_dir = self.get_zip_base_path(Path(tmpdir))
94 recipes_as_dicts: list[dict] = []
95 for i, recipe_zip_file in enumerate(source_dir.glob("*.zip")):
96 try:
97 recipe_dir = str(source_dir.joinpath(f"recipe_{i + 1}"))
98 os.makedirs(recipe_dir)
100 with zipfile.ZipFile(recipe_zip_file) as recipe_zip:
101 recipe_zip.extractall(recipe_dir)
103 recipe_source_dir = Path(recipe_dir)
104 try:
105 recipe_json_path = next(recipe_source_dir.glob("*.json"))
106 except StopIteration as e:
107 raise Exception("recipe.json not found") from e
109 with open(recipe_json_path) as f:
110 recipes_as_dicts.append(self._process_recipe_document(recipe_source_dir, json.load(f)))
112 except Exception as e:
113 self.report_entries.append(
114 ReportEntryCreate(
115 report_id=self.report_id,
116 success=False,
117 message="Failed to parse recipe",
118 exception=f"{type(e).__name__}: {e}",
119 )
120 )
122 recipes = [self.clean_recipe_dictionary(x) for x in recipes_as_dicts]
123 results = self.import_recipes_to_database(recipes)
124 recipe_lookup = {r.slug: r for r in recipes}
125 for slug, recipe_id, status in results:
126 if status:
127 try:
128 r = recipe_lookup.get(slug)
129 if not r or not r.image:
130 continue
132 except StopIteration:
133 continue
135 self.import_image(slug, r.image, recipe_id)