Skip to content

API Reference

wright.models

Pydantic data models for recipe definition, costing, and categorization.

All models are data-source agnostic — no file I/O, no database assumptions.

Servings = int | ServingRange module-attribute

A recipe yields either an exact number of servings or a range.

DEFAULT_CATEGORY_RULES = [CategoryRule(category='Pantry', priority=0, keywords=['brandy', 'candied', 'canned', 'compote', 'garlic powder', 'jam', 'jello', 'juice', 'liqueur', 'nut butter', 'onion powder', 'peanut butter']), CategoryRule(category='Fats & Oils', priority=1, keywords=['oil']), CategoryRule(category='Produce', priority=2, keywords=['apple', 'apricot', 'avocado', 'banana', 'bell pepper', 'blueberr', 'carrot', 'cherr', 'cucumber', 'eggplant', 'fruit', 'garlic', 'gooseberr', 'herb', 'lemon', 'mushroom', 'onion', 'orange', 'peach', 'pear', 'plum', 'raspberr', 'rhubarb', 'spinach', 'spring onion', 'strawberr', 'vegetable', 'zucchini']), CategoryRule(category='Specialty Items', priority=3, keywords=['essence']), CategoryRule(category='Dairy & Eggs', priority=4, keywords=['butter', 'cheese', 'cream', 'egg', 'milk', 'parmesan', 'quark', 'ricotta', 'sour cream', 'yogurt']), CategoryRule(category='Meat', priority=5, keywords=['pork', 'beef', 'chicken', 'meat']), CategoryRule(category='Dry Goods', priority=6, keywords=['almond flour', 'baking powder', 'baking soda', 'bread crumbs', 'cocoa', 'coconut flake', 'corn starch', 'flour', 'sliced almond', 'powder', 'psyllium', 'salt', 'semolina', 'starch', 'sugar', 'yeast']), CategoryRule(category='Specialty Items', priority=7, keywords=['almond', 'bay leaf', 'cake glaze', 'chia', 'chocolate', 'cinnamon', 'clove', 'extract', 'gelatin', 'ginger', 'hazelnut', 'nutmeg', 'nuts', 'pepper', 'poppy', 'raisin', 'spice', 'vanilla', 'walnut']), CategoryRule(category='Pantry', priority=8, keywords=['cookie', 'honey', 'rum', 'sauerkraut', 'wine'])] module-attribute

Default categorization rules based on a US grocery store layout.

Pass your own list to categorize_item() to match a different store layout or language.

BaseIngredient = Ingredient module-attribute

Alias for :class:Ingredient. Provided for subclassing in applications that import from wright.models import BaseIngredient.

BaseRecipe = Recipe module-attribute

Alias for :class:Recipe. Provided for subclassing in applications that import from wright.models import BaseRecipe.

Assembly

Bases: BaseModel

A domain-agnostic collection of components.

Use Assembly directly for construction, brewing, manufacturing, or any non-food domain. Recipe subclasses it with food-specific fields like prep_time, cook_time, and servings.

All planning functions (:func:~wright.planning.generate_shopping_list, :func:~wright.planning.analyze_menu) accept Assembly — so you can use the full pipeline without dummy food fields.

all_materials property

Flatten all materials across all components.

size_up(factor)

Return a new Assembly with all material quantities scaled.

Material

Bases: BaseModel

A bill-of-materials item for any domain (food, construction, etc.).

Use :class:Ingredient for food-specific contexts, or subclass Material directly for non-food domains (e.g., Lumber, Hardware, Paint).

scale(factor)

Return a new Material with quantity scaled by the given factor.

Component

Bases: BaseModel

A domain-agnostic named group of materials.

For food domains, use :class:RecipeComponent (which adds an ingredients alias). For non-food domains, use Component directly or subclass it (e.g., WallAssembly, BatchStage).

scale(factor)

Return a new Component with all materials scaled.

Ingredient

Bases: Material

A food ingredient used in a recipe.

Inherits all fields from :class:Material. Subclass in your application layer to add domain-specific metadata (vendor, nutrition, etc.).

scale(factor)

Return a new Ingredient with quantity scaled by the given factor.

Recipe

Bases: Assembly

A complete recipe with components and optional serving information.

Subclasses :class:Assembly with food-specific fields. Recipes are data-source agnostic — populate from YAML, JSON, a database, or pure Python. Subclass to add domain-specific metadata (pricing, translations, etc.).

all_ingredients property

Flatten all ingredients across all components.

all_materials property

Flatten all materials across all components.

double()

Return a new Recipe with all ingredient quantities doubled.

size_up(factor)

Return a new Recipe with all ingredient quantities scaled.

RecipeComponent

Bases: Component

A named component or sub-recipe (e.g., 'Chocolate Shortcrust Dough').

Inherits from :class:Component. The ingredients property is an alias for materials, typed as list[Ingredient] for food domains.

Backward-compatible: accepts ingredients= in the constructor (mapped to materials=).

ingredients property writable

Food-domain alias for :attr:Component.materials.

scale(factor)

Return a new RecipeComponent with all ingredients scaled.

ServingRange

Bases: BaseModel

A range of servings a recipe yields.

midpoint property

Return the midpoint of the serving range.

scale(factor)

Return a new ServingRange scaled by the given factor.

