Skip to content

Commit 7fc1756

Browse files
authored
Merge pull request #1086 from gerrod3/pt-uv-metadata
Fix pull-through metadata serving and saving
2 parents 6008980 + 9e2bd40 commit 7fc1756

File tree

5 files changed

+110
-3
lines changed

5 files changed

+110
-3
lines changed

CHANGES/1083.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed pull-through PEP 658 metadata not being served correctly for certain tools.

CHANGES/1087.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed pull-through PEP 658 metadata not being saved correctly with the package.

docs/user/guides/publish.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ pulp python distribution update --name foo --remote bar
105105
Functionality may not work or may be incomplete. Also, backwards compatibility when upgrading
106106
is not guaranteed.
107107

108+
!!! warning
109+
Chaining pull-through indices, having a pull-through point to another pull-through, does not
110+
work.
111+
108112
## Use the newly created distribution
109113

110114
The metadata and packages can now be retrieved from the distribution:

pulp_python/app/models.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .provenance import Provenance
2626
from .utils import (
2727
artifact_to_python_content_data,
28+
artifact_to_metadata_artifact,
2829
canonicalize_name,
2930
python_content_to_json,
3031
PYPI_LAST_SERIAL,
@@ -215,7 +216,10 @@ def init_from_artifact_and_relative_path(artifact, relative_path):
215216
"""Used when downloading package from pull-through cache."""
216217
path = PurePath(relative_path)
217218
data = artifact_to_python_content_data(path.name, artifact, domain=get_domain())
218-
return PythonPackageContent(**data)
219+
artifacts = {path.name: artifact}
220+
if metadata_artifact := artifact_to_metadata_artifact(path.name, artifact):
221+
artifacts[f"{path.name}.metadata"] = metadata_artifact
222+
return PythonPackageContent(**data), artifacts
219223

220224
def __str__(self):
221225
"""
@@ -320,11 +324,25 @@ def get_remote_artifact_url(self, relative_path=None, request=None):
320324
"""Get url for remote_artifact"""
321325
if request and (url := request.query.get("redirect")):
322326
# This is a special case for pull-through caching
327+
# To handle PEP 658, it states that if the package has metadata available then it
328+
# should be found at the download URL + ".metadata". Thus if the request url ends with
329+
# ".metadata" then we need to add ".metadata" to the redirect url if not present.
330+
if relative_path:
331+
if relative_path.endswith(".metadata") and not url.endswith(".metadata"):
332+
url += ".metadata"
333+
# Handle special case for bug in pip (TODO file issue in pip) where it appends
334+
# ".metadata" to the redirect url instead of the request url
335+
if url.endswith(".metadata") and not relative_path.endswith(".metadata"):
336+
setattr(self, "_real_relative_path", url.rsplit("/", 1)[1])
323337
return url
324338
return super().get_remote_artifact_url(relative_path, request=request)
325339

326340
def get_remote_artifact_content_type(self, relative_path=None):
327-
"""Return PythonPackageContent."""
341+
"""Return PythonPackageContent, except for metadata artifacts."""
342+
if hasattr(self, "_real_relative_path"):
343+
relative_path = getattr(self, "_real_relative_path")
344+
if relative_path and relative_path.endswith(".whl.metadata"):
345+
return None
328346
return PythonPackageContent
329347

330348
class Meta:

pulp_python/tests/functional/api/test_full_mirror.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111

1212
from pypi_simple import ProjectPage
1313
from packaging.version import parse
14-
from urllib.parse import urljoin, urlsplit
14+
from urllib.parse import urljoin, urlsplit, urlunsplit
1515
from random import sample
16+
from hashlib import sha256
1617

1718

1819
def test_pull_through_install(
@@ -202,3 +203,85 @@ def test_pull_through_filtering_bad_names(python_remote_factory, python_distribu
202203
# Should have no packages with None version (they get filtered out)
203204
assert len(project_page.packages) > 0
204205
assert all(package.version is not None for package in project_page.packages)
206+
207+
208+
@pytest.mark.parallel
209+
def test_pull_through_metadata(python_remote_factory, python_distribution_factory):
210+
"""
211+
Tests that metadata is correctly served when using pull-through.
212+
213+
So when requesting the metadata url according to PEP 658 you should just need to add .metadata
214+
to the end of the url path. Since pull-through includes a redirect query parameter we need to
215+
test adding .metadata to the end of the url path vs adding it to the end of redirect query.
216+
"""
217+
remote = python_remote_factory(includes=["pytz"])
218+
distro = python_distribution_factory(remote=remote.pulp_href)
219+
220+
url = f"{distro.base_url}simple/pytz/"
221+
project_page = ProjectPage.from_response(requests.get(url), "pytz")
222+
filename1 = "pytz-2023.2-py2.py3-none-any.whl"
223+
filename2 = "pytz-2023.3-py2.py3-none-any.whl"
224+
package1 = next(p for p in project_page.packages if p.filename == filename1)
225+
package2 = next(p for p in project_page.packages if p.filename == filename2)
226+
assert package1.has_metadata
227+
assert package2.has_metadata
228+
229+
# The correct way to get the metadata url: add to path (uv does this)
230+
parts1 = urlsplit(package1.url)
231+
url1 = urlunsplit((parts1[0], parts1[1], parts1[2] + ".metadata", parts1[3], parts1[4]))
232+
r = requests.get(url1)
233+
assert r.status_code == 200
234+
assert sha256(r.content).hexdigest() == package1.metadata_digests["sha256"]
235+
236+
# The incorrect way to get the metadata url: add to end of string (pip does this)
237+
url2 = package2.url + ".metadata"
238+
r = requests.get(url2)
239+
assert r.status_code == 200
240+
assert sha256(r.content).hexdigest() == package2.metadata_digests["sha256"]
241+
242+
243+
@pytest.mark.parallel
244+
def test_pull_through_metadata_with_repo(
245+
python_repo_factory,
246+
python_remote_factory,
247+
python_distribution_factory,
248+
pulpcore_bindings,
249+
):
250+
"""Tests that metadata is correctly saved when using pull-through with a repository."""
251+
remote = python_remote_factory(url=PYPI_URL, includes=["pip"])
252+
repo = python_repo_factory()
253+
distro = python_distribution_factory(repository=repo.pulp_href, remote=remote.pulp_href)
254+
255+
pip_url = f"{distro.base_url}simple/pip/"
256+
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
257+
filename = "pip-26.0.1-py3-none-any.whl"
258+
package = next(p for p in project_page.packages if p.filename == filename)
259+
assert package.has_metadata
260+
assert "?redirect=" in package.url
261+
262+
# Retrieve the metadata and assert the content was not saved to the repository
263+
parts = urlsplit(package.url)
264+
url = urlunsplit((parts[0], parts[1], parts[2] + ".metadata", parts[3], parts[4]))
265+
r = requests.get(url)
266+
assert r.status_code == 200
267+
assert sha256(r.content).hexdigest() == package.metadata_digests["sha256"]
268+
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
269+
package = next(p for p in project_page.packages if p.filename == filename)
270+
assert package.has_metadata
271+
assert "?redirect=" in package.url
272+
273+
# Now retrieve the package and assert the content was saved with metadata
274+
r = requests.get(package.url)
275+
assert r.status_code == 200
276+
pa = pulpcore_bindings.ArtifactsApi.list(sha256=package.digests["sha256"])
277+
assert pa.count == 1
278+
ma = pulpcore_bindings.ArtifactsApi.list(sha256=package.metadata_digests["sha256"])
279+
assert ma.count == 1
280+
281+
# Check the simple page is updated to point to the local repository
282+
project_page = ProjectPage.from_response(requests.get(pip_url), "pip")
283+
package = next(p for p in project_page.packages if p.filename == filename)
284+
assert "?redirect=" not in package.url
285+
r = requests.get(package.metadata_url)
286+
assert r.status_code == 200
287+
assert sha256(r.content).hexdigest() == package.metadata_digests["sha256"]

0 commit comments

Comments
 (0)