1+ import hashlib
2+ import logging
13import pkginfo
24import re
35import shutil
46import tempfile
7+ import zipfile
58import json
69from collections import defaultdict
710from django .conf import settings
1114from packaging .version import parse , InvalidVersion
1215from pulpcore .plugin .models import Remote
1316
17+ logger = logging .getLogger (__name__ )
18+
1419
1520PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
1621"""TODO This serial constant is temporary until Python repositories implements serials"""
@@ -91,6 +96,9 @@ def parse_project_metadata(project):
9196 dictionary: of python project metadata
9297
9398 """
99+ metadata_sha256_value = project .get ("metadata_sha256" ) or ""
100+ logger .info (f"parse_project_metadata: metadata_sha256 = { metadata_sha256_value } " )
101+
94102 return {
95103 # Core metadata
96104 # Version 1.0
@@ -130,7 +138,7 @@ def parse_project_metadata(project):
130138 # Release metadata
131139 "packagetype" : project .get ("packagetype" ) or "" ,
132140 "python_version" : project .get ("python_version" ) or "" ,
133- "metadata_sha256" : "" , # TODO
141+ "metadata_sha256" : metadata_sha256_value ,
134142 }
135143
136144
@@ -158,9 +166,9 @@ def parse_metadata(project, version, distribution):
158166 package ["url" ] = distribution .get ("url" ) or ""
159167 package ["sha256" ] = distribution .get ("digests" , {}).get ("sha256" ) or ""
160168 package ["python_version" ] = distribution .get ("python_version" ) or package .get ("python_version" )
161- package ["requires_python" ] = distribution .get ("requires_python" ) or package .get (
162- "requires_python"
163- ) # noqa: E501
169+ package ["requires_python" ] = distribution .get ("requires_python" ) or "" # package.get(
170+ # "requires_python"
171+ # ) # noqa: E501
164172 package ["metadata_sha256" ] = distribution .get ("data-dist-info-metadata" , {}).get (
165173 "sha256"
166174 ) or package .get ("metadata_sha256" )
@@ -181,6 +189,9 @@ def get_project_metadata_from_file(filename):
181189 packagetype = DIST_EXTENSIONS [extensions [pkg_type_index ]]
182190
183191 metadata = DIST_TYPES [packagetype ](filename )
192+ computed_hash = compute_metadata_sha256 (filename )
193+ metadata .metadata_sha256 = computed_hash
194+ logger .info (f"Set metadata.metadata_sha256 to: { computed_hash } for { filename } " )
184195 metadata .packagetype = packagetype
185196 if packagetype == "sdist" :
186197 metadata .python_version = "source"
@@ -193,6 +204,45 @@ def get_project_metadata_from_file(filename):
193204 return metadata
194205
195206
207+ def compute_metadata_sha256 (filename : str ) -> str :
208+ """
209+ Compute SHA256 hash of the metadata file from a Python package.
210+
211+ Returns SHA256 hash or empty string if metadata cannot be extracted.
212+ """
213+ logger .info (f"Computing metadata SHA256 for wheel: { filename } " )
214+
215+ if not filename .endswith (".whl" ):
216+ logger .debug (f"File { filename } is not a wheel, skipping metadata SHA256 computation" )
217+ return ""
218+
219+ try :
220+ with tempfile .NamedTemporaryFile () as temp_file :
221+ with open (filename , "rb" ) as source :
222+ shutil .copyfileobj (source , temp_file )
223+ temp_file .flush ()
224+
225+ logger .debug (f"Copied wheel to temp file: { temp_file .name } " )
226+
227+ with zipfile .ZipFile (temp_file .name , "r" ) as f :
228+ logger .debug (f"Wheel contains files: { f .namelist ()} " )
229+
230+ for file_path in f .namelist ():
231+ if file_path .endswith (".dist-info/METADATA" ):
232+ logger .info (f"Found METADATA file: { file_path } " )
233+ metadata_content = f .read (file_path )
234+ hash_value = hashlib .sha256 (metadata_content ).hexdigest ()
235+ logger .info (f"Computed metadata SHA256: { hash_value } " )
236+ return hash_value
237+
238+ logger .warning (f"No METADATA file found in wheel { filename } " )
239+
240+ except Exception as e :
241+ logger .error (f"Error computing metadata SHA256 for { filename } : { e } " )
242+
243+ return ""
244+
245+
196246def artifact_to_python_content_data (filename , artifact , domain = None ):
197247 """
198248 Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
@@ -209,6 +259,10 @@ def artifact_to_python_content_data(filename, artifact, domain=None):
209259 data ["filename" ] = filename
210260 data ["pulp_domain" ] = domain or artifact .pulp_domain
211261 data ["_pulp_domain" ] = data ["pulp_domain" ]
262+
263+ logger .info (
264+ f"artifact_to_python_content_data returning metadata_sha256: { data .get ('metadata_sha256' )} "
265+ )
212266 return data
213267
214268
@@ -448,7 +502,7 @@ def write_simple_detail_json(project_name, project_packages):
448502 "filename" : package ["filename" ],
449503 "url" : package ["url" ],
450504 "hashes" : {"sha256" : package ["sha256" ]},
451- "requires_python " : package ["requires_python" ] or None ,
505+ "requires-python " : package ["requires_python" ] or None ,
452506 # data-dist-info-metadata is deprecated alias for core-metadata
453507 "data-dist-info-metadata" : (
454508 {"sha256" : package ["metadata_sha256" ]} if package ["metadata_sha256" ] else False
0 commit comments