Spaces:
Sleeping
Sleeping
| 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"<div class='hdr'>{html.escape(feed_title)}</div>"] | |
| if not items: | |
| out.append("<div class='empty'>No items found.</div>") | |
| return "\n".join(out) | |
| out.append("<ul class='list'>") | |
| for it in items: | |
| t = f"<div class='time'>{html.escape(it['time'])}</div>" if it["time"] else "" | |
| s = f"<div class='sum'>{html.escape(it['summary'][:500])}</div>" if it["summary"] else "" | |
| out.append( | |
| "<li class='card'>" | |
| f"<a class='ttl' href='{html.escape(it['link'])}' target='_blank' rel='noopener noreferrer'>{html.escape(it['title'])}</a>" | |
| f"{t}{s}" | |
| "</li>" | |
| ) | |
| out.append("</ul>") | |
| 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}<extra></extra>", | |
| ) | |
| ) | |
| 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 "", "", "", "<div class='badge'>Select a state/UT.</div>" | |
| # 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"<div class='badge'>Could not load <code>{html.escape(url)}</code>. " | |
| f"Showing national feed instead.</div>" | |
| ) + 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}", "<div class='empty'>No data.</div>" | |
| 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"<div class='empty'>Could not load national feed. {html.escape(str(e))}</div>" | |
| # 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() | |