Spaces:
Running
Running
Commit Β·
13b211e
1
Parent(s): 8593ea0
Updated mapping manager
Browse files- README.md +12 -9
- USER_MANUAL.md +136 -0
- admin_rename_sf_district.py +13 -3
- app.py +156 -20
- hf_store.py +33 -19
README.md
CHANGED
|
@@ -19,11 +19,11 @@ An automated, end-to-end data pipeline to harvest school data from the KYS UDISE
|
|
| 19 |
2. **Intelligent Retries:** Automatically detects CAPTCHA failures and retries only missing data. The terminal UI wipes clean automatically before each operation.
|
| 20 |
3. **UDISE Geo-Decoding:** For non-actual state scrapes (e.g., KVS, NVS, NAVY, IAF), it parses the UDISE code to determine the *real* State, District, and Block.
|
| 21 |
4. **Automated UDISE-Tracking Math Engine:** When building the master sheet, an intelligent math engine automatically resolves renamed districts and blocks!
|
| 22 |
-
- It cross-references newly scraped UDISE codes against a cloud-stored
|
| 23 |
- If a district was simply renamed (e.g., EAST DISTRICT -> GANGTOK), the math engine statistically tracks the UDISE codes, proves the rename, and automatically generates a mapping rule in the cloud (`manual_district_mapping.parquet`).
|
| 24 |
- For complex fractured districts, it drills down to the Block level (`manual_block_mapping.parquet`) to perfectly map schools to their old districts.
|
| 25 |
5. **HuggingFace Cloud Architecture:** Syncs directly to a HuggingFace dataset organized into 3 folders:
|
| 26 |
-
- `district_reference`: Source of truth for
|
| 27 |
- `mapping_rules`: Automated + manual district & block mapping rules (Dataset 2) & the SF Baseline Master.
|
| 28 |
- `scraped_data`: Holds both `raw` per-state files and the final `mapped` master sheet.
|
| 29 |
6. **Smart District Flagging (Hybrid System):** Because the math engine automatically handles all simple renames behind the scenes, the UI only flags *genuinely brand-new* or highly ambiguous districts for your manual review in the Master Sheet tab!
|
|
@@ -84,22 +84,25 @@ The core scraping workflow, all on one screen:
|
|
| 84 |
|
| 85 |
Combines all raw state data from HuggingFace into a single mapped master Excel file, and manages flagged districts.
|
| 86 |
|
| 87 |
-
- **State Coverage Table:** Connects to your HuggingFace dataset and displays which states are ready to build. You can delete raw data directly from here.
|
| 88 |
-
- **Review New Districts:** Automatically detects newly scraped districts not in your
|
| 89 |
- **Build Master Sheet:** Automatically:
|
| 90 |
1. Pulls all raw complete state parquets.
|
| 91 |
2. Applies geo-decoding for KVS/NVS/NAVY schools.
|
| 92 |
3. Applies district back-mapping based on your rules.
|
| 93 |
-
4. Tags schools with their `
|
| 94 |
5. Saves the final mapped file to `scraped_data/mapped/` on HuggingFace and provides an Excel download.
|
| 95 |
|
| 96 |
---
|
| 97 |
|
| 98 |
### πΊοΈ Tab 3: Mapping Manager
|
| 99 |
|
| 100 |
-
A full database management dashboard to maintain your
|
| 101 |
|
| 102 |
-
- **
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
---
|
| 105 |
|
|
@@ -114,7 +117,7 @@ A full database management dashboard to maintain your Salesforce mappings.
|
|
| 114 |
If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
|
| 115 |
|
| 116 |
**`admin_rename_sf_district.py`**
|
| 117 |
-
If an old
|
| 118 |
```powershell
|
| 119 |
python admin_rename_sf_district.py --state "SIKKIM" --old "EAST DISTRICT (GANGTOK)" --new "GANGTOK"
|
| 120 |
```
|
|
@@ -155,7 +158,7 @@ python export_to_excel.py --state "GOA"
|
|
| 155 |
| `output/goa_district_id_map.json` | District dropdown mapping IDs for the portal |
|
| 156 |
| `output/goa_schools_by_category.json` | Raw scraped JSON arrays & API responses |
|
| 157 |
| `output_excel/goa_Schools.xlsx` | Raw Excel Export (before mapping) |
|
| 158 |
-
| `HF Dataset: district_reference/` | Dataset 1: The
|
| 159 |
| `HF Dataset: mapping_rules/` | Dataset 2: District and Block mapping rules |
|
| 160 |
| `HF Dataset: scraped_data/raw/{state}.parquet` | Per-state raw scraped data |
|
| 161 |
| `HF Dataset: scraped_data/mapped/` | Final combined mapped master output |
|
|
|
|
| 19 |
2. **Intelligent Retries:** Automatically detects CAPTCHA failures and retries only missing data. The terminal UI wipes clean automatically before each operation.
|
| 20 |
3. **UDISE Geo-Decoding:** For non-actual state scrapes (e.g., KVS, NVS, NAVY, IAF), it parses the UDISE code to determine the *real* State, District, and Block.
|
| 21 |
4. **Automated UDISE-Tracking Math Engine:** When building the master sheet, an intelligent math engine automatically resolves renamed districts and blocks!
|
| 22 |
+
- It cross-references newly scraped UDISE codes against a cloud-stored Scholarship Application Baseline (`baseline_master.parquet`).
|
| 23 |
- If a district was simply renamed (e.g., EAST DISTRICT -> GANGTOK), the math engine statistically tracks the UDISE codes, proves the rename, and automatically generates a mapping rule in the cloud (`manual_district_mapping.parquet`).
|
| 24 |
- For complex fractured districts, it drills down to the Block level (`manual_block_mapping.parquet`) to perfectly map schools to their old districts.
|
| 25 |
5. **HuggingFace Cloud Architecture:** Syncs directly to a HuggingFace dataset organized into 3 folders:
|
| 26 |
+
- `district_reference`: Source of truth for Scholarship Application district tracking (Dataset 1).
|
| 27 |
- `mapping_rules`: Automated + manual district & block mapping rules (Dataset 2) & the SF Baseline Master.
|
| 28 |
- `scraped_data`: Holds both `raw` per-state files and the final `mapped` master sheet.
|
| 29 |
6. **Smart District Flagging (Hybrid System):** Because the math engine automatically handles all simple renames behind the scenes, the UI only flags *genuinely brand-new* or highly ambiguous districts for your manual review in the Master Sheet tab!
|
|
|
|
| 84 |
|
| 85 |
Combines all raw state data from HuggingFace into a single mapped master Excel file, and manages flagged districts.
|
| 86 |
|
| 87 |
+
- **State Coverage Table:** Connects to your HuggingFace dataset and displays which states are ready to build. You can delete raw data directly from here (which strictly auto-cleans any pending districts for that state!).
|
| 88 |
+
- **Review New Districts:** Automatically detects newly scraped districts not in your Scholarship Application database. You can instantly map them to older Scholarship Application districts or rename them before building.
|
| 89 |
- **Build Master Sheet:** Automatically:
|
| 90 |
1. Pulls all raw complete state parquets.
|
| 91 |
2. Applies geo-decoding for KVS/NVS/NAVY schools.
|
| 92 |
3. Applies district back-mapping based on your rules.
|
| 93 |
+
4. Tags schools with their `Status` (e.g., `present` or `new districts found`).
|
| 94 |
5. Saves the final mapped file to `scraped_data/mapped/` on HuggingFace and provides an Excel download.
|
| 95 |
|
| 96 |
---
|
| 97 |
|
| 98 |
### πΊοΈ Tab 3: Mapping Manager
|
| 99 |
|
| 100 |
+
A full database management dashboard to maintain your Scholarship Application mappings.
|
| 101 |
|
| 102 |
+
- **Scholarship Application District Reference:** View and manage your master district list.
|
| 103 |
+
- **Click-to-Edit Rows:** Click any row in the table to instantly populate a quick-edit dropdown for updating its Status.
|
| 104 |
+
- **Bulk Excel Import/Export:** Expand the accordion to download the table, make bulk edits in Excel, and drag-and-drop it back to seamlessly sync with the cloud.
|
| 105 |
+
- **Smart CRUD Tools:** Add, Rename, and Delete districts. If you rename a district here, it perfectly cascades and updates your manual mapping rules automatically!
|
| 106 |
|
| 107 |
---
|
| 108 |
|
|
|
|
| 117 |
If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
|
| 118 |
|
| 119 |
**`admin_rename_sf_district.py`**
|
| 120 |
+
If an old Scholarship Application district name is permanently outdated and you want to rename it everywhere (becoming the new Baseline truth):
|
| 121 |
```powershell
|
| 122 |
python admin_rename_sf_district.py --state "SIKKIM" --old "EAST DISTRICT (GANGTOK)" --new "GANGTOK"
|
| 123 |
```
|
|
|
|
| 158 |
| `output/goa_district_id_map.json` | District dropdown mapping IDs for the portal |
|
| 159 |
| `output/goa_schools_by_category.json` | Raw scraped JSON arrays & API responses |
|
| 160 |
| `output_excel/goa_Schools.xlsx` | Raw Excel Export (before mapping) |
|
| 161 |
+
| `HF Dataset: district_reference/` | Dataset 1: The Scholarship Application reference mapping |
|
| 162 |
| `HF Dataset: mapping_rules/` | Dataset 2: District and Block mapping rules |
|
| 163 |
| `HF Dataset: scraped_data/raw/{state}.parquet` | Per-state raw scraped data |
|
| 164 |
| `HF Dataset: scraped_data/mapped/` | Final combined mapped master output |
|
USER_MANUAL.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π« School Data Fetcher - Complete User Manual
|
| 2 |
+
|
| 3 |
+
Welcome to the **School Data Fetcher**. This application is designed to scrape, clean, geo-decode, and map school data from UDISE+ and prepare it for seamless integration into your Scholarship Application database.
|
| 4 |
+
|
| 5 |
+
This manual provides a detailed, step-by-step guide to using every feature of the app.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## π Table of Contents
|
| 10 |
+
1. [Overview of the Workflow](#1-overview-of-the-workflow)
|
| 11 |
+
2. [Tab 1: π Scraper](#2-tab-1--scraper)
|
| 12 |
+
3. [Tab 2: π Master Sheet (Builder)](#3-tab-2--master-sheet-builder)
|
| 13 |
+
4. [Tab 3: πΊοΈ Mapping Manager](#4-tab-3--mapping-manager)
|
| 14 |
+
5. [Tab 4: π₯ Download History](#5-tab-4--download-history)
|
| 15 |
+
6. [Best Practices & Tips](#6-best-practices--tips)
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 1. Overview of the Workflow
|
| 20 |
+
|
| 21 |
+
The standard operational workflow for using this app is:
|
| 22 |
+
1. **Scrape Data:** Select a state and pull the latest data from the government portal.
|
| 23 |
+
2. **Handle Missing Data:** Retry any failed records to ensure 100% data capture.
|
| 24 |
+
3. **Check for New Districts:** Go to the Master Sheet tab to see if the state created any new districts this year.
|
| 25 |
+
4. **Update Reference:** If new districts exist, the scraper will have already automatically added them to the Mapping Manager (Tab 3) as `new districts found`. You just need to add them to your actual Scholarship Application system and update their status to `present` in Tab 3.
|
| 26 |
+
5. **Map & Build:** Create rules to map old/split districts to new ones, then build the final Excel file.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## 2. Tab 1: π Scraper
|
| 31 |
+
|
| 32 |
+
This is where you fetch the raw data directly from UDISE+.
|
| 33 |
+
|
| 34 |
+
### π Location & π School Types
|
| 35 |
+
1. **Select a State:** Choose the state (or Pan-India organization like KVS/NVS) from the dropdown.
|
| 36 |
+
2. **Select Categories:** By default, all 7 categories are selected. It is highly recommended to leave all of them selected unless you only need a very specific subset of data.
|
| 37 |
+
|
| 38 |
+
### βΆ Start Scraping
|
| 39 |
+
Click **Start Scraping** to begin the process.
|
| 40 |
+
- The app will securely navigate the government portal and bypass captchas.
|
| 41 |
+
- You will see live progress updates (e.g., `[14/50] Processing...`).
|
| 42 |
+
- Once finished, the app will instantly **Auto-Push** the raw data to your secure cloud storage on HuggingFace.
|
| 43 |
+
|
| 44 |
+
### β» Fix Missing Data
|
| 45 |
+
Sometimes, the government website times out or fails a captcha repeatedly.
|
| 46 |
+
- If the final status says **"β οΈ Some Records Are Incomplete"**, the **Fix Missing Data** button will appear.
|
| 47 |
+
- Click it. The app will *only* retry the specific schools that failed, saving you from having to restart the entire scrape.
|
| 48 |
+
- Keep clicking it until the status turns green (**β
All Data Retrieved!**).
|
| 49 |
+
|
| 50 |
+
### βοΈ Advanced Settings
|
| 51 |
+
- **Retry attempts:** You can increase the number of times the scraper tries to solve a captcha per school (default is 5). Higher numbers are more thorough but slower.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 3. Tab 2: π Master Sheet (Builder)
|
| 56 |
+
|
| 57 |
+
Once you have successfully scraped a state, you must process the raw data into a clean "Master Sheet".
|
| 58 |
+
|
| 59 |
+
### The Review Process
|
| 60 |
+
When you open this tab and select your newly scraped state, the app will automatically compare the raw data against your known **Scholarship Application District Reference**.
|
| 61 |
+
|
| 62 |
+
You will see one of two things:
|
| 63 |
+
|
| 64 |
+
#### Scenario A: "β
No flagged districts β all clear!"
|
| 65 |
+
This means the scraped state hasn't added any new districts, and all data perfectly matches what your Scholarship Application already knows.
|
| 66 |
+
- You can immediately click **Build Master Sheet**.
|
| 67 |
+
|
| 68 |
+
#### Scenario B: The Flagged Districts Table Appears
|
| 69 |
+
This means the scraped state contains districts that your Scholarship Application doesn't know about yet. A red table will appear showing you the anomalous districts.
|
| 70 |
+
|
| 71 |
+
**How to handle new districts:**
|
| 72 |
+
1. **Valid New District:** If the district is genuinely new (e.g., the state split a district in two), you must first add it to your Scholarship Application system. Then, go to **Tab 3: Mapping Manager**, mark it as `present`, and come back here.
|
| 73 |
+
2. **Name Mismatch:** If the district is just spelled differently (e.g., "PONDICHERRY" vs "PUDUCHERRY"), use the **Create Mapping Rule** section below the table to permanently teach the app how to map the new spelling to your expected spelling.
|
| 74 |
+
|
| 75 |
+
### Creating Mapping Rules
|
| 76 |
+
If you need to fix a name mismatch or map a split block to an old district:
|
| 77 |
+
1. Select the **Raw District Name** (how it appears in the scrape).
|
| 78 |
+
2. Select the **Target District Name** (how it should appear in your Scholarship Application).
|
| 79 |
+
3. (Optional) Select a specific Block if the rule should only apply to that block.
|
| 80 |
+
4. Click **Add Rule**. The app will remember this rule forever!
|
| 81 |
+
|
| 82 |
+
### ποΈ Build Final Master Sheet
|
| 83 |
+
Once all anomalies are resolved, click **Build Master Sheet**.
|
| 84 |
+
- The app will geo-decode the data, apply all mapping rules, and filter out strictly out-of-scope regions (like Navy schools located in non-operational states).
|
| 85 |
+
- A final, polished Excel file will be generated for you to download.
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## 4. Tab 3: πΊοΈ Mapping Manager
|
| 90 |
+
|
| 91 |
+
This tab manages your **Scholarship Application Reference** (Dataset 1). This is the master list of all States and Districts that currently exist in your database.
|
| 92 |
+
|
| 93 |
+
It tracks whether a district is `present` (fully set up in your system) or `new districts found` (detected by the scraper, but not yet configured by your IT team).
|
| 94 |
+
|
| 95 |
+
You have three powerful ways to manage this data:
|
| 96 |
+
|
| 97 |
+
### Method 1: Click-to-Edit Rows (Fastest for quick fixes)
|
| 98 |
+
1. The main reference table is intentionally locked to prevent typos.
|
| 99 |
+
2. Click **any cell** in the table, and the app will instantly copy that row's State and District into the **Edit Status** form directly below the table.
|
| 100 |
+
3. Use the **Status Dropdown** to select either `present` or `new districts found`.
|
| 101 |
+
4. Click the blue **πΎ Update Row** button to permanently save your changes to the cloud.
|
| 102 |
+
|
| 103 |
+
### Method 2: Bulk Excel Import / Export (Best for team collaboration)
|
| 104 |
+
1. Click the **π¦ Bulk Excel Import / Export** accordion to expand it.
|
| 105 |
+
2. Click **π¦ Prepare Current View for Download** and download the Excel file.
|
| 106 |
+
3. You can send this file to your team or make massive bulk edits in Microsoft Excel.
|
| 107 |
+
4. Once updated, drag and drop the Excel file into the **Upload Updated Excel** box. The app will instantly read the file and sync all changes to the cloud.
|
| 108 |
+
|
| 109 |
+
### Method 3: Smart Dropdown Tools (Best for structural changes)
|
| 110 |
+
At the bottom of the tab, you'll find tools to Add, Rename, or Delete districts.
|
| 111 |
+
- **Add a District:** Use this if you are manually creating a district in your Scholarship Application before scraping.
|
| 112 |
+
- **Rename a District:** Use this if your Scholarship Application changes a district name. **Crucially, using this tool will automatically cascade the name change to all of your existing mapping rules!**
|
| 113 |
+
- *Note: Some fields (like Add a District) are standard textboxes, while others (like Rename and Delete) use smart dropdowns so you can easily select existing names.*
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 5. Tab 4: π₯ Download History
|
| 118 |
+
|
| 119 |
+
This tab serves as your cloud backup and archive.
|
| 120 |
+
|
| 121 |
+
- Every time you click **Build Master Sheet** in Tab 2, a permanent snapshot of that final Excel file is saved to the HuggingFace cloud.
|
| 122 |
+
- If you ever lose a file or need to access a master sheet from a previous month, simply go to this tab.
|
| 123 |
+
- Click **π Refresh File List**, select the file by its timestamp, and download it instantly.
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## 6. Best Practices & Tips
|
| 128 |
+
|
| 129 |
+
- **Do Not Interrupt the Scraper:** If you close the browser tab while Tab 1 is actively scraping, the process will stop. Let it run in the background.
|
| 130 |
+
- **Always Resolve Anomalies:** Never ignore red tables in Tab 2. If you force a build without mapping new districts, those schools will carry the anomalous names into your final database, which will cause data ingestion errors.
|
| 131 |
+
- **Use the Rename Tool for Name Changes:** If you need to rename a district that has mapping rules attached to it, always use the **Rename a District** tool in Tab 3 rather than editing the table directly. The tool ensures your mapping rules don't break.
|
| 132 |
+
- **Smart Raw Deletion Cleanup:** If you decide you don't want a scraped state anymore and click "ποΈ Delete" in Tab 2, the app will auto-clean your database. It will delete all `new districts found` for that state, but perfectly preserve any `present` districts to ensure your established database is never corrupted.
|
| 133 |
+
- **Clear Filters Before Exporting:** In Tab 3, the Excel Export downloads exactly what is visible on your screen. If you only want to download "Delhi", apply the filter first before clicking Prepare for Download!
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
*End of Manual*
|
admin_rename_sf_district.py
CHANGED
|
@@ -63,14 +63,24 @@ def rename_sf_district(state: str, old_name: str, new_name: str):
|
|
| 63 |
# Pull existing rules
|
| 64 |
current_dist, current_block = pull_mapping_rules(token, repo)
|
| 65 |
|
| 66 |
-
#
|
| 67 |
if not current_dist.empty:
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
if not current_block.empty:
|
| 70 |
-
|
|
|
|
|
|
|
| 71 |
|
| 72 |
merged_dist = pd.concat([current_dist, auto_dist], ignore_index=True) if not auto_dist.empty else current_dist
|
|
|
|
|
|
|
|
|
|
| 73 |
merged_block = pd.concat([current_block, auto_block], ignore_index=True) if not auto_block.empty else current_block
|
|
|
|
|
|
|
| 74 |
|
| 75 |
push_mapping_rules(merged_dist, merged_block, token, repo)
|
| 76 |
print("Mapping rules successfully updated!")
|
|
|
|
| 63 |
# Pull existing rules
|
| 64 |
current_dist, current_block = pull_mapping_rules(token, repo)
|
| 65 |
|
| 66 |
+
# Safely cascade the rename to existing mapping rules instead of dropping them
|
| 67 |
if not current_dist.empty:
|
| 68 |
+
mask = (current_dist["State"] == state) & (current_dist["Dist_NEW"] == old_name)
|
| 69 |
+
if mask.sum() > 0:
|
| 70 |
+
current_dist.loc[mask, "Dist_NEW"] = new_name
|
| 71 |
+
|
| 72 |
if not current_block.empty:
|
| 73 |
+
mask = (current_block["State"] == state) & (current_block["Dist_NEW"] == old_name)
|
| 74 |
+
if mask.sum() > 0:
|
| 75 |
+
current_block.loc[mask, "Dist_NEW"] = new_name
|
| 76 |
|
| 77 |
merged_dist = pd.concat([current_dist, auto_dist], ignore_index=True) if not auto_dist.empty else current_dist
|
| 78 |
+
if not merged_dist.empty:
|
| 79 |
+
merged_dist = merged_dist.drop_duplicates(subset=["State", "Dist_OLD", "Dist_NEW"])
|
| 80 |
+
|
| 81 |
merged_block = pd.concat([current_block, auto_block], ignore_index=True) if not auto_block.empty else current_block
|
| 82 |
+
if not merged_block.empty:
|
| 83 |
+
merged_block = merged_block.drop_duplicates(subset=["State", "Dist_OLD", "Block_OLD", "Dist_NEW"])
|
| 84 |
|
| 85 |
push_mapping_rules(merged_dist, merged_block, token, repo)
|
| 86 |
print("Mapping rules successfully updated!")
|
app.py
CHANGED
|
@@ -251,9 +251,13 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
|
|
| 251 |
re_done_s = re.compile(r"DONE\.\s+Total:\s*(\d+)\s*\|.*?Success:\s*(\d+).*?No Data:\s*(\d+).*?Captcha Failed:\s*(\d+)")
|
| 252 |
re_done_r = re.compile(r"RETRY DONE\.\s+Attempted:\s*(\d+)\s*\|.*?Resolved:\s*(\d+).*?Still captcha-failed:\s*(\d+)")
|
| 253 |
|
|
|
|
| 254 |
def _auto_export_excel():
|
| 255 |
"""Auto-generate Excel from JSON after scrape completes."""
|
| 256 |
excel = get_excel_file(state)
|
|
|
|
|
|
|
|
|
|
| 257 |
try:
|
| 258 |
proc = subprocess.run(
|
| 259 |
[sys.executable, "export_to_excel.py", "--state", state],
|
|
@@ -261,6 +265,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
|
|
| 261 |
cwd=os.path.dirname(os.path.abspath(__file__))
|
| 262 |
)
|
| 263 |
if proc.returncode == 0 and os.path.exists(excel):
|
|
|
|
| 264 |
return gr.update(value=excel, visible=True)
|
| 265 |
except Exception:
|
| 266 |
pass
|
|
@@ -317,7 +322,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
|
|
| 317 |
if success_count == 0 and failed_count == 0:
|
| 318 |
phase_text = "β No schools found in selected categories."; banner_rendered = _banner_html("β", "No Schools Found", "The selected categories have no schools.", "nodata")
|
| 319 |
elif not has_failures:
|
| 320 |
-
phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All school data fetched successfully.", "success")
|
| 321 |
else:
|
| 322 |
phase_text = f"β οΈ {failed_count} record(s) incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Are Incomplete", f"{failed_count} record(s) could not be fetched. Please proceed to Step 2 to fetch missing data.", "warning")
|
| 323 |
mr = re_done_r.search(line)
|
|
@@ -326,7 +331,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
|
|
| 326 |
success_count = resolved; failed_count = still_failed; no_data_count = 0; current = total = attempted; is_done = True; has_failures = still_failed > 0
|
| 327 |
stats_rendered = _stats_html(resolved, 0, still_failed, attempted)
|
| 328 |
if not has_failures:
|
| 329 |
-
phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All missing data fetched.
|
| 330 |
else:
|
| 331 |
phase_text = f"β οΈ {still_failed} record(s) still incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Still Incomplete", f"{still_failed} record(s) failed. You can run Step 2 again or proceed to Step 3.", "warning")
|
| 332 |
yield _emit()
|
|
@@ -338,7 +343,7 @@ def _stream(pytest_args: list, state: str, max_retries: int, mode: str, target_c
|
|
| 338 |
fc = check_missed_schools(out_file)
|
| 339 |
if fc is not None:
|
| 340 |
if fc == 0 and success_count > 0:
|
| 341 |
-
is_done, has_failures = True, False; phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All school data fetched successfully.", "success")
|
| 342 |
elif fc > 0:
|
| 343 |
is_done, has_failures = True, True; failed_count = fc; phase_text = f"β οΈ {fc} record(s) incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Are Incomplete", f"{fc} record(s) failed. Please proceed to Step 2 to fetch missing data.", "warning")
|
| 344 |
yield _emit()
|
|
@@ -1005,7 +1010,7 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
|
|
| 1005 |
flagged_table = gr.Dataframe(
|
| 1006 |
label="New districts detected (Type the Old SF name in the last column to rename it)",
|
| 1007 |
headers=["State", "District", "Reason", "Rename District (Optional)"],
|
| 1008 |
-
interactive=
|
| 1009 |
)
|
| 1010 |
flagged_status_html = gr.HTML(value="")
|
| 1011 |
|
|
@@ -1219,65 +1224,108 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
|
|
| 1219 |
border:1px solid rgba(99,102,241,0.2); border-radius:14px; padding:18px 22px; margin-bottom:18px;">
|
| 1220 |
<div style="font-weight:700; font-size:1.05em; margin-bottom:6px;">πΊοΈ Mapping Manager</div>
|
| 1221 |
<div style="font-size:.9em; color:var(--body-text-color-subdued); line-height:1.7;">
|
| 1222 |
-
<b>
|
| 1223 |
</div>
|
| 1224 |
</div>
|
| 1225 |
""")
|
| 1226 |
|
| 1227 |
-
|
| 1228 |
-
# ββ SF District Reference CRUD βββββββββββββββββββββββββ
|
| 1229 |
-
gr.HTML("<div class='section-label'>π SF District Reference</div>")
|
| 1230 |
with gr.Row():
|
| 1231 |
refresh_ref_btn = gr.Button("π Load from Dataset", variant="secondary", scale=1)
|
| 1232 |
|
| 1233 |
# Filters
|
| 1234 |
with gr.Row():
|
| 1235 |
ref_filter_state = gr.Dropdown(label="Filter by State", choices=["All"], value="All", scale=1, interactive=True)
|
| 1236 |
-
ref_filter_status = gr.Dropdown(label="Filter by Status", choices=["All", "
|
| 1237 |
ref_filter_dist = gr.Textbox(label="Search District", placeholder="Type to search...", scale=2, interactive=True)
|
| 1238 |
|
| 1239 |
# The DataFrame
|
| 1240 |
ref_full_state = gr.State(pd.DataFrame())
|
| 1241 |
ref_table = gr.Dataframe(
|
| 1242 |
-
label="
|
| 1243 |
interactive=False, wrap=True
|
| 1244 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
ref_status_html = gr.HTML(value="")
|
| 1246 |
|
| 1247 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Add a District</div>")
|
| 1248 |
with gr.Row():
|
| 1249 |
ref_state_add = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
|
| 1250 |
ref_dist_add = gr.Textbox(label="District", placeholder="e.g. ALLURI SITHARAMA RAJU", scale=2)
|
| 1251 |
-
ref_status_add = gr.Dropdown(choices=["
|
| 1252 |
ref_add_btn = gr.Button("β Add District", variant="primary", scale=1)
|
| 1253 |
ref_add_status = gr.HTML(value="")
|
| 1254 |
|
| 1255 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Rename a District (automatically updates mappings)</div>")
|
| 1256 |
with gr.Row():
|
| 1257 |
-
ref_ren_state = gr.
|
| 1258 |
-
ref_ren_old = gr.
|
| 1259 |
ref_ren_new = gr.Textbox(label="New Name", placeholder="e.g. VISAKHAPATNAM (NEW)", scale=2)
|
| 1260 |
ref_rename_btn = gr.Button("βοΈ Rename", variant="secondary", scale=1)
|
| 1261 |
ref_rename_status = gr.HTML(value="")
|
| 1262 |
|
| 1263 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Delete Actions</div>")
|
| 1264 |
with gr.Row():
|
| 1265 |
-
ref_del_state = gr.
|
| 1266 |
-
ref_del_dist = gr.
|
| 1267 |
ref_del_dist_btn = gr.Button("ποΈ Delete District", variant="stop", scale=1)
|
| 1268 |
ref_del_state_btn = gr.Button("π¨ Delete Entire State", variant="stop", scale=1)
|
| 1269 |
ref_del_status = gr.HTML(value="")
|
| 1270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1271 |
def ui_refresh_ref():
|
| 1272 |
from hf_store import get_hf_credentials, pull_district_reference
|
| 1273 |
token, repo = get_hf_credentials()
|
| 1274 |
if not token or not repo:
|
| 1275 |
-
|
|
|
|
|
|
|
| 1276 |
df = pull_district_reference(token, repo)
|
| 1277 |
if df.empty:
|
| 1278 |
-
|
|
|
|
|
|
|
| 1279 |
states = sorted(list(df["State"].unique()))
|
| 1280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
|
| 1282 |
def filter_ref_table(df, filter_state, filter_status, search_dist):
|
| 1283 |
if df is None or df.empty: return df
|
|
@@ -1285,11 +1333,87 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
|
|
| 1285 |
if filter_state and filter_state != "All":
|
| 1286 |
res = res[res["State"] == filter_state]
|
| 1287 |
if filter_status and filter_status != "All":
|
| 1288 |
-
res = res[res["
|
| 1289 |
if search_dist:
|
| 1290 |
res = res[res["District"].str.contains(search_dist.upper(), na=False)]
|
| 1291 |
return res
|
| 1292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
def ui_ref_add(state, district, sf_status):
|
| 1294 |
from hf_store import get_hf_credentials, update_district_reference_add
|
| 1295 |
token, repo = get_hf_credentials()
|
|
@@ -1334,11 +1458,23 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
|
|
| 1334 |
except Exception as ex:
|
| 1335 |
return f"<p style='color:#ef4444;'>β Error: {ex}</p>"
|
| 1336 |
|
| 1337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
ref_filter_state.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1339 |
ref_filter_status.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1340 |
ref_filter_dist.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1342 |
ref_add_btn.click(fn=ui_ref_add, inputs=[ref_state_add, ref_dist_add, ref_status_add], outputs=[ref_add_status])
|
| 1343 |
ref_rename_btn.click(fn=ui_ref_rename, inputs=[ref_ren_state, ref_ren_old, ref_ren_new], outputs=[ref_rename_status])
|
| 1344 |
ref_del_dist_btn.click(fn=ui_ref_del_dist, inputs=[ref_del_state, ref_del_dist], outputs=[ref_del_status])
|
|
|
|
| 251 |
re_done_s = re.compile(r"DONE\.\s+Total:\s*(\d+)\s*\|.*?Success:\s*(\d+).*?No Data:\s*(\d+).*?Captcha Failed:\s*(\d+)")
|
| 252 |
re_done_r = re.compile(r"RETRY DONE\.\s+Attempted:\s*(\d+)\s*\|.*?Resolved:\s*(\d+).*?Still captcha-failed:\s*(\d+)")
|
| 253 |
|
| 254 |
+
excel_cache = [None]
|
| 255 |
def _auto_export_excel():
|
| 256 |
"""Auto-generate Excel from JSON after scrape completes."""
|
| 257 |
excel = get_excel_file(state)
|
| 258 |
+
if excel_cache[0] == excel and os.path.exists(excel):
|
| 259 |
+
return gr.update(value=excel, visible=True)
|
| 260 |
+
|
| 261 |
try:
|
| 262 |
proc = subprocess.run(
|
| 263 |
[sys.executable, "export_to_excel.py", "--state", state],
|
|
|
|
| 265 |
cwd=os.path.dirname(os.path.abspath(__file__))
|
| 266 |
)
|
| 267 |
if proc.returncode == 0 and os.path.exists(excel):
|
| 268 |
+
excel_cache[0] = excel
|
| 269 |
return gr.update(value=excel, visible=True)
|
| 270 |
except Exception:
|
| 271 |
pass
|
|
|
|
| 322 |
if success_count == 0 and failed_count == 0:
|
| 323 |
phase_text = "β No schools found in selected categories."; banner_rendered = _banner_html("β", "No Schools Found", "The selected categories have no schools.", "nodata")
|
| 324 |
elif not has_failures:
|
| 325 |
+
phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All school data fetched successfully.<br><br><span style='color:#3b82f6;font-weight:600;'>β³ Pushing data to cloud... please wait.</span>", "success")
|
| 326 |
else:
|
| 327 |
phase_text = f"β οΈ {failed_count} record(s) incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Are Incomplete", f"{failed_count} record(s) could not be fetched. Please proceed to Step 2 to fetch missing data.", "warning")
|
| 328 |
mr = re_done_r.search(line)
|
|
|
|
| 331 |
success_count = resolved; failed_count = still_failed; no_data_count = 0; current = total = attempted; is_done = True; has_failures = still_failed > 0
|
| 332 |
stats_rendered = _stats_html(resolved, 0, still_failed, attempted)
|
| 333 |
if not has_failures:
|
| 334 |
+
phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All missing data fetched.<br><br><span style='color:#3b82f6;font-weight:600;'>β³ Pushing data to cloud... please wait.</span>", "success")
|
| 335 |
else:
|
| 336 |
phase_text = f"β οΈ {still_failed} record(s) still incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Still Incomplete", f"{still_failed} record(s) failed. You can run Step 2 again or proceed to Step 3.", "warning")
|
| 337 |
yield _emit()
|
|
|
|
| 343 |
fc = check_missed_schools(out_file)
|
| 344 |
if fc is not None:
|
| 345 |
if fc == 0 and success_count > 0:
|
| 346 |
+
is_done, has_failures = True, False; phase_text = "β
All data retrieved!"; banner_rendered = _banner_html("β
", "All Data Retrieved!", "All school data fetched successfully.<br><br><span style='color:#3b82f6;font-weight:600;'>β³ Pushing data to cloud... please wait.</span>", "success")
|
| 347 |
elif fc > 0:
|
| 348 |
is_done, has_failures = True, True; failed_count = fc; phase_text = f"β οΈ {fc} record(s) incomplete."; banner_rendered = _banner_html("β οΈ", "Some Records Are Incomplete", f"{fc} record(s) failed. Please proceed to Step 2 to fetch missing data.", "warning")
|
| 349 |
yield _emit()
|
|
|
|
| 1010 |
flagged_table = gr.Dataframe(
|
| 1011 |
label="New districts detected (Type the Old SF name in the last column to rename it)",
|
| 1012 |
headers=["State", "District", "Reason", "Rename District (Optional)"],
|
| 1013 |
+
interactive=False, wrap=True
|
| 1014 |
)
|
| 1015 |
flagged_status_html = gr.HTML(value="")
|
| 1016 |
|
|
|
|
| 1224 |
border:1px solid rgba(99,102,241,0.2); border-radius:14px; padding:18px 22px; margin-bottom:18px;">
|
| 1225 |
<div style="font-weight:700; font-size:1.05em; margin-bottom:6px;">πΊοΈ Mapping Manager</div>
|
| 1226 |
<div style="font-size:.9em; color:var(--body-text-color-subdued); line-height:1.7;">
|
| 1227 |
+
<b>Scholarship Application Reference</b>: The mirror of what Scholarship Application currently knows. When you add a new district to Scholarship Application, find it here and change its status from <i>new districts found</i> to <i>present</i>.
|
| 1228 |
</div>
|
| 1229 |
</div>
|
| 1230 |
""")
|
| 1231 |
|
| 1232 |
+
gr.HTML("<div class='section-label'>π Scholarship Application District Reference</div>")
|
|
|
|
|
|
|
| 1233 |
with gr.Row():
|
| 1234 |
refresh_ref_btn = gr.Button("π Load from Dataset", variant="secondary", scale=1)
|
| 1235 |
|
| 1236 |
# Filters
|
| 1237 |
with gr.Row():
|
| 1238 |
ref_filter_state = gr.Dropdown(label="Filter by State", choices=["All"], value="All", scale=1, interactive=True)
|
| 1239 |
+
ref_filter_status = gr.Dropdown(label="Filter by Status", choices=["All", "present", "new districts found"], value="All", scale=1, interactive=True)
|
| 1240 |
ref_filter_dist = gr.Textbox(label="Search District", placeholder="Type to search...", scale=2, interactive=True)
|
| 1241 |
|
| 1242 |
# The DataFrame
|
| 1243 |
ref_full_state = gr.State(pd.DataFrame())
|
| 1244 |
ref_table = gr.Dataframe(
|
| 1245 |
+
label="Scholarship Application District Reference (Editable)",
|
| 1246 |
interactive=False, wrap=True
|
| 1247 |
)
|
| 1248 |
+
|
| 1249 |
+
with gr.Accordion("π¦ Bulk Excel Import / Export", open=False):
|
| 1250 |
+
with gr.Row():
|
| 1251 |
+
ref_prep_dl_btn = gr.Button("π¦ Prepare Current View for Download")
|
| 1252 |
+
ref_dl_btn = gr.DownloadButton("π₯ Download Excel", visible=False)
|
| 1253 |
+
|
| 1254 |
+
with gr.Row():
|
| 1255 |
+
ref_upload = gr.File(label="Upload Updated Excel", file_types=[".xlsx"])
|
| 1256 |
+
|
| 1257 |
+
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0; padding-top:10px; border-top:1px solid #ddd;'>Click any row in the table above to edit its status here:</div>")
|
| 1258 |
+
with gr.Row():
|
| 1259 |
+
ref_edit_state = gr.Textbox(label="State", interactive=False, scale=2)
|
| 1260 |
+
ref_edit_dist = gr.Textbox(label="District", interactive=False, scale=2)
|
| 1261 |
+
ref_edit_status = gr.Dropdown(choices=["present", "new districts found"], label="Status", scale=1)
|
| 1262 |
+
ref_edit_btn = gr.Button("πΎ Update Row", variant="primary", scale=1)
|
| 1263 |
+
|
| 1264 |
ref_status_html = gr.HTML(value="")
|
| 1265 |
|
| 1266 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Add a District</div>")
|
| 1267 |
with gr.Row():
|
| 1268 |
ref_state_add = gr.Textbox(label="State", placeholder="e.g. ANDHRA PRADESH", scale=2)
|
| 1269 |
ref_dist_add = gr.Textbox(label="District", placeholder="e.g. ALLURI SITHARAMA RAJU", scale=2)
|
| 1270 |
+
ref_status_add = gr.Dropdown(choices=["present", "new districts found"], value="present", label="Status", scale=1)
|
| 1271 |
ref_add_btn = gr.Button("β Add District", variant="primary", scale=1)
|
| 1272 |
ref_add_status = gr.HTML(value="")
|
| 1273 |
|
| 1274 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Rename a District (automatically updates mappings)</div>")
|
| 1275 |
with gr.Row():
|
| 1276 |
+
ref_ren_state = gr.Dropdown(label="State", choices=[], allow_custom_value=True, scale=2)
|
| 1277 |
+
ref_ren_old = gr.Dropdown(label="Old Name", choices=[], allow_custom_value=True, scale=2)
|
| 1278 |
ref_ren_new = gr.Textbox(label="New Name", placeholder="e.g. VISAKHAPATNAM (NEW)", scale=2)
|
| 1279 |
ref_rename_btn = gr.Button("βοΈ Rename", variant="secondary", scale=1)
|
| 1280 |
ref_rename_status = gr.HTML(value="")
|
| 1281 |
|
| 1282 |
gr.HTML("<div style='font-size:.8em;font-weight:600;margin:10px 0 4px 0;'>Delete Actions</div>")
|
| 1283 |
with gr.Row():
|
| 1284 |
+
ref_del_state = gr.Dropdown(label="State", choices=[], allow_custom_value=True, scale=2)
|
| 1285 |
+
ref_del_dist = gr.Dropdown(label="District", choices=[], allow_custom_value=True, scale=2)
|
| 1286 |
ref_del_dist_btn = gr.Button("ποΈ Delete District", variant="stop", scale=1)
|
| 1287 |
ref_del_state_btn = gr.Button("π¨ Delete Entire State", variant="stop", scale=1)
|
| 1288 |
ref_del_status = gr.HTML(value="")
|
| 1289 |
|
| 1290 |
+
def on_ref_table_select(df, evt: gr.SelectData):
|
| 1291 |
+
row = evt.index[0]
|
| 1292 |
+
if df is None or df.empty or row >= len(df):
|
| 1293 |
+
return gr.update(), gr.update(), gr.update(), ""
|
| 1294 |
+
|
| 1295 |
+
state = df.iloc[row]["State"]
|
| 1296 |
+
dist = df.iloc[row]["District"]
|
| 1297 |
+
status = df.iloc[row]["Status"]
|
| 1298 |
+
return gr.update(value=state), gr.update(value=dist), gr.update(value=status), ""
|
| 1299 |
+
|
| 1300 |
+
def ui_ref_edit_row(state, district, new_status):
|
| 1301 |
+
from hf_store import get_hf_credentials, update_district_reference_add
|
| 1302 |
+
token, repo = get_hf_credentials()
|
| 1303 |
+
if not token or not repo or not state or not district:
|
| 1304 |
+
return "<p style='color:#ef4444;'>β οΈ Please select a row from the table first.</p>"
|
| 1305 |
+
try:
|
| 1306 |
+
update_district_reference_add(state, district, new_status, token, repo)
|
| 1307 |
+
return f"<p style='color:#22c55e;font-weight:600;'>β
Successfully updated {district} to '{new_status}'. Refresh the table to see changes.</p>"
|
| 1308 |
+
except Exception as ex:
|
| 1309 |
+
return f"<p style='color:#ef4444;'>β Error: {ex}</p>"
|
| 1310 |
+
|
| 1311 |
def ui_refresh_ref():
|
| 1312 |
from hf_store import get_hf_credentials, pull_district_reference
|
| 1313 |
token, repo = get_hf_credentials()
|
| 1314 |
if not token or not repo:
|
| 1315 |
+
empty = pd.DataFrame()
|
| 1316 |
+
u = gr.update()
|
| 1317 |
+
return empty, empty, u, u, u, u, u, "<p style='color:#ef4444;'>β οΈ HF credentials not set.</p>"
|
| 1318 |
df = pull_district_reference(token, repo)
|
| 1319 |
if df.empty:
|
| 1320 |
+
empty = pd.DataFrame()
|
| 1321 |
+
u = gr.update()
|
| 1322 |
+
return empty, empty, u, u, u, u, u, "<p style='color:#f59e0b;'>β οΈ Scholarship Application Reference empty.</p>"
|
| 1323 |
states = sorted(list(df["State"].unique()))
|
| 1324 |
+
dists = sorted(list(df["District"].unique()))
|
| 1325 |
+
st_up = gr.update(choices=["All"] + states, value="All")
|
| 1326 |
+
st_dd = gr.update(choices=states)
|
| 1327 |
+
dist_dd = gr.update(choices=dists)
|
| 1328 |
+
return df, df, st_up, st_dd, dist_dd, st_dd, dist_dd, f"<p style='color:#22c55e;font-weight:600;'>β
Loaded {len(df)} rows.</p>"
|
| 1329 |
|
| 1330 |
def filter_ref_table(df, filter_state, filter_status, search_dist):
|
| 1331 |
if df is None or df.empty: return df
|
|
|
|
| 1333 |
if filter_state and filter_state != "All":
|
| 1334 |
res = res[res["State"] == filter_state]
|
| 1335 |
if filter_status and filter_status != "All":
|
| 1336 |
+
res = res[res["Status"] == filter_status]
|
| 1337 |
if search_dist:
|
| 1338 |
res = res[res["District"].str.contains(search_dist.upper(), na=False)]
|
| 1339 |
return res
|
| 1340 |
|
| 1341 |
+
def ui_save_table_edits(df, full_df, filter_state, filter_status, search_dist):
|
| 1342 |
+
from hf_store import get_hf_credentials, push_district_reference
|
| 1343 |
+
token, repo = get_hf_credentials()
|
| 1344 |
+
if df is None or df.empty:
|
| 1345 |
+
return "<p style='color:#ef4444;'>β οΈ Table is empty.</p>", full_df
|
| 1346 |
+
# The user edited 'df'. We need to merge it back into 'full_df'
|
| 1347 |
+
# This is a bit complex if they deleted rows or changed keys.
|
| 1348 |
+
# Actually, if they are filtering, df only contains a subset.
|
| 1349 |
+
# Let's just overwrite the subset in full_df based on index if we kept index,
|
| 1350 |
+
# but Gradio df doesn't keep original indices easily.
|
| 1351 |
+
# Simplest way: They should only save edits if viewing All/All without search.
|
| 1352 |
+
if filter_state != "All" or filter_status != "All" or search_dist:
|
| 1353 |
+
return "<p style='color:#ef4444;'>β οΈ Please clear all filters (set to 'All', clear search) before saving direct table edits.</p>", full_df
|
| 1354 |
+
|
| 1355 |
+
try:
|
| 1356 |
+
df["State"] = df["State"].astype(str).str.strip().str.upper()
|
| 1357 |
+
df["District"] = df["District"].astype(str).str.strip().str.upper()
|
| 1358 |
+
df["Status"] = df["Status"].astype(str).str.strip()
|
| 1359 |
+
|
| 1360 |
+
invalid_statuses = df[~df["Status"].isin(["present", "new districts found"])]
|
| 1361 |
+
if not invalid_statuses.empty:
|
| 1362 |
+
return "<p style='color:#ef4444;'>β οΈ Invalid Status found. You can only use 'present' or 'new districts found'.</p>", full_df
|
| 1363 |
+
|
| 1364 |
+
df = df.drop_duplicates()
|
| 1365 |
+
push_district_reference(df, token, repo)
|
| 1366 |
+
return f"<p style='color:#22c55e;font-weight:600;'>β
Saved {len(df)} rows directly to cloud.</p>", df
|
| 1367 |
+
except Exception as ex:
|
| 1368 |
+
return f"<p style='color:#ef4444;'>β Error: {ex}</p>", full_df
|
| 1369 |
+
|
| 1370 |
+
def ui_prep_excel_download(df):
|
| 1371 |
+
if df is None or df.empty:
|
| 1372 |
+
return gr.update(visible=False), "<p style='color:#ef4444;'>β οΈ No data to download.</p>"
|
| 1373 |
+
import tempfile
|
| 1374 |
+
import os
|
| 1375 |
+
fd, temp_excel = tempfile.mkstemp(suffix=".xlsx")
|
| 1376 |
+
os.close(fd)
|
| 1377 |
+
df.to_excel(temp_excel, index=False)
|
| 1378 |
+
return gr.update(value=temp_excel, visible=True), "<p style='color:#22c55e;'>β
Ready to download!</p>"
|
| 1379 |
+
|
| 1380 |
+
def ui_upload_excel(file):
|
| 1381 |
+
if not file: return pd.DataFrame(), "<p style='color:#ef4444;'>β οΈ No file uploaded.</p>"
|
| 1382 |
+
import pandas as pd
|
| 1383 |
+
from hf_store import get_hf_credentials, push_district_reference, pull_district_reference
|
| 1384 |
+
token, repo = get_hf_credentials()
|
| 1385 |
+
try:
|
| 1386 |
+
uploaded_df = pd.read_excel(file.name)
|
| 1387 |
+
if "State" not in uploaded_df.columns or "District" not in uploaded_df.columns or "Status" not in uploaded_df.columns:
|
| 1388 |
+
return pd.DataFrame(), "<p style='color:#ef4444;'>β οΈ Excel must have State, District, and Status columns.</p>"
|
| 1389 |
+
|
| 1390 |
+
uploaded_df["State"] = uploaded_df["State"].astype(str).str.strip().str.upper()
|
| 1391 |
+
uploaded_df["District"] = uploaded_df["District"].astype(str).str.strip().str.upper()
|
| 1392 |
+
uploaded_df["Status"] = uploaded_df["Status"].astype(str).str.strip()
|
| 1393 |
+
|
| 1394 |
+
invalid_statuses = uploaded_df[~uploaded_df["Status"].isin(["present", "new districts found"])]
|
| 1395 |
+
if not invalid_statuses.empty:
|
| 1396 |
+
return pd.DataFrame(), "<p style='color:#ef4444;'>β οΈ Invalid Status found in Excel. You can only use 'present' or 'new districts found'.</p>"
|
| 1397 |
+
|
| 1398 |
+
uploaded_df = uploaded_df.drop_duplicates(subset=["State", "District"])
|
| 1399 |
+
|
| 1400 |
+
full_df = pull_district_reference(token, repo)
|
| 1401 |
+
if not full_df.empty:
|
| 1402 |
+
full_df.set_index(["State", "District"], inplace=True)
|
| 1403 |
+
uploaded_df.set_index(["State", "District"], inplace=True)
|
| 1404 |
+
full_df.update(uploaded_df)
|
| 1405 |
+
new_rows = uploaded_df[~uploaded_df.index.isin(full_df.index)]
|
| 1406 |
+
full_df = pd.concat([full_df, new_rows])
|
| 1407 |
+
full_df.reset_index(inplace=True)
|
| 1408 |
+
df_to_push = full_df
|
| 1409 |
+
else:
|
| 1410 |
+
df_to_push = uploaded_df.copy()
|
| 1411 |
+
|
| 1412 |
+
push_district_reference(df_to_push, token, repo)
|
| 1413 |
+
return df_to_push, f"<p style='color:#22c55e;font-weight:600;'>β
Successfully merged {len(uploaded_df)} uploaded rows into the dataset!</p>"
|
| 1414 |
+
except Exception as ex:
|
| 1415 |
+
return pd.DataFrame(), f"<p style='color:#ef4444;'>β Error: {ex}</p>"
|
| 1416 |
+
|
| 1417 |
def ui_ref_add(state, district, sf_status):
|
| 1418 |
from hf_store import get_hf_credentials, update_district_reference_add
|
| 1419 |
token, repo = get_hf_credentials()
|
|
|
|
| 1458 |
except Exception as ex:
|
| 1459 |
return f"<p style='color:#ef4444;'>β Error: {ex}</p>"
|
| 1460 |
|
| 1461 |
+
# Wirings
|
| 1462 |
+
refresh_ref_btn.click(
|
| 1463 |
+
fn=ui_refresh_ref,
|
| 1464 |
+
outputs=[ref_table, ref_full_state, ref_filter_state,
|
| 1465 |
+
ref_ren_state, ref_ren_old,
|
| 1466 |
+
ref_del_state, ref_del_dist,
|
| 1467 |
+
ref_status_html]
|
| 1468 |
+
)
|
| 1469 |
ref_filter_state.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1470 |
ref_filter_status.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1471 |
ref_filter_dist.change(fn=filter_ref_table, inputs=[ref_full_state, ref_filter_state, ref_filter_status, ref_filter_dist], outputs=[ref_table])
|
| 1472 |
|
| 1473 |
+
ref_table.select(fn=on_ref_table_select, inputs=[ref_table], outputs=[ref_edit_state, ref_edit_dist, ref_edit_status, ref_status_html])
|
| 1474 |
+
ref_edit_btn.click(fn=ui_ref_edit_row, inputs=[ref_edit_state, ref_edit_dist, ref_edit_status], outputs=[ref_status_html])
|
| 1475 |
+
ref_prep_dl_btn.click(fn=ui_prep_excel_download, inputs=[ref_table], outputs=[ref_dl_btn, ref_status_html])
|
| 1476 |
+
ref_upload.upload(fn=ui_upload_excel, inputs=[ref_upload], outputs=[ref_table, ref_status_html])
|
| 1477 |
+
|
| 1478 |
ref_add_btn.click(fn=ui_ref_add, inputs=[ref_state_add, ref_dist_add, ref_status_add], outputs=[ref_add_status])
|
| 1479 |
ref_rename_btn.click(fn=ui_ref_rename, inputs=[ref_ren_state, ref_ren_old, ref_ren_new], outputs=[ref_rename_status])
|
| 1480 |
ref_del_dist_btn.click(fn=ui_ref_del_dist, inputs=[ref_del_state, ref_del_dist], outputs=[ref_del_status])
|
hf_store.py
CHANGED
|
@@ -122,7 +122,7 @@ DISTRICT_REF_PATH = "district_reference/district_reference.parquet"
|
|
| 122 |
def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
|
| 123 |
"""
|
| 124 |
Push the SF district reference table to HF dataset.
|
| 125 |
-
Expected columns: State, District,
|
| 126 |
Returns commit URL.
|
| 127 |
"""
|
| 128 |
return _upload_parquet(df, DISTRICT_REF_PATH, token, repo, "Update district_reference")
|
|
@@ -131,20 +131,20 @@ def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
|
|
| 131 |
def pull_district_reference(token: str, repo: str) -> pd.DataFrame:
|
| 132 |
"""
|
| 133 |
Pull the SF district reference from HF dataset.
|
| 134 |
-
Returns DataFrame with columns: State, District,
|
| 135 |
Returns empty DataFrame if not yet seeded.
|
| 136 |
"""
|
| 137 |
df = _download_parquet(DISTRICT_REF_PATH, token, repo)
|
| 138 |
if df.empty:
|
| 139 |
-
return pd.DataFrame(columns=["State", "District", "
|
| 140 |
return df
|
| 141 |
|
| 142 |
|
| 143 |
def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tuple:
|
| 144 |
"""
|
| 145 |
Seed Dataset 1 from a CSV (like master_all_states.csv or SF mapping CSV).
|
| 146 |
-
All states/districts in the CSV β
|
| 147 |
-
Any states in current scraped data NOT in CSV β
|
| 148 |
Returns (df, commit_url)
|
| 149 |
"""
|
| 150 |
csv_df = pd.read_csv(csv_path)
|
|
@@ -163,9 +163,9 @@ def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tu
|
|
| 163 |
csv_df = csv_df.dropna(subset=[state_col])
|
| 164 |
csv_df["State"] = csv_df[state_col].astype(str).str.strip().str.upper()
|
| 165 |
csv_df["District"] = csv_df[district_col].astype(str).str.strip().str.upper()
|
| 166 |
-
csv_df["
|
| 167 |
|
| 168 |
-
ref_df = csv_df[["State", "District", "
|
| 169 |
|
| 170 |
# Pull existing scraped metadata to check for any states not in the CSV
|
| 171 |
meta = _pull_metadata(token, repo)
|
|
@@ -176,7 +176,7 @@ def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tu
|
|
| 176 |
if state_upper not in csv_states:
|
| 177 |
# State exists in scraped data but not in SF CSV β pending
|
| 178 |
# We don't know districts yet; they'll be added when detected
|
| 179 |
-
new_rows.append({"State": state_upper, "District": "ALL", "
|
| 180 |
|
| 181 |
if new_rows:
|
| 182 |
ref_df = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
|
|
@@ -192,7 +192,7 @@ def update_district_reference_add(state: str, district: str, sf_status: str, tok
|
|
| 192 |
district = district.strip().upper()
|
| 193 |
# Remove existing row if present
|
| 194 |
df = df[~((df["State"] == state) & (df["District"] == district))]
|
| 195 |
-
new_row = pd.DataFrame([{"State": state, "District": district, "
|
| 196 |
df = pd.concat([df, new_row], ignore_index=True)
|
| 197 |
return push_district_reference(df, token, repo)
|
| 198 |
|
|
@@ -240,7 +240,7 @@ def delete_state_reference(state: str, token: str, repo: str) -> str:
|
|
| 240 |
|
| 241 |
def bulk_accept_flagged_districts(token: str, repo: str) -> None:
|
| 242 |
"""
|
| 243 |
-
Take all currently flagged districts, add them to Dataset 1 with '
|
| 244 |
and clear the flagged_districts list from metadata.
|
| 245 |
"""
|
| 246 |
meta = _pull_metadata(token, repo)
|
|
@@ -255,7 +255,7 @@ def bulk_accept_flagged_districts(token: str, repo: str) -> None:
|
|
| 255 |
state = f["state"].strip().upper()
|
| 256 |
dist = f["district"].strip().upper()
|
| 257 |
if not ((df["State"] == state) & (df["District"] == dist)).any():
|
| 258 |
-
new_rows.append({"State": state, "District": dist, "
|
| 259 |
|
| 260 |
if new_rows:
|
| 261 |
df = pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True)
|
|
@@ -361,9 +361,9 @@ def delete_mapping_rule(state: str, dist_old: str, dist_new: str, token: str, re
|
|
| 361 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 362 |
|
| 363 |
def get_flagged_districts(token: str, repo: str) -> list:
|
| 364 |
-
"""Return all districts from Dataset 1 that have
|
| 365 |
ref_df = pull_district_reference(token, repo)
|
| 366 |
-
if ref_df.empty or "
|
| 367 |
return []
|
| 368 |
|
| 369 |
try:
|
|
@@ -379,7 +379,7 @@ def get_flagged_districts(token: str, repo: str) -> list:
|
|
| 379 |
except:
|
| 380 |
active_states = set(ref_df["State"].str.upper()) # fallback
|
| 381 |
|
| 382 |
-
pending = ref_df[(ref_df["
|
| 383 |
|
| 384 |
dist_df, _ = pull_mapping_rules(token, repo)
|
| 385 |
mapped = set()
|
|
@@ -397,7 +397,7 @@ def get_flagged_districts(token: str, repo: str) -> list:
|
|
| 397 |
flags.append({
|
| 398 |
"state": state,
|
| 399 |
"district": district,
|
| 400 |
-
"reason": "
|
| 401 |
})
|
| 402 |
return flags
|
| 403 |
|
|
@@ -409,7 +409,7 @@ def rename_district_in_reference(state: str, district: str, new_name: str, token
|
|
| 409 |
exists = not ref_df[(ref_df["State"] == state) & (ref_df["District"] == new_name)].empty
|
| 410 |
|
| 411 |
# Target row to update/delete
|
| 412 |
-
mask = (ref_df["State"] == state) & (ref_df["District"] == district) & (ref_df["
|
| 413 |
|
| 414 |
if not ref_df[mask].empty:
|
| 415 |
if exists:
|
|
@@ -483,7 +483,7 @@ def _detect_and_flag_new_districts(state_name: str, df: pd.DataFrame, token: str
|
|
| 483 |
"""
|
| 484 |
Compare districts in newly scraped df against Dataset 1 and Dataset 2.
|
| 485 |
Any district not in Dataset 1 AND not mapped in Dataset 2 is truly new.
|
| 486 |
-
Add them straight to Dataset 1 with
|
| 487 |
"""
|
| 488 |
state_upper = state_name.strip().upper()
|
| 489 |
if state_upper in NON_ACTUAL_STATES:
|
|
@@ -555,13 +555,13 @@ def _detect_and_flag_new_districts(state_name: str, df: pd.DataFrame, token: str
|
|
| 555 |
return
|
| 556 |
|
| 557 |
# Add directly to Dataset 1 as pending
|
| 558 |
-
new_rows = [{"State": state_upper, "District": d, "
|
| 559 |
updated_ref = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
|
| 560 |
push_district_reference(updated_ref, token, repo)
|
| 561 |
|
| 562 |
|
| 563 |
def delete_state_file(state_name: str, token: str, repo: str) -> None:
|
| 564 |
-
"""Delete a state's raw dataset file and remove it from metadata.json."""
|
| 565 |
api = _hf()
|
| 566 |
skey = _state_key(state_name)
|
| 567 |
try:
|
|
@@ -579,6 +579,20 @@ def delete_state_file(state_name: str, token: str, repo: str) -> None:
|
|
| 579 |
if state_name in meta:
|
| 580 |
del meta[state_name]
|
| 581 |
_push_metadata(meta, token, repo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
|
| 583 |
|
| 584 |
def pull_state_file(state_name: str, token: str, repo: str) -> pd.DataFrame:
|
|
|
|
| 122 |
def push_district_reference(df: pd.DataFrame, token: str, repo: str) -> str:
|
| 123 |
"""
|
| 124 |
Push the SF district reference table to HF dataset.
|
| 125 |
+
Expected columns: State, District, Status ('present' | 'new districts found')
|
| 126 |
Returns commit URL.
|
| 127 |
"""
|
| 128 |
return _upload_parquet(df, DISTRICT_REF_PATH, token, repo, "Update district_reference")
|
|
|
|
| 131 |
def pull_district_reference(token: str, repo: str) -> pd.DataFrame:
|
| 132 |
"""
|
| 133 |
Pull the SF district reference from HF dataset.
|
| 134 |
+
Returns DataFrame with columns: State, District, Status
|
| 135 |
Returns empty DataFrame if not yet seeded.
|
| 136 |
"""
|
| 137 |
df = _download_parquet(DISTRICT_REF_PATH, token, repo)
|
| 138 |
if df.empty:
|
| 139 |
+
return pd.DataFrame(columns=["State", "District", "Status"])
|
| 140 |
return df
|
| 141 |
|
| 142 |
|
| 143 |
def seed_district_reference_from_csv(csv_path: str, token: str, repo: str) -> tuple:
|
| 144 |
"""
|
| 145 |
Seed Dataset 1 from a CSV (like master_all_states.csv or SF mapping CSV).
|
| 146 |
+
All states/districts in the CSV β Status = 'present'
|
| 147 |
+
Any states in current scraped data NOT in CSV β Status = 'new districts found'
|
| 148 |
Returns (df, commit_url)
|
| 149 |
"""
|
| 150 |
csv_df = pd.read_csv(csv_path)
|
|
|
|
| 163 |
csv_df = csv_df.dropna(subset=[state_col])
|
| 164 |
csv_df["State"] = csv_df[state_col].astype(str).str.strip().str.upper()
|
| 165 |
csv_df["District"] = csv_df[district_col].astype(str).str.strip().str.upper()
|
| 166 |
+
csv_df["Status"] = "present"
|
| 167 |
|
| 168 |
+
ref_df = csv_df[["State", "District", "Status"]].drop_duplicates()
|
| 169 |
|
| 170 |
# Pull existing scraped metadata to check for any states not in the CSV
|
| 171 |
meta = _pull_metadata(token, repo)
|
|
|
|
| 176 |
if state_upper not in csv_states:
|
| 177 |
# State exists in scraped data but not in SF CSV β pending
|
| 178 |
# We don't know districts yet; they'll be added when detected
|
| 179 |
+
new_rows.append({"State": state_upper, "District": "ALL", "Status": "new districts found"})
|
| 180 |
|
| 181 |
if new_rows:
|
| 182 |
ref_df = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
|
|
|
|
| 192 |
district = district.strip().upper()
|
| 193 |
# Remove existing row if present
|
| 194 |
df = df[~((df["State"] == state) & (df["District"] == district))]
|
| 195 |
+
new_row = pd.DataFrame([{"State": state, "District": district, "Status": sf_status}])
|
| 196 |
df = pd.concat([df, new_row], ignore_index=True)
|
| 197 |
return push_district_reference(df, token, repo)
|
| 198 |
|
|
|
|
| 240 |
|
| 241 |
def bulk_accept_flagged_districts(token: str, repo: str) -> None:
|
| 242 |
"""
|
| 243 |
+
Take all currently flagged districts, add them to Dataset 1 with 'new districts found',
|
| 244 |
and clear the flagged_districts list from metadata.
|
| 245 |
"""
|
| 246 |
meta = _pull_metadata(token, repo)
|
|
|
|
| 255 |
state = f["state"].strip().upper()
|
| 256 |
dist = f["district"].strip().upper()
|
| 257 |
if not ((df["State"] == state) & (df["District"] == dist)).any():
|
| 258 |
+
new_rows.append({"State": state, "District": dist, "Status": "new districts found"})
|
| 259 |
|
| 260 |
if new_rows:
|
| 261 |
df = pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True)
|
|
|
|
| 361 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 362 |
|
| 363 |
def get_flagged_districts(token: str, repo: str) -> list:
|
| 364 |
+
"""Return all districts from Dataset 1 that have Status == 'new districts found', filtered by states with raw data."""
|
| 365 |
ref_df = pull_district_reference(token, repo)
|
| 366 |
+
if ref_df.empty or "Status" not in ref_df.columns:
|
| 367 |
return []
|
| 368 |
|
| 369 |
try:
|
|
|
|
| 379 |
except:
|
| 380 |
active_states = set(ref_df["State"].str.upper()) # fallback
|
| 381 |
|
| 382 |
+
pending = ref_df[(ref_df["Status"] == "new districts found") & (ref_df["State"].str.upper().isin(active_states))]
|
| 383 |
|
| 384 |
dist_df, _ = pull_mapping_rules(token, repo)
|
| 385 |
mapped = set()
|
|
|
|
| 397 |
flags.append({
|
| 398 |
"state": state,
|
| 399 |
"district": district,
|
| 400 |
+
"reason": "New district detected"
|
| 401 |
})
|
| 402 |
return flags
|
| 403 |
|
|
|
|
| 409 |
exists = not ref_df[(ref_df["State"] == state) & (ref_df["District"] == new_name)].empty
|
| 410 |
|
| 411 |
# Target row to update/delete
|
| 412 |
+
mask = (ref_df["State"] == state) & (ref_df["District"] == district) & (ref_df["Status"] == "new districts found")
|
| 413 |
|
| 414 |
if not ref_df[mask].empty:
|
| 415 |
if exists:
|
|
|
|
| 483 |
"""
|
| 484 |
Compare districts in newly scraped df against Dataset 1 and Dataset 2.
|
| 485 |
Any district not in Dataset 1 AND not mapped in Dataset 2 is truly new.
|
| 486 |
+
Add them straight to Dataset 1 with Status = 'new districts found'.
|
| 487 |
"""
|
| 488 |
state_upper = state_name.strip().upper()
|
| 489 |
if state_upper in NON_ACTUAL_STATES:
|
|
|
|
| 555 |
return
|
| 556 |
|
| 557 |
# Add directly to Dataset 1 as pending
|
| 558 |
+
new_rows = [{"State": state_upper, "District": d, "Status": "new districts found"} for d in new_districts]
|
| 559 |
updated_ref = pd.concat([ref_df, pd.DataFrame(new_rows)], ignore_index=True)
|
| 560 |
push_district_reference(updated_ref, token, repo)
|
| 561 |
|
| 562 |
|
| 563 |
def delete_state_file(state_name: str, token: str, repo: str) -> None:
|
| 564 |
+
"""Delete a state's raw dataset file and remove it from metadata.json, and also clean up pending newly found districts for that state."""
|
| 565 |
api = _hf()
|
| 566 |
skey = _state_key(state_name)
|
| 567 |
try:
|
|
|
|
| 579 |
if state_name in meta:
|
| 580 |
del meta[state_name]
|
| 581 |
_push_metadata(meta, token, repo)
|
| 582 |
+
|
| 583 |
+
# Also clean up any "new districts found" for this deleted state in the District Reference
|
| 584 |
+
try:
|
| 585 |
+
ref_df = pull_district_reference(token, repo)
|
| 586 |
+
if not ref_df.empty:
|
| 587 |
+
# We want to KEEP rows that are NOT (State == state_name and Status == 'new districts found')
|
| 588 |
+
original_len = len(ref_df)
|
| 589 |
+
state_upper = state_name.upper()
|
| 590 |
+
mask = ~((ref_df["State"].str.upper() == state_upper) & (ref_df["Status"] == "new districts found"))
|
| 591 |
+
cleaned_ref = ref_df[mask]
|
| 592 |
+
if len(cleaned_ref) < original_len:
|
| 593 |
+
push_district_reference(cleaned_ref, token, repo)
|
| 594 |
+
except Exception as e:
|
| 595 |
+
print(f"Failed to cleanup district reference on state delete: {e}")
|
| 596 |
|
| 597 |
|
| 598 |
def pull_state_file(state_name: str, token: str, repo: str) -> pd.DataFrame:
|