Skip to main content

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

CI docs Python 3.11+ License: MIT

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.

wright

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.


3pm German Baking

wright is created and maintained by 3pm German Baking, LLC a farmers market bakery in Asheville, NC.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wright_core-0.1.0.tar.gz (38.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wright_core-0.1.0-py3-none-any.whl (45.4 kB view details)

Uploaded Python 3

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

Hashes for wright_core-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3ed1b31f63f520330f028986286f1e6145ccad3d60742e305baac9be841a4a48
MD5 e2bec7b400a77e06a25dd774dc8a7925
BLAKE2b-256 990be1b9420e3f13cd84ef96d4c38d0cafe1f9a630bd4a7aa6d9c60844b92a2f

See more details on using hashes here.

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

Hashes for wright_core-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7664ebcfbbe085700b2b249e62aeebe21aff4df77445faf0e1956aaf0c04ce76
MD5 13a22e0149f403b5577cb13166c14e8c
BLAKE2b-256 4600b7f9476f6e2cb6b5642e737f37c9fbef22733945f9f295a3f509ac0c7277

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page