Reorganize modules in yaml2ical
Reorganize modules in yaml2ical so that: - Reading meetings from YAML (and checking conflicts) is in meeting.py - Functions to produce iCal output are in ical.py - Date recurrence functions are in recurrence.py There is no need for a generic utils.py anymore. Further refactoring of functions and object classes is coming. Change-Id: I35d2833f5eac088c13e684bf094a455591135dff
This commit is contained in:
parent
923218b121
commit
4ab55d5c77
|
@ -14,7 +14,7 @@ import argparse
|
|||
import logging
|
||||
import os
|
||||
|
||||
from yaml2ical import utils
|
||||
from yaml2ical import ical
|
||||
|
||||
|
||||
# logging settings
|
||||
|
@ -43,7 +43,7 @@ project infrastructure.
|
|||
dest="ical_dir",
|
||||
help="output directory (one file per meeting)")
|
||||
outputtype.add_argument("-o", "--output",
|
||||
dest="ical",
|
||||
dest="icalfile",
|
||||
help="output file (one file for all meetings)")
|
||||
parser.add_argument("-f", "--force",
|
||||
dest="force",
|
||||
|
@ -78,16 +78,16 @@ def main():
|
|||
else:
|
||||
raise Exception("Directory for storing iCals is not empty, "
|
||||
"suggest running with -f to remove old files.")
|
||||
utils.convert_yaml_to_ical(yaml_dir, outputdir=ical_dir)
|
||||
ical.convert_yaml_to_ical(yaml_dir, outputdir=ical_dir)
|
||||
else:
|
||||
ical = os.path.abspath(args.ical)
|
||||
if os.path.exists(ical):
|
||||
icalfile = os.path.abspath(args.icalfile)
|
||||
if os.path.exists(icalfile):
|
||||
if force:
|
||||
os.remove(ical)
|
||||
os.remove(icalfile)
|
||||
else:
|
||||
raise Exception("Output file already exists, suggest running "
|
||||
"with -f to overwrite previous file.")
|
||||
utils.convert_yaml_to_ical(yaml_dir, outputfile=ical)
|
||||
ical.convert_yaml_to_ical(yaml_dir, outputfile=ical)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
# 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 icalendar
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
|
||||
from yaml2ical import meeting
|
||||
from yaml2ical import recurrence
|
||||
|
||||
|
||||
class Yaml2IcalCalendar(icalendar.Calendar):
|
||||
"""A calendar in ics format."""
|
||||
|
||||
def __init__(self):
|
||||
super(Yaml2IcalCalendar, self).__init__()
|
||||
self.add('prodid', '-//yaml2ical agendas//EN')
|
||||
self.add('version', '2.0')
|
||||
|
||||
def add_meeting(self, meeting):
|
||||
"""Add this meeting to the calendar."""
|
||||
|
||||
for sch in meeting.schedules:
|
||||
# one Event per iCal file
|
||||
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)
|
||||
|
||||
event.add('summary', meeting.project)
|
||||
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))
|
||||
event.add('description', ical_descript)
|
||||
|
||||
# get starting date
|
||||
start_date = datetime.datetime.utcnow()
|
||||
if sch.freq.startswith('biweekly'):
|
||||
meet_on_even = sch.freq.endswith('even')
|
||||
next_meeting = recurrence.next_biweekly_meeting(
|
||||
start_date,
|
||||
sch.day,
|
||||
meet_on_even=meet_on_even)
|
||||
else:
|
||||
next_meeting = recurrence.next_weekday(start_date, sch.day)
|
||||
|
||||
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
|
||||
if sch.freq.startswith('biweekly'):
|
||||
cadence = ()
|
||||
# NOTE(lbragstad): Setting the `cadence` for the schedule
|
||||
# will allow for alternating meetings. Typically there are
|
||||
# only 4 weeks in a month but adding `5` and `6` allow for
|
||||
# cases where there are 5 meetings in a month, which would
|
||||
# otherwise be unsupported if only setting `cadence` to
|
||||
# either (1, 3) or (2, 4).
|
||||
if sch.freq == 'biweekly-odd':
|
||||
cadence = (1, 3, 5)
|
||||
elif sch.freq == 'biweekly-even':
|
||||
cadence = (2, 4, 6)
|
||||
rule_dict = {'freq': 'monthly',
|
||||
'byday': sch.day[0:2],
|
||||
'bysetpos': cadence}
|
||||
event.add('rrule', rule_dict)
|
||||
else:
|
||||
event.add('rrule', {'freq': sch.freq})
|
||||
|
||||
# add meeting length
|
||||
# TODO(jotan): determine duration to use for OpenStack meetings
|
||||
event.add('duration', datetime.timedelta(hours=1))
|
||||
|
||||
# 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_yaml_to_ical(yaml_dir, outputdir=None, outputfile=None):
|
||||
"""Convert meeting YAML files to iCal.
|
||||
|
||||
If meeting_list is specified, only those meetings in yaml_dir with
|
||||
filenames contained in meeting_list are converted; otherwise,
|
||||
all meeting in yaml_dir are converted.
|
||||
|
||||
:param yaml_dir: directory where meeting.yaml files are stored
|
||||
:param outputdir: location to store iCal files (one file per meeting)
|
||||
:param outputfile: output iCal file (one single file for all meetings)
|
||||
|
||||
"""
|
||||
meetings = meeting.load_meetings(yaml_dir)
|
||||
|
||||
# Check uniqueness and conflicts here before writing out to .ics
|
||||
meeting.check_for_meeting_conflicts(meetings)
|
||||
|
||||
# convert meetings to a list of ical
|
||||
if outputdir:
|
||||
for m in meetings:
|
||||
cal = Yaml2IcalCalendar()
|
||||
cal.add_meeting(m)
|
||||
filename = os.path.basename(m._filename).split('.')[0] + '.ics'
|
||||
cal.write_to_disk(os.path.join(outputdir, filename))
|
||||
|
||||
# convert meetings into a single ical
|
||||
if outputfile:
|
||||
cal = Yaml2IcalCalendar()
|
||||
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)))
|
|
@ -13,30 +13,19 @@
|
|||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import icalendar
|
||||
import pytz
|
||||
import yaml
|
||||
|
||||
from yaml2ical import schedule
|
||||
|
||||
class Schedule:
|
||||
"""A meeting schedule."""
|
||||
|
||||
WEEKDAYS = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
|
||||
'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
def __init__(self, sched_yaml):
|
||||
"""Initialize schedule from yaml."""
|
||||
|
||||
|
||||
class Yaml2IcalCalendar(icalendar.Calendar):
|
||||
"""A calendar in ics format."""
|
||||
|
||||
def __init__(self):
|
||||
super(Yaml2IcalCalendar, self).__init__()
|
||||
self.add('prodid', '-//yaml2ical agendas//EN')
|
||||
self.add('version', '2.0')
|
||||
|
||||
def write_to_disk(self, filename):
|
||||
# write ical files to disk
|
||||
with open(filename, 'wb') as ics:
|
||||
ics.write(self.to_ical())
|
||||
self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M')
|
||||
self.day = sched_yaml['day']
|
||||
self.irc = sched_yaml['irc']
|
||||
self.freq = sched_yaml['frequency']
|
||||
|
||||
|
||||
class Meeting:
|
||||
|
@ -46,90 +35,6 @@ class Meeting:
|
|||
"""Initialize meeting from yaml file name 'filename'."""
|
||||
pass
|
||||
|
||||
def add_to_calendar(self, cal):
|
||||
"""Add this meeting to an existing calendar."""
|
||||
|
||||
for sch in self.schedules:
|
||||
# one Event per iCal file
|
||||
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)
|
||||
|
||||
event.add('summary', self.project)
|
||||
event.add('location', '#' + sch.irc)
|
||||
|
||||
# add ical description
|
||||
project_descript = "Project: %s" % (self.project)
|
||||
chair_descript = "Chair: %s" % (self.chair)
|
||||
descript_descript = "Description: %s" % (self.description)
|
||||
ical_descript = "\n".join((project_descript,
|
||||
chair_descript,
|
||||
descript_descript))
|
||||
event.add('description', ical_descript)
|
||||
|
||||
# get starting date
|
||||
start_date = datetime.datetime.utcnow()
|
||||
if sch.freq.startswith('biweekly'):
|
||||
meet_on_even = sch.freq.endswith('even')
|
||||
next_meeting = next_biweekly_meeting(start_date,
|
||||
WEEKDAYS[sch.day],
|
||||
meet_on_even=meet_on_even)
|
||||
else:
|
||||
next_meeting = next_weekday(start_date,
|
||||
WEEKDAYS[sch.day])
|
||||
|
||||
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
|
||||
if sch.freq.startswith('biweekly'):
|
||||
cadence = ()
|
||||
# NOTE(lbragstad): Setting the `cadence` for the schedule
|
||||
# will allow for alternating meetings. Typically there are
|
||||
# only 4 weeks in a month but adding `5` and `6` allow for
|
||||
# cases where there are 5 meetings in a month, which would
|
||||
# otherwise be unsupported if only setting `cadence` to
|
||||
# either (1, 3) or (2, 4).
|
||||
if sch.freq == 'biweekly-odd':
|
||||
cadence = (1, 3, 5)
|
||||
elif sch.freq == 'biweekly-even':
|
||||
cadence = (2, 4, 6)
|
||||
rule_dict = {'freq': 'monthly',
|
||||
'byday': sch.day[0:2],
|
||||
'bysetpos': cadence}
|
||||
event.add('rrule', rule_dict)
|
||||
else:
|
||||
event.add('rrule', {'freq': sch.freq})
|
||||
|
||||
# add meeting length
|
||||
# TODO(jotan): determine duration to use for OpenStack meetings
|
||||
event.add('duration', datetime.timedelta(hours=1))
|
||||
|
||||
# add event to calendar
|
||||
cal.add_component(event)
|
||||
|
||||
|
||||
def next_weekday(ref_date, weekday):
|
||||
"""Return the date of the next meeting.
|
||||
|
||||
:param ref_date: datetime object of meeting
|
||||
:param weekday: weekday the meeting is held on
|
||||
|
||||
:returns: datetime object of the next meeting time
|
||||
"""
|
||||
|
||||
days_ahead = weekday - ref_date.weekday()
|
||||
if days_ahead <= 0: # target day already happened this week
|
||||
days_ahead += 7
|
||||
return ref_date + datetime.timedelta(days_ahead)
|
||||
|
||||
|
||||
def load_meetings(yaml_source):
|
||||
"""Build YAML object and load meeting data
|
||||
|
@ -178,37 +83,55 @@ def _load_meeting(meeting_yaml):
|
|||
# of having every Meeting object build a list of Schedule objects.
|
||||
m.schedules = []
|
||||
for sch in yaml_obj['schedule']:
|
||||
s = schedule.Schedule(sch)
|
||||
s = Schedule(sch)
|
||||
m.schedules.append(s)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def next_biweekly_meeting(current_date_time, weekday, meet_on_even=False):
|
||||
"""Calculate the next biweekly meeting.
|
||||
class MeetingConflictError(Exception):
|
||||
pass
|
||||
|
||||
:param current_date_time: the current datetime object
|
||||
:param weekday: scheduled day of the meeting
|
||||
:param meet_on_even: True if meeting on even weeks and False if meeting
|
||||
on odd weeks
|
||||
:returns: datetime object of next meeting
|
||||
|
||||
def _extract_meeting_info(meeting_obj):
|
||||
"""Pull out meeting info of Meeting object.
|
||||
|
||||
:param meeting_obj: Meeting object
|
||||
:returns: a dictionary of meeting info
|
||||
|
||||
"""
|
||||
first_day_of_mo = current_date_time.replace(day=1)
|
||||
day_of_week = first_day_of_mo.strftime("%w")
|
||||
adjustment = (8 - int(day_of_week)) % (7 - weekday)
|
||||
if meet_on_even:
|
||||
adjustment += 7
|
||||
next_meeting = first_day_of_mo + datetime.timedelta(adjustment)
|
||||
meeting_info = []
|
||||
for schedule in meeting_obj.schedules:
|
||||
info = {'name': meeting_obj.project,
|
||||
'filename': meeting_obj._filename,
|
||||
'day': schedule.day,
|
||||
'time': schedule.time,
|
||||
'irc_room': schedule.irc}
|
||||
meeting_info.append(info)
|
||||
|
||||
if current_date_time > next_meeting:
|
||||
next_meeting = next_meeting + datetime.timedelta(14)
|
||||
if current_date_time > next_meeting:
|
||||
current_date_time = current_date_time.replace(
|
||||
month=current_date_time.month + 1, day=1)
|
||||
first_wday_next_mo = next_weekday(current_date_time, weekday)
|
||||
if meet_on_even:
|
||||
next_meeting = first_wday_next_mo + datetime.timedelta(7)
|
||||
else:
|
||||
next_meeting = first_wday_next_mo
|
||||
return next_meeting
|
||||
return meeting_info
|
||||
|
||||
|
||||
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)):
|
||||
meeting_info = _extract_meeting_info(meetings[i])
|
||||
for j in range(i + 1, len(meetings)):
|
||||
next_meeting_info = _extract_meeting_info(meetings[j])
|
||||
for current_meeting in meeting_info:
|
||||
for next_meeting in next_meeting_info:
|
||||
if current_meeting['day'] != next_meeting['day']:
|
||||
continue
|
||||
if current_meeting['time'] != next_meeting['time']:
|
||||
continue
|
||||
if current_meeting['irc_room'] != next_meeting['irc_room']:
|
||||
continue
|
||||
msg_dict = {'first': current_meeting['filename'],
|
||||
'second': next_meeting['filename']}
|
||||
raise MeetingConflictError("Conflict between %(first)s "
|
||||
"and %(second)s." % msg_dict)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# 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
|
||||
|
||||
|
||||
WEEKDAYS = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
|
||||
'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
||||
|
||||
def next_weekday(ref_date, day):
|
||||
"""Return the date of the next meeting.
|
||||
|
||||
:param ref_date: datetime object of meeting
|
||||
:param day: weekday the meeting is held on
|
||||
|
||||
:returns: datetime object of the next meeting time
|
||||
"""
|
||||
|
||||
weekday = WEEKDAYS[day]
|
||||
days_ahead = weekday - ref_date.weekday()
|
||||
if days_ahead <= 0: # target day already happened this week
|
||||
days_ahead += 7
|
||||
return ref_date + datetime.timedelta(days_ahead)
|
||||
|
||||
|
||||
def next_biweekly_meeting(current_date_time, day, meet_on_even=False):
|
||||
"""Calculate the next biweekly meeting.
|
||||
|
||||
:param current_date_time: the current datetime object
|
||||
:param day: scheduled day of the meeting
|
||||
:param meet_on_even: True if meeting on even weeks and False if meeting
|
||||
on odd weeks
|
||||
:returns: datetime object of next meeting
|
||||
|
||||
"""
|
||||
weekday = WEEKDAYS[day]
|
||||
first_day_of_mo = current_date_time.replace(day=1)
|
||||
day_of_week = first_day_of_mo.strftime("%w")
|
||||
adjustment = (8 - int(day_of_week)) % (7 - weekday)
|
||||
if meet_on_even:
|
||||
adjustment += 7
|
||||
next_meeting = first_day_of_mo + datetime.timedelta(adjustment)
|
||||
|
||||
if current_date_time > next_meeting:
|
||||
next_meeting = next_meeting + datetime.timedelta(14)
|
||||
if current_date_time > next_meeting:
|
||||
current_date_time = current_date_time.replace(
|
||||
month=current_date_time.month + 1, day=1)
|
||||
first_wday_next_mo = next_weekday(current_date_time, weekday)
|
||||
if meet_on_even:
|
||||
next_meeting = first_wday_next_mo + datetime.timedelta(7)
|
||||
else:
|
||||
next_meeting = first_wday_next_mo
|
||||
return next_meeting
|
|
@ -1,25 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
||||
class Schedule:
|
||||
"""A meeting schedule."""
|
||||
|
||||
def __init__(self, sched_yaml):
|
||||
"""Initialize schedule from yaml."""
|
||||
|
||||
self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M')
|
||||
self.day = sched_yaml['day']
|
||||
self.irc = sched_yaml['irc']
|
||||
self.freq = sched_yaml['frequency']
|
|
@ -10,7 +10,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
from yaml2ical import meeting
|
||||
|
@ -26,12 +25,18 @@ class MeetingTestCase(unittest.TestCase):
|
|||
self.assertEqual('Weekly meeting for Subteam project.\n',
|
||||
m.description)
|
||||
|
||||
def test_calculate_next_biweekly_meeting_meet_on_even(self):
|
||||
test_time = datetime.datetime(2014, 10, 5, 2, 47, 28, 832666)
|
||||
test_weekday = 2
|
||||
meet_on_even = True
|
||||
next_meeting = meeting.next_biweekly_meeting(test_time,
|
||||
test_weekday,
|
||||
meet_on_even=meet_on_even)
|
||||
expected_meeting = datetime.datetime(2014, 10, 8, 2, 47, 28, 832666)
|
||||
self.assertEqual(expected_meeting, next_meeting)
|
||||
def test_exception_raised_when_conflict_detected(self):
|
||||
"""Exception is raised when a meeting conflict is detected."""
|
||||
meeting_one = meeting.load_meetings(sample_data.FIRST_MEETING_YAML)
|
||||
meeting_two = meeting.load_meetings(sample_data.SECOND_MEETING_YAML)
|
||||
meeting_list = [meeting_one.pop(), meeting_two.pop()]
|
||||
self.assertRaises(meeting.MeetingConflictError,
|
||||
meeting.check_for_meeting_conflicts,
|
||||
meeting_list)
|
||||
|
||||
def test_no_exception_raised_with_diff_irc_rooms(self):
|
||||
"""No exception raised when using different IRC rooms."""
|
||||
meeting_one = meeting.load_meetings(sample_data.FIRST_MEETING_YAML)
|
||||
meeting_two = meeting.load_meetings(sample_data.THIRD_MEETING_YAML)
|
||||
meeting_list = [meeting_one.pop(), meeting_two.pop()]
|
||||
meeting.check_for_meeting_conflicts(meeting_list)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# 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 unittest
|
||||
|
||||
from yaml2ical import recurrence
|
||||
|
||||
|
||||
class RecurrenceTestCase(unittest.TestCase):
|
||||
|
||||
def test_calculate_next_biweekly_meeting_meet_on_even(self):
|
||||
test_time = datetime.datetime(2014, 10, 5, 2, 47, 28, 832666)
|
||||
test_weekday = 'Wednesday'
|
||||
meet_on_even = True
|
||||
next_meeting = recurrence.next_biweekly_meeting(
|
||||
test_time,
|
||||
test_weekday,
|
||||
meet_on_even=meet_on_even)
|
||||
expected_meeting = datetime.datetime(2014, 10, 8, 2, 47, 28, 832666)
|
||||
self.assertEqual(expected_meeting, next_meeting)
|
|
@ -1,36 +0,0 @@
|
|||
# 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 unittest
|
||||
|
||||
from yaml2ical import meeting
|
||||
from yaml2ical.tests import sample_data
|
||||
from yaml2ical import utils
|
||||
|
||||
|
||||
class UtilsTestCase(unittest.TestCase):
|
||||
|
||||
def test_exception_raised_when_conflict_detected(self):
|
||||
"""Exception is raised when a meeting conflict is detected."""
|
||||
meeting_one = meeting.load_meetings(sample_data.FIRST_MEETING_YAML)
|
||||
meeting_two = meeting.load_meetings(sample_data.SECOND_MEETING_YAML)
|
||||
meeting_list = [meeting_one.pop(), meeting_two.pop()]
|
||||
self.assertRaises(utils.MeetingConflictError,
|
||||
utils._check_for_meeting_conflicts,
|
||||
meeting_list)
|
||||
|
||||
def test_no_exception_raised_with_diff_irc_rooms(self):
|
||||
"""No exception raised when using different IRC rooms."""
|
||||
meeting_one = meeting.load_meetings(sample_data.FIRST_MEETING_YAML)
|
||||
meeting_two = meeting.load_meetings(sample_data.THIRD_MEETING_YAML)
|
||||
meeting_list = [meeting_one.pop(), meeting_two.pop()]
|
||||
utils._check_for_meeting_conflicts(meeting_list)
|
|
@ -1,102 +0,0 @@
|
|||
# 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 logging
|
||||
import os
|
||||
|
||||
from yaml2ical import meeting
|
||||
|
||||
"""Utility functions."""
|
||||
|
||||
|
||||
class MeetingConflictError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _extract_meeting_info(meeting_obj):
|
||||
"""Pull out meeting info of Meeting object.
|
||||
|
||||
:param meeting_obj: Meeting object
|
||||
:returns: a dictionary of meeting info
|
||||
|
||||
"""
|
||||
meeting_info = []
|
||||
for schedule in meeting_obj.schedules:
|
||||
info = {'name': meeting_obj.project,
|
||||
'filename': meeting_obj._filename,
|
||||
'day': schedule.day,
|
||||
'time': schedule.time,
|
||||
'irc_room': schedule.irc}
|
||||
meeting_info.append(info)
|
||||
|
||||
return meeting_info
|
||||
|
||||
|
||||
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)):
|
||||
meeting_info = _extract_meeting_info(meetings[i])
|
||||
for j in range(i + 1, len(meetings)):
|
||||
next_meeting_info = _extract_meeting_info(meetings[j])
|
||||
for current_meeting in meeting_info:
|
||||
for next_meeting in next_meeting_info:
|
||||
if current_meeting['day'] != next_meeting['day']:
|
||||
continue
|
||||
if current_meeting['time'] != next_meeting['time']:
|
||||
continue
|
||||
if current_meeting['irc_room'] != next_meeting['irc_room']:
|
||||
continue
|
||||
msg_dict = {'first': current_meeting['filename'],
|
||||
'second': next_meeting['filename']}
|
||||
raise MeetingConflictError("Conflict between %(first)s "
|
||||
"and %(second)s." % msg_dict)
|
||||
|
||||
|
||||
def convert_yaml_to_ical(yaml_dir, outputdir=None, outputfile=None):
|
||||
"""Convert meeting YAML files to iCal.
|
||||
|
||||
If meeting_list is specified, only those meetings in yaml_dir with
|
||||
filenames contained in meeting_list are converted; otherwise,
|
||||
all meeting in yaml_dir are converted.
|
||||
|
||||
:param yaml_dir: directory where meeting.yaml files are stored
|
||||
:param outputdir: location to store iCal files (one file per meeting)
|
||||
:param outputfile: output iCal file (one single file for all meetings)
|
||||
|
||||
"""
|
||||
meetings = meeting.load_meetings(yaml_dir)
|
||||
|
||||
# Check uniqueness and conflicts here before writing out to .ics
|
||||
_check_for_meeting_conflicts(meetings)
|
||||
|
||||
# convert meetings to a list of ical
|
||||
if outputdir:
|
||||
for m in meetings:
|
||||
cal = meeting.Yaml2IcalCalendar()
|
||||
m.add_to_calendar(cal)
|
||||
filename = os.path.basename(m._filename).split('.')[0] + '.ics'
|
||||
cal.write_to_disk(os.path.join(outputdir, filename))
|
||||
|
||||
# convert meetings into a single ical
|
||||
if outputfile:
|
||||
cal = meeting.Yaml2IcalCalendar()
|
||||
for m in meetings:
|
||||
m.add_to_calendar(cal)
|
||||
cal.write_to_disk(outputfile)
|
||||
|
||||
# TODO(jotan): verify converted ical is valid
|
||||
logging.info('Wrote %d meetings to iCal' % (len(meetings)))
|
Loading…
Reference in New Issue