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 ,
5350 PYPI_LAST_SERIAL ,
5451 PYPI_SERIAL_CONSTANT ,
5552 get_remote_package_filter ,
53+ get_remote_simple_page ,
5654)
5755
5856from pulp_python .app import tasks
@@ -120,6 +118,11 @@ def get_content(repository_version):
120118 """Returns queryset of the content in this repository version."""
121119 return PythonPackageContent .objects .filter (pk__in = repository_version .content )
122120
121+ @staticmethod
122+ def get_projects_metadata (repository_version ):
123+ """Returns queryset of the project metadata in this repository version."""
124+ return ProjectMetadataContent .objects .filter (pk__in = repository_version .content )
125+
123126 def should_redirect (self , repo_version = None ):
124127 """Checks if there is a publication the content app can serve."""
125128 if self .distribution .publication :
@@ -136,6 +139,12 @@ def get_rvc(self):
136139 content = self .get_content (repo_ver )
137140 return repo_ver , content
138141
142+ def get_rvcm (self ):
143+ """Takes the base_path and returns the repository_version, content, and project metadata."""
144+ repo_ver , content = self .get_rvc ()
145+ project_metadata = self .get_projects_metadata (repo_ver ) if repo_ver else None
146+ return repo_ver , content , project_metadata
147+
139148 def initial (self , request , * args , ** kwargs ):
140149 """Perform common initialization tasks for PyPI endpoints."""
141150 super ().initial (request , * args , ** kwargs )
@@ -312,42 +321,37 @@ def parse_package(release_package):
312321
313322 rfilter = get_remote_package_filter (remote )
314323 if not rfilter .filter_project (package ):
315- return {}
324+ return {}, {}
316325
317- url = remote .get_remote_artifact_url (f"simple/{ package } /" )
318- remote .headers = remote .headers or []
319- remote .headers .append ({"Accept" : ACCEPT_JSON_PREFERRED })
320- downloader = remote .get_downloader (url = url , max_retries = 1 )
321- try :
322- d = downloader .fetch ()
323- except (ClientError , TimeoutException ):
326+ page = get_remote_simple_page (package , remote )
327+ if not page :
324328 log .info (f"Failed to fetch { package } simple page from { remote .url } " )
325- return {}
329+ return {}, {}
326330
327- if d .headers ["content-type" ] == PYPI_SIMPLE_V1_JSON :
328- page = ProjectPage .from_json_data (json .load (open (d .path , "rb" )), base_url = url )
329- else :
330- page = ProjectPage .from_html (package , open (d .path , "rb" ).read (), base_url = url )
331- return {
331+ releases = {
332332 p .filename : parse_package (p )
333333 for p in page .packages
334334 if rfilter .filter_release (package , p .version )
335335 }
336+ return releases , ProjectMetadataContent .from_simple_page (page ).to_metadata ()
336337
337338 @extend_schema (operation_id = "pypi_simple_package_read" , summary = "Get package simple page" )
338339 def retrieve (self , request , path , package ):
339340 """Retrieves the simple api html/json page for a package."""
340341 media_type = request .accepted_renderer .media_type
341342
342- repo_ver , content = self .get_rvc ()
343+ repo_ver , content , metadatas = self .get_rvcm ()
343344 # Should I redirect if the normalized name is different?
344345 normalized = canonicalize_name (package )
345346 releases = {}
347+ project_metadata = {}
346348 if self .distribution .remote :
347- releases = self .pull_through_package_simple (normalized , path , self .distribution .remote )
349+ releases , project_metadata = self .pull_through_package_simple (
350+ normalized , path , self .distribution .remote
351+ )
348352 elif self .should_redirect (repo_version = repo_ver ):
349353 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
350- if content :
354+ if content is not None :
351355 packages = content .filter (name__normalize = normalized ).values (
352356 "filename" ,
353357 "sha256" ,
@@ -366,17 +370,25 @@ def retrieve(self, request, path, package):
366370 for p in packages
367371 }
368372 releases .update (local_releases )
369- if not releases :
373+ if metadatas is not None :
374+ local_project_metadata = (
375+ metadatas .filter (project_name = normalized )
376+ .values ("tracks" , "alternate_locations" )
377+ .first ()
378+ )
379+ if local_project_metadata :
380+ project_metadata .update (local_project_metadata )
381+ if not (releases or project_metadata ):
370382 return HttpResponseNotFound (f"{ normalized } does not exist." )
371383
372384 media_type = request .accepted_renderer .media_type
373385 headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
374386
375387 if media_type == PYPI_SIMPLE_V1_JSON :
376- detail_data = write_simple_detail_json (normalized , releases .values ())
388+ detail_data = write_simple_detail_json (normalized , releases .values (), project_metadata )
377389 return Response (detail_data , headers = headers )
378390 else :
379- detail_data = write_simple_detail (normalized , releases .values ())
391+ detail_data = write_simple_detail (normalized , releases .values (), project_metadata )
380392 kwargs = {"content_type" : media_type , "headers" : headers }
381393 return HttpResponse (detail_data , ** kwargs )
382394
0 commit comments