|
| 1 | +"""Sphinx extension to generate a list of upcoming meeting dates.""" |
| 2 | + |
| 3 | +import datetime as dt |
| 4 | +import os |
| 5 | + |
| 6 | +from docutils import nodes |
| 7 | +from sphinx.util.docutils import SphinxDirective |
| 8 | + |
| 9 | + |
| 10 | +def utc_hour(date): |
| 11 | + if 4 <= date.month <= 10: |
| 12 | + # Daylight saving time in Europe and the US |
| 13 | + return 19 if date.month % 2 == 0 else 16 |
| 14 | + else: |
| 15 | + # Winter time in Europe and the US |
| 16 | + return 20 if date.month % 2 == 0 else 17 |
| 17 | + |
| 18 | + |
| 19 | +def first_tuesday(year, month): |
| 20 | + first = dt.date(year, month, 1) |
| 21 | + days_ahead = (1 - first.weekday()) % 7 |
| 22 | + return first + dt.timedelta(days=days_ahead) |
| 23 | + |
| 24 | + |
| 25 | +def upcoming_meetings(today, count): |
| 26 | + meetings = [] |
| 27 | + year, month = today.year, today.month |
| 28 | + while len(meetings) < count: |
| 29 | + meeting_date = first_tuesday(year, month) |
| 30 | + if meeting_date >= today: |
| 31 | + meetings.append((meeting_date, utc_hour(meeting_date))) |
| 32 | + month += 1 |
| 33 | + if month > 12: |
| 34 | + month = 1 |
| 35 | + year += 1 |
| 36 | + return meetings |
| 37 | + |
| 38 | + |
| 39 | +def past_meetings(today, count): |
| 40 | + meetings = [] |
| 41 | + year, month = today.year, today.month |
| 42 | + while len(meetings) < count: |
| 43 | + meeting_date = first_tuesday(year, month) |
| 44 | + if meeting_date < today: |
| 45 | + meetings.append((meeting_date, utc_hour(meeting_date))) |
| 46 | + month -= 1 |
| 47 | + if month < 1: |
| 48 | + month = 12 |
| 49 | + year -= 1 |
| 50 | + meetings.reverse() |
| 51 | + return meetings |
| 52 | + |
| 53 | + |
| 54 | +class MeetingDatesDirective(SphinxDirective): |
| 55 | + has_content = False |
| 56 | + |
| 57 | + def run(self): |
| 58 | + bullets = nodes.bullet_list() |
| 59 | + for date, hour in upcoming_meetings(dt.date.today(), 6): |
| 60 | + item = nodes.list_item() |
| 61 | + text = f"{date.strftime('%B %d, %Y')} - {hour:02d}:00 UTC" |
| 62 | + url = f"https://arewemeetingyet.com/UTC/{date.isoformat()}/{hour}:00/Docs Community Meeting" |
| 63 | + |
| 64 | + paragraph = nodes.paragraph() |
| 65 | + ref = nodes.reference("", text, refuri=url) |
| 66 | + paragraph += ref |
| 67 | + item += paragraph |
| 68 | + bullets += item |
| 69 | + |
| 70 | + return [bullets] |
| 71 | + |
| 72 | + |
| 73 | +def generate_ics(app, exception): |
| 74 | + if exception: |
| 75 | + return |
| 76 | + |
| 77 | + lines = [ |
| 78 | + "BEGIN:VCALENDAR", |
| 79 | + "VERSION:2.0", |
| 80 | + "PRODID:-//Python Docs WG//Meeting dates//EN", |
| 81 | + "X-WR-CALDESC:Python Docs WG meetings from https://docs-community.readthedocs.io/", |
| 82 | + "X-WR-CALNAME:Python Docs WG meetings", |
| 83 | + ] |
| 84 | + today = dt.date.today() |
| 85 | + meetings = past_meetings(today, 12) + upcoming_meetings(today, 12) |
| 86 | + for date, hour in meetings: |
| 87 | + start = dt.datetime(date.year, date.month, date.day, hour, 0, 0) |
| 88 | + end = start + dt.timedelta(hours=1) |
| 89 | + lines += [ |
| 90 | + "BEGIN:VEVENT", |
| 91 | + f"UID:{start.strftime('%Y%m%dT%H%M%SZ')}@python-docs-community", |
| 92 | + f"DTSTAMP:{dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ')}", |
| 93 | + f"DTSTART:{start.strftime('%Y%m%dT%H%M%SZ')}", |
| 94 | + f"DTEND:{end.strftime('%Y%m%dT%H%M%SZ')}", |
| 95 | + "SUMMARY:Python Docs WG", |
| 96 | + f"URL:https://arewemeetingyet.com/UTC/{date.isoformat()}/{hour}:00/Python Docs WG meeting", |
| 97 | + "END:VEVENT", |
| 98 | + ] |
| 99 | + lines += ["END:VCALENDAR"] |
| 100 | + ics = "\r\n".join(lines) + "\r\n" |
| 101 | + |
| 102 | + with open(os.path.join(app.outdir, "docs-community-meetings.ics"), "w") as f: |
| 103 | + f.write(ics) |
| 104 | + |
| 105 | + |
| 106 | +def setup(app): |
| 107 | + app.add_directive("meeting-dates", MeetingDatesDirective) |
| 108 | + app.connect("build-finished", generate_ics) |
| 109 | + return {"version": "1.0", "parallel_read_safe": True} |
0 commit comments