11#!/usr/bin/env python3
22"""Fetch all package names + versions from the LDPoV npm catalog.
33
4- Reads LDPOV_ORG_ID, LDPOV_CATALOG_TOKEN, NPM_URL from ../.env or environment.
5- Uses S3 redirect_map for package discovery, then fetches versions from the registry.
4+ Reads LDPOV_ORG_ID from ../.env or environment.
5+ Fetches the redirect_map from S3 and extracts package names + versions
6+ directly from the tarball keys — no registry calls needed.
67Writes public/data/ldpov/javascript/catalog.json.
78"""
89
1213import re
1314import subprocess
1415import urllib .parse
15- from base64 import b64encode
16- from concurrent .futures import ThreadPoolExecutor , as_completed
17- from urllib .request import Request , urlopen
16+ from collections import defaultdict
1817
1918_env : dict [str , str ] = {}
2019_env_path = os .path .join (os .path .dirname (__file__ ), ".." , ".env" )
2524 k , v = line .split ("=" , 1 )
2625 _env [k .strip ()] = v .strip ()
2726
28- ORG_ID = os .environ .get ("LDPOV_ORG_ID" , _env .get ("LDPOV_ORG_ID" , "" ))
29- TOKEN = os .environ .get ("LDPOV_CATALOG_TOKEN" , _env .get ("LDPOV_CATALOG_TOKEN" , "" ))
30- BASE_URL = os .environ .get ("NPM_URL" , _env .get ("NPM_URL" , "" ))
27+ ORG_ID = os .environ .get ("LDPOV_ORG_ID" , _env .get ("LDPOV_ORG_ID" , "" ))
3128
32- if not ORG_ID or not TOKEN :
33- raise SystemExit ("ERROR: set LDPOV_ORG_ID and LDPOV_CATALOG_TOKEN in .env or environment" )
34- if not BASE_URL :
35- raise SystemExit ("ERROR: set NPM_URL in .env or environment" )
29+ if not ORG_ID :
30+ raise SystemExit ("ERROR: set LDPOV_ORG_ID in .env or environment" )
3631
37- AUTH = b64encode (f"x:{ TOKEN } " .encode ()).decode ()
32+
33+ def _version_key (v : str ) -> list :
34+ return [int (x ) if x .isdigit () else x for x in re .split (r"[.\-]" , v .split ("+" )[0 ])]
3835
3936
40- def get_package_list () -> list [ str ] :
37+ def main () -> None :
4138 s3_path = f"s3://curated-catalog/env=prod/org-id={ ORG_ID } /repo-type=npm/redirect_map.json"
42- print (f"Fetching npm package list from S3: { s3_path } " , flush = True )
39+ print (f"Fetching npm redirect_map from S3: { s3_path } " , flush = True )
4340 result = subprocess .run (
4441 ["aws" , "s3" , "cp" , s3_path , "-" ],
4542 capture_output = True , text = True , check = True ,
4643 )
4744 data = json .loads (result .stdout )
48- names = sorted (
49- {urllib .parse .unquote (k .split ("/-/" )[0 ]) for k in data },
50- key = str .lower ,
51- )
52- print (f"Found { len (names )} packages" , flush = True )
53- return names
54-
55-
56- def fetch_versions (name : str ) -> tuple [str , list [str ]]:
57- encoded = urllib .parse .quote (name , safe = "@" )
58- url = f"{ BASE_URL } { encoded } "
59- try :
60- req = Request (url , headers = {"Authorization" : f"Basic { AUTH } " })
61- with urlopen (req , timeout = 30 ) as r :
62- doc = json .loads (r .read ())
63- versions = list (doc .get ("versions" , {}).keys ())
64- return name , sorted (versions , key = _version_key )
65- except Exception :
66- return name , []
67-
68-
69- def _version_key (v : str ) -> list :
70- base = v .split ("-" )[0 ]
71- nums = re .findall (r"\d+" , base )
72- return [int (n ) for n in nums [:3 ]] + [0 ] * (3 - len (nums [:3 ]))
73-
74-
75- def main () -> None :
76- names = get_package_list ()
77- total = len (names )
78-
79- results : list [dict ] = []
80- done = 0
81- print (f"Fetching versions with 30 workers..." , flush = True )
82-
83- with ThreadPoolExecutor (max_workers = 30 ) as pool :
84- futures = {pool .submit (fetch_versions , n ): n for n in names }
85- for future in as_completed (futures ):
86- name , versions = future .result ()
87- if versions :
88- results .append ({"name" : name , "versions" : versions })
89- done += 1
90- if done % 500 == 0 or done == total :
91- print (f" { done } /{ total } " , flush = True )
92-
93- results .sort (key = lambda x : x ["name" ].lower ())
45+ print (f"Found { len (data )} redirect entries" , flush = True )
46+
47+ pkg_versions : dict [str , set [str ]] = defaultdict (set )
48+ for raw_key in data :
49+ key = urllib .parse .unquote (raw_key )
50+ if "/-/" not in key :
51+ continue
52+ name_part , filename = key .split ("/-/" , 1 )
53+ filename = filename .split ("#" )[0 ]
54+ if not filename .endswith (".tgz" ):
55+ continue
56+ stem = filename [:- 4 ]
57+ short_name = name_part .split ("/" )[- 1 ]
58+ prefix = short_name + "-"
59+ if stem .startswith (prefix ):
60+ pkg_versions [name_part ].add (stem [len (prefix ):])
61+
62+ packages = [
63+ {"name" : name , "versions" : sorted (versions , key = _version_key )}
64+ for name , versions in sorted (pkg_versions .items (), key = lambda x : x [0 ].lower ())
65+ ]
9466
9567 out = {
9668 "generated" : datetime .datetime .utcnow ().isoformat () + "Z" ,
97- "packages" : results ,
69+ "packages" : packages ,
9870 }
9971
10072 out_path = os .path .join (
@@ -104,7 +76,7 @@ def main() -> None:
10476 with open (out_path , "w" ) as f :
10577 json .dump (out , f , separators = ("," , ":" ))
10678
107- print (f"Wrote public/data/ldpov/javascript/catalog.json — { len (results ) } packages ( { total - len ( results )} skipped, no versions) " )
79+ print (f"Wrote public/data/ldpov/javascript/catalog.json — { len (packages )} packages " )
10880
10981
11082if __name__ == "__main__" :
0 commit comments