1- import json
21import logging
32
4- from aiohttp .client_exceptions import ClientError
53from rest_framework .viewsets import ViewSet
64from rest_framework .renderers import BrowsableAPIRenderer , JSONRenderer , TemplateHTMLRenderer
75from rest_framework .response import Response
2725from packaging .utils import canonicalize_name
2826from urllib .parse import urljoin , urlparse , urlunsplit
2927from pathlib import PurePath
30- from pypi_simple import ACCEPT_JSON_PREFERRED , ProjectPage
3128
3229from pulpcore .plugin .viewsets import OperationPostponedResponse
3330from pulpcore .plugin .tasking import dispatch
3431from pulpcore .plugin .util import get_domain , get_url
35- from pulpcore .plugin .exceptions import TimeoutException
3632from pulp_python .app .models import (
33+ ProjectMetadataContent ,
3734 PythonDistribution ,
3835 PythonPackageContent ,
3936 PythonPublication ,
5451 PYPI_LAST_SERIAL ,
5552 PYPI_SERIAL_CONSTANT ,
5653 get_remote_package_filter ,
54+ get_remote_simple_page ,
5755)
5856
5957from pulp_python .app import tasks
@@ -127,6 +125,11 @@ def get_provenances(repository_version):
127125 """Returns queryset of the provenance for this repository version."""
128126 return PackageProvenance .objects .filter (pk__in = repository_version .content )
129127
128+ @staticmethod
129+ def get_projects_metadata (repository_version ):
130+ """Returns queryset of the project metadata in this repository version."""
131+ return ProjectMetadataContent .objects .filter (pk__in = repository_version .content )
132+
130133 def should_redirect (self , repo_version = None ):
131134 """Checks if there is a publication the content app can serve."""
132135 if self .distribution .publication :
@@ -143,6 +146,12 @@ def get_rvc(self):
143146 content = self .get_content (repo_ver )
144147 return repo_ver , content
145148
149+ def get_rvcm (self ):
150+ """Takes the base_path and returns the repository_version, content, and project metadata."""
151+ repo_ver , content = self .get_rvc ()
152+ project_metadata = self .get_projects_metadata (repo_ver ) if repo_ver else None
153+ return repo_ver , content , project_metadata
154+
146155 def initial (self , request , * args , ** kwargs ):
147156 """Perform common initialization tasks for PyPI endpoints."""
148157 super ().initial (request , * args , ** kwargs )
@@ -330,42 +339,37 @@ def parse_package(release_package):
330339
331340 rfilter = get_remote_package_filter (remote )
332341 if not rfilter .filter_project (package ):
333- return {}
342+ return {}, {}
334343
335- url = remote .get_remote_artifact_url (f"simple/{ package } /" )
336- remote .headers = remote .headers or []
337- remote .headers .append ({"Accept" : ACCEPT_JSON_PREFERRED })
338- downloader = remote .get_downloader (url = url , max_retries = 1 )
339- try :
340- d = downloader .fetch ()
341- except (ClientError , TimeoutException ):
344+ page = get_remote_simple_page (package , remote )
345+ if not page :
342346 log .info (f"Failed to fetch { package } simple page from { remote .url } " )
343- return {}
347+ return {}, {}
344348
345- if d .headers ["content-type" ] == PYPI_SIMPLE_V1_JSON :
346- page = ProjectPage .from_json_data (json .load (open (d .path , "rb" )), base_url = url )
347- else :
348- page = ProjectPage .from_html (package , open (d .path , "rb" ).read (), base_url = url )
349- return {
349+ releases = {
350350 p .filename : parse_package (p )
351351 for p in page .packages
352352 if rfilter .filter_release (package , p .version )
353353 }
354+ return releases , ProjectMetadataContent .from_simple_page (page ).to_metadata ()
354355
355356 @extend_schema (operation_id = "pypi_simple_package_read" , summary = "Get package simple page" )
356357 def retrieve (self , request , path , package ):
357358 """Retrieves the simple api html/json page for a package."""
358359 media_type = request .accepted_renderer .media_type
359360
360- repo_ver , content = self .get_rvc ()
361+ repo_ver , content , metadatas = self .get_rvcm ()
361362 # Should I redirect if the normalized name is different?
362363 normalized = canonicalize_name (package )
363364 releases = {}
365+ project_metadata = {}
364366 if self .distribution .remote :
365- releases = self .pull_through_package_simple (normalized , path , self .distribution .remote )
367+ releases , project_metadata = self .pull_through_package_simple (
368+ normalized , path , self .distribution .remote
369+ )
366370 elif self .should_redirect (repo_version = repo_ver ):
367371 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
368- if content :
372+ if content is not None :
369373 local_packages = content .filter (name__normalize = normalized )
370374 packages = local_packages .values (
371375 "filename" ,
@@ -393,17 +397,25 @@ def retrieve(self, request, path, package):
393397 for p in packages
394398 }
395399 releases .update (local_releases )
396- if not releases :
400+ if metadatas is not None :
401+ local_project_metadata = (
402+ metadatas .filter (project_name = normalized )
403+ .values ("tracks" , "alternate_locations" )
404+ .first ()
405+ )
406+ if local_project_metadata :
407+ project_metadata .update (local_project_metadata )
408+ if not (releases or project_metadata ):
397409 return HttpResponseNotFound (f"{ normalized } does not exist." )
398410
399411 media_type = request .accepted_renderer .media_type
400412 headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
401413
402414 if media_type == PYPI_SIMPLE_V1_JSON :
403- detail_data = write_simple_detail_json (normalized , releases .values ())
415+ detail_data = write_simple_detail_json (normalized , releases .values (), project_metadata )
404416 return Response (detail_data , headers = headers )
405417 else :
406- detail_data = write_simple_detail (normalized , releases .values ())
418+ detail_data = write_simple_detail (normalized , releases .values (), project_metadata )
407419 kwargs = {"content_type" : media_type , "headers" : headers }
408420 return HttpResponse (detail_data , ** kwargs )
409421
0 commit comments