duongve commited on
Commit
320a5a6
·
verified ·
1 Parent(s): 0652687

Upload 8 files

Browse files
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