recipe_grid.recipe: Recipe Data Model

The recipe_grid.recipe module defines the Directed Acyclic Graph (DAG) data structure used to describe recipes.

Overview

Recipes are defined as a series of trees, principally consisting of Ingredient nodes and Step nodes.

An Ingredient node contains a name and an optional quantity.

A Step node contains a description of the step and a series of input nodes (children).

A simple recipe might only consist of a single tree of Ingredient and Step nodes. For example, here’s how a simple pasta meal might be represented.

digraph foo {

   subgraph {
       node [shape=ellipse]

       onion [label=<Onion<br/>(Ingredient)>]
       tomatoes [label=<Chopped Tomatoes<br/>(Ingredient)>]
       pasta [label=<Pasta<br/>(Ingredient)>]
   }

   subgraph {
       node [shape=rectangle]

       chop [label=<Chop<br/>(Step)>]
       fry [label=<Fry<br/>(Step)>]
       boildown [label=<Boil down<br/>(Step)>]
       boil [label=<Boil<br/>(Step)>]
       mix [label=<Mix<br/>(Step)>]
   }

   chop -> onion;

   fry -> chop;

   boildown -> fry;
   boildown -> tomatoes;

   boil -> pasta;

   mix -> boildown;
   mix -> boil;
}

Sometimes a sub tree within a recipe might be identified as its own sub recipe and given an emphasising border and title in the rendered recipe table. For example, in the recipe above we might make the pasta sauce a sub recipe. To represent this, a SubRecipe node type is used which always has exactly one child and one or more named outputs. In this example, there is only one output (‘Pasta sauce’). We’ll return to the case with multiple named outputs later.

digraph foo {

   subgraph {
       node [shape=ellipse]

       onion [label=<Onion<br/>(Ingredient)>]
       tomatoes [label=<Chopped Tomatoes<br/>(Ingredient)>]
       pasta [label=<Pasta<br/>(Ingredient)>]
   }

   subgraph {
       node [shape=rectangle]

       chop [label=<Chop<br/>(Step)>]
       fry [label=<Fry<br/>(Step)>]
       boildown [label=<Boil down<br/>(Step)>]
       boil [label=<Boil<br/>(Step)>]
       mix [label=<Mix<br/>(Step)>]
   }

   subgraph {
       node [shape=Mrecord]

       sauce [label="{<fs>Pasta sauce|<fo>(SubRecipe)}"]
   }

   chop -> onion;

   fry -> chop;

   boildown -> fry;
   boildown -> tomatoes;

   sauce:fo -> boildown;

   boil -> pasta;

   mix -> sauce:fs:n;
   mix -> boil;
}

In other cases, an ingredient or whole sub recipe may be split into parts and used separately. In this case, a Reference node may be used to refer to a SubRecipe which must be the root of a previous tree in the recipe. For example, in the recipe below, a Sub Recipe for grated cheese is referenced in two places, with 75% being mixed into the main recipe and 25% being used to top it:

digraph foo {

   subgraph {
       node [shape=ellipse]

       cheese [label=<Cheese<br/>(Ingredient)>]
       white_sauce [label=<White sauce<br/>(Ingredient)>]
       pasta [label=<Pasta<br/>(Ingredient)>]
   }

   subgraph {
       node [shape=rectangle]

       grate [label=<Grate<br/>(Step)>]
       mix [label=<Mix<br/>(Step)>]
       top [label=<Top<br/>(Step)>]
   }

   subgraph {
       node [shape=diamond]

       refm [label=<75%<br/>(Reference)>]
       reft [label=<25%<br/>(Reference)>]
   }

   subgraph {
       node [shape=Mrecord]

       grated_cheese [label="{<fc>Grated cheese|<fo>(SubRecipe)}"]
   }

   grate -> cheese;
   grated_cheese:fo -> grate;

   mix -> refm;
   mix -> white_sauce;
   mix -> pasta;

   top -> mix;
   top -> reft;

   subgraph {
         edge [constraint=false,style=dashed];

         refm:sw -> grated_cheese:fc:n
         reft:sw -> grated_cheese:fc:n
   }
}

A final case is where a SubRecipe has more than one output. This might occur when, for example, when vegetables are boiled and both the vegetables and the water are reserved and used in a later step. Each output may then be referenced separately by Reference nodes. SubRecipe nodes with more than one output must be the root of a recipe tree.

digraph foo {
   subgraph {
       node [shape=ellipse]

       vegetables [label=<Vegetables<br/>(Ingredient)>]
       gravy_granules [label=<Gravy granules<br/>(Ingredient)>]
   }

   subgraph {
       node [shape=rectangle]

       boil [label=<Boil, reserving water<br/>(Step)>]
       make [label=<Make gravy<br/>(Step)>]
       pour [label=<Pour over<br/>(Step)>]
   }

   subgraph {
       node [shape=diamond]

       refv [label=<(Reference)>]
       refw [label=<(Reference)>]
   }

   subgraph {
       node [shape=Mrecord]

       boiled_veg [label="{{<fv>Boiled Vegetables|<fw>Water}|<fo>(SubRecipe)}"]
   }

   boil -> vegetables;
   boiled_veg -> boil;

   make -> gravy_granules;
   make -> refw;

   pour -> make;
   pour -> refv;

   subgraph {
       edge [constraint=false,style=dashed];

       refv:sw -> boiled_veg:fv:n
       refw:sw -> boiled_veg:fw:n
   }
}

Data structures

A Recipe is defined at its root by a Recipe instance. In a simple document there will be a single Recipe instance which contains the entire DAG for that recipe. For more complex documents where the recipe is presented in several sections, each section of the recipe will be represented by its own Recipe with back-references to prior Recipes in Recipe.follows.

