Skip to content

Commit 8f4f288

Browse files
committed
Add more fields and validation to twine upload endpoint
1 parent 001147e commit 8f4f288

File tree

5 files changed

+145
-35
lines changed

5 files changed

+145
-35
lines changed

pulp_python/app/pypi/serializers.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from gettext import gettext as _
33

44
from rest_framework import serializers
5-
from pulp_python.app.utils import DIST_EXTENSIONS
5+
from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
66
from pulpcore.plugin.models import Artifact
77
from pulpcore.plugin.util import get_domain
88
from django.db.utils import IntegrityError
9+
from packaging.version import Version, InvalidVersion
910

1011
log = logging.getLogger(__name__)
1112

@@ -54,6 +55,22 @@ class PackageUploadSerializer(serializers.Serializer):
5455
min_length=64,
5556
max_length=64,
5657
)
58+
protocol_version = serializers.ChoiceField(
59+
help_text=_("Protocol version to use for the upload. Only version 1 is supported."),
60+
required=False,
61+
choices=(1,),
62+
default=1
63+
)
64+
filetype = serializers.ChoiceField(
65+
help_text=_("Type of artifact to upload."),
66+
required=False,
67+
choices=("bdist_wheel", "sdist"),
68+
)
69+
metadata_version = serializers.ChoiceField(
70+
help_text=_("Metadata version of the uploaded package."),
71+
required=False,
72+
choices=SUPPORTED_METADATA_VERSIONS,
73+
)
5774

5875
def validate(self, data):
5976
"""Validates the request."""
@@ -63,14 +80,26 @@ def validate(self, data):
6380
file = data.get("content")
6481
for ext, packagetype in DIST_EXTENSIONS.items():
6582
if file.name.endswith(ext):
83+
if filetype := data.get("filetype"):
84+
if filetype != packagetype:
85+
raise serializers.ValidationError(
86+
{
87+
"filetype": _(
88+
"filetype {} does not match found filetype {} for file {}"
89+
).format(filetype, packagetype, file.name)
90+
}
91+
)
6692
break
6793
else:
6894
raise serializers.ValidationError(
69-
_(
70-
"Extension on {} is not a valid python extension "
71-
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
72-
).format(file.name)
95+
{
96+
"content": _(
97+
"Extension on {} is not a valid python extension "
98+
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
99+
).format(file.name)
100+
}
73101
)
102+
74103
sha256 = data.get("sha256_digest")
75104
digests = {"sha256": sha256} if sha256 else None
76105
artifact = Artifact.init_and_validate(file, expected_digests=digests)

pulp_python/app/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
1818
"""TODO This serial constant is temporary until Python repositories implements serials"""
1919
PYPI_SERIAL_CONSTANT = 1000000000
20+
SUPPORTED_METADATA_VERSIONS = ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4")
2021

2122
SIMPLE_API_VERSION = "1.0"
2223

pulp_python/pytest_plugin.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
PYTHON_XS_PROJECT_SPECIFIER,
99
PYTHON_EGG_FILENAME,
1010
PYTHON_URL,
11+
PYTHON_EGG_URL,
12+
PYTHON_WHEEL_URL,
13+
PYTHON_WHEEL_FILENAME,
1114
)
1215

1316

@@ -183,6 +186,20 @@ def _gen_python_content(relative_path=PYTHON_EGG_FILENAME, url=None, **body):
183186
yield _gen_python_content
184187

185188

189+
@pytest.fixture
190+
def python_empty_repo_distro(python_repo_factory, python_distribution_factory):
191+
"""Returns an empty repo with and distribution serving it."""
192+
193+
def _generate_empty_repo_distro(repo_body=None, distro_body=None):
194+
repo_body = repo_body or {}
195+
distro_body = distro_body or {}
196+
repo = python_repo_factory(**repo_body)
197+
distro = python_distribution_factory(repository=repo, **distro_body)
198+
return repo, distro
199+
200+
yield _generate_empty_repo_distro
201+
202+
186203
# Utility fixtures
187204

188205

@@ -217,3 +234,16 @@ def _gen_summary(repository_version=None, repository=None, version=None):
217234
def get_href(item):
218235
"""Tries to get the href from the given item, whether it is a string or object."""
219236
return item if isinstance(item, str) else item.pulp_href
237+
238+
239+
@pytest.fixture(scope="session")
240+
def python_package_dist_directory(tmp_path_factory, http_get):
241+
"""Creates a temp dir to hold package distros for uploading."""
242+
dist_dir = tmp_path_factory.mktemp("dist")
243+
egg_file = dist_dir / PYTHON_EGG_FILENAME
244+
wheel_file = dist_dir / PYTHON_WHEEL_FILENAME
245+
with open(egg_file, "wb") as f:
246+
f.write(http_get(PYTHON_EGG_URL))
247+
with open(wheel_file, "wb") as f:
248+
f.write(http_get(PYTHON_WHEEL_URL))
249+
yield dist_dir, egg_file, wheel_file

pulp_python/tests/functional/api/test_pypi_apis.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
PYTHON_MD_PROJECT_SPECIFIER,
1212
PYTHON_MD_PYPI_SUMMARY,
1313
PYTHON_EGG_FILENAME,
14-
PYTHON_EGG_URL,
1514
PYTHON_EGG_SHA256,
16-
PYTHON_WHEEL_FILENAME,
17-
PYTHON_WHEEL_URL,
1815
PYTHON_WHEEL_SHA256,
1916
SHELF_PYTHON_JSON,
2017
)
@@ -26,20 +23,6 @@
2623
PYPI_SERIAL_CONSTANT = 1000000000
2724

2825

29-
@pytest.fixture
30-
def python_empty_repo_distro(python_repo_factory, python_distribution_factory):
31-
"""Returns an empty repo with and distribution serving it."""
32-
33-
def _generate_empty_repo_distro(repo_body=None, distro_body=None):
34-
repo_body = repo_body or {}
35-
distro_body = distro_body or {}
36-
repo = python_repo_factory(**repo_body)
37-
distro = python_distribution_factory(repository=repo, **distro_body)
38-
return repo, distro
39-
40-
yield _generate_empty_repo_distro
41-
42-
4326
@pytest.mark.parallel
4427
def test_empty_index(python_bindings, python_empty_repo_distro):
4528
"""Checks that summary stats are 0 when index is empty."""
@@ -80,19 +63,6 @@ def test_published_index(
8063
assert summary.to_dict() == PYTHON_MD_PYPI_SUMMARY
8164

8265

83-
@pytest.fixture(scope="module")
84-
def python_package_dist_directory(tmp_path_factory, http_get):
85-
"""Creates a temp dir to hold package distros for uploading."""
86-
dist_dir = tmp_path_factory.mktemp("dist")
87-
egg_file = dist_dir / PYTHON_EGG_FILENAME
88-
wheel_file = dist_dir / PYTHON_WHEEL_FILENAME
89-
with open(egg_file, "wb") as f:
90-
f.write(http_get(PYTHON_EGG_URL))
91-
with open(wheel_file, "wb") as f:
92-
f.write(http_get(PYTHON_WHEEL_URL))
93-
yield dist_dir, egg_file, wheel_file
94-
95-
9666
@pytest.mark.parallel
9767
def test_package_upload(
9868
python_content_summary, python_empty_repo_distro, python_package_dist_directory, monitor_task

pulp_python/tests/functional/api/test_upload.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import pytest
2+
import requests
23
from pulp_python.tests.functional.constants import (
34
PYTHON_EGG_FILENAME,
45
PYTHON_EGG_URL,
56
PYTHON_WHEEL_FILENAME,
67
PYTHON_WHEEL_URL,
8+
PYTHON_EGG_SHA256,
9+
PYTHON_WHEEL_SHA256,
710
)
11+
from urllib.parse import urljoin
812

913

1014
@pytest.mark.parametrize(
@@ -42,3 +46,79 @@ def test_synchronous_package_upload(
4246
with pytest.raises(python_bindings.ApiException) as ctx:
4347
python_bindings.ContentPackagesApi.upload(**content_body)
4448
assert ctx.value.status == 403
49+
50+
51+
@pytest.mark.parallel
52+
def test_legacy_upload_invalid_protocol_version(
53+
python_empty_repo_distro, python_package_dist_directory
54+
):
55+
_, egg_file, _ = python_package_dist_directory
56+
_, distro = python_empty_repo_distro()
57+
url = urljoin(distro.base_url, "legacy/")
58+
with open(egg_file, "rb") as f:
59+
response = requests.post(
60+
url,
61+
data={"sha256_digest": PYTHON_EGG_SHA256, "protocol_version": 2},
62+
files={"content": f},
63+
auth=("admin", "password"),
64+
)
65+
assert response.status_code == 400
66+
assert response.json()["protocol_version"] == ['"2" is not a valid choice.']
67+
68+
with open(egg_file, "rb") as f:
69+
response = requests.post(
70+
url,
71+
data={"sha256_digest": PYTHON_EGG_SHA256, "protocol_version": 0},
72+
files={"content": f},
73+
auth=("admin", "password"),
74+
)
75+
assert response.status_code == 400
76+
assert response.json()["protocol_version"] == ['"0" is not a valid choice.']
77+
78+
79+
@pytest.mark.parallel
80+
def test_legacy_upload_invalid_filetype(python_empty_repo_distro, python_package_dist_directory):
81+
_, egg_file, wheel_file = python_package_dist_directory
82+
_, distro = python_empty_repo_distro()
83+
url = urljoin(distro.base_url, "legacy/")
84+
with open(egg_file, "rb") as f:
85+
response = requests.post(
86+
url,
87+
data={"sha256_digest": PYTHON_EGG_SHA256, "filetype": "bdist_wheel"},
88+
files={"content": f},
89+
auth=("admin", "password"),
90+
)
91+
assert response.status_code == 400
92+
assert response.json()["filetype"] == [
93+
"filetype bdist_wheel does not match found filetype sdist for file shelf-reader-0.1.tar.gz"
94+
]
95+
96+
with open(wheel_file, "rb") as f:
97+
response = requests.post(
98+
url,
99+
data={"sha256_digest": PYTHON_WHEEL_SHA256, "filetype": "sdist"},
100+
files={"content": f},
101+
auth=("admin", "password"),
102+
)
103+
assert response.status_code == 400
104+
assert response.json()["filetype"] == [
105+
"filetype sdist does not match found filetype bdist_wheel for file shelf_reader-0.1-py2-none-any.whl" # noqa: E501
106+
]
107+
108+
109+
@pytest.mark.parallel
110+
def test_legacy_upload_invalid_metadata_version(
111+
python_empty_repo_distro, python_package_dist_directory
112+
):
113+
_, egg_file, _ = python_package_dist_directory
114+
_, distro = python_empty_repo_distro()
115+
url = urljoin(distro.base_url, "legacy/")
116+
with open(egg_file, "rb") as f:
117+
response = requests.post(
118+
url,
119+
data={"sha256_digest": PYTHON_EGG_SHA256, "metadata_version": "3.0"},
120+
files={"content": f},
121+
auth=("admin", "password"),
122+
)
123+
assert response.status_code == 400
124+
assert response.json()["metadata_version"] == ['"3.0" is not a valid choice.']

0 commit comments

Comments
 (0)