88from  urllib .parse  import  quote 
99from  pathlib  import  Path 
1010from  github  import  Github 
11- from  typing  import  List , Dict 
11+ from  typing  import  List , Dict , Set 
12+ 
13+ # Define yanked versions - modify this dictionary as needed 
14+ yanked_versions  =  {
15+          "confluent-kafka" : {
16+              "2.11.0+gr" ,
17+              "2.11.0+gr.1" ,
18+          },
19+     }
1220
1321HTML_TEMPLATE  =  """<!DOCTYPE html> 
1422 <html> 
2129 </body> 
2230 </html> 
2331""" 
24-      
32+ 
2533def  normalize (name ):
2634    """Normalize package name according to PEP 503.""" 
2735    return  re .sub (r"[-_.]+" , "-" , name ).lower ()
@@ -32,12 +40,25 @@ def calculate_sha256(file_path):
3240
3341    return  digest .hexdigest ()
3442
43+ def  extract_version_from_filename (filename : str ) ->  str :
44+     """Extract version from wheel or sdist filename.""" 
45+     # Remove extension 
46+     name  =  filename .replace ('.tar.gz' , '' ).replace ('.whl' , '' )
47+     
48+     # For wheels: package-version-python-abi-platform 
49+     # For sdist: package-version 
50+     parts  =  name .split ('-' )
51+     if  len (parts ) >=  2 :
52+         return  parts [1 ]
53+     return  "" 
54+ 
3555class  PackageIndexBuilder :
36-     def  __init__ (self , token : str , repo_name : str , output_dir : str ):
56+     def  __init__ (self , token : str , repo_name : str , output_dir : str ,  yanked_versions :  Dict [ str ,  Set [ str ]]  =   None ):
3757        self .github  =  Github (token )
3858        self .repo  =  self .github .get_repo (repo_name )
3959        self .output_dir  =  Path (output_dir )
4060        self .packages : Dict [str , List [Dict ]] =  {}
61+         self .yanked_versions  =  yanked_versions  or  {}
4162
4263        # Set up authenticated session 
4364        self .session  =  requests .Session ()
@@ -46,9 +67,13 @@ def __init__(self, token: str, repo_name: str, output_dir: str):
4667            "Accept" : "application/octet-stream" ,
4768        })
4869
49-     def  collect_packages (self ):
70+     def  is_version_yanked (self , package_name : str , version : str ) ->  bool :
71+         """Check if a specific version of a package is yanked.""" 
72+         normalized_package  =  normalize (package_name )
73+         return  normalized_package  in  self .yanked_versions  and  version  in  self .yanked_versions [normalized_package ]
5074
51-         print  ("Query release assets" )
75+     def  collect_packages (self ):
76+         print ("Query release assets" )
5277
5378        for  release  in  self .repo .get_releases ():
5479            for  asset  in  release .get_assets ():
@@ -57,11 +82,13 @@ def collect_packages(self):
5782                    if  package_name  not  in   self .packages :
5883                        self .packages [package_name ] =  []
5984
85+                     version  =  extract_version_from_filename (asset .name )
6086                    self .packages [package_name ].append ({
6187                        'filename' : asset .name ,
6288                        'url' : asset .url ,
6389                        'size' : asset .size ,
6490                        'upload_time' : asset .created_at .strftime ('%Y-%m-%d %H:%M:%S' ),
91+                         'version' : version ,
6592                    })
6693
6794    def  generate_index_html (self ):
@@ -84,7 +111,9 @@ def generate_index_html(self):
84111            file_links  =  []
85112            assets  =  sorted (assets , key = lambda  x : x ["filename" ])
86113            for  filename , items  in  itertools .groupby (assets , key = lambda  x : x ["filename" ]):
87-                 url  =  next (items )['url' ]
114+                 asset_info  =  next (items )
115+                 url  =  asset_info ['url' ]
116+                 version  =  asset_info ['version' ]
88117
89118                # Download the file 
90119                with  open (package_dir  /  filename , 'wb' ) as  f :
@@ -96,7 +125,15 @@ def generate_index_html(self):
96125                            f .write (chunk )
97126
98127                sha256_hash  =  calculate_sha256 (package_dir  /  filename )
99-                 file_links .append (f'<a href="{ quote (filename )}  #sha256={ sha256_hash }  ">{ filename }  </a><br/>' )
128+ 
129+                 # Check if this version is yanked 
130+                 yanked_attr  =  "" 
131+                 if  self .is_version_yanked (package , version ):
132+                     yanked_attr  =  ' data-yanked="true"' 
133+ 
134+                 file_links .append (
135+                     f'<a href="{ quote (filename )}  #sha256={ sha256_hash }  "{ yanked_attr }  >{ filename }  </a><br/>' 
136+                 )
100137
101138            package_index  =  HTML_TEMPLATE .format (
102139                package_name = f"Links for { package }  " ,
@@ -126,7 +163,7 @@ def main():
126163        print  ("Missing required environment variables" )
127164        sys .exit (1 )
128165
129-     builder  =  PackageIndexBuilder (token , repo , output_dir )
166+     builder  =  PackageIndexBuilder (token , repo , output_dir ,  yanked_versions )
130167    builder .build ()
131168
132169if  __name__  ==  "__main__" :
0 commit comments