From 6772a14a6156f01a8ce62f4b12d7573d065d8512 Mon Sep 17 00:00:00 2001 From: Jitka Obselkova Date: Fri, 21 Nov 2025 21:34:53 +0100 Subject: [PATCH 1/3] Refactor code --- pulp_python/app/pypi/views.py | 2 -- .../tests/functional/api/test_pypi_apis.py | 2 +- ...le_json_api.py => test_pypi_simple_api.py} | 23 ++++++++----------- pulp_python/tests/functional/constants.py | 4 ++++ 4 files changed, 14 insertions(+), 17 deletions(-) rename pulp_python/tests/functional/api/{test_pypi_simple_json_api.py => test_pypi_simple_api.py} (87%) diff --git a/pulp_python/app/pypi/views.py b/pulp_python/app/pypi/views.py index 4bbbb8a6..b7808a9e 100644 --- a/pulp_python/app/pypi/views.py +++ b/pulp_python/app/pypi/views.py @@ -352,8 +352,6 @@ def parse_package(release_package): @extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page") def retrieve(self, request, path, package): """Retrieves the simple api html/json page for a package.""" - media_type = request.accepted_renderer.media_type - repo_ver, content = self.get_rvc() # Should I redirect if the normalized name is different? normalized = canonicalize_name(package) diff --git a/pulp_python/tests/functional/api/test_pypi_apis.py b/pulp_python/tests/functional/api/test_pypi_apis.py index 6ca8eaa4..403420cc 100644 --- a/pulp_python/tests/functional/api/test_pypi_apis.py +++ b/pulp_python/tests/functional/api/test_pypi_apis.py @@ -5,6 +5,7 @@ from urllib.parse import urljoin from pulp_python.tests.functional.constants import ( + PYPI_SERIAL_CONSTANT, PYTHON_SM_PROJECT_SPECIFIER, PYTHON_SM_FIXTURE_RELEASES, PYTHON_SM_FIXTURE_CHECKSUMS, @@ -20,7 +21,6 @@ PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" -PYPI_SERIAL_CONSTANT = 1000000000 @pytest.mark.parallel diff --git a/pulp_python/tests/functional/api/test_pypi_simple_json_api.py b/pulp_python/tests/functional/api/test_pypi_simple_api.py similarity index 87% rename from pulp_python/tests/functional/api/test_pypi_simple_json_api.py rename to pulp_python/tests/functional/api/test_pypi_simple_api.py index e2c70896..982523db 100644 --- a/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +++ b/pulp_python/tests/functional/api/test_pypi_simple_api.py @@ -4,15 +4,18 @@ import requests from pulp_python.tests.functional.constants import ( + PYPI_SERIAL_CONSTANT, PYTHON_EGG_FILENAME, + PYTHON_EGG_SHA256, PYTHON_EGG_URL, PYTHON_SM_PROJECT_SPECIFIER, PYTHON_WHEEL_FILENAME, + PYTHON_WHEEL_METADATA_SHA256, + PYTHON_WHEEL_SHA256, PYTHON_WHEEL_URL, ) API_VERSION = "1.1" -PYPI_SERIAL_CONSTANT = 1000000000 PYPI_TEXT_HTML = "text/html" PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html" @@ -72,27 +75,19 @@ def test_simple_json_detail_api( assert data["versions"] == ["0.1"] # Check data of a wheel - file_whl = next( - (i for i in data["files"] if i["filename"] == "shelf_reader-0.1-py2-none-any.whl"), None - ) + file_whl = next((i for i in data["files"] if i["filename"] == PYTHON_WHEEL_FILENAME), None) assert file_whl is not None, "wheel file not found" assert file_whl["url"] - assert file_whl["hashes"] == { - "sha256": "2eceb1643c10c5e4a65970baf63bde43b79cbdac7de81dae853ce47ab05197e9" - } + assert file_whl["hashes"] == {"sha256": PYTHON_WHEEL_SHA256} assert file_whl["requires-python"] is None - assert file_whl["data-dist-info-metadata"] == { - "sha256": "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350" - } + assert file_whl["data-dist-info-metadata"] == {"sha256": PYTHON_WHEEL_METADATA_SHA256} assert file_whl["size"] == 22455 assert file_whl["upload-time"] is not None # Check data of a tarball - file_tar = next((i for i in data["files"] if i["filename"] == "shelf-reader-0.1.tar.gz"), None) + file_tar = next((i for i in data["files"] if i["filename"] == PYTHON_EGG_FILENAME), None) assert file_tar is not None, "tar file not found" assert file_tar["url"] - assert file_tar["hashes"] == { - "sha256": "04cfd8bb4f843e35d51bfdef2035109bdea831b55a57c3e6a154d14be116398c" - } + assert file_tar["hashes"] == {"sha256": PYTHON_EGG_SHA256} assert file_tar["requires-python"] is None assert file_tar["data-dist-info-metadata"] is False assert file_tar["size"] == 19097 diff --git a/pulp_python/tests/functional/constants.py b/pulp_python/tests/functional/constants.py index 2855b8a9..4150720f 100644 --- a/pulp_python/tests/functional/constants.py +++ b/pulp_python/tests/functional/constants.py @@ -150,6 +150,8 @@ PYTHON_WHEEL_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), PYTHON_WHEEL_FILENAME) PYTHON_WHEEL_SHA256 = "2eceb1643c10c5e4a65970baf63bde43b79cbdac7de81dae853ce47ab05197e9" +PYTHON_WHEEL_METADATA_SHA256 = "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350" + PYTHON_XS_FIXTURE_CHECKSUMS = { PYTHON_EGG_FILENAME: PYTHON_EGG_SHA256, PYTHON_WHEEL_FILENAME: PYTHON_WHEEL_SHA256, @@ -353,3 +355,5 @@ VULNERABILITY_REPORT_TEST_PACKAGES = [ "django==5.2.1", ] + +PYPI_SERIAL_CONSTANT = 1000000000 From 37626da96891bd732af19700c1996b9e06757b9f Mon Sep 17 00:00:00 2001 From: Jitka Obselkova Date: Fri, 21 Nov 2025 22:06:41 +0100 Subject: [PATCH 2/3] Add data-dist-info-metadata to Simple HTML API --- pulp_python/app/utils.py | 8 ++- .../tests/functional/api/test_pypi_apis.py | 21 ------- .../functional/api/test_pypi_simple_api.py | 59 +++++++++++++++++++ 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index 4363f11e..c41a393d 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -41,6 +41,8 @@ """ +# TODO in the future: data-requires-python (PEP 503) +# TODO now: strip empty lines simple_detail_template = """ @@ -50,7 +52,11 @@

