Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- Dockerfile +20 -0
- app.py +70 -0
- docker-compose.yml +14 -0
- requirements.txt +5 -0
- static/css/style.css +125 -0
- static/js/script.js +96 -0
- templates/index.html +44 -0
- weather_api.py +54 -0
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python runtime as a parent image
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Set the working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy the current directory contents into the container at /app
|
8 |
+
COPY . /app
|
9 |
+
|
10 |
+
# Install any needed packages specified in requirements.txt
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Make port 7860 available to the world outside this container
|
14 |
+
EXPOSE 7860
|
15 |
+
|
16 |
+
# Define environment variable
|
17 |
+
ENV NAME WeatherDashboard
|
18 |
+
|
19 |
+
# Run app.py when the container launches
|
20 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, render_template, request, jsonify
|
2 |
+
from weather_api import get_weather_data, save_weather_data, get_cached_weather_data
|
3 |
+
from flask_mail import Mail, Message
|
4 |
+
|
5 |
+
app = Flask(__name__)
|
6 |
+
|
7 |
+
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
|
8 |
+
app.config['MAIL_PORT'] = 587
|
9 |
+
app.config['MAIL_USE_TLS'] = True
|
10 |
+
app.config['MAIL_USERNAME'] = '[email protected]'
|
11 |
+
app.config['MAIL_PASSWORD'] = 'your_password'
|
12 |
+
|
13 |
+
mail = Mail(app)
|
14 |
+
|
15 |
+
@app.route('/')
|
16 |
+
def index():
|
17 |
+
return render_template('index.html')
|
18 |
+
|
19 |
+
@app.route('/weather', methods=['GET'])
|
20 |
+
def weather():
|
21 |
+
city = request.args.get('city')
|
22 |
+
if city:
|
23 |
+
# Check cache first
|
24 |
+
cached_data = get_cached_weather_data(city)
|
25 |
+
if cached_data:
|
26 |
+
return jsonify(cached_data)
|
27 |
+
|
28 |
+
# If not in cache, fetch from API
|
29 |
+
data = get_weather_data(city = city)
|
30 |
+
if data:
|
31 |
+
save_weather_data(city, data)
|
32 |
+
return jsonify(data)
|
33 |
+
else:
|
34 |
+
return jsonify({'error': 'City not found'}), 404
|
35 |
+
else:
|
36 |
+
return jsonify({'error': 'No city provided'}), 400
|
37 |
+
|
38 |
+
@app.route('/weatherlocation', methods=['GET'])
|
39 |
+
def weatherlocation():
|
40 |
+
lat = request.args.get('lat')
|
41 |
+
lon = request.args.get('lon')
|
42 |
+
if lat and lon:
|
43 |
+
# Check cache first
|
44 |
+
cached_data = get_cached_weather_data(lat+lon)
|
45 |
+
if cached_data:
|
46 |
+
return jsonify(cached_data)
|
47 |
+
|
48 |
+
# If not in cache, fetch from API
|
49 |
+
data = get_weather_data(lat = lat, lon = lon)
|
50 |
+
if data:
|
51 |
+
save_weather_data(lat+lon, data)
|
52 |
+
return jsonify(data)
|
53 |
+
else:
|
54 |
+
return jsonify({'error': 'City not found'}), 404
|
55 |
+
else:
|
56 |
+
return jsonify({'error': 'No city provided'}), 400
|
57 |
+
|
58 |
+
@app.route('/subscribe', methods=['POST'])
|
59 |
+
def subscribe():
|
60 |
+
email = request.json.get('email')
|
61 |
+
if email:
|
62 |
+
msg = Message('Weather Subscription', sender='[email protected]', recipients=[email])
|
63 |
+
msg.body = 'Thank you for subscribing to daily weather updates!'
|
64 |
+
mail.send(msg)
|
65 |
+
return jsonify({'message': 'Subscription successful, please check your email to confirm.'})
|
66 |
+
else:
|
67 |
+
return jsonify({'error': 'No email provided'}), 400
|
68 |
+
|
69 |
+
if __name__ == '__main__':
|
70 |
+
app.run(debug=True, host='0.0.0.0', port=7860)
|
docker-compose.yml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3'
|
2 |
+
|
3 |
+
services:
|
4 |
+
web:
|
5 |
+
build: .
|
6 |
+
ports:
|
7 |
+
- "7860:7860"
|
8 |
+
environment:
|
9 |
+
- FLASK_ENV=development
|
10 |
+
- MAIL_SERVER=smtp.example.com
|
11 |
+
- MAIL_PORT=587
|
12 |
+
- MAIL_USE_TLS=true
|
13 | |
14 |
+
- MAIL_PASSWORD=your_password
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask
|
2 |
+
requests
|
3 |
+
Flask-Mail
|
4 |
+
beautifulsoup4
|
5 |
+
Werkzeug
|
static/css/style.css
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
html, body {
|
2 |
+
margin: 0;
|
3 |
+
padding: 0;
|
4 |
+
width: 100%;
|
5 |
+
height: 100%;
|
6 |
+
font-family: Arial, sans-serif;
|
7 |
+
background-color: #e0f7fa;
|
8 |
+
color: #333;
|
9 |
+
}
|
10 |
+
|
11 |
+
.container {
|
12 |
+
display: flex;
|
13 |
+
flex-direction: column;
|
14 |
+
height: 100%;
|
15 |
+
}
|
16 |
+
|
17 |
+
header {
|
18 |
+
text-align: center;
|
19 |
+
background-color: #42a5f5;
|
20 |
+
color: white;
|
21 |
+
padding: 10px;
|
22 |
+
border-radius: 5px;
|
23 |
+
}
|
24 |
+
|
25 |
+
main {
|
26 |
+
flex: 1;
|
27 |
+
display: flex;
|
28 |
+
flex-direction: column;
|
29 |
+
justify-content: space-between;
|
30 |
+
padding: 20px;
|
31 |
+
overflow-y: auto;
|
32 |
+
}
|
33 |
+
|
34 |
+
.content {
|
35 |
+
display: flex;
|
36 |
+
justify-content: space-between;
|
37 |
+
align-items: flex-start;
|
38 |
+
}
|
39 |
+
|
40 |
+
.search-section, .subscribe-section {
|
41 |
+
text-align: center;
|
42 |
+
margin: 20px 0;
|
43 |
+
}
|
44 |
+
|
45 |
+
.search-section {
|
46 |
+
flex: 0 0 30%;
|
47 |
+
display: flex;
|
48 |
+
flex-direction: column;
|
49 |
+
align-items: center;
|
50 |
+
}
|
51 |
+
|
52 |
+
.search-section input, .subscribe-section input {
|
53 |
+
padding: 10px;
|
54 |
+
width: 80%;
|
55 |
+
margin: 5px 0;
|
56 |
+
}
|
57 |
+
|
58 |
+
.search-section button, .subscribe-section button {
|
59 |
+
padding: 10px 20px;
|
60 |
+
margin: 5px;
|
61 |
+
background-color: #42a5f5;
|
62 |
+
color: white;
|
63 |
+
border: none;
|
64 |
+
border-radius: 5px;
|
65 |
+
cursor: pointer;
|
66 |
+
}
|
67 |
+
|
68 |
+
.search-section button#location-button {
|
69 |
+
background-color: #757575;
|
70 |
+
}
|
71 |
+
|
72 |
+
.weather-section {
|
73 |
+
flex: 1;
|
74 |
+
display: flex;
|
75 |
+
flex-direction: column;
|
76 |
+
}
|
77 |
+
|
78 |
+
.current-weather, .forecast {
|
79 |
+
background-color: #bbdefb;
|
80 |
+
padding: 20px;
|
81 |
+
margin: 10px 0;
|
82 |
+
border-radius: 5px;
|
83 |
+
}
|
84 |
+
|
85 |
+
.current-weather {
|
86 |
+
display: flex;
|
87 |
+
justify-content: space-between;
|
88 |
+
align-items: center;
|
89 |
+
}
|
90 |
+
|
91 |
+
.current-weather .weather-details {
|
92 |
+
flex: 1;
|
93 |
+
}
|
94 |
+
|
95 |
+
.current-weather .weather-details p {
|
96 |
+
margin: 5px 0;
|
97 |
+
}
|
98 |
+
|
99 |
+
.weather-icon {
|
100 |
+
text-align: center;
|
101 |
+
flex: 0 0 400px;
|
102 |
+
}
|
103 |
+
|
104 |
+
.weather-icon img {
|
105 |
+
width: 50px;
|
106 |
+
height: 50px;
|
107 |
+
}
|
108 |
+
|
109 |
+
.forecast {
|
110 |
+
display: flex;
|
111 |
+
justify-content: space-between;
|
112 |
+
}
|
113 |
+
|
114 |
+
.forecast-day {
|
115 |
+
background-color: #eceff1;
|
116 |
+
padding: 10px;
|
117 |
+
border-radius: 5px;
|
118 |
+
text-align: center;
|
119 |
+
width: 23%;
|
120 |
+
}
|
121 |
+
|
122 |
+
.forecast-day img {
|
123 |
+
width: 40px;
|
124 |
+
height: 40px;
|
125 |
+
}
|
static/js/script.js
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.getElementById('search-button').addEventListener('click', function() {
|
2 |
+
let city = document.getElementById('city-input').value;
|
3 |
+
fetchWeatherData(city);
|
4 |
+
});
|
5 |
+
|
6 |
+
document.getElementById('location-button').addEventListener('click', function() {
|
7 |
+
if (navigator.geolocation) {
|
8 |
+
navigator.geolocation.getCurrentPosition(position => {
|
9 |
+
let lat = position.coords.latitude;
|
10 |
+
let lon = position.coords.longitude;
|
11 |
+
fetchWeatherDataByLocation(lat, lon);
|
12 |
+
});
|
13 |
+
} else {
|
14 |
+
alert('Geolocation is not supported by this browser.');
|
15 |
+
}
|
16 |
+
});
|
17 |
+
|
18 |
+
function fetchWeatherData(city) {
|
19 |
+
fetch(`/weather?city=${city}`)
|
20 |
+
.then(response => response.json())
|
21 |
+
.then(data => {
|
22 |
+
if (data.error) {
|
23 |
+
alert(data.error);
|
24 |
+
} else {
|
25 |
+
displayWeatherData(data);
|
26 |
+
}
|
27 |
+
})
|
28 |
+
.catch(error => console.error('Error:', error));
|
29 |
+
}
|
30 |
+
|
31 |
+
function fetchWeatherDataByLocation(lat, lon) {
|
32 |
+
fetch(`/weatherlocation?lat=${lat}&lon=${lon}`)
|
33 |
+
.then(response => response.json())
|
34 |
+
.then(data => {
|
35 |
+
if (data.error) {
|
36 |
+
alert(data.error);
|
37 |
+
} else {
|
38 |
+
displayWeatherData(data);
|
39 |
+
}
|
40 |
+
})
|
41 |
+
.catch(error => console.error('Error:', error));
|
42 |
+
}
|
43 |
+
|
44 |
+
function displayWeatherData(data) {
|
45 |
+
let currentWeather = document.getElementById('current-weather');
|
46 |
+
currentWeather.innerHTML = `
|
47 |
+
<div class="weather-details">
|
48 |
+
<h2>${data.location.name} (${data.current.last_updated})</h2>
|
49 |
+
<p>Temperature: ${data.current.temp_c}°C</p>
|
50 |
+
<p>Wind: ${data.current.wind_kph} KPH</p>
|
51 |
+
<p>Humidity: ${data.current.humidity}%</p>
|
52 |
+
</div>
|
53 |
+
<div class="weather-icon">
|
54 |
+
<img src="${data.current.condition.icon}" alt="${data.current.condition.text}">
|
55 |
+
<p>${data.current.condition.text}</p>
|
56 |
+
</div>
|
57 |
+
`;
|
58 |
+
|
59 |
+
let forecast = document.getElementById('forecast');
|
60 |
+
forecast.innerHTML = '';
|
61 |
+
|
62 |
+
let lastFourDays = data.forecast.forecastday.slice(-4);
|
63 |
+
lastFourDays.forEach(day => {
|
64 |
+
forecast.innerHTML += `
|
65 |
+
<div class="forecast-day">
|
66 |
+
<h3>${day.date}</h3>
|
67 |
+
<img src="${day.day.condition.icon}" alt="${day.day.condition.text}">
|
68 |
+
<p>Temp: ${day.day.avgtemp_c}°C</p>
|
69 |
+
<p>Wind: ${day.day.maxwind_kph} KPH</p>
|
70 |
+
<p>Humidity: ${day.day.avghumidity}%</p>
|
71 |
+
</div>
|
72 |
+
`;
|
73 |
+
});
|
74 |
+
}
|
75 |
+
|
76 |
+
|
77 |
+
document.getElementById('subscribe-form').addEventListener('submit', function(event) {
|
78 |
+
event.preventDefault();
|
79 |
+
let email = document.getElementById('email-input').value;
|
80 |
+
fetch('/subscribe', {
|
81 |
+
method: 'POST',
|
82 |
+
headers: {
|
83 |
+
'Content-Type': 'application/json',
|
84 |
+
},
|
85 |
+
body: JSON.stringify({ email: email })
|
86 |
+
})
|
87 |
+
.then(response => response.json())
|
88 |
+
.then(data => {
|
89 |
+
if (data.error) {
|
90 |
+
alert(data.error);
|
91 |
+
} else {
|
92 |
+
alert(data.message);
|
93 |
+
}
|
94 |
+
})
|
95 |
+
.catch(error => console.error('Error:', error));
|
96 |
+
});
|
templates/index.html
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Weather Dashboard</title>
|
7 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<div class="container">
|
11 |
+
<header>
|
12 |
+
<h1>Weather Dashboard</h1>
|
13 |
+
</header>
|
14 |
+
<main>
|
15 |
+
<div class="content">
|
16 |
+
<section class="search-section">
|
17 |
+
<input type="text" id="city-input" placeholder="E.g., New York, London, Tokyo">
|
18 |
+
<button id="search-button">Search</button>
|
19 |
+
<p>or</p>
|
20 |
+
<button id="location-button">Use Current Location</button>
|
21 |
+
</section>
|
22 |
+
<section class="weather-section">
|
23 |
+
<div class="current-weather" id="current-weather">
|
24 |
+
<!-- Current weather data will be displayed here -->
|
25 |
+
</div>
|
26 |
+
<h2>4-Day Forecast</h2>
|
27 |
+
<div class="forecast" id="forecast">
|
28 |
+
<!-- <h2>4-Day Forecast</h2> -->
|
29 |
+
<!-- Forecast data will be displayed here -->
|
30 |
+
</div>
|
31 |
+
</section>
|
32 |
+
</div>
|
33 |
+
<section class="subscribe-section">
|
34 |
+
<h2>Subscribe to Daily Weather Updates</h2>
|
35 |
+
<form id="subscribe-form">
|
36 |
+
<input type="email" id="email-input" placeholder="Enter your email">
|
37 |
+
<button type="submit">Subscribe</button>
|
38 |
+
</form>
|
39 |
+
</section>
|
40 |
+
</main>
|
41 |
+
</div>
|
42 |
+
<script src="/static/js/script.js"></script>
|
43 |
+
</body>
|
44 |
+
</html>
|
weather_api.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from datetime import datetime, timedelta
|
3 |
+
|
4 |
+
|
5 |
+
def is_time_difference_greater_than_one_hour(time1, time2):
|
6 |
+
datetime1 = datetime.strptime(time1, "%Y-%m-%d %H:%M:%S")
|
7 |
+
datetime2 = datetime.strptime(time2, "%Y-%m-%d %H:%M:%S")
|
8 |
+
|
9 |
+
difference = abs(datetime1 - datetime2)
|
10 |
+
|
11 |
+
return difference >= timedelta(hours=1)
|
12 |
+
|
13 |
+
API_KEY = '0eab9492e3024ce4969105357241507'
|
14 |
+
weather_cache = {}
|
15 |
+
date_check = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
16 |
+
|
17 |
+
def get_weather_data(city = "",lat = "",lon = ""):
|
18 |
+
if city !="":
|
19 |
+
url = f'http://api.weatherapi.com/v1/forecast.json?key={API_KEY}&q={city}&days=5'
|
20 |
+
response = requests.get(url)
|
21 |
+
if response.status_code == 200:
|
22 |
+
return response.json()
|
23 |
+
else:
|
24 |
+
return None
|
25 |
+
elif lat != "" and lon != "":
|
26 |
+
url = f'http://api.weatherapi.com/v1/forecast.json?key={API_KEY}&q={lat},{lon}&days=5'
|
27 |
+
response = requests.get(url)
|
28 |
+
if response.status_code == 200:
|
29 |
+
return response.json()
|
30 |
+
else:
|
31 |
+
return None
|
32 |
+
return None
|
33 |
+
|
34 |
+
def save_weather_data(city, data):
|
35 |
+
global weather_cache
|
36 |
+
timestamp = datetime.now()
|
37 |
+
weather_cache[city] = {
|
38 |
+
'data': data,
|
39 |
+
'timestamp': timestamp
|
40 |
+
}
|
41 |
+
|
42 |
+
def get_cached_weather_data(city):
|
43 |
+
global weather_cache, date_check
|
44 |
+
#Remove all weather_cache when more than 50 caches or There's a one-hour time difference.
|
45 |
+
if len(weather_cache) >= 50 or is_time_difference_greater_than_one_hour(date_check,datetime.now().strftime("%Y-%m-%d %H:%M:%S")):
|
46 |
+
weather_cache = {}
|
47 |
+
date_check = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
48 |
+
|
49 |
+
cached_data = weather_cache.get(city)
|
50 |
+
if cached_data:
|
51 |
+
# Check if the cached data is from today
|
52 |
+
if cached_data['timestamp'].date() == datetime.now().date():
|
53 |
+
return cached_data['data']
|
54 |
+
return None
|