PurchasedItem

Bases: Protocol

Protocol for grocery price data used in cost calculations.

Any object with these attributes and methods satisfies the protocol. This allows the library to work with SQLModel ORM objects, plain dataclasses, namedtuples, or Pydantic models — no adapter needed.

tag_set property

Parse comma-separated tags into a set (lowercase).

matches_requirements(require_tags)

Check if this item satisfies all required tags.

If require_tags is empty, returns True (matches any item).

Purchase

Bases: BaseModel

Concrete item for getting started without writing custom classes.

tag_set property

Parse tags into a set of lowercase strings.

matches_requirements(require_tags)

Check if this item satisfies all required tags.

NutritionInfo

Bases: BaseModel

Nutritional values per 100 grams of an ingredient.

All nutrient amounts are in grams per 100g of the ingredient, except kcal which is total energy per 100g.

If kcal is not explicitly provided, it is approximated using the Atwater general factor system::

kcal ≈ protein_g * 4 + carbs_g * 4 + fat_g * 9 + fiber_g * 2

computed_kcal property

Approximate kcal from macros using Atwater general factors.

effective_kcal property

Return explicit kcal if set, otherwise compute from macros.

MacroPerServing

Bases: BaseModel

Macro breakdown for a single serving.

Supports + (add), * (scale), and sum() via .zero().

zero() classmethod

Return a zero-valued instance.

For use with sum(..., start=MacroPerServing.zero()).

RecipeMacros

Bases: BaseModel

Total and per-serving macro breakdown for a recipe.

Supports * to scale macros by batch quantity. per_serving is derived from total / servings_used — no redundant storage.

per_serving property

Macros per serving, derived from total / servings_used.

FoodRecord

Bases: BaseModel

Nutritional data for a single food item, keyed by ingredient name.

Maps an ingredient name (e.g. "Rolled Oats") to its per-100g nutritional profile. Designed to be loaded from YAML or populated from an external source (USDA, etc.).

The source field documents where the data came from (e.g. "usda-fdc", "nutritiondata.self.com") for auditing.

PriceRange

Bases: BaseModel

A price range representing minimum and maximum costs.

midpoint property

Return the midpoint of the price range.

__add__(other)

Add two price ranges together.

IngredientCost

Bases: BaseModel

Cost breakdown for a single ingredient.

RecipeCost

Bases: BaseModel

Full cost breakdown for a recipe.

CategoryRule

Bases: BaseModel

One categorization rule: keywords that map to a store aisle category.

Rules are evaluated in priority order (lowest first). When an ingredient name contains any keyword from a rule, it is assigned that category.

categorize_item(item_name, *, rules=None)

Categorize a BOM item based on keyword rules.

Rules are evaluated in priority order (lowest first). The first rule whose keywords match the item name (case-insensitive substring) determines the category.

Parameters:

Name Type Description Default
item_name str

Name of the item to categorize (ingredient, material, etc.).

required
rules list[CategoryRule] | None

Optional list of CategoryRule objects. If None or empty, returns None (uncategorized).

None

Returns:

Type Description
str | None

Category name string, or None if no rules matched.

wright.costing

Cost calculation logic — pure functions, no file I/O.

calculate_recipe_cost(recipe, purchases, *, density_data=None, recipe_index=None, matcher=None)

Calculate the full cost breakdown for a recipe.

Resolves ingredients to purchase items. When an ingredient has product_ref set, the function looks up the referenced recipe in recipe_index and recursively costs it — supporting arbitrary nesting depths with cycle detection.

Parameters:

Name Type Description Default
recipe Recipe

The recipe to cost.

required
purchases Iterable[PurchasedItem]

Available purchase price data (any PurchasedItem).

required
density_data DensityData | None

Optional density data for unit conversion.

None
recipe_index Mapping[str, Recipe] | None

Optional mapping of recipe name → Recipe for resolving product_ref references.

None
matcher ItemMatcher | None

Optional custom matching function. Defaults to :func:find_matching_purchases (exact name + tag filter).

None

Returns:

Type Description
RecipeCost

RecipeCost with ingredient-level breakdown and totals.

Raises:

Type Description
RecipeCostErrors

If any ingredients cannot be matched, converted, or if a cycle is detected.

See Also

:func:calculate_item_costs — for per-item costing of arbitrary materials (non-recipe BOM items).

calculate_ingredient_cost(material, purchase, *, density_data=None, converter=None, ureg=None)

Calculate the cost of a BOM item based on a purchase item's price.

Handles unit conversion between BOM units and purchase units. For discrete units (each, packet), uses direct multiplication. For pinch units with non-discrete purchase units, estimates ~0.25 tsp.

Parameters:

Name Type Description Default
material Material

The BOM item to cost.

required
purchase PurchasedItem

The purchase item to use for pricing.

required
density_data DensityData | None

Optional density data for unit conversion.

None
converter Callable[[Material, PurchasedItem, DensityData], Decimal | None] | None

Optional custom cost function (material, purchase, density_data) -> Decimal | None. Called first; if it returns a Decimal, that value is used. If it returns None, falls through to the built-in cascade.

None

Returns:

Type Description
Decimal

The cost of the material amount.

Raises:

Type Description
UnitConversionError

If units cannot be converted.

