dindizz commited on
Commit
4295692
·
verified ·
1 Parent(s): f7e7cbb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -0
app.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import html
3
+ import datetime as dt
4
+ from typing import Dict, List, Tuple
5
+
6
+ import feedparser
7
+ import gradio as gr
8
+ import plotly.graph_objects as go
9
+ import requests
10
+
11
+ # -----------------------------
12
+ # Config
13
+ # -----------------------------
14
+ BASE_RSS = "https://sachet.ndma.gov.in/cap_public_website/rss"
15
+ UA = "SachetMapRSS/1.0 (+https://huggingface.co/spaces)"
16
+
17
+ # Known tricky slugs or long names -> explicit mapping to RSS file names (without '.xml')
18
+ MANUAL_SLUGS: Dict[str, str] = {
19
+ "andaman and nicobar islands": "andaman_and_nicobar_islands",
20
+ "dadra and nagar haveli and daman and diu": "dadra_and_nagar_haveli_and_daman_and_diu",
21
+ "jammu and kashmir": "jammu_and_kashmir",
22
+ "nct of delhi": "delhi",
23
+ "delhi": "delhi",
24
+ "daman and diu": "dadra_and_nagar_haveli_and_daman_and_diu", # legacy handle
25
+ "ladakh": "ladakh",
26
+ "lakshadweep": "lakshadweep",
27
+ "puducherry": "puducherry",
28
+ "dadra and nagar haveli": "dadra_and_nagar_haveli_and_daman_and_diu",
29
+ }
30
+
31
+ # State/UT centroids (approx.) for click targets (lat, lon).
32
+ # 36 entities: 28 States + 8 UTs
33
+ STATES = [
34
+ ("Andhra Pradesh", 15.9, 79.7),
35
+ ("Arunachal Pradesh", 28.1, 94.6),
36
+ ("Assam", 26.1, 92.9),
37
+ ("Bihar", 25.9, 85.2),
38
+ ("Chhattisgarh", 21.3, 82.0),
39
+ ("Goa", 15.3, 74.0),
40
+ ("Gujarat", 22.3, 70.8),
41
+ ("Haryana", 29.1, 76.0),
42
+ ("Himachal Pradesh", 31.8, 77.2),
43
+ ("Jharkhand", 23.7, 85.5),
44
+ ("Karnataka", 14.5, 75.8),
45
+ ("Kerala", 10.3, 76.5),
46
+ ("Madhya Pradesh", 23.8, 78.8),
47
+ ("Maharashtra", 19.7, 75.7),
48
+ ("Manipur", 24.7, 93.8),
49
+ ("Meghalaya", 25.5, 91.3),
50
+ ("Mizoram", 23.2, 92.8),
51
+ ("Nagaland", 26.1, 94.3),
52
+ ("Odisha", 20.5, 84.4),
53
+ ("Punjab", 31.0, 75.4),
54
+ ("Rajasthan", 26.9, 73.9),
55
+ ("Sikkim", 27.6, 88.5),
56
+ ("Tamil Nadu", 11.1, 78.6),
57
+ ("Telangana", 17.9, 79.6),
58
+ ("Tripura", 23.8, 91.3),
59
+ ("Uttar Pradesh", 27.0, 80.9),
60
+ ("Uttarakhand", 30.1, 79.0),
61
+ ("West Bengal", 23.2, 87.9),
62
+ # UTs
63
+ ("Andaman and Nicobar Islands", 11.7, 92.7),
64
+ ("Chandigarh", 30.7, 76.8),
65
+ ("Dadra and Nagar Haveli and Daman and Diu", 20.3, 73.0),
66
+ ("Delhi", 28.6, 77.2),
67
+ ("Jammu and Kashmir", 33.2, 75.0),
68
+ ("Ladakh", 34.2, 77.6),
69
+ ("Lakshadweep", 10.8, 72.6),
70
+ ("Puducherry", 11.9, 79.8),
71
+ ]
72
+
73
+ NATIONAL_FEED = f"{BASE_RSS}/rss_india.xml"
74
+
75
+
76
+ # -----------------------------
77
+ # Helpers
78
+ # -----------------------------
79
+ def _slugify(name: str) -> str:
80
+ key = name.strip().lower()
81
+ if key in MANUAL_SLUGS:
82
+ return MANUAL_SLUGS[key]
83
+ # Generic rule: replace non-alnum with underscores; collapse repeats
84
+ import re
85
+ s = re.sub(r"[^a-z0-9]+", "_", key).strip("_")
86
+ return s
87
+
88
+ def state_to_feed(name: str, mapping_override: Dict[str, str] | None = None) -> str:
89
+ """
90
+ Build a candidate RSS URL for a state/UT. Accepts optional overrides:
91
+ {'Tamil Nadu': 'https://.../rss_tamil_nadu.xml'}
92
+ """
93
+ if mapping_override and name in mapping_override:
94
+ return mapping_override[name].strip()
95
+
96
+ slug = _slugify(name)
97
+ return f"{BASE_RSS}/rss_{slug}.xml"
98
+
99
+ def fetch_bytes(url: str, timeout: int = 12) -> bytes:
100
+ r = requests.get(url, headers={"User-Agent": UA}, timeout=timeout)
101
+ r.raise_for_status()
102
+ return r.content
103
+
104
+ def iso_utc(t) -> str:
105
+ if not t:
106
+ return ""
107
+ try:
108
+ return dt.datetime(*t[:6], tzinfo=dt.timezone.utc).isoformat().replace("+00:00", "Z")
109
+ except Exception:
110
+ return ""
111
+
112
+ def parse_feed(url: str, max_items: int = 20) -> Tuple[str, List[Dict]]:
113
+ """
114
+ Returns (feed_title, items)
115
+ items: [{title, link, time, summary}]
116
+ """
117
+ raw = fetch_bytes(url)
118
+ parsed = feedparser.parse(raw)
119
+
120
+ feed_title = parsed.feed.get("title") or "Feed"
121
+ items = []
122
+ for e in parsed.entries[:max_items]:
123
+ title = e.get("title", "Untitled")
124
+ link = e.get("link", "#")
125
+ t = iso_utc(e.get("published_parsed") or e.get("updated_parsed"))
126
+ summary = e.get("summary", "") or e.get("description", "")
127
+ items.append({
128
+ "title": title,
129
+ "link": link,
130
+ "time": t,
131
+ "summary": " ".join(summary.split())
132
+ })
133
+ return feed_title, items
134
+
135
+ def render_items_html(feed_title: str, items: List[Dict]) -> str:
136
+ out = [f"<div class='hdr'>{html.escape(feed_title)}</div>"]
137
+ if not items:
138
+ out.append("<div class='empty'>No items found.</div>")
139
+ return "\n".join(out)
140
+
141
+ out.append("<ul class='list'>")
142
+ for it in items:
143
+ t = f"<div class='time'>{html.escape(it['time'])}</div>" if it["time"] else ""
144
+ s = f"<div class='sum'>{html.escape(it['summary'][:500])}</div>" if it["summary"] else ""
145
+ out.append(
146
+ "<li class='card'>"
147
+ f"<a class='ttl' href='{html.escape(it['link'])}' target='_blank' rel='noopener noreferrer'>{html.escape(it['title'])}</a>"
148
+ f"{t}{s}"
149
+ "</li>"
150
+ )
151
+ out.append("</ul>")
152
+ return "\n".join(out)
153
+
154
+
155
+ # -----------------------------
156
+ # Plotly Map
157
+ # -----------------------------
158
+ def make_map() -> go.Figure:
159
+ lats = [lat for _, lat, _ in STATES]
160
+ lons = [lon for _, _, lon in STATES]
161
+ names = [name for name, _, _ in STATES]
162
+
163
+ fig = go.Figure(
164
+ data=go.Scattergeo(
165
+ lat=lats,
166
+ lon=lons,
167
+ text=names,
168
+ customdata=names, # we read this on click
169
+ mode="markers+text",
170
+ textposition="top center",
171
+ marker=dict(size=8),
172
+ hovertemplate="%{customdata}<extra></extra>",
173
+ )
174
+ )
175
+ fig.update_geos(
176
+ scope="asia",
177
+ projection_type="natural earth",
178
+ showcountries=True,
179
+ countrycolor="rgba(120,120,120,0.4)",
180
+ showsubunits=False,
181
+ lataxis_showgrid=True,
182
+ lonaxis_showgrid=True,
183
+ fitbounds="locations",
184
+ visible=True,
185
+ resolution=110
186
+ )
187
+ # Center roughly over India
188
+ fig.update_geos(center=dict(lat=22.5, lon=80.0), lataxis_range=[6, 36], lonaxis_range=[68, 98])
189
+ fig.update_layout(
190
+ margin=dict(l=20, r=20, t=10, b=10),
191
+ dragmode=False
192
+ )
193
+ return fig
194
+
195
+
196
+ # -----------------------------
197
+ # Gradio App
198
+ # -----------------------------
199
+ CSS = """
200
+ :root { --fg:#111; --muted:#666; --bg:#fff; --card:#fafafa; --link:#0b57d0; }
201
+ @media (prefers-color-scheme: dark) {
202
+ :root { --fg:#eee; --muted:#aaa; --bg:#0b0b0b; --card:#141414; --link:#7fb0ff; }
203
+ }
204
+ .wrap { max-width: 1100px; margin: 0 auto; }
205
+ .feedbox { background: var(--card); border: 1px solid rgba(127,127,127,.25); border-radius: 12px; padding: 12px 14px; }
206
+ .hdr { font-weight: 700; font-size: 18px; margin-bottom: 8px; color: var(--fg); }
207
+ .list { list-style: none; padding: 0; margin: 0; }
208
+ .card { padding: 10px 0; border-top: 1px solid rgba(127,127,127,.2); }
209
+ .card:first-child { border-top: none; }
210
+ .ttl { color: var(--link); text-decoration: none; font-weight: 600; }
211
+ .ttl:hover { text-decoration: underline; }
212
+ .time { color: var(--muted); font-size: 12px; margin-top: 2px; }
213
+ .sum { margin-top: 6px; color: var(--fg); white-space: pre-wrap; }
214
+ .empty { color: var(--muted); }
215
+ .err { color: #b00020; }
216
+ .badge { display:inline-block; padding:2px 8px; border:1px solid rgba(127,127,127,.25); border-radius:999px; font-size:12px; color:var(--muted); }
217
+ """
218
+
219
+ with gr.Blocks(css=CSS, fill_height=True, theme=gr.themes.Soft()) as demo:
220
+ gr.Markdown("## SACHET — India Map of State/UT RSS Feeds (Minimal)")
221
+
222
+ with gr.Row():
223
+ map_plot = gr.Plot(value=make_map(), label="Click a marker to load the state/UT feed")
224
+ with gr.Column(scale=1, min_width=300):
225
+ selected = gr.Textbox(label="Selected State/UT", interactive=False)
226
+ feed_url_box = gr.Textbox(label="Resolved Feed URL", interactive=False)
227
+ max_items = gr.Slider(5, 50, value=20, step=1, label="Items")
228
+ refresh = gr.Button("Refresh")
229
+ gr.Markdown(
230
+ "### Optional: Custom mapping\n"
231
+ "Paste JSON as `{ \"Tamil Nadu\": \"https://.../rss_tamil_nadu.xml\", ... }`"
232
+ )
233
+ mapping_json = gr.Code(language="json", value="", lines=6)
234
+ error_box = gr.Markdown(elem_classes=["err"])
235
+
236
+ with gr.Row():
237
+ with gr.Column():
238
+ feed_html = gr.HTML(elem_classes=["feedbox"])
239
+
240
+ def resolve_and_render(state_click_payload, items_n, mapping_text):
241
+ """
242
+ state_click_payload: Plotly 'selected' or 'click' data
243
+ """
244
+ # Extract the state name from click payload
245
+ state = None
246
+ try:
247
+ # Gradio 4: for .select the payload is dict with 'points'
248
+ if isinstance(state_click_payload, dict) and "points" in state_click_payload and state_click_payload["points"]:
249
+ state = state_click_payload["points"][0].get("customdata") or state_click_payload["points"][0].get("text")
250
+ # Some Gradio variants may send just the label
251
+ elif isinstance(state_click_payload, str):
252
+ state = state_click_payload
253
+ except Exception:
254
+ state = None
255
+
256
+ if not state:
257
+ return gr.update(value=""), gr.update(value=""), gr.update(value=""), "<div class='badge'>Select a marker on the map.</div>"
258
+
259
+ # Optional mapping overrides (user input)
260
+ overrides = {}
261
+ if mapping_text:
262
+ try:
263
+ overrides = json.loads(mapping_text)
264
+ if not isinstance(overrides, dict):
265
+ overrides = {}
266
+ except Exception:
267
+ overrides = {}
268
+
269
+ # Resolve URL
270
+ url = state_to_feed(state, overrides)
271
+
272
+ # Try fetch and parse; fall back to national feed if 404
273
+ try:
274
+ title, items = parse_feed(url, max_items=int(items_n))
275
+ html_out = render_items_html(f"{state} — {title}", items)
276
+ return state, url, "", html_out
277
+ except Exception as e:
278
+ # Try a secondary slug variant: remove underscores
279
+ alt_url = url.replace("_", "")
280
+ try:
281
+ title, items = parse_feed(alt_url, max_items=int(items_n))
282
+ html_out = render_items_html(f"{state} — {title}", items)
283
+ return state, alt_url, f"Note: primary URL failed, used fallback.", html_out
284
+ except Exception as e2:
285
+ # Fall back to national feed to keep UI useful
286
+ try:
287
+ title, items = parse_feed(NATIONAL_FEED, max_items=int(items_n))
288
+ html_out = (
289
+ f"<div class='badge'>Could not load <code>{html.escape(url)}</code>. "
290
+ f"Showing national feed instead.</div>"
291
+ ) + render_items_html(f"India — {title}", items)
292
+ return state, url, f"⚠️ {type(e2).__name__}: {e2}", html_out
293
+ except Exception as e3:
294
+ return state, url, f"⚠️ {type(e3).__name__}: {e3}", "<div class='empty'>No data.</div>"
295
+
296
+ # Initial: show national feed
297
+ def initial_load(n_items):
298
+ try:
299
+ title, items = parse_feed(NATIONAL_FEED, max_items=int(n_items))
300
+ return render_items_html(f"India — {title}", items)
301
+ except Exception as e:
302
+ return f"<div class='empty'>Could not load national feed. {html.escape(str(e))}</div>"
303
+
304
+ demo.load(lambda n: initial_load(n), [max_items], [feed_html])
305
+
306
+ # Wire both click and select events for robustness
307
+ map_plot.select(resolve_and_render, [map_plot, max_items, mapping_json], [selected, feed_url_box, error_box, feed_html])
308
+ map_plot.click(resolve_and_render, [map_plot, max_items, mapping_json], [selected, feed_url_box, error_box, feed_html])
309
+ refresh.click(resolve_and_render, [map_plot, max_items, mapping_json], [selected, feed_url_box, error_box, feed_html])
310
+
311
+ if __name__ == "__main__":
312
+ demo.launch()