1818 HttpResponseNotFound ,
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
6769CACHE_CONTROL = "max-age=600, public"
6870
6971
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 ]
93+
94+
7095class PyPISimpleHTMLRenderer (TemplateHTMLRenderer ):
7196 media_type = PYPI_SIMPLE_V1_HTML
7297
@@ -299,13 +324,9 @@ def get_provenance_url(self, package, version, filename):
299324 self .base_api_url , f"{ base_path } /integrity/{ package } /{ version } /{ filename } /provenance/"
300325 )
301326
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-
308327 @extend_schema (summary = "Get index simple page" )
328+ @method_decorator (_add_cache_control_to_304 )
329+ @method_decorator (condition (etag_func = _etag_func ))
309330 @PythonApiCache (base_key = find_base_path_cached )
310331 def list (self , request , path ):
311332 """Gets the simple api html page for the index."""
@@ -319,11 +340,7 @@ def list(self, request, path):
319340 .iterator ()
320341 )
321342 media_type = request .accepted_renderer .media_type
322- headers = {
323- "X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ),
324- "Cache-Control" : CACHE_CONTROL ,
325- "ETag" : self ._make_etag (repo_version ),
326- }
343+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ), "Cache-Control" : CACHE_CONTROL }
327344
328345 if media_type == PYPI_SIMPLE_V1_JSON :
329346 index_data = write_simple_index_json (names )
@@ -368,17 +385,17 @@ def parse_package(release_package):
368385 }
369386
370387 @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 ))
371390 @PythonApiCache (base_key = find_base_path_cached )
372391 def retrieve (self , request , path , package ):
373392 """Retrieves the simple api html/json page for a package."""
374393 repo_ver , content = self .get_rvc ()
375394 # Should I redirect if the normalized name is different?
376395 normalized = canonicalize_name (package )
377396 releases = {}
378- is_pull_through = False
379397 if self .distribution .remote :
380398 releases = self .pull_through_package_simple (normalized , path , self .distribution .remote )
381- is_pull_through = True
382399 elif self .should_redirect (repo_version = repo_ver ):
383400 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
384401 if content is not None :
@@ -421,12 +438,7 @@ def retrieve(self, request, path, package):
421438 return HttpResponseNotFound (f"{ normalized } does not exist." )
422439
423440 media_type = request .accepted_renderer .media_type
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 )
441+ headers = {"X-PyPI-Last-Serial" : str (PYPI_SERIAL_CONSTANT ), "Cache-Control" : CACHE_CONTROL }
430442
431443 if media_type == PYPI_SIMPLE_V1_JSON :
432444 detail_data = write_simple_detail_json (normalized , releases .values ())
0 commit comments