# utils.py import os import re import csv import json import ftfy from datetime import datetime from dateutil.parser import parse as date_parse from schemas import DEFAULT_BLANK_FIELDS, CHOICE_OPTIONS from config import MIN_CONFIDENCE_THRESHOLD from agent import Field def serialize_knowledge_base(knowledge_base: dict) -> dict: serializable_kb = {} for variant, data in knowledge_base.items(): serializable_kb[variant] = {field_name: field_obj.to_dict() for field_name, field_obj in data.items()} return serializable_kb def deserialize_knowledge_base(cached_data: dict) -> dict: knowledge_base = {} for variant, data in cached_data.items(): knowledge_base[variant] = { field_name: Field.from_dict(field_data) for field_name, field_data in data.items() } return knowledge_base class SafeCSVWriter: def __init__(self, filepath, schema): self.filepath = filepath self.schema = schema self.file = None self.writer = None def __enter__(self): os.makedirs(os.path.dirname(self.filepath), exist_ok=True) file_exists = os.path.isfile(self.filepath) and os.path.getsize(self.filepath) > 0 self.file = open(self.filepath, 'a', newline='', encoding='utf-8') self.writer = csv.DictWriter(self.file, fieldnames=self.schema) if not file_exists: self.writer.writeheader() self.file.flush() return self def writerow(self, row): if self.writer: self.writer.writerow(row) self.file.flush() def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() def normalize_restricted_traffic(value: str) -> str: if not value: return "No" v = value.lower().strip() if v in['yes', 'true', '1', 'restricted']: return "Yes" return "No" def validate_row_integrity(row: dict, schema: list) -> bool: if 'event' in schema and not row.get('event'): print(f"[VALIDATION FAIL] Missing 'event' field.") return False if row.get('date'): if len(row['date']) > 50 or "http" in row['date']: row['date'] = "" return True def format_cutoff_time(val: str) -> str: """Strictly parses cutoff text into HH:MM format.""" val = str(val).lower().strip() if not val or val in['na', 'n/a', 'none', 'null', 'unknown', '']: return "" # 1. Match HH:MM or HH:MM:SS m = re.search(r'\b(\d{1,2}):(\d{2})(?::\d{2})?\b', val) if m: return f"{int(m.group(1)):02d}:{m.group(2)}" # 2. Match decimal hours (e.g. "8.5 hrs") m = re.search(r'(\d+\.\d+)\s*(?:h|hr|hour)', val) if m: h_float = float(m.group(1)) h = int(h_float) mins = int(round((h_float - h) * 60)) return f"{h:02d}:{mins:02d}" # 3. Match X hours Y mins m = re.search(r'(\d+)\s*(?:h|hr|hour)[^0-9]*(\d+)\s*(?:m|min)', val) if m: return f"{int(m.group(1)):02d}:{int(m.group(2)):02d}" # 4. Match exact X hours (e.g. "8 hours", "10 hrs") m = re.search(r'\b(\d+)\s*(?:h|hr|hour)\b', val) if m: return f"{int(m.group(1)):02d}:00" return "" def format_final_row(festival_name_input: str, variant_name: str, data: dict, schema: list) -> dict | None: def normalize_complex_value(val): if isinstance(val, str): val = val.strip() # If the string literally starts with AttributedDict, kill it. if val.startswith("AttributedDict"): return "" if val.startswith('{') and val.endswith('}'): try: val = json.loads(val) except json.JSONDecodeError: return val elif val.startswith('[') and val.endswith(']'): try: val = json.loads(val) except json.JSONDecodeError: return val else: return val if isinstance(val, dict): for key in['amount', 'value', 'price', 'final', 'standard', 'date', 'time', 'name']: if key in val: return normalize_complex_value(val[key]) parts =[] for v in val.values(): if isinstance(v, (int, float, str, bool)): parts.append(str(v)) return " | ".join(parts) if parts else "" if isinstance(val, list): strings =[str(i) for i in val if isinstance(i, (int, float, str, bool))] return ", ".join(strings) if strings else "" return str(val) if val is not None else "" def get_value(field_name: str) -> any: field_obj = data.get(field_name) if not isinstance(field_obj, Field): return "" if field_obj.inferred_by == 'pre_processed_data': return field_obj.value min_thresh = 0.4 if "inference" in field_obj.inferred_by else MIN_CONFIDENCE_THRESHOLD if field_obj.confidence >= min_thresh: return field_obj.value return "" def finalize_value(value: any) -> str: value = normalize_complex_value(value) text = ftfy.fix_text(str(value)).strip() # Aggressive cleaning for corrupted stringified objects if "AttributedDict" in text or "{" in text or "}" in text: return "" if text.lower() in["na", "n/a", "", "none", "not specified", "null", "unknown"]: return "" return text row = {} for key in schema: if key in DEFAULT_BLANK_FIELDS: row[key] = "" continue raw_value = finalize_value(get_value(key)) if not raw_value: row[key] = "" continue if key == "city": row[key] = raw_value.split(',')[0].strip() elif key == "registrationCost": cleaned = re.sub(r'[^\d.]', '', raw_value) row[key] = cleaned elif key in["runningDistance", "cyclingDistance", "swimDistance"]: match = re.search(r'(\d+\.?\d*)', raw_value) row[key] = match.group(1) if match else "" elif key in["date", "lastDate"]: if re.match(r"\d{4}-\d{2}-\d{2}", raw_value): try: row[key] = datetime.strptime(raw_value, "%Y-%m-%d").strftime("%d-%m-%Y") except ValueError: row[key] = "" elif re.match(r"\d{2}-\d{2}-\d{4}", raw_value): row[key] = raw_value else: try: dt = date_parse(raw_value, fuzzy=True, dayfirst=True) if 2020 <= dt.year <= 2030: row[key] = dt.strftime("%d-%m-%Y") else: row[key] = "" except (ValueError, TypeError, OverflowError): row[key] = "" elif key == "startTime": try: dt = date_parse(raw_value, fuzzy=True) row[key] = dt.strftime("%H:%M") except (ValueError, TypeError, OverflowError): row[key] = "" elif "Cutoff" in key: row[key] = format_cutoff_time(raw_value) elif key in["firstEdition", "lastEdition", "editionYear"]: match = re.search(r'\b((?:19|20)\d{2})\b', raw_value) row[key] = match.group(1) if match else "" elif key == "restrictedTraffic": row[key] = normalize_restricted_traffic(raw_value) else: row[key] = raw_value if festival_name_input and festival_name_input.lower() not in variant_name.lower(): row["event"] = f"{festival_name_input} - {variant_name}" else: row["event"] = variant_name if not row.get("organiser") and festival_name_input: row["organiser"] = festival_name_input if row.get("date"): try: dt = datetime.strptime(row["date"], "%d-%m-%Y") row["month"] = dt.strftime("%B") if not row.get("editionYear"): row["editionYear"] = str(dt.year) except Exception: pass if not row.get("approvalStatus"): row["approvalStatus"] = "Approved" if not validate_row_integrity(row, schema): return None return row