33
44from aiohttp .client_exceptions import ClientError
55from rest_framework .viewsets import ViewSet
6+ from rest_framework .renderers import BrowsableAPIRenderer , JSONRenderer , TemplateHTMLRenderer
67from rest_framework .response import Response
8+ from rest_framework .exceptions import NotAcceptable
79from django .core .exceptions import ObjectDoesNotExist
810from django .shortcuts import redirect
911from datetime import datetime , timezone , timedelta
4345)
4446from pulp_python .app .utils import (
4547 write_simple_index ,
48+ write_simple_index_json ,
4649 write_simple_detail ,
50+ write_simple_detail_json ,
4751 python_content_to_json ,
4852 PYPI_LAST_SERIAL ,
4953 PYPI_SERIAL_CONSTANT ,
5761ORIGIN_HOST = settings .CONTENT_ORIGIN if settings .CONTENT_ORIGIN else settings .PYPI_API_HOSTNAME
5862BASE_CONTENT_URL = urljoin (ORIGIN_HOST , settings .CONTENT_PATH_PREFIX )
5963
64+ PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
65+ PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
66+
67+
68+ class PyPISimpleHTMLRenderer (TemplateHTMLRenderer ):
69+ media_type = PYPI_SIMPLE_V1_HTML
70+
71+
72+ class PyPISimpleJSONRenderer (JSONRenderer ):
73+ media_type = PYPI_SIMPLE_V1_JSON
74+
6075
6176class PyPIMixin :
6277 """Mixin to get index specific info."""
@@ -235,14 +250,42 @@ class SimpleView(PackageUploadMixin, ViewSet):
235250 ],
236251 }
237252
253+ def perform_content_negotiation (self , request , force = False ):
254+ """
255+ Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found.
256+ """
257+ try :
258+ return super ().perform_content_negotiation (request , force )
259+ except NotAcceptable :
260+ return TemplateHTMLRenderer (), TemplateHTMLRenderer .media_type # text/html
261+
262+ def get_renderers (self ):
263+ """
264+ Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones.
265+ """
266+ if self .action in ["list" , "retrieve" ]:
267+ # Ordered by priority if multiple content types are present
268+ return [TemplateHTMLRenderer (), PyPISimpleHTMLRenderer (), PyPISimpleJSONRenderer ()]
269+ else :
270+ return [JSONRenderer (), BrowsableAPIRenderer ()]
271+
238272 @extend_schema (summary = "Get index simple page" )
239273 def list (self , request , path ):
240274 """Gets the simple api html page for the index."""
241275 repo_version , content = self .get_rvc ()
242276 if self .should_redirect (repo_version = repo_version ):
243277 return redirect (urljoin (self .base_content_url , f"{ path } /simple/" ))
244278 names = content .order_by ("name" ).values_list ("name" , flat = True ).distinct ().iterator ()
245- return StreamingHttpResponse (write_simple_index (names , streamed = True ))
279+ media_type = request .accepted_renderer .media_type
280+
281+ if media_type == PYPI_SIMPLE_V1_JSON :
282+ index_data = write_simple_index_json (names )
283+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
284+ return Response (index_data , headers = headers )
285+ else :
286+ index_data = write_simple_index (names , streamed = True )
287+ kwargs = {"content_type" : media_type }
288+ return StreamingHttpResponse (index_data , ** kwargs )
246289
247290 def pull_through_package_simple (self , package , path , remote ):
248291 """Gets the package's simple page from remote."""
@@ -252,7 +295,12 @@ def parse_package(release_package):
252295 stripped_url = urlunsplit (chain (parsed [:3 ], ("" , "" )))
253296 redirect_path = f"{ path } /{ release_package .filename } ?redirect={ stripped_url } "
254297 d_url = urljoin (self .base_content_url , redirect_path )
255- return release_package .filename , d_url , release_package .digests .get ("sha256" , "" )
298+ return {
299+ "filename" : release_package .filename ,
300+ "url" : d_url ,
301+ "sha256" : release_package .digests .get ("sha256" , "" ),
302+ # todo: more fields?
303+ }
256304
257305 rfilter = get_remote_package_filter (remote )
258306 if not rfilter .filter_project (package ):
@@ -269,7 +317,7 @@ def parse_package(release_package):
269317 except TimeoutException :
270318 return HttpResponse (f"{ remote .url } timed out while fetching { package } ." , status = 504 )
271319
272- if d .headers ["content-type" ] == "application/vnd.pypi.simple.v1+json" :
320+ if d .headers ["content-type" ] == PYPI_SIMPLE_V1_JSON :
273321 page = ProjectPage .from_json_data (json .load (open (d .path , "rb" )), base_url = url )
274322 else :
275323 page = ProjectPage .from_html (package , open (d .path , "rb" ).read (), base_url = url )
@@ -290,7 +338,15 @@ def retrieve(self, request, path, package):
290338 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
291339 packages = (
292340 content .filter (name__normalize = normalized )
293- .values_list ("filename" , "sha256" , "name" )
341+ .values_list (
342+ "filename" ,
343+ "sha256" ,
344+ "name" ,
345+ "sha256_metadata" ,
346+ "requires_python" ,
347+ "yanked" ,
348+ "yanked_reason" ,
349+ )
294350 .iterator ()
295351 )
296352 try :
@@ -300,8 +356,28 @@ def retrieve(self, request, path, package):
300356 else :
301357 packages = chain ([present ], packages )
302358 name = present [2 ]
303- releases = ((f , urljoin (self .base_content_url , f"{ path } /{ f } " ), d ) for f , d , _ in packages )
304- return StreamingHttpResponse (write_simple_detail (name , releases , streamed = True ))
359+ releases = (
360+ {
361+ "filename" : f ,
362+ "url" : urljoin (self .base_content_url , f"{ path } /{ f } " ),
363+ "sha256" : s ,
364+ "sha256_metadata" : sm ,
365+ "requires_python" : rp ,
366+ "yanked" : y ,
367+ "yanked_reason" : yr ,
368+ }
369+ for f , s , _ , sm , rp , y , yr in packages
370+ )
371+ media_type = request .accepted_renderer .media_type
372+
373+ if media_type == PYPI_SIMPLE_V1_JSON :
374+ detail_data = write_simple_detail_json (name , releases )
375+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
376+ return Response (detail_data , headers = headers )
377+ else :
378+ detail_data = write_simple_detail (name , releases , streamed = True )
379+ kwargs = {"content_type" : media_type }
380+ return StreamingHttpResponse (detail_data , kwargs )
305381
306382 @extend_schema (
307383 request = PackageUploadSerializer ,
0 commit comments