Updating Files
نجحت جميع الفحوصات
CI/CD Pipeline - Weather App 🌤️ / test (push) Successful in 41s
CI/CD Pipeline - Weather App 🌤️ / build_and_push_image (push) Successful in 46s
CI/CD Pipeline - Weather App 🌤️ / deploy (push) Successful in 7m15s

هذا الالتزام موجود في:
ahmedgamalyousef
2025-10-20 14:08:01 +03:00
الأصل d88db9e8e3
التزام feb25052b0
6 ملفات معدلة مع 145 إضافات و137 حذوفات

عرض الملف

@@ -74,4 +74,5 @@ jobs:
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}" run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
- name: 🚀 Deploy Application - name: 🚀 Deploy Application
run: $HOME/ghaymah/bin/gy resource app launch run: $HOME/ghaymah/bin/gy resource app launch

2
.gitignore مباع
عرض الملف

@@ -1 +1 @@
.history .history

عرض الملف

@@ -66,18 +66,18 @@ echo "✅ Image pushed: ${DOCKER_USERNAME}/${IMAGENAME}:${new_tag}"
# Ghaymah deployment # Ghaymah deployment
if command -v gy &> /dev/null; then if command -v gy &> /dev/null; then
echo "☁️ Setting up Ghaymah deployment..." echo "☁️ Setting up Ghaymah deployment..."
# Create project with the project name # Create project with the project name
echo "🔧 Creating project: ${project_name}" echo "🔧 Creating project: ${project_name}"
project_id=$(gy resource project create --set .name=${project_name} | awk '/ID:/ {print $NF}') project_id=$(gy resource project create --set .name=${project_name} | awk '/ID:/ {print $NF}')
if [ -z "$project_id" ]; then if [ -z "$project_id" ]; then
echo "❌ Failed to get project ID" echo "❌ Failed to get project ID"
exit 1 exit 1
fi fi
echo "✅ Project ID: ${project_id}" echo "✅ Project ID: ${project_id}"
# Create .ghaymah.json configuration using both names # Create .ghaymah.json configuration using both names
cat > .ghaymah.json <<EOF cat > .ghaymah.json <<EOF
{ {
@@ -101,16 +101,16 @@ if command -v gy &> /dev/null; then
"resourceTier": "t1" "resourceTier": "t1"
} }
EOF EOF
echo "✅ Configuration file created:" echo "✅ Configuration file created:"
echo " - Project: ${project_name} (ID: ${project_id})" echo " - Project: ${project_name} (ID: ${project_id})"
echo " - App: ${app_name}" echo " - App: ${app_name}"
echo " - Image: ${DOCKER_USERNAME}/${IMAGENAME}:${new_tag}" echo " - Image: ${DOCKER_USERNAME}/${IMAGENAME}:${new_tag}"
# Deploy to Ghaymah # Deploy to Ghaymah
echo "🚀 Deploying to Ghaymah..." echo "🚀 Deploying to Ghaymah..."
gy resource app launch gy resource app launch
echo "🎉 Deployment successful!" echo "🎉 Deployment successful!"
echo "📊 Project: ${project_name}" echo "📊 Project: ${project_name}"
echo "📱 App: ${app_name}" echo "📱 App: ${app_name}"

عرض الملف

@@ -15,5 +15,4 @@ RUN echo '<!DOCTYPE html><html><head><title>Weather App</title></head><body><h1>
EXPOSE 5000 EXPOSE 5000
CMD ["python", "app.py"] CMD ["python", "app.py"]

عرض الملف

