diff gcalendarwatch.py @ 85:f75b3a109b66

rewrite gcalendarwatch to read from calsync's mongo data only, not google services
author drewp@bigasterisk.com
date Sat, 07 Sep 2024 16:14:45 -0700
parents 5f7ae444ecae
children
line wrap: on
line diff
--- a/gcalendarwatch.py	Sat Sep 07 16:13:55 2024 -0700
+++ b/gcalendarwatch.py	Sat Sep 07 16:14:45 2024 -0700
@@ -1,40 +1,33 @@
 """
-sync google calendar into mongodb, return a few preset queries as RDF graphs
+read calendar from mongodb, return a few preset queries as RDF graphs
 
 gcalendarwatch.conf looks like this:
 {
-  "minutes_between_polls" : 60
-  "mongo" : {"host" : "h1", "port" : 27017, "database" : "dbname", "collection" : "pim"},
+  "mongo" : {"host" : "h1", "port" : 27017, "database" : "dbname"},
 }
-
-This should be updated to use
-http://googledevelopers.blogspot.com/2013/07/google-calendar-api-push-notifications.html
-and update faster with less polling
 """
 import datetime
-import functools
 import json
 import logging
-import time
-import traceback
-from typing import Iterable, cast
+import re
+from typing import Sequence, cast
 
 import background_loop
 import pymongo.collection
+import pymongo.database
 from dateutil.tz import tzlocal
 from patchablegraph import PatchableGraph
 from patchablegraph.handler import GraphEvents, StaticGraph
 from pymongo import MongoClient
-from rdflib import Namespace, URIRef
+from rdflib import Namespace
 from starlette.applications import Starlette
 from starlette.requests import Request
-from starlette.responses import HTMLResponse, PlainTextResponse
+from starlette.responses import HTMLResponse
 from starlette.routing import Route
 from starlette_exporter import PrometheusMiddleware, handle_metrics
 
-from datetimemath import dayRange
+from datetimemath import dayRange, limitDays
 from graphconvert import asGraph
-from ingest import SyncToMongo
 from localtypes import Conf, Record
 
 logging.basicConfig(level=logging.INFO)
