2
2
"""Myst Markdown changelog generator."""
3
3
4
4
import os
5
- from collections import defaultdict
6
- from typing import Dict , List
5
+ from collections import OrderedDict
6
+ from typing import Dict , List , Tuple
7
7
8
8
import httpx
9
9
from dateutil import parser
12
12
"Accept" : "application/vnd.github.v3+json" ,
13
13
}
14
14
15
- if os .getenv ("GITHUB_TOKEN" ) is not None :
16
- HEADERS ["Authorization" ] = f"token { os .getenv ('GITHUB_TOKEN' )} "
17
-
18
15
OWNER = "aeon-toolkit"
19
16
REPO = "aeon"
20
17
GITHUB_REPOS = "https://api.github.com/repos"
18
+ EXCLUDED_USERS = ["github-actions[bot]" ]
21
19
22
- def fetch_merged_pull_requests (page : int = 1 ) -> List [Dict ]: # noqa
23
- """Fetch a page of pull requests"""
20
+
21
+ def fetch_merged_pull_requests (page : int = 1 ) -> List [Dict ]:
22
+ """Fetch a page of pull requests."""
24
23
params = {
25
24
"base" : "main" ,
26
25
"state" : "closed" ,
@@ -37,7 +36,8 @@ def fetch_merged_pull_requests(page: int = 1) -> List[Dict]: # noqa
37
36
return [pr for pr in r .json () if pr ["merged_at" ]]
38
37
39
38
40
- def fetch_latest_release (): # noqa
39
+ def fetch_latest_release () -> Dict :
40
+ """Fetch latest release."""
41
41
response = httpx .get (
42
42
f"{ GITHUB_REPOS } /{ OWNER } /{ REPO } /releases/latest" , headers = HEADERS
43
43
)
@@ -48,11 +48,11 @@ def fetch_latest_release(): # noqa
48
48
raise ValueError (response .text , response .status_code )
49
49
50
50
51
- def fetch_pull_requests_since_last_release () -> List [Dict ]: # noqa
52
- """Fetch pull requests and filter based on merged date"""
51
+ def fetch_pull_requests_since_last_release () -> List [Dict ]:
52
+ """Fetch pull requests and filter based on merged date. """
53
53
release = fetch_latest_release ()
54
54
published_at = parser .parse (release ["published_at" ])
55
- print ( # noqa
55
+ print ( # noqa: T201
56
56
f"Latest release { release ['tag_name' ]} was published at { published_at } "
57
57
)
58
58
@@ -64,13 +64,16 @@ def fetch_pull_requests_since_last_release() -> List[Dict]: # noqa
64
64
all_pulls .extend (
65
65
[p for p in pulls if parser .parse (p ["merged_at" ]) > published_at ]
66
66
)
67
- is_exhausted = any (parser .parse (p ["merged_at" ]) < published_at for p in pulls ) or len (pulls ) == 0
67
+ is_exhausted = (
68
+ any (parser .parse (p ["merged_at" ]) < published_at for p in pulls )
69
+ or len (pulls ) == 0
70
+ )
68
71
page += 1
69
72
return all_pulls
70
73
71
74
72
- def github_compare_tags (tag_left : str , tag_right : str = "HEAD" ): # noqa
73
- """Compare commit between two tags"""
75
+ def github_compare_tags (tag_left : str , tag_right : str = "HEAD" ) -> Dict :
76
+ """Compare commit between two tags. """
74
77
response = httpx .get (
75
78
f"{ GITHUB_REPOS } /{ OWNER } /{ REPO } /compare/{ tag_left } ...{ tag_right } "
76
79
)
@@ -80,87 +83,156 @@ def github_compare_tags(tag_left: str, tag_right: str = "HEAD"): # noqa
80
83
raise ValueError (response .text , response .status_code )
81
84
82
85
83
- EXCLUDED_USERS = ["github-actions[bot]" ]
84
-
85
- def render_contributors (prs : List , fmt : str = "myst" ): # noqa
86
- """Find unique authors and print a list in given format"""
86
+ def render_contributors (prs : list , fmt : str = "myst" , n_prs : int = - 1 ):
87
+ """Find unique authors and print a list in given format."""
87
88
authors = sorted ({pr ["user" ]["login" ] for pr in prs }, key = lambda x : x .lower ())
88
89
89
90
header = "Contributors\n "
90
91
if fmt == "github" :
91
- print (f"### { header } " ) # noqa
92
- print (", " .join (f"@{ user } " for user in authors if user not in EXCLUDED_USERS )) # noqa
92
+ print (f"### { header } " ) # noqa: T201
93
+ print ( # noqa: T201
94
+ ", " .join (f"@{ user } " for user in authors if user not in EXCLUDED_USERS )
95
+ )
93
96
elif fmt == "myst" :
94
- print (f"## { header } " ) # noqa
95
- print (",\n " .join ("{user}" + f"`{ user } `" for user in authors if user not in EXCLUDED_USERS )) # noqa
97
+ print (f"## { header } " ) # noqa: T201
98
+ print ( # noqa: T201
99
+ "The following have contributed to this release through a collective "
100
+ f"{ n_prs } GitHub Pull Requests:\n "
101
+ )
102
+ print ( # noqa: T201
103
+ ",\n " .join (
104
+ "{user}" + f"`{ user } `" for user in authors if user not in EXCLUDED_USERS
105
+ )
106
+ )
107
+
108
+
109
+ def assign_pr_category (
110
+ assigned : Dict , categories : List [List ], pr_idx : int , pr_labels : List , pkg_title : str
111
+ ):
112
+ """Assign a PR to a category."""
113
+ has_category = False
114
+ for cat in categories :
115
+ if not set (cat [1 ]).isdisjoint (set (pr_labels )):
116
+ has_category = True
117
+
118
+ if cat [0 ] not in assigned [pkg_title ]:
119
+ assigned [pkg_title ][cat [0 ]] = []
120
+
121
+ assigned [pkg_title ][cat [0 ]].append (pr_idx )
122
+
123
+ if not has_category :
124
+ if "Other" not in assigned [pkg_title ]:
125
+ assigned [pkg_title ]["Other" ] = []
96
126
127
+ assigned [pkg_title ]["Other" ].append (pr_idx )
97
128
98
- def assign_prs (prs , categs : List [Dict [str , List [str ]]]): # noqa
99
- """Assign PR to categories based on labels"""
100
- assigned = defaultdict (list )
129
+
130
+ def assign_prs (
131
+ prs : List [Dict ], packages : List [List ], categories : List [List ]
132
+ ) -> Tuple [Dict , int ]:
133
+ """Assign all PRs to packages and categories based on labels."""
134
+ assigned = {}
135
+ prs_removed = 0
101
136
102
137
for i , pr in enumerate (prs ):
103
- for cat in categs :
104
- pr_labels = [label ["name" ] for label in pr ["labels" ]]
105
- if cat ["title" ] != "Not Included" and "no changelog" in pr_labels :
106
- continue
107
- if not set (cat ["labels" ]).isdisjoint (set (pr_labels )):
108
- assigned [cat ["title" ]].append (i )
109
-
110
- assigned ["Other" ] = list (
111
- set (range (len (prs ))) - {i for _ , l in assigned .items () for i in l }
112
- )
138
+ pr_labels = [label ["name" ] for label in pr ["labels" ]]
139
+
140
+ if "no changelog" in pr_labels :
141
+ prs_removed += 1
142
+ continue
143
+
144
+ has_package = False
145
+ for pkg in packages :
146
+ if not set (pkg [1 ]).isdisjoint (set (pr_labels )):
147
+ has_package = True
148
+
149
+ if pkg [0 ] not in assigned :
150
+ assigned [pkg [0 ]] = {}
151
+
152
+ assign_pr_category (assigned , categories , i , pr_labels , pkg [0 ])
153
+
154
+ if not has_package :
155
+ if "Other" not in assigned :
156
+ assigned ["Other" ] = OrderedDict ()
113
157
114
- if "Not Included" in assigned :
115
- assigned .pop ("Not Included" )
158
+ assign_pr_category (assigned , categories , i , pr_labels , "Other" )
116
159
117
- return assigned
160
+ # order assignments
161
+ assigned = OrderedDict ({k : v for k , v in sorted (assigned .items ())})
162
+ if "Other" in assigned :
163
+ assigned .move_to_end ("Other" )
118
164
165
+ for key in assigned :
166
+ assigned [key ] = OrderedDict ({k : v for k , v in sorted (assigned [key ].items ())})
167
+ if "Other" in assigned [key ]:
168
+ assigned [key ].move_to_end ("Other" )
119
169
120
- def render_row (pr ): # noqa
170
+ return assigned , prs_removed
171
+
172
+
173
+ def render_row (pr : Dict ): # noqa
121
174
"""Render a single row with PR in Myst Markdown format"""
122
- print ( # noqa
175
+ print ( # noqa: T201
123
176
"-" ,
124
177
pr ["title" ],
125
178
"({pr}" + f"`{ pr ['number' ]} `)" ,
126
179
"{user}" + f"`{ pr ['user' ]['login' ]} `" ,
127
180
)
128
181
129
182
130
- def render_changelog (prs , assigned ): # noqa
131
- # sourcery skip: use-named-expression
132
- """Render changelog"""
133
- for title , _ in assigned .items ():
134
- pr_group = [prs [i ] for i in assigned [title ]]
135
- if pr_group :
136
- print (f"\n ## { title } \n " ) # noqa
183
+ def render_changelog (prs : List [Dict ], assigned : Dict ):
184
+ """Render changelog."""
185
+ for pkg_title , group in assigned .items ():
186
+ print (f"\n ## { pkg_title } " ) # noqa: T201
187
+
188
+ for cat_title , pr_idx in group .items ():
189
+ print (f"\n ### { cat_title } \n " ) # noqa: T201
190
+ pr_group = [prs [i ] for i in pr_idx ]
137
191
138
192
for pr in sorted (pr_group , key = lambda x : parser .parse (x ["merged_at" ])):
139
193
render_row (pr )
140
194
141
195
142
196
if __name__ == "__main__" :
197
+ # don't commit the actual token, it will get revoked!
198
+ os .environ ["GITHUB_TOKEN" ] = ""
199
+
200
+ if os .getenv ("GITHUB_TOKEN" ) is not None and os .getenv ("GITHUB_TOKEN" ) != "" :
201
+ HEADERS ["Authorization" ] = f"token { os .getenv ('GITHUB_TOKEN' )} "
202
+
203
+ # if you edit these, consider editing the PR template as well
204
+ packages = [
205
+ ["Annotation" , ["annotation" ]],
206
+ ["Benchmarking" , ["benchmarking" ]],
207
+ ["Classification" , ["classification" ]],
208
+ ["Clustering" , ["clustering" ]],
209
+ ["Distances" , ["distances" ]],
210
+ ["Forecasting" , ["forecasting" ]],
211
+ ["Regression" , ["regression" ]],
212
+ ["Transformations" , ["transformations" ]],
213
+ ]
143
214
categories = [
144
- {"title" : "Enhancements" , "labels" : ["enhancement" ]},
145
- {"title" : "Fixes" , "labels" : ["bug" ]},
146
- {"title" : "Maintenance" , "labels" : ["maintenance" ]},
147
- {"title" : "Refactored" , "labels" : ["refactor" ]},
148
- {"title" : "Documentation" , "labels" : ["documentation" ]},
149
- {"title" : "Not Included" , "labels" : ["no changelog" ]}, # this is deleted
215
+ ["Bug Fixes" , ["bug" ]],
216
+ ["Documentation" , ["documentation" ]],
217
+ ["Enhancements" , ["enhancement" ]],
218
+ ["Maintenance" , ["maintenance" ]],
219
+ ["Refactored" , ["refactor" ]],
150
220
]
151
221
152
222
pulls = fetch_pull_requests_since_last_release ()
153
- print (f"Found { len (pulls )} merged PRs since last release" ) # noqa
154
- assigned = assign_prs (pulls , categories )
223
+ print (f"Found { len (pulls )} merged PRs since last release" ) # noqa: T201
224
+
225
+ assigned , prs_removed = assign_prs (pulls , packages , categories )
226
+
155
227
render_changelog (pulls , assigned )
156
- print () # noqa
157
- render_contributors (pulls )
228
+ print () # noqa: T201
229
+ render_contributors (pulls , fmt = "myst" , n_prs = len ( pulls ) - prs_removed )
158
230
159
231
release = fetch_latest_release ()
160
232
diff = github_compare_tags (release ["tag_name" ])
161
233
if diff ["total_commits" ] != len (pulls ):
162
234
raise ValueError (
163
235
"Something went wrong and not all PR were fetched. "
164
- f' There are { len (pulls )} PRs but { diff [" total_commits" ]} in the diff. '
236
+ f" There are { len (pulls )} PRs but { diff [' total_commits' ]} in the diff. "
165
237
"Please verify that all PRs are included in the changelog."
166
- ) # noqa
238
+ )
0 commit comments