pulp-python 3.31.2__py3-none-any.whl → 3.32.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
12
12
 
13
13
  name = "pulp_python.app"
14
14
  label = "python"
15
- version = "3.31.2"
15
+ version = "3.32.0"
16
16
  python_package_name = "pulp-python"
17
17
  domain_compatible = True
18
18
 
@@ -0,0 +1,32 @@
1
+ from pulpcore.plugin.cache import CacheKeys, SyncContentCache
2
+ from pulpcore.plugin.util import cache_key
3
+
4
+ ACCEPT_HEADER_KEY = "accept_header"
5
+
6
+
7
+ class PythonApiCache(SyncContentCache):
8
+ """
9
+ Cache for the Simple API.
10
+
11
+ Adds Accept header to the cache key so HTML and JSON responses are cached separately.
12
+ """
13
+
14
+ def __init__(self, base_key=None):
15
+ keys = (CacheKeys.path, CacheKeys.method, ACCEPT_HEADER_KEY)
16
+ super().__init__(base_key=base_key, keys=keys)
17
+
18
+ def make_key(self, request):
19
+ all_keys = {
20
+ CacheKeys.path: request.path,
21
+ CacheKeys.method: request.method,
22
+ ACCEPT_HEADER_KEY: request.headers.get("accept", ""),
23
+ }
24
+ return ":".join(all_keys[k] for k in self.keys)
25
+
26
+
27
+ def find_base_path_cached(request, cached):
28
+ """
29
+ Resolve the distribution base_path for use as the Redis cache base_key.
30
+ """
31
+ path = request.resolver_match.kwargs["path"]
32
+ return cache_key(path)
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  import logging
2
3
  from datetime import datetime, timedelta, timezone
3
4
  from itertools import chain
@@ -15,9 +16,11 @@ from django.http.response import (
15
16
  HttpResponseBadRequest,
16
17
  HttpResponseForbidden,
17
18
  HttpResponseNotFound,
18
- StreamingHttpResponse,
19
19
  )
20
20
  from django.shortcuts import redirect
21
+ from django.utils.decorators import method_decorator
22
+ from django.views.decorators.cache import cache_control
23
+ from django.views.decorators.http import condition
21
24
  from drf_spectacular.utils import extend_schema
22
25
  from dynaconf import settings
23
26
  from packaging.utils import canonicalize_name
@@ -31,6 +34,7 @@ from pulpcore.plugin.util import get_domain, get_url
31
34
  from pulpcore.plugin.viewsets import OperationPostponedResponse
32
35
 
33
36
  from pulp_python.app import tasks
37
+ from pulp_python.app.cache import PythonApiCache, find_base_path_cached
34
38
  from pulp_python.app.models import (
35
39
  PackageProvenance,
36
40
  PythonDistribution,
@@ -65,6 +69,17 @@ PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
65
69
  PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
66
70
 
67
71
 
72
+ def _etag_func(request, path, **kwargs):
73
+ """Compute unquoted ETag for the condition decorator. Returns None if no repo."""
74
+ try:
75
+ distro = PyPIMixin.get_distribution(path)
76
+ repo_ver = PyPIMixin.get_repository_version(distro)
77
+ except Http404:
78
+ return None
79
+ raw = f"{repo_ver.number}:{repo_ver.pulp_created.isoformat()}"
80
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
81
+
82
+
68
83
  class PyPISimpleHTMLRenderer(TemplateHTMLRenderer):
69
84
  media_type = PYPI_SIMPLE_V1_HTML
70
85
 
@@ -298,6 +313,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
298
313
  )
299
314
 
300
315
  @extend_schema(summary="Get index simple page")
316
+ @method_decorator(cache_control(max_age=600, public=True))
317
+ @method_decorator(condition(etag_func=_etag_func))
318
+ @PythonApiCache(base_key=find_base_path_cached)
301
319
  def list(self, request, path):
302
320
  """Gets the simple api html page for the index."""
303
321
  repo_version, content = self.get_rvc()
@@ -316,9 +334,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
316
334
  index_data = write_simple_index_json(names)
317
335
  return Response(index_data, headers=headers)
318
336
  else:
319
- index_data = write_simple_index(names, streamed=True)
337
+ index_data = write_simple_index(names)
320
338
  kwargs = {"content_type": media_type, "headers": headers}
