3
3
4
4
from aiohttp .client_exceptions import ClientError
5
5
from rest_framework .viewsets import ViewSet
6
+ from rest_framework .renderers import BrowsableAPIRenderer , JSONRenderer , TemplateHTMLRenderer
6
7
from rest_framework .response import Response
8
+ from rest_framework .exceptions import NotAcceptable
7
9
from django .core .exceptions import ObjectDoesNotExist
8
10
from django .shortcuts import redirect
9
11
from datetime import datetime , timezone , timedelta
43
45
)
44
46
from pulp_python .app .utils import (
45
47
write_simple_index ,
48
+ write_simple_index_json ,
46
49
write_simple_detail ,
50
+ write_simple_detail_json ,
47
51
python_content_to_json ,
48
52
PYPI_LAST_SERIAL ,
49
53
PYPI_SERIAL_CONSTANT ,
57
61
ORIGIN_HOST = settings .CONTENT_ORIGIN if settings .CONTENT_ORIGIN else settings .PYPI_API_HOSTNAME
58
62
BASE_CONTENT_URL = urljoin (ORIGIN_HOST , settings .CONTENT_PATH_PREFIX )
59
63
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
+
60
75
61
76
class PyPIMixin :
62
77
"""Mixin to get index specific info."""
@@ -235,14 +250,42 @@ class SimpleView(PackageUploadMixin, ViewSet):
235
250
],
236
251
}
237
252
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
+
238
272
@extend_schema (summary = "Get index simple page" )
239
273
def list (self , request , path ):
240
274
"""Gets the simple api html page for the index."""
241
275
repo_version , content = self .get_rvc ()
242
276
if self .should_redirect (repo_version = repo_version ):
243
277
return redirect (urljoin (self .base_content_url , f"{ path } /simple/" ))
244
278
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 )
246
289
247
290
def pull_through_package_simple (self , package , path , remote ):
248
291
"""Gets the package's simple page from remote."""
@@ -252,7 +295,12 @@ def parse_package(release_package):
252
295
stripped_url = urlunsplit (chain (parsed [:3 ], ("" , "" )))
253
296
redirect_path = f"{ path } /{ release_package .filename } ?redirect={ stripped_url } "
254
297
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
+ }
256
304
257
305
rfilter = get_remote_package_filter (remote )
258
306
if not rfilter .filter_project (package ):
@@ -269,7 +317,7 @@ def parse_package(release_package):
269
317
except TimeoutException :
270
318
return HttpResponse (f"{ remote .url } timed out while fetching { package } ." , status = 504 )
271
319
272
- if d .headers ["content-type" ] == "application/vnd.pypi.simple.v1+json" :
320
+ if d .headers ["content-type" ] == PYPI_SIMPLE_V1_JSON :
273
321
page = ProjectPage .from_json_data (json .load (open (d .path , "rb" )), base_url = url )
274
322
else :
275
323
page = ProjectPage .from_html (package , open (d .path , "rb" ).read (), base_url = url )
@@ -290,7 +338,15 @@ def retrieve(self, request, path, package):
290
338
return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
291
339
packages = (
292
340
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
+ )
294
350
.iterator ()
295
351
)
296
352
try :
@@ -300,8 +356,28 @@ def retrieve(self, request, path, package):
300
356
else :
301
357
packages = chain ([present ], packages )
302
358
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 )
305
381
306
382
@extend_schema (
307
383
request = PackageUploadSerializer ,
0 commit comments