|
| 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): |
| 26 | + meetings = [] |
| 27 | + year, month = today.year, today.month |
| 28 | + while len(meetings) < 6: # Max of six meeting entries |
| 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 | +class MeetingDatesDirective(SphinxDirective): |
| 40 | + has_content = False |
| 41 | + |
| 42 | + def run(self): |
| 43 | + today = dt.date.today() |
| 44 | + meetings = upcoming_meetings(today) |
| 45 | + self.env.meeting_dates = meetings |
| 46 | + |
| 47 | + bullets = nodes.bullet_list() |
| 48 | + for date, hour in meetings: |
| 49 | + item = nodes.list_item() |
| 50 | + text = f"{date.strftime('%B %d, %Y')} - {hour:02d}:00 UTC" |
| 51 | + url = f"https://arewemeetingyet.com/UTC/{date.isoformat()}/{hour}:00/Docs Community Meeting" |
| 52 | + |
| 53 | + paragraph = nodes.paragraph() |
| 54 | + ref = nodes.reference("", text, refuri=url) |
| 55 | + paragraph += ref |
| 56 | + item += paragraph |
| 57 | + bullets += item |
| 58 | + |
| 59 | + return [bullets] |
| 60 | + |
| 61 | + |
| 62 | +def generate_ics(app, exception): |
| 63 | + if exception: |
| 64 | + return |
| 65 | + |
| 66 | + meetings = app.env.meeting_dates |
| 67 | + |
| 68 | + lines = [ |
| 69 | + "BEGIN:VCALENDAR", |
| 70 | + "VERSION:2.0", |
| 71 | + "PRODID:-//Python Docs Community//Meeting Dates//EN", |
| 72 | + ] |
| 73 | + for date, hour in meetings: |
| 74 | + start = dt.datetime(date.year, date.month, date.day, hour, 0, 0) |
| 75 | + end = start + dt.timedelta(hours=1) |
| 76 | + lines += [ |
| 77 | + "BEGIN:VEVENT", |
| 78 | + f"DTSTART:{start.strftime('%Y%m%dT%H%M%SZ')}", |
| 79 | + f"DTEND:{end.strftime('%Y%m%dT%H%M%SZ')}", |
| 80 | + "SUMMARY:Docs Community Meeting", |
| 81 | + f"URL:https://arewemeetingyet.com/UTC/{date.isoformat()}/{hour}:00/Docs Community Meeting", |
| 82 | + "END:VEVENT", |
| 83 | + ] |
| 84 | + lines += ["END:VCALENDAR"] |
| 85 | + ics = ( |
| 86 | + "\r\n".join(lines) + "\r\n" |
| 87 | + ) # Required by spec for some reason: https://datatracker.ietf.org/doc/html/rfc5545#section-3.1 |
| 88 | + |
| 89 | + path = os.path.join(app.outdir, "docs-community-meetings.ics") |
| 90 | + with open(path, "w") as f: |
| 91 | + f.write(ics) |
| 92 | + |
| 93 | + |
| 94 | +def setup(app): |
| 95 | + app.add_directive("meeting-dates", MeetingDatesDirective) |
| 96 | + app.connect("build-finished", generate_ics) |
| 97 | + return {"version": "1.0", "parallel_read_safe": True} |
0 commit comments