Links for {{ project_name }}

{% for pkg in project_packages %} - {{ pkg.filename }}
{% endfor %} diff --git a/pulp_python/tests/functional/api/test_pypi_apis.py b/pulp_python/tests/functional/api/test_pypi_apis.py index 403420cc..1a5626d7 100644 --- a/pulp_python/tests/functional/api/test_pypi_apis.py +++ b/pulp_python/tests/functional/api/test_pypi_apis.py @@ -6,9 +6,6 @@ from pulp_python.tests.functional.constants import ( PYPI_SERIAL_CONSTANT, - PYTHON_SM_PROJECT_SPECIFIER, - PYTHON_SM_FIXTURE_RELEASES, - PYTHON_SM_FIXTURE_CHECKSUMS, PYTHON_MD_PROJECT_SPECIFIER, PYTHON_MD_PYPI_SUMMARY, PYTHON_EGG_FILENAME, @@ -17,8 +14,6 @@ SHELF_PYTHON_JSON, ) -from pulp_python.tests.functional.utils import ensure_simple - PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" @@ -213,22 +208,6 @@ def test_simple_redirect_with_publications( assert response.url == str(urljoin(pulp_content_url, f"{distro.base_path}/simple/")) -@pytest.mark.parallel -def test_simple_correctness_live( - python_remote_factory, python_repo_with_sync, python_distribution_factory -): - """Checks that the simple api on live distributions are correct.""" - remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER) - repo = python_repo_with_sync(remote) - distro = python_distribution_factory(repository=repo) - proper, msgs = ensure_simple( - urljoin(distro.base_url, "simple/"), - PYTHON_SM_FIXTURE_RELEASES, - sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS, - ) - assert proper is True, msgs - - @pytest.mark.parallel def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribution_factory): """Checks the data of `pypi/{package_name}/json` endpoint.""" diff --git a/pulp_python/tests/functional/api/test_pypi_simple_api.py b/pulp_python/tests/functional/api/test_pypi_simple_api.py index 982523db..9e054d11 100644 --- a/pulp_python/tests/functional/api/test_pypi_simple_api.py +++ b/pulp_python/tests/functional/api/test_pypi_simple_api.py @@ -8,12 +8,16 @@ PYTHON_EGG_FILENAME, PYTHON_EGG_SHA256, PYTHON_EGG_URL, + PYTHON_SM_FIXTURE_CHECKSUMS, + PYTHON_SM_FIXTURE_RELEASES, PYTHON_SM_PROJECT_SPECIFIER, PYTHON_WHEEL_FILENAME, PYTHON_WHEEL_METADATA_SHA256, PYTHON_WHEEL_SHA256, PYTHON_WHEEL_URL, + PYTHON_XS_FIXTURE_CHECKSUMS, ) +from pulp_python.tests.functional.utils import ensure_simple API_VERSION = "1.1" @@ -22,6 +26,61 @@ PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json" +@pytest.mark.parallel +def test_simple_html_index_api( + python_remote_factory, python_repo_with_sync, python_distribution_factory +): + remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = python_repo_with_sync(remote) + distro = python_distribution_factory(repository=repo) + + url = urljoin(distro.base_url, "simple/") + headers = {"Accept": PYPI_SIMPLE_V1_HTML} + + response = requests.get(url, headers=headers) + assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML + assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT) + + proper, msgs = ensure_simple( + url, PYTHON_SM_FIXTURE_RELEASES, sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS + ) + assert proper, f"Simple API validation failed: {msgs}" + + +def test_simple_html_detail_api( + delete_orphans_pre, + monitor_task, + python_bindings, + python_content_factory, + python_distribution_factory, + python_repo_factory, +): + content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL) + content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL) + body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]} + + repo = python_repo_factory() + monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task) + distro = python_distribution_factory(repository=repo) + + url = f'{urljoin(distro.base_url, "simple/")}shelf-reader' + headers = {"Accept": PYPI_SIMPLE_V1_HTML} + + response = requests.get(url, headers=headers) + assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML + assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT) + + proper, msgs = ensure_simple( + urljoin(distro.base_url, "simple/"), + {"shelf-reader": [PYTHON_WHEEL_FILENAME, PYTHON_EGG_FILENAME]}, + sha_digests=PYTHON_XS_FIXTURE_CHECKSUMS, + ) + assert proper, f"Simple API validation failed: {msgs}" + + html_content = response.text + assert f'data-dist-info-metadata="sha256={PYTHON_WHEEL_METADATA_SHA256}' in html_content + + @pytest.mark.parallel def test_simple_json_index_api( python_remote_factory, python_repo_with_sync, python_distribution_factory From ef3c7e1ad6ebad7947902c888c3339ae92fbf822 Mon Sep 17 00:00:00 2001 From: Jitka Halova Date: Fri, 12 Dec 2025 21:01:03 +0100 Subject: [PATCH 3/3] Expose metadata file --- pulp_python/app/serializers.py | 38 +++++++ pulp_python/app/tasks/sync.py | 25 +++- pulp_python/app/tasks/upload.py | 7 +- pulp_python/app/utils.py | 53 +++++++-- .../tests/functional/api/test_pypi_apis.py | 107 ++++++++++++++++++ 5 files changed, 219 insertions(+), 11 deletions(-) diff --git a/pulp_python/app/serializers.py b/pulp_python/app/serializers.py index 091a27a1..0174bbaf 100644 --- a/pulp_python/app/serializers.py +++ b/pulp_python/app/serializers.py @@ -22,6 +22,7 @@ ) from pulp_python.app.utils import ( DIST_EXTENSIONS, + artifact_to_metadata_artifact, artifact_to_python_content_data, get_project_metadata_from_file, parse_project_metadata, @@ -93,11 +94,35 @@ class Meta: model = python_models.PythonDistribution +class PythonSingleContentArtifactField(core_serializers.SingleContentArtifactField): + """ + Custom field with overridden get_attribute method. Meant to be used only in + PythonPackageContentSerializer to handle possible existence of metadata artifact. + """ + + def get_attribute(self, instance): + if instance._artifacts.count() == 0: + return None + elif instance._artifacts.count() == 1: + return instance._artifacts.all()[0] + else: + main_content_artifacts = instance.contentartifact_set.exclude( + relative_path__endswith=".metadata" + ) + if main_content_artifacts.exists(): + return main_content_artifacts.first().artifact + return instance._artifacts.all()[0] + + class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploadSerializer): """ A Serializer for PythonPackageContent. """ + artifact = PythonSingleContentArtifactField( + help_text=_("Artifact file representing the physical content"), + ) + # Core metadata # Version 1.0 author = serializers.CharField( @@ -386,8 +411,21 @@ def deferred_validate(self, data): if attestations := data.pop("attestations", None): data["provenance"] = self.handle_attestations(filename, data["sha256"], attestations) + # Create metadata artifact for wheel files + if filename.endswith(".whl"): + if metadata_artifact := artifact_to_metadata_artifact(filename, artifact): + data["metadata_artifact"] = metadata_artifact + data["metadata_sha256"] = metadata_artifact.sha256 + return data + def get_artifacts(self, validated_data): + artifacts = super().get_artifacts(validated_data) + if metadata_artifact := validated_data.pop("metadata_artifact", None): + relative_path = f"{validated_data['filename']}.metadata" + artifacts[relative_path] = metadata_artifact + return artifacts + def retrieve(self, validated_data): content = python_models.PythonPackageContent.objects.filter( sha256=validated_data["sha256"], _pulp_domain=get_domain() diff --git a/pulp_python/app/tasks/sync.py b/pulp_python/app/tasks/sync.py index d7058e8e..a7f9f516 100644 --- a/pulp_python/app/tasks/sync.py +++ b/pulp_python/app/tasks/sync.py @@ -229,11 +229,15 @@ async def create_content(self, pkg): create a Content Unit to put into the pipeline """ declared_contents = {} + page = await aget_remote_simple_page(pkg.name, self.remote) + upstream_pkgs = {pkg.filename: pkg for pkg in page.packages} + for version, dists in pkg.releases.items(): for package in dists: entry = parse_metadata(pkg.info, version, package) url = entry.pop("url") size = package["size"] or None + d_artifacts = [] artifact = Artifact(sha256=entry["sha256"], size=size) package = PythonPackageContent(**entry) @@ -245,11 +249,28 @@ async def create_content(self, pkg): remote=self.remote, deferred_download=self.deferred_download, ) - dc = DeclarativeContent(content=package, d_artifacts=[da]) + d_artifacts.append(da) + + if upstream_pkg := upstream_pkgs.get(entry["filename"]): + if upstream_pkg.has_metadata: + url = upstream_pkg.metadata_url + md_sha256 = upstream_pkg.metadata_digests.get("sha256") + artifact = Artifact(sha256=md_sha256) + + metadata_artifact = DeclarativeArtifact( + artifact=artifact, + url=url, + relative_path=f"{entry['filename']}.metadata", + remote=self.remote, + deferred_download=self.deferred_download, + ) + d_artifacts.append(metadata_artifact) + + dc = DeclarativeContent(content=package, d_artifacts=d_artifacts) declared_contents[entry["filename"]] = dc await self.python_stage.put(dc) - if pkg.releases and (page := await aget_remote_simple_page(pkg.name, self.remote)): + if pkg.releases and page: if self.remote.provenance: await self.sync_provenance(page, declared_contents) diff --git a/pulp_python/app/tasks/upload.py b/pulp_python/app/tasks/upload.py index dcd7aa72..bf98342d 100644 --- a/pulp_python/app/tasks/upload.py +++ b/pulp_python/app/tasks/upload.py @@ -15,7 +15,7 @@ Provenance, verify_provenance, ) -from pulp_python.app.utils import artifact_to_python_content_data +from pulp_python.app.utils import artifact_to_metadata_artifact, artifact_to_python_content_data def upload(artifact_sha256, filename, attestations=None, repository_pk=None): @@ -97,6 +97,11 @@ def create_content(artifact_sha256, filename, domain): def create(): content = PythonPackageContent.objects.create(**data) ContentArtifact.objects.create(artifact=artifact, content=content, relative_path=filename) + + if metadata_artifact := artifact_to_metadata_artifact(filename, artifact): + ContentArtifact.objects.create( + artifact=metadata_artifact, content=content, relative_path=f"{filename}.metadata" + ) return content new_content = create() diff --git a/pulp_python/app/utils.py b/pulp_python/app/utils.py index c41a393d..84b03fd4 100644 --- a/pulp_python/app/utils.py +++ b/pulp_python/app/utils.py @@ -1,4 +1,5 @@ import hashlib +import logging import pkginfo import re import shutil @@ -14,10 +15,13 @@ from packaging.requirements import Requirement from packaging.version import parse, InvalidVersion from pypi_simple import ACCEPT_JSON_PREFERRED, ProjectPage -from pulpcore.plugin.models import Remote +from pulpcore.plugin.models import Artifact, Remote from pulpcore.plugin.exceptions import TimeoutException +log = logging.getLogger(__name__) + + PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" """TODO This serial constant is temporary until Python repositories implements serials""" PYPI_SERIAL_CONSTANT = 1000000000 @@ -206,11 +210,11 @@ def get_project_metadata_from_file(filename): return metadata -def compute_metadata_sha256(filename: str) -> str | None: +def extract_wheel_metadata(filename: str) -> bytes | None: """ - Compute SHA256 hash of the metadata file from a Python package. + Extract the metadata file content from a wheel file. - Returns SHA256 hash or None if metadata cannot be extracted. + Returns the raw metadata content as bytes or None if metadata cannot be extracted. """ if not filename.endswith(".whl"): return None @@ -218,13 +222,22 @@ def compute_metadata_sha256(filename: str) -> str | None: with zipfile.ZipFile(filename, "r") as f: for file_path in f.namelist(): if file_path.endswith(".dist-info/METADATA"): - metadata_content = f.read(file_path) - return hashlib.sha256(metadata_content).hexdigest() - except (zipfile.BadZipFile, KeyError, OSError): - pass + return f.read(file_path) + except (zipfile.BadZipFile, KeyError, OSError) as e: + log.warning(f"Failed to extract metadata file from {filename}: {e}") return None +def compute_metadata_sha256(filename: str) -> str | None: + """ + Compute SHA256 hash of the metadata file from a Python package. + + Returns SHA256 hash or None if metadata cannot be extracted. + """ + metadata_content = extract_wheel_metadata(filename) + return hashlib.sha256(metadata_content).hexdigest() if metadata_content else None + + def artifact_to_python_content_data(filename, artifact, domain=None): """ Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent. @@ -233,6 +246,7 @@ def artifact_to_python_content_data(filename, artifact, domain=None): # because pkginfo validates that the filename has a valid extension before # reading it with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file: + artifact.file.seek(0) shutil.copyfileobj(artifact.file, temp_file) temp_file.flush() metadata = get_project_metadata_from_file(temp_file.name) @@ -245,6 +259,28 @@ def artifact_to_python_content_data(filename, artifact, domain=None): return data +def artifact_to_metadata_artifact(filename: str, artifact: Artifact) -> Artifact | None: + """ + Creates artifact for metadata from the provided wheel artifact. + """ + if not filename.endswith(".whl"): + return None + + with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file: + artifact.file.seek(0) + shutil.copyfileobj(artifact.file, temp_file) + temp_file.flush() + metadata_content = extract_wheel_metadata(temp_file.name) + if not metadata_content: + return None + with tempfile.NamedTemporaryFile(suffix=".metadata") as metadata_temp: + metadata_temp.write(metadata_content) + metadata_temp.flush() + metadata_artifact = Artifact.init_and_validate(metadata_temp.name) + metadata_artifact.save() + return metadata_artifact + + def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -> dict: """ Fetches metadata for a specific release from PyPI's JSON API. A release can contain @@ -408,6 +444,7 @@ def find_artifact(): _art = models.RemoteArtifact.objects.filter(content_artifact=content_artifact).first() return _art + # todo: fix .first() content_artifact = content.contentartifact_set.first() artifact = find_artifact() origin = settings.CONTENT_ORIGIN or settings.PYPI_API_HOSTNAME or "" diff --git a/pulp_python/tests/functional/api/test_pypi_apis.py b/pulp_python/tests/functional/api/test_pypi_apis.py index 1a5626d7..081f86a7 100644 --- a/pulp_python/tests/functional/api/test_pypi_apis.py +++ b/pulp_python/tests/functional/api/test_pypi_apis.py @@ -10,7 +10,10 @@ PYTHON_MD_PYPI_SUMMARY, PYTHON_EGG_FILENAME, PYTHON_EGG_SHA256, + PYTHON_WHEEL_FILENAME, PYTHON_WHEEL_SHA256, + PYTHON_WHEEL_URL, + PYTHON_XS_PROJECT_SPECIFIER, SHELF_PYTHON_JSON, ) @@ -137,6 +140,110 @@ def test_package_upload_simple( assert summary.added["python.python"]["count"] == 1 +# todo: tests + moving +# PythonPackageSingleArtifactContentUploadViewSet - create +def test_wheel_package_upload_with_metadata_1( + delete_orphans_pre, + pulp_content_url, + python_content_factory, + python_distribution_factory, + python_repo, +): + # pdb.set_trace() + python_content_factory( + repository=python_repo, relative_path=PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL + ) + distro = python_distribution_factory(repository=python_repo) + + # Test that metadata is accessible + relative_path = f"{distro.base_path}/{PYTHON_WHEEL_FILENAME}.metadata" + metadata_url = urljoin(pulp_content_url, relative_path) + metadata_response = requests.get(metadata_url) + assert metadata_response.status_code == 200 + assert len(metadata_response.content) > 0 + assert "Name: shelf-reader" in metadata_response.text + + +# PythonPackageSingleArtifactContentUploadViewSet - upload +def test_wheel_package_upload_with_metadata_2( + delete_orphans_pre, + download_python_file, + monitor_task, + pulp_content_url, + python_bindings, + python_distribution_factory, + python_repo, +): + python_file = download_python_file(PYTHON_WHEEL_FILENAME, PYTHON_WHEEL_URL) + content_body = {"file": python_file} + content = python_bindings.ContentPackagesApi.upload(**content_body) + + body = {"add_content_units": [content.pulp_href]} + monitor_task(python_bindings.RepositoriesPythonApi.modify(python_repo.pulp_href, body).task) + distro = python_distribution_factory(repository=python_repo) + + # Test that metadata is accessible + relative_path = f"{distro.base_path}/{PYTHON_WHEEL_FILENAME}.metadata" + metadata_url = urljoin(pulp_content_url, relative_path) + metadata_response = requests.get(metadata_url) + assert metadata_response.status_code == 200 + assert len(metadata_response.content) > 0 + assert "Name: shelf-reader" in metadata_response.text + + +# PythonRepositoryViewSet - sync +def test_wheel_package_upload_with_metadata_3( + delete_orphans_pre, + pulp_content_url, + python_distribution_factory, + python_remote_factory, + python_repo_with_sync, +): + remote = python_remote_factory(includes=PYTHON_XS_PROJECT_SPECIFIER) + repo = python_repo_with_sync(remote) + distro = python_distribution_factory(repository=repo) + + # Test that metadata is accessible + relative_path = f"{distro.base_path}/{PYTHON_WHEEL_FILENAME}.metadata" + metadata_url = urljoin(pulp_content_url, relative_path) + metadata_response = requests.get(metadata_url) + assert metadata_response.status_code == 200 + assert len(metadata_response.content) > 0 + assert "Name: shelf-reader" in metadata_response.text + + +# SimpleView - create +def test_wheel_package_upload_with_metadata_4( + delete_orphans_pre, + monitor_task, + pulp_content_url, + python_content_summary, + python_empty_repo_distro, + python_package_dist_directory, +): + repo, distro = python_empty_repo_distro() + url = urljoin(distro.base_url, "simple/") + dist_dir, egg_file, wheel_file = python_package_dist_directory + response = requests.post( + url, + data={"sha256_digest": PYTHON_WHEEL_SHA256}, + files={"content": open(wheel_file, "rb")}, + auth=("admin", "password"), + ) + assert response.status_code == 202 + monitor_task(response.json()["task"]) + summary = python_content_summary(repository=repo) + assert summary.added["python.python"]["count"] == 1 + + # Test that metadata is accessible + relative_path = f"{distro.base_path}/{PYTHON_WHEEL_FILENAME}.metadata" + metadata_url = urljoin(pulp_content_url, relative_path) + metadata_response = requests.get(metadata_url) + assert metadata_response.status_code == 200 + assert len(metadata_response.content) > 0 + assert "Name: shelf-reader" in metadata_response.text + + @pytest.mark.parallel def test_twine_upload( pulpcore_bindings,