Skip to content

Commit

Permalink
feat: Move Flask application to production-grade Gunicorn WSGI server
Browse files Browse the repository at this point in the history
- Add-on `dwd_global_rad_server_dev`: Migrated from Flask development server to Gunicorn WSGI server for production use.
- Updated Dockerfile to install and configure Gunicorn.
- Configured Gunicorn to log to stdout/stderr to ensure logs are captured by Docker.
- Retained existing logging setup within `app.py` for consistency.
  • Loading branch information
aschmere committed Jul 1, 2024
1 parent ceb38d0 commit 55f1845
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 68 deletions.
72 changes: 55 additions & 17 deletions dwd_global_rad_api_server_dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
FROM python:3.12-slim
# Stage 1: Build Stage
FROM continuumio/miniconda3:latest AS builder

# Set environment variables for PROJ
ENV PROJ_LIB=/usr/share/proj
ENV PROJ_DIR=/usr
ENV PATH="/opt/conda/bin:$PATH"

# Set work directory
WORKDIR /app

# Install system dependencies and clean up APT lists
RUN apt-get update && apt-get install -y \
build-essential \
libfreetype6-dev \
libpng-dev \
pkg-config \
sudo \
git \
ffmpeg \
procps \
libhdf5-dev \
gcc \
g++ \
ncdu \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --upgrade pip setuptools wheel

# Copy requirements file
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY app.py /app/app.py
COPY main.py /app/main.py
# Update Conda, create environment, install packages, and clean up caches
RUN conda update -n base -c defaults conda && \
conda install -y -c conda-forge python=3.12 hdf5 netcdf4 proj gdal geos && \
pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
conda clean --all -y && \
pip cache purge && \
rm -rf /opt/conda/pkgs

# Install Gunicorn in the base Conda environment
RUN pip install gunicorn

# Copy the rest of your application files
COPY . .

# Stage 2: Final Stage
FROM continuumio/miniconda3:latest

# Set environment variables for PROJ
ENV PROJ_LIB=/usr/share/proj
ENV PROJ_DIR=/usr
ENV PATH="/opt/conda/bin:$PATH"

# Set work directory
WORKDIR /app

EXPOSE 5002
# Run the application
CMD ["python", "app.py"]
# Copy the environment from the build stage
COPY --from=builder /opt/conda /opt/conda

# Copy application files
COPY --from=builder /app /app

# Expose the port the app runs on
EXPOSE 5001

# Run the application using Gunicorn with stdout/stderr logging
CMD ["gunicorn", "--workers", "1", "--threads", "1", "-b", "0.0.0.0:5001", "--capture-output", "--access-logfile", "-", "--error-logfile", "-", "app:app"]





27 changes: 4 additions & 23 deletions dwd_global_rad_api_server_dev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import logging
import dwd_global_radiation as dgr
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# Instantiate main object
objGlobalRadiation = dgr.GlobalRadiation()
# Configure logging with a custom formatter

class CustomFormatter(logging.Formatter):
def format(self, record):
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
Expand All @@ -33,15 +34,11 @@ def format(self, record):
@app.route('/locations/<name>/forecast/<int:number_of_hours>h', methods=['GET'])
def get_forecast_for_future_hour(name, number_of_hours):
try:
# Get the current datetime
datetime_input = datetime.now(timezone.utc)

# Get the location
location = objGlobalRadiation.get_location_by_name(name)
if not location:
return jsonify({'error': 'Location not found'}), 404

# Get the forecast for the specified future hour
forecast = location.get_forecast_for_future_hour(datetime_input, number_of_hours)
return jsonify(forecast)
except Exception as e:
Expand Down Expand Up @@ -75,8 +72,6 @@ def process():
except Exception as e:
app.logger.error(f"Error processing request: {e}")
return jsonify({'error': str(e)}), 500
# Instantiate main object
objGlobalRadiation = dgr.GlobalRadiation()

@app.route('/locations', methods=['GET'])
def get_locations():
Expand All @@ -85,7 +80,6 @@ def get_locations():
return jsonify({'error': 'No locations found'}), 404

serializable_locations = [loc.to_dict() for loc in locations]
print(json.dumps(serializable_locations, indent=2)) # Pretty-print the JSON data
return jsonify(serializable_locations)

@app.route('/locations', methods=['POST'])
Expand All @@ -95,16 +89,13 @@ def add_location():
latitude = data['latitude']
longitude = data['longitude']
try:
# Call the add_location method from the GlobalRadiation class
objGlobalRadiation.add_location(name=name, latitude=latitude, longitude=longitude)
return jsonify({'status': 'Location added successfully'}), 200
except ValueError as e:
# Handle the exception and return a 400 Bad Request response
return jsonify({'error': str(e)}), 400

@app.route('/locations/<name>', methods=['GET'])
def get_location_by_name(name):
"""Fetch a specific location by name."""
location = objGlobalRadiation.get_location_by_name(name)
if location is None:
return jsonify({'error': 'Location not found'}), 404
Expand All @@ -113,10 +104,7 @@ def get_location_by_name(name):

@app.route('/forecasts', methods=['GET'])
def fetch_forecasts():
"""Check if forecast data is available."""
# Fetch the forecast data from the DWD servers
objGlobalRadiation.fetch_forecasts()

forecast_data = objGlobalRadiation.forecast_data
if forecast_data is None:
return jsonify({'error': 'Forecast data not found'}), 404
Expand All @@ -125,13 +113,8 @@ def fetch_forecasts():

@app.route('/measurements', methods=['GET'])
def fetch_measurements():
"""Fetch the measurement data for a specific location by name."""

# Get the 'hours' query parameter from the request, default to 3 if not provided
hours = request.args.get('hours', default=3, type=int)
# Fetch the measurement data from the DWD servers
objGlobalRadiation.fetch_measurements(max_hour_age_of_measurement=hours)

measurement_data = objGlobalRadiation.measurement_data
if measurement_data is None:
return jsonify({'error': 'Forecast data not found'}), 404
Expand All @@ -140,17 +123,15 @@ def fetch_measurements():

@app.route('/locations/<name>', methods=['DELETE'])
def remove_location(name):
"""Remove a specific location by name."""
try:
objGlobalRadiation.remove_location(name)
return jsonify({'status': 'Location removed successfully'}), 200
except ValueError as e:
return jsonify({'error': str(e)}), 404
@app.route('/status', methods=['GET'])

@app.route('/status', methods=['GET'])
def get_status():
"""Endpoint to check if the API server is running."""
return '', 204 # No Content status
return '', 204

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)
2 changes: 1 addition & 1 deletion dwd_global_rad_api_server_dev/config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
name: DWD Global Rad Api Server (Development)
version: "0.1.0"
version: "0.1.1"
slug: dwd_global_rad_api_server_dev
description: DWD Global Rad Api Server
arch:
Expand Down
4 changes: 2 additions & 2 deletions dwd_global_rad_api_server_dev/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
matplotlib
cartopy
numpy==2.0.0
numpy<2
xarray
netCDF4==1.7.0
netCDF4
flask
requests
dwd_global_radiation
Expand Down

This file was deleted.

This file was deleted.

0 comments on commit 55f1845

Please sign in to comment.