|
| 1 | +""" |
| 2 | +Generate email content for PHEP 3 quarterly reminders. |
| 3 | +Parses schedule.md and extracts the current quarter's information. |
| 4 | +""" |
| 5 | + |
| 6 | +import re |
| 7 | +from datetime import datetime |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | + |
| 11 | +def get_current_quarter(): |
| 12 | + """Return current year and quarter (1-4).""" |
| 13 | + now = datetime.now() |
| 14 | + quarter = (now.month - 1) // 3 + 1 |
| 15 | + return now.year, quarter |
| 16 | + |
| 17 | + |
| 18 | +def parse_schedule(schedule_path): |
| 19 | + """Parse schedule.md and return dict of quarters with their content.""" |
| 20 | + content = Path(schedule_path).read_text() |
| 21 | + |
| 22 | + # Split by quarter headers (#### YYYY - Quarter N:) |
| 23 | + quarter_pattern = r'#### (\d{4}) - Quarter (\d):' |
| 24 | + sections = re.split(quarter_pattern, content) |
| 25 | + |
| 26 | + quarters = {} |
| 27 | + # sections[0] is empty/before first match, then year, quarter, content triplets |
| 28 | + for i in range(1, len(sections) - 2, 3): |
| 29 | + year = int(sections[i]) |
| 30 | + quarter = int(sections[i + 1]) |
| 31 | + quarter_content = sections[i + 2].strip() |
| 32 | + quarters[(year, quarter)] = quarter_content |
| 33 | + |
| 34 | + return quarters |
| 35 | + |
| 36 | + |
| 37 | +def parse_table(table_text): |
| 38 | + """Parse a markdown table and return list of (package, version, info) tuples.""" |
| 39 | + lines = table_text.strip().split('\n') |
| 40 | + rows = [] |
| 41 | + for line in lines: |
| 42 | + # Skip header and separator rows |
| 43 | + if line.startswith('|') and '---' not in line: |
| 44 | + cells = [c.strip() for c in line.split('|')[1:-1]] |
| 45 | + if len(cells) >= 3 and cells[0]: # Skip empty header rows |
| 46 | + rows.append((cells[0], cells[1], cells[2])) |
| 47 | + return rows |
| 48 | + |
| 49 | + |
| 50 | +def extract_quarter_data(quarter_content): |
| 51 | + """Extract adopt and drop tables from quarter content.""" |
| 52 | + adopt_match = re.search( |
| 53 | + r'###### Adopt support for:\s*\n((?:\|.*\n)+)', |
| 54 | + quarter_content |
| 55 | + ) |
| 56 | + drop_match = re.search( |
| 57 | + r'###### Can drop support for:\s*\n((?:\|.*\n)+)', |
| 58 | + quarter_content |
| 59 | + ) |
| 60 | + |
| 61 | + adopt_items = parse_table(adopt_match.group(1)) if adopt_match else [] |
| 62 | + drop_items = parse_table(drop_match.group(1)) if drop_match else [] |
| 63 | + |
| 64 | + return adopt_items, drop_items |
| 65 | + |
| 66 | + |
| 67 | +def format_email(year, quarter, adopt_items, drop_items): |
| 68 | + """Generate email subject and body.""" |
| 69 | + quarter_names = {1: "Q1", 2: "Q2", 3: "Q3", 4: "Q4"} |
| 70 | + q_name = quarter_names[quarter] |
| 71 | + |
| 72 | + subject = f"PHEP 3 Reminder: {q_name} {year} Support Schedule" |
| 73 | + |
| 74 | + body = f"""Hello PyHC Community, |
| 75 | +
|
| 76 | +This is a quarterly reminder about the PHEP 3 Python & Upstream Package Support Policy. |
| 77 | +
|
| 78 | +""" |
| 79 | + |
| 80 | + if adopt_items: |
| 81 | + body += f"## Adopt Support For (by end of {q_name} {year})\n\n" |
| 82 | + body += "The following package versions should be supported by PyHC packages:\n\n" |
| 83 | + for package, version, info in adopt_items: |
| 84 | + body += f"- **{package}** {version} ({info})\n" |
| 85 | + body += "\n" |
| 86 | + |
| 87 | + if drop_items: |
| 88 | + body += f"## Can Drop Support For (as of {q_name} {year})\n\n" |
| 89 | + body += "PyHC packages may now drop support for:\n\n" |
| 90 | + for package, version, info in drop_items: |
| 91 | + body += f"- **{package}** {version} ({info})\n" |
| 92 | + body += "\n" |
| 93 | + |
| 94 | + if not adopt_items and not drop_items: |
| 95 | + body += "No changes to the support schedule this quarter.\n\n" |
| 96 | + |
| 97 | + body += """--- |
| 98 | +
|
| 99 | +For the full support schedule and Gantt chart, visit: |
| 100 | +https://heliopython.org/docs/pheps/phep-3-support-schedule/ |
| 101 | +
|
| 102 | +For the complete PHEP 3 specification: |
| 103 | +https://github.com/heliophysicsPy/standards/blob/main/pheps/phep-0003.md |
| 104 | +
|
| 105 | +Questions? Reply to this email or discuss on PyHC Slack. |
| 106 | +
|
| 107 | +Best regards, |
| 108 | +PyHC Tech Lead |
| 109 | +""" |
| 110 | + |
| 111 | + return subject, body |
| 112 | + |
| 113 | + |
| 114 | +def main(): |
| 115 | + script_dir = Path(__file__).parent |
| 116 | + schedule_path = script_dir / "schedule.md" |
| 117 | + |
| 118 | + year, quarter = get_current_quarter() |
| 119 | + print(f"Current quarter: Q{quarter} {year}") |
| 120 | + |
| 121 | + quarters = parse_schedule(schedule_path) |
| 122 | + |
| 123 | + if (year, quarter) not in quarters: |
| 124 | + print(f"Warning: No data found for Q{quarter} {year}") |
| 125 | + # Try to find the nearest future quarter |
| 126 | + future_quarters = [(y, q) for y, q in quarters.keys() if (y, q) >= (year, quarter)] |
| 127 | + if future_quarters: |
| 128 | + year, quarter = min(future_quarters) |
| 129 | + print(f"Using next available quarter: Q{quarter} {year}") |
| 130 | + else: |
| 131 | + print("No future quarters found in schedule. Using empty content.") |
| 132 | + subject = f"PHEP 3 Reminder: Q{quarter} {year} Support Schedule" |
| 133 | + body = "No schedule data available for this quarter. Please check the PHEP 3 support schedule page." |
| 134 | + Path(script_dir / "email_subject.txt").write_text(subject) |
| 135 | + Path(script_dir / "email_body.txt").write_text(body) |
| 136 | + return |
| 137 | + |
| 138 | + quarter_content = quarters[(year, quarter)] |
| 139 | + adopt_items, drop_items = extract_quarter_data(quarter_content) |
| 140 | + |
| 141 | + print(f"Found {len(adopt_items)} adopt items, {len(drop_items)} drop items") |
| 142 | + |
| 143 | + subject, body = format_email(year, quarter, adopt_items, drop_items) |
| 144 | + |
| 145 | + # Write outputs for the GitHub Action to use |
| 146 | + Path(script_dir / "email_subject.txt").write_text(subject) |
| 147 | + Path(script_dir / "email_body.txt").write_text(body) |
| 148 | + |
| 149 | + print(f"\nSubject: {subject}") |
| 150 | + print(f"\nBody preview:\n{body[:500]}...") |
| 151 | + |
| 152 | + |
| 153 | +if __name__ == "__main__": |
| 154 | + main() |
0 commit comments