Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

fix: Fallback for Missing Display Attribute #31

Merged
merged 2 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions AppLambda/src/models/mealie.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from datetime import datetime
from enum import Enum
from fractions import Fraction
from json import JSONDecodeError
from typing import Any

Expand All @@ -10,6 +11,20 @@
from ..models.core import BaseSyncEvent, Source
from ._base import APIBase

INGREDIENT_QTY_PRECISION = 3
MAX_INGREDIENT_DENOMINATOR = 32

SUPERSCRIPT = dict(zip("1234567890", "¹²³⁴⁵⁶⁷⁸⁹⁰", strict=False))
SUBSCRIPT = dict(zip("1234567890", "₁₂₃₄₅₆₇₈₉₀", strict=False))


def display_fraction(fraction: Fraction):
return (
"".join([SUPERSCRIPT[c] for c in str(fraction.numerator)])
+ "/"
+ "".join([SUBSCRIPT[c] for c in str(fraction.denominator)])
)


class MealieBase(APIBase):
@classmethod
Expand Down Expand Up @@ -72,6 +87,7 @@ def __str__(self) -> str:

class UnitFoodBase(MealieBase):
name: str
plural_name: str | None = None
description: str = ""
extras: dict | None = {}

Expand All @@ -80,6 +96,7 @@ class Unit(UnitFoodBase):
id: str | None
fraction: bool
abbreviation: str
plural_abbreviation: str | None = ""
use_abbreviation: bool | None

def __str__(self) -> str:
Expand Down Expand Up @@ -139,6 +156,7 @@ class MealieShoppingListItemBase(MealieBase):
checked: bool = False
position: int | None = None

disable_amount: bool | None = None
is_food: bool = False

note: str | None = ""
Expand Down Expand Up @@ -181,6 +199,111 @@ class MealieShoppingListItemOut(MealieShoppingListItemBase):
position: int
recipe_references: list[MealieShoppingListItemRecipeRefOut] = []

def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)

# calculate missing is_food and disable_amount values
# we can't do this in a validator since they depend on each other
if self.is_food is None and self.disable_amount is not None:
self.is_food = not self.disable_amount
elif self.disable_amount is None and self.is_food is not None:
self.disable_amount = not self.is_food
elif self.is_food is None and self.disable_amount is None:
self.is_food = bool(self.food)
self.disable_amount = not self.is_food

# format the display property, if Mealie doesn't give us one
if not self.display:
self.display = self._format_display()

def _format_quantity_for_display(self) -> str:
"""How the quantity should be displayed"""

qty: float | Fraction

# decimal
if not self.unit or not self.unit.fraction:
qty = round(self.quantity or 0, INGREDIENT_QTY_PRECISION)
if qty.is_integer():
return str(int(qty))

else:
return str(qty)

# fraction
qty = Fraction(self.quantity or 0).limit_denominator(MAX_INGREDIENT_DENOMINATOR)
if qty.denominator == 1:
return str(qty.numerator)

if qty.numerator <= qty.denominator:
return display_fraction(qty)

# convert an improper fraction into a mixed fraction (e.g. 11/4 --> 2 3/4)
whole_number = 0
while qty.numerator > qty.denominator:
whole_number += 1
qty -= 1

return f"{whole_number} {display_fraction(qty)}"

def _format_unit_for_display(self) -> str:
if not self.unit:
return ""

use_plural = self.quantity and self.quantity > 1
unit_val = ""
if self.unit.use_abbreviation:
if use_plural:
unit_val = self.unit.plural_abbreviation or self.unit.abbreviation
else:
unit_val = self.unit.abbreviation

if not unit_val:
if use_plural:
unit_val = self.unit.plural_name or self.unit.name
else:
unit_val = self.unit.name

return unit_val

def _format_food_for_display(self) -> str:
if not self.food:
return ""

use_plural = (not self.quantity) or self.quantity > 1
if use_plural:
return self.food.plural_name or self.food.name
else:
return self.food.name

def _format_display(self) -> str:
components = []

use_food = True
if self.is_food is False:
use_food = False
elif self.disable_amount is True:
use_food = False

# ingredients with no food come across with a qty of 1, which looks weird
# e.g. "1 2 tbsp of olive oil"
if self.quantity and (use_food or self.quantity != 1):
components.append(self._format_quantity_for_display())

if not use_food:
components.append(self.note or "")
else:
if self.quantity and self.unit:
components.append(self._format_unit_for_display())

if self.food:
components.append(self._format_food_for_display())

if self.note:
components.append(self.note)

return " ".join(components).strip()


class MealieShoppingListItemsCollectionOut(MealieBase):
"""Container for bulk shopping list item changes"""
Expand Down
50 changes: 50 additions & 0 deletions tests/model_tests/test_mealie_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest
from AppLambda.src.models.mealie import Food, MealieShoppingListItemOut, Unit
from tests.utils.generators import random_bool, random_string, random_int


@pytest.mark.parametrize("use_fraction", [True, False])
@pytest.mark.parametrize("use_food", [True, False])
def test_mealie_shopping_list_generates_missing_display(use_fraction: bool, use_food: bool):
food = Food(name=random_string())
unit = Unit(name=random_string(), fraction=use_fraction, abbreviation=random_string(), use_abbreviation=False)
list_item = MealieShoppingListItemOut(
id=random_string(),
display="",
shopping_list_id=random_string(),
checked=random_bool(),
position=random_int(1, 100),
disable_amount=None,
is_food=use_food,
note=random_string(),
quantity=random_int(1, 100) + 0.5,
food=food,
unit=unit,
)

qty_display = (str(int(list_item.quantity)) + " ¹/₂") if use_fraction else str(list_item.quantity)
if use_food:
assert list_item.display == f"{qty_display} {unit.name} {food.name} {list_item.note}"
else:
assert list_item.display == f"{qty_display} {list_item.note}"


def test_mealie_shopping_list_not_overwrite_existing_display():
food = Food(name=random_string())
unit = Unit(name=random_string(), fraction=True, abbreviation=random_string(), use_abbreviation=False)
display = random_string()
list_item = MealieShoppingListItemOut(
id=random_string(),
display=display,
shopping_list_id=random_string(),
checked=random_bool(),
position=random_int(1, 100),
disable_amount=None,
is_food=True,
note=random_string(),
quantity=random_int(1, 100) + 0.5,
food=food,
unit=unit,
)

assert list_item.display == display
Loading