yaml2ical/yaml2ical/meeting.py

179 lines
6.3 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
from io import StringIO
import os
import os.path
import yaml
from yaml2ical.recurrence import supported_recurrences
DATES = {
'Monday': datetime.datetime(1900, 1, 1).date(),
'Tuesday': datetime.datetime(1900, 1, 2).date(),
'Wednesday': datetime.datetime(1900, 1, 3).date(),
'Thursday': datetime.datetime(1900, 1, 4).date(),
'Friday': datetime.datetime(1900, 1, 5).date(),
'Saturday': datetime.datetime(1900, 1, 6).date(),
'Sunday': datetime.datetime(1900, 1, 7).date(),
}
ONE_WEEK = datetime.timedelta(weeks=1)
class Schedule(object):
"""A meeting schedule."""
def __init__(self, meeting, sched_yaml):
"""Initialize schedule from yaml."""
self.project = meeting.project
self.filefrom = meeting.filefrom
try:
self.utc = sched_yaml['time']
self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M')
# Sanitize the Day
self.day = sched_yaml['day'].lower().capitalize()
self.irc = sched_yaml['irc']
self.freq = sched_yaml['frequency']
self.recurrence = supported_recurrences[sched_yaml['frequency']]
except KeyError as e:
print("Invalid YAML meeting schedule definition - missing "
"attribute '{0}'".format(e.args[0]))
raise
if self.day not in DATES.keys():
raise ValueError("'%s' is not a valid day of the week")
# NOTE(tonyb): We need to do this datetime shenanigans is so we can
# deal with meetings that start on day1 and end on day2.
self.meeting_start = datetime.datetime.combine(DATES[self.day],
self.time.time())
self.meeting_end = (self.meeting_start + datetime.timedelta(hours=1))
if self.day == 'Sunday' and self.meeting_end.strftime("%a") == 'Mon':
self.meeting_start = self.meeting_start - ONE_WEEK
self.meeting_end = self.meeting_end - ONE_WEEK
def conflicts(self, other):
"""Checks for conflicting schedules."""
alternating = set(['biweekly-odd', 'biweekly-even'])
# NOTE(tonyb): .meeting_start also includes the day of the week. So no
# need to check .day explictly
return ((self.irc == other.irc) and
((self.meeting_start < other.meeting_end) and
(other.meeting_start < self.meeting_end)) and
(set([self.freq, other.freq]) != alternating))
class Meeting(object):
"""An OpenStack meeting."""
def __init__(self, data):
"""Initialize meeting from meeting yaml description."""
yaml_obj = yaml.safe_load(data)
try:
self.chair = yaml_obj['chair']
self.description = yaml_obj['description']
self.project = yaml_obj['project']
except KeyError as e:
print("Invalid YAML meeting definition - missing "
"attribute '{0}'".format(e.args[0]))
raise
# Find any extra values the user has provided that they might
# want to have access to in their templates.
self.extras = {}
self.extras.update(yaml_obj)
for k in ['chair', 'description', 'project', 'schedule']:
if k in self.extras:
del self.extras[k]
try:
self.filefrom = os.path.basename(data.name)
except AttributeError:
self.filefrom = "stdin"
self.schedules = []
for sch in yaml_obj['schedule']:
s = Schedule(self, sch)
self.schedules.append(s)
@classmethod
def fromfile(cls, yaml_file):
f = open(yaml_file, 'r')
return cls(f)
@classmethod
def fromstring(cls, yaml_string):
s = StringIO(yaml_string)
return cls(s)
def load_meetings(yaml_source):
"""Build YAML object and load meeting data
:param yaml_source: source data to load, which can be a directory or
stream.
:returns: list of meeting objects
"""
meetings = []
# Determine what the yaml_source is. Files must have .yaml extension
# to be considered valid.
if os.path.isdir(yaml_source):
for root, dirs, files in os.walk(yaml_source):
for f in files:
# Build the entire file path and append to the list of yaml
# meetings
if os.path.splitext(f)[1] == '.yaml':
yaml_file = os.path.join(root, f)
meetings.append(Meeting.fromfile(yaml_file))
elif (os.path.isfile(yaml_source) and
os.path.splitext(yaml_source)[1] == '.yaml'):
meetings.append(Meeting.fromfile(yaml_source))
elif isinstance(yaml_source, str):
return [Meeting.fromstring(yaml_source)]
if not meetings:
# If we don't have a .yaml file, a directory of .yaml files, or any
# YAML data fail out here.
raise ValueError("No .yaml file, directory containing .yaml files, "
"or YAML data found.")
else:
return meetings
class MeetingConflictError(Exception):
pass
def check_for_meeting_conflicts(meetings):
"""Check if a list of meetings have conflicts.
:param meetings: list of Meeting objects
"""
for i in range(len(meetings)):
schedules = meetings[i].schedules
for j in range(i + 1, len(meetings)):
other_schedules = meetings[j].schedules
for schedule in schedules:
for other_schedule in other_schedules:
if schedule.conflicts(other_schedule):
msg_dict = {'one': schedule.filefrom,
'two': other_schedule.filefrom}
raise MeetingConflictError(
"Conflict between %(one)s and %(two)s" % msg_dict)