Updating Files
هذا الالتزام موجود في:
3
.github/workflows/cicd.yaml
مباع
3
.github/workflows/cicd.yaml
مباع
@@ -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
مباع
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"]
|
||||||
|
|
||||||
|
|||||||
84
README.md
84
README.md
@@ -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
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)
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم