| """ |
| 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", "") |
|
|
| |
| _old_master_df: pd.DataFrame | None = None |
| _mapped_masters: dict[str, pd.DataFrame] = {} |
| _cache_loaded: bool = False |
|
|
|
|
| |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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.""" |
| _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), |
| } |
|
|