From 30d6d0dac5f2832c977044d9157d60c954b3f2c4 Mon Sep 17 00:00:00 2001 From: aschmere Date: Sun, 30 Jun 2024 16:51:26 +0200 Subject: [PATCH] feat: Move Flask application to production-grade Gunicorn WSGI server - 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. --- dwd_global_rad_api_server_dev/Dockerfile | 57 ++++++++++++------- dwd_global_rad_api_server_dev/app.py | 27 ++------- dwd_global_rad_api_server_dev/config.yaml | 2 +- .../requirements.txt | 4 +- .../dwd_global_rad_api_server/finish | 4 -- .../services.d/dwd_global_rad_api_server/run | 21 ------- 6 files changed, 42 insertions(+), 73 deletions(-) delete mode 100644 dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/finish delete mode 100644 dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/run diff --git a/dwd_global_rad_api_server_dev/Dockerfile b/dwd_global_rad_api_server_dev/Dockerfile index e974a60..a4ba0f8 100644 --- a/dwd_global_rad_api_server_dev/Dockerfile +++ b/dwd_global_rad_api_server_dev/Dockerfile @@ -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"] + diff --git a/dwd_global_rad_api_server_dev/app.py b/dwd_global_rad_api_server_dev/app.py index 6afd143..67d62c4 100644 --- a/dwd_global_rad_api_server_dev/app.py +++ b/dwd_global_rad_api_server_dev/app.py @@ -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' @@ -33,15 +34,11 @@ def format(self, record): @app.route('/locations//forecast/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: @@ -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(): @@ -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']) @@ -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/', 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 @@ -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 @@ -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 @@ -140,17 +123,15 @@ def fetch_measurements(): @app.route('/locations/', 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) diff --git a/dwd_global_rad_api_server_dev/config.yaml b/dwd_global_rad_api_server_dev/config.yaml index 88ca1aa..d7fbf59 100644 --- a/dwd_global_rad_api_server_dev/config.yaml +++ b/dwd_global_rad_api_server_dev/config.yaml @@ -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: diff --git a/dwd_global_rad_api_server_dev/requirements.txt b/dwd_global_rad_api_server_dev/requirements.txt index 5048360..0c302db 100644 --- a/dwd_global_rad_api_server_dev/requirements.txt +++ b/dwd_global_rad_api_server_dev/requirements.txt @@ -1,8 +1,8 @@ matplotlib cartopy -numpy==2.0.0 +numpy<2 xarray -netCDF4==1.7.0 +netCDF4 flask requests dwd_global_radiation diff --git a/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/finish b/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/finish deleted file mode 100644 index f063245..0000000 --- a/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/finish +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/execlineb -S0 - -# Print an info message -echo "Application stopped." \ No newline at end of file diff --git a/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/run b/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/run deleted file mode 100644 index 6c462a9..0000000 --- a/dwd_global_rad_api_server_dev/services.d/dwd_global_rad_api_server/run +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -# Print an info message -echo "Starting the application..." - -# Activate the virtual environment -source /app/venv/bin/activate - -# Debug: Print which Python is being used -which python - -# Debug: Print Python version -python --version - -# Debug: List files in /app directory -ls -l /app - -# Start the application -exec python /app/app.py - -