calculate_ingredient_cost_range(material, purchases, *, density_data=None)

Calculate the cost range for a material across multiple purchase sources.

Parameters:

Name Type Description Default
material Material

The BOM item to cost.

required
purchases Iterable[PurchasedItem]

Matching purchase items (output of :func:find_matching_purchases).

required
density_data DensityData | None

Optional density data for unit conversion.

None

Returns:

Type Description
IngredientCost

IngredientCost with price range and source information.

Raises:

Type Description
UnitConversionError

If none of the purchase items can be converted to the material's unit.

convert_with_density(ingredient_name, quantity, from_unit, to_unit, density_data)

Try to convert quantity using density data.

Supports two types of conversions: 1. liquids: density in g/ml (e.g., lemon juice: 1.03 g/ml). 2. volume_weights: direct g per volume unit (e.g., cinnamon: 2.6 g/tsp).

Parameters:

Name Type Description Default
ingredient_name str

Name of the ingredient (case-insensitive lookup).

required
quantity float

Amount to convert.

required
from_unit str

Source unit (e.g., "g").

required
to_unit str

Target unit (e.g., "tsp").

required
density_data DensityData

Dictionary with optional "liquids" and "volume_weights" sections.

required

Returns:

Type Description
float | None

Converted quantity, or None if no conversion is available.

get_top_cost_drivers(recipe_cost, n=5)

Return the top N ingredients by cost midpoint, descending.

Parameters:

Name Type Description Default
recipe_cost RecipeCost

A fully calculated RecipeCost.

required
n int

How many top drivers to return (default 5).

5

Returns:

Type Description
list[IngredientCost]

List of IngredientCost sorted by price midpoint descending,

list[IngredientCost]

capped at n.

convert_ingredient_to_grams(material, *, raise_on_error=True, ureg=None, density_data=None)

Return the gram quantity for a material.

For packet units, uses equivalent_quantity (e.g. 1 packet = 8 g). For gram units, uses quantity directly. For other weight units, converts via pint. Falls back to density-based conversion when pint cannot convert (e.g. volume units like ml → g for liquids).

Parameters:

Name Type Description Default
material Material

The BOM item to resolve to grams.

required
raise_on_error bool

If True (default), raises UnitConversionError on failure. If False, returns 0.0 silently.

True
ureg UnitRegistry | None

Optional pint unit registry.

None
density_data DensityData | None

Optional density data for volume→weight conversion.

None

Returns:

Type Description
float

Gram quantity, or 0.0 when unresolvable and raise_on_error is False.

Raises:

Type Description
UnitConversionError

If the unit cannot be resolved to grams and raise_on_error is True.

wright.matching

Ingredient to purchase matching logic — pure functions, no I/O.

ItemMatcher = Callable[[Material, Iterable[PurchasedItem]], list[PurchasedItem]] module-attribute

ItemPicker = Callable[[Material, Iterable[PurchasedItem]], PurchasedItem | None] module-attribute

PinnedPurchases = Mapping[str, PurchasedItem] module-attribute

find_matching_purchases(material, purchases)

Find all purchase items that satisfy a material's requirements.

Uses permissive matching: - Matches by exact item name - If require_tags is empty, matches any item with that name - If require_tags is specified, item must have all required tags

Parameters:

Name Type Description Default
material Material

The BOM item to match.

required
purchases Iterable[PurchasedItem]

Available purchase price data (any PurchasedItem protocol).

required

Returns:

Type Description
list[PurchasedItem]

List of matching PurchasedItem objects (may contain multiple from

list[PurchasedItem]

different stores/vendors).

Raises:

Type Description
IngredientNotFoundError

If no matching purchase items are found.

match_all_ingredients(materials, purchases, *, matcher=None)

Find matching purchases for a collection of materials.

Materials with the same name but different tag requirements get separate entries keyed by a compound key.

Parameters:

Name Type Description Default
materials Iterable[Material]

BOM items to match.

required
purchases Iterable[PurchasedItem]

Available purchase price data.

required
matcher ItemMatcher | None

Optional custom matching function. Defaults to :func:find_matching_purchases.

None

Returns:

Type Description
dict[str, list[PurchasedItem]]

Dictionary mapping material key to list of matching PurchasedItem

dict[str, list[PurchasedItem]]

objects.

Raises:

Type Description
IngredientNotFoundError

If any material cannot be matched.

cheapest_picker(material, purchases)

Pick the candidate with the lowest price per unit.

When units are pint-compatible, prices are normalized to a common unit before comparison. Otherwise raw price / quantity is used.

first_picker(material, purchases)

Pick the first candidate.

recent_picker(material, purchases)

Pick the candidate with the most recent purchase date.

Falls back to first-picker when no dates are available.

pinned_picker(pinned)

Return a picker that looks up exact material names in pinned.

Parameters:

Name Type Description Default
pinned PinnedPurchases

Mapping of material name → purchase to use.

required

Returns:

Type Description
ItemPicker

A ItemPicker that returns pinned.get(name) or None.

compatible_unit_recent_picker(material, purchases)

Pick the candidate with compatible units and most recent purchase date.

Prefers purchases whose unit is compatible with the material unit (or exact match). Falls back to all candidates if none are compatible. Returns the most recently purchased among qualifying candidates.

