1+ import os
2+ import re
3+ from collections import defaultdict
4+ from typing import Any
5+
6+ import requests
7+
8+
9+ ORG_NAME = os .environ ["ORG_NAME" ]
10+ TOKEN = os .environ ["GITHUB_TOKEN" ]
11+
12+ HEADERS = {
13+ "Accept" : "application/vnd.github+json" ,
14+ "Authorization" : f"Bearer { TOKEN } " ,
15+ }
16+
17+ API_URL = f"https://api.github.com/orgs/{ ORG_NAME } /repos?type=public&per_page=100"
18+
19+ SCHOOL_PREFIX_MAP = {
20+ "NYCU_" : "NYCU" ,
21+ "NEHS_" : "NEHS" ,
22+ }
23+
24+ SEMESTER_PATTERN = re .compile (r"^(\d{3})(上|下)\s+" )
25+ SCHOOL_NAME_PATTERN = re .compile (r"^(交大|竹科實中)\s+" )
26+
27+
28+ def fetch_repos () -> list [dict [str , Any ]]:
29+ repos : list [dict [str , Any ]] = []
30+ url = API_URL
31+
32+ while url :
33+ response = requests .get (url , headers = HEADERS , timeout = 30 )
34+ response .raise_for_status ()
35+ repos .extend (response .json ())
36+
37+ next_url = None
38+ link_header = response .headers .get ("Link" )
39+ if link_header :
40+ links = requests .utils .parse_header_links (
41+ link_header .rstrip (">" ).replace (">," , ",<" )
42+ )
43+ for link in links :
44+ if link .get ("rel" ) == "next" :
45+ next_url = link .get ("url" )
46+ break
47+
48+ url = next_url
49+
50+ return repos
51+
52+
53+ def detect_school (repo_name : str ) -> str :
54+ for prefix , school in SCHOOL_PREFIX_MAP .items ():
55+ if repo_name .startswith (prefix ):
56+ return school
57+ return "Other"
58+
59+
60+ def parse_description (description : str | None ) -> tuple [str | None , str ]:
61+ if not description :
62+ return None , ""
63+
64+ desc = description .strip ()
65+
66+ semester_match = SEMESTER_PATTERN .match (desc )
67+ if not semester_match :
68+ return None , desc
69+
70+ semester = f"{ semester_match .group (1 )} { semester_match .group (2 )} "
71+ rest = desc [semester_match .end ():].strip ()
72+
73+ rest = SCHOOL_NAME_PATTERN .sub ("" , rest ).strip ()
74+
75+ return semester , rest
76+
77+
78+ def semester_sort_key (semester : str | None ) -> tuple [int , int ]:
79+ if not semester :
80+ return (- 1 , - 1 )
81+
82+ year = int (semester [:- 1 ])
83+ half = semester [- 1 ]
84+ half_order = 1 if half == "下" else 0
85+ return (year , half_order )
86+
87+
88+ def display_title (repo : dict [str , Any ]) -> str :
89+ _ , parsed_course_name = parse_description (repo .get ("description" ))
90+ return parsed_course_name or repo ["name" ]
91+
92+
93+ def repo_sort_key (repo : dict [str , Any ]) -> tuple [str , str ]:
94+ semester , _ = parse_description (repo .get ("description" ))
95+ return (semester or "" , display_title (repo ).lower ())
96+
97+
98+ def build_markdown (repos : list [dict [str , Any ]]) -> str :
99+ grouped : dict [str , dict [str | None , list [dict [str , Any ]]]] = defaultdict (
100+ lambda : defaultdict (list )
101+ )
102+
103+ for repo in repos :
104+ school = detect_school (repo ["name" ])
105+ semester , _ = parse_description (repo .get ("description" ))
106+ grouped [school ][semester ].append (repo )
107+
108+ school_order = ["NYCU" , "NEHS" , "Other" ]
109+
110+ parts : list [str ] = [
111+ "<!-- AUTO-GENERATED: DO NOT EDIT -->" ,
112+ "# Coursework" ,
113+ "" ,
114+ "This repository indexes my coursework repositories." ,
115+ "" ,
116+ ]
117+
118+ for school in school_order :
119+ semesters = grouped .get (school )
120+ if not semesters :
121+ continue
122+
123+ parts .append (f"## { school } " )
124+ parts .append ("" )
125+
126+ sorted_semesters = sorted (
127+ semesters .keys (),
128+ key = semester_sort_key ,
129+ reverse = True ,
130+ )
131+
132+ for semester in sorted_semesters :
133+ repos_in_semester = sorted (semesters [semester ], key = repo_sort_key )
134+
135+ heading = semester if semester else "Uncategorized"
136+ parts .append (f"### { heading } " )
137+ parts .append ("" )
138+
139+ for repo in repos_in_semester :
140+ title = display_title (repo )
141+ url = repo ["html_url" ]
142+ parts .append (f"- [{ title } ]({ url } )" )
143+
144+ parts .append ("" )
145+
146+ return "\n " .join (parts ).rstrip () + "\n "
147+
148+
149+ def main () -> None :
150+ repos = fetch_repos ()
151+
152+ repos = [repo for repo in repos if not repo .get ("private" , False )]
153+ repos = [repo for repo in repos if repo ["name" ].lower () != "schoolwork" ]
154+
155+ repos = sorted (repos , key = lambda r : r ["name" ])
156+
157+ content = build_markdown (repos )
158+
159+ with open ("README.md" , "w" , encoding = "utf-8" ) as file :
160+ file .write (content )
161+
162+
163+ if __name__ == "__main__" :
164+ main ()
0 commit comments