| from __future__ import annotations |
|
|
| import logging |
| from typing import Any |
|
|
| from .models import Formula, Ingredient |
|
|
| logger = logging.getLogger("pino.ifra") |
|
|
|
|
| |
| |
| |
| 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, |
| } |
|
|