1010#
1111
1212import os
13+ import posixpath
1314from urllib .parse import urlparse , urlunparse
1415
1516from typing import Dict
2930from python_inspector .utils_pypi import PypiSimpleRepository
3031
3132
33+ def get_sdist_from_urls (urls : list ) -> Optional [dict ]:
34+ """Extract source distribution info from PyPI urls array."""
35+ for entry in urls or []:
36+ if entry .get ("packagetype" ) == "sdist" :
37+ return {
38+ "url" : entry .get ("url" , "" ),
39+ "sha256" : entry .get ("digests" , {}).get ("sha256" , "" ),
40+ "md5" : entry .get ("digests" , {}).get ("md5" ) or entry .get ("md5_digest" , "" ),
41+ "size" : entry .get ("size" ),
42+ "filename" : entry .get ("filename" , "" ),
43+ }
44+ return None
45+
46+
3247async def get_pypi_data_from_purl (
3348 purl : str ,
3449 environment : Environment ,
@@ -51,33 +66,52 @@ async def get_pypi_data_from_purl(
5166 if not version :
5267 raise Exception ("Version is not specified in the purl" )
5368
54- # Todo: address the case where several index URLs are passed
55- if index_urls :
56- # Backward compatibility: If pypi.org is passed as index url, always resolve against it.
57- # When multiple index URLs are supported and the todo above is fixed, then this hack can be removed.
58- if "https://pypi.org/simple" in index_urls :
59- index_url = None
60- else :
61- index_url = index_urls [0 ]
62- else :
63- index_url = None
69+ api_urls = []
70+ pypi_org_url = f"https://pypi.org/pypi/{ name } /{ version } /json"
71+ for index_url in index_urls or []:
72+ if index_url == "https://pypi.org/simple" :
73+ continue
74+ base_path = index_url .removesuffix ("/simple" ) + "/pypi"
75+ api_urls .append ((base_path , f"{ base_path } /{ name } /{ version } /json" ))
76+ api_urls .append (("https://pypi.org/pypi" , pypi_org_url ))
6477
65- base_path = (
66- index_url .removesuffix ("/simple" ) + "/pypi" if index_url else "https://pypi.org/pypi"
67- )
78+ from python_inspector .utils import get_response_async
6879
69- api_url = f"{ base_path } /{ name } /{ version } /json"
80+ response = None
81+ api_url = None
82+ base_path = None
83+ info = {}
84+ for bp , url in api_urls :
85+ repo_response = await get_response_async (url )
86+ if not repo_response :
87+ continue
7088
71- from python_inspector .utils import get_response_async
89+ if not response :
90+ response = repo_response
91+ api_url = url
92+ base_path = bp
93+ info = response .get ("info" ) or {}
94+
95+ if not info .get ("project_urls" ):
96+ repo_info = repo_response .get ("info" ) or {}
97+ info ["project_urls" ] = repo_info .get ("project_urls" )
98+
99+ if info .get ("project_urls" ):
100+ break
72101
73- response = await get_response_async (api_url )
74102 if not response :
75103 return None
76104
77- info = response .get ("info" ) or {}
105+ sdist_info = get_sdist_from_urls ( response .get ("urls" , []))
78106 homepage_url = info .get ("home_page" )
79107 project_urls = info .get ("project_urls" ) or {}
108+
80109 code_view_url = get_pypi_codeview_url (project_urls )
110+ vcs_url = None
111+ if code_view_url :
112+ vcs_url = code_view_url .rstrip ("/" )
113+ if not vcs_url .endswith (".git" ):
114+ vcs_url = vcs_url + ".git"
81115 bug_tracking_url = get_pypi_bugtracker_url (project_urls )
82116 python_version = get_python_version_from_env_tag (python_version = environment .python_version )
83117 valid_distribution_urls = []
@@ -145,6 +179,12 @@ def canonicalize_url(url: str):
145179
146180 urls_sanitized [url_sanitized ] = value
147181
182+ urls_by_filename = {
183+ posixpath .basename (urlparse (e .get ("url" )).path ): e
184+ for e in response .get ("urls" ) or []
185+ if e .get ("url" )
186+ }
187+
148188 def remove_credentials_from_url (url : str ):
149189 # Parse the URL into its components
150190 parsed = urlparse (url )
@@ -156,17 +196,18 @@ def remove_credentials_from_url(url: str):
156196 # Create a new parsed result object, replacing the old netloc
157197 # with our new one that has no credentials.
158198 parsed = parsed ._replace (netloc = new_netloc )
159- url_without_credentials = urlunparse (parsed )
160-
161- return url_without_credentials
199+ return urlunparse (parsed )
162200
163201 # iterate over the valid distribution urls and return the first
164202 # one that is matching.
165203 for dist_url in valid_distribution_urls :
166- if dist_url not in urls_sanitized :
204+ url_data = urls_sanitized .get (dist_url )
205+ if not url_data :
206+ filename = posixpath .basename (urlparse (dist_url ).path )
207+ url_data = urls_by_filename .get (filename )
208+ if not url_data :
167209 continue
168210
169- url_data = urls_sanitized .get (dist_url )
170211 digests = url_data .get ("digests" ) or {}
171212
172213 return PackageData (
@@ -176,6 +217,8 @@ def remove_credentials_from_url(url: str):
176217 api_data_url = remove_credentials_from_url (api_url ),
177218 bug_tracking_url = bug_tracking_url ,
178219 code_view_url = code_view_url ,
220+ extra_data = {"source_artifact" : sdist_info } if sdist_info else {},
221+ vcs_url = vcs_url ,
179222 license_expression = info .get ("license_expression" ),
180223 declared_license = get_declared_license (info ),
181224 download_url = remove_credentials_from_url (dist_url ),
0 commit comments