Functional Weave
Code in Python

hospitality.allergen-matrix@1.0.0

impl/python.py

2,958 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import List, Optional, Sequence

from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .hospitality_allergen_matrix_data import ALLERGENS, ALLERGENS_HISTORY, Allergen  ← this capability’s own data, compiled from data/allergens.json into the same file by fune build
from .hospitality_allergen_matrix_types import AllergenRow, IngredientAllergens


def _list_in_force(jurisdiction: str, on_date: str) -> List[Allergen]:
    rows = [
        a
        for a in ALLERGENS
        if a.jurisdiction == jurisdiction and a.valid_from <= on_date and (a.valid_to is None or on_date <= a.valid_to)
    ]
    if rows:
        return sorted(rows, key=lambda a: a.annex_number)
    # A history-pruned build may have dropped the list that applied then; say so
    # rather than answering with nothing.
    if ALLERGENS_HISTORY != "full":
        earliest: Optional[str] = None
        for a in ALLERGENS:
            if a.jurisdiction == jurisdiction and (earliest is None or a.valid_from < earliest):
                earliest = a.valid_from
        if earliest is not None and on_date < earliest:
            raise ValueError(
                "no allergen list for %s on %s: this build was installed with history=%s, so it only carries lists from %s"
                % (jurisdiction, on_date, ALLERGENS_HISTORY, earliest)
            )
    raise ValueError("no allergen list for %s on %s" % (jurisdiction, on_date))


def _dedupe(names: List[str]) -> List[str]:
    seen: List[str] = []
    for name in names:
        if name not in seen:
            seen.append(name)
    return seen


def allergen_matrix(ingredients: Sequence[IngredientAllergens], jurisdiction: str, on_date: str) -> List[AllergenRow]:
    """The regulated allergens in a dish, from what its ingredients declare.

    Every allergen on the list gets a row, in the legal order, so a menu or
    allergen chart shows "none" as a decision rather than a gap. An allergen any
    ingredient contains is "contains"; otherwise one any ingredient may contain
    is "may-contain". The sources say which ingredients to check.
    """
    add_days(on_date, 0)
    allergens = _list_in_force(jurisdiction, on_date)
    known = {a.code for a in allergens}
    for ingredient in ingredients:
        for code in list(ingredient.contains) + list(ingredient.may_contain):
            if code not in known:
                raise ValueError(
                    'unknown allergen "%s" in "%s": not on the %s list on %s' % (code, ingredient.name, jurisdiction, on_date)
                )
    rows: List[AllergenRow] = []
    for allergen in allergens:
        contains = [i.name for i in ingredients if allergen.code in i.contains]
        may = [i.name for i in ingredients if allergen.code in i.may_contain]
        rows.append(
            AllergenRow(
                code=allergen.code,
                name=allergen.name,
                status="contains" if contains else "may-contain" if may else "none",
                sources=_dedupe(contains if contains else may),
            )
        )
    return rows