From 80c09363f969515a2087ca1ae53c9e7d97d85b69 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Fri, 31 May 2024 23:45:33 +0200 Subject: [PATCH 1/3] Raise exception on non-existing event; update full event --- spond/spond.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/spond/spond.py b/spond/spond.py index 8dbf0b5..eb923bb 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -358,7 +358,6 @@ async def update_event(self, uid: str, updates: dict): "autoAccept": False, "payment": {}, "attachments": [], - "id": None, "tasks": { "openTasks": [], "assignedTasks": [ @@ -374,10 +373,19 @@ async def update_event(self, uid: str, updates: dict): }, } + if not self.events: + await self.get_events() + for event in self.events: + if event["id"] == uid: + base_event.update(event) + url = f"{self.API_BASE_URL}sponds/{uid}" + break + else: + errmsg = f"No event with id='{uid}' existing" + raise ValueError(errmsg) + for key in base_event: - if event.get(key) is not None and not updates.get(key): - base_event[key] = event[key] - elif updates.get(key) is not None: + if updates.get(key) is not None: base_event[key] = updates[key] data = dict(base_event) From 720acea42f136c25a17e1179b05acd0a9ddb52c1 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Tue, 25 Jun 2024 02:35:44 +0200 Subject: [PATCH 2/3] Enable creation of new events in `update_events` --- spond/spond.py | 64 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/spond/spond.py b/spond/spond.py index eb923bb..060013a 100644 --- a/spond/spond.py +++ b/spond/spond.py @@ -313,28 +313,26 @@ async def get_event(self, uid: str) -> dict: @_SpondBase.require_authentication async def update_event(self, uid: str, updates: dict): """ - Updates an existing event. + Updates an existing event or creates a new one. Parameters ---------- uid : str - UID of the event. + UID of the event to be updated. If no event of that UID exists, + a new one will be created with default settings. updates : dict - The changes. e.g. if you want to change the description -> {'description': "New Description with changes"} + The changes to existing event or default template for new events. + e.g. if you want to change the description -> + {'description': "New Description with changes"} + For a new event this should at a minimum include entries for + (list of) 'owners', 'recipients' (a dict of {"group": {"id": GID}}), + 'heading', 'startTimestamp', 'endTimestamp' (in datetime.isoformat). Returns ------- json results of post command """ - if not self.events: - await self.get_events() - for event in self.events: - if event["id"] == uid: - break - - url = f"{self.api_url}sponds/{uid}" - base_event: dict = { "heading": None, "description": None, @@ -358,37 +356,39 @@ async def update_event(self, uid: str, updates: dict): "autoAccept": False, "payment": {}, "attachments": [], - "tasks": { - "openTasks": [], - "assignedTasks": [ - { - "name": None, - "description": "", - "type": "ASSIGNED", - "id": None, - "adultsOnly": True, - "assignments": {"memberIds": [], "profiles": [], "remove": []}, - } - ], - }, + "recipients": {"group": {"id": None}}, + "tasks": {"openTasks": [], "assignedTasks": []}, } + data = dict(base_event) if not self.events: await self.get_events() for event in self.events: if event["id"] == uid: - base_event.update(event) - url = f"{self.API_BASE_URL}sponds/{uid}" + data.update(event) + url = f"{self.api_url}sponds/{uid}" break else: - errmsg = f"No event with id='{uid}' existing" - raise ValueError(errmsg) - - for key in base_event: + # No event of that id, create a new one (id to be set by Spond) + if ( + len(updates.get("owners", [])) < 1 + or updates["owners"][0].get("id") is None + ): + errmsg = '"owners" need to have a valid user id' + raise ValueError(errmsg) + if ( + "recipients" not in updates + or updates["recipients"].get("group").get("id") is None + ): + errmsg = '"recipients" need to contain a "group" with valid id' + raise ValueError(errmsg) + updates.pop("id", None) + url = f"{self.api_url}sponds" + + for key in data: if updates.get(key) is not None: - base_event[key] = updates[key] + data[key] = updates[key] - data = dict(base_event) async with self.clientsession.post( url, json=data, headers=self.auth_headers ) as r: From fc30d4ff0ac2fbda3ec518b0c6b86a44ee6c49b5 Mon Sep 17 00:00:00 2001 From: Derek Homeier Date: Tue, 25 Jun 2024 02:45:23 +0200 Subject: [PATCH 3/3] Add example script for reading events in iCal format and posting to Spond --- postics.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 postics.py diff --git a/postics.py b/postics.py new file mode 100644 index 0000000..0692a5f --- /dev/null +++ b/postics.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python + +import argparse +import asyncio + +import ics + +from config import password, username +from spond import spond + +DESCRIPTION = """ +Read in iCal events from .ics file[s] and post them to Spond. +""".strip() + + +def ics2spond(event): + """Create Spond event dictionary from ics.Event""" + + return { + "heading": event.name, + "description": event.description, + "startTimestamp": event.begin.isoformat(), + "endTimestamp": event.end.isoformat(), + "location": {"feature": event.location}, + } + + +async def post_events(args, gid=None, owners=[]): + """ + Read Calendar from .ics file[s] and post all events to Spond. + + Parameters + ---------- + args : argparse.Namespace + Command line arguments and options returned by ArgumentParser.parse_args(), + containing options and file name[s] (wildcards supported). + gid : str + 'id' of Spond group to post to (default: first group from `get_groups()` for user). + owners : list + list of user's {'id': uid} (default: [user] from `config.username`). + """ + + s = spond.Spond(username=username, password=password) + + if len(owners) == 0: + user = await s.get_person(username) + owners = [user["profile"]] + + if gid is None: + groups = await s.get_groups() + for mygroup in groups: + if mygroup["contactPerson"]["id"] == owners[0]["id"]: + break + else: + raise ValueError(f"No group with contact person {owners[0]['id']} found") + recipients = {"group": mygroup} + else: + recipients = {"group": {"id": gid}} + + if not args.quiet: + print(f"Posting as {username} ({owners[0]['id']}): {recipients['group']['id']}") + + for filename in args.filename: # Support wildcards + if not args.quiet: + print(f"Reading {filename}:") + calendar = ics.Calendar(open(filename).read()) + for event in calendar.events: + updates = {"owners": owners, "recipients": recipients} + updates.update(ics2spond(event)) + uid = getattr(event, "uid", "") + if args.verbose: + print(event.serialize()) + elif not args.quiet: + print(event.name) + events = await s.update_event(uid, updates) + await s.clientsession.close() + + +def main(args=None): + """The main function called by the `postics` script.""" + parser = argparse.ArgumentParser( + description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter + ) + # parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") + parser.add_argument( + "-v", + "--verbose", + default=False, + action="store_true", + help="verbose mode; echo full iCal events parsed", + ) + parser.add_argument( + "-q", + "--quiet", + default=False, + action="store_true", + help="quiet mode; do not echo iCal event names", + ) + parser.add_argument( + "-g", "--gid", default=None, help="specify Spond group ID of recipients group" + ) + parser.add_argument( + "filename", + nargs="+", + help="Path to one or more ics files; wildcards are supported", + ) + + args = parser.parse_args(args) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + asyncio.run(post_events(args, gid=args.gid)) + + +if __name__ == "__main__": + main()