99from collections .abc import Sequence
1010from datetime import UTC , datetime
1111from pathlib import Path
12- from typing import Any
12+ from typing import TypedDict
1313
1414from jinja2 import Environment , FileSystemLoader
15- from readme_parser import ParsedGroup , ParsedSection , parse_readme , parse_sponsors , slugify
15+ from readme_parser import AlsoSee , ParsedGroup , ParsedSection , parse_readme , parse_sponsors , slugify
1616
1717GITHUB_REPO_URL_RE = re .compile (r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$" )
1818MARKDOWN_LINK_RE = re .compile (r"\[[^\]]+\]\(([^)\s]+)\)" )
3636}
3737
3838
39+ class TemplateSubcategory (TypedDict ):
40+ name : str
41+ value : str
42+ slug : str
43+ url : str
44+
45+
46+ class TemplateEntry (TypedDict ):
47+ name : str
48+ url : str
49+ description : str
50+ categories : list [str ]
51+ groups : list [str ]
52+ subcategories : list [TemplateSubcategory ]
53+ stars : int | None
54+ owner : str | None
55+ last_commit_at : str | None
56+ source_type : str | None
57+ also_see : list [AlsoSee ]
58+
59+
60+ class SyntheticCategory (TypedDict ):
61+ name : str
62+ slug : str
63+ description : str
64+ description_html : str
65+
66+
67+ TemplateCategory = ParsedSection | SyntheticCategory
68+
69+
3970def detect_source_type (url : str ) -> str | None :
4071 """Detect source type from URL domain. Returns None for GitHub URLs."""
4172 if GITHUB_REPO_URL_RE .match (url ):
@@ -64,13 +95,13 @@ def load_stars(path: Path) -> dict[str, dict]:
6495 return {}
6596
6697
67- def sort_entries (entries : list [ dict ]) -> list [dict ]:
98+ def sort_entries (entries : Sequence [ TemplateEntry ]) -> list [TemplateEntry ]:
6899 """Sort entries by stars descending, then name ascending.
69100
70101 Three tiers: starred entries first, stdlib second, other non-starred last.
71102 """
72103
73- def sort_key (entry : dict ) -> tuple [int , int , int , str ]:
104+ def sort_key (entry : TemplateEntry ) -> tuple [int , int , int , str ]:
74105 stars = entry ["stars" ]
75106 name = entry ["name" ].lower ()
76107 if stars is not None :
@@ -84,13 +115,7 @@ def sort_key(entry: dict) -> tuple[int, int, int, str]:
84115
85116
86117def build_robots_txt () -> str :
87- return (
88- "User-agent: *\n "
89- "Content-Signal: search=yes, ai-input=yes, ai-train=yes\n "
90- "Allow: /\n "
91- "\n "
92- f"Sitemap: { SITEMAP_URL } \n "
93- )
118+ return f"User-agent: *\n Content-Signal: search=yes, ai-input=yes, ai-train=yes\n Allow: /\n \n Sitemap: { SITEMAP_URL } \n "
94119
95120
96121def category_path (category : ParsedSection ) -> str :
@@ -117,7 +142,7 @@ def subcategory_public_url(category_slug: str, subcategory_slug: str) -> str:
117142 return f"{ SITE_URL } categories/{ category_slug } /{ subcategory_slug } /"
118143
119144
120- def synthetic_category (name : str , slug : str ) -> dict [ str , str ] :
145+ def synthetic_category (name : str , slug : str ) -> SyntheticCategory :
121146 return {"name" : name , "slug" : slug , "description" : "" , "description_html" : "" }
122147
123148
@@ -202,7 +227,7 @@ def annotate_entries_with_stars(
202227 if not entry or "stars" not in entry :
203228 continue
204229 stripped = line .rstrip ("\n " )
205- ending = line [len (stripped ):]
230+ ending = line [len (stripped ) :]
206231 annotated = f"{ stripped } ({ format_stars (entry ['stars' ])} ){ ending } "
207232 break
208233 out .append (annotated )
@@ -233,35 +258,35 @@ def remove_sponsors_section(markdown: str) -> str:
233258def extract_entries (
234259 categories : list [ParsedSection ],
235260 groups : list [ParsedGroup ],
236- ) -> list [dict ]:
261+ ) -> list [TemplateEntry ]:
237262 """Flatten categories into individual library entries for table display.
238263
239264 Entries appearing in multiple categories are merged into a single entry
240265 with lists of categories and groups.
241266 """
242267 cat_to_group = {cat ["name" ]: group ["name" ] for group in groups for cat in group ["categories" ]}
243268
244- seen : dict [tuple [str , str ], dict [ str , Any ] ] = {} # (url, name) -> entry
245- entries : list [dict [ str , Any ] ] = []
269+ seen : dict [tuple [str , str ], TemplateEntry ] = {} # (url, name) -> entry
270+ entries : list [TemplateEntry ] = []
246271 for cat in categories :
247272 group_name = cat_to_group .get (cat ["name" ], "Other" )
248273 for entry in cat ["entries" ]:
249274 key = (entry ["url" ], entry ["name" ])
250- existing : dict [ str , Any ] | None = seen .get (key )
275+ existing = seen .get (key )
251276 if existing is None :
252- existing = {
253- " name" : entry ["name" ],
254- " url" : entry ["url" ],
255- " description" : entry ["description" ],
256- " categories" : [],
257- " groups" : [],
258- " subcategories" : [],
259- " stars" : None ,
260- " owner" : None ,
261- " last_commit_at" : None ,
262- " source_type" : detect_source_type (entry ["url" ]),
263- " also_see" : entry ["also_see" ],
264- }
277+ existing = TemplateEntry (
278+ name = entry ["name" ],
279+ url = entry ["url" ],
280+ description = entry ["description" ],
281+ categories = [],
282+ groups = [],
283+ subcategories = [],
284+ stars = None ,
285+ owner = None ,
286+ last_commit_at = None ,
287+ source_type = detect_source_type (entry ["url" ]),
288+ also_see = entry ["also_see" ],
289+ )
265290 seen [key ] = existing
266291 entries .append (existing )
267292 if cat ["name" ] not in existing ["categories" ]:
@@ -273,12 +298,14 @@ def extract_entries(
273298 scoped = f"{ cat ['name' ]} > { subcat } "
274299 if not any (s ["value" ] == scoped for s in existing ["subcategories" ]):
275300 sub_slug = slugify (subcat )
276- existing ["subcategories" ].append ({
277- "name" : subcat ,
278- "value" : scoped ,
279- "slug" : sub_slug ,
280- "url" : f"/categories/{ cat ['slug' ]} /{ sub_slug } /" ,
281- })
301+ existing ["subcategories" ].append (
302+ TemplateSubcategory (
303+ name = subcat ,
304+ value = scoped ,
305+ slug = sub_slug ,
306+ url = f"/categories/{ cat ['slug' ]} /{ sub_slug } /" ,
307+ )
308+ )
282309 return entries
283310
284311
@@ -303,10 +330,7 @@ def build(repo_root: Path) -> None:
303330 all_top_level_slugs = cat_slugs + group_slugs + [BUILTIN_SLUG ]
304331 duplicates = {s for s , n in Counter (all_top_level_slugs ).items () if n > 1 }
305332 if duplicates :
306- raise ValueError (
307- f"slug collision in /categories/ namespace: { sorted (duplicates )} . "
308- "Rename a category or group so their slugs differ."
309- )
333+ raise ValueError (f"slug collision in /categories/ namespace: { sorted (duplicates )} . Rename a category or group so their slugs differ." )
310334 total_entries = sum (c ["entry_count" ] for c in categories )
311335 entries = extract_entries (categories , parsed_groups )
312336 build_date = datetime .now (UTC )
@@ -377,14 +401,14 @@ def build(repo_root: Path) -> None:
377401 categories_dir = site_dir / "categories"
378402
379403 def render_category (
380- category : dict ,
404+ category : TemplateCategory ,
381405 * ,
382406 category_url : str ,
383- entries : list [ dict ],
407+ entries : Sequence [ TemplateEntry ],
384408 current_path : str ,
385409 page_dir : Path ,
386- parent_category : dict | None = None ,
387- group_categories : list | None = None ,
410+ parent_category : ParsedSection | None = None ,
411+ group_categories : Sequence [ ParsedSection ] | None = None ,
388412 ) -> None :
389413 page_dir .mkdir (parents = True , exist_ok = True )
390414 (page_dir / "index.html" ).write_text (
@@ -443,7 +467,7 @@ def render_category(
443467 encoding = "utf-8" ,
444468 )
445469
446- subcat_to_entries : dict [str , list [dict ]] = {}
470+ subcat_to_entries : dict [str , list [TemplateEntry ]] = {}
447471 subcat_meta : dict [str , tuple [str , str , str ]] = {} # value -> (cat_slug, sub_slug, sub_name)
448472 cat_slug_by_url_prefix = {f"/categories/{ c ['slug' ]} /" : c ["slug" ] for c in categories }
449473 cat_by_slug = {c ["slug" ]: c for c in categories }
@@ -472,9 +496,7 @@ def render_category(
472496 if static_src .exists ():
473497 shutil .copytree (static_src , static_dst , dirs_exist_ok = True )
474498
475- markdown_index = annotate_entries_with_stars (
476- remove_sponsors_section (readme_text ), stars_data
477- )
499+ markdown_index = annotate_entries_with_stars (remove_sponsors_section (readme_text ), stars_data )
478500 llms_template = (website / "templates" / "llms.txt" ).read_text (encoding = "utf-8" )
479501 llms_txt = build_llms_txt (llms_template , readme_text , stars_data )
480502 (site_dir / "robots.txt" ).write_text (build_robots_txt (), encoding = "utf-8" )
0 commit comments