mattbitzesty's picture
feat: Milestone 4 clean code snapshot
b233cf7
Raw
History Blame Contribute Delete
5.66 kB
from __future__ import annotations
import logging
from typing import Any
from .models import Formula, Ingredient
logger = logging.getLogger("pino.ifra")
# Embedded IFRA Category 4 (fine fragrance) restriction table.
# Values are the maximum finished-product concentration in % (w/w).
# Restricted materials only; most common materials are unrestricted.
IFRA_RESTRICTIONS: dict[str, dict[str, Any]] = {
"64-17-5": {"material": "Ethanol", "category_4_pct": 100.0, "note": "unrestricted"},
"5989-27-5": {"material": "D-Limonene", "category_4_pct": 33.0, "note": "sensitization"},
"138-86-3": {"material": "Limonene", "category_4_pct": 33.0, "note": "sensitization"},
"97-53-0": {"material": "Eugenol", "category_4_pct": 6.5, "note": "sensitization"},
"97-54-1": {"material": "Isoeugenol", "category_4_pct": 2.0, "note": "sensitization"},
"80-54-6": {"material": "Lilial", "category_4_pct": 0.0, "note": "prohibited"},
"122-40-7": {"material": "Alpha-amyl cinnamic aldehyde", "category_4_pct": 5.8, "note": "sensitization"},
"101-86-0": {"material": "Alpha-hexyl cinnamic aldehyde", "category_4_pct": 6.6, "note": "sensitization"},
"104-55-2": {"material": "Cinnamaldehyde", "category_4_pct": 4.0, "note": "sensitization"},
"143-50-0": {"material": "Methyl eugenol", "category_4_pct": 0.02, "note": "systemic toxicity"},
"92-48-8": {"material": "6-Methylcoumarin", "category_4_pct": 0.0, "note": "prohibited"},
"91-64-5": {"material": "Coumarin", "category_4_pct": 1.5, "note": "hepatotoxicity"},
"121-33-5": {"material": "Vanillin", "category_4_pct": 25.0, "note": "sensitization"},
"8016-38-4": {"material": "Bergamot oil", "category_4_pct": 4.0, "note": "phototoxicity"},
"8007-75-8": {"material": "Lime oil", "category_4_pct": 4.0, "note": "phototoxicity"},
"8008-56-8": {"material": "Lemon oil", "category_4_pct": 12.0, "note": "phototoxicity"},
"8000-27-9": {"material": "Bitter orange oil", "category_4_pct": 12.0, "note": "phototoxicity"},
"8000-48-4": {"material": "Grapefruit oil", "category_4_pct": 10.0, "note": "phototoxicity"},
"8008-31-9": {"material": "Mandarin oil", "category_4_pct": 20.0, "note": "phototoxicity"},
"9000-05-9": {"material": "Peru balsam", "category_4_pct": 2.0, "note": "sensitization"},
"9000-50-4": {"material": "Styrax resinoid", "category_4_pct": 2.0, "note": "sensitization"},
"8014-09-3": {"material": "Ylang-ylang oil", "category_4_pct": 7.5, "note": "sensitization"},
"8000-34-8": {"material": "Clove oil", "category_4_pct": 5.0, "note": "sensitization"},
"8006-84-6": {"material": "Cinnamon bark oil", "category_4_pct": 4.0, "note": "sensitization"},
"8007-08-7": {"material": "Cinnamon leaf oil", "category_4_pct": 4.0, "note": "sensitization"},
"9000-64-0": {"material": "Oakmoss absolute", "category_4_pct": 0.6, "note": "sensitization"},
"68606-12-6": {"material": "Treemoss absolute", "category_4_pct": 0.6, "note": "sensitization"},
"119-36-8": {"material": "Methyl salicylate", "category_4_pct": 4.0, "note": "systemic toxicity"},
"89-68-9": {"material": "p-Chloro-m-cresol", "category_4_pct": 0.0, "note": "prohibited"},
"83-66-9": {"material": "Musk ambrette", "category_4_pct": 0.0, "note": "prohibited"},
"81-14-1": {"material": "Musk ketone", "category_4_pct": 4.0, "note": "systemic toxicity"},
"105-95-3": {"material": "Ethylene brassylate", "category_4_pct": 20.0, "note": "sensitization"},
"81-15-2": {"material": "Musk xylene", "category_4_pct": 0.0, "note": "prohibited"},
"131-11-3": {"material": "Dimethyl phthalate", "category_4_pct": 5.0, "note": "systemic toxicity"},
"84-66-2": {"material": "Diethyl phthalate", "category_4_pct": 5.0, "note": "systemic toxicity"},
"25265-71-8": {"material": "Dipropylene glycol", "category_4_pct": 100.0, "note": "unrestricted"},
"120-51-4": {"material": "Benzyl benzoate", "category_4_pct": 35.0, "note": "unrestricted"},
}
def check_ifra_restrictions(formula: Formula) -> dict[str, Any]:
"""
Check a formula against the embedded IFRA Category 4 restriction table.
Returns a dict with 'passed', 'violations', and 'max_usage'.
"""
violations: list[dict[str, Any]] = []
max_usage: list[dict[str, Any]] = []
weights = formula.weight_fractions
passed = True
for ing, wf in zip(formula.ingredients, weights):
cas = ing.cas
if cas not in IFRA_RESTRICTIONS:
continue
restriction = IFRA_RESTRICTIONS[cas]
limit = restriction["category_4_pct"]
pct = wf * 100.0
max_usage.append(
{
"cas": cas,
"material": restriction["material"],
"used_pct": pct,
"limit_pct": limit,
}
)
if limit == 0.0 and pct > 0.0:
violations.append(
{
"cas": cas,
"material": restriction["material"],
"used_pct": pct,
"limit_pct": limit,
"reason": restriction["note"],
}
)
passed = False
elif limit > 0.0 and pct > limit:
violations.append(
{
"cas": cas,
"material": restriction["material"],
"used_pct": pct,
"limit_pct": limit,
"reason": restriction["note"],
}
)
passed = False
return {
"passed": passed,
"violations": violations,
"max_usage": max_usage,
}