File size: 3,354 Bytes
89d9642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
"""The user profile that drives filtering and ranking."""

from __future__ import annotations

from dataclasses import dataclass, field

# Allergen options, mapped to OFF's canonical allergen tags. Using OFF's tags
# means we match on the curated `allergens_tags`/`traces_tags` arrays rather
# than guessing from ingredient text with regexes.
ALLERGEN_CHOICES: dict[str, str] = {
    "Gluten": "en:gluten",
    "Milk / Dairy": "en:milk",
    "Eggs": "en:eggs",
    "Peanuts": "en:peanuts",
    "Tree nuts": "en:nuts",
    "Soybeans": "en:soybeans",
    "Fish": "en:fish",
    "Crustaceans": "en:crustaceans",
    "Molluscs": "en:molluscs",
    "Sesame seeds": "en:sesame-seeds",
    "Celery": "en:celery",
    "Mustard": "en:mustard",
    "Sulphites": "en:sulphur-dioxide-and-sulphites",
    "Lupin": "en:lupin",
}

DIET_CHOICES = ["Vegetarian", "Vegan", "Palm-oil free"]

# Per-100g ceilings for health conditions.
#
# The app this replaces tested `sodium_100g > 5.0` -- five *grams* of sodium per
# 100 g, a level essentially no food reaches, so the rule never fired. These
# thresholds come from the FDA "high in" labelling guidance: a food is high in a
# nutrient at 20% of the Daily Value per serving. Expressed per 100 g against
# DVs of 2300 mg sodium and 20 g saturated fat.
SODIUM_CEILING_G_100G = 0.46  # 20% of the 2300 mg DV
SALT_CEILING_G_100G = SODIUM_CEILING_G_100G * 2.5
SATURATED_FAT_CEILING_G_100G = 4.0  # 20% of the 20 g DV


@dataclass
class UserProfile:
    """Everything the engine needs to know about a person.

    Allergens are stored as OFF tags, not display labels, so the filters are
    exact set operations.
    """

    user_id: str = ""
    age: int = 30
    gender: str = "Prefer not to say"
    allergens: list[str] = field(default_factory=list)  # OFF tags, e.g. 'en:peanuts'
    diets: list[str] = field(default_factory=list)  # from DIET_CHOICES
    high_blood_pressure: bool = False
    high_cholesterol: bool = False
    avoid_flagged_additives: bool = False

    @property
    def wants_vegan(self) -> bool:
        return "Vegan" in self.diets

    @property
    def wants_vegetarian(self) -> bool:
        # Vegan is stricter than vegetarian and implies it.
        return "Vegetarian" in self.diets or self.wants_vegan

    @property
    def wants_palm_oil_free(self) -> bool:
        return "Palm-oil free" in self.diets

    def to_dict(self) -> dict:
        return {
            "age": self.age,
            "gender": self.gender,
            "allergens": self.allergens,
            "diets": self.diets,
            "high_blood_pressure": self.high_blood_pressure,
            "high_cholesterol": self.high_cholesterol,
            "avoid_flagged_additives": self.avoid_flagged_additives,
        }

    @classmethod
    def from_dict(cls, data: dict) -> UserProfile:
        return cls(
            user_id=str(data.get("_id", data.get("user_id", ""))),
            age=int(data.get("age", 30)),
            gender=data.get("gender", "Prefer not to say"),
            allergens=list(data.get("allergens", [])),
            diets=list(data.get("diets", [])),
            high_blood_pressure=bool(data.get("high_blood_pressure", False)),
            high_cholesterol=bool(data.get("high_cholesterol", False)),
            avoid_flagged_additives=bool(data.get("avoid_flagged_additives", False)),
        )