From 968e536d3a53974a22ebc43f12a8b4a5762d31db Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 4 Apr 2026 00:00:43 -0400 Subject: [PATCH] fix(events): normalize naive datetimes to UTC in HTTP route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All datetime parsing now uses _parse_dt() which adds UTC tzinfo when none is present, matching the fix already applied in tools.py. This prevents asyncpg errors when comparing naive datetimes against TIMESTAMPTZ columns — the root cause of events not appearing in the calendar view. Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/routes/events.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/fabledassistant/routes/events.py b/src/fabledassistant/routes/events.py index 7c9ef4e..4108f3e 100644 --- a/src/fabledassistant/routes/events.py +++ b/src/fabledassistant/routes/events.py @@ -1,7 +1,7 @@ """Calendar events REST API.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from quart import Blueprint, g, jsonify, request @@ -11,6 +11,14 @@ import fabledassistant.services.events as events_svc events_bp = Blueprint("events", __name__, url_prefix="/api/events") +def _parse_dt(value: str) -> datetime: + """Parse ISO 8601 datetime string, ensuring UTC-awareness.""" + dt = datetime.fromisoformat(value.replace("Z", "+00:00")) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt + + def _get_current_user_id() -> int: return g.user.id @@ -23,8 +31,8 @@ async def list_events(): if not date_from_str or not date_to_str: return jsonify({"error": "from and to query params are required"}), 400 try: - date_from = datetime.fromisoformat(date_from_str) - date_to = datetime.fromisoformat(date_to_str) + date_from = _parse_dt(date_from_str) + date_to = _parse_dt(date_to_str) except ValueError: return jsonify({"error": "Invalid datetime format"}), 400 events = await events_svc.list_events( @@ -42,8 +50,8 @@ async def create_event(): if not data.get("title") or not data.get("start_dt"): return jsonify({"error": "title and start_dt are required"}), 400 try: - start_dt = datetime.fromisoformat(data["start_dt"]) - end_dt = datetime.fromisoformat(data["end_dt"]) if data.get("end_dt") else None + start_dt = _parse_dt(data["start_dt"]) + end_dt = _parse_dt(data["end_dt"]) if data.get("end_dt") else None except ValueError: return jsonify({"error": "Invalid datetime format"}), 400 event = await events_svc.create_event( @@ -90,7 +98,7 @@ async def update_event(event_id: int): for dt_field in ("start_dt", "end_dt"): if dt_field in data and data[dt_field]: try: - fields[dt_field] = datetime.fromisoformat(data[dt_field]) + fields[dt_field] = _parse_dt(data[dt_field]) except ValueError: return jsonify({"error": f"Invalid datetime for {dt_field}"}), 400 event = await events_svc.update_event(