321
- return StreamingHttpResponse(index_data, **kwargs)
339
+ return HttpResponse(index_data, **kwargs)
322
340
 
323
341
  def pull_through_package_simple(self, package, path, remote):
324
342
  """Gets the package's simple page from remote."""
@@ -355,6 +373,9 @@ class SimpleView(PackageUploadMixin, ViewSet):
355
373
  }
356
374
 
357
375
  @extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
376
+ @method_decorator(cache_control(max_age=600, public=True))
377
+ @method_decorator(condition(etag_func=_etag_func))
378
+ @PythonApiCache(base_key=find_base_path_cached)
358
379
  def retrieve(self, request, path, package):
359
380
  """Retrieves the simple api html/json page for a package."""
360
381
  repo_ver, content = self.get_rvc()
@@ -245,8 +245,9 @@ def test_simple_redirect_with_publications(
245
245
  assert response.url == str(urljoin(pulp_content_url, f"{distro.base_path}/simple/"))
246
246
 
247
247
 
248
- @pytest.mark.parallel
249
- def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribution_factory):
248
+ def test_pypi_json(
249
+ delete_orphans_pre, python_remote_factory, python_repo_with_sync, python_distribution_factory
250
+ ):
250
251
  """Checks the data of `pypi/{package_name}/json` endpoint."""
251
252
  remote = python_remote_factory(policy="immediate")
252
253
  repo = python_repo_with_sync(remote)
@@ -260,8 +261,8 @@ def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribu
260
261
  assert_pypi_json(response.json())
261
262
 
262
263
 
