|
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_TEXT_HTML = "text/html" |
| 65 | +PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" |
| 66 | +PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json" |
| 67 | + |
| 68 | + |
| 69 | +class PyPISimpleHTMLRenderer(TemplateHTMLRenderer): |
| 70 | + media_type = PYPI_SIMPLE_V1_HTML |
| 71 | + |
| 72 | + |
| 73 | +class PyPISimpleJSONRenderer(JSONRenderer): |
| 74 | + media_type = PYPI_SIMPLE_V1_JSON |
| 75 | + |
60 | 76 |
|
61 | 77 | class PyPIMixin: |
62 | 78 | """Mixin to get index specific info.""" |
@@ -235,14 +251,49 @@ class SimpleView(PackageUploadMixin, ViewSet): |
235 | 251 | ], |
236 | 252 | } |
237 | 253 |
|
| 254 | + def perform_content_negotiation(self, request, force=False): |
| 255 | + """ |
| 256 | + Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found. |
| 257 | + """ |
| 258 | + try: |
| 259 | + return super().perform_content_negotiation(request, force) |
| 260 | + except NotAcceptable: |
| 261 | + return TemplateHTMLRenderer(), "text/html" |
| 262 | + |
| 263 | + def get_renderers(self): |
| 264 | + """ |
| 265 | + Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones. |
| 266 | + """ |
| 267 | + if self.action in ["list", "retrieve"]: |
| 268 | + # Ordered by priority if multiple content types are present |
| 269 | + return [ |
| 270 | + TemplateHTMLRenderer(), |
| 271 | + PyPISimpleHTMLRenderer(), |
| 272 | + PyPISimpleJSONRenderer(), |
| 273 | + ] |
| 274 | + else: |
| 275 | + return [JSONRenderer(), BrowsableAPIRenderer()] |
| 276 | + |
| 277 | + |
238 | 278 | @extend_schema(summary="Get index simple page") |
239 | 279 | def list(self, request, path): |
240 | 280 | """Gets the simple api html page for the index.""" |
241 | 281 | repo_version, content = self.get_rvc() |
242 | 282 | if self.should_redirect(repo_version=repo_version): |
243 | 283 | 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)) |
| 284 | + |
| 285 | + names = content.order_by("name").values_list("name", flat=True).distinct() |
| 286 | + media_type = request.accepted_renderer.media_type |
| 287 | + |
| 288 | + if media_type == PYPI_SIMPLE_V1_JSON: |
| 289 | + index_data = write_simple_index_json(list(names)) |
| 290 | + headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} |
| 291 | + return Response(index_data, headers=headers) |
| 292 | + else: |
| 293 | + index_data = write_simple_index(names.iterator(), streamed=True) |
| 294 | + kwargs = {"content_type": media_type} |
| 295 | + response = StreamingHttpResponse(index_data, **kwargs) |
| 296 | + return response |
246 | 297 |
|
247 | 298 | def pull_through_package_simple(self, package, path, remote): |
248 | 299 | """Gets the package's simple page from remote.""" |
@@ -301,7 +352,16 @@ def retrieve(self, request, path, package): |
301 | 352 | packages = chain([present], packages) |
302 | 353 | name = present[2] |
303 | 354 | 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)) |
| 355 | + media_type = request.accepted_renderer.media_type |
| 356 | + |
| 357 | + if media_type == PYPI_SIMPLE_V1_JSON: |
| 358 | + detail_data = write_simple_detail_json(name, list(releases)) |
| 359 | + headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)} |
| 360 | + return Response(detail_data, headers=headers) |
| 361 | + else: |
| 362 | + detail_data = write_simple_detail(name, releases, streamed=True) |
| 363 | + kwargs = {"content_type": media_type} |
| 364 | + return StreamingHttpResponse(detail_data, kwargs) |
305 | 365 |
|
306 | 366 | @extend_schema( |
307 | 367 | request=PackageUploadSerializer, |
|
0 commit comments