Spaces:
Sleeping
Sleeping
| """ | |
| hf_reader.py — HuggingFace data layer for the School Name Resolver. | |
| Reads: | |
| 1. master_all_states.csv (Old Master) from HF_RESOLVER_REPO | |
| 2. scraped_data/mapped/*.parquet (new masters) from HF_SCRAPER_REPO | |
| All data is cached in memory after first load. Call refresh_all_caches() to force reload. | |
| """ | |
| import os | |
| import re | |
| import pandas as pd | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from datetime import datetime | |
| from rapidfuzz import process, fuzz | |
| try: | |
| from admin_patterns import normalize_with_patterns_dynamic | |
| except ImportError: | |
| normalize_with_patterns_dynamic = lambda s, st: s | |
| HF_TOKEN = os.getenv("HF_TOKEN", "") | |
| HF_SCRAPER_REPO = os.getenv("HF_SCRAPER_REPO", "") | |
| # ─── Module-level caches ─────────────────────────────────────────────────────── | |
| _old_master_df: pd.DataFrame | None = None | |
| _mapped_masters: dict[str, pd.DataFrame] = {} | |
| _cache_loaded: bool = False | |
| # ─── Internal helpers ────────────────────────────────────────────────────────── | |
| def _load_old_master() -> pd.DataFrame: | |
| """Download and return baseline_master.parquet from HF_SCRAPER_REPO.""" | |
| if not HF_SCRAPER_REPO: | |
| print("[hf_reader] HF_SCRAPER_REPO not set — skipping Old Master load.") | |
| return pd.DataFrame() | |
| try: | |
| path = hf_hub_download( | |
| repo_id=HF_SCRAPER_REPO, | |
| filename="mapping_rules/baseline_master.parquet", | |
| repo_type="dataset", | |
| token=HF_TOKEN or None, | |
| force_download=True, | |
| ) | |
| df = pd.read_parquet(path) | |
| if "School_Udise_Code__c" in df.columns: | |
| df["School_Udise_Code__c"] = df["School_Udise_Code__c"].astype(str).str.strip() | |
| print(f"[hf_reader] Old Master loaded: {len(df):,} rows.") | |
| return df | |
| except Exception as e: | |
| print(f"[hf_reader] Could not load baseline_master.parquet: {e}") | |
| return pd.DataFrame() | |
| def _load_mapped_masters() -> dict[str, pd.DataFrame]: | |
| """Download all parquet files from scraped_data/mapped/ in HF_SCRAPER_REPO.""" | |
| if not HF_SCRAPER_REPO: | |
| print("[hf_reader] HF_SCRAPER_REPO not set — skipping mapped masters load.") | |
| return {} | |
| api = HfApi() | |
| results: dict[str, pd.DataFrame] = {} | |
| try: | |
| all_files = list(api.list_repo_files( | |
| repo_id=HF_SCRAPER_REPO, | |
| repo_type="dataset", | |
| token=HF_TOKEN or None, | |
| )) | |
| mapped_files = sorted( | |
| [f for f in all_files if f.startswith("scraped_data/mapped/") and f.endswith(".parquet")], | |
| reverse=True, | |
| ) | |
| print(f"[hf_reader] Found {len(mapped_files)} mapped master(s) in scraper repo.") | |
| for file_path in mapped_files: | |
| try: | |
| local_path = hf_hub_download( | |
| repo_id=HF_SCRAPER_REPO, | |
| filename=file_path, | |
| repo_type="dataset", | |
| token=HF_TOKEN or None, | |
| force_download=True, | |
| ) | |
| df = pd.read_parquet(local_path) | |
| if "School_Udise_Code__c" in df.columns: | |
| df["School_Udise_Code__c"] = df["School_Udise_Code__c"].astype(str).str.strip() | |
| stem = file_path.split("/")[-1].replace(".parquet", "") | |
| results[stem] = df | |
| print(f"[hf_reader] Loaded: {stem} ({len(df):,} rows)") | |
| except Exception as e: | |
| print(f"[hf_reader] Failed to load {file_path}: {e}") | |
| except Exception as e: | |
| print(f"[hf_reader] Could not list scraper repo files: {e}") | |
| return results | |
| def _ensure_loaded(): | |
| """Load all caches if they haven't been loaded yet.""" | |
| global _old_master_df, _mapped_masters, _cache_loaded | |
| if not _cache_loaded: | |
| print("[hf_reader] Initializing caches... This may take a few minutes if downloading from HuggingFace.") | |
| _old_master_df = _load_old_master() | |
| print("[hf_reader] Old Master loaded.") | |
| _mapped_masters = _load_mapped_masters() | |
| print(f"[hf_reader] Mapped Masters loaded: {len(_mapped_masters)} files.") | |
| _cache_loaded = True | |
| print("[hf_reader] All caches fully loaded and ready.") | |
| def get_state_hierarchy() -> dict: | |
| """ | |
| Returns a nested dictionary of the geographic hierarchy: | |
| { state: { district: { block: [villages] } } } | |
| Built by combining all loaded data sources. | |
| """ | |
| _ensure_loaded() | |
| hier = {} | |
| dfs_to_process = [] | |
| if _old_master_df is not None and not _old_master_df.empty: | |
| dfs_to_process.append(_old_master_df) | |
| dfs_to_process.extend(_mapped_masters.values()) | |
| for df in dfs_to_process: | |
| if df.empty: continue | |
| s_col = "School_State__c" | |
| d_col = "School_District__c" | |
| b_col = "School_Block__c" | |
| v_col = "School_Village__c" | |
| if s_col not in df.columns: continue | |
| df_tmp = df.copy() | |
| for col in [s_col, d_col, b_col, v_col]: | |
| if col not in df_tmp.columns: | |
| df_tmp[col] = "" | |
| else: | |
| df_tmp[col] = df_tmp[col].fillna("").astype(str).str.strip().str.upper() | |
| for _, r in df_tmp.iterrows(): | |
| st = r[s_col] | |
| di = r[d_col] | |
| bl = r[b_col] | |
| vi = r[v_col] | |
| if not st: continue | |
| if st not in hier: hier[st] = {} | |
| if not di: continue | |
| if di not in hier[st]: hier[st][di] = {} | |
| if not bl: continue | |
| if bl not in hier[st][di]: hier[st][di][bl] = set() | |
| if vi: | |
| hier[st][di][bl].add(vi) | |
| for s in hier: | |
| for d in hier[s]: | |
| for b in hier[s][d]: | |
| hier[s][d][b] = sorted(list(hier[s][d][b])) | |
| return hier | |
| def search_by_name_fuzzy(query_name: str, state: str, district: str, block: str, village: str = None, max_results=10) -> list[str]: | |
| """ | |
| Fuzzy searches the combined data sources for a school name. | |
| Returns a list of unique matched UDISE codes. | |
| """ | |
| _ensure_loaded() | |
| if not query_name: | |
| return [] | |
| dfs_to_process = [] | |
| if _old_master_df is not None and not _old_master_df.empty: | |
| dfs_to_process.append(_old_master_df) | |
| dfs_to_process.extend(_mapped_masters.values()) | |
| if not dfs_to_process: | |
| return [] | |
| combined_rows = [] | |
| for df in dfs_to_process: | |
| if df.empty: continue | |
| if "School_Name__c" not in df.columns or "School_Udise_Code__c" not in df.columns: continue | |
| mask = pd.Series(True, index=df.index) | |
| if state: | |
| if "School_State__c" in df.columns: | |
| mask = mask & (df["School_State__c"].astype(str).str.strip().str.upper() == state.upper()) | |
| else: | |
| continue | |
| if district: | |
| if "School_District__c" in df.columns: | |
| mask = mask & (df["School_District__c"].astype(str).str.strip().str.upper() == district.upper()) | |
| else: | |
| continue | |
| if block: | |
| if "School_Block__c" in df.columns: | |
| mask = mask & (df["School_Block__c"].astype(str).str.strip().str.upper() == block.upper()) | |
| else: | |
| continue | |
| if village: | |
| if "School_Village__c" in df.columns: | |
| mask = mask & (df["School_Village__c"].astype(str).str.strip().str.upper() == village.upper()) | |
| else: | |
| continue | |
| filtered = df[mask] | |
| if not filtered.empty: | |
| filtered_sub = filtered[["School_Udise_Code__c", "School_Name__c"]].copy() | |
| combined_rows.append(filtered_sub) | |
| if not combined_rows: | |
| return [] | |
| combined_df = pd.concat(combined_rows, ignore_index=True).drop_duplicates() | |
| combined_df["School_Name__c"] = combined_df["School_Name__c"].astype(str).str.strip() | |
| combined_df = combined_df[combined_df["School_Name__c"] != ""] | |
| if combined_df.empty: | |
| return [] | |
| choices = combined_df["School_Name__c"].tolist() | |
| state_for_patterns = (state or "ARUNACHAL PRADESH").upper() | |
| candidates_raw = process.extract( | |
| query_name, | |
| choices, | |
| scorer=fuzz.token_set_ratio, | |
| processor=lambda s: normalize_with_patterns_dynamic(s, state_for_patterns), | |
| limit=max_results, | |
| ) | |
| matched_udises = [] | |
| seen_udises = set() | |
| for choice, score, idx in candidates_raw: | |
| udise = combined_df.iloc[idx]["School_Udise_Code__c"] | |
| udise = str(udise).strip() | |
| if udise and udise not in seen_udises: | |
| seen_udises.add(udise) | |
| matched_udises.append(udise) | |
| return matched_udises | |
| # ─── Public API ─────────────────────────────────────────────────────────────── | |
| def refresh_all_caches() -> str: | |
| """Force-reload all data from HuggingFace. Returns a status message.""" | |
| global _old_master_df, _mapped_masters, _cache_loaded | |
| _cache_loaded = False | |
| _old_master_df = None | |
| _mapped_masters = {} | |
| _ensure_loaded() | |
| n_masters = len(_mapped_masters) | |
| sf_rows = len(_old_master_df) if _old_master_df is not None else 0 | |
| return ( | |
| f"✅ Caches refreshed — Old Master: {sf_rows:,} rows, " | |
| f"Mapped Masters: {n_masters} file(s) loaded." | |
| ) | |
| def _pretty_label(stem: str, idx: int) -> tuple[str, str]: | |
| """Turn a filename stem like 'mapped_master_2026_jul_01_10_19_pm' into a readable label and year_month.""" | |
| match_str = re.search(r"(\d{4})_([a-zA-Z]{3})_(\d{2})", stem) | |
| if match_str: | |
| y, m_str, d = match_str.groups() | |
| date_str = f"{d}-{m_str.capitalize()}-{y}" | |
| ym = f"{y}-{m_str.capitalize()}" | |
| else: | |
| match_num = re.search(r"(\d{4})(\d{2})(\d{2})", stem) | |
| if match_num: | |
| y, m, d = match_num.groups() | |
| date_str = f"{d}/{m}/{y}" | |
| month_str = datetime.strptime(m, "%m").strftime("%b").capitalize() | |
| ym = f"{y}-{month_str}" | |
| else: | |
| date_str = stem | |
| ym = "" | |
| if idx == 0: | |
| return f"Latest Scraped Master ({date_str})", ym | |
| else: | |
| return f"Older Scraped Master ({date_str})", ym | |
| def search_udise(udise_code: str, marksheet_name: str = "") -> list[dict]: | |
| """ | |
| Search for a UDISE code across all data sources. | |
| Returns a list of dicts. | |
| """ | |
| _ensure_loaded() | |
| results: list[dict] = [] | |
| if marksheet_name and marksheet_name.strip(): | |
| results.append({ | |
| "source": "Marksheet (entered by you)", | |
| "source_type": "marksheet", | |
| "name": marksheet_name.strip(), | |
| "state": "", | |
| "district": "", | |
| "block": "", | |
| "year_month": datetime.now().strftime("%Y-%b").capitalize(), | |
| }) | |
| if _old_master_df is not None and not _old_master_df.empty: | |
| mask = _old_master_df["School_Udise_Code__c"] == str(udise_code).strip() | |
| matches = _old_master_df[mask] | |
| for _, row in matches.iterrows(): | |
| results.append({ | |
| "source": "Old Master (baseline_master.parquet)", | |
| "source_type": "legacy_db", | |
| "name": str(row.get("School_Name__c", "")).strip(), | |
| "state": str(row.get("School_State__c", "")).strip(), | |
| "district": str(row.get("School_District__c", "")).strip(), | |
| "block": str(row.get("School_Block__c", "")).strip(), | |
| "village": str(row.get("School_Village__c", "")).strip(), | |
| "year_month": "2025", | |
| }) | |
| for idx, (stem, df) in enumerate(_mapped_masters.items()): | |
| if "School_Udise_Code__c" not in df.columns: | |
| continue | |
| mask = df["School_Udise_Code__c"] == str(udise_code).strip() | |
| matches = df[mask] | |
| label, ym = _pretty_label(stem, idx) | |
| source_type = "latest_master" if idx == 0 else "old_master" | |
| for _, row in matches.iterrows(): | |
| results.append({ | |
| "source": label, | |
| "source_type": source_type, | |
| "name": str(row.get("School_Name__c", "")).strip(), | |
| "state": str(row.get("School_State__c", "")).strip(), | |
| "district": str(row.get("School_District__c", "")).strip(), | |
| "block": str(row.get("School_Block__c", "")).strip(), | |
| "village": str(row.get("School_Village__c", "")).strip(), | |
| "year_month": ym, | |
| }) | |
| return results | |
| def get_udise_choices(state: str, district: str, block: str, village: str = None) -> list[str]: | |
| """Returns a list of 'UDISE - School Name' for the given location filter.""" | |
| if not state: | |
| return [] | |
| _ensure_loaded() | |
| dfs_to_process = [] | |
| if _old_master_df is not None and not _old_master_df.empty: | |
| dfs_to_process.append(_old_master_df) | |
| dfs_to_process.extend(_mapped_masters.values()) | |
| if not dfs_to_process: | |
| return [] | |
| combined_rows = [] | |
| for df in dfs_to_process: | |
| if df.empty: continue | |
| if "School_Name__c" not in df.columns or "School_Udise_Code__c" not in df.columns: continue | |
| mask = pd.Series(True, index=df.index) | |
| if state: | |
| if "School_State__c" in df.columns: | |
| mask &= (df["School_State__c"].astype(str).str.strip().str.upper() == state.upper()) | |
| else: continue | |
| if district: | |
| if "School_District__c" in df.columns: | |
| mask &= (df["School_District__c"].astype(str).str.strip().str.upper() == district.upper()) | |
| else: continue | |
| if block: | |
| if "School_Block__c" in df.columns: | |
| mask &= (df["School_Block__c"].astype(str).str.strip().str.upper() == block.upper()) | |
| else: continue | |
| if village: | |
| if "School_Village__c" in df.columns: | |
| mask &= (df["School_Village__c"].astype(str).str.strip().str.upper() == village.upper()) | |
| else: continue | |
| filtered = df[mask] | |
| if not filtered.empty: | |
| filtered_sub = filtered[["School_Udise_Code__c", "School_Name__c"]].copy() | |
| combined_rows.append(filtered_sub) | |
| if not combined_rows: | |
| return [] | |
| combined_df = pd.concat(combined_rows, ignore_index=True).drop_duplicates() | |
| combined_df["School_Udise_Code__c"] = combined_df["School_Udise_Code__c"].astype(str).str.strip() | |
| combined_df["School_Name__c"] = combined_df["School_Name__c"].astype(str).str.strip() | |
| combined_df = combined_df[(combined_df["School_Udise_Code__c"] != "") & (combined_df["School_Name__c"] != "")] | |
| if combined_df.empty: | |
| return [] | |
| choices = (combined_df["School_Udise_Code__c"] + " - " + combined_df["School_Name__c"]).unique().tolist() | |
| return sorted(choices) | |
| def get_config_status() -> dict: | |
| """Return a dict describing the current configuration state.""" | |
| _ensure_loaded() | |
| return { | |
| "scraper_repo": HF_SCRAPER_REPO or "⚠️ Not set (HF_SCRAPER_REPO)", | |
| "sf_baseline_rows": len(_old_master_df) if _old_master_df is not None else 0, | |
| "mapped_masters_count": len(_mapped_masters), | |
| "token_set": bool(HF_TOKEN), | |
| } | |