class recipe_grid.recipe.Recipe(recipe_trees: Tuple[RecipeTreeNode, ...], follows: Recipe | None = None)

A recipe, defined in terms of a series of recipe trees. Later trees may reference the outputs of earlier trees resulting in a Directed Acyclic Graph (DAG) structure describing a recipe.

recipe_trees: Tuple[RecipeTreeNode, ...]

The recipe tree roots for this Recipe.

follows: Recipe | None = None

If this recipe contains References to SubRecipes in another Recipe, this parameter should be set accordingly. (References are looked for recursively.

scale(factor: int | float | Fraction) Recipe

Return a copy of this recipe with all scalable values and quantities scaled by the given factor.

DAG Structure

Recipe trees are in turn defined using subclasses of RecipeTreeNode:

class recipe_grid.recipe.RecipeTreeNode
iter_children() Iterable[RecipeTreeNode]

Iterate over the children of this node.

substitute(old: RecipeTreeNode, new: RecipeTreeNode) RecipeTreeNode

Return a copy of this recipe tree with the node old replaced with new. (The old tree will remain intact).

class recipe_grid.recipe.Ingredient(description: ScaledValueString, quantity: Quantity | None = None)

A leaf node in a tree describing an ingredient to be used.

description: ScaledValueString

A description of the ingredient.

quantity: Quantity | None = None

The quantity of the ingredient, of specified. If None, the ingredient is quantity-less.

class recipe_grid.recipe.Step(description: ScaledValueString, inputs: Tuple[RecipeTreeNode, ...])

A node in a tree where the node represents a step in a recipe (e.g. ‘mix’) and the children represent the inputs to that step. Children may be other Step instances, Ingredient instances or Reference instances referring to outputs of other SubRecipes.

description: ScaledValueString

A description of the step to be carried out.

inputs: Tuple[RecipeTreeNode, ...]

The inputs to (i.e. children) of this step.

class recipe_grid.recipe.SubRecipe(sub_tree: RecipeTreeNode, output_names: Tuple[ScaledValueString, ...], show_output_names: bool = True)

A sub recipe is a node representing a logical division in a recipe with some semantic significance. For example, a pie recipe may divide the recipe into two sub recipes: one for the filling and another for the pastry.

sub_tree: RecipeTreeNode

The steps describing this sub-recipe.

output_names: Tuple[ScaledValueString, ...]

One or more names given to the outputs of this sub recipe.

In the simple case there will be exactly one named output. In our pie recipe example, the sub recipe for the filling might have a single output named “Filling” and the pastry sub recipe might have one named “Pastry”.

For sub recipes which produce multiple outputs, names for these must be enumerated. For example, a sub recipe describing boiling some vegetables where both the vegetables and water will be used, two output names (e.g. “Boiled Vegetables” and “Vegetable Water” might be given).

Note

Sub recipes with a single output name may appear anywhere within a recipe tree. Where a SubRecipe is not at the root of a recipe it will typically be rendered inline but inset and labelled with the output name.

Sub recipes with more than one named output may only be used as the root of a recipe tree.

show_output_names: bool = True

Specifies whether the output name(s) for this subrecipe should be rendered. For example when a sub-recipe consists of a single ingredient (e.g. ‘300g spam’) with a single output (e.g. ‘spam’), adding extra labelling would just be a distraction and so this setting should be False.

class recipe_grid.recipe.Reference(sub_recipe: SubRecipe, output_index: int = 0, amount: Quantity | Proportion = Proportion(value=1.0, percentage=False, remainder_wording=None, preposition=''))

A reference to a named output of a SubRecipe.

output_index: int = 0

The SubRecipe and output index being referenced. Only sub recipes which form the root of a recipe tree may be referenced.

amount: Quantity | Proportion = Proportion(value=1.0, percentage=False, remainder_wording=None, preposition='')

The amount of the referenced output to use. Default: all of it.

Quantities and Proportions

Absolute quantities (for ingredients and references) and relative proportions (for references only) are defined as follows:

class recipe_grid.recipe.Quantity(value: int | float | Fraction, unit: str | None = None, value_unit_spacing: str = '', preposition: str = '')

An absolute quantity.

Suggested rendering:

q.value
+ (q.value_unit_spacing + q.unit if q.unit is not None else "")
+ q.preposition
class recipe_grid.recipe.Proportion(value: int | float | Fraction | None = None, percentage: bool | None = None, remainder_wording: str | None = None, preposition: str = '')

A relative proportion.

Suggested rendering:

(
    (q.value * 100 if q.percentage else q.value)
    if q.value is None else
    q.remainder_wording
)
+ q.preposition

Exceptions

The DAG structure invariants are enforced by checks in their constructors and any non-conforming input will produce a RecipeInvariantError subclass.

exception recipe_grid.recipe.RecipeInvariantError

Base class for exceptions thrown when an invariant of the Recipe data structure is violated.

exception recipe_grid.recipe.MultiOutputSubRecipeUsedAsNonRootNodeError

Thrown when a SubRecipe with more than one named output is added as non-root node in a recipe tree.

exception recipe_grid.recipe.OutputIndexError

Thrown when a Reference refers to output which does not exist in the referenced SubRecipe.

exception recipe_grid.recipe.ZeroOutputSubRecipeError

Thrown if a SubRecipe is defined with no named outputs.

exception recipe_grid.recipe.ReferenceToInvalidSubRecipeError

Thrown if a Reference refers to a SubRecipe node which is not a root node of a proceeding recipe tree in the same Recipe.