nun mal erste komplette Version mit API und Frontend (React !)
This commit is contained in:
20
api/.dockerignore
Normal file
20
api/.dockerignore
Normal file
@@ -0,0 +1,20 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.git/
|
||||
.gitignore
|
||||
README.md
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
tests/
|
||||
*.db
|
||||
*.sqlite
|
||||
21
api/Dockerfile
Normal file
21
api/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System-Abhängigkeiten installieren
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python-Abhängigkeiten installieren
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Anwendung kopieren
|
||||
COPY main.py .
|
||||
|
||||
# Port freigeben
|
||||
EXPOSE 8000
|
||||
|
||||
# Anwendung starten
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
337
api/main.py
Normal file
337
api/main.py
Normal file
@@ -0,0 +1,337 @@
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
import logging
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Umgebungsvariablen laden
|
||||
env_path = Path(__file__).parent.parent / '.env'
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
# Datenbank-Konfiguration
|
||||
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = int(os.getenv('DB_PORT', 5432))
|
||||
DB_NAME = os.getenv('DB_NAME', 'wetterstation')
|
||||
DB_USER = os.getenv('DB_USER')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
||||
|
||||
# FastAPI App erstellen
|
||||
app = FastAPI(
|
||||
title="Wetterstation API",
|
||||
description="API zum Auslesen von Wetterdaten",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# CORS Middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# Pydantic Models
|
||||
class WeatherData(BaseModel):
|
||||
id: int
|
||||
datetime: datetime
|
||||
temperature: Optional[float] = None
|
||||
humidity: Optional[int] = None
|
||||
pressure: Optional[float] = None
|
||||
wind_speed: Optional[float] = None
|
||||
wind_gust: Optional[float] = None
|
||||
wind_dir: Optional[float] = None
|
||||
rain: Optional[float] = None
|
||||
rain_rate: Optional[float] = None
|
||||
received_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class WeatherStats(BaseModel):
|
||||
avg_temperature: Optional[float] = None
|
||||
min_temperature: Optional[float] = None
|
||||
max_temperature: Optional[float] = None
|
||||
avg_humidity: Optional[float] = None
|
||||
avg_pressure: Optional[float] = None
|
||||
avg_wind_speed: Optional[float] = None
|
||||
max_wind_gust: Optional[float] = None
|
||||
total_rain: Optional[float] = None
|
||||
data_points: int
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
database: str
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
# Datenbankverbindung
|
||||
def get_db_connection():
|
||||
"""Erstellt eine Datenbankverbindung"""
|
||||
try:
|
||||
conn = psycopg.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
dbname=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
row_factory=dict_row
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error(f"Datenbankverbindungsfehler: {e}")
|
||||
raise HTTPException(status_code=500, detail="Datenbankverbindung fehlgeschlagen")
|
||||
|
||||
|
||||
# API Endpoints
|
||||
@app.get("/", tags=["General"])
|
||||
async def root():
|
||||
"""Root Endpoint"""
|
||||
return {
|
||||
"message": "Wetterstation API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health", response_model=HealthResponse, tags=["General"])
|
||||
async def health_check():
|
||||
"""Health Check Endpoint"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
conn.close()
|
||||
db_status = "connected"
|
||||
except Exception:
|
||||
db_status = "disconnected"
|
||||
|
||||
return {
|
||||
"status": "ok" if db_status == "connected" else "error",
|
||||
"database": db_status,
|
||||
"timestamp": datetime.now()
|
||||
}
|
||||
|
||||
|
||||
@app.get("/weather/latest", response_model=WeatherData, tags=["Weather Data"])
|
||||
async def get_latest_weather():
|
||||
"""Gibt die neuesten Wetterdaten zurück"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT * FROM weather_data
|
||||
ORDER BY datetime DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Keine Daten verfügbar")
|
||||
|
||||
return dict(result)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/current", response_model=WeatherData, tags=["Weather Data"])
|
||||
async def get_current_weather():
|
||||
"""Alias für /weather/latest - gibt aktuelle Wetterdaten zurück"""
|
||||
return await get_latest_weather()
|
||||
|
||||
|
||||
@app.get("/weather/history", response_model=List[WeatherData], tags=["Weather Data"])
|
||||
async def get_weather_history(
|
||||
hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück (max 168 = 7 Tage)"),
|
||||
limit: int = Query(1000, ge=1, le=10000, description="Maximale Anzahl Datensätze")
|
||||
):
|
||||
"""Gibt historische Wetterdaten der letzten X Stunden zurück"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT * FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
||||
ORDER BY datetime DESC
|
||||
LIMIT %s
|
||||
""", (hours, limit))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/range", response_model=List[WeatherData], tags=["Weather Data"])
|
||||
async def get_weather_by_date_range(
|
||||
start: datetime = Query(..., description="Startdatum (ISO 8601)"),
|
||||
end: datetime = Query(..., description="Enddatum (ISO 8601)"),
|
||||
limit: int = Query(10000, ge=1, le=50000, description="Maximale Anzahl Datensätze")
|
||||
):
|
||||
"""Gibt Wetterdaten für einen bestimmten Zeitraum zurück"""
|
||||
if start >= end:
|
||||
raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
|
||||
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT * FROM weather_data
|
||||
WHERE datetime BETWEEN %s AND %s
|
||||
ORDER BY datetime ASC
|
||||
LIMIT %s
|
||||
""", (start, end, limit))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/stats", response_model=WeatherStats, tags=["Statistics"])
|
||||
async def get_weather_statistics(
|
||||
hours: int = Query(24, ge=1, le=168, description="Zeitraum in Stunden für Statistiken")
|
||||
):
|
||||
"""Gibt aggregierte Statistiken für den angegebenen Zeitraum zurück"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(temperature) as avg_temperature,
|
||||
MIN(temperature) as min_temperature,
|
||||
MAX(temperature) as max_temperature,
|
||||
AVG(humidity) as avg_humidity,
|
||||
AVG(pressure) as avg_pressure,
|
||||
AVG(wind_speed) as avg_wind_speed,
|
||||
MAX(wind_gust) as max_wind_gust,
|
||||
SUM(rain) as total_rain,
|
||||
COUNT(*) as data_points
|
||||
FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
||||
""", (hours,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result or result['data_points'] == 0:
|
||||
raise HTTPException(status_code=404, detail="Keine Daten für den Zeitraum verfügbar")
|
||||
|
||||
return dict(result)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/daily", response_model=List[WeatherStats], tags=["Statistics"])
|
||||
async def get_daily_statistics(
|
||||
days: int = Query(7, ge=1, le=90, description="Anzahl Tage zurück (max 90)")
|
||||
):
|
||||
"""Gibt tägliche Statistiken für die letzten X Tage zurück"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
DATE(datetime) as date,
|
||||
AVG(temperature) as avg_temperature,
|
||||
MIN(temperature) as min_temperature,
|
||||
MAX(temperature) as max_temperature,
|
||||
AVG(humidity) as avg_humidity,
|
||||
AVG(pressure) as avg_pressure,
|
||||
AVG(wind_speed) as avg_wind_speed,
|
||||
MAX(wind_gust) as max_wind_gust,
|
||||
SUM(rain) as total_rain,
|
||||
COUNT(*) as data_points
|
||||
FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||
GROUP BY DATE(datetime)
|
||||
ORDER BY date DESC
|
||||
""", (days,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/temperature", response_model=List[dict], tags=["Weather Data"])
|
||||
async def get_temperature_data(
|
||||
hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück")
|
||||
):
|
||||
"""Gibt nur Temperatur-Zeitreihen zurück (optimiert für Diagramme)"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT datetime, temperature
|
||||
FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
||||
AND temperature IS NOT NULL
|
||||
ORDER BY datetime ASC
|
||||
""", (hours,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/wind", response_model=List[dict], tags=["Weather Data"])
|
||||
async def get_wind_data(
|
||||
hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück")
|
||||
):
|
||||
"""Gibt nur Wind-Daten zurück (Geschwindigkeit, Richtung, Böen)"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT datetime, wind_speed, wind_gust, wind_dir
|
||||
FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
||||
ORDER BY datetime ASC
|
||||
""", (hours,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/weather/rain", response_model=List[dict], tags=["Weather Data"])
|
||||
async def get_rain_data(
|
||||
hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück")
|
||||
):
|
||||
"""Gibt nur Regen-Daten zurück"""
|
||||
conn = get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT datetime, rain, rain_rate
|
||||
FROM weather_data
|
||||
WHERE datetime >= NOW() - make_interval(hours => %s)
|
||||
ORDER BY datetime ASC
|
||||
""", (hours,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
return [dict(row) for row in results]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
5
api/requirements.txt
Normal file
5
api/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.32.0
|
||||
psycopg[binary]>=3.2.0
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.10.0
|
||||
Reference in New Issue
Block a user