1+ import hashlib
12import logging
23from datetime import datetime , timedelta , timezone
34from itertools import chain
1516 HttpResponseBadRequest ,
1617 HttpResponseForbidden ,
1718 HttpResponseNotFound ,
18- StreamingHttpResponse ,
1919)
2020from django .shortcuts import redirect
2121from drf_spectacular .utils import extend_schema
3131from pulpcore .plugin .viewsets import OperationPostponedResponse
3232
3333from pulp_python .app import tasks
34+ from pulp_python .app .cache import PythonApiCache , find_base_path_cached
3435from pulp_python .app .models import (
3536 PackageProvenance ,
3637 PythonDistribution ,
6364
6465PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
6566PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
67+ CACHE_CONTROL = "max-age=600, public"
6668
6769
6870class PyPISimpleHTMLRenderer (TemplateHTMLRenderer ):
@@ -297,7 +299,14 @@ def get_provenance_url(self, package, version, filename):
297299 self .base_api_url , f"{ base_path } /integrity/{ package } /{ version } /{ filename } /provenance/"
298300 )
299301
302+ @staticmethod
303+ def _make_etag (repo_version ):
304+ """Builds a quoted ETag from the repo version. Changes when content changes."""
305+ raw = f"{ repo_version .number } :{ repo_version .pulp_created .isoformat ()} "
306+ return f'"{ hashlib .sha256 (raw .encode ()).hexdigest ()[:16 ]} "'
307+
300308 @extend_schema (summary = "Get index simple page" )
309+ @PythonApiCache (base_key = find_base_path_cached )
301310 def list (self , request , path ):
302311 """Gets the simple api html page for the index."""
303312 repo_version , content = self .get_rvc ()
@@ -310,15 +319,19 @@ def list(self, request, path):
310319 .iterator ()
311320 )
312321 media_type = request .accepted_renderer .media_type
313- headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
322+ headers = {
323+ "X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ),
324+ "Cache-Control" : CACHE_CONTROL ,
325+ "ETag" : self ._make_etag (repo_version ),
326+ }
314327
315328 if media_type == PYPI_SIMPLE_V1_JSON :
316329 index_data = write_simple_index_json (names )
317330 return Response (index_data , headers = headers )
318331 else :
319- index_data = write_simple_index (names , streamed = True )
332+ index_data = write_simple_index (names )
320333 kwargs = {"content_type" : media_type , "headers" : headers }
321- return StreamingHttpResponse (index_data , ** kwargs )
334+ return HttpResponse (index_data , ** kwargs )
322335
323336 def pull_through_package_simple (self , package , path , remote ):
324337 """Gets the package's simple page from remote."""
@@ -355,14 +368,17 @@ def parse_package(release_package):
355368 }
356369
357370 @extend_schema (operation_id = "pypi_simple_package_read" , summary = "Get package simple page" )
371+ @PythonApiCache (base_key = find_base_path_cached )
358372 def retrieve (self , request , path , package ):
359373 """Retrieves the simple api html/json page for a package."""
360374 repo_ver , content = self .get_rvc ()
361375 # Should I redirect if the normalized name is different?
362376 normalized = canonicalize_name (package )
363377 releases = {}
378+ is_pull_through = False
364379 if self .distribution .remote :
365380 releases = self .pull_through_package_simple (normalized , path , self .distribution .remote )
381+ is_pull_through = True
366382 elif self .should_redirect (repo_version = repo_ver ):
367383 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
368384 if content is not None :
@@ -405,7 +421,12 @@ def retrieve(self, request, path, package):
405421 return HttpResponseNotFound (f"{ normalized } does not exist." )
406422
407423 media_type = request .accepted_renderer .media_type
408- headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
424+ headers = {
425+ "X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ),
426+ "Cache-Control" : CACHE_CONTROL ,
427+ }
428+ if not is_pull_through :
429+ headers ["ETag" ] = self ._make_etag (repo_ver )
409430
410431 if media_type == PYPI_SIMPLE_V1_JSON :
411432 detail_data = write_simple_detail_json (normalized , releases .values ())
0 commit comments