yaml2ical/yaml2ical/ical.py
Thierry Carrez b413607a97 Update hacking version
yaml2ical was using a very old version that started to break with
newer Python, so it's time to upgrade. This requires a few adjustments
to quiet new warnings.

Change-Id: Ib081dcfeca7c40f877059793dab58d00a7b0d53e
2021-07-15 14:49:54 +02:00

160 lines
6.4 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
import os
import os.path
import icalendar
import pytz
class Yaml2IcalCalendar(icalendar.Calendar):
"""A calendar in ics format."""
def __init__(self, calname=None, caldescription=None):
super(Yaml2IcalCalendar, self).__init__()
self.add('prodid', '-//yaml2ical agendas//EN')
self.add('version', '2.0')
if calname is not None:
self.add('X-WR-CALNAME', calname)
if caldescription is not None:
self.add('X-WR-CALDESC', caldescription)
def add_meeting(self, meeting):
"""Add this meeting to the calendar."""
for sch in meeting.schedules:
self.add_schedule(meeting, sch)
def add_schedule(self, meeting, sch, exdate=None):
event = icalendar.Event()
# NOTE(jotan): I think the summary field needs to be unique per
# event in an ical file (at least, for it to work with
# Google Calendar)
summary = meeting.project
# NOTE(tonyb): If we're adding an a place holder event for a
# cancelled meeting make that as obvious as possible in the
# summary.
if exdate:
# NOTE(tonyb): Because some iCal consumers require that the
# summary be unique, and adding multiple "CANCELLED: $x"
# entries would violate that rule, append the (UTC)
# timestamp for the cancelled meeting.
suffix = exdate.date_str
summary = 'CANCELLED: %s (%s)' % (summary, suffix)
event.add('summary', summary)
event.add('location', '#' + sch.irc)
# add ical description
project_descript = "Project: %s" % (meeting.project)
chair_descript = "Chair: %s" % (meeting.chair)
descript_descript = "Description: %s" % (meeting.description)
ical_descript = "\n".join((project_descript,
chair_descript,
descript_descript))
# Add URLs, if present, to the description
if 'agenda_url' in meeting.extras:
ical_descript = "\n".join((ical_descript,
"Agenda URL: %s" %
(meeting.extras['agenda_url'])))
if 'project_url' in meeting.extras:
ical_descript = "\n".join((ical_descript,
"Project URL: %s" %
(meeting.extras['project_url'])))
# NOTE(tonyb): If we're adding an a place holder event for a
# cancelled meeting do not add an rrule and set dtstart to the
# skipped date.
if not exdate:
# get starting date
next_meeting = sch.recurrence.next_occurence(sch.start_date,
sch.day)
# NOTE(aschultz): to handle adhoc meetings, we check to make
# sure there is a next meeting before trying to add it to the
# the calendar
if next_meeting is None:
return
next_meeting_date = datetime.datetime(next_meeting.year,
next_meeting.month,
next_meeting.day,
sch.time.hour,
sch.time.minute,
tzinfo=pytz.utc)
event.add('dtstart', next_meeting_date)
# add recurrence rule
event.add('rrule', sch.recurrence.rrule())
event.add('description', ical_descript)
else:
event.add('dtstart', exdate.date)
event.add('description', exdate.reason)
event.add('duration', datetime.timedelta(minutes=sch.duration))
# Add exdate (exclude date) if present
if not exdate and hasattr(sch, 'skip_dates'):
for skip_date in sch.skip_dates:
event.add('exdate', skip_date.date)
# NOTE(tonyb): If this is a skipped meeting add a
# non-recurring event with an obvious summary.
self.add_schedule(meeting, sch, exdate=skip_date)
# Add update timestamp and unique ID for the event.
# Note: we need a way to uniquely identify the meeting, even if its
# details are modified. We don't really have much to go on to do
# that, so best effort just use the project name and date.
event.add('dtstamp', meeting.last_update)
start_date = exdate.date if exdate else next_meeting_date
event.add('uid', '%s-%s' % (
meeting.project.replace(' ', '').lower(),
datetime.datetime.strftime(start_date, '%Y%m%d')))
# add event to calendar
self.add_component(event)
def write_to_disk(self, filename):
# write ical files to disk
with open(filename, 'wb') as ics:
ics.write(self.to_ical())
def convert_meetings_to_ical(meetings, outputdir=None, outputfile=None,
calname=None, caldescription=None):
"""Converts a meeting list to iCal.
:param meetings: list of meetings to convert
:param outputdir: location to store iCal files (one file per meeting)
:param outputfile: output iCal file (one single file for all meetings)
"""
# convert meetings to a list of ical
if outputdir:
for m in meetings:
cal = Yaml2IcalCalendar()
cal.add_meeting(m)
cal.write_to_disk(os.path.join(outputdir, m.outfile))
# convert meetings into a single ical
if outputfile:
cal = Yaml2IcalCalendar(calname, caldescription)
for m in meetings:
cal.add_meeting(m)
cal.write_to_disk(outputfile)
# TODO(jotan): verify converted ical is valid
logging.info('Wrote %d meetings to iCal' % (len(meetings)))