263
- @pytest.mark.parallel
264
264
  def test_pypi_json_content_app(
265
+ delete_orphans_pre,
265
266
  python_remote_factory,
266
267
  python_repo_with_sync,
267
268
  python_publication_factory,
@@ -0,0 +1,101 @@
1
+ from urllib.parse import urljoin
2
+
3
+ import pytest
4
+ import requests
5
+
6
+ from pulp_python.tests.functional.constants import (
7
+ PYPI_SIMPLE_V1_HTML,
8
+ PYPI_SIMPLE_V1_JSON,
9
+ PYTHON_SM_PROJECT_SPECIFIER,
10
+ )
11
+
12
+
13
+ @pytest.fixture
14
+ def skip_without_cache(pulp_settings):
15
+ """
16
+ Skip test if server-side caching is not enabled.
17
+ """
18
+ if not pulp_settings.CACHE_ENABLED:
19
+ pytest.skip("CACHE_ENABLED is not set")
20
+
21
+
22
+ @pytest.fixture
23
+ def synced_distro(
24
+ skip_without_cache,
25
+ python_remote_factory,
26
+ python_repo_with_sync,
27
+ python_distribution_factory,
28
+ ):
29
+ """
30
+ Sync a repo and create a distribution for cache tests.
31
+ """
32
+ remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
33
+ repo = python_repo_with_sync(remote)
34
+ return python_distribution_factory(repository=repo)
35
+
36
+
37
+ @pytest.mark.parallel
38
+ def test_simple_cache_hit_miss_and_headers(synced_distro):
39
+ """
40
+ First request is a MISS, second is a HIT. Cache headers are present and stable.
41
+ """
42
+ index_url = urljoin(synced_distro.base_url, "simple/")
43
+ detail_url = f"{index_url}aiohttp"
44
+
45
+ for url in [index_url, detail_url]:
46
+ r1 = requests.get(url)
47
+ assert r1.status_code == 200
48
+ assert r1.headers["X-PULP-CACHE"] == "MISS"
49
+ assert r1.headers["Cache-Control"] == "max-age=600, public"
50
+ assert r1.headers["ETag"].startswith('"') and r1.headers["ETag"].endswith('"')
51
+
52
+ r2 = requests.get(url)
53
+ assert r2.status_code == 200
54
+ assert r2.headers["X-PULP-CACHE"] == "HIT"
55
+ assert r2.headers["Cache-Control"] == r1.headers["Cache-Control"]
56
+ assert r2.headers["ETag"] == r1.headers["ETag"]
57
+
58
+
59
+ @pytest.mark.parallel
60
+ def test_simple_cache_separate_accept_headers(synced_distro):
61
+ """
62
+ HTML and JSON responses are cached separately.
63
+ """
64
+ url = urljoin(synced_distro.base_url, "simple/")
65
+
66
+ for header in [PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_JSON]:
67
+ r = requests.get(url, headers={"Accept": header})
68
+ assert r.status_code == 200
69
+ assert r.headers["X-PULP-CACHE"] == "MISS"
70
+
71
+ for header in [PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_JSON]:
72
+ r = requests.get(url, headers={"Accept": header})
73
+ assert r.status_code == 200
74
+ assert r.headers["X-PULP-CACHE"] == "HIT"
75
+
76
+
77
+ @pytest.mark.parallel
78
+ def test_simple_cache_etag_conditional_request(synced_distro):
79
+ """
80
+ Matching If-None-Match returns 304, non-matching returns 200.
81
+ """
82
+ url = urljoin(synced_distro.base_url, "simple/")
83
+
84
+ r1 = requests.get(url)
85
+ assert r1.status_code == 200
86
+ etag = r1.headers["ETag"]
87
+ cache_control = r1.headers["Cache-Control"]
88
+
89
+ r2 = requests.get(url, headers={"If-None-Match": etag})
90
+ assert r2.status_code == 304
91
+ assert r2.headers["ETag"] == etag
92
+ assert r2.headers["Cache-Control"] == cache_control
93
+ assert "X-PULP-CACHE" not in r2.headers
94
+ assert len(r2.content) == 0
95
+
96
+ r3 = requests.get(url, headers={"If-None-Match": '"old"'})
97
+ assert r3.status_code == 200
98
+ assert r3.headers["ETag"] == etag
99
+ assert r3.headers["Cache-Control"] == cache_control
100
+ assert r3.headers["X-PULP-CACHE"] == "HIT"
101
+ assert len(r3.content) > 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.31.2
3
+ Version: 3.32.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: pulpcore<3.115,>=3.105.0
23
+ Requires-Dist: pulpcore<3.130,>=3.105.0
24
24
  Requires-Dist: pkginfo<1.13.0,>=1.12.0
25
25
  Requires-Dist: bandersnatch<6.7,>=6.6.0
26
26
  Requires-Dist: pypi-simple<2.0,>=1.8.0
@@ -1,6 +1,7 @@
1
1
  pulp_python/__init__.py,sha256=GIuTLoBTc-07dSLJUh8xrZPRz8x-jJ61pfR0J1IjnzI,65
2
2
  pulp_python/pytest_plugin.py,sha256=LNnLjOkeEu2X4gJi614bHVmbsHyEwooHYIeecr96Qy4,8606
3
- pulp_python/app/__init__.py,sha256=Sxuni7NCBKiQ9nYqBToQB56-Q_DWCCPnQoqqzDiM1KU,2490
3
+ pulp_python/app/__init__.py,sha256=VZCkFs4G-R_9gqInqPfPFeXSclvXgRlnM-0lppXUeIs,2490
4
+ pulp_python/app/cache.py,sha256=Lp9jJNZmM-ps_CeQb3ffvOTuaf7d83BpVyXcJOPxv5k,974
4
5
  pulp_python/app/exceptions.py,sha256=mPNcWyuzF0XeOPybm-G6oDKtRqvfj4jyp41iaYqcqkA,1314
5
6
  pulp_python/app/global_access_conditions.py,sha256=MZJtyoVsr-4hRaty6mKDqh3caOHd5UKJjEWLV2crOLs,1080
6
7
  pulp_python/app/modelresource.py,sha256=4SFAdqk6lozi_cZz4uqDIqhqPAZF-7l5jJwPn-xGZFs,1249
@@ -41,7 +42,7 @@ pulp_python/app/migrations/0022_pythonblocklistentry.py,sha256=EbtjZuN65myTAHVWr
41
42
  pulp_python/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
43
  pulp_python/app/pypi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
44
  pulp_python/app/pypi/serializers.py,sha256=rE0Cci2IMg4NbNWlVdo-8zPaJCAUq2Re57q_T3jMt0Y,5030
44
- pulp_python/app/pypi/views.py,sha256=F55Zp4GPAQDVk936Szu2ECge8p1-5A160iqzrMDTZGc,21668
45
+ pulp_python/app/pypi/views.py,sha256=guGJeH9D4HZ5cG4YaByjwIr-1CrC2ziD7YkM_gPgH_U,22616
45
46
  pulp_python/app/tasks/__init__.py,sha256=lTFpVvpDKbqv9RC0b2RYU8Bo6svDjrA-djt16pADFr8,284
46
47
  pulp_python/app/tasks/publish.py,sha256=bjsJzqJbLu7TF5rLb-UsZMmlNnc_LKw-sdHX9Gcatbw,4334
47
48
  pulp_python/app/tasks/repair.py,sha256=5InzdbjW8y3AC4Vj2PsNLm3wGGTr8D3LcfPw_WA2Fks,12257
@@ -67,10 +68,11 @@ pulp_python/tests/functional/api/test_domains.py,sha256=uEA7dIBXaXah3WQ6g6xSIjdX
67
68
  pulp_python/tests/functional/api/test_download_content.py,sha256=5IuaHXyLakPkjm5sLTxtTl7Dq0yl7N36HpObnIa0Sks,4950
68
69
  pulp_python/tests/functional/api/test_export_import.py,sha256=rHns9wdaeP-vtfW_qoGHU9EVuJ4YuWE0-rjl_8otAgU,4530
69
70
  pulp_python/tests/functional/api/test_full_mirror.py,sha256=eU5HzgBBFOnX1q4m8ELx-t-I3U4TWXL-LNrlc2BcWVc,12045
70
- pulp_python/tests/functional/api/test_pypi_apis.py,sha256=mhST0GV-uDrVPsBWYRGaJx1pp9htzbYiUGuIGOI0s_s,13218
71
+ pulp_python/tests/functional/api/test_pypi_apis.py,sha256=TyehTS_Jo83yAb1d0l4XOWUJ-g4380UraCsKvhF_FWo,13224
71
72
  pulp_python/tests/functional/api/test_pypi_simple_api.py,sha256=tOUl8WXgJkMoxGyLbA-WorCxIRKfGWM-27_dK_Jc0Q4,7171
72
73
  pulp_python/tests/functional/api/test_rbac.py,sha256=-xNWqvKKU-v1uC6tCRGBK0aWaff25K9C-AfzQkdHhhI,10513
73
74
  pulp_python/tests/functional/api/test_repair.py,sha256=4FR7jx_LA2K-pnRJXKkhq-1uUoWXST1leZ9XdyhGM2U,12909
75
+ pulp_python/tests/functional/api/test_simple_cache.py,sha256=XPK56vujbAPh6ENQZjCXIEExoPF_scbrcxMtS2iTAWM,3175
74
76
  pulp_python/tests/functional/api/test_sync.py,sha256=TTHR1CpZeRoD97RKxj2pZPsP0OS9ibYKGPOh1WfKISg,13801
75
77
  pulp_python/tests/functional/api/test_upload.py,sha256=PZ0HK7UU9gvxNaRlB_EAY1-pcghDtx22DMGk5C1oM2I,5879
76
78
  pulp_python/tests/functional/api/test_vulnerability_report.py,sha256=Rv492Wrvu1FY7O_moo9DTB6OkI-OZURj_fuTKbENLh8,1730
@@ -78,9 +80,9 @@ pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation,
78
80
  pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation,sha256=muTQ8dqYSSdx76DlaPjB1REcNIS-aak-Na0TkASxu8M,10426
79
81
  pulp_python/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
82
  pulp_python/tests/unit/test_models.py,sha256=TBI0yKsrdbnJSPeBFfxSqhXK7zaNvR6qg5JehGH3Pds,229
81
- pulp_python-3.31.2.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
82
- pulp_python-3.31.2.dist-info/METADATA,sha256=baT0dVyozWQL7UQh3iIuFCMtqGNoEAEf1lAqhq4_F_0,1744
83
- pulp_python-3.31.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
84
- pulp_python-3.31.2.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
85
- pulp_python-3.31.2.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
86
- pulp_python-3.31.2.dist-info/RECORD,,
83
+ pulp_python-3.32.0.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
84
+ pulp_python-3.32.0.dist-info/METADATA,sha256=Lp9PYh6JUdO-cfIsmGV8ih-xGZXPrDczNairwtRM9-0,1744
85
+ pulp_python-3.32.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
86
+ pulp_python-3.32.0.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
87
+ pulp_python-3.32.0.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
88
+ pulp_python-3.32.0.dist-info/RECORD,,