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
21+ from django .utils .decorators import method_decorator
22+ from django .views .decorators .http import condition
2123from drf_spectacular .utils import extend_schema
2224from dynaconf import settings
2325from packaging .utils import canonicalize_name
3133from pulpcore .plugin .viewsets import OperationPostponedResponse
3234
3335from pulp_python .app import tasks
36+ from pulp_python .app .cache import PythonApiCache , find_base_path_cached
3437from pulp_python .app .models import (
3538 PackageProvenance ,
3639 PythonDistribution ,
6366
6467PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
6568PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
69+ CACHE_CONTROL = "max-age=600, public"
70+
71+
72+ def _add_cache_control_to_304 (func ):
73+ """Ensure 304 responses include Cache-Control."""
74+
75+ def wrapper (* args , ** kwargs ):
76+ response = func (* args , ** kwargs )
77+ if response .status_code == 304 :
78+ response ["Cache-Control" ] = CACHE_CONTROL
79+ return response
80+
81+ return wrapper
82+
83+
84+ def _etag_func (request , path , ** kwargs ):
85+ """Compute unquoted ETag for the condition decorator. Returns None if no repo."""
86+ distro = PyPIMixin .get_distribution (path )
87+ try :
88+ repo_ver = PyPIMixin .get_repository_version (distro )
89+ except Http404 :
90+ return None
91+ raw = f"{ repo_ver .number } :{ repo_ver .pulp_created .isoformat ()} "
92+ return hashlib .sha256 (raw .encode ()).hexdigest ()[:16 ]
6693
6794
6895class PyPISimpleHTMLRenderer (TemplateHTMLRenderer ):
@@ -298,6 +325,9 @@ def get_provenance_url(self, package, version, filename):
298325 )
299326
300327 @extend_schema (summary = "Get index simple page" )
328+ @method_decorator (_add_cache_control_to_304 )
329+ @method_decorator (condition (etag_func = _etag_func ))
330+ @PythonApiCache (base_key = find_base_path_cached )
301331 def list (self , request , path ):
302332 """Gets the simple api html page for the index."""
303333 repo_version , content = self .get_rvc ()
@@ -310,15 +340,15 @@ def list(self, request, path):
310340 .iterator ()
311341 )
312342 media_type = request .accepted_renderer .media_type
313- headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
343+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ), "Cache-Control" : CACHE_CONTROL }
314344
315345 if media_type == PYPI_SIMPLE_V1_JSON :
316346 index_data = write_simple_index_json (names )
317347 return Response (index_data , headers = headers )
318348 else :
319- index_data = write_simple_index (names , streamed = True )
349+ index_data = write_simple_index (names )
320350 kwargs = {"content_type" : media_type , "headers" : headers }
321- return StreamingHttpResponse (index_data , ** kwargs )
351+ return HttpResponse (index_data , ** kwargs )
322352
323353 def pull_through_package_simple (self , package , path , remote ):
324354 """Gets the package's simple page from remote."""
@@ -355,6 +385,9 @@ def parse_package(release_package):
355385 }
356386
357387 @extend_schema (operation_id = "pypi_simple_package_read" , summary = "Get package simple page" )
388+ @method_decorator (_add_cache_control_to_304 )
389+ @method_decorator (condition (etag_func = _etag_func ))
390+ @PythonApiCache (base_key = find_base_path_cached )
358391 def retrieve (self , request , path , package ):
359392 """Retrieves the simple api html/json page for a package."""
360393 repo_ver , content = self .get_rvc ()
@@ -405,7 +438,7 @@ def retrieve(self, request, path, package):
405438 return HttpResponseNotFound (f"{ normalized } does not exist." )
406439
407440 media_type = request .accepted_renderer .media_type
408- headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT )}
441+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ), "Cache-Control" : CACHE_CONTROL }
409442
410443 if media_type == PYPI_SIMPLE_V1_JSON :
411444 detail_data = write_simple_detail_json (normalized , releases .values ())
0 commit comments