Multi-domain Python library for bill-of-materials planning, recipe costing, shopping list aggregation, unit conversion, allergen detection, and nutrition analysis. Works for food recipes, construction, brewing, and manufacturing.
Project description
wright
wright /rīt/ — noun: a maker or builder. From Old English wyrhta (worker), as in shipwright, wheelwright, playwright. Here: a wright for your recipes, assemblies, and bills of materials.
Pure Python library for production planning, cost calculation, shopping list generation, allergen detection, nutrition analysis, and supply tracking.
Data-source agnostic. No I/O inside the core — models are plain Pydantic, the
PurchasedItem protocol accepts anything. Subclass to add your own fields.
Domains: food recipes, construction materials, brewing grain bills, manufacturing BOMs — any domain where you need to aggregate named items with quantities and units into a consolidated supply list with costs.
pip install wright-core
Recipes and ingredients
from wright import Recipe, Ingredient, RecipeComponent
cake = Recipe(
name="Lemon Cake",
components=[
RecipeComponent(
name="Batter",
ingredients=[
Ingredient(name="Flour", quantity=300, unit="g"),
Ingredient(name="Butter", quantity=200, unit="g"),
Ingredient(name="Lemon Juice", quantity=3, unit="tbsp"),
],
)
],
prep_time=30,
cook_time=45,
servings=12,
)
# Scale it
double_batch = cake * 2 # same as cake.size_up(2)
half_batch = cake * 0.5
Costing
from decimal import Decimal
from wright import Purchase, calculate_recipe_cost
groceries = [
Purchase(name="Flour", quantity=1000, unit="g", price=Decimal("3.99")),
Purchase(name="Butter", quantity=500, unit="g", price=Decimal("5.49")),
Purchase(name="Lemon Juice", quantity=250, unit="ml", price=Decimal("1.99")),
]
cost = calculate_recipe_cost(cake, groceries)
print(cost.total_cost_range.midpoint) # → 3.10
print(cost.cost_per_serving_range.midpoint) # → 0.26
Planning a production run
from datetime import date
from wright import (
ProductionRun,
ProductionItem,
generate_shopping_list,
group_shopping_items,
calculate_shopping_list_cost,
analyze_menu,
DEFAULT_CATEGORY_RULES,
)
session = ProductionRun(
date=date(2026, 6, 20),
production=[ProductionItem(assembly="Lemon Cake", quantity=3)],
target_dates=[date(2026, 6, 20)],
)
shopping = generate_shopping_list(session, [cake])
Group items by store aisle:
grouped = group_shopping_items(
shopping.all_items,
category_rules=DEFAULT_CATEGORY_RULES,
)
for group in grouped:
for item in group.items:
print(f" {item.name:<22s} {item.quantity:g} {item.unit}")
Dairy & Eggs ----------------------------------------------
Butter 600 g
Lemon Juice 9 tbsp
Dry Goods -------------------------------------------------
Flour 900 g
Enrich with costs and analyze:
costs = calculate_shopping_list_cost(shopping, groceries)
total = sum(c.total_cost for c in costs if c.total_cost is not None)
# → Decimal('9.30')
menu = analyze_menu(
[ProductionItem(assembly="Lemon Cake", quantity=3)],
[cake],
groceries,
)
for item in menu.top_drivers:
print(f" {item.item.name}: ${item.total_cost} ({menu.cost_share(item):.0%})")
# → Butter: $3.29 (35%)
# → Flour: $2.39 (26%)
Full grocery list example with 3 recipes, 16 grocery items, and formatted output. Meal prep planner — 5-day week, 2 cook sessions, macros per day.
Allergens and dietary badges
from wright import detect_allergens, detect_dietary_properties
allergens = detect_allergens(cake, allergy_map={"milk": "Dairy", "wheat": "Wheat"})
# → ["Dairy", "Gluten", "Eggs"]
badges = detect_dietary_properties(cake)
# → ["VEGAN", "DAIRY-FREE", "GLUTEN-FREE"]
Supplement keyword detection with purchase data:
badges = detect_dietary_properties(
cake,
ingredient_properties=lambda ing: (
frozenset({"vegan", "gluten-free"}) if "gf" in ing.require_tags else frozenset()
),
)
Nutrition
from wright import calculate_recipe_macros, NutritionInfo, FoodRecord
registry = [
FoodRecord(
ingredient="Flour",
nutrition=NutritionInfo(protein_g=10, carbs_g=76, fat_g=1, kcal=364),
),
FoodRecord(
ingredient="Butter",
nutrition=NutritionInfo(protein_g=0.9, carbs_g=0.1, fat_g=81, kcal=717),
),
]
macros = calculate_recipe_macros(cake, nutrition_registry=registry)
print(macros.per_serving.kcal)
Supply tracking
from wright import Stock, SupplyItem
stock = Stock([SupplyItem(name="Flour", quantity=2000, unit="g")])
stock, deficit = stock.use([SupplyItem(name="Flour", quantity=900, unit="g")])
# deficit → [] — stock covers it
Pricing
from wright import margin_price, multiplier_price
margin_price(Decimal("2.00"), 0.67) # → 6.06 (67% margin)
multiplier_price(Decimal("2.00"), 3) # → 6.00 (3× cost)
Everything is injectable
from wright import chain, pinned_picker, cheapest_picker
# Compose pickers: pinned first, then cheapest
picker = chain(pinned_picker({"Butter": my_brand}), cheapest_picker)
items = calculate_shopping_list_cost(shopping, groceries, picker=picker)
# Custom volume display for metric users
shopping = generate_shopping_list(
session,
[cake],
display_normalizer=lambda q, u: ...,
)
# Custom name matcher
cost = calculate_recipe_cost(
cake,
groceries,
matcher=my_fuzzy_matcher,
)
Non-food domains
Assembly, Component, and Material work for construction, brewing,
manufacturing, or any bill-of-materials domain. No dummy food fields needed:
from datetime import date
from decimal import Decimal
from wright import (
Assembly,
Component,
ProductionItem,
ProductionRun,
Purchase,
generate_shopping_list,
calculate_item_costs,
)
# ── Two home projects ──────────────────────────────────────────────────────
deck = Assembly(
name="Backyard Deck",
components=[
Component(
name="Framing",
materials=[
Material(name="2x6 Pressure-Treated", quantity=48, unit="ft"),
Material(name="Joist Hangers", quantity=16, unit="each"),
],
),
Component(
name="Surface",
materials=[
Material(name='5/4" Cedar Decking', quantity=160, unit="ft"),
Material(name='2" Stainless Screws', quantity=600, unit="each"),
],
),
],
)
bed = Assembly(
name="Raised Garden Bed",
components=[
Component(
name="Frame",
materials=[
Material(name="2x8 Cedar", quantity=24, unit="ft"),
Material(name='3" Deck Screws', quantity=64, unit="each"),
],
),
],
)
# ── Hardware store prices ──────────────────────────────────────────────────
prices = [
Purchase(
name="2x6 Pressure-Treated",
quantity=8,
unit="ft",
price=Decimal("12.97"),
store="Home Depot",
),
Purchase(
name='2" Stainless Screws',
quantity=100,
unit="each",
price=Decimal("3.49"),
store="Home Depot",
),
]
# ── Cost one project ───────────────────────────────────────────────────────
deck_costs = calculate_item_costs(deck.all_materials, prices)
total = sum(c.total_cost for c in deck_costs if c.total_cost is not None)
print(f"Deck materials: ${total}")
# ── Plan a weekend build session ───────────────────────────────────────────
plan = ProductionRun(
date=date(2026, 6, 20),
production=[
ProductionItem(assembly="Backyard Deck", quantity=1),
ProductionItem(assembly="Raised Garden Bed", quantity=1),
],
target_dates=[date(2026, 6, 20)],
)
shopping = generate_shopping_list(plan, [deck, bed])
for item in shopping.all_items:
print(f"{item.name}: {item.quantity:.0f} {item.unit}")
# ── Cross-reference with home inventory ───────────────────────────────────
from wright import Stock, SupplyItem
# What's already in the garage
garage_stock = Stock([
SupplyItem(name='2" Stainless Screws', quantity=100, unit="each"),
SupplyItem(name="Joist Hangers", quantity=8, unit="each"),
])
# Deduct stock — get only what you still need to buy
garage_stock, buy_list = garage_stock.use(deck.all_materials)
for item in buy_list:
print(f"Buy: {item.name} — {item.quantity:.0f} {item.unit}")
# → Buy: 2x6 Pressure-Treated — 48 ft
# → Buy: Joist Hangers — 8 each (16 needed − 8 on hand)
# → Buy: 5/4" Cedar Decking — 160 ft
# → Buy: 2" Stainless Screws — 500 each (600 needed − 100 on hand)
The same calculate_ingredient_cost() and Stock work across all domains.
Requirements
Python 3.11+. Dependencies: pydantic>=2.8.2, pint>=0.25, pyyaml>=6.0.3.
License
MIT. See LICENSE.
wright is created and maintained by 3pm German Baking, LLC a farmers market bakery in Asheville, NC.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wright_core-0.1.0.tar.gz.
File metadata
- Download URL: wright_core-0.1.0.tar.gz
- Upload date:
- Size: 38.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ed1b31f63f520330f028986286f1e6145ccad3d60742e305baac9be841a4a48
|
|
| MD5 |
e2bec7b400a77e06a25dd774dc8a7925
|
|
| BLAKE2b-256 |
990be1b9420e3f13cd84ef96d4c38d0cafe1f9a630bd4a7aa6d9c60844b92a2f
|
File details
Details for the file wright_core-0.1.0-py3-none-any.whl.
File metadata
- Download URL: wright_core-0.1.0-py3-none-any.whl
- Upload date:
- Size: 45.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7664ebcfbbe085700b2b249e62aeebe21aff4df77445faf0e1956aaf0c04ce76
|
|
| MD5 |
13a22e0149f403b5577cb13166c14e8c
|
|
| BLAKE2b-256 |
4600b7f9476f6e2cb6b5642e737f37c9fbef22733945f9f295a3f509ac0c7277
|