fix timezone shenanigans
This commit is contained in:
parent
75dbaebccd
commit
e9f63e8658
2 changed files with 62 additions and 22 deletions
|
@ -22,7 +22,8 @@ services:
|
||||||
- "8004:8000"
|
- "8004:8000"
|
||||||
environment:
|
environment:
|
||||||
# FILL THESE IN
|
# FILL THESE IN
|
||||||
BEAVERHABITS_URL: ""
|
TIMEZONE: EST
|
||||||
|
BEAVERHABITS_URL: "" # no path, just the base URL
|
||||||
BEAVERHABITS_USERNAME: "" # yes i know this is bad
|
BEAVERHABITS_USERNAME: "" # yes i know this is bad
|
||||||
BEAVERHABITS_PASSWORD: "" # however, i don't care
|
BEAVERHABITS_PASSWORD: "" # however, i don't care
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
79
app.py
79
app.py
|
@ -1,7 +1,8 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
from datetime import datetime, timedelta, date
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from flask import Flask, request, Response, abort, jsonify
|
from flask import Flask, request, Response, abort, jsonify
|
||||||
|
@ -12,12 +13,14 @@ import requests
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Configuration via ENV
|
# Configuration via ENV
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
BEAVER_URL = os.getenv("BEAVERHABITS_URL", "")
|
BEAVER_URL = os.getenv("BEAVERHABITS_URL", "http://beaverhabits.lan")
|
||||||
USERNAME = os.getenv("BEAVERHABITS_USERNAME", "")
|
USERNAME = os.getenv("BEAVERHABITS_USERNAME", "")
|
||||||
PASSWORD = os.getenv("BEAVERHABITS_PASSWORD", "")
|
PASSWORD = os.getenv("BEAVERHABITS_PASSWORD", "")
|
||||||
# How long to reuse a token (secs)
|
# How long to reuse a token (secs)
|
||||||
TOKEN_TTL = int(os.getenv("BEAVERHABITS_TOKEN_TTL", "3600"))
|
TOKEN_TTL = int(os.getenv("BEAVERHABITS_TOKEN_TTL", "3600"))
|
||||||
|
|
||||||
|
TZ = ZoneInfo(os.getenv("TIMEZONE", "UTC"))
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@ -35,6 +38,10 @@ NS = {"D": "DAV:", "C": "urn:ietf:params:xml:ns:caldav"}
|
||||||
for p, uri in NS.items():
|
for p, uri in NS.items():
|
||||||
ET.register_namespace(p, uri)
|
ET.register_namespace(p, uri)
|
||||||
|
|
||||||
|
def to_bytes(elem: ET.Element) -> bytes:
|
||||||
|
"""Serialize an ElementTree Element to UTF-8 XML with declaration."""
|
||||||
|
return ET.tostring(elem, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Beaver Habits token cache with TTL
|
# Beaver Habits token cache with TTL
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@ -73,51 +80,85 @@ def beaver_headers() -> dict[str,str]:
|
||||||
return {"Accept": "application/json",
|
return {"Accept": "application/json",
|
||||||
"Authorization": f"Bearer {get_token()}"}
|
"Authorization": f"Bearer {get_token()}"}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Utility: Datetime functions
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def local_now() -> datetime:
|
||||||
|
"""
|
||||||
|
Return “now” as an aware datetime in our configured TZ.
|
||||||
|
"""
|
||||||
|
return datetime.now(TZ)
|
||||||
|
|
||||||
|
|
||||||
|
def next_midnight_local(now: datetime) -> datetime:
|
||||||
|
"""
|
||||||
|
Given an aware datetime ‘now’, return tomorrow at 00:00 in the same TZ.
|
||||||
|
"""
|
||||||
|
tomorrow = now.date() + timedelta(days=1)
|
||||||
|
# Year, month, day at midnight
|
||||||
|
return datetime(tomorrow.year, tomorrow.month, tomorrow.day, tzinfo=TZ)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Utility: build iCalendar VTODO
|
# Utility: build iCalendar VTODO
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
def make_vtodo(uid: str, summary: str, completed: bool) -> bytes:
|
def make_vtodo(uid: str, summary: str, completed: bool) -> bytes:
|
||||||
|
"""
|
||||||
|
Construct a VCALENDAR/VTODO where:
|
||||||
|
- DTSTAMP = local_now()
|
||||||
|
- DUE = next_midnight_local(local_now())
|
||||||
|
"""
|
||||||
|
now = local_now()
|
||||||
|
due = next_midnight_local(now)
|
||||||
|
|
||||||
cal = Calendar()
|
cal = Calendar()
|
||||||
cal.add("prodid", "-//CalDAV⇆BeaverHabits Proxy//EN")
|
cal.add("prodid", "-//CalDAV⇆BeaverHabits Proxy//EN")
|
||||||
cal.add("version", "2.0")
|
cal.add("version", "2.0")
|
||||||
|
|
||||||
todo = Todo()
|
todo = Todo()
|
||||||
todo.add("uid", uid)
|
todo.add("uid", uid)
|
||||||
todo.add("summary", summary)
|
todo.add("summary", summary)
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
todo.add("dtstamp", now)
|
todo.add("dtstamp", now)
|
||||||
todo.add("due", now + datetime.timedelta(days=1))
|
# todo.add("due", due) # causes problems
|
||||||
todo.add("status", "COMPLETED" if completed else "NEEDS-ACTION")
|
todo.add("status", "COMPLETED" if completed else "NEEDS-ACTION")
|
||||||
|
|
||||||
cal.add_component(todo)
|
cal.add_component(todo)
|
||||||
return cal.to_ical()
|
return cal.to_ical()
|
||||||
|
|
||||||
def to_bytes(elem: ET.Element) -> bytes:
|
|
||||||
return ET.tostring(elem, encoding="utf-8", xml_declaration=True)
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Helper: fetch habits + today's completion from Beaver
|
# Helper: fetch habits + today's completion from Beaver
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
def fetch_tasks() -> dict[str, dict]:
|
def fetch_tasks() -> dict[str, dict]:
|
||||||
"""Return a dict of {habit_id: {SUMMARY, STATUS, DTSTAMP, DUE}}."""
|
"""
|
||||||
|
Fetch habit list + today's completion from Beaver Habits.
|
||||||
|
Returns a dict:
|
||||||
|
{ habit_id: { "SUMMARY": <name>, "STATUS": <flag> } }
|
||||||
|
DATES are handled later by make_vtodo().
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
r = _session.get(f"{BEAVER_URL}/api/v1/habits", headers=beaver_headers(), timeout=5)
|
r = _session.get(f"{BEAVER_URL}/api/v1/habits",
|
||||||
|
headers=beaver_headers(), timeout=5)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
habits = r.json()
|
habits = r.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Failed to list habits: %s", e)
|
log.error("Failed to list habits: %s", e)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
today = datetime.date.today().strftime("%d-%m-%Y")
|
# Use the same date format Beaver expects
|
||||||
|
today_str = local_now().strftime("%d-%m-%Y")
|
||||||
out: dict[str, dict] = {}
|
out: dict[str, dict] = {}
|
||||||
|
|
||||||
for h in habits:
|
for h in habits:
|
||||||
hid = h["id"]
|
hid = h["id"]
|
||||||
name = h["name"]
|
name = h["name"]
|
||||||
# check completion for today
|
|
||||||
try:
|
try:
|
||||||
params = {
|
params = {
|
||||||
"date_fmt": "%d-%m-%Y",
|
"date_fmt": "%d-%m-%Y",
|
||||||
"date_start": today,
|
"date_start": today_str,
|
||||||
"date_end": today,
|
"date_end": today_str,
|
||||||
"sort": "asc"
|
"sort": "asc",
|
||||||
}
|
}
|
||||||
rc = _session.get(
|
rc = _session.get(
|
||||||
f"{BEAVER_URL}/api/v1/habits/{hid}/completions",
|
f"{BEAVER_URL}/api/v1/habits/{hid}/completions",
|
||||||
|
@ -127,19 +168,17 @@ def fetch_tasks() -> dict[str, dict]:
|
||||||
)
|
)
|
||||||
rc.raise_for_status()
|
rc.raise_for_status()
|
||||||
done_list = rc.json()
|
done_list = rc.json()
|
||||||
completed = today in done_list
|
completed = today_str in done_list
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("Failed to get completions for %s", hid)
|
log.warning("Failed to fetch completions for habit %s", hid)
|
||||||
completed = False
|
completed = False
|
||||||
|
|
||||||
out[hid] = {
|
out[hid] = {
|
||||||
"SUMMARY": name,
|
"SUMMARY": name,
|
||||||
"STATUS": "COMPLETED" if completed else "NEEDS-ACTION",
|
"STATUS": "COMPLETED" if completed else "NEEDS-ACTION",
|
||||||
"DTSTAMP": datetime.datetime.now(datetime.timezone.utc),
|
|
||||||
"DUE": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1),
|
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
|
|
||||||
|
return out
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Standard PROPFIND / discovery endpoints
|
# Standard PROPFIND / discovery endpoints
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@ -256,7 +295,7 @@ def single_task(uid):
|
||||||
|
|
||||||
summary = tasks_map[uid]["SUMMARY"]
|
summary = tasks_map[uid]["SUMMARY"]
|
||||||
completed = (tasks_map[uid]["STATUS"] == "COMPLETED")
|
completed = (tasks_map[uid]["STATUS"] == "COMPLETED")
|
||||||
today = datetime.date.today()
|
today = date.today()
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return Response(make_vtodo(uid, summary, completed),
|
return Response(make_vtodo(uid, summary, completed),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue