Skip to main content

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

From recipe web page to shopping list

The distribution ships a chainable CLI: an LLM extracts a validated recipe from any web page (you bring the provider SDK and key), and wright turns it into a consolidated shopping list deterministically.

# One pipe: recipe URL in, shopping list out
uvx --with openai wright-core parse https://example.com/recipe | uvx wright-core shop

# Or in two steps, with review in between
uvx --with openai wright-core parse https://example.com/recipe > recipe.yaml
uvx wright-core shop recipe.yaml --servings 12 --units metric

# Per-recipe scaling, then consolidation
uvx --with openai wright-core parse URL_A | uvx wright-core scale --servings 24 > a.json
uvx --with openai wright-core parse URL_B | uvx wright-core scale --batches 2 > b.json
uvx wright-core shop a.json b.json

# Machine-readable output for agents (stdout)
uvx --with openai wright-core parse URL | uvx wright-core shop --format json

Supported providers: OpenAI, Anthropic, Gemini (via its OpenAI-compatible endpoint). Pages with schema.org JSON-LD get authoritative structured input automatically. See the agents guide for the full pipeline, the comparison with other tools, and how to call it from an agent.

Package layout: one distribution, two packages — wright is the pure library (no I/O, no CLI, no LLM SDKs); wright_recipes is the application layer (CLI, providers, fetch) and depends on wright, never the reverse.

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.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}")

# Cost breakdown by component (framing vs. surface)
from wright import cost_by_component

breakdown = cost_by_component(deck, prices)
for comp, cost in breakdown.items():
    print(f"  {comp}: ${cost:.2f}")

# Per-linear-foot cost
per_foot = calculate_item_costs(
    deck.all_materials,
    prices,
    per_unit=(1, "ft"),
)

# ── 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.

Metadata

Release files for wright-core 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wright-core 0.3.1
File Size Uploaded
wright_core-0.3.1.tar.gz 55.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wright-core 0.3.1
File Interpreter ABI Platform
wright_core-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 122.7 kB

Release files / wright_core-0.3.1.tar.gz

Download URL wright_core-0.3.1.tar.gz
Size 55.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ad958c30b1e1f19214d910706a128e6ad06ab73808d6da813aa045154a31c2d4
BLAKE2b-256 checksum
How to use checksums
c1988f204834fe400ae6d198fd5611586d30715962f59746e0e75ef69939bba2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","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}

Release files / wright_core-0.3.1-py3-none-any.whl

Download URL wright_core-0.3.1-py3-none-any.whl
Size 66.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0cf05bed543ed073d17523cde135a1bd1370d47aad597c2c2a6f5753888fe8ff
BLAKE2b-256 checksum
How to use checksums
51a292615afeec8ee9c64af692c1ac9fa6c53f91a08e25f99ad2435f4eea95f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","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}

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page