This is the default picker used by :func:calculate_shopping_list_cost and :func:calculate_item_costs when no explicit picker is supplied.

chain(*pickers)

Compose multiple pickers — returns the first non-None result.

Parameters:

Name Type Description Default
pickers ItemPicker

Pickers to try, in priority order.

()

Returns:

Type Description
ItemPicker

A ItemPicker that tries each picker in sequence.

wright.planning

Shopping list generation and cost enrichment — pure functions, no I/O.

ShoppingItemWithCost = MaterialCost module-attribute

Backward-compatibility alias for :class:MaterialCost.

IngredientGroup

Bases: BaseModel

A group of related shopping items.

MaterialCost dataclass

Item enriched with pricing information.

Used for costing bill-of-materials items and shopping list items alike. The underlying :class:SupplyItem is accessible via .item, and common fields (name, quantity, unit, tags) are exposed directly as properties for convenience.

missing_price instance-attribute

True if no grocery data was found for this item.

name property

Item name (delegates to :attr:item.name).

price_per_unit instance-attribute

Price per display unit (e.g. per 100g, per each).

price_unit instance-attribute

Display unit for the price (e.g. '100g', 'each').

purchase_date instance-attribute

Date of the grocery purchase used for pricing.

quantity property

Item quantity (delegates to :attr:item.quantity).

store instance-attribute

Store with the best / most recent price.

tags property

Item tags (delegates to :attr:item.tags).

total_cost instance-attribute

Total cost for this item.

unit property

Item unit (delegates to :attr:item.unit).

MenuAnalysis dataclass

Cost analysis for an arbitrary menu (a set of recipes with quantities).

Attributes:

Name Type Description
production list[ProductionItem]

The recipe/quantity pairs that were analyzed.

items list[MaterialCost]

All aggregated ingredients with cost data, sorted by total_cost descending (missing-price items last).

total_cost Decimal | None

Grand total of all ingredient costs (None if any prices are missing).

missing_ingredients list[str]

Names of ingredients whose price could not be found.

known_total property

Sum of costs for all items that have a price.

top_drivers property

Items with known costs, sorted by total_cost descending.

cost_share(item)

Return this item's share of total cost as a fraction 0–1.

ShoppingList

Bases: BaseModel

Generated shopping list from a production run.

all_items property

Get all items across all groups.

analyze_menu(production, assemblies, purchases, *, density_data=None, matcher=None, picker=None, date=None)

Analyze the ingredient costs for an arbitrary menu or project.

Builds a virtual production run from production, generates the aggregated shopping list, then enriches every line with the selected purchase price.

Parameters:

Name Type Description Default
production list[ProductionItem]

List of ProductionItem (assembly name + quantity).

required
assemblies Iterable[Assembly]

Assemblies keyed by .name (list, tuple, etc.).

required
purchases Iterable[PurchasedItem]

Available purchase price data.

required
density_data DensityData | None

Optional density data for unit conversion.

None
matcher ItemMatcher | None

Optional custom matching function. Defaults to :func:find_matching_purchases.

None
picker ItemPicker | None

Optional custom picking function. Defaults to compatible-unit most-recent selection.

None
date date | None

Optional date for the virtual production run. Defaults to date.today().

None

Returns:

Type Description
MenuAnalysis

A MenuAnalysis with aggregated items, total cost, and

MenuAnalysis

convenience helpers.

calculate_item_costs(items, purchases, *, density_data=None, matcher=None, picker=None, price_display_fn=None, per_unit=None)

Cost arbitrary items — food, construction materials, tools, etc.

Reuses the same matching, picking, and costing pipeline as :func:calculate_shopping_list_cost, but works on a flat list of Material instead of a ShoppingList.

When per_unit is provided, each item's total_cost is scaled to represent the cost for that unit quantity instead of the full material quantity. For example, if a BOM lists "10 lb Barley" with total_cost=$20 and per_unit=(12, "oz"), the result will show total_cost=$1.50 (cost per 12 oz).

Parameters:

Name Type Description Default
items Sequence[Material]

Items to cost (recipe ingredients, lumber, hardware, etc.).

required
purchases Iterable[PurchasedItem]

Available purchase price data.

required
density_data DensityData | None

Optional density data for unit conversion.

None
matcher ItemMatcher | None

Optional custom matching function.

None
picker ItemPicker | None

Optional custom picking function.

None
price_display_fn Callable[[SupplyItem, PurchasedItem], tuple[Decimal, str]] | None

Optional callback (item, purchase) -> (price_per_unit, display_unit) for customizing unit price display. Defaults to per-100g for metric weight units, per-100ml for metric volume units, per-package otherwise.

None
per_unit tuple[float, str] | None

Optional (quantity, unit) pair to scale costs to a per-unit basis (e.g. (12, "oz") for cost per 12 oz).

None

Returns:

Type Description
list[MaterialCost]

List of MaterialCost, one per input item.

See Also

:func:calculate_recipe_cost — for full recipe costing with serving breakdown and per-ingredient cost ranges.

calculate_shopping_list_cost(shopping_list, purchases, *, density_data=None, matcher=None, picker=None, price_display_fn=None)

Enrich each item with cost information.