@@ -63,87 +56,67 @@
 }"""
 
 
-class ReadMongoEvents(object):
-    """read events from mongodb"""
-
-    def __init__(self, collection: pymongo.collection.Collection):
-        self.collection = collection
-
-    def getEvents(self, t1: datetime.datetime, t2: datetime.datetime) -> Iterable[Record]:
-        if t1.tzinfo is None or t2.tzinfo is None:
-            raise TypeError("tz-naive datetimes")
-        for doc in self.collection.find({"startTime": {"$gte": t1, "$lt": t2}}).sort([("startTime", 1)]):
-            doc['uri'] = doc.pop('_id')
-            if 'feedId' in doc:
-                doc['feed'] = URIRef('old_event')
-            yield doc
+def filterStarred(recs: Sequence[Record], maxCount=15) -> list[Record]:
+    recs = sorted(recs, key=lambda r: r['start'])
+    out = []
+    for rec in recs:
+        if m := re.search(r'(.*)\*\s*$', rec['title']):
+            rec = rec.copy()
+            rec['title'] = m.group(1)
+            out.append(rec)
+            if len(out) >= maxCount:
+                break
+    return out
 
 
-def update(
-        # this is incompletely type-checked:
-        sync: SyncToMongo,  # curried by main
-        first_run: bool,  # passed by background_loop
-        cal=None  # sometimes passed by us
-) -> int:
-    log.info(f"updating {cal or 'all'}")
-    try:
-        n = sync.update(cal=cal, days=120)
-    except Exception:
-        traceback.print_exc()
-        log.error("update failed")
-        n = 0
-    return n
+class SyncGraphsToMongo(object):
+    """reads mongodb (that calsync wrote); edits graphs"""
+    calendarsCollection: pymongo.collection.Collection
+    eventsCollection: pymongo.collection.Collection
 
+    def __init__(self, conf: Conf, db: pymongo.database.Database, agendaGraph: PatchableGraph, countdownGraph: PatchableGraph,
+                 currentEventsGraph: PatchableGraph):
+        self.conf = conf
+        self.eventsCollection = db.get_collection('test_gcalendar')
+        self.calendarsCollection = db.get_collection('test_gcalendar_cals')
+
+        self.agendaGraph = agendaGraph
+        self.countdownGraph = countdownGraph
+        self.currentEventsGraph = currentEventsGraph
 
-def statusMsg(conf, loop: background_loop.Loop):
-    period = conf['minutes_between_polls'] * 60
-    ago = time.time() - loop.lastSuccessRun
-    if not loop.everSucceeded:
-        msg = "no completed updates %d sec after startup" % ago
-        if ago > period * 1.1:
-            raise ValueError(msg)
-    else:
-        msg = "last update was %d sec ago" % ago
-        if ago > period * 1.1:
-            raise ValueError(msg)
-    return msg
-
+    def _getEvents(self, t1: datetime.datetime, t2: datetime.datetime) -> list[Record]:
+        if t1.tzinfo is None or t2.tzinfo is None:
+            raise TypeError("tz-naive datetimes")
+        return list(self.eventsCollection.find({"startTime": {"$gte": t1, "$lt": t2}}).sort([("startTime", 1)]))
 
-async def PollNow(loop: background_loop.Loop, req: Request) -> PlainTextResponse:
-    body = await req.body()
-    cals = json.loads(body).get('cals', None) if body else [None]
-    n = 0
-    for cal in cals:
-        n += await loop.runNow(cal=cal)
-    msg = f"found {n} new records"
-    log.info(msg)
-    return PlainTextResponse(msg)
+    def updateGraphs(self, first_run):
+        s, e = dayRange(120)
+        currentRecords = self._getEvents(s, e)
+        cals = list(self.calendarsCollection.find())
+        self.agendaGraph.setToGraph(asGraph(self.conf, cals, limitDays(currentRecords, days=2)))
+        self.countdownGraph.setToGraph(asGraph(self.conf, cals, filterStarred(currentRecords, maxCount=15), extraClasses=[EV['CountdownEvent']]))
 
-
-def updateCurrentEvents(conf: Conf, currentEventsGraph: PatchableGraph, collection: pymongo.collection.Collection, first_run: bool):
-    now = datetime.datetime.now(tzlocal())
-    events = list(collection.find({"startTime": {"$lte": now}, "endTime": {"$gte": now}}))
-    currentEventsGraph.setToGraph(asGraph(conf, cals=[], events=events, extraClasses=[EV['CurrentEvent']]))
+        now = datetime.datetime.now(tzlocal())
+        events = list(self.eventsCollection.find({"startTime": {"$lte": now}, "endTime": {"$gte": now}}))
+        self.currentEventsGraph.setToGraph(asGraph(self.conf, cals=[], events=events, extraClasses=[EV['CurrentEvent']]))
 
 
 def main():
-    agendaGraph = PatchableGraph()  # next few days
-    countdownGraph = PatchableGraph()  # next n of starred events
-    currentEventsGraph = PatchableGraph()  # events happening now
+    agendaGraph = PatchableGraph(label='agenda')  # next few days
+    countdownGraph = PatchableGraph(label='countdown')  # next n of starred events
+    currentEventsGraph = PatchableGraph(label='currentEvents')  # events happening now
+
     conf = cast(Conf, json.load(open("gcalendarwatch.conf")))
     m = conf['mongo']
-    mongoOut = MongoClient(m['host'], m['port'], tz_aware=True)[m['database']][m['collection']]
-    sync = SyncToMongo(conf, mongoOut, agendaGraph, countdownGraph)
-    read = ReadMongoEvents(mongoOut)
+    db = MongoClient(m['host'], m['port'], tz_aware=True)[m['database']]
+    sync = SyncGraphsToMongo(conf, db, agendaGraph, countdownGraph, currentEventsGraph)
 
-    s, e = dayRange(120)
-    sync.updateGraphs(read.getEvents(s, e))
-
-    loop = background_loop.loop_forever(functools.partial(update, sync=sync), conf['minutes_between_polls'] * 60)
-    background_loop.loop_forever(functools.partial(updateCurrentEvents, conf, currentEventsGraph, mongoOut), 5, metric_prefix="current_events")
+    # todo: this should watch for mongodb edits, or get a signal from calsync
+    background_loop.loop_forever(sync.updateGraphs, 5, metric_prefix="update_graphs")
 
     def getRoot(request: Request) -> HTMLResponse:
-        return HTMLResponse(content=open("index.html").read().replace("MSG", statusMsg(conf, loop)))
+        return HTMLResponse(content=open("index.html").read())
+
     moreNs = {
         "": "http://bigasterisk.com/event#",
         "cal": "http://bigasterisk.com/calendar/",
@@ -159,7 +132,6 @@
                         Route('/graph/calendar/countdown/events', GraphEvents(countdownGraph)),
                         Route('/graph/currentEvents', StaticGraph(currentEventsGraph, moreNs)),
                         Route('/graph/currentEvents/events', GraphEvents(currentEventsGraph)),
-                        Route('/pollNow', functools.partial(PollNow, loop), methods=['POST'])
                     ])
 
     app.add_middleware(PrometheusMiddleware, group_paths=True, filter_unhandled_paths=True, app_name='gcalendarwatch')