|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import datetime as dt |
3 | 4 | import dataclasses |
4 | 5 | import json |
5 | 6 |
|
6 | 7 | from release_management import ROOT_DIR, load_python_releases |
7 | 8 |
|
8 | 9 | TYPE_CHECKING = False |
9 | 10 | if TYPE_CHECKING: |
10 | | - from release_management import VersionMetadata |
| 11 | + from release_management import ReleaseInfo, VersionMetadata |
| 12 | + |
| 13 | +# Seven years captures the full lifecycle from prereleases to end-of-life |
| 14 | +TODAY = dt.date.today() |
| 15 | +SEVEN_YEARS_AGO = TODAY.replace(year=TODAY.year - 7) |
| 16 | + |
| 17 | +# https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11 |
| 18 | +CALENDAR_ESCAPE_TEXT = str.maketrans({ |
| 19 | + '\\': r'\\', |
| 20 | + ';': r'\;', |
| 21 | + ',': r'\,', |
| 22 | + '\n': r'\n', |
| 23 | +}) |
11 | 24 |
|
12 | 25 |
|
13 | 26 | def create_release_json() -> str: |
@@ -48,3 +61,55 @@ def version_info(metadata: VersionMetadata, /) -> dict[str, str | int]: |
48 | 61 | 'end_of_life': end_of_life, |
49 | 62 | 'release_manager': metadata.release_manager, |
50 | 63 | } |
| 64 | + |
| 65 | + |
| 66 | +def create_release_schedule_calendar() -> str: |
| 67 | + python_releases = load_python_releases() |
| 68 | + releases = [] |
| 69 | + for version, all_releases in python_releases.releases.items(): |
| 70 | + pep_number = python_releases.metadata[version].pep |
| 71 | + for release in all_releases: |
| 72 | + # Keep size reasonable by omitting releases older than 7 years |
| 73 | + if release.date < SEVEN_YEARS_AGO: |
| 74 | + continue |
| 75 | + releases.append((pep_number, release)) |
| 76 | + releases.sort(key=lambda r: r[1].date) |
| 77 | + lines = release_schedule_calendar_lines(releases) |
| 78 | + return '\r\n'.join(lines) |
| 79 | + |
| 80 | + |
| 81 | +def release_schedule_calendar_lines( |
| 82 | + releases: list[tuple[int, ReleaseInfo]], / |
| 83 | +) -> list[str]: |
| 84 | + dtstamp = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ') |
| 85 | + |
| 86 | + lines = [ |
| 87 | + 'BEGIN:VCALENDAR', |
| 88 | + 'VERSION:2.0', |
| 89 | + 'PRODID:-//Python Software Foundation//Python release schedule//EN', |
| 90 | + 'X-WR-CALDESC:Python releases schedule from https://peps.python.org', |
| 91 | + 'X-WR-CALNAME:Python releases schedule', |
| 92 | + ] |
| 93 | + for pep_number, release in releases: |
| 94 | + normalised_stage = release.stage.replace(' ', '') |
| 95 | + normalised_stage = normalised_stage.translate(CALENDAR_ESCAPE_TEXT) |
| 96 | + if release.note: |
| 97 | + normalised_note = release.note.translate(CALENDAR_ESCAPE_TEXT) |
| 98 | + note = (f'DESCRIPTION:Note: {normalised_note}',) |
| 99 | + else: |
| 100 | + note = () |
| 101 | + lines += ( |
| 102 | + 'BEGIN:VEVENT', |
| 103 | + f'DTSTAMP:{dtstamp}', |
| 104 | + f'UID:python-{normalised_stage}@releases.python.org', |
| 105 | + f'DTSTART;VALUE=DATE:{release.date.strftime("%Y%m%d")}', |
| 106 | + f'SUMMARY:Python {release.stage}', |
| 107 | + *note, |
| 108 | + f'URL:https://peps.python.org/pep-{pep_number:04d}/', |
| 109 | + 'END:VEVENT', |
| 110 | + ) |
| 111 | + lines += ( |
| 112 | + 'END:VCALENDAR', |
| 113 | + '', |
| 114 | + ) |
| 115 | + return lines |
0 commit comments