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 Jun 30, 2024
1 parent ceb38d0 commit 30d6d0d
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 73 deletions.
57 changes: 35 additions & 22 deletions dwd_global_rad_api_server_dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
FROM python:3.12-slim

RUN apt-get update && apt-get install -y \
build-essential \
libfreetype6-dev \
libpng-dev \
pkg-config \
sudo \
git \
ffmpeg \
procps \
libhdf5-dev \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --upgrade pip setuptools wheel
# Use Miniconda as the base image
FROM continuumio/miniconda3:latest

COPY requirements.txt .
RUN pip install -r requirements.txt
# Set shell
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

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

COPY app.py /app/app.py
COPY main.py /app/main.py
# Set work directory
WORKDIR /app

EXPOSE 5002
# Run the application
CMD ["python", "app.py"]
# Copy requirements file
COPY requirements.txt .

# Create and activate a Conda environment, then install Python dependencies
RUN conda update -n base -c defaults conda && \
conda create -n myenv python=3.12 && \
echo "source activate myenv" > ~/.bashrc && \
/bin/bash -c "source activate myenv" && \
conda install -y -n myenv -c conda-forge hdf5 netcdf4 proj gdal geos && \
/bin/bash -c "source activate myenv" && \
pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

# Install Gunicorn in the Conda environment
RUN /bin/bash -c "source activate myenv" && pip install gunicorn

# Copy the rest of your application files
COPY . .

# Expose the port the app runs on
EXPOSE 5001

# Run the application using Gunicorn with stdout/stderr logging
CMD ["/bin/bash", "-c", "source activate myenv && exec gunicorn -w 4 -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 30d6d0d

Please sign in to comment.