Skip to content

Commit 5db692e

Browse files
committed
Add Simple API integration
1 parent 20631e5 commit 5db692e

4 files changed

Lines changed: 76 additions & 2 deletions

File tree

pulp_python/app/pypi/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from pulp_python.app.cache import PythonApiCache, find_base_path_cached
3838
from pulp_python.app.models import (
3939
PackageProvenance,
40+
PackageYank,
4041
PythonDistribution,
4142
PythonPackageContent,
4243
PythonPublication,
@@ -384,6 +385,7 @@ def retrieve(self, request, path, package):
384385
normalized = canonicalize_name(package)
385386
releases = {}
386387
if self.distribution.remote:
388+
# todo: what if no repo?
387389
releases = self.pull_through_package_simple(normalized, path, self.distribution.remote)
388390
elif self.should_redirect(repo_version=repo_ver):
389391
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
@@ -409,6 +411,11 @@ def retrieve(self, request, path, package):
409411
"version",
410412
"has_provenance",
411413
)
414+
yank_markers = dict(
415+
PackageYank.objects.filter(
416+
pk__in=repo_ver.content, name_normalized=normalized
417+
).values_list("version", "yanked_reason")
418+
)
412419
local_releases = {
413420
p["filename"]: {
414421
**p,
@@ -419,6 +426,8 @@ def retrieve(self, request, path, package):
419426
if p["has_provenance"]
420427
else None
421428
),
429+
"yanked": p["version"] in yank_markers,
430+
"yanked_reason": yank_markers.get(p["version"], ""),
422431
}
423432
for p in packages
424433
}

pulp_python/app/tasks/sync.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from pulp_python.app.models import (
2626
PackageProvenance,
2727
PythonPackageContent,
28+
PackageYank,
2829
PythonRemote,
2930
)
3031
from pulp_python.app.provenance import Provenance
@@ -265,6 +266,15 @@ async def create_content(self, pkg):
265266
)
266267
d_artifacts.append(metadata_artifact)
267268

269+
if upstream_pkg.is_yanked:
270+
yank_marker = PackageYank(
271+
name_normalized=pkg.name,
272+
version=version,
273+
yanked_reason=upstream_pkg.yanked_reason or "",
274+
)
275+
yank_dc = DeclarativeContent(content=yank_marker, d_artifacts=[])
276+
await self.python_stage.put(yank_dc)
277+
268278
dc = DeclarativeContent(content=package, d_artifacts=d_artifacts)
269279
declared_contents[entry["filename"]] = dc
270280
await self.python_stage.put(dc)

pulp_python/app/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
</html>
5151
"""
5252

53-
# TODO in the future: data-yanked (not implemented yet because it is mutable)
5453
simple_detail_template = """<!DOCTYPE html>
5554
<html>
5655
<head>
@@ -62,6 +61,7 @@
6261
{%- for pkg in project_packages %}
6362
<a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
6463
{%- if pkg.requires_python %} data-requires-python="{{ pkg.requires_python }}" {%- endif %}
64+
{%- if pkg.yanked %} data-yanked="{{ pkg.yanked_reason }}" {%- endif %}
6565
{%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}" data-core-metadata="sha256={{ pkg.metadata_sha256 }}"
6666
{%- endif %} {% if pkg.provenance -%}
6767
data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
@@ -591,7 +591,12 @@ def write_simple_detail_json(project_name, project_packages):
591591
"core-metadata": (
592592
{"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
593593
),
594-
# yanked and yanked_reason are not implemented because they are mutable
594+
# PEP 592
595+
"yanked": (
596+
package["yanked_reason"]
597+
if package["yanked"] and package["yanked_reason"]
598+
else package["yanked"]
599+
),
595600
# (v1.1, PEP 700)
596601
"size": package["size"],
597602
"upload-time": format_upload_time(package["upload_time"]),

pulp_python/tests/functional/api/test_yank.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import requests
55

66
from pulp_python.tests.functional.constants import (
7+
PYPI_SIMPLE_V1_JSON,
78
PYTHON_FIXTURES_URL,
89
TWINE_EGG_FILENAME,
910
TWINE_EGG_URL,
@@ -66,6 +67,16 @@ def test_yank_and_unyank(
6667
summary = python_content_summary(repository=repo, version=2)
6768
assert summary.added["python.python_yank"]["count"] == 1
6869

70+
# Simple API JSON check - yanked files should have yanked reason
71+
simple_url = f"{urljoin(distro.base_url, 'simple/')}{TWINE_NAME}"
72+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
73+
assert response.json()["files"][0]["yanked"] == "broken"
74+
assert response.json()["files"][1]["yanked"] == "broken"
75+
76+
# Simple API HTML check - data-yanked attribute should be present
77+
response = requests.get(simple_url)
78+
assert response.text.count('data-yanked="broken"') == 2
79+
6980
# Yank again - idempotent, no new repo version
7081
response = yank(distro, TWINE_NAME, TWINE_VERSION, yanked_reason="broken")
7182
assert response.status_code == 202
@@ -85,6 +96,15 @@ def test_yank_and_unyank(
8596
summary = python_content_summary(repository=repo, version=3)
8697
assert summary.removed["python.python_yank"]["count"] == 1
8798

99+
# Simple API JSON - yanked should be False after unyank
100+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
101+
assert response.json()["files"][0]["yanked"] == False
102+
assert response.json()["files"][1]["yanked"] == False
103+
104+
# Simple API HTML - data-yanked should not be present
105+
response = requests.get(simple_url)
106+
assert "data-yanked" not in response.text
107+
88108
# Unyank again - idempotent, no new repo version
89109
response = unyank(distro, TWINE_NAME, TWINE_VERSION)
90110
assert response.status_code == 202
@@ -94,6 +114,36 @@ def test_yank_and_unyank(
94114
assert repo.latest_version_href == version_3
95115

96116

117+
def test_yank_sync(
118+
delete_orphans_pre,
119+
python_remote_factory,
120+
python_repo_with_sync,
121+
python_content_summary,
122+
python_distribution_factory,
123+
):
124+
"""
125+
Syncing a yanked package from upstream creates a PackageYank marker.
126+
"""
127+
remote = python_remote_factory(includes=["twine==5.1.0"])
128+
repo = python_repo_with_sync(remote)
129+
distro = python_distribution_factory(repository=repo)
130+
131+
# Sync should have created a yank marker for twine 5.1.0
132+
summary = python_content_summary(repository=repo, version=1)
133+
assert summary.added["python.python"]["count"] == 2
134+
assert summary.added["python.python_yank"]["count"] == 1
135+
136+
# Simple API JSON check - yanked reason from upstream
137+
simple_url = f"{urljoin(distro.base_url, 'simple/')}{TWINE_NAME}"
138+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
139+
assert response.json()["files"][0]["yanked"] == "https://github.com/pypa/twine/issues/1125"
140+
assert response.json()["files"][1]["yanked"] == "https://github.com/pypa/twine/issues/1125"
141+
142+
# Simple API HTML check - data-yanked attribute present
143+
response = requests.get(simple_url)
144+
assert response.text.count("data-yanked=") == 2
145+
146+
97147
@pytest.mark.parallel
98148
def test_partial_yank(
99149
monitor_task,

0 commit comments

Comments
 (0)