For each item: 1. Convert to an ingredient for matching. 2. Find matching purchase items via matcher. 3. Select one purchase via picker (default: :func:compatible_unit_recent_picker). 4. Calculate cost using unit conversion. 5. Compute a readable price per display unit via price_display_fn.

Parameters:

Name Type Description Default
shopping_list ShoppingList

Generated shopping list.

required
purchases Iterable[PurchasedItem]

Available purchase price data.

required
density_data DensityData | None

Optional density data for unit conversion.

None
matcher ItemMatcher | None

Optional custom matching function. Defaults to :func:find_matching_purchases.

None
picker ItemPicker | None

Optional custom picking function. Defaults to :func:compatible_unit_recent_picker. Use :func:chain to compose.

None
price_display_fn Callable[[SupplyItem, PurchasedItem], tuple[Decimal, str]] | None

Optional callback (item, purchase) -> (price_per_unit, display_unit) for customizing unit price display. Defaults to per-100g for metric weight units, per-100ml for metric volume units, per-package otherwise.

None

Returns:

Type Description
list[MaterialCost]

List of MaterialCost, one per item.

See Also

:func:calculate_item_costs — for costing a flat list of :class:Material items directly.

cost_by_component(assembly, purchases, *, density_data=None, matcher=None, picker=None)

Calculate total cost per component for an assembly.

For each component, costs all materials using the same matching, picking, and costing pipeline as :func:calculate_item_costs.

Parameters:

Name Type Description Default
assembly Assembly

The assembly to analyze.

required
purchases Iterable[PurchasedItem]

Available purchase price data.

required
density_data DensityData | None

Optional density data for unit conversion.

None
matcher ItemMatcher | None

Optional custom matching function.

None
picker ItemPicker | None

Optional custom picking function.

None

Returns:

Type Description
dict[str, Decimal]

Dictionary mapping component name to its total cost (using

dict[str, Decimal]

the midpoint of available price ranges when multiple sources

dict[str, Decimal]

exist). Components with no cost data contribute Decimal("0").

estimate_total_items(session, assemblies)

Estimate the total number of items a production run will produce.

Uses the midpoint of each assembly's serving range multiplied by batch quantity. Assemblies without servings contribute 0.

Parameters:

Name Type Description Default
session ProductionRun

The production run.

required
assemblies Iterable[Assembly]

Assemblies keyed by .name (list, tuple, etc.).

required

Returns:

Type Description
int

Estimated total item count (rounded to nearest integer).

format_quantity(quantity)

Format a quantity as int if whole, else one decimal place.

generate_shopping_list(session, assemblies, *, volume_normalizer=None, display_normalizer=None, category_rules=None, key_fn=None, item_factory=None, merge_numeric=None)

Generate a consolidated shopping list from a production run.

Aggregates materials across all assemblies, normalizing volume units to ml for consistent accumulation. Byproduct and zero-quantity items are excluded.

Parameters:

Name Type Description Default
session ProductionRun

The production run to generate a list for.

required
assemblies Iterable[Assembly]

Assemblies referenced by production items (list or tuple). Must contain every assembly referenced by the session's production items.

required
volume_normalizer Callable[[float, str], tuple[float, str]] | None

Optional function (quantity, unit) -> (quantity, unit) called on each material to normalize units for accumulation. Defaults to :func:normalize_volume_to_ml (converts volume units to ml).

None
display_normalizer Callable[..., tuple[float, str]] | None

Optional function (quantity, unit, *, name="") -> (quantity, unit) called to format accumulated quantities for display. Receives the ingredient name (from the grouping key) for name-aware display decisions. Defaults to :func:normalize_volume_us.

None
category_rules list | None

Optional list of :class:CategoryRule for grouping items by department. Pass DEFAULT_CATEGORY_RULES for store layout grouping. If None, all items go to "Other".

None
key_fn Callable[[Material], tuple] | None

Optional function (material) -> tuple that produces a grouping key. Defaults to (name, tuple(sorted(tags))). Use a custom key to group by vendor, department, or any other material attribute.

None
item_factory Callable[[tuple, float, str, set[str]], SupplyItem] | None

Optional function (key, quantity, unit, tags) -> SupplyItem that creates a display item from the accumulated data. The key argument is the grouping tuple produced by key_fn. Defaults to SupplyItem(name=key[0], ...). Use a custom factory to produce subclass instances (e.g. ShoppingItem with a vendor field).

None

Returns:

Type Description
ShoppingList

ShoppingList with grouped items.

Raises:

Type Description
KeyError

If a production item references an assembly name not in the assemblies (wrapped as RecipeLoadError in the full application).

group_shopping_items(items, *, kitchen_items=None, category_rules=None)

Group items by ingredient category.

Parameters:

Name Type Description Default
items list[SupplyItem]

Shopping items to group.

required
kitchen_items frozenset[str] | None

Item names to exclude (e.g. {"water"}). Defaults to an empty set.

None
category_rules list | None

Optional categorization rules for :func:categorize_item. If not provided, items are placed in "Other".

None

Returns:

Type Description
list[IngredientGroup]

List of IngredientGroup sorted alphabetically.

normalize_metric(quantity, unit, name='')

Convert metric units to display-friendly forms (volume and weight).

Volume rules
  • = 1 L → L

  • = 100 ml (but < 1 L) → ml

  • < 100 ml → keep original unit (tsp/tbsp often better for small)
