import json import html import datetime as dt from typing import Dict, List, Tuple import feedparser import gradio as gr import plotly.graph_objects as go import requests # ----------------------------- # Config # ----------------------------- BASE_RSS = "https://sachet.ndma.gov.in/cap_public_website/rss" UA = "SachetMapRSS/1.1 (+https://huggingface.co/spaces)" # Known tricky slugs or long names -> explicit mapping to RSS file names (without '.xml') MANUAL_SLUGS: Dict[str, str] = { "andaman and nicobar islands": "andaman_and_nicobar_islands", "dadra and nagar haveli and daman and diu": "dadra_and_nagar_haveli_and_daman_and_diu", "jammu and kashmir": "jammu_and_kashmir", "nct of delhi": "delhi", "delhi": "delhi", "daman and diu": "dadra_and_nagar_haveli_and_daman_and_diu", # legacy handle "ladakh": "ladakh", "lakshadweep": "lakshadweep", "puducherry": "puducherry", "dadra and nagar haveli": "dadra_and_nagar_haveli_and_daman_and_diu", } # State/UT centroids (approx.) for display markers (lat, lon). # 36 entities: 28 States + 8 UTs STATES = [ ("Andhra Pradesh", 15.9, 79.7), ("Arunachal Pradesh", 28.1, 94.6), ("Assam", 26.1, 92.9), ("Bihar", 25.9, 85.2), ("Chhattisgarh", 21.3, 82.0), ("Goa", 15.3, 74.0), ("Gujarat", 22.3, 70.8), ("Haryana", 29.1, 76.0), ("Himachal Pradesh", 31.8, 77.2), ("Jharkhand", 23.7, 85.5), ("Karnataka", 14.5, 75.8), ("Kerala", 10.3, 76.5), ("Madhya Pradesh", 23.8, 78.8), ("Maharashtra", 19.7, 75.7), ("Manipur", 24.7, 93.8), ("Meghalaya", 25.5, 91.3), ("Mizoram", 23.2, 92.8), ("Nagaland", 26.1, 94.3), ("Odisha", 20.5, 84.4), ("Punjab", 31.0, 75.4), ("Rajasthan", 26.9, 73.9), ("Sikkim", 27.6, 88.5), ("Tamil Nadu", 11.1, 78.6), ("Telangana", 17.9, 79.6), ("Tripura", 23.8, 91.3), ("Uttar Pradesh", 27.0, 80.9), ("Uttarakhand", 30.1, 79.0), ("West Bengal", 23.2, 87.9), # UTs ("Andaman and Nicobar Islands", 11.7, 92.7), ("Chandigarh", 30.7, 76.8), ("Dadra and Nagar Haveli and Daman and Diu", 20.3, 73.0), ("Delhi", 28.6, 77.2), ("Jammu and Kashmir", 33.2, 75.0), ("Ladakh", 34.2, 77.6), ("Lakshadweep", 10.8, 72.6), ("Puducherry", 11.9, 79.8), ] NATIONAL_FEED = f"{BASE_RSS}/rss_india.xml" # ----------------------------- # Helpers # ----------------------------- def _slugify(name: str) -> str: key = name.strip().lower() if key in MANUAL_SLUGS: return MANUAL_SLUGS[key] import re s = re.sub(r"[^a-z0-9]+", "_", key).strip("_") return s def state_to_feed(name: str, mapping_override: Dict[str, str] | None = None) -> str: """ Build the RSS URL for a state/UT. Accepts optional overrides: {'Tamil Nadu': 'https://.../rss_tamil_nadu.xml'} """ if mapping_override and name in mapping_override: return mapping_override[name].strip() slug = _slugify(name) return f"{BASE_RSS}/rss_{slug}.xml" def fetch_bytes(url: str, timeout: int = 12) -> bytes: r = requests.get(url, headers={"User-Agent": UA}, timeout=timeout) r.raise_for_status() return r.content def iso_utc(t) -> str: if not t: return "" try: return dt.datetime(*t[:6], tzinfo=dt.timezone.utc).isoformat().replace("+00:00", "Z") except Exception: return "" def parse_feed(url: str, max_items: int = 20) -> Tuple[str, List[Dict]]: """ Returns (feed_title, items) items: [{title, link, time, summary}] """ raw = fetch_bytes(url) parsed = feedparser.parse(raw) feed_title = parsed.feed.get("title") or "Feed" items = [] for e in parsed.entries[:max_items]: title = e.get("title", "Untitled") link = e.get("link", "#") t = iso_utc(e.get("published_parsed") or e.get("updated_parsed")) summary = e.get("summary", "") or e.get("description", "") items.append({ "title": title, "link": link, "time": t, "summary": " ".join(summary.split()) }) return feed_title, items def render_items_html(feed_title: str, items: List[Dict]) -> str: out = [f"
{html.escape(feed_title)}
"] if not items: out.append("
No items found.
") return "\n".join(out) out.append("") return "\n".join(out) # ----------------------------- # Plotly Map (display only) # ----------------------------- def make_map() -> go.Figure: lats = [lat for _, lat, _ in STATES] lons = [lon for _, _, lon in STATES] names = [name for name, _, _ in STATES] fig = go.Figure( data=go.Scattergeo( lat=lats, lon=lons, text=names, customdata=names, # for potential future use mode="markers+text", textposition="top center", marker=dict(size=8), hovertemplate="%{customdata}", ) ) fig.update_geos( scope="asia", projection_type="natural earth", showcountries=True, countrycolor="rgba(120,120,120,0.4)", showsubunits=False, lataxis_showgrid=True, lonaxis_showgrid=True, fitbounds="locations", visible=True, resolution=110 ) # Center roughly over India fig.update_geos(center=dict(lat=22.5, lon=80.0), lataxis_range=[6, 36], lonaxis_range=[68, 98]) fig.update_layout( margin=dict(l=20, r=20, t=10, b=10), dragmode=False ) return fig # ----------------------------- # Gradio App # ----------------------------- CSS = """ :root { --fg:#111; --muted:#666; --bg:#fff; --card:#fafafa; --link:#0b57d0; } @media (prefers-color-scheme: dark) { :root { --fg:#eee; --muted:#aaa; --bg:#0b0b0b; --card:#141414; --link:#7fb0ff; } } .wrap { max-width: 1100px; margin: 0 auto; } .feedbox { background: var(--card); border: 1px solid rgba(127,127,127,.25); border-radius: 12px; padding: 12px 14px; } .hdr { font-weight: 700; font-size: 18px; margin-bottom: 8px; color: var(--fg); } .list { list-style: none; padding: 0; margin: 0; } .card { padding: 10px 0; border-top: 1px solid rgba(127,127,127,.2); } .card:first-child { border-top: none; } .ttl { color: var(--link); text-decoration: none; font-weight: 600; } .ttl:hover { text-decoration: underline; } .time { color: var(--muted); font-size: 12px; margin-top: 2px; } .sum { margin-top: 6px; color: var(--fg); white-space: pre-wrap; } .empty { color: var(--muted); } .err { color: #b00020; } .badge { display:inline-block; padding:2px 8px; border:1px solid rgba(127,127,127,.25); border-radius:999px; font-size:12px; color:var(--muted); } """ STATE_CHOICES = ["India (National Feed)"] + [s[0] for s in STATES] with gr.Blocks(css=CSS, fill_height=True, theme=gr.themes.Soft()) as demo: gr.Markdown("## SACHET — India Map of State/UT RSS Feeds (Minimal)") with gr.Row(): map_plot = gr.Plot(value=make_map(), label="Reference Map (static)") with gr.Column(scale=1, min_width=320): state_dropdown = gr.Dropdown( choices=STATE_CHOICES, value="India (National Feed)", label="Select State/UT" ) selected = gr.Textbox(label="Selected", interactive=False) feed_url_box = gr.Textbox(label="Resolved Feed URL", interactive=False) max_items = gr.Slider(5, 50, value=20, step=1, label="Items") refresh = gr.Button("Load/Refresh", variant="primary") gr.Markdown( "### Optional: Custom mapping\n" "Paste JSON as `{ \"Tamil Nadu\": \"https://.../rss_tamil_nadu.xml\", ... }`" ) mapping_json = gr.Code(language="json", value="", lines=6) error_box = gr.Markdown(elem_classes=["err"]) with gr.Row(): with gr.Column(): feed_html = gr.HTML(elem_classes=["feedbox"]) # ------------------------- # Actions # ------------------------- def resolve_and_render(state_name: str, items_n: int, mapping_text: str): # Normalize state label state = (state_name or "").strip() if not state: return "", "", "", "
Select a state/UT.
" # Optional mapping overrides (user input) overrides = {} if mapping_text: try: overrides = json.loads(mapping_text) if not isinstance(overrides, dict): overrides = {} except Exception: overrides = {} # Resolve URL if state.lower().startswith("india"): url = NATIONAL_FEED else: url = state_to_feed(state, overrides) # Try fetch and parse; fallback to national if error try: title, items = parse_feed(url, max_items=int(items_n)) html_out = render_items_html(f"{state} — {title}", items) return state, url, "", html_out except Exception as e: # Try a secondary slug variant if not India if not state.lower().startswith("india"): alt_url = url.replace("_", "") try: title, items = parse_feed(alt_url, max_items=int(items_n)) html_out = render_items_html(f"{state} — {title}", items) return state, alt_url, "Note: primary URL failed, used fallback.", html_out except Exception: pass # Final fallback: national try: title, items = parse_feed(NATIONAL_FEED, max_items=int(items_n)) html_out = ( f"
Could not load {html.escape(url)}. " f"Showing national feed instead.
" ) + render_items_html(f"India — {title}", items) return state, url, f"⚠️ {type(e).__name__}: {e}", html_out except Exception as e3: return state, url, f"⚠️ {type(e3).__name__}: {e3}", "
No data.
" def initial_load(n_items: int): try: title, items = parse_feed(NATIONAL_FEED, max_items=int(n_items)) return render_items_html(f"India — {title}", items) except Exception as e: return f"
Could not load national feed. {html.escape(str(e))}
" # Initial content demo.load(lambda n: initial_load(n), [max_items], [feed_html]) # Wire dropdown and button state_dropdown.change( resolve_and_render, [state_dropdown, max_items, mapping_json], [selected, feed_url_box, error_box, feed_html], ) refresh.click( resolve_and_render, [state_dropdown, max_items, mapping_json], [selected, feed_url_box, error_box, feed_html], ) if __name__ == "__main__": demo.launch()