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.
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
¶
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.).
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=).
ServingRange
¶
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.
Purchase
¶
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
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
¶
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 |
required |
density_data
|
DensityData | None
|
Optional density data for unit conversion. |
None
|
recipe_index
|
Mapping[str, Recipe] | None
|
Optional mapping of recipe name → |
None
|
matcher
|
ItemMatcher | None
|
Optional custom matching function. Defaults to
:func: |
None
|
Returns:
| Type | Description |
|---|---|
RecipeCost
|
|
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
|
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: |
required |
density_data
|
DensityData | None
|
Optional density data for unit conversion. |
None
|
Returns:
| Type | Description |
|---|---|
IngredientCost
|
|
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., |
required |
to_unit
|
str
|
Target unit (e.g., |
required |
density_data
|
DensityData
|
Dictionary with optional |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
Converted quantity, or |
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 |
required |
n
|
int
|
How many top drivers to return (default 5). |
5
|
Returns:
| Type | Description |
|---|---|
list[IngredientCost]
|
List of |
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
|
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 |
Raises:
| Type | Description |
|---|---|
UnitConversionError
|
If the unit cannot be resolved to grams and
raise_on_error is |
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: |
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 |
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 |
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 ( |
missing_ingredients |
list[str]
|
Names of ingredients whose price could not be found. |
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 |
required |
assemblies
|
Iterable[Assembly]
|
Assemblies keyed by |
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: |
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 |
None
|
Returns:
| Type | Description |
|---|---|
MenuAnalysis
|
A |
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
|
None
|
per_unit
|
tuple[float, str] | None
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
list[MaterialCost]
|
List of |
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: |
None
|
picker
|
ItemPicker | None
|
Optional custom picking function. Defaults to
:func: |
None
|
price_display_fn
|
Callable[[SupplyItem, PurchasedItem], tuple[Decimal, str]] | None
|
Optional callback
|
None
|
Returns:
| Type | Description |
|---|---|
list[MaterialCost]
|
List of |
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 |
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 |
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 |
None
|
display_normalizer
|
Callable[..., tuple[float, str]] | None
|
Optional function
|
None
|
category_rules
|
list | None
|
Optional list of :class: |
None
|
key_fn
|
Callable[[Material], tuple] | None
|
Optional function |
None
|
item_factory
|
Callable[[tuple, float, str, set[str]], SupplyItem] | None
|
Optional function |
None
|
Returns:
| Type | Description |
|---|---|
ShoppingList
|
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If a production item references an assembly name not in
the assemblies (wrapped as |
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. |
None
|
category_rules
|
list | None
|
Optional categorization rules for
:func: |
None
|
Returns:
| Type | Description |
|---|---|
list[IngredientGroup]
|
List of |
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 |
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 |
required |
servings
|
int
|
The number of servings to spread the cost across. |
required |
Returns:
| Type | Description |
|---|---|
PriceRange
|
|
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. |
required |
ingredient_properties
|
Callable[[Ingredient], frozenset[str]] | None
|
Optional callback
|
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
|
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:
- If ingredient_properties returns a frozenset containing the property
name, that is authoritative (
True). - If ingredient_properties returns a non-empty frozenset without the property, the ingredient is skipped for that property.
- If ingredient_properties returns
frozenset()(or the callback isNone), 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
|
ingredient_properties
|
Callable[[Ingredient], frozenset[str]] | None
|
Optional callback |
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: |
None
|
badge_implies
|
dict[str, set[str]] | None
|
Mapping of badge slug → set of redundant badges.
Defaults to :data: |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
List of display strings (e.g. |
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):
- 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. - nutrition_registry — lookup the ingredient's name in the
provided data (
NutritionRegistrymapping orIterable[FoodRecord]). - ingredient_nutrition_lookup — call the provided callback with
the ingredient name; if it returns
NutritionInfo, use it. - fallback — skip the ingredient (zero contribution).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recipe
|
Recipe
|
The recipe to analyze. |
required |
nutrition_registry
|
Iterable[FoodRecord] | NutritionRegistry | None
|
|
None
|
ingredient_nutrition_lookup
|
Callable[[str], NutritionInfo | None] | None
|
Optional callback
|
None
|
recipe_index
|
Mapping[str, Recipe] | None
|
Optional mapping of recipe name → |
None
|
Returns:
| Type | Description |
|---|---|
RecipeMacros
|
|
Raises:
| Type | Description |
|---|---|
RecipeCycleError
|
If a cycle is detected in |
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
¶
RecipeLoadError
¶
PurchaseLoadError
¶
UnitConversionError
¶
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. |