Weight rules
  • = 1 kg → kg

  • < 1 kg → g

Parameters:

Name Type Description Default
quantity float

The quantity to normalize.

required
unit str

The unit to normalize.

required
name str

Ingredient name (ignored by this normalizer; accepted for compatibility with name-aware display normalizers).

''

normalize_volume_to_ml(quantity, unit)

Convert volume units to ml for consistent accumulation.

Non-volume units are returned unchanged.

normalize_volume_us(quantity, unit, name='')

Convert volume units to grocery store formats.

Rules
  • = 1 gallon → gallons

  • = 1 quart (but < 1 gallon) → quarts

  • = 8 floz (but < 1 quart) → fluid ounces

  • < 8 floz → keep original unit (tsp/tbsp for small amounts)

Parameters:

Name Type Description Default
quantity float

The quantity to normalize.

required
unit str

The unit to normalize.

required
name str

Ingredient name (ignored by this normalizer; accepted for compatibility with name-aware display normalizers).

''

wright.pricing

Pricing calculations — pure functions, no I/O.

margin_price(cost, margin)

Calculate a sale price from cost using a target margin.

Uses the standard margin formula

price = cost / (1 - margin)

For example, a 67% margin on a $2.00 cost gives: 2.00 / (1 - 0.67) = $6.06

Parameters:

Name Type Description Default
cost Decimal

The ingredient (or total) cost.

required
margin Decimal | float

Target profit margin as a Decimal or float (0 < margin < 1).

required

Returns:

Type Description
Decimal

Suggested sale price.

Raises:

Type Description
ValueError

If margin is not strictly between 0 and 1.

multiplier_price(cost, multiplier)

Calculate a sale price from cost using a simple multiplier.

For example, 3× cost on a $2.00 cost gives $6.00.

Parameters:

Name Type Description Default
cost Decimal

The ingredient (or total) cost.

required
multiplier float

Price multiplier (must be > 0).

required

Returns:

Type Description
Decimal

Suggested sale price.

Raises:

Type Description
ValueError

If multiplier is not positive.

per_serving_price(recipe_cost, servings)

Calculate the per-serving price range for a given number of servings.

Parameters:

Name Type Description Default
recipe_cost RecipeCost

A fully calculated RecipeCost.

required
servings int

The number of servings to spread the cost across.

required

Returns:

Type Description
PriceRange

PriceRange with min and max cost per serving.

Raises:

Type Description
ValueError

If servings is less than 1.

wright.allergens

Allergen and dietary badge detection — pure functions, no I/O.

BADGE_DISPLAY = {'vegan': 'VEGAN', 'gluten-free': 'GLUTEN-FREE', 'dairy-free': 'DAIRY-FREE', 'nut-free': 'NUT-FREE', 'soy-free': 'SOY-FREE', 'organic': 'ORGANIC', 'local': 'LOCAL', 'no-refined-sugar': 'NO REFINED SUGAR'} module-attribute

BADGE_IMPLIES = {'vegan': {'dairy-free'}} module-attribute

DEFAULT_NON_VEGAN_KEYS = frozenset({'egg', 'honey', 'gelatin', 'lard', 'meat', 'chicken', 'beef', 'pork', 'fish', 'shrimp', 'anchovy', 'milk', 'cream', 'butter', 'cheese', 'yogurt', 'sour cream', 'cream cheese', 'whey', 'casein'}) module-attribute

Default ingredient-name keywords that disqualify vegan.

DEFAULT_DAIRY_KEYS = frozenset({'milk', 'cream', 'butter', 'cheese', 'yogurt', 'sour cream', 'cream cheese', 'whey', 'casein'}) module-attribute

Default ingredient-name keywords that disqualify dairy-free.

DEFAULT_GLUTEN_KEYS = frozenset({'flour', 'wheat', 'barley', 'rye', 'spelt', 'semolina'}) module-attribute

Default ingredient-name keywords that disqualify gluten-free.

detect_allergens(recipe, allergy_map, *, ingredient_properties=None)

Detect allergens present in a recipe's ingredients.

Consult the ingredient_properties callback (if provided) per ingredient to suppress wheat/dairy keyword matches when the ingredient is known to be gluten-free or vegan.

Parameters:

Name Type Description Default
recipe Recipe

The recipe to scan.

required
allergy_map dict[str, str]

Mapping of lowercase keyword → display name (e.g. {"milk": "Milk", "wheat": "Wheat"}).

required
ingredient_properties Callable[[Ingredient], frozenset[str]] | None

Optional callback (ingredient) -> frozenset[str] for authoritative dietary properties (e.g. {"vegan", "gluten-free"}).

None

Returns:

Type Description
list[str]

Sorted list of allergen display names.

detect_allergens_from_names(ingredient_names, allergy_map, *, ingredient_properties_for_name=None)

Detect allergens from a plain list of ingredient name strings.

Useful when working with flat ingredient lists rather than structured Recipe objects.

Parameters:

Name Type Description Default
ingredient_names list[str]

List of ingredient name strings.

required
allergy_map dict[str, str]

Mapping of lowercase keyword → display name.

required
ingredient_properties_for_name Callable[[str], frozenset[str]] | None

Optional callback (name) -> frozenset[str] for authoritative dietary properties.

None

Returns:

