view 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 source

"""
read calendar from mongodb, return a few preset queries as RDF graphs

gcalendarwatch.conf looks like this:
{
  "mongo" : {"host" : "h1", "port" : 27017, "database" : "dbname"},
}
"""
import datetime
import json
import logging
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
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.routing import Route
from starlette_exporter import PrometheusMiddleware, handle_metrics

from datetimemath import dayRange, limitDays
from graphconvert import asGraph
from localtypes import Conf, Record

logging.basicConfig(level=logging.INFO)
log = logging.getLogger()

EV = Namespace("http://bigasterisk.com/event#")
"""
example:
{
 'id': 'l640999999999999999999999c',
 'summary': 'sec.......',
 'start': {'timeZone': 'America/Los_Angeles', 'dateTime': '2014-09-25T16:00:00-07:00'},
 'end': {'timeZone': 'America/Los_Angeles', 'dateTime': '2014-09-25T17:00:00-07:00'},
 'endTimeUnspecified': True,
 'created': '2014-09-08T20:39:00.000Z',
 'creator': {'self': True, 'displayName': '...', 'email': '...'},
 'etag': '"2829999999999000"',
 'htmlLink': 'https://www.google.com/calendar/event?eid=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbEBt',
 'iCalUID': 'l640998888888888888888888888888888com',
 'kind': 'calendar#event',
 'organizer': {'self': True, 'displayName': '...', 'email': '...'},
 'reminders': {'useDefault': True},
 'sequence': 0,
 'status': 'confirmed',
 'updated': '2014-09-17T04:28:56.997Z',
}"""


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


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 _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)]))

    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']]))

        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(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']
    db = MongoClient(m['host'], m['port'], tz_aware=True)[m['database']]
    sync = SyncGraphsToMongo(conf, db, agendaGraph, countdownGraph, currentEventsGraph)

    # 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())

    moreNs = {
        "": "http://bigasterisk.com/event#",
        "cal": "http://bigasterisk.com/calendar/",
        "event": "http://bigasterisk.com/calendarEvent/",
    }

    app = Starlette(debug=True,
                    routes=[
                        Route('/', getRoot),
                        Route('/graph/calendar/upcoming', StaticGraph(agendaGraph, moreNs)),
                        Route('/graph/calendar/upcoming/events', GraphEvents(agendaGraph)),
                        Route('/graph/calendar/countdown', StaticGraph(countdownGraph, moreNs)),
                        Route('/graph/calendar/countdown/events', GraphEvents(countdownGraph)),
                        Route('/graph/currentEvents', StaticGraph(currentEventsGraph, moreNs)),
                        Route('/graph/currentEvents/events', GraphEvents(currentEventsGraph)),
                    ])

    app.add_middleware(PrometheusMiddleware, group_paths=True, filter_unhandled_paths=True, app_name='gcalendarwatch')
    app.add_route("/metrics", handle_metrics)
    return app


app = main()