Source code for usda_fdc.analysis.recipe

"""
Recipe analysis module.

This module provides tools for analyzing recipes, including ingredient parsing,
nutrient calculation, and recipe comparison.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Union, Any, Tuple
import re

from ..client import FdcClient
from ..models import Food
from .analysis import NutrientAnalysis, analyze_food
from .dri import DriType, Gender


[docs]@dataclass class Ingredient: """ Represents a recipe ingredient. Attributes: food: The food. amount: The amount of the ingredient. unit: The unit of the amount. weight_g: The weight in grams. """ food: Food amount: float unit: str weight_g: float
[docs]@dataclass class Recipe: """ Represents a recipe with ingredients. Attributes: name: The name of the recipe. ingredients: The ingredients in the recipe. servings: The number of servings the recipe makes. instructions: The recipe instructions. total_weight_g: The total weight of the recipe in grams. """ name: str ingredients: List[Ingredient] = field(default_factory=list) servings: int = 1 instructions: Optional[str] = None total_weight_g: float = 0.0
[docs] def add_ingredient(self, ingredient: Ingredient) -> None: """ Add an ingredient to the recipe. Args: ingredient: The ingredient to add. """ self.ingredients.append(ingredient) self.total_weight_g += ingredient.weight_g
[docs] def get_weight_per_serving(self) -> float: """ Get the weight per serving in grams. Returns: The weight per serving. """ if self.servings > 0: return self.total_weight_g / self.servings return 0.0
[docs]@dataclass class RecipeAnalysis: """ Represents the nutrient analysis of a recipe. Attributes: recipe: The recipe being analyzed. total_analysis: The nutrient analysis of the entire recipe. per_serving_analysis: The nutrient analysis per serving. ingredient_analyses: The nutrient analyses of each ingredient. """ recipe: Recipe total_analysis: NutrientAnalysis per_serving_analysis: NutrientAnalysis ingredient_analyses: List[NutrientAnalysis] = field(default_factory=list)
[docs]def parse_ingredient( text: str, client: FdcClient, default_unit: str = "g" ) -> Optional[Ingredient]: """ Parse an ingredient from text. Args: text: The ingredient text. client: The FDC client. default_unit: The default unit to use if not specified. Returns: An Ingredient object if parsing succeeds, None otherwise. """ # Regular expression to match amount, unit, and food pattern = r"^([\d.\/]+)\s*([a-zA-Z]+)?\s+(.+)$" match = re.match(pattern, text.strip()) if not match: return None # Extract amount, unit, and food name amount_str, unit, food_name = match.groups() # Parse amount (handle fractions) if "/" in amount_str: num, denom = amount_str.split("/") amount = float(num) / float(denom) else: amount = float(amount_str) # Use default unit if not specified if unit is None: unit = default_unit # Search for the food search_results = client.search(food_name, page_size=1) if not search_results.foods: return None # Get the food details food = client.get_food(search_results.foods[0].fdc_id) # Calculate weight in grams weight_g = 0.0 # Try to find a matching portion for portion in food.food_portions: if portion.measure_unit and portion.measure_unit.lower() == unit.lower(): weight_g = portion.gram_weight * amount break # If no matching portion, assume the unit is grams if weight_g == 0.0: if unit.lower() in ["g", "gram", "grams"]: weight_g = amount elif unit.lower() in ["kg", "kilogram", "kilograms"]: weight_g = amount * 1000 elif unit.lower() in ["oz", "ounce", "ounces"]: weight_g = amount * 28.35 elif unit.lower() in ["lb", "pound", "pounds"]: weight_g = amount * 453.59 else: # Default to 100g per unit if no conversion is available weight_g = amount * 100 return Ingredient(food=food, amount=amount, unit=unit, weight_g=weight_g)
[docs]def create_recipe( name: str, ingredient_texts: List[str], client: FdcClient, servings: int = 1, instructions: Optional[str] = None ) -> Recipe: """ Create a recipe from ingredient texts. Args: name: The name of the recipe. ingredient_texts: The ingredient texts. client: The FDC client. servings: The number of servings the recipe makes. instructions: The recipe instructions. Returns: A Recipe object. """ recipe = Recipe(name=name, servings=servings, instructions=instructions) for text in ingredient_texts: ingredient = parse_ingredient(text, client) if ingredient: recipe.add_ingredient(ingredient) return recipe
[docs]def analyze_recipe( recipe: Recipe, dri_type: DriType = DriType.RDA, gender: Gender = Gender.MALE ) -> RecipeAnalysis: """ Analyze the nutrient content of a recipe. Args: recipe: The recipe to analyze. dri_type: The type of DRI to use for comparison. gender: The gender to use for DRI comparison. Returns: A RecipeAnalysis object. """ # Create a combined food for the entire recipe combined_food = Food( fdc_id=0, description=recipe.name, data_type="Recipe", nutrients=[], food_portions=[] ) # Analyze each ingredient ingredient_analyses = [] for ingredient in recipe.ingredients: # Analyze the ingredient analysis = analyze_food( ingredient.food, dri_type=dri_type, gender=gender, serving_size=ingredient.weight_g ) ingredient_analyses.append(analysis) # Add the ingredient's nutrients to the combined food for nutrient_id, nutrient_value in analysis.nutrients.items(): # Find or create the nutrient in the combined food found = False for combined_nutrient in combined_food.nutrients: if combined_nutrient.id == nutrient_value.nutrient.usda_id: # Add the amount combined_nutrient.amount += nutrient_value.amount found = True break if not found and nutrient_value.nutrient.usda_id: # Create a new nutrient from ..models import Nutrient as FdcNutrient combined_food.nutrients.append(FdcNutrient( id=nutrient_value.nutrient.usda_id, name=nutrient_value.nutrient.display_name, amount=nutrient_value.amount, unit_name=nutrient_value.unit )) # Analyze the combined food for the total recipe total_analysis = analyze_food( combined_food, dri_type=dri_type, gender=gender, serving_size=recipe.total_weight_g ) # Create a per-serving analysis per_serving_food = Food( fdc_id=0, description=f"{recipe.name} (per serving)", data_type="Recipe", nutrients=[] ) # Add nutrients to the per-serving food for nutrient in combined_food.nutrients: from ..models import Nutrient as FdcNutrient per_serving_food.nutrients.append(FdcNutrient( id=nutrient.id, name=nutrient.name, amount=nutrient.amount / recipe.servings if recipe.servings > 0 else 0, unit_name=nutrient.unit_name )) # Analyze the per-serving food per_serving_analysis = analyze_food( per_serving_food, dri_type=dri_type, gender=gender, serving_size=recipe.get_weight_per_serving() ) return RecipeAnalysis( recipe=recipe, total_analysis=total_analysis, per_serving_analysis=per_serving_analysis, ingredient_analyses=ingredient_analyses )