Type Description
list[str]

Sorted list of allergen display names.

detect_dietary_properties(recipe, *, resolve=True, ingredient_properties=None, non_vegan_keys=None, dairy_keys=None, gluten_keys=None, badge_display=None, badge_implies=None)

Derive dietary/quality display badges from a recipe's ingredients.

A badge is awarded only when ALL non-byproduct ingredients qualify.

Supported built-in badges (in display order): - VEGAN -- no animal products detected. - DAIRY-FREE -- no dairy detected (suppressed when VEGAN present). - GLUTEN-FREE -- no gluten/wheat detected.

Additional badges (e.g. keto, paleo) are supported via the ingredient_properties callback -- any property key returned by the callback is tracked the same way.

Detection priority per ingredient:

  1. If ingredient_properties returns a frozenset containing the property name, that is authoritative (True).
  2. If ingredient_properties returns a non-empty frozenset without the property, the ingredient is skipped for that property.
  3. If ingredient_properties returns frozenset() (or the callback is None), keyword disqualification is used: any ingredient name matching non_vegan_keys / dairy_keys / gluten_keys disqualifies the corresponding badge.

Parameters:

Name Type Description Default
recipe Recipe

The recipe to scan.

required
resolve bool

When True (default), returns display-ready badges with redundant badges suppressed. When False, returns raw badge slugs (e.g. ["vegan", "dairy-free"]) without suppression.

True
ingredient_properties Callable[[Ingredient], frozenset[str]] | None

Optional callback (ingredient) -> frozenset[str] that returns dietary properties for an ingredient from an external data source (e.g. purchase tags, brand database).

None
non_vegan_keys frozenset[str] | None

Ingredient-name keywords that disqualify vegan. Defaults to the built-in English food vocabulary.

None
dairy_keys frozenset[str] | None

Ingredient-name keywords that disqualify dairy-free. Defaults to the built-in set.

None
gluten_keys frozenset[str] | None

Ingredient-name keywords that disqualify gluten-free. Defaults to the built-in set.

None
badge_display dict[str, str] | None

Mapping of badge slug → display label. Defaults to :data:BADGE_DISPLAY.

None
badge_implies dict[str, set[str]] | None

Mapping of badge slug → set of redundant badges. Defaults to :data:BADGE_IMPLIES.

None

Returns:

Type Description
list[str]

List of display strings (e.g. ["VEGAN", "GLUTEN-FREE"]).

wright.macros

Macro calculation logic — pure functions, no file I/O.

Recursive product_ref support mirrors the costing module.

calculate_recipe_macros(recipe, nutrition_registry=None, *, ingredient_nutrition_lookup=None, recipe_index=None, density_data=None)

Calculate total and per-serving macros for a recipe.

For each ingredient, macros are computed from (in priority order):

  1. product_ref — recurse into the referenced sub-recipe (resolved via recipe_index) and scale its total macros by the ingredient's gram quantity relative to the sub-recipe's net_weight_grams.
  2. nutrition_registry — lookup the ingredient's name in the provided data (NutritionRegistry mapping or Iterable[FoodRecord]).
  3. ingredient_nutrition_lookup — call the provided callback with the ingredient name; if it returns NutritionInfo, use it.
  4. fallback — skip the ingredient (zero contribution).

Parameters:

Name Type Description Default
recipe Recipe

The recipe to analyze.

required
nutrition_registry Iterable[FoodRecord] | NutritionRegistry | None

Iterable[FoodRecord] (keyed by .ingredient) or NutritionRegistry mapping. The primary data source.

None
ingredient_nutrition_lookup Callable[[str], NutritionInfo | None] | None

Optional callback (name) -> NutritionInfo | None for looking up nutrition data by ingredient name from an external source (e.g., USDA API, local database). Acts as a secondary fallback when the registry has no entry.

None
recipe_index Mapping[str, Recipe] | None

Optional mapping of recipe name → Recipe for resolving product_ref references.

None

Returns:

Type Description
RecipeMacros

RecipeMacros with total and per-serving breakdown.

Raises:

Type Description
RecipeCycleError

If a cycle is detected in product_ref references.

wright.supply

Supply tracking — Stock class for pantry/shopping list stock management.

SupplyItem

Bases: BaseModel

A named item with a quantity — used for stock, needs, and deficits.

to_qty()

Return this item as a pint Quantity.

Stock

Immutable named-quantity collection (pantry, inventory, or consolidated needs).

All methods return new instances; the original is never mutated.

stock = Stock() stock = stock.add([SupplyItem(name="Flour", quantity=2000, unit="g")]) stock, deficit = stock.use([SupplyItem(name="Flour", quantity=900, unit="g")]) deficit []

add(items)

Return a new Stock with items merged in.

Same-name entries have their quantities summed (with unit conversion).

from_yaml(path) classmethod

Load pantry stock from a YAML file.

Expects a top-level pantry key with a list of entries, each containing name, quantity, and unit:

.. code-block:: yaml

pantry:
  - name: Wheat flour
    quantity: 25
    unit: lb
  - name: Sugar
    quantity: 5
    unit: kg

items()

Iterate over (name, item) pairs in the stock.

remove(items)

Return a new Stock with items unconditionally removed.

Quantities are floored at 0; zero-quantity entries are dropped. Unknown item names are silently ignored.