@@ -1,24 +1,24 @@
# 🌤️ Complete GitHub Actions CI/CD Weather Forecast App on Ghaymah Cloud # 🌤️ Complete GitHub Actions CI/CD Weather Forecast App on Ghaymah Cloud
A simple modern **Flask-based weather forecast application** that fetches real-time weather data from [OpenWeatherMap](https://openweathermap.org/) and provides a beautiful, responsive UI. This project is **Dockerized** and supports **CI/CD deployment** to **Ghaymah Cloud** using the included shell script. A simple modern **Flask-based weather forecast application** that fetches real-time weather data from [OpenWeatherMap](https://openweathermap.org/) and provides a beautiful, responsive UI. This project is **Dockerized** and supports **CI/CD deployment** to **Ghaymah Cloud** using the included shell script.
## 🚀 Features ## 🚀 Features
- 🌍 Search weather for any city worldwide - 🌍 Search weather for any city worldwide
- 🌡️ Shows temperature, feels-like, humidity, pressure, wind speed, and visibility - 🌡️ Shows temperature, feels-like, humidity, pressure, wind speed, and visibility
- 🌅 Displays sunrise and sunset times - 🌅 Displays sunrise and sunset times
- 📊 Auto-updating with timestamp - 📊 Auto-updating with timestamp
- 🎨 Responsive and modern UI (HTML, CSS, JS embedded in Flask) - 🎨 Responsive and modern UI (HTML, CSS, JS embedded in Flask)
- 🐳 Dockerized for portability - 🐳 Dockerized for portability
- ☁️ CI/CD ready with Ghaymah Cloud - ☁️ CI/CD ready with Ghaymah Cloud
## 📂 Project Structure ## 📂 Project Structure
``` ```
. .
├── .github/workflows/cicd.yaml # GitHub Actions CI/CD workflow for my App ├── .github/workflows/cicd.yaml # GitHub Actions CI/CD workflow for my App
├── app.py # Flask application with weather API integration ├── app.py # Flask application with weather API integration
├── CICDpipeline.sh # CI/CD pipeline script for Docker + Ghaymah Cloud ├── CICDpipeline.sh # CI/CD pipeline script for Docker + Ghaymah Cloud
├── Dockerfile # Dockerfile to containerize the Flask app ├── Dockerfile # Dockerfile to containerize the Flask app
@@ -27,14 +27,14 @@ A simple modern **Flask-based weather forecast application** that fetches real-
└── README.md # Documentation └── README.md # Documentation
``` ```
## ⚙️ Prerequisites ## ⚙️ Prerequisites
Before running or deploying the app, ensure you have: Before running or deploying the app, ensure you have:
- [Python 3.9+](https://www.python.org/downloads/) - [Python 3.9+](https://www.python.org/downloads/)
- [Docker](https://docs.docker.com/get-docker/) - [Docker](https://docs.docker.com/get-docker/)
- [Ghaymah CLI (`gy`)](https://docs.ghaymah.io) installed and configured - [Ghaymah CLI (`gy`)](https://docs.ghaymah.io) installed and configured
- An [OpenWeatherMap API key](https://home.openweathermap.org/api_keys) - An [OpenWeatherMap API key](https://home.openweathermap.org/api_keys)
@@ -91,16 +91,16 @@ Before running or deploying the app, ensure you have:
## 🛠️ Local Development ## 🛠️ Local Development
1. Clone this repository: 1. Clone this repository:
```bash ```bash
git clone https://github.com/your-username/weather-app.git git clone https://github.com/your-username/weather-app.git
cd weather-app cd weather-app
``` ```
2. Create and activate a virtual environment: 2. Create and activate a virtual environment:
```bash ```bash
python -m venv venv python -m venv venv
@@ -108,71 +108,71 @@ Before running or deploying the app, ensure you have:
venv\Scripts\activate # Windows venv\Scripts\activate # Windows
``` ```
3. Install dependencies: 3. Install dependencies:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Run the app: 4. Run the app:
```bash ```bash
python app.py python app.py
``` ```
5. Open your browser at: 5. Open your browser at:
👉 http://127.0.0.1:5000 👉 http://127.0.0.1:5000
## 🐳 Running with Docker ## 🐳 Running with Docker
1. Build the Docker image: 1. Build the Docker image:
```bash ```bash
docker build -t weather-app:1 . docker build -t weather-app:1 .
``` ```
2. Run the container: 2. Run the container:
```bash ```bash
docker run -p 5000:5000 weather-app:1 docker run -p 5000:5000 weather-app:1
``` ```
3. Visit the app at: 3. Visit the app at:
👉 http://localhost:5000 👉 http://localhost:5000
# ☁️ Deployment to Ghaymah Cloud uisng **CICDpipeline.sh** Script # ☁️ Deployment to Ghaymah Cloud uisng **CICDpipeline.sh** Script
This project includes a **CICDpipeline.sh** script that: This project includes a **CICDpipeline.sh** script that:
- Builds and tags Docker images - Builds and tags Docker images
- Pushes them to Docker Hub - Pushes them to Docker Hub
- Creates a `.ghaymah.json` config - Creates a `.ghaymah.json` config
- Deploys the app to **Ghaymah Cloud** - Deploys the app to **Ghaymah Cloud**
### 🔧 Steps ### 🔧 Steps
1. Make the script executable: 1. Make the script executable:
```bash ```bash
chmod +x CICDpipeline.sh chmod +x CICDpipeline.sh
``` ```
2. Run the deployment script: 2. Run the deployment script:
```bash ```bash
./CICDpipeline.sh ./CICDpipeline.sh
``` ```
3. Follow the prompts: 3. Follow the prompts:
- Enter your **Ghaymah Project Name** - Enter your **Ghaymah Project Name**
- Enter your **App Name** - Enter your **App Name**
4. If Ghaymah CLI is installed, the app will be deployed automatically. 4. If Ghaymah CLI is installed, the app will be deployed automatically.
✅ Your app will be accessible at: ✅ Your app will be accessible at:
``` ```
https://<your-app-name>.hosted.ghaymah.systems https://<your-app-name>.hosted.ghaymah.systems
@@ -180,5 +180,3 @@ https://<your-app-name>.hosted.ghaymah.systems
## 🎥 Project Record ## 🎥 Project Record
[Watch the Demo](https://app.gitpasha.com/AhmedGamalYousef/Complete-CI-CD-Project-pipeline-on-Ghaymah-Cloud/src/branch/main/demo/CICDonGhaymahCloud.mp4) [Watch the Demo](https://app.gitpasha.com/AhmedGamalYousef/Complete-CI-CD-Project-pipeline-on-Ghaymah-Cloud/src/branch/main/demo/CICDonGhaymahCloud.mp4)

176
app.py
عرض الملف

@@ -1,13 +1,14 @@
from flask import Flask, request, jsonify, render_template_string
import requests
import os import os
from datetime import datetime from datetime import datetime
import requests
from flask import Flask, jsonify, render_template_string, request
app = Flask(__name__) app = Flask(__name__)
# Get API key from environment variable (preferred) or use demo key # Get API key from environment variable (preferred) or use demo key
API_KEY = os.getenv('API_KEY', '6cf356ceb2ec0ff941855d4a43144e0e') API_KEY = os.getenv("API_KEY", "6cf356ceb2ec0ff941855d4a43144e0e")
BASE_URL = 'https://api.openweathermap.org/data/2.5/weather' BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
# HTML Template with embedded CSS and JavaScript - MUST BE DEFINED BEFORE ROUTES # HTML Template with embedded CSS and JavaScript - MUST BE DEFINED BEFORE ROUTES
HTML_TEMPLATE = """ HTML_TEMPLATE = """
@@ -24,7 +25,7 @@ HTML_TEMPLATE = """
box-sizing: border-box; box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
} }
body { body {
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
min-height: 100vh; min-height: 100vh;
@@ -33,7 +34,7 @@ HTML_TEMPLATE = """
align-items: center; align-items: center;
padding: 20px; padding: 20px;
} }
.container { .container {
max-width: 800px; max-width: 800px;
width: 100%; width: 100%;
@@ -42,29 +43,29 @@ HTML_TEMPLATE = """
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
} }
.header { .header {
background: #0984e3; background: #0984e3;
color: white; color: white;
padding: 30px; padding: 30px;
text-align: center; text-align: center;
} }
.header h1 { .header h1 {
font-size: 2.5rem; font-size: 2.5rem;
margin-bottom: 10px; margin-bottom: 10px;
} }
.header p { .header p {
opacity: 0.9; opacity: 0.9;
} }
.search-container { .search-container {
padding: 30px; padding: 30px;
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
.search-input { .search-input {
flex: 1; flex: 1;
padding: 15px 20px; padding: 15px 20px;
@@ -74,11 +75,11 @@ HTML_TEMPLATE = """
outline: none; outline: none;
transition: border-color 0.3s; transition: border-color 0.3s;
} }
.search-input:focus { .search-input:focus {
border-color: #0984e3; border-color: #0984e3;
} }
.search-btn { .search-btn {
background: #0984e3; background: #0984e3;
color: white; color: white;
@@ -90,27 +91,27 @@ HTML_TEMPLATE = """
font-weight: 600; font-weight: 600;
transition: background 0.3s; transition: background 0.3s;
} }
.search-btn:hover { .search-btn:hover {
background: #0770c4; background: #0770c4;
} }
.weather-info { .weather-info {
padding: 0 30px 30px; padding: 0 30px 30px;
display: none; display: none;
} }
.current-weather { .current-weather {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
} }
.city-name { .city-name {
font-size: 2rem; font-size: 2rem;
margin-bottom: 10px; margin-bottom: 10px;
color: #2d3436; color: #2d3436;
} }
.weather-main { .weather-main {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -118,49 +119,49 @@ HTML_TEMPLATE = """
gap: 20px; gap: 20px;
margin: 20px 0; margin: 20px 0;
} }
.temperature { .temperature {
font-size: 4rem; font-size: 4rem;
font-weight: 300; font-weight: 300;
color: #0984e3; color: #0984e3;
} }
.weather-icon { .weather-icon {
width: 100px; width: 100px;
height: 100px; height: 100px;
} }
.description { .description {
font-size: 1.5rem; font-size: 1.5rem;
color: #636e72; color: #636e72;
margin-bottom: 20px; margin-bottom: 20px;
} }
.details { .details {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px; gap: 20px;
} }
.detail-card { .detail-card {
background: #f8f9fa; background: #f8f9fa;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
text-align: center; text-align: center;
} }
.detail-title { .detail-title {
font-size: 0.9rem; font-size: 0.9rem;
color: #636e72; color: #636e72;
margin-bottom: 5px; margin-bottom: 5px;
} }
.detail-value { .detail-value {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
color: #2d3436; color: #2d3436;
} }
.error-message { .error-message {
background: #ff7675; background: #ff7675;
color: white; color: white;
@@ -170,13 +171,13 @@ HTML_TEMPLATE = """
margin: 0 30px 30px; margin: 0 30px 30px;
display: none; display: none;
} }
.loading { .loading {
text-align: center; text-align: center;
padding: 30px; padding: 30px;
display: none; display: none;
} }
.spinner { .spinner {
border: 5px solid #f3f3f3; border: 5px solid #f3f3f3;
border-top: 5px solid #0984e3; border-top: 5px solid #0984e3;
@@ -186,12 +187,12 @@ HTML_TEMPLATE = """
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 20px; margin: 0 auto 20px;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
.footer { .footer {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
@@ -199,20 +200,20 @@ HTML_TEMPLATE = """
font-size: 0.9rem; font-size: 0.9rem;
border-top: 1px solid #eee; border-top: 1px solid #eee;
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.header h1 { .header h1 {
font-size: 2rem; font-size: 2rem;
} }
.search-container { .search-container {
flex-direction: column; flex-direction: column;
} }
.temperature { .temperature {
font-size: 3rem; font-size: 3rem;
} }
.weather-icon { .weather-icon {
width: 80px; width: 80px;
height: 80px; height: 80px;
@@ -226,19 +227,19 @@ HTML_TEMPLATE = """
<h1>Weather Forecast</h1> <h1>Weather Forecast</h1>
<p>Get real-time weather information for any city worldwide</p> <p>Get real-time weather information for any city worldwide</p>
</div> </div>
<div class="search-container"> <div class="search-container">
<input type="text" class="search-input" id="cityInput" placeholder="Enter city name..." autocomplete="off"> <input type="text" class="search-input" id="cityInput" placeholder="Enter city name..." autocomplete="off">
<button class="search-btn" id="searchBtn">Search</button> <button class="search-btn" id="searchBtn">Search</button>
</div> </div>
<div class="error-message" id="errorMessage"></div> <div class="error-message" id="errorMessage"></div>
<div class="loading" id="loading"> <div class="loading" id="loading">
<div class="spinner"></div> <div class="spinner"></div>
<p>Fetching weather data...</p> <p>Fetching weather data...</p>
</div> </div>
<div class="weather-info" id="weatherInfo"> <div class="weather-info" id="weatherInfo">
<div class="current-weather"> <div class="current-weather">
<h2 class="city-name" id="cityName">-</h2> <h2 class="city-name" id="cityName">-</h2>
@@ -248,7 +249,7 @@ HTML_TEMPLATE = """
</div> </div>
<p class="description" id="description">-</p> <p class="description" id="description">-</p>
</div> </div>
<div class="details"> <div class="details">
<div class="detail-card"> <div class="detail-card">
<div class="detail-title">Feels Like</div> <div class="detail-title">Feels Like</div>
@@ -276,10 +277,10 @@ HTML_TEMPLATE = """
</div> </div>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<p>Last updated from OpenSenseMap: <span id="timestamp">-</span></p> <p>Last updated from OpenSenseMap: <span id="timestamp">-</span></p>
<p>© 2025 Ahmed Gamal Yousef <p> <p>© 2025 Ahmed Gamal Yousef <p>
</div> </div>
</div> </div>
@@ -290,7 +291,7 @@ HTML_TEMPLATE = """
const weatherInfo = document.getElementById('weatherInfo'); const weatherInfo = document.getElementById('weatherInfo');
const errorMessage = document.getElementById('errorMessage'); const errorMessage = document.getElementById('errorMessage');
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
// Elements to update // Elements to update
const cityName = document.getElementById('cityName'); const cityName = document.getElementById('cityName');
const temperature = document.getElementById('temperature'); const temperature = document.getElementById('temperature');
@@ -303,21 +304,21 @@ HTML_TEMPLATE = """
const visibility = document.getElementById('visibility'); const visibility = document.getElementById('visibility');
const sunriseSunset = document.getElementById('sunriseSunset'); const sunriseSunset = document.getElementById('sunriseSunset');
const timestamp = document.getElementById('timestamp'); const timestamp = document.getElementById('timestamp');
// Search weather function // Search weather function
function searchWeather() { function searchWeather() {
const city = cityInput.value.trim(); const city = cityInput.value.trim();
if (!city) { if (!city) {
showError('Please enter a city name'); showError('Please enter a city name');
return; return;
} }
// Show loading, hide weather and error // Show loading, hide weather and error
loading.style.display = 'block'; loading.style.display = 'block';
weatherInfo.style.display = 'none'; weatherInfo.style.display = 'none';
errorMessage.style.display = 'none'; errorMessage.style.display = 'none';
// Fetch weather data // Fetch weather data
fetch(`/weather?city=${encodeURIComponent(city)}`) fetch(`/weather?city=${encodeURIComponent(city)}`)
.then(response => { .then(response => {
@@ -342,7 +343,7 @@ HTML_TEMPLATE = """
visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`; visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`;
sunriseSunset.textContent = `${data.sunrise} / ${data.sunset}`; sunriseSunset.textContent = `${data.sunrise} / ${data.sunset}`;
timestamp.textContent = data.timestamp; timestamp.textContent = data.timestamp;
// Show weather info, hide loading // Show weather info, hide loading
weatherInfo.style.display = 'block'; weatherInfo.style.display = 'block';
loading.style.display = 'none'; loading.style.display = 'none';
@@ -352,23 +353,23 @@ HTML_TEMPLATE = """
loading.style.display = 'none'; loading.style.display = 'none';
}); });
} }
// Show error message // Show error message
function showError(message) { function showError(message) {
errorMessage.textContent = message; errorMessage.textContent = message;
errorMessage.style.display = 'block'; errorMessage.style.display = 'block';
weatherInfo.style.display = 'none'; weatherInfo.style.display = 'none';
} }
// Event listeners // Event listeners
searchBtn.addEventListener('click', searchWeather); searchBtn.addEventListener('click', searchWeather);
cityInput.addEventListener('keypress', function(e) { cityInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
searchWeather(); searchWeather();
} }
}); });
// Try to get weather for a default city on load // Try to get weather for a default city on load
cityInput.value = 'London'; cityInput.value = 'London';
searchWeather(); searchWeather();
@@ -378,33 +379,38 @@ HTML_TEMPLATE = """
</html> </html>
""" """
def get_weather(city): def get_weather(city):
"""Fetch weather data from OpenWeatherMap API""" """Fetch weather data from OpenWeatherMap API"""
try: try:
url = f"{BASE_URL}?q={city}&appid={API_KEY}&units=metric" url = f"{BASE_URL}?q={city}&appid={API_KEY}&units=metric"
response = requests.get(url, timeout=10) response = requests.get(url, timeout=10)
response.raise_for_status() # Raises an HTTPError for bad responses response.raise_for_status() # Raises an HTTPError for bad responses
data = response.json() data = response.json()
main = data['main'] main = data["main"]
weather = data['weather'][0] weather = data["weather"][0]
sys = data.get('sys', {}) sys = data.get("sys", {})
return { return {
'city': data['name'], "city": data["name"],
'country': sys.get('country', ''), "country": sys.get("country", ""),
'temperature': round(main['temp']), "temperature": round(main["temp"]),
'feels_like': round(main['feels_like']), "feels_like": round(main["feels_like"]),
'description': weather['description'].title(), "description": weather["description"].title(),
'humidity': main['humidity'], "humidity": main["humidity"],
'pressure': main['pressure'], "pressure": main["pressure"],
'wind_speed': round(data['wind']['speed'], 1), "wind_speed": round(data["wind"]["speed"], 1),
'wind_deg': data['wind'].get('deg', 0), "wind_deg": data["wind"].get("deg", 0),
'visibility': data.get('visibility', 0), "visibility": data.get("visibility", 0),
'icon': weather['icon'], "icon": weather["icon"],
'sunrise': datetime.fromtimestamp(sys.get('sunrise', 0)).strftime('%H:%M') if sys.get('sunrise') else 'N/A', "sunrise": datetime.fromtimestamp(sys.get("sunrise", 0)).strftime("%H:%M")
'sunset': datetime.fromtimestamp(sys.get('sunset', 0)).strftime('%H:%M') if sys.get('sunset') else 'N/A', if sys.get("sunrise")
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') else "N/A",
"sunset": datetime.fromtimestamp(sys.get("sunset", 0)).strftime("%H:%M")
if sys.get("sunset")
else "N/A",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
} }
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"API request failed: {e}") print(f"API request failed: {e}")
@@ -413,29 +419,33 @@ def get_weather(city):
print(f"Error parsing weather data: {e}") print(f"Error parsing weather data: {e}")
return None return None
@app.route('/')
@app.route("/")
def home(): def home():
return render_template_string(HTML_TEMPLATE) return render_template_string(HTML_TEMPLATE)
@app.route('/weather', methods=['GET', 'POST'])
@app.route("/weather", methods=["GET", "POST"])
def weather(): def weather():
if request.method == 'POST': if request.method == "POST":
city = request.form.get('city') city = request.form.get("city")
else: else:
city = request.args.get('city') city = request.args.get("city")
if not city: if not city:
return jsonify({'error': 'City parameter is required'}), 400 return jsonify({"error": "City parameter is required"}), 400
weather_data = get_weather(city) weather_data = get_weather(city)
if weather_data: if weather_data:
return jsonify(weather_data) return jsonify(weather_data)
else: else:
return jsonify({'error': 'City not found or API request failed'}), 404 return jsonify({"error": "City not found or API request failed"}), 404
@app.route('/health')
@app.route("/health")
def health(): def health():
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()}) return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False) if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)