3030 can_download_file ,
3131 download_file_with_size_limit ,
3232 send_get_http_raw ,
33+ send_head_http_raw ,
3334 stream_file_with_size_limit ,
3435)
3536
@@ -465,6 +466,33 @@ def extract_attestation(attestation_data: dict) -> dict | None:
465466 return attestations [0 ]
466467
467468
469+ # as per https://github.com/pypi/inspector/blob/main/inspector/main.py line 125
470+ INSPECTOR_TEMPLATE = (
471+ "{inspector_url_scheme}://{inspector_url_netloc}/project/"
472+ "{name}/{version}/packages/{first}/{second}/{rest}/{filename}"
473+ )
474+
475+
476+ @dataclass
477+ class PyPIInspectorAsset :
478+ """The package PyPI inspector information."""
479+
480+ #: the pypi inspector link to the tarball
481+ package_sdist_link : str
482+
483+ #: the pypi inspector link(s) to the wheel(s)
484+ package_whl_links : list [str ]
485+
486+ #: a mapping of inspector links to whether they are reachable
487+ package_link_reachability : dict [str , bool ]
488+
489+ def __bool__ (self ) -> bool :
490+ """Determine if this inspector object is empty."""
491+ if (self .package_sdist_link or self .package_whl_links ) and self .package_link_reachability :
492+ return True
493+ return False
494+
495+
468496@dataclass
469497class PyPIPackageJsonAsset :
470498 """The package JSON hosted on the PyPI registry."""
@@ -487,6 +515,9 @@ class PyPIPackageJsonAsset:
487515 #: the source code temporary location name
488516 package_sourcecode_path : str
489517
518+ #: the pypi inspector information about this package
519+ inspector_asset : PyPIInspectorAsset
520+
490521 #: The size of the asset (in bytes). This attribute is added to match the AssetLocator
491522 #: protocol and is not used because pypi API registry does not provide it.
492523 @property
@@ -753,6 +784,91 @@ def get_sha256(self) -> str | None:
753784 logger .debug ("Found sha256 hash: %s" , artifact_hash )
754785 return artifact_hash
755786
787+ def get_inspector_links (self ) -> bool :
788+ """Generate PyPI inspector links for this package version's distributions and fill in the inspector asset.
789+
790+ Returns
791+ -------
792+ bool
793+ True if the link generation was successful, False otherwise.
794+ """
795+ if self .inspector_asset :
796+ return True
797+
798+ if not self .package_json and not self .download ("" ):
799+ logger .warning ("No package metadata available, cannot get links" )
800+ return False
801+
802+ releases = self .get_releases ()
803+ if releases is None :
804+ logger .warning ("Package has no releases, cannot create inspector links." )
805+ return False
806+
807+ version = self .component_version
808+ if self .component_version is None :
809+ version = self .get_latest_version ()
810+
811+ if version is None :
812+ logger .warning ("No version set, and no latest version exists. cannot create inspector links." )
813+ return False
814+
815+ distributions = json_extract (releases , [version ], list )
816+
817+ if not distributions :
818+ logger .warning (
819+ "Package has no distributions for release version %s. Cannot create inspector links." , version
820+ )
821+ return False
822+
823+ for distribution in distributions :
824+ package_type = json_extract (distribution , ["packagetype" ], str )
825+ if package_type is None :
826+ logger .warning ("The version %s has no 'package type' field in a distribution" , version )
827+ continue
828+
829+ name = json_extract (self .package_json , ["info" , "name" ], str )
830+ if name is None :
831+ logger .warning ("The version %s has no 'name' field in a distribution" , version )
832+ continue
833+
834+ blake2b_256 = json_extract (distribution , ["digests" , "blake2b_256" ], str )
835+ if blake2b_256 is None :
836+ logger .warning ("The version %s has no 'blake2b_256' field in a distribution" , version )
837+ continue
838+
839+ filename = json_extract (distribution , ["filename" ], str )
840+ if filename is None :
841+ logger .warning ("The version %s has no 'filename' field in a distribution" , version )
842+ continue
843+
844+ link = INSPECTOR_TEMPLATE .format (
845+ inspector_url_scheme = self .pypi_registry .inspector_url_scheme ,
846+ inspector_url_netloc = self .pypi_registry .inspector_url_netloc ,
847+ name = name ,
848+ version = version ,
849+ first = blake2b_256 [0 :2 ],
850+ second = blake2b_256 [2 :4 ],
851+ rest = blake2b_256 [4 :],
852+ filename = filename ,
853+ )
854+
855+ # use a head request because we don't care about the response contents
856+ reachable = False
857+ if send_head_http_raw (link ):
858+ reachable = True # link was reachable
859+
860+ if package_type == "sdist" :
861+ self .inspector_asset .package_sdist_link = link
862+ self .inspector_asset .package_link_reachability [link ] = reachable
863+ elif package_type == "bdist_wheel" :
864+ self .inspector_asset .package_whl_links .append (link )
865+ self .inspector_asset .package_link_reachability [link ] = reachable
866+ else : # no other package types exist, so else statement should never occur
867+ logger .debug ("Unknown package distribution type: %s" , package_type )
868+
869+ # if all distributions were invalid and went along a 'continue' path
870+ return bool (self .inspector_asset )
871+
756872
757873def find_or_create_pypi_asset (
758874 asset_name : str , asset_version : str | None , pypi_registry_info : PackageRegistryInfo
@@ -790,6 +906,8 @@ def find_or_create_pypi_asset(
790906 logger .debug ("Failed to create PyPIPackageJson asset." )
791907 return None
792908
793- asset = PyPIPackageJsonAsset (asset_name , asset_version , False , package_registry , {}, "" )
909+ asset = PyPIPackageJsonAsset (
910+ asset_name , asset_version , False , package_registry , {}, "" , PyPIInspectorAsset ("" , [], {})
911+ )
794912 pypi_registry_info .metadata .append (asset )
795913 return asset
0 commit comments