Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
import googlemaps
|
| 4 |
+
import re
|
| 5 |
+
import gradio as gr
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import requests
|
| 8 |
+
from geopy.geocoders import Nominatim
|
| 9 |
+
from datetime import datetime, date, timedelta
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
dotenv_path = os.getenv('DOTENV_PATH', None)
|
| 15 |
+
if dotenv_path:
|
| 16 |
+
load_dotenv(dotenv_path)
|
| 17 |
+
else:
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 21 |
+
if not api_key:
|
| 22 |
+
raise EnvironmentError("GOOGLE_API_KEY not found in environment variables.")
|
| 23 |
+
gmaps = googlemaps.Client(key=api_key)
|
| 24 |
+
def get_directions(
|
| 25 |
+
source: str,
|
| 26 |
+
destination: str,
|
| 27 |
+
mode: str,
|
| 28 |
+
departure_time: str):
|
| 29 |
+
"""
|
| 30 |
+
Returns a Markdown-formatted string with step-by-step directions and a static map URL.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
source: Starting address or place name.
|
| 34 |
+
destination: Ending address or place name.
|
| 35 |
+
mode: Transport mode: driving, walking, bicycling, or transit.
|
| 36 |
+
departure_time: Trip start time in "YYYY-MM-DD HH:MM" format.
|
| 37 |
+
waypoints: Optional comma-separated list of waypoints.
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
A single Markdown string summarizing the route or an error message.
|
| 41 |
+
"""
|
| 42 |
+
# Parse departure time
|
| 43 |
+
try:
|
| 44 |
+
dt = datetime.strptime(departure_time.strip(), "%Y-%m-%d %H:%M")
|
| 45 |
+
except ValueError:
|
| 46 |
+
return (
|
| 47 |
+
"**⚠️ Invalid time format.** Please enter departure time as YYYY-MM-DD HH:MM."
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Build waypoints list
|
| 51 |
+
wp_list = None
|
| 52 |
+
|
| 53 |
+
# Call Directions API
|
| 54 |
+
directions = gmaps.directions(
|
| 55 |
+
origin=source,
|
| 56 |
+
destination=destination,
|
| 57 |
+
mode=mode.lower(),
|
| 58 |
+
departure_time=dt,
|
| 59 |
+
waypoints=wp_list,
|
| 60 |
+
optimize_waypoints=True,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
if not directions:
|
| 64 |
+
return "**No route found.**"
|
| 65 |
+
|
| 66 |
+
# Use first route & leg
|
| 67 |
+
route = directions[0]
|
| 68 |
+
leg = route["legs"][0]
|
| 69 |
+
|
| 70 |
+
# Build summary
|
| 71 |
+
summary = f"**Trip from '{leg['start_address']}' to '{leg['end_address']}'**\n"
|
| 72 |
+
summary += f"- Total distance: {leg['distance']['text']}\n"
|
| 73 |
+
summary += f"- Estimated duration: {leg['duration']['text']}\n\n"
|
| 74 |
+
summary += "### Step-by-Step Directions:\n"
|
| 75 |
+
|
| 76 |
+
for i, step in enumerate(leg["steps"], start=1):
|
| 77 |
+
travel_mode = step.get("travel_mode", "").upper()
|
| 78 |
+
if travel_mode == "TRANSIT":
|
| 79 |
+
details = step.get("transit_details", {})
|
| 80 |
+
line = details.get("line", {})
|
| 81 |
+
vehicle = line.get("vehicle", {}).get("type", "Transit")
|
| 82 |
+
line_name = line.get("short_name") or line.get("name", "")
|
| 83 |
+
dep = details.get("departure_stop", {}).get("name", "")
|
| 84 |
+
arr = details.get("arrival_stop", {}).get("name", "")
|
| 85 |
+
stops = details.get("num_stops", "")
|
| 86 |
+
instr = f"Take {vehicle} {line_name} from {dep} to {arr} ({stops} stops)"
|
| 87 |
+
dist = step.get("distance", {}).get("text", "")
|
| 88 |
+
dur = details.get("departure_time",{}).get("text","")
|
| 89 |
+
else:
|
| 90 |
+
raw = step.get("html_instructions", "")
|
| 91 |
+
instr = re.sub(r"<[^>]+>", "", raw)
|
| 92 |
+
dist = step.get("distance", {}).get("text", "")
|
| 93 |
+
dur = step.get("departure_time",{}).get("text","")
|
| 94 |
+
|
| 95 |
+
summary += f"{i}. {instr} ({dist},{dur})\n"
|
| 96 |
+
|
| 97 |
+
# Static map snapshot
|
| 98 |
+
poly = route.get("overview_polyline", {}).get("points")
|
| 99 |
+
if poly:
|
| 100 |
+
static_map_url = (
|
| 101 |
+
"https://maps.googleapis.com/maps/api/staticmap"
|
| 102 |
+
f"?size=600x400&path=enc:{poly}&key={api_key}"
|
| 103 |
+
)
|
| 104 |
+
summary += "\n---\n"
|
| 105 |
+
summary += ("Here’s a **static map snapshot** of this route:\n" + static_map_url)
|
| 106 |
+
|
| 107 |
+
return summary
|
| 108 |
+
|
| 109 |
+
WEATHER_CODES = {
|
| 110 |
+
0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
|
| 111 |
+
45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
|
| 112 |
+
61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Slight snowfall", 73: "Moderate snowfall",
|
| 113 |
+
75: "Heavy snowfall", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers",
|
| 114 |
+
95: "Thunderstorm", 96: "Thunderstorm with hail"
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_weather_forecast_range(location_name, start_date, end_date):
|
| 119 |
+
try:
|
| 120 |
+
if isinstance(start_date, str):
|
| 121 |
+
start_date = datetime.strptime(start_date, "%Y-%m-%d").date()
|
| 122 |
+
if isinstance(end_date, str):
|
| 123 |
+
end_date = datetime.strptime(end_date, "%Y-%m-%d").date()
|
| 124 |
+
|
| 125 |
+
today = date.today()
|
| 126 |
+
days_ahead = (end_date - today).days
|
| 127 |
+
|
| 128 |
+
if days_ahead > 15:
|
| 129 |
+
return {"error": "Weather data only available up to 15 days from today."}
|
| 130 |
+
|
| 131 |
+
geolocator = Nominatim(user_agent="weather_api")
|
| 132 |
+
location = geolocator.geocode(location_name)
|
| 133 |
+
if not location:
|
| 134 |
+
return {"error": f"Could not find coordinates for '{location_name}'."}
|
| 135 |
+
lat, lon = location.latitude, location.longitude
|
| 136 |
+
|
| 137 |
+
include_hourly = days_ahead <= 6
|
| 138 |
+
|
| 139 |
+
url = "https://api.open-meteo.com/v1/forecast"
|
| 140 |
+
params = {
|
| 141 |
+
"latitude": lat,
|
| 142 |
+
"longitude": lon,
|
| 143 |
+
"daily": "sunrise,sunset,uv_index_max,temperature_2m_max,temperature_2m_min,weather_code",
|
| 144 |
+
"temperature_unit": "celsius",
|
| 145 |
+
"windspeed_unit": "kmh",
|
| 146 |
+
"timeformat": "iso8601",
|
| 147 |
+
"timezone": "auto",
|
| 148 |
+
"start_date": start_date.isoformat(),
|
| 149 |
+
"end_date": end_date.isoformat()
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
if include_hourly:
|
| 153 |
+
params["hourly"] = "temperature_2m,weather_code,uv_index,visibility"
|
| 154 |
+
|
| 155 |
+
response = requests.get(url, params=params)
|
| 156 |
+
if response.status_code != 200:
|
| 157 |
+
return {"error": f"API error {response.status_code}: {response.text}"}
|
| 158 |
+
raw = response.json()
|
| 159 |
+
|
| 160 |
+
forecasts = []
|
| 161 |
+
for idx, d in enumerate(raw["daily"]["time"]):
|
| 162 |
+
day_result = {
|
| 163 |
+
"date": d,
|
| 164 |
+
"sunrise": raw["daily"]["sunrise"][idx].split("T")[1],
|
| 165 |
+
"sunset": raw["daily"]["sunset"][idx].split("T")[1],
|
| 166 |
+
"uv_max": round(raw["daily"]["uv_index_max"][idx], 1),
|
| 167 |
+
"temp_min": round(raw["daily"]["temperature_2m_min"][idx]),
|
| 168 |
+
"temp_max": round(raw["daily"]["temperature_2m_max"][idx]),
|
| 169 |
+
"weather": WEATHER_CODES.get(int(raw["daily"]["weather_code"][idx]), "Unknown")
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if include_hourly and "hourly" in raw:
|
| 173 |
+
hourly_df = pd.DataFrame({
|
| 174 |
+
"time": raw["hourly"]["time"],
|
| 175 |
+
"temp": raw["hourly"]["temperature_2m"],
|
| 176 |
+
"code": raw["hourly"]["weather_code"],
|
| 177 |
+
"uv": raw["hourly"]["uv_index"],
|
| 178 |
+
"visibility": [v / 1000 for v in raw["hourly"]["visibility"]]
|
| 179 |
+
})
|
| 180 |
+
|
| 181 |
+
hourly_df["time"] = pd.to_datetime(hourly_df["time"])
|
| 182 |
+
target_date = datetime.strptime(d, "%Y-%m-%d").date()
|
| 183 |
+
df_day = hourly_df[hourly_df["time"].dt.date == target_date]
|
| 184 |
+
|
| 185 |
+
df_day["weather"] = df_day["code"].apply(lambda c: WEATHER_CODES.get(int(c), "Unknown"))
|
| 186 |
+
|
| 187 |
+
day_result["hourly"] = [
|
| 188 |
+
{
|
| 189 |
+
"time": t.strftime("%Y-%m-%d %H:%M"),
|
| 190 |
+
"temp": f"{round(temp)}°C",
|
| 191 |
+
"weather": w,
|
| 192 |
+
"uv": round(uv, 1),
|
| 193 |
+
"visibility": f"{round(vis, 1)} km"
|
| 194 |
+
}
|
| 195 |
+
for t, temp, w, uv, vis in zip(
|
| 196 |
+
df_day["time"], df_day["temp"], df_day["weather"],
|
| 197 |
+
df_day["uv"], df_day["visibility"]
|
| 198 |
+
)
|
| 199 |
+
]
|
| 200 |
+
else:
|
| 201 |
+
day_result["note"] = "Hourly weather data is only available for the next 7 days."
|
| 202 |
+
|
| 203 |
+
forecasts.append(day_result)
|
| 204 |
+
|
| 205 |
+
return forecasts
|
| 206 |
+
|
| 207 |
+
except Exception as e:
|
| 208 |
+
return {"error": str(e)}
|
| 209 |
+
|
| 210 |
+
def get_weather_forecast_range_ui(location_name, start_date, end_date):
|
| 211 |
+
result = get_weather_forecast_range(location_name, start_date, end_date)
|
| 212 |
+
|
| 213 |
+
if "error" in result:
|
| 214 |
+
return f"**⚠️ Error:** {result['error']}"
|
| 215 |
+
|
| 216 |
+
# Save JSON silently
|
| 217 |
+
save_dir = Path("logs")
|
| 218 |
+
save_dir.mkdir(exist_ok=True)
|
| 219 |
+
save_path = save_dir / f"weather_{location_name}_{start_date}_to_{end_date}.json"
|
| 220 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
| 221 |
+
json.dump(result, f, indent=2)
|
| 222 |
+
|
| 223 |
+
# Generate Markdown summary
|
| 224 |
+
md = f"## Weather Forecast for {location_name}\n\n"
|
| 225 |
+
for day in result:
|
| 226 |
+
md += f"### 📅 {day['date']}\n"
|
| 227 |
+
md += f"- 🌅 Sunrise: {day['sunrise']}\n"
|
| 228 |
+
md += f"- 🌇 Sunset: {day['sunset']}\n"
|
| 229 |
+
md += f"- 🌡️ Temp: {day['temp_min']}°C – {day['temp_max']}°C\n"
|
| 230 |
+
md += f"- 🌤️ Weather: {day['weather']}\n"
|
| 231 |
+
md += f"- ☀️ UV Index Max: {day['uv_max']}\n"
|
| 232 |
+
|
| 233 |
+
if "hourly" in day:
|
| 234 |
+
md += f"**Hourly Forecast:**\n"
|
| 235 |
+
for h in day["hourly"]:
|
| 236 |
+
md += f" - {h['time']}: {h['temp']}, {h['weather']}, UV {h['uv']}, Visibility {h['visibility']}\n"
|
| 237 |
+
elif "note" in day:
|
| 238 |
+
md += f"_{day['note']}_\n"
|
| 239 |
+
md += "\n"
|
| 240 |
+
|
| 241 |
+
return md
|
| 242 |
+
|
| 243 |
+
demo = gr.TabbedInterface(
|
| 244 |
+
[
|
| 245 |
+
gr.Interface(
|
| 246 |
+
fn=get_directions,
|
| 247 |
+
inputs=[
|
| 248 |
+
gr.Textbox(label="Source Location", placeholder="e.g. New York City"),
|
| 249 |
+
gr.Textbox(label="Destination Location", placeholder="e.g. Los Angeles"),
|
| 250 |
+
gr.Radio(["driving", "walking", "bicycling", "transit"], label="Travel Mode", value="driving"),
|
| 251 |
+
gr.Textbox(label="Departure Time (YYYY-MM-DD HH:MM)", placeholder="e.g. 2025-06-08 14:30"),
|
| 252 |
+
],
|
| 253 |
+
outputs=gr.Markdown(label="Directions Summary"),
|
| 254 |
+
api_name="get direction"
|
| 255 |
+
),
|
| 256 |
+
gr.Interface(
|
| 257 |
+
fn=get_weather_forecast_range_ui,
|
| 258 |
+
inputs=[
|
| 259 |
+
gr.Textbox(label="Location Name", placeholder="Enter a city or place name"),
|
| 260 |
+
gr.Textbox(label="Start Date (YYYY-MM-DD)", value=date.today()),
|
| 261 |
+
gr.Textbox(label="End Date (YYYY-MM-DD)", value=date.today() + timedelta(days=6)),
|
| 262 |
+
],
|
| 263 |
+
outputs=gr.Markdown(label="Readable Forecast"),
|
| 264 |
+
api_name="get weather forecast"
|
| 265 |
+
)
|
| 266 |
+
],
|
| 267 |
+
tab_names=["Google Maps Directions", "Weather Forecast"]
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
if __name__ == "__main__":
|
| 271 |
+
demo.launch()
|