11import hashlib
2+ import logging
23import pkginfo
34import re
45import shutil
1213from packaging .utils import canonicalize_name
1314from packaging .requirements import Requirement
1415from packaging .version import parse , InvalidVersion
15- from pulpcore .plugin .models import Remote
16+ from pulpcore .plugin .models import Artifact , Remote
17+
18+
19+ log = logging .getLogger (__name__ )
1620
1721
1822PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
@@ -199,25 +203,34 @@ def get_project_metadata_from_file(filename):
199203 return metadata
200204
201205
202- def compute_metadata_sha256 (filename : str ) -> str | None :
206+ def extract_wheel_metadata (filename : str ) -> bytes | None :
203207 """
204- Compute SHA256 hash of the metadata file from a Python package .
208+ Extract the metadata file content from a wheel file .
205209
206- Returns SHA256 hash or None if metadata cannot be extracted.
210+ Returns the raw metadata content as bytes or None if metadata cannot be extracted.
207211 """
208212 if not filename .endswith (".whl" ):
209213 return None
210214 try :
211215 with zipfile .ZipFile (filename , "r" ) as f :
212216 for file_path in f .namelist ():
213217 if file_path .endswith (".dist-info/METADATA" ):
214- metadata_content = f .read (file_path )
215- return hashlib .sha256 (metadata_content ).hexdigest ()
216- except (zipfile .BadZipFile , KeyError , OSError ):
217- pass
218+ return f .read (file_path )
219+ except (zipfile .BadZipFile , KeyError , OSError ) as e :
220+ log .warning (f"Failed to extract metadata file from { filename } : { e } " )
218221 return None
219222
220223
224+ def compute_metadata_sha256 (filename : str ) -> str | None :
225+ """
226+ Compute SHA256 hash of the metadata file from a Python package.
227+
228+ Returns SHA256 hash or None if metadata cannot be extracted.
229+ """
230+ metadata_content = extract_wheel_metadata (filename )
231+ return hashlib .sha256 (metadata_content ).hexdigest () if metadata_content else None
232+
233+
221234def artifact_to_python_content_data (filename , artifact , domain = None ):
222235 """
223236 Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
@@ -238,6 +251,27 @@ def artifact_to_python_content_data(filename, artifact, domain=None):
238251 return data
239252
240253
254+ def artifact_to_metadata_artifact (filename : str , artifact : Artifact ) -> Artifact | None :
255+ """
256+ Creates artifact for metadata from the provided wheel artifact.
257+ """
258+ if not filename .endswith (".whl" ):
259+ return None
260+
261+ with tempfile .NamedTemporaryFile ("wb" , dir = "." , suffix = filename ) as temp_file :
262+ shutil .copyfileobj (artifact .file , temp_file )
263+ temp_file .flush ()
264+ metadata_content = extract_wheel_metadata (temp_file .name )
265+ if not metadata_content :
266+ return None
267+ with tempfile .NamedTemporaryFile (suffix = ".metadata" ) as metadata_temp :
268+ metadata_temp .write (metadata_content )
269+ metadata_temp .flush ()
270+ metadata_artifact = Artifact .init_and_validate (metadata_temp .name )
271+ metadata_artifact .save ()
272+ return metadata_artifact
273+
274+
241275def fetch_json_release_metadata (name : str , version : str , remotes : set [Remote ]) -> dict :
242276 """
243277 Fetches metadata for a specific release from PyPI's JSON API. A release can contain
0 commit comments