22"""Myst Markdown changelog generator."""
33
44import os
5- from collections import defaultdict
6- from typing import Dict , List
5+ from collections import OrderedDict
6+ from typing import Dict , List , Tuple
77
88import httpx
99from dateutil import parser
1212 "Accept" : "application/vnd.github.v3+json" ,
1313}
1414
15- if os .getenv ("GITHUB_TOKEN" ) is not None :
16- HEADERS ["Authorization" ] = f"token { os .getenv ('GITHUB_TOKEN' )} "
17-
1815OWNER = "aeon-toolkit"
1916REPO = "aeon"
2017GITHUB_REPOS = "https://api.github.com/repos"
18+ EXCLUDED_USERS = ["github-actions[bot]" ]
2119
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."""
2423 params = {
2524 "base" : "main" ,
2625 "state" : "closed" ,
@@ -37,7 +36,8 @@ def fetch_merged_pull_requests(page: int = 1) -> List[Dict]: # noqa
3736 return [pr for pr in r .json () if pr ["merged_at" ]]
3837
3938
40- def fetch_latest_release (): # noqa
39+ def fetch_latest_release () -> Dict :
40+ """Fetch latest release."""
4141 response = httpx .get (
4242 f"{ GITHUB_REPOS } /{ OWNER } /{ REPO } /releases/latest" , headers = HEADERS
4343 )
@@ -48,11 +48,11 @@ def fetch_latest_release(): # noqa
4848 raise ValueError (response .text , response .status_code )
4949
5050
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. """
5353 release = fetch_latest_release ()
5454 published_at = parser .parse (release ["published_at" ])
55- print ( # noqa
55+ print ( # noqa: T201
5656 f"Latest release { release ['tag_name' ]} was published at { published_at } "
5757 )
5858
@@ -64,13 +64,16 @@ def fetch_pull_requests_since_last_release() -> List[Dict]: # noqa
6464 all_pulls .extend (
6565 [p for p in pulls if parser .parse (p ["merged_at" ]) > published_at ]
6666 )
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+ )
6871 page += 1
6972 return all_pulls
7073
7174
72- def github.qkg1.toppare_tags (tag_left : str , tag_right : str = "HEAD" ): # noqa
73- """Compare commit between two tags"""
75+ def github.qkg1.toppare_tags (tag_left : str , tag_right : str = "HEAD" ) -> Dict :
76+ """Compare commit between two tags. """
7477 response = httpx .get (
7578 f"{ GITHUB_REPOS } /{ OWNER } /{ REPO } /compare/{ tag_left } ...{ tag_right } "
7679 )
@@ -80,87 +83,156 @@ def github.qkg1.toppare_tags(tag_left: str, tag_right: str = "HEAD"): # noqa
8083 raise ValueError (response .text , response .status_code )
8184
8285
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."""
8788 authors = sorted ({pr ["user" ]["login" ] for pr in prs }, key = lambda x : x .lower ())
8889
8990 header = "Contributors\n "
9091 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+ )
9396 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" ] = []
96126
127+ assigned [pkg_title ]["Other" ].append (pr_idx )
97128
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
101136
102137 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 ()
113157
114- if "Not Included" in assigned :
115- assigned .pop ("Not Included" )
158+ assign_pr_category (assigned , categories , i , pr_labels , "Other" )
116159
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" )
118164
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" )
119169
120- def render_row (pr ): # noqa
170+ return assigned , prs_removed
171+
172+
173+ def render_row (pr : Dict ): # noqa
121174 """Render a single row with PR in Myst Markdown format"""
122- print ( # noqa
175+ print ( # noqa: T201
123176 "-" ,
124177 pr ["title" ],
125178 "({pr}" + f"`{ pr ['number' ]} `)" ,
126179 "{user}" + f"`{ pr ['user' ]['login' ]} `" ,
127180 )
128181
129182
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 ]
137191
138192 for pr in sorted (pr_group , key = lambda x : parser .parse (x ["merged_at" ])):
139193 render_row (pr )
140194
141195
142196if __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+ ]
143214 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" ]],
150220 ]
151221
152222 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+
155227 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 )
158230
159231 release = fetch_latest_release ()
160232 diff = github.qkg1.toppare_tags (release ["tag_name" ])
161233 if diff ["total_commits" ] != len (pulls ):
162234 raise ValueError (
163235 "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. "
165237 "Please verify that all PRs are included in the changelog."
166- ) # noqa
238+ )
0 commit comments