|  | 
| 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, NotFound | 
| 7 | 9 | from django.core.exceptions import ObjectDoesNotExist | 
| 8 | 10 | from django.shortcuts import redirect | 
| 9 | 11 | from datetime import datetime, timezone, timedelta | 
|  | 
| 17 | 19 |     HttpResponseBadRequest, | 
| 18 | 20 |     StreamingHttpResponse, | 
| 19 | 21 |     HttpResponse, | 
|  | 22 | +    JsonResponse, | 
| 20 | 23 | ) | 
| 21 | 24 | from drf_spectacular.utils import extend_schema | 
| 22 | 25 | from dynaconf import settings | 
|  | 
| 43 | 46 | ) | 
| 44 | 47 | from pulp_python.app.utils import ( | 
| 45 | 48 |     write_simple_index, | 
|  | 49 | +    write_simple_index_json, | 
| 46 | 50 |     write_simple_detail, | 
|  | 51 | +    write_simple_detail_json, | 
| 47 | 52 |     python_content_to_json, | 
| 48 | 53 |     PYPI_LAST_SERIAL, | 
| 49 | 54 |     PYPI_SERIAL_CONSTANT, | 
|  | 
| 57 | 62 | ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME | 
| 58 | 63 | BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX) | 
| 59 | 64 | 
 | 
|  | 65 | +# PYPI_TEXT_HTML = "text/html" | 
|  | 66 | +PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" | 
|  | 67 | +PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json" | 
|  | 68 | + | 
|  | 69 | + | 
|  | 70 | +class PyPISimpleHTMLRenderer(TemplateHTMLRenderer): | 
|  | 71 | +    media_type = PYPI_SIMPLE_V1_HTML | 
|  | 72 | + | 
|  | 73 | + | 
|  | 74 | +class PyPISimpleJSONRenderer(JSONRenderer): | 
|  | 75 | +    media_type = PYPI_SIMPLE_V1_JSON | 
|  | 76 | + | 
| 60 | 77 | 
 | 
| 61 | 78 | class PyPIMixin: | 
| 62 | 79 |     """Mixin to get index specific info.""" | 
| @@ -235,14 +252,55 @@ class SimpleView(PackageUploadMixin, ViewSet): | 
| 235 | 252 |         ], | 
| 236 | 253 |     } | 
| 237 | 254 | 
 | 
|  | 255 | +    def perform_content_negotiation(self, request, force=False): | 
|  | 256 | +        """ | 
|  | 257 | +        Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found. | 
|  | 258 | +        """ | 
|  | 259 | +        try: | 
|  | 260 | +            return super().perform_content_negotiation(request, force) | 
|  | 261 | +        except NotAcceptable: | 
|  | 262 | +            return TemplateHTMLRenderer(), "text/html" | 
|  | 263 | + | 
|  | 264 | +    def get_renderers(self): | 
|  | 265 | +        """ | 
|  | 266 | +        Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones. | 
|  | 267 | +        """ | 
|  | 268 | +        if self.action in ["list", "retrieve"]: | 
|  | 269 | +            # Ordered by priority if multiple content types are present | 
|  | 270 | +            return [ | 
|  | 271 | +                TemplateHTMLRenderer(), | 
|  | 272 | +                PyPISimpleHTMLRenderer(), | 
|  | 273 | +                PyPISimpleJSONRenderer(), | 
|  | 274 | +            ] | 
|  | 275 | +        else: | 
|  | 276 | +            return [JSONRenderer(), BrowsableAPIRenderer()] | 
|  | 277 | + | 
|  | 278 | +    def handle_exception(self, exc): | 
|  | 279 | +        # todo: fixes test_pull_through_filter? | 
|  | 280 | +        if isinstance(exc, (Http404, NotFound)): | 
|  | 281 | +            # Force JSON response since JSONRenderer is not in get_renderers() | 
|  | 282 | +            return JsonResponse({"detail": str(exc)}, status=404) | 
|  | 283 | +        return super().handle_exception(exc) | 
|  | 284 | + | 
| 238 | 285 |     @extend_schema(summary="Get index simple page") | 
| 239 | 286 |     def list(self, request, path): | 
| 240 | 287 |         """Gets the simple api html page for the index.""" | 
| 241 | 288 |         repo_version, content = self.get_rvc() | 
| 242 | 289 |         if self.should_redirect(repo_version=repo_version): | 
| 243 | 290 |             return redirect(urljoin(self.base_content_url, f"{path}/simple/")) | 
| 244 |  | -        names = content.order_by("name").values_list("name", flat=True).distinct().iterator() | 
| 245 |  | -        return StreamingHttpResponse(write_simple_index(names, streamed=True)) | 
|  | 291 | + | 
|  | 292 | +        names = content.order_by("name").values_list("name", flat=True).distinct() | 
|  | 293 | +        media_type = request.accepted_renderer.media_type | 
|  | 294 | + | 
|  | 295 | +        if media_type == PYPI_SIMPLE_V1_JSON: | 
|  | 296 | +            index_data = write_simple_index_json(list(names)) | 
|  | 297 | +            headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} | 
|  | 298 | +            return Response(index_data, headers=headers) | 
|  | 299 | +        else: | 
|  | 300 | +            index_data = write_simple_index(names.iterator(), streamed=True) | 
|  | 301 | +            kwargs = {"content_type": media_type} | 
|  | 302 | +            response = StreamingHttpResponse(index_data, **kwargs) | 
|  | 303 | +            return response | 
| 246 | 304 | 
 | 
| 247 | 305 |     def pull_through_package_simple(self, package, path, remote): | 
| 248 | 306 |         """Gets the package's simple page from remote.""" | 
| @@ -301,7 +359,16 @@ def retrieve(self, request, path, package): | 
| 301 | 359 |             packages = chain([present], packages) | 
| 302 | 360 |             name = present[2] | 
| 303 | 361 |         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)) | 
|  | 362 | +        media_type = request.accepted_renderer.media_type | 
|  | 363 | + | 
|  | 364 | +        if media_type == PYPI_SIMPLE_V1_JSON: | 
|  | 365 | +            detail_data = write_simple_detail_json(name, list(releases)) | 
|  | 366 | +            headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} | 
|  | 367 | +            return Response(detail_data, headers=headers) | 
|  | 368 | +        else: | 
|  | 369 | +            detail_data = write_simple_detail(name, releases, streamed=True) | 
|  | 370 | +            kwargs = {"content_type": media_type} | 
|  | 371 | +            return StreamingHttpResponse(detail_data, kwargs) | 
| 305 | 372 | 
 | 
| 306 | 373 |     @extend_schema( | 
| 307 | 374 |         request=PackageUploadSerializer, | 
|  | 
0 commit comments