|
|
|
|
|
from misc.rules import RULES
|
|
|
|
|
|
PRIORITY_ATTRS = ["soft_bag", "foam", "paper_cup", "carton", "greasy_or_wet", "hazard"]
|
|
|
ATTR_LABELS = {
|
|
|
"soft_bag": "Soft bag / plastic wrap",
|
|
|
"foam": "Foam / Styrofoam",
|
|
|
"paper_cup": "Paper cup",
|
|
|
"carton": "Carton",
|
|
|
"greasy_or_wet": "Greasy or wet",
|
|
|
"hazard": "Hazardous item",
|
|
|
}
|
|
|
|
|
|
|
|
|
def _normalize_city(city: str | None) -> str:
|
|
|
if not city:
|
|
|
return "default"
|
|
|
city = city.strip().lower()
|
|
|
if "," in city:
|
|
|
city = city.split(",", 1)[0].strip()
|
|
|
return city or "default"
|
|
|
|
|
|
|
|
|
def _title_city(city_key: str) -> str:
|
|
|
if city_key in ("default", "", None):
|
|
|
return "Default"
|
|
|
return " ".join(w.capitalize() for w in city_key.split())
|
|
|
|
|
|
|
|
|
def _normalize_bool(val) -> bool:
|
|
|
if isinstance(val, bool):
|
|
|
return val
|
|
|
if val is None:
|
|
|
return False
|
|
|
if isinstance(val, (int, float)):
|
|
|
return val != 0
|
|
|
s = str(val).strip().lower()
|
|
|
if s in {"1", "true", "yes", "y", "on"}:
|
|
|
return True
|
|
|
if s in {"0", "false", "no", "n", "off", ""}:
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
|
|
|
def _normalize_attrs(attrs: dict | None) -> dict:
|
|
|
attrs = attrs or {}
|
|
|
return {k: _normalize_bool(v) for k, v in attrs.items()}
|
|
|
|
|
|
|
|
|
def _material_key_case_insensitive(table: dict, material: str | None) -> str | None:
|
|
|
"""
|
|
|
Return the correct material key from `table` using robust case handling.
|
|
|
Tries:
|
|
|
1) exact
|
|
|
2) Title Case (e.g., "plastic bottle" -> "Plastic Bottle")
|
|
|
3) case-insensitive scan over existing keys
|
|
|
"""
|
|
|
if not material:
|
|
|
return None
|
|
|
m = material.strip()
|
|
|
if not m:
|
|
|
return None
|
|
|
|
|
|
|
|
|
if m in table:
|
|
|
return m
|
|
|
|
|
|
|
|
|
m_title = " ".join(w.capitalize() for w in m.split())
|
|
|
if m_title in table:
|
|
|
return m_title
|
|
|
|
|
|
|
|
|
m_lower = m.lower()
|
|
|
for k in table.keys():
|
|
|
if k.lower() == m_lower:
|
|
|
return k
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
def _lookup_material_rules(city_key: str, material: str | None) -> tuple[dict, str]:
|
|
|
"""
|
|
|
Resolve the material rules for a city with case-insensitive matching.
|
|
|
Fallback order:
|
|
|
1) City table match
|
|
|
2) City's "Trash"
|
|
|
3) Default table match
|
|
|
4) Default "Trash"
|
|
|
5) {"default": "Landfill"}
|
|
|
Returns (rules_dict, resolved_material_name)
|
|
|
"""
|
|
|
city_table = RULES.get(city_key, RULES["default"])
|
|
|
|
|
|
key = _material_key_case_insensitive(city_table, material)
|
|
|
if key:
|
|
|
return city_table[key], key
|
|
|
|
|
|
if "Trash" in city_table:
|
|
|
return city_table["Trash"], "Trash"
|
|
|
|
|
|
default_table = RULES["default"]
|
|
|
key = _material_key_case_insensitive(default_table, material)
|
|
|
if key:
|
|
|
return default_table[key], key
|
|
|
|
|
|
if "Trash" in default_table:
|
|
|
return default_table["Trash"], "Trash"
|
|
|
|
|
|
return {"default": "Landfill"}, material or "Unknown"
|
|
|
|
|
|
|
|
|
def decide_action(material: str, attrs: dict | None, city: str | None):
|
|
|
"""
|
|
|
Decide action using case-insensitive material matching and boolean-normalized attrs.
|
|
|
"""
|
|
|
city_key = _normalize_city(city)
|
|
|
city_disp = _title_city(city_key)
|
|
|
norm_attrs = _normalize_attrs(attrs)
|
|
|
|
|
|
mat_rules, resolved_mat = _lookup_material_rules(city_key, material)
|
|
|
|
|
|
|
|
|
for a in PRIORITY_ATTRS:
|
|
|
if norm_attrs.get(a) and a in mat_rules:
|
|
|
attr_label = ATTR_LABELS.get(a, a.replace("_", " "))
|
|
|
action = mat_rules[a]
|
|
|
return action, f"{resolved_mat} marked as '{attr_label}' → {action} ({city_disp})"
|
|
|
|
|
|
|
|
|
action = mat_rules.get("default", "Landfill")
|
|
|
return action, f"{resolved_mat} → {action} ({city_disp})"
|
|
|
|