to_yaml(path)

Write stock to a YAML file under a pantry key.

use(needed, *, density_data=None)

Deduct needed from stock where possible.

Returns (reduced_stock, deficit) where deficit contains only items with a remaining shortfall (empty if everything was covered). The original stock is not modified.

values()

Iterate over item values in the stock.

wright.session

Production run models for batch planning.

Data-source agnostic — assemblies are referenced by name, not loaded here.

ProductionItem

Bases: BaseModel

An assembly (or recipe) to be produced in a specific quantity.

Uses assembly= as the canonical constructor argument. recipe= is accepted for backward compatibility (mapped to assembly).

recipe property

Backward-compatible alias for :attr:assembly.

ProductionRun

Bases: BaseModel

A production run producing multiple assemblies for one or more target dates.

__add__(other)

Merge two production runs.

Production items are combined by assembly name (summing quantities). Target dates are unioned and deduplicated. The earliest date is kept.

convert_name_to_filename(name, *, transliterations=_DEFAULT_TRANSLITERATIONS)

Convert an assembly name to kebab-case filename.

Accented characters are decomposed via Unicode NFKD normalization (é → e, ñ → n). German umlauts are transliterated to their digraph equivalents (ä → ae, ö → oe, ü → ue, ß → ss) by default. Pass transliterations={} to skip, or provide your own mappings for other scripts.

Examples:

'Chocolate Chip Cookie''chocolate-chip-cookie' 'Käsekuchen''kaesekuchen'

Parameters:

Name Type Description Default
name str

The assembly name.

required
transliterations dict[str, str] | None

Optional mapping of character → replacement string. Applied before NFKD normalization. Defaults to German umlaut digraphs.

_DEFAULT_TRANSLITERATIONS

Returns:

Type Description
str

Kebab-case filename without extension.

wright.errors

Custom exceptions for the wright package.

RecipeCoreError

Bases: Exception

Base exception for all wright errors.

IngredientNotFoundError

Bases: RecipeCoreError

Raised when an ingredient cannot be matched to any grocery item.

RecipeLoadError

Bases: RecipeCoreError

Raised when a recipe file cannot be loaded or parsed.

PurchaseLoadError

Bases: RecipeCoreError

Raised when a grocery file cannot be loaded or parsed.

UnitConversionError

Bases: RecipeCoreError

Raised when unit conversion between ingredient and grocery units fails.

RecipeCostErrors

Bases: RecipeCoreError

Raised when one or more ingredients in a recipe could not be costed.

Collects all ingredient errors instead of stopping at the first failure, so the user can see everything that needs to be fixed in one run.

wright.units

Pint unit registry with common unit classification sets.

Wright ships a pint.UnitRegistry with pre-defined custom units and exposes it as :data:ureg. All helper functions accept an optional ureg= parameter so you can inject your own registry.

Add a custom unit to wright's registry::

>>> from wright.units import ureg, parse_quantity
>>> ureg.define("loaf = 1 * count")
>>> ureg.define("@alias loaf = loaves")
>>> parse_quantity(2, "loaves").magnitude
2.0

Inject a separate registry (required when two registries must not share state)::

>>> import pint
>>> from wright.units import parse_quantity
>>> my_ureg = pint.UnitRegistry()
>>> my_ureg.define("each = 1 * count")
>>> my_ureg.define("crate = 24 * each")
>>> parse_quantity(3, "crate", ureg=my_ureg).to("each").magnitude
72.0

Pre-defined units: each, packet, pinch, can, clove, vial (all = 1 * count) with common aliases.

ureg = _ureg module-attribute

DISCRETE_UNITS = frozenset({'each', 'packet', 'packets', 'ea', 'piece', 'pieces'}) module-attribute

Units that represent countable items, not measurable quantities.

PINCH_UNITS = frozenset({'pinch', 'pinches'}) module-attribute

Approximate units handled specially in cost calculation.

WEIGHT_UNITS = frozenset({'g', 'gram', 'grams', 'oz', 'ounce', 'ounces', 'lb', 'lbs', 'pound', 'pounds', 'kg', 'kilogram', 'kilograms'}) module-attribute

All recognized weight units.

VOLUME_UNITS = frozenset({'tsp', 'teaspoon', 'tbsp', 'tablespoon', 'cup', 'floz', 'fluid_ounce', 'ml', 'milliliter', 'millilitre', 'liter', 'litre', 'l'}) module-attribute

All recognized volume units (used for normalization to canonical volume unit).

parse_quantity(value, unit, *, ureg=None)

Parse a value and unit string into a pint Quantity.

Parameters:

Name Type Description Default
value float

The numeric quantity.

required
unit str

The unit string (e.g., "g", "oz", "cups", "each").

required
ureg UnitRegistry | None

Optional unit registry. Defaults to the module-level registry.

None

Returns:

Type Description
Quantity

A pint Quantity object.

are_compatible(unit_a, unit_b, *, ureg=None)

Check whether two units are dimensionally compatible.

Parameters:

Name Type Description Default
unit_a str

First unit string.

required
unit_b str

Second unit string.

required
ureg UnitRegistry | None

Optional unit registry. Defaults to the module-level registry.

None

Returns:

Type Description
bool

True if the units share the same dimensionality.