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