#!/usr/bin/python3 # Makes the weekly meeting schedule, following some rules: # * Recognize a bunch of US-centric holidays and move meeting from Mon -> Tue # * Put in special notices when US daylight/standard changes occur # * Never hold a meeting from December 23 through December 31 inclusive # # Using the script: # - it's recommended to use a venv # - pip install -r requirements.txt # - python generate_calendar.py prune 2022 # - python generate_calendar.py generate 2024 # - git commit meeting.ical -m"update for 2024" import datetime import pathlib import sys import click import pytz import icalendar from holidays.countries.united_states import UnitedStates from datetime import date from dateutil.relativedelta import relativedelta as rd, MO from holidays.constants import OCT, NOV CALENDAR_FILE = pathlib.Path("meeting.ical") class CircuitPythonHoliday(UnitedStates): def _populate(self, year): super()._populate(year) for k, v in list(self.items()): if 'Lincoln' in v or 'Columbus' in v: del self[k] self[date(year, OCT, 1) + rd(weekday=MO(+2))] = "Indigenous Peoples' Day" self[date(year, NOV, 11)] = "Veterans Day" hols = CircuitPythonHoliday(state='NY') tz = pytz.timezone('US/Eastern') meeting_duration = datetime.timedelta(seconds=3600) def localize(d): d = tz.localize(d) return d def first_monday(year): d = datetime.datetime(year, 1, 1, 14) while d.weekday() != 0: d += datetime.timedelta(days=1) return d def add_holiday_notice(calendar, d, note): d = localize(d) event = icalendar.Event() event.add('summary', note + ' -- Meeting Postponed due to holiday') event.add('dtstart', icalendar.vDatetime(d)) event.add('dtend', icalendar.vDatetime(d + meeting_duration)) event.add('dtstamp', localize(datetime.datetime(d.year, 1, 1))) calendar.add_component(event) def add_meeting_notice(calendar, d, note): d = localize(d) event = icalendar.Event() event.add('summary', 'CircuitPython Discord Meeting' + note) event.add('dtstart', icalendar.vDatetime(d)) event.add('dtend', icalendar.vDatetime(d + meeting_duration)) event.add('dtstamp', localize(datetime.datetime(d.year, 1, 1))) if 0: # This doesn't work, makes google not show the calendar at all event.add('conference', 'https://adafru.it/discord', parameters= {'VALUE':'URI'}) calendar.add_component(event) def make_calendar(calendar, year): d0 = first_monday(year) olddst = None while d0 < datetime.datetime(year, 12, 23): d = d0 hol = hols.get(d, None) if hol is not None: add_holiday_notice(calendar, d, hol) d = d + datetime.timedelta(days=1) dst = tz.utcoffset(d) if dst != olddst: note = '\n(2PM in UTC%+d)' % ( dst.total_seconds()//3600) olddst = dst else: note = '' add_meeting_notice(calendar, d, note) d0 += datetime.timedelta(days=7) def empty_calendar(): calendar = icalendar.Calendar() calendar.add('prodid', '-//circuitpython weekly meeting generator//circuitpython.org//') calendar.add('version', '0.0.0-beta0') return calendar @click.group @click.pass_context def cli(ctx): if CALENDAR_FILE.exists(): with CALENDAR_FILE.open('rb') as f: content = f.read() calendar = icalendar.Calendar.from_ical(content) else: calendar = empty_calendar() ctx.obj = calendar @cli.command @click.argument("year", type=int) @click.pass_context def generate(ctx, year): calendar = ctx.obj calendar.subcomponents = [ component for component in calendar.subcomponents if component.name != 'VEVENT' or component['DTSTART'].dt.year != year] make_calendar(calendar, year) with CALENDAR_FILE.open('wb') as f: f.write(calendar.to_ical()) @cli.command @click.argument("year", type=int) @click.pass_context def prune(ctx, year): thresh = localize(datetime.datetime(year+1, 1, 1)) calendar = ctx.obj calendar.subcomponents = [ component for component in calendar.subcomponents if component.name != 'VEVENT' or component['DTSTART'].dt >= thresh] with CALENDAR_FILE.open('wb') as f: f.write(calendar.to_ical()) if __name__ == '__main__': cli()