diff --git a/dwd_global_rad_api_server_dev/Dockerfile b/dwd_global_rad_api_server_dev/Dockerfile index e974a60..e9ae7f4 100644 --- a/dwd_global_rad_api_server_dev/Dockerfile +++ b/dwd_global_rad_api_server_dev/Dockerfile @@ -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"] + + + + + 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 - -