rickyt commited on
Commit
1d20c3b
·
1 Parent(s): 0cb1b37
Files changed (2) hide show
  1. app.py +135 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # Gradio app: Xweather hourly forecast table (next 12 hours)
3
+ # Requirements: pip install gradio requests pandas python-dateutil
4
+
5
+ import os
6
+ import requests
7
+ import pandas as pd
8
+ import gradio as gr
9
+ from zoneinfo import ZoneInfo
10
+
11
+ BASE_URL = "https://data.api.xweather.com/forecasts/"
12
+
13
+ CLIENT_ID = "BlZV8kShcnDxJ2ugQ3b65"
14
+ CLIENT_SECRET = "JYvA8vAJJqEO6yP5QixQw59V3oUKqO9HHvj7ZI2R"
15
+
16
+ def fetch_hourly_table(location: str):
17
+ """
18
+ location: either 'lat,lon' (e.g., '-6.29,106.82') or a place string (e.g., 'Jakarta, ID')
19
+ returns: (pandas.DataFrame, info_markdown)
20
+ """
21
+ if not CLIENT_ID or not CLIENT_SECRET:
22
+ return None, "❌ Missing API credentials. Please set XWEATHER_CLIENT_ID and XWEATHER_CLIENT_SECRET."
23
+
24
+ location = (location or "").strip()
25
+ if not location:
26
+ return None, "❌ Enter a location (e.g., '-6.29,106.82' or 'Jakarta, ID')."
27
+
28
+ # Build request
29
+ # {id} may be 'city,cc' or 'lat,lon'. Keep comma unescaped so the API recognizes coordinates.
30
+ url = BASE_URL + requests.utils.quote(location, safe=",")
31
+ params = {
32
+ "client_id": CLIENT_ID,
33
+ "client_secret": CLIENT_SECRET,
34
+ "filter": "1hr", # hourly periods
35
+ "limit": 12 # next 12 hours
36
+ }
37
+
38
+ try:
39
+ r = requests.get(url, params=params, timeout=20)
40
+ except requests.RequestException as e:
41
+ return None, f"❌ Network error: {e}"
42
+
43
+ if r.status_code == 404:
44
+ # Common case: no coverage for that location
45
+ return None, f"⚠️ No forecast coverage for this location (404). Try a nearby city or adjust coordinates."
46
+ if r.status_code != 200:
47
+ # Show a short snippet of the response for debugging
48
+ body = r.text[:400]
49
+ return None, f"❌ API error {r.status_code}:\n```\n{body}\n```"
50
+
51
+ try:
52
+ data = r.json()
53
+ except ValueError:
54
+ return None, "❌ Failed to parse JSON response."
55
+
56
+ # Xweather returns a top-level list of records for this endpoint
57
+ # (loc/place/periods/profile per record)
58
+ # Fallbacks included in case the format changes.
59
+ records = []
60
+ if isinstance(data, list):
61
+ records = data
62
+ elif isinstance(data, dict) and "response" in data:
63
+ records = data["response"]
64
+ else:
65
+ records = [data]
66
+
67
+ if not records or "periods" not in records[0]:
68
+ return None, "⚠️ Unexpected response format or no periods returned."
69
+
70
+ rec = records[0]
71
+ periods = (rec.get("periods") or [])[:12]
72
+ if not periods:
73
+ return None, "⚠️ No hourly periods returned."
74
+
75
+ # Use location timezone if provided
76
+ tz_str = (rec.get("profile") or {}).get("tz")
77
+ tz = None
78
+ if tz_str:
79
+ try:
80
+ tz = ZoneInfo(tz_str)
81
+ except Exception:
82
+ tz = None
83
+
84
+ rows = []
85
+ for p in periods:
86
+ # Prefer ISO time (already timezone-aware with offset)
87
+ iso = p.get("dateTimeISO") or p.get("validTime")
88
+ dt = pd.to_datetime(iso) if iso else None
89
+ if tz and getattr(dt, "tzinfo", None) is not None:
90
+ try:
91
+ dt = dt.tz_convert(tz)
92
+ except Exception:
93
+ pass
94
+
95
+ rows.append({
96
+ "Time": dt.strftime("%Y-%m-%d %H:%M %Z") if isinstance(dt, pd.Timestamp) else iso,
97
+ "Weather": p.get("weatherPrimary") or p.get("weather"),
98
+ "Temp (°C)": p.get("tempC") if p.get("tempC") is not None else p.get("avgTempC"),
99
+ "Feels (°C)": p.get("feelslikeC") if p.get("feelslikeC") is not None else p.get("avgFeelslikeC"),
100
+ "Humidity (%)": p.get("humidity"),
101
+ "POP (%)": p.get("pop"),
102
+ "Precip (mm)": p.get("precipMM"),
103
+ "Wind (kph)": p.get("windSpeedKPH"),
104
+ "Wind Dir": p.get("windDir"),
105
+ "Visibility (km)": p.get("visibilityKM"),
106
+ "Cloud Cover (%)": p.get("sky"),
107
+ })
108
+
109
+ df = pd.DataFrame(rows)
110
+
111
+ place = rec.get("place") or {}
112
+ place_str = ", ".join([x for x in [place.get("name"), place.get("state"), place.get("country")] if x])
113
+ loc_str = f"**Location**: {place_str or location}"
114
+ tz_info = f" • **Timezone**: {tz_str}" if tz_str else ""
115
+ meta = f"{loc_str}{tz_info} • Periods: {len(df)} (filter=1hr, limit=12)"
116
+
117
+ return df, meta
118
+
119
+ with gr.Blocks(fill_height=True) as demo:
120
+ gr.Markdown("## Xweather — Hourly Forecast (Next 12 Hours)")
121
+ with gr.Row():
122
+ loc = gr.Textbox(
123
+ label="Location",
124
+ placeholder="lat,lon (e.g., -6.21,106.85) or place name (e.g., 'Jakarta, ID')",
125
+ value="0.46876,116.16879"
126
+ )
127
+ btn = gr.Button("Get forecast", variant="primary")
128
+ out_table = gr.Dataframe(label="Hourly forecast", interactive=False, wrap=True)
129
+ out_info = gr.Markdown()
130
+
131
+ btn.click(fetch_hourly_table, inputs=loc, outputs=[out_table, out_info])
132
+
133
+ if __name__ == "__main__":
134
+ # For local runs; on Spaces, Gradio will call this file directly.
135
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ requests
3
+ pandas
4
+ plotly
5
+ python-dotenv