From c4fbfe53a3041cdfdeda2fd47526818ca7185291 Mon Sep 17 00:00:00 2001 From: Scott Wallace Date: Tue, 25 Feb 2025 18:19:38 +0000 Subject: [PATCH] Bring everything up-to-date --- Dockerfile | 21 ++++---- main.py | 139 ++++++++++++++++++++++++++--------------------------- 2 files changed, 78 insertions(+), 82 deletions(-) diff --git a/Dockerfile b/Dockerfile index a8692bc..2c0b6d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,19 @@ -FROM python:3.8-slim-buster - +FROM python:3.11-alpine AS base # Keeps Python from generating .pyc files in the container -ENV PYTHONDONTWRITEBYTECODE 1 - -# Turns off buffering for easier container logging -ENV PYTHONUNBUFFERED 1 +FROM base AS build ADD requirements.txt . -RUN python -m pip install -r requirements.txt +RUN python -m pip install --user -r requirements.txt +FROM base AS app +RUN adduser -h /app -D appuser +COPY --from=build --chown=appuser /root/.local /app/.local WORKDIR /app -COPY main.py /app - -RUN useradd appuser && chown -R appuser /app +COPY --chown=appuser main.py /app USER appuser +FROM app +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 EXPOSE 5000 - ENTRYPOINT ["python", "main.py"] diff --git a/main.py b/main.py index 6e2f247..9d06205 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ Present observational weather data to Prometheus import json import os -from typing import Dict, List, TypedDict +from typing import TypedDict import requests from flask import Flask, Response @@ -21,33 +21,28 @@ class PromData(TypedDict): """ key: str - labels: Dict[str, str] + labels: dict[str, str] type: str value: float -def fetch_weather_data(location: int, apikey: str) -> Dict[str, Dict[str, float]]: +def fetch_weather_data(location: str, apikey: str) -> dict[str, dict[str, float]]: """ Fetch current data from the Met Office for the provided postcode """ - obs_data = requests.get( - f'http://api.weatherapi.com/v1/current.json?' - f'key={apikey}&' - f'q={location}&' - f'aqi=yes' - ) + obs_data = requests.get(f"http://api.weatherapi.com/v1/current.json?key={apikey}&q={location}&aqi=yes", timeout=30) return dict(json.loads(obs_data.content)) -@app.route('/metrics') +@app.route("/metrics") def metrics() -> Response: """ Output Prometheus-style metrics """ - apikey = os.environ.get('WEATHER_APIKEY') - location = os.environ.get('WEATHER_LOCATION') + apikey = os.environ.get("WEATHER_APIKEY") + location = os.environ.get("WEATHER_LOCATION") if not location: return Response( @@ -55,88 +50,88 @@ def metrics() -> Response: status=500, ) - latest_data = fetch_weather_data(int(location), str(apikey))['current'] + latest_data = fetch_weather_data(location, str(apikey))["current"] - ret_data: List[PromData] = [ + ret_data: list[PromData] = [ { - 'key': 'sensor_weather_outdoor_temperature_celsius', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_temperature_celsius", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['temp_c']), + "type": "gauge", + "value": float(latest_data["temp_c"]), }, { - 'key': 'sensor_weather_outdoor_humidity_percent', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_humidity_percent", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['humidity']), + "type": "gauge", + "value": float(latest_data["humidity"]), }, { - 'key': 'sensor_weather_outdoor_uv', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_uv", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['uv']), + "type": "gauge", + "value": float(latest_data["uv"]), }, { - 'key': 'sensor_weather_outdoor_cloud_percent', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_cloud_percent", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['cloud']), + "type": "gauge", + "value": float(latest_data["cloud"]), }, { - 'key': 'sensor_weather_outdoor_precip', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_precip", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['precip_mm']), + "type": "gauge", + "value": float(latest_data["precip_mm"]), }, { - 'key': 'sensor_weather_outdoor_wind_speed', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_wind_speed", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['wind_kph']), + "type": "gauge", + "value": float(latest_data["wind_kph"]), }, { - 'key': 'sensor_weather_outdoor_wind_gust', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_wind_gust", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['gust_kph']), + "type": "gauge", + "value": float(latest_data["gust_kph"]), }, { - 'key': 'sensor_weather_outdoor_wind_direction', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_wind_direction", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['wind_degree']), + "type": "gauge", + "value": float(latest_data["wind_degree"]), }, { - 'key': 'sensor_weather_outdoor_temp_feel_c', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_temp_feel_c", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['feelslike_c']), + "type": "gauge", + "value": float(latest_data["feelslike_c"]), }, { - 'key': 'sensor_weather_outdoor_pressure', - 'labels': { - 'location': location, + "key": "sensor_weather_outdoor_pressure", + "labels": { + "location": location, }, - 'type': 'gauge', - 'value': float(latest_data['pressure_mb']), + "type": "gauge", + "value": float(latest_data["pressure_mb"]), }, ] @@ -146,17 +141,19 @@ def metrics() -> Response: ret_strs.append(f'# TYPE {item["key"]} {item["type"]}') ret_strs.append( str(item["key"]) - + '{' + + "{" + " ".join([f'{key}="{val}"' for key, val in item["labels"].items()]) - + '} ' - + str(item["value"]) + + "} " + + str( + item["value"], + ) ) - resp = Response('\n'.join(ret_strs)) - resp.headers['Content-type'] = 'text/plain' + resp = Response("\n".join(ret_strs)) + resp.headers["Content-type"] = "text/plain" return resp -if __name__ == '__main__': - app.run(host='0.0.0.0') +if __name__ == "__main__": + app.run(host="0.0.0.0")