mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat: adding the rest ofthe nutrition properties from schema.org (#4301)
This commit is contained in:
		| @@ -0,0 +1,39 @@ | ||||
| """'add the rest of the schema.org nutrition properties' | ||||
|  | ||||
| Revision ID: 602927e1013e | ||||
| Revises: 1fe4bd37ccc8 | ||||
| Create Date: 2024-10-01 14:17:00.611398 | ||||
|  | ||||
| """ | ||||
|  | ||||
| import sqlalchemy as sa | ||||
|  | ||||
| from alembic import op | ||||
|  | ||||
| # revision identifiers, used by Alembic. | ||||
| revision = "602927e1013e" | ||||
| down_revision: str | None = "1fe4bd37ccc8" | ||||
| branch_labels: str | tuple[str, ...] | None = None | ||||
| depends_on: str | tuple[str, ...] | None = None | ||||
|  | ||||
|  | ||||
| def upgrade(): | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     with op.batch_alter_table("recipe_nutrition", schema=None) as batch_op: | ||||
|         batch_op.add_column(sa.Column("cholesterol_content", sa.String(), nullable=True)) | ||||
|         batch_op.add_column(sa.Column("saturated_fat_content", sa.String(), nullable=True)) | ||||
|         batch_op.add_column(sa.Column("trans_fat_content", sa.String(), nullable=True)) | ||||
|         batch_op.add_column(sa.Column("unsaturated_fat_content", sa.String(), nullable=True)) | ||||
|  | ||||
|     # ### end Alembic commands ### | ||||
|  | ||||
|  | ||||
| def downgrade(): | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     with op.batch_alter_table("recipe_nutrition", schema=None) as batch_op: | ||||
|         batch_op.drop_column("unsaturated_fat_content") | ||||
|         batch_op.drop_column("trans_fat_content") | ||||
|         batch_op.drop_column("saturated_fat_content") | ||||
|         batch_op.drop_column("cholesterol_content") | ||||
|  | ||||
|     # ### end Alembic commands ### | ||||
| @@ -17,6 +17,14 @@ export function useNutritionLabels() { | ||||
|       label: i18n.tc("recipe.calories"), | ||||
|       suffix: i18n.tc("recipe.calories-suffix"), | ||||
|     }, | ||||
|     carbohydrateContent: { | ||||
|       label: i18n.tc("recipe.carbohydrate-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|     cholesterolContent: { | ||||
|       label: i18n.tc("recipe.cholesterol-content"), | ||||
|       suffix: i18n.tc("recipe.milligrams"), | ||||
|     }, | ||||
|     fatContent: { | ||||
|       label: i18n.tc("recipe.fat-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
| @@ -29,6 +37,10 @@ export function useNutritionLabels() { | ||||
|       label: i18n.tc("recipe.protein-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|     saturatedFatContent: { | ||||
|       label: i18n.tc("recipe.saturated-fat-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|     sodiumContent: { | ||||
|       label: i18n.tc("recipe.sodium-content"), | ||||
|       suffix: i18n.tc("recipe.milligrams"), | ||||
| @@ -37,8 +49,12 @@ export function useNutritionLabels() { | ||||
|       label: i18n.tc("recipe.sugar-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|     carbohydrateContent: { | ||||
|       label: i18n.tc("recipe.carbohydrate-content"), | ||||
|     transFatContent: { | ||||
|       label: i18n.tc("recipe.trans-fat-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|     unsaturatedFatContent: { | ||||
|       label: i18n.tc("recipe.unsaturated-fat-content"), | ||||
|       suffix: i18n.tc("recipe.grams"), | ||||
|     }, | ||||
|   }; | ||||
|   | ||||
| @@ -461,6 +461,7 @@ | ||||
|     "calories-suffix": "calories", | ||||
|     "carbohydrate-content": "Carbohydrate", | ||||
|     "categories": "Categories", | ||||
|     "cholesterol-content": "Cholesterol", | ||||
|     "comment-action": "Comment", | ||||
|     "comment": "Comment", | ||||
|     "comments": "Comments", | ||||
| @@ -507,6 +508,7 @@ | ||||
|     "recipe-updated": "Recipe updated", | ||||
|     "remove-from-favorites": "Remove from Favorites", | ||||
|     "remove-section": "Remove Section", | ||||
|     "saturated-fat-content": "Saturated fat", | ||||
|     "save-recipe-before-use": "Save recipe before use", | ||||
|     "section-title": "Section Title", | ||||
|     "servings": "Servings", | ||||
| @@ -517,7 +519,9 @@ | ||||
|     "sugar-content": "Sugar", | ||||
|     "title": "Title", | ||||
|     "total-time": "Total Time", | ||||
|     "trans-fat-content": "Trans-fat", | ||||
|     "unable-to-delete-recipe": "Unable to Delete Recipe", | ||||
|     "unsaturated-fat-content": "Unsaturated fat", | ||||
|     "no-recipe": "No Recipe", | ||||
|     "locked-by-owner": "Locked by Owner", | ||||
|     "join-the-conversation": "Join the Conversation", | ||||
|   | ||||
| @@ -194,12 +194,16 @@ export interface MergeUnit { | ||||
| } | ||||
| export interface Nutrition { | ||||
|   calories?: string | null; | ||||
|   fatContent?: string | null; | ||||
|   proteinContent?: string | null; | ||||
|   carbohydrateContent?: string | null; | ||||
|   cholesterolContent?: string | null; | ||||
|   fatContent?: string | null; | ||||
|   fiberContent?: string | null; | ||||
|   proteinContent?: string | null; | ||||
|   saturatedFatContent?: string | null; | ||||
|   sodiumContent?: string | null; | ||||
|   sugarContent?: string | null; | ||||
|   transFatContent?: string | null; | ||||
|   unsaturatedFatContent?: string | null; | ||||
| } | ||||
| export interface ParsedIngredient { | ||||
|   input?: string | null; | ||||
|   | ||||
| @@ -9,28 +9,52 @@ class Nutrition(SqlAlchemyBase): | ||||
|     __tablename__ = "recipe_nutrition" | ||||
|     id: Mapped[int] = mapped_column(sa.Integer, primary_key=True) | ||||
|     recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True) | ||||
|  | ||||
|     calories: Mapped[str | None] = mapped_column(sa.String) | ||||
|     carbohydrate_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     cholesterol_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     fat_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     fiber_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     protein_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     carbohydrate_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     saturated_fat_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|  | ||||
|     # `serving_size` is not a scaling factor, but a per-serving volume or mass | ||||
|     # according to schema.org. E.g., "2 L", "500 g", "5 cups", etc. | ||||
|     # | ||||
|     # Ignoring for now because it's too difficult to work around variable units | ||||
|     # in translation for the frontend. Also, it causes cognitive dissonance wrt | ||||
|     # "servings" (i.e., "serves 2" etc.), which is an unrelated concept that | ||||
|     # might cause confusion. | ||||
|     # | ||||
|     # serving_size: Mapped[str | None] = mapped_column(sa.String) | ||||
|  | ||||
|     sodium_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     sugar_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     trans_fat_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|     unsaturated_fat_content: Mapped[str | None] = mapped_column(sa.String) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         calories=None, | ||||
|         carbohydrate_content=None, | ||||
|         cholesterol_content=None, | ||||
|         fat_content=None, | ||||
|         fiber_content=None, | ||||
|         protein_content=None, | ||||
|         saturated_fat_content=None, | ||||
|         sodium_content=None, | ||||
|         sugar_content=None, | ||||
|         carbohydrate_content=None, | ||||
|         trans_fat_content=None, | ||||
|         unsaturated_fat_content=None, | ||||
|     ) -> None: | ||||
|         self.calories = calories | ||||
|         self.carbohydrate_content = carbohydrate_content | ||||
|         self.cholesterol_content = cholesterol_content | ||||
|         self.fat_content = fat_content | ||||
|         self.fiber_content = fiber_content | ||||
|         self.protein_content = protein_content | ||||
|         self.saturated_fat_content = saturated_fat_content | ||||
|         self.sodium_content = sodium_content | ||||
|         self.sugar_content = sugar_content | ||||
|         self.carbohydrate_content = carbohydrate_content | ||||
|         self.trans_fat_content = trans_fat_content | ||||
|         self.unsaturated_fat_content = unsaturated_fat_content | ||||
|   | ||||
| @@ -187,7 +187,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | ||||
|         settings: dict | None = None, | ||||
|         **_, | ||||
|     ) -> None: | ||||
|         self.nutrition = Nutrition(**nutrition) if nutrition else Nutrition() | ||||
|         self.nutrition = Nutrition(**(nutrition or {})) | ||||
|  | ||||
|         if recipe_instructions is not None: | ||||
|             self.recipe_instructions = [RecipeInstruction(**step, session=session) for step in recipe_instructions] | ||||
| @@ -198,7 +198,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | ||||
|         if assets: | ||||
|             self.assets = [RecipeAsset(**a) for a in assets] | ||||
|  | ||||
|         self.settings = RecipeSettings(**settings) if settings else RecipeSettings() | ||||
|         self.settings = RecipeSettings(**(settings or {})) | ||||
|  | ||||
|         if notes: | ||||
|             self.notes = [Note(**n) for n in notes] | ||||
|   | ||||
| @@ -104,15 +104,7 @@ def content_with_meta(group_slug: str, recipe: Recipe) -> str: | ||||
|  | ||||
|             ingredients.append(s) | ||||
|  | ||||
|     nutrition: dict[str, str | None] = {} | ||||
|     if recipe.nutrition: | ||||
|         nutrition["calories"] = recipe.nutrition.calories | ||||
|         nutrition["fatContent"] = recipe.nutrition.fat_content | ||||
|         nutrition["fiberContent"] = recipe.nutrition.fiber_content | ||||
|         nutrition["proteinContent"] = recipe.nutrition.protein_content | ||||
|         nutrition["carbohydrateContent"] = recipe.nutrition.carbohydrate_content | ||||
|         nutrition["sodiumContent"] = recipe.nutrition.sodium_content | ||||
|         nutrition["sugarContent"] = recipe.nutrition.sugar_content | ||||
|     nutrition: dict[str, str | None] = recipe.nutrition.model_dump(by_alias=True) if recipe.nutrition else {} | ||||
|  | ||||
|     as_schema_org = { | ||||
|         "@context": "https://schema.org", | ||||
|   | ||||
| @@ -1,14 +1,24 @@ | ||||
| from pydantic import ConfigDict | ||||
| from pydantic.alias_generators import to_camel | ||||
|  | ||||
| from mealie.schema._mealie import MealieModel | ||||
|  | ||||
|  | ||||
| class Nutrition(MealieModel): | ||||
|     calories: str | None = None | ||||
|     fat_content: str | None = None | ||||
|     protein_content: str | None = None | ||||
|     carbohydrate_content: str | None = None | ||||
|     cholesterol_content: str | None = None | ||||
|     fat_content: str | None = None | ||||
|     fiber_content: str | None = None | ||||
|     protein_content: str | None = None | ||||
|     saturated_fat_content: str | None = None | ||||
|     sodium_content: str | None = None | ||||
|     sugar_content: str | None = None | ||||
|     model_config = ConfigDict(from_attributes=True, coerce_numbers_to_str=True) | ||||
|     trans_fat_content: str | None = None | ||||
|     unsaturated_fat_content: str | None = None | ||||
|  | ||||
|     model_config = ConfigDict( | ||||
|         from_attributes=True, | ||||
|         coerce_numbers_to_str=True, | ||||
|         alias_generator=to_camel, | ||||
|     ) | ||||
|   | ||||
| @@ -12,6 +12,18 @@ from mealie.services.scraper import cleaner | ||||
| from ._migration_base import BaseMigrator | ||||
| from .utils.migration_helpers import scrape_image, split_by_line_break, split_by_semicolon | ||||
|  | ||||
| nutrition_map = { | ||||
|     "carbohydrate": "carbohydrateContent", | ||||
|     "protein": "proteinContent", | ||||
|     "fat": "fatContent", | ||||
|     "saturatedfat": "saturatedFatContent", | ||||
|     "transfat": "transFatContent", | ||||
|     "sodium": "sodiumContent", | ||||
|     "fiber": "fiberContent", | ||||
|     "sugar": "sugarContent", | ||||
|     "unsaturatedfat": "unsaturatedFatContent", | ||||
| } | ||||
|  | ||||
|  | ||||
| class MyRecipeBoxMigrator(BaseMigrator): | ||||
|     def __init__(self, **kwargs): | ||||
| @@ -53,22 +65,26 @@ class MyRecipeBoxMigrator(BaseMigrator): | ||||
|         except Exception: | ||||
|             return None | ||||
|  | ||||
|     def parse_nutrition(self, input: Any) -> dict | None: | ||||
|         if not input or not isinstance(input, str): | ||||
|     def parse_nutrition(self, input_: Any) -> dict | None: | ||||
|         if not input_ or not isinstance(input_, str): | ||||
|             return None | ||||
|  | ||||
|         nutrition = {} | ||||
|  | ||||
|         vals = [x.strip() for x in input.split(",") if x] | ||||
|         vals = (x.strip() for x in input_.split("\n") if x) | ||||
|         for val in vals: | ||||
|             try: | ||||
|                 key, value = val.split(":", maxsplit=1) | ||||
|                 key, value = (x.strip() for x in val.split(":", maxsplit=1)) | ||||
|  | ||||
|                 if not (key and value): | ||||
|                     continue | ||||
|  | ||||
|                 key = nutrition_map.get(key.lower(), key) | ||||
|  | ||||
|             except ValueError: | ||||
|                 continue | ||||
|  | ||||
|             nutrition[key.strip()] = value.strip() | ||||
|             nutrition[key] = value | ||||
|  | ||||
|         return cleaner.clean_nutrition(nutrition) if nutrition else None | ||||
|  | ||||
|   | ||||
| @@ -37,6 +37,19 @@ def get_value_as_string_or_none(dictionary: dict, key: str): | ||||
|         return None | ||||
|  | ||||
|  | ||||
| nutrition_map = { | ||||
|     "Calories": "calories", | ||||
|     "Fat": "fatContent", | ||||
|     "Saturated Fat": "saturatedFatContent", | ||||
|     "Cholesterol": "cholesterolContent", | ||||
|     "Sodium": "sodiumContent", | ||||
|     "Sugar": "sugarContent", | ||||
|     "Carbohydrate": "carbohydrateContent", | ||||
|     "Fiber": "fiberContent", | ||||
|     "Protein": "proteinContent", | ||||
| } | ||||
|  | ||||
|  | ||||
| class PlanToEatMigrator(BaseMigrator): | ||||
|     def __init__(self, **kwargs): | ||||
|         super().__init__(**kwargs) | ||||
| @@ -63,16 +76,7 @@ class PlanToEatMigrator(BaseMigrator): | ||||
|  | ||||
|     def _parse_recipe_nutrition_from_row(self, row: dict) -> dict: | ||||
|         """Parses the nutrition data from the row""" | ||||
|  | ||||
|         nut_dict: dict = {} | ||||
|  | ||||
|         nut_dict["calories"] = get_value_as_string_or_none(row, "Calories") | ||||
|         nut_dict["fatContent"] = get_value_as_string_or_none(row, "Fat") | ||||
|         nut_dict["proteinContent"] = get_value_as_string_or_none(row, "Protein") | ||||
|         nut_dict["carbohydrateContent"] = get_value_as_string_or_none(row, "Carbohydrate") | ||||
|         nut_dict["fiberContent"] = get_value_as_string_or_none(row, "Fiber") | ||||
|         nut_dict["sodiumContent"] = get_value_as_string_or_none(row, "Sodium") | ||||
|         nut_dict["sugarContent"] = get_value_as_string_or_none(row, "Sugar") | ||||
|         nut_dict = {normalized_k: row[k] for k, normalized_k in nutrition_map.items() if k in row} | ||||
|  | ||||
|         return cleaner.clean_nutrition(nut_dict) | ||||
|  | ||||
|   | ||||
| @@ -495,7 +495,7 @@ def clean_nutrition(nutrition: dict | None) -> dict[str, str]: | ||||
|     list of valid keys | ||||
|  | ||||
|     Assumptionas: | ||||
|         - All units are supplied in grams, expect sodium which maybe be in milligrams | ||||
|         - All units are supplied in grams, expect sodium and cholesterol which maybe be in milligrams | ||||
|  | ||||
|     Returns: | ||||
|         dict[str, str]: If the argument is None, or not a dictionary, an empty dictionary is returned | ||||
| @@ -509,9 +509,10 @@ def clean_nutrition(nutrition: dict | None) -> dict[str, str]: | ||||
|             if matched_digits := MATCH_DIGITS.search(val): | ||||
|                 output_nutrition[key] = matched_digits.group(0).replace(",", ".") | ||||
|  | ||||
|     if sodium := nutrition.get("sodiumContent", None): | ||||
|         if isinstance(sodium, str) and "m" not in sodium and "g" in sodium: | ||||
|     for key in ["sodiumContent", "cholesterolContent"]: | ||||
|         if val := nutrition.get(key, None): | ||||
|             if isinstance(val, str) and "m" not in val and "g" in val: | ||||
|                 with contextlib.suppress(AttributeError, TypeError): | ||||
|                 output_nutrition["sodiumContent"] = str(float(output_nutrition["sodiumContent"]) * 1000) | ||||
|                     output_nutrition[key] = str(float(output_nutrition[key]) * 1000) | ||||
|  | ||||
|     return output_nutrition | ||||
|   | ||||
							
								
								
									
										13
									
								
								tests/fixtures/fixture_users.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								tests/fixtures/fixture_users.py
									
									
									
									
										vendored
									
									
								
							| @@ -173,8 +173,7 @@ def g2_user(session: Session, admin_token, api_client: TestClient): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| @fixture(scope="module") | ||||
| def unique_user(session: Session, api_client: TestClient): | ||||
| def _unique_user(session: Session, api_client: TestClient): | ||||
|     registration = utils.user_registration_factory() | ||||
|     response = api_client.post("/api/users/register", json=registration.model_dump(by_alias=True)) | ||||
|     assert response.status_code == 201 | ||||
| @@ -213,6 +212,16 @@ def unique_user(session: Session, api_client: TestClient): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| @fixture(scope="function") | ||||
| def unique_user_fn_scoped(session: Session, api_client: TestClient): | ||||
|     yield from _unique_user(session, api_client) | ||||
|  | ||||
|  | ||||
| @fixture(scope="module") | ||||
| def unique_user(session: Session, api_client: TestClient): | ||||
|     yield from _unique_user(session, api_client) | ||||
|  | ||||
|  | ||||
| @fixture(scope="module") | ||||
| def user_tuple(session: Session, admin_token, api_client: TestClient) -> Generator[list[utils.TestUser], None, None]: | ||||
|     group_name = utils.random_string() | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import os | ||||
| from dataclasses import dataclass | ||||
| from dataclasses import dataclass, field | ||||
| from pathlib import Path | ||||
| from tempfile import TemporaryDirectory | ||||
| from zipfile import ZipFile | ||||
| @@ -8,6 +8,7 @@ import pytest | ||||
| from fastapi.testclient import TestClient | ||||
|  | ||||
| from mealie.schema.group.group_migration import SupportedMigrations | ||||
| from mealie.schema.recipe.recipe import Recipe | ||||
| from mealie.schema.reports.reports import ReportEntryOut | ||||
| from tests import data as test_data | ||||
| from tests.utils import api_routes | ||||
| @@ -19,18 +20,94 @@ from tests.utils.fixture_schemas import TestUser | ||||
| class MigrationTestData: | ||||
|     typ: SupportedMigrations | ||||
|     archive: Path | ||||
|     search_slug: str | ||||
|  | ||||
|     nutrition_filter: set[str] = field(default_factory=set) | ||||
|     nutrition_entries: set[str] = field( | ||||
|         default_factory=lambda: { | ||||
|             "calories", | ||||
|             "carbohydrateContent", | ||||
|             "cholesterolContent", | ||||
|             "fatContent", | ||||
|             "fiberContent", | ||||
|             "proteinContent", | ||||
|             "saturatedFatContent", | ||||
|             "sodiumContent", | ||||
|             "sugarContent", | ||||
|             "transFatContent", | ||||
|             "unsaturatedFatContent", | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|  | ||||
| test_cases = [ | ||||
|     MigrationTestData(typ=SupportedMigrations.nextcloud, archive=test_data.migrations_nextcloud), | ||||
|     MigrationTestData(typ=SupportedMigrations.paprika, archive=test_data.migrations_paprika), | ||||
|     MigrationTestData(typ=SupportedMigrations.chowdown, archive=test_data.migrations_chowdown), | ||||
|     MigrationTestData(typ=SupportedMigrations.copymethat, archive=test_data.migrations_copymethat), | ||||
|     MigrationTestData(typ=SupportedMigrations.mealie_alpha, archive=test_data.migrations_mealie), | ||||
|     MigrationTestData(typ=SupportedMigrations.tandoor, archive=test_data.migrations_tandoor), | ||||
|     MigrationTestData(typ=SupportedMigrations.plantoeat, archive=test_data.migrations_plantoeat), | ||||
|     MigrationTestData(typ=SupportedMigrations.myrecipebox, archive=test_data.migrations_myrecipebox), | ||||
|     MigrationTestData(typ=SupportedMigrations.recipekeeper, archive=test_data.migrations_recipekeeper), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.nextcloud, | ||||
|         archive=test_data.migrations_nextcloud, | ||||
|         search_slug="skillet-shepherd-s-pie", | ||||
|         nutrition_filter={ | ||||
|             "transFatContent", | ||||
|             "unsaturatedFatContent", | ||||
|         }, | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.paprika, | ||||
|         archive=test_data.migrations_paprika, | ||||
|         search_slug="zucchini-kartoffel-frittata", | ||||
|         nutrition_entries=set(), | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.chowdown, | ||||
|         archive=test_data.migrations_chowdown, | ||||
|         search_slug="roasted-okra", | ||||
|         nutrition_entries=set(), | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.copymethat, | ||||
|         archive=test_data.migrations_copymethat, | ||||
|         search_slug="spam-zoodles", | ||||
|         nutrition_entries=set(), | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.mealie_alpha, | ||||
|         archive=test_data.migrations_mealie, | ||||
|         search_slug="old-fashioned-beef-stew", | ||||
|         nutrition_filter={ | ||||
|             "cholesterolContent", | ||||
|             "saturatedFatContent", | ||||
|             "transFatContent", | ||||
|             "unsaturatedFatContent", | ||||
|         }, | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.tandoor, | ||||
|         archive=test_data.migrations_tandoor, | ||||
|         search_slug="texas-red-chili", | ||||
|         nutrition_entries=set(), | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.plantoeat, | ||||
|         archive=test_data.migrations_plantoeat, | ||||
|         search_slug="test-recipe", | ||||
|         nutrition_filter={ | ||||
|             "unsaturatedFatContent", | ||||
|             "transFatContent", | ||||
|         }, | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.myrecipebox, | ||||
|         archive=test_data.migrations_myrecipebox, | ||||
|         search_slug="beef-cheese-piroshki", | ||||
|         nutrition_filter={ | ||||
|             "cholesterolContent", | ||||
|         }, | ||||
|     ), | ||||
|     MigrationTestData( | ||||
|         typ=SupportedMigrations.recipekeeper, | ||||
|         archive=test_data.migrations_recipekeeper, | ||||
|         search_slug="zucchini-bread", | ||||
|         nutrition_entries=set(), | ||||
|     ), | ||||
| ] | ||||
|  | ||||
| test_ids = [ | ||||
| @@ -47,7 +124,8 @@ test_ids = [ | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("mig", test_cases, ids=test_ids) | ||||
| def test_recipe_migration(api_client: TestClient, unique_user: TestUser, mig: MigrationTestData) -> None: | ||||
| def test_recipe_migration(api_client: TestClient, unique_user_fn_scoped: TestUser, mig: MigrationTestData) -> None: | ||||
|     unique_user = unique_user_fn_scoped | ||||
|     payload = { | ||||
|         "migration_type": mig.typ.value, | ||||
|     } | ||||
| @@ -91,6 +169,19 @@ def test_recipe_migration(api_client: TestClient, unique_user: TestUser, mig: Mi | ||||
|     events = query_data["items"] | ||||
|     assert len(events) | ||||
|  | ||||
|     # Validate recipe content | ||||
|     response = api_client.get(api_routes.recipes_slug(mig.search_slug), headers=unique_user.token) | ||||
|     recipe = Recipe(**assert_deserialize(response)) | ||||
|  | ||||
|     if mig.nutrition_entries: | ||||
|         assert recipe.nutrition is not None | ||||
|         nutrition = recipe.nutrition.model_dump(by_alias=True) | ||||
|  | ||||
|         for k in mig.nutrition_entries.difference(mig.nutrition_filter): | ||||
|             assert k in nutrition and nutrition[k] is not None | ||||
|  | ||||
|     # TODO: validate other types of content | ||||
|  | ||||
|  | ||||
| def test_bad_mealie_alpha_data_is_ignored(api_client: TestClient, unique_user: TestUser): | ||||
|     with TemporaryDirectory() as tmpdir: | ||||
|   | ||||
| @@ -481,20 +481,24 @@ nutrition_test_cases = ( | ||||
|         }, | ||||
|     ), | ||||
|     CleanerCase( | ||||
|         test_id="special support for sodiumContent (g -> mg)", | ||||
|         test_id="special support for sodiumContent/cholesterolContent (g -> mg)", | ||||
|         input={ | ||||
|             "cholesterolContent": "10g", | ||||
|             "sodiumContent": "10g", | ||||
|         }, | ||||
|         expected={ | ||||
|             "cholesterolContent": "10000.0", | ||||
|             "sodiumContent": "10000.0", | ||||
|         }, | ||||
|     ), | ||||
|     CleanerCase( | ||||
|         test_id="special support for sodiumContent (mg -> mg)", | ||||
|         test_id="special support for sodiumContent/cholesterolContent (mg -> mg)", | ||||
|         input={ | ||||
|             "cholesterolContent": "10000mg", | ||||
|             "sodiumContent": "10000mg", | ||||
|         }, | ||||
|         expected={ | ||||
|             "cholesterolContent": "10000", | ||||
|             "sodiumContent": "10000", | ||||
|         }, | ||||
|     ), | ||||
|   | ||||
| @@ -23,6 +23,12 @@ async def test_recipe_parser(recipe_test_data: RecipeSiteTestCase): | ||||
|     recipe, _ = await scraper.create_from_html(recipe_test_data.url, translator) | ||||
|  | ||||
|     assert recipe.slug == recipe_test_data.expected_slug | ||||
|  | ||||
|     assert len(recipe.recipe_instructions or []) == recipe_test_data.num_steps | ||||
|  | ||||
|     assert len(recipe.recipe_ingredient) == recipe_test_data.num_ingredients | ||||
|  | ||||
|     actual = recipe.nutrition.model_dump() if recipe.nutrition else {} | ||||
|     assert recipe_test_data.num_nutrition_entries == len(actual.items()) | ||||
|  | ||||
|     assert recipe.org_url == recipe_test_data.url | ||||
|   | ||||
| @@ -13,6 +13,7 @@ class RecipeSiteTestCase: | ||||
|     num_steps: int | ||||
|     html_file: Path | ||||
|  | ||||
|     num_nutrition_entries: int = 0 | ||||
|     include_tags: bool = False | ||||
|     expected_tags: set[str] | None = None | ||||
|  | ||||
| @@ -26,6 +27,7 @@ def get_recipe_test_cases(): | ||||
|             expected_slug="taiwanese-three-cup-chicken-san-bei-ji-recipe", | ||||
|             num_ingredients=10, | ||||
|             num_steps=3, | ||||
|             num_nutrition_entries=11, | ||||
|         ), | ||||
|         RecipeSiteTestCase( | ||||
|             url="https://www.rezeptwelt.de/backen-herzhaft-rezepte/schinken-kaese-waffeln-ohne-viel-schnickschnack/4j0bkiig-94d4d-106529-cfcd2-is97x2ml", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user