|
@@ -1,7 +1,9 @@
|
|
|
#!/usr/bin/env python3
|
|
|
-from datetime import date,timedelta
|
|
|
-import sys, argparse
|
|
|
+from datetime import date, timedelta
|
|
|
+import sys
|
|
|
+import argparse
|
|
|
from argparse import RawTextHelpFormatter
|
|
|
+import yaml
|
|
|
|
|
|
# text appended to the end of generated emails
|
|
|
email_body = """
|
|
@@ -12,18 +14,22 @@ Other upcoming events:
|
|
|
https://cs.uwaterloo.ca/twiki/view/CrySP/UpcomingEvents
|
|
|
"""
|
|
|
|
|
|
+
|
|
|
def str_to_date(string_date):
|
|
|
+ if type(string_date) is date:
|
|
|
+ return string_date
|
|
|
try:
|
|
|
return date(*[int(s) for s in string_date.split('-')])
|
|
|
except:
|
|
|
print("invalid date: " + string_date)
|
|
|
sys.exit()
|
|
|
|
|
|
+
|
|
|
class CryspCalendar:
|
|
|
"""Representation of a CrySP weekly meeting schedule for a term"""
|
|
|
def __init__(self, start, end=None, weeks=None, names=[],
|
|
|
- constraints=[], practice_talks=[], half_practices=[], notes=[],
|
|
|
- no_repeat=False):
|
|
|
+ constraints=[], practice_talks=[], half_practices=[],
|
|
|
+ notes=[], irc_handles=dict(), no_repeat=False):
|
|
|
self.dates = self.meeting_dates(start, end, weeks)
|
|
|
self.names = names
|
|
|
self.constraints = self.parse_constraints(constraints)
|
|
@@ -31,9 +37,12 @@ class CryspCalendar:
|
|
|
self.practice_talks.update(self.parse_practice_talks(half_practices,
|
|
|
half=True))
|
|
|
self.notes = self.parse_notes(notes)
|
|
|
- self.notes.update(self.parse_notes(map(lambda l:l[1:], practice_talks)))
|
|
|
- self.notes.update(self.parse_notes(map(lambda l:l[1:], half_practices)))
|
|
|
+ self.notes.update(self.parse_notes(map(lambda l: l[1:],
|
|
|
+ practice_talks)))
|
|
|
+ self.notes.update(self.parse_notes(map(lambda l: l[1:],
|
|
|
+ half_practices)))
|
|
|
self.verify_constraints()
|
|
|
+ self.irc_handles = irc_handles
|
|
|
self.no_repeat = no_repeat
|
|
|
|
|
|
def meeting_dates(self, start, end=None, weeks=None):
|
|
@@ -53,18 +62,19 @@ class CryspCalendar:
|
|
|
return ret
|
|
|
|
|
|
def parse_constraints(self, constraints):
|
|
|
+ """takes an iterable of (name,datestr,...) tuples"""
|
|
|
constraint_dict = {}
|
|
|
for constraint in constraints:
|
|
|
name = constraint[0]
|
|
|
date_strs = constraint[1:]
|
|
|
dates = []
|
|
|
for date_str in date_strs:
|
|
|
- if '--' in date_str:
|
|
|
- #date_range = list(map(str_to_date, date_str.split('--')))
|
|
|
+ if type(date_str) is not date and '--' in date_str:
|
|
|
+ # date_range = list(map(str_to_date, date_str.split('--')))
|
|
|
date_range = date_str.split('--')
|
|
|
date_range = [str_to_date(date_range[0]),
|
|
|
str_to_date(date_range[1])]
|
|
|
- [dates.append(d) for d in self.dates \
|
|
|
+ [dates.append(d) for d in self.dates
|
|
|
if date_range[0] <= d <= date_range[1]]
|
|
|
else:
|
|
|
dates.append(str_to_date(date_str))
|
|
@@ -72,25 +82,27 @@ class CryspCalendar:
|
|
|
return constraint_dict
|
|
|
|
|
|
def parse_practice_talks(self, practice, half=False):
|
|
|
+ """'practice' is an iterable of (name, day, note) tuples"""
|
|
|
practice_dict = {}
|
|
|
for talk in practice:
|
|
|
name = talk[0]
|
|
|
- day = str_to_date(talk[1])
|
|
|
+ day = str_to_date(talk[1])
|
|
|
if day in practice_dict:
|
|
|
assert half,\
|
|
|
'conflicting talks: %s and %s on %s' %\
|
|
|
(practice_dict[day][0], name, day)
|
|
|
practice_dict[day].append(name)
|
|
|
elif half:
|
|
|
- practice_dict[day] = [name,]
|
|
|
+ practice_dict[day] = [name, ]
|
|
|
else:
|
|
|
practice_dict[day] = [name, '']
|
|
|
return practice_dict
|
|
|
|
|
|
def parse_notes(self, notes):
|
|
|
+ """takes an iterable of (day, note) tuples"""
|
|
|
notes_dict = {}
|
|
|
for note in notes:
|
|
|
- day = str_to_date(note[0])
|
|
|
+ day = str_to_date(note[0])
|
|
|
message = note[1]
|
|
|
assert day not in notes_dict or notes_dict[day] == message,\
|
|
|
'conflicting notes: "%s" and "%s" on %s' %\
|
|
@@ -102,7 +114,7 @@ class CryspCalendar:
|
|
|
for name in self.constraints:
|
|
|
for day in self.constraints[name]:
|
|
|
assert day in self.dates,\
|
|
|
- 'constraint added for %s on %s, which is not a meeting date' %\
|
|
|
+ "constraint %s on %s isnt a meeting date" %\
|
|
|
(name, day)
|
|
|
for day in self.practice_talks:
|
|
|
assert day in self.dates,\
|
|
@@ -122,8 +134,8 @@ class CryspCalendar:
|
|
|
if i < len(self.practice_talks[day]):
|
|
|
name = self.practice_talks[day][i]
|
|
|
if name is None:
|
|
|
- constrained_names = (name for name in names \
|
|
|
- if name not in self.constraints \
|
|
|
+ constrained_names = (name for name in names
|
|
|
+ if name not in self.constraints
|
|
|
or day not in self.constraints[name])
|
|
|
name = next(constrained_names, None)
|
|
|
if name and name in names:
|
|
@@ -144,18 +156,21 @@ class CryspCalendar:
|
|
|
note = self.notes.get(day, '')
|
|
|
return speaker1, speaker2, note
|
|
|
|
|
|
- def table(self):
|
|
|
+ def table(self, today=date.today()):
|
|
|
"""Returns a string containing a twiki table."""
|
|
|
table = '| *Date* | *Speakers* | *Notes* |\n'
|
|
|
names = self.names.copy()
|
|
|
for day in self.dates:
|
|
|
- table += day.strftime('| %b %e |')
|
|
|
+ color = "%SILVER% " if day <= today else ""
|
|
|
+ endcolor = " %ENDCOLOR%" if day <= today else ""
|
|
|
speaker1, speaker2, note = self.date_to_speakers(day, names)
|
|
|
- if speaker1:
|
|
|
- table += ' ' + speaker1
|
|
|
- if speaker2:
|
|
|
- table += ', ' + speaker2
|
|
|
- table += ' | ' + note + ' |\n'
|
|
|
+ speaker2 = ', ' + speaker2 if speaker2 else ""
|
|
|
+ row = "| {}{}{} | {}{}{}{} | {}{}{} |\n".format(
|
|
|
+ color, day.strftime('%b %e'), endcolor,
|
|
|
+ color, speaker1, speaker2, endcolor,
|
|
|
+ color, note, endcolor
|
|
|
+ )
|
|
|
+ table += row
|
|
|
return table
|
|
|
|
|
|
def email(self, weeks=2, today=date.today()):
|
|
@@ -175,7 +190,7 @@ class CryspCalendar:
|
|
|
if speaker1:
|
|
|
emailstr += speaker1
|
|
|
if speaker2:
|
|
|
- emailstr += ', ' + speaker2
|
|
|
+ emailstr += ', ' + speaker2
|
|
|
if note != '':
|
|
|
emailstr += ' (' + note + ')'
|
|
|
else:
|
|
@@ -184,82 +199,171 @@ class CryspCalendar:
|
|
|
emailstr += email_body
|
|
|
return emailstr
|
|
|
|
|
|
+ def irc(self, today=date.today()):
|
|
|
+ """Returns a string for putting in the IRC channel topic"""
|
|
|
+ names = self.names.copy()
|
|
|
+ i = 0
|
|
|
+ for day in self.dates:
|
|
|
+ if day <= today:
|
|
|
+ self.date_to_speakers(day, names)
|
|
|
+ continue
|
|
|
+ i += 1
|
|
|
+ speaker1, speaker2, note = self.date_to_speakers(day, names)
|
|
|
+ if speaker1:
|
|
|
+ speaker1 = self.irc_handles.get(speaker1, speaker1)
|
|
|
+ if speaker2:
|
|
|
+ speaker2 = ', ' + self.irc_handles.get(speaker2, speaker2)
|
|
|
+ if note != '':
|
|
|
+ note = ' (' + note + ')'
|
|
|
+ return "{}{}{}".format(speaker1, speaker2, note)
|
|
|
+
|
|
|
+
|
|
|
def parse_constraintsfile(filename):
|
|
|
with open(filename, 'r') as f:
|
|
|
tweaks = {'c': [], 'p': [], 'h': [], 'n': []}
|
|
|
for line in f:
|
|
|
- l = [x.strip() for x in line.rstrip().split(',')]
|
|
|
- tweaks[l[0]].append(l[1:])
|
|
|
+ line = [x.strip() for x in line.rstrip().split(',')]
|
|
|
+ tweaks[line[0]].append(line[1:])
|
|
|
return tweaks['c'], tweaks['p'], tweaks['h'], tweaks['n']
|
|
|
|
|
|
+
|
|
|
+def parse_yaml(filename, today=date.today()):
|
|
|
+ with open(filename, 'r') as f:
|
|
|
+ struct = yaml.safe_load(f)
|
|
|
+ currentTerm = None
|
|
|
+ for term in struct['terms'].values():
|
|
|
+ if 'start' in term and 'end' in term:
|
|
|
+ term['end'] = str_to_date(term['end'])
|
|
|
+ term['start'] = str_to_date(term['start'])
|
|
|
+ if currentTerm is None or (term['end'] >= today and
|
|
|
+ term['start'] > currentTerm['start']):
|
|
|
+ currentTerm = term
|
|
|
+ names = currentTerm['speakers']
|
|
|
+ for name in names:
|
|
|
+ assert name not in currentTerm['out']
|
|
|
+
|
|
|
+ def triple_check(field_name, location):
|
|
|
+ return field_name in location and \
|
|
|
+ location[field_name] is not None and \
|
|
|
+ len(location[field_name]) > 0
|
|
|
+
|
|
|
+ constraints = [[name] + struct['speakers'][name]['constraints']
|
|
|
+ for name in names
|
|
|
+ if triple_check('constraints', struct['speakers'][name])]
|
|
|
+ practice_talks = [(talk['name'], talk['date'], talk['note'])
|
|
|
+ for talk in currentTerm['practice_talks']]
|
|
|
+ half_practices = [(talk['name'], talk['date'], talk['note'])
|
|
|
+ for talk in currentTerm['half_practices']]
|
|
|
+ for speaker in struct['speakers']:
|
|
|
+ location = struct['speakers'][speaker]
|
|
|
+ if triple_check('practice_talks', location):
|
|
|
+ practice_talks += {(speaker, talk['date'], talk['note'])
|
|
|
+ for talk in location['practice_talks']}
|
|
|
+ if triple_check('half_practices', location):
|
|
|
+ half_practices += {(speaker, talk['date'], talk['note'])
|
|
|
+ for talk in location['half_practices']}
|
|
|
+ notes = [(note['date'], note['note']) for note in currentTerm['notes']]
|
|
|
+ irc_handles = {name: struct['speakers'][name]['irc']
|
|
|
+ for name in struct['speakers']
|
|
|
+ if 'irc' in struct['speakers'][name]}
|
|
|
+ return CryspCalendar(currentTerm['start'], currentTerm['end'],
|
|
|
+ names=names, constraints=constraints,
|
|
|
+ practice_talks=practice_talks,
|
|
|
+ half_practices=half_practices,
|
|
|
+ notes=notes, irc_handles=irc_handles)
|
|
|
+
|
|
|
+
|
|
|
def make_parser():
|
|
|
- parser = argparse.ArgumentParser(description ='''\
|
|
|
+ parser = argparse.ArgumentParser(description='''\
|
|
|
Print a speaker schedule for the CrySP weekly meetings.
|
|
|
-All dates are ISO formatted (yyyy-mm-dd). Constraints in the constraints file
|
|
|
-can also be ranges of ISO formatted dates (yyyy-mm-dd--yyyy-mm-dd).''',
|
|
|
- formatter_class=RawTextHelpFormatter)
|
|
|
+All dates are ISO formatted (yyyy-mm-dd). Constraints in files can also be
|
|
|
+ranges of ISO formatted dates (yyyy-mm-dd--yyyy-mm-dd).''',
|
|
|
+ epilog='''\
|
|
|
+Now that the lab is so large, you likely want to use one of the file options,
|
|
|
+probably YAML. The command line options still function if you want to play
|
|
|
+with how this script works to learn, though.
|
|
|
+The YAML file has the following format:
|
|
|
+There are two main objects, a "terms" object that stores data about particular
|
|
|
+terms, and a "speakers" ojbect that stores data about the speakers.
|
|
|
+The "terms" object contains any number of term objects with any name. Each
|
|
|
+term has the following objects:
|
|
|
+ start: the date of the first meeting
|
|
|
+ end: a date on and beyond which there are no more meetings
|
|
|
+ speakers: an ordered list of names for each speaker this term
|
|
|
+ out: a list to store lab members who won't speak this term (ignored)
|
|
|
+ practice_talks: a list of practice talks, of the form
|
|
|
+ {date: _, name: _, note: _}
|
|
|
+ half_practices: a list of half practice talks (format is the same)
|
|
|
+ notes: notes, of the form
|
|
|
+ {date: _, note: _}
|
|
|
+The "speakers" object contains any number of speaker objects. The name of each
|
|
|
+speaker object should be the same as the name in the speakers list, if they
|
|
|
+are speaking this term. The names can have spaces. Each speaker object has
|
|
|
+the following objects:
|
|
|
+ irc: the IRC handle of the speaker, if they're in CrySP IRC
|
|
|
+ email: the email address of the speaker (check the UW whitepages if need be)
|
|
|
+ constraints: a list of dates/date ranges the speaker cannot speak on
|
|
|
+ practice_talks: a list of practice talks, each of the form:
|
|
|
+ {date: _, note: _}
|
|
|
+ half_practices: a list of half practice talks (format is the same)
|
|
|
+''',
|
|
|
+ formatter_class=RawTextHelpFormatter)
|
|
|
+ parser.add_argument('-y', '--yaml',
|
|
|
+ metavar='FILE',
|
|
|
+ help='full description of the term via YAML file.')
|
|
|
parser.add_argument('-s', '--start',
|
|
|
- metavar = 'DATE', required = True,
|
|
|
- help =
|
|
|
- 'date of the first meeting')
|
|
|
- parser.add_argument('-e', '--end', metavar = 'DATE',
|
|
|
- help =
|
|
|
- 'last date to schedule a meeting on or before\n' \
|
|
|
+ metavar='DATE',
|
|
|
+ help='date of the first meeting')
|
|
|
+ parser.add_argument('-e', '--end', metavar='DATE',
|
|
|
+ help='last date to schedule a meeting on or before\n'
|
|
|
'(if -w also specified, uses whatever is shortest)')
|
|
|
parser.add_argument('-w', '--weeks', type=int,
|
|
|
- help =
|
|
|
- 'number of weeks to schedule for\n' \
|
|
|
+ help='number of weeks to schedule for\n'
|
|
|
'(if -e also specified, uses whatever is shortest)')
|
|
|
|
|
|
group = parser.add_mutually_exclusive_group()
|
|
|
group.add_argument('-n', '--names',
|
|
|
- nargs = '+', default = [], metavar = 'NAME',
|
|
|
- help =
|
|
|
- 'names to schedule in their speaking order')
|
|
|
+ nargs='+', default=[], metavar='NAME',
|
|
|
+ help='names to schedule in their speaking order')
|
|
|
group.add_argument('-f', '--namesfile',
|
|
|
- metavar = 'FILE',
|
|
|
- help =
|
|
|
- 'path of a file that contains a comma-seperated\n' \
|
|
|
- ' list of names to schedule in their speaking order')
|
|
|
+ metavar='FILE',
|
|
|
+ help='path of a file that contains a comma-seperated\n'
|
|
|
+ 'list of names to schedule in their speaking order')
|
|
|
|
|
|
parser.add_argument('-c', '--constraintsfile',
|
|
|
- metavar = 'FILE',
|
|
|
- help =
|
|
|
- 'Provide constraints, practice talks, and notes\n' \
|
|
|
- 'via supplied CSV file. The CSV can contain the\n' \
|
|
|
- 'following lines:\n' \
|
|
|
- 'constraints - dates where someone cannot speak:\n' \
|
|
|
- '"c,[name],[date/range1],[date/range2],[...]"\n' \
|
|
|
- 'notes - goes in the notes column for that date:\n' \
|
|
|
- '"n,[date],[note]"\n' \
|
|
|
- 'practice talks - dates where something other than\n' \
|
|
|
- 'the usual CrySP meeting talks is happening:\n' \
|
|
|
- '"p,[name],[date],[note (e.g., "practice talk")]"\n' \
|
|
|
- 'half-slot - a rare case where someone/thing needs\n' \
|
|
|
- 'only one of the speaker slots on this date:\n' \
|
|
|
+ metavar='FILE',
|
|
|
+ help='Provide constraints, practice talks, and notes\n'
|
|
|
+ 'via supplied CSV file. The CSV can contain the\n'
|
|
|
+ 'following lines:\n'
|
|
|
+ 'constraints - dates where someone cannot speak:\n'
|
|
|
+ '"c,[name],[date/range1],[date/range2],[...]"\n'
|
|
|
+ 'notes - goes in the notes column for that date:\n'
|
|
|
+ '"n,[date],[note]"\n'
|
|
|
+ 'practice talks - dates where something other than\n'
|
|
|
+ 'the usual CrySP meeting talks is happening:\n'
|
|
|
+ '"p,[name],[date],[note (e.g., "practice talk")]"\n'
|
|
|
+ 'half-slot - a rare case where someone/thing needs\n'
|
|
|
+ 'only one of the speaker slots on this date:\n'
|
|
|
'"h,[name],[date],[note]"\n')
|
|
|
|
|
|
parser.add_argument('-r', '--no-repeat',
|
|
|
- action = 'store_true',
|
|
|
- help =
|
|
|
- 'disables repeating the list of names')
|
|
|
+ action='store_true',
|
|
|
+ help='disables repeating the list of names')
|
|
|
parser.add_argument('-E', '--email',
|
|
|
- nargs = '?', const = 2, default = False,
|
|
|
- metavar = 'WEEKS',
|
|
|
- help =
|
|
|
- 'print an email for notifying who is speaking\n' \
|
|
|
+ nargs='?', const=2, default=False,
|
|
|
+ metavar='WEEKS',
|
|
|
+ help='print an email for notifying who is speaking\n'
|
|
|
'the next two (or specified) weeks')
|
|
|
+ parser.add_argument('-i', '--irc',
|
|
|
+ action='store_true',
|
|
|
+ help='print names formatted for an irc channel topic')
|
|
|
+ parser.add_argument('-d', '--today',
|
|
|
+ default=date.today(), metavar='DATE',
|
|
|
+ help='pretend the script is being run on the day DATE')
|
|
|
return parser
|
|
|
|
|
|
-def main(argv=None):
|
|
|
- if argv is None:
|
|
|
- argv = sys.argv
|
|
|
- parser = make_parser()
|
|
|
- args = parser.parse_args()
|
|
|
-
|
|
|
- if not args.end and not args.weeks:
|
|
|
- parser.error('requires -w/--weeks or -e/--end')
|
|
|
|
|
|
+def old_format(args):
|
|
|
if args.namesfile:
|
|
|
with open(args.namesfile, 'r') as f:
|
|
|
names = [x.strip() for x in f.read().split(',')]
|
|
@@ -269,15 +373,36 @@ def main(argv=None):
|
|
|
if args.constraintsfile:
|
|
|
constraints, practice, halves, notes = \
|
|
|
parse_constraintsfile(args.constraintsfile)
|
|
|
- cal = CryspCalendar(args.start, args.end, args.weeks, names,
|
|
|
- constraints, practice, halves, notes,
|
|
|
- args.no_repeat)
|
|
|
+ return CryspCalendar(args.start, args.end, args.weeks, names,
|
|
|
+ constraints, practice, halves, notes,
|
|
|
+ args.no_repeat)
|
|
|
+ return CryspCalendar(args.start, args.end, args.weeks, names,
|
|
|
+ no_repeat=args.no_repeat)
|
|
|
+
|
|
|
+
|
|
|
+def main(argv=None):
|
|
|
+ if argv is None:
|
|
|
+ argv = sys.argv
|
|
|
+ parser = make_parser()
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ if not args.end and not args.weeks and not args.yaml:
|
|
|
+ parser.error('requires -w/--weeks or -e/--end')
|
|
|
+
|
|
|
+ if not args.yaml:
|
|
|
+ cal = old_format(args)
|
|
|
+ else:
|
|
|
+ cal = parse_yaml(args.yaml)
|
|
|
+
|
|
|
+ today = str_to_date(args.today)
|
|
|
+ if args.email:
|
|
|
+ print(cal.email(int(args.email), today))
|
|
|
+ elif args.irc:
|
|
|
+ print(cal.irc(today))
|
|
|
else:
|
|
|
- cal = CryspCalendar(args.start, args.end, args.weeks, names,
|
|
|
- no_repeat=args.no_repeat)
|
|
|
+ print(cal.table(today))
|
|
|
+ return 0
|
|
|
|
|
|
- print(cal.table() if not args.email else cal.email(int(args.email)))
|
|
|
- return 0;
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
sys.exit(main())
|