1#!/usr/bin/env python3 2 3""" 4Generate a summary of last week's issues tagged with "topic: feature". 5 6The summary will include a list of new and changed issues and is sent each 7Monday at 0200 CE(S)T to the typing-sig mailing list. Due to limitation 8with GitHub Actions, the mail is sent from a private server, currently 9maintained by @srittau. 10""" 11 12from __future__ import annotations 13 14import datetime 15from dataclasses import dataclass 16from typing import Any, Iterable, Sequence 17 18import requests 19 20ISSUES_API_URL = "https://api.github.com/repos/python/typing/issues" 21ISSUES_URL = "https://github.com/python/typing/issues?q=label%3A%22topic%3A+feature%22" 22ISSUES_LABEL = "topic: feature" 23SENDER_EMAIL = "Typing Bot <[email protected]>" 24RECEIVER_EMAIL = "[email protected]" 25 26 27@dataclass 28class Issue: 29 number: int 30 title: str 31 url: str 32 created: datetime.datetime 33 user: str 34 pull_request: bool = False 35 36 37def main() -> None: 38 since = previous_week_start() 39 issues = fetch_issues(since) 40 new, updated = split_issues(issues, since) 41 print_summary(since, new, updated) 42 43 44def previous_week_start() -> datetime.date: 45 today = datetime.date.today() 46 return today - datetime.timedelta(days=today.weekday() + 7) 47 48 49def fetch_issues(since: datetime.date) -> list[Issue]: 50 """Return (new, updated) issues.""" 51 j = requests.get( 52 ISSUES_API_URL, 53 params={ 54 "labels": ISSUES_LABEL, 55 "since": f"{since:%Y-%m-%d}T00:00:00Z", 56 "per_page": "100", 57 "state": "open", 58 }, 59 headers={"Accept": "application/vnd.github.v3+json"}, 60 ).json() 61 assert isinstance(j, list) 62 return [parse_issue(j_i) for j_i in j] 63 64 65def parse_issue(j: Any) -> Issue: 66 number = j["number"] 67 title = j["title"] 68 url = j["html_url"] 69 created_at = datetime.datetime.fromisoformat(j["created_at"][:-1]) 70 user = j["user"]["login"] 71 pull_request = "pull_request" in j 72 assert isinstance(number, int) 73 assert isinstance(title, str) 74 assert isinstance(url, str) 75 assert isinstance(user, str) 76 return Issue(number, title, url, created_at, user, pull_request) 77 78 79def split_issues( 80 issues: Iterable[Issue], since: datetime.date 81) -> tuple[list[Issue], list[Issue]]: 82 new = [] 83 updated = [] 84 for issue in issues: 85 if issue.created.date() >= since: 86 new.append(issue) 87 else: 88 updated.append(issue) 89 new.sort(key=lambda i: i.number) 90 updated.sort(key=lambda i: i.number) 91 return new, updated 92 93 94def print_summary( 95 since: datetime.date, new: Sequence[Issue], changed: Sequence[Issue] 96) -> None: 97 print(f"From: {SENDER_EMAIL}") 98 print(f"To: {RECEIVER_EMAIL}") 99 print(f"Subject: Opened and changed typing issues week {since:%G-W%V}") 100 print() 101 print(generate_mail(new, changed)) 102 103 104def generate_mail(new: Sequence[Issue], changed: Sequence[Issue]) -> str: 105 if len(new) == 0 and len(changed) == 0: 106 s = ( 107 "No issues or pull requests with the label 'topic: feature' were opened\n" 108 "or updated last week in the typing repository on GitHub.\n\n" 109 ) 110 else: 111 s = ( 112 "The following is an overview of all issues and pull requests in the\n" 113 "typing repository on GitHub with the label 'topic: feature'\n" 114 "that were opened or updated last week, excluding closed issues.\n\n" 115 "---------------------------------------------------\n\n" 116 ) 117 if len(new) > 0: 118 s += "The following issues and pull requests were opened last week: \n\n" 119 s += "".join(generate_issue_text(issue) for issue in new) 120 s += "\n---------------------------------------------------\n\n" 121 if len(changed) > 0: 122 s += "The following issues and pull requests were updated last week: \n\n" 123 s += "".join(generate_issue_text(issue) for issue in changed) 124 s += "\n---------------------------------------------------\n\n" 125 s += ( 126 "All issues and pull requests with the label 'topic: feature'\n" 127 "can be viewed under the following URL:\n\n" 128 ) 129 s += ISSUES_URL 130 return s 131 132 133def generate_issue_text(issue: Issue) -> str: 134 s = f"#{issue.number:<5} " 135 if issue.pull_request: 136 s += "[PR] " 137 s += f"{issue.title}\n" 138 s += f" opened by @{issue.user}\n" 139 s += f" {issue.url}\n" 140 return s 141 142 143if __name__ == "__main__": 144 main() 145