HexaWetter v1.2.0: Security, Admin, Radar, UI

This commit is contained in:
TheOnlyMace
2026-06-18 23:59:08 +02:00
parent 7ba9687619
commit 3589f7fdcf
72 changed files with 695 additions and 409 deletions

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
import re
from datetime import datetime, timedelta, timezone
from app.config import RADAR_PRODUCT, RADAR_RETENTION_HOURS, RAW_RADAR_DIR
from app.database import upsert_radar_meta
from app.http_client import get_client
from app.services.dwd_radar import RADAR_FILE_RE, fetch_radar_index
_FILENAME_RE = re.compile(r"^[a-z0-9_-]+$", re.IGNORECASE)
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def cleanup_old_radar_files() -> None:
cutoff = utc_now() - timedelta(hours=RADAR_RETENTION_HOURS)
if not RAW_RADAR_DIR.exists():
return
for path in RAW_RADAR_DIR.rglob("*"):
if not path.is_file() or not RADAR_FILE_RE.fullmatch(path.name):
continue
modified = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
if modified < cutoff:
path.unlink(missing_ok=True)
async def sync_radar_files(product: str = RADAR_PRODUCT, limit: int = 6) -> dict:
if not _FILENAME_RE.fullmatch(product):
raise ValueError("Invalid product")
latest = await fetch_radar_index(product=product, limit=limit)
target_dir = RAW_RADAR_DIR / "composite" / product
target_dir.mkdir(parents=True, exist_ok=True)
downloaded: list[str] = []
skipped: list[str] = []
client = get_client()
for item in latest["files"]:
out = target_dir / item["name"]
if out.exists() and out.stat().st_size > 0:
skipped.append(item["name"])
continue
response = await client.get(item["url"], timeout=90.0)
response.raise_for_status()
out.write_bytes(response.content)
downloaded.append(item["name"])
modified = datetime.fromtimestamp(out.stat().st_mtime, timezone.utc)
await upsert_radar_meta(product, item["name"], out.stat().st_size, modified, "pending")
cleanup_old_radar_files()
return {
"product": product,
"target_dir": str(target_dir),
"downloaded": downloaded,
"skipped": skipped,
}

View File

@@ -86,11 +86,11 @@ async def fetch_all_warnings() -> list[dict[str, Any]]:
client = get_client()
response = await client.get(DWD_WARNINGS_URL, timeout=10.0)
response.raise_for_status()
text = response.text
if text.startswith("warnWetter.loadWarnings("):
text = text[text.index("(") + 1 : text.rindex(")")]
payload = json.loads(text)
response.raise_for_status()
text = response.text
if text.startswith("warnWetter.loadWarnings("):
text = text[text.index("(") + 1 : text.rindex(")")]
payload = json.loads(text)
warnings = _flatten_warnings(payload)
await cache_set(cache_key, warnings, CACHE_TTL_WARNINGS)

View File

@@ -4,6 +4,7 @@ import re
from datetime import datetime, timedelta, timezone
from xml.etree import ElementTree as ET
from app.cache import cache_get, cache_set
from app.http_client import get_client
from app.config import CACHE_TTL_WMS_TIMES, DWD_WMS_URL
@@ -59,10 +60,10 @@ async def fetch_wms_time_steps(layer: str, minutes: int = 120) -> dict:
params = {"service": "WMS", "version": "1.3.0", "request": "GetCapabilities"}
client = get_client()
response = await client.get(DWD_WMS_URL, params=params, timeout=15.0)
response.raise_for_status()
dimension = _find_layer_dimension(response.text, layer)
if not dimension:
raise ValueError(f"No TIME dimension found for layer {layer}")
response.raise_for_status()
dimension = _find_layer_dimension(response.text, layer)
if not dimension:
raise ValueError(f"No TIME dimension found for layer {layer}")
if "/" in dimension:
start, end, step_minutes = _parse_time_dimension(dimension)