pulp-python 3.19.1__tar.gz → 3.20.1__tar.gz
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.
- {pulp_python-3.19.1 → pulp_python-3.20.1}/CHANGES.md +25 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/PKG-INFO +2 -2
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.20.1/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +18 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/models.py +2 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/pypi/views.py +92 -28
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/serializers.py +13 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/tasks/publish.py +3 -1
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/utils.py +87 -16
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_consume_content.py +1 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_domains.py +1 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_full_mirror.py +48 -4
- pulp_python-3.20.1/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +126 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/PKG-INFO +2 -2
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/SOURCES.txt +2 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/requires.txt +1 -1
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pyproject.toml +3 -3
- {pulp_python-3.19.1 → pulp_python-3.20.1}/COMMITMENT +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/COPYRIGHT +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/LICENSE +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/MANIFEST.in +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/README.md +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/functest_requirements.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/urls.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/viewsets.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/setup.cfg +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/test_requirements.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.1}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,31 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.20.1 (2025-11-18) {: #3.20.1 }
|
|
12
|
+
|
|
13
|
+
#### Bugfixes {: #3.20.1-bugfix }
|
|
14
|
+
|
|
15
|
+
- Fixed pull-through caching not checking the repository if package was not present on remote.
|
|
16
|
+
[#1004](https://github.com/pulp/pulp_python/issues/1004)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3.20.0 (2025-11-07) {: #3.20.0 }
|
|
21
|
+
|
|
22
|
+
#### Features {: #3.20.0-feature }
|
|
23
|
+
|
|
24
|
+
- Added JSON-based Simple API (PEP 691).
|
|
25
|
+
[#625](https://github.com/pulp/pulp_python/issues/625)
|
|
26
|
+
- Updated tasks to always return JSON-serializable value.
|
|
27
|
+
[#972](https://github.com/pulp/pulp_python/issues/972)
|
|
28
|
+
|
|
29
|
+
#### Bugfixes {: #3.20.0-bugfix }
|
|
30
|
+
|
|
31
|
+
- Fixed publication error when package's dists contain differing versions of its name.
|
|
32
|
+
[#907](https://github.com/pulp/pulp_python/issues/907)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
11
36
|
## 3.19.1 (2025-09-14) {: #3.19.1 }
|
|
12
37
|
|
|
13
38
|
#### Bugfixes {: #3.19.1-bugfix }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.20.1
|
|
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.100,>=3.
|
|
23
|
+
Requires-Dist: pulpcore<3.100,>=3.85.0
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
25
|
Requires-Dist: bandersnatch<6.6,>=6.3.0
|
|
26
26
|
Requires-Dist: pypi-simple<2.0,>=1.5.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.25 on 2025-11-04 07:34
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("python", "0015_alter_pythonpackagecontent_options"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name="pythonpackagecontent",
|
|
15
|
+
name="metadata_sha256",
|
|
16
|
+
field=models.CharField(max_length=64, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -192,6 +192,8 @@ class PythonPackageContent(Content):
|
|
|
192
192
|
packagetype = models.TextField(choices=PACKAGE_TYPES)
|
|
193
193
|
python_version = models.TextField()
|
|
194
194
|
sha256 = models.CharField(db_index=True, max_length=64)
|
|
195
|
+
metadata_sha256 = models.CharField(max_length=64, null=True)
|
|
196
|
+
# yanked and yanked_reason are not implemented because they are mutable
|
|
195
197
|
|
|
196
198
|
# From pulpcore
|
|
197
199
|
PROTECTED_FROM_RECLAIM = False
|
|
@@ -3,7 +3,9 @@ import logging
|
|
|
3
3
|
|
|
4
4
|
from aiohttp.client_exceptions import ClientError
|
|
5
5
|
from rest_framework.viewsets import ViewSet
|
|
6
|
+
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, TemplateHTMLRenderer
|
|
6
7
|
from rest_framework.response import Response
|
|
8
|
+
from rest_framework.exceptions import NotAcceptable
|
|
7
9
|
from django.core.exceptions import ObjectDoesNotExist
|
|
8
10
|
from django.shortcuts import redirect
|
|
9
11
|
from datetime import datetime, timezone, timedelta
|
|
@@ -13,6 +15,7 @@ from django.db import transaction
|
|
|
13
15
|
from django.db.utils import DatabaseError
|
|
14
16
|
from django.http.response import (
|
|
15
17
|
Http404,
|
|
18
|
+
HttpResponseNotFound,
|
|
16
19
|
HttpResponseForbidden,
|
|
17
20
|
HttpResponseBadRequest,
|
|
18
21
|
StreamingHttpResponse,
|
|
@@ -43,7 +46,9 @@ from pulp_python.app.pypi.serializers import (
|
|
|
43
46
|
)
|
|
44
47
|
from pulp_python.app.utils import (
|
|
45
48
|
write_simple_index,
|
|
49
|
+
write_simple_index_json,
|
|
46
50
|
write_simple_detail,
|
|
51
|
+
write_simple_detail_json,
|
|
47
52
|
python_content_to_json,
|
|
48
53
|
PYPI_LAST_SERIAL,
|
|
49
54
|
PYPI_SERIAL_CONSTANT,
|
|
@@ -57,6 +62,17 @@ log = logging.getLogger(__name__)
|
|
|
57
62
|
ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
|
|
58
63
|
BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX)
|
|
59
64
|
|
|
65
|
+
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
66
|
+
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PyPISimpleHTMLRenderer(TemplateHTMLRenderer):
|
|
70
|
+
media_type = PYPI_SIMPLE_V1_HTML
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PyPISimpleJSONRenderer(JSONRenderer):
|
|
74
|
+
media_type = PYPI_SIMPLE_V1_JSON
|
|
75
|
+
|
|
60
76
|
|
|
61
77
|
class PyPIMixin:
|
|
62
78
|
"""Mixin to get index specific info."""
|
|
@@ -235,6 +251,25 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
235
251
|
],
|
|
236
252
|
}
|
|
237
253
|
|
|
254
|
+
def perform_content_negotiation(self, request, force=False):
|
|
255
|
+
"""
|
|
256
|
+
Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
return super().perform_content_negotiation(request, force)
|
|
260
|
+
except NotAcceptable:
|
|
261
|
+
return TemplateHTMLRenderer(), TemplateHTMLRenderer.media_type # text/html
|
|
262
|
+
|
|
263
|
+
def get_renderers(self):
|
|
264
|
+
"""
|
|
265
|
+
Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones.
|
|
266
|
+
"""
|
|
267
|
+
if self.action in ["list", "retrieve"]:
|
|
268
|
+
# Ordered by priority if multiple content types are present
|
|
269
|
+
return [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer(), PyPISimpleJSONRenderer()]
|
|
270
|
+
else:
|
|
271
|
+
return [JSONRenderer(), BrowsableAPIRenderer()]
|
|
272
|
+
|
|
238
273
|
@extend_schema(summary="Get index simple page")
|
|
239
274
|
def list(self, request, path):
|
|
240
275
|
"""Gets the simple api html page for the index."""
|
|
@@ -242,7 +277,16 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
242
277
|
if self.should_redirect(repo_version=repo_version):
|
|
243
278
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/"))
|
|
244
279
|
names = content.order_by("name").values_list("name", flat=True).distinct().iterator()
|
|
245
|
-
|
|
280
|
+
media_type = request.accepted_renderer.media_type
|
|
281
|
+
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
282
|
+
|
|
283
|
+
if media_type == PYPI_SIMPLE_V1_JSON:
|
|
284
|
+
index_data = write_simple_index_json(names)
|
|
285
|
+
return Response(index_data, headers=headers)
|
|
286
|
+
else:
|
|
287
|
+
index_data = write_simple_index(names, streamed=True)
|
|
288
|
+
kwargs = {"content_type": media_type, "headers": headers}
|
|
289
|
+
return StreamingHttpResponse(index_data, **kwargs)
|
|
246
290
|
|
|
247
291
|
def pull_through_package_simple(self, package, path, remote):
|
|
248
292
|
"""Gets the package's simple page from remote."""
|
|
@@ -252,11 +296,17 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
252
296
|
stripped_url = urlunsplit(chain(parsed[:3], ("", "")))
|
|
253
297
|
redirect_path = f"{path}/{release_package.filename}?redirect={stripped_url}"
|
|
254
298
|
d_url = urljoin(self.base_content_url, redirect_path)
|
|
255
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
"filename": release_package.filename,
|
|
301
|
+
"url": d_url,
|
|
302
|
+
"sha256": release_package.digests.get("sha256", ""),
|
|
303
|
+
"requires_python": release_package.requires_python,
|
|
304
|
+
"metadata_sha256": (release_package.metadata_digests or {}).get("sha256"),
|
|
305
|
+
}
|
|
256
306
|
|
|
257
307
|
rfilter = get_remote_package_filter(remote)
|
|
258
308
|
if not rfilter.filter_project(package):
|
|
259
|
-
|
|
309
|
+
return {}
|
|
260
310
|
|
|
261
311
|
url = remote.get_remote_artifact_url(f"simple/{package}/")
|
|
262
312
|
remote.headers = remote.headers or []
|
|
@@ -264,44 +314,58 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
264
314
|
downloader = remote.get_downloader(url=url, max_retries=1)
|
|
265
315
|
try:
|
|
266
316
|
d = downloader.fetch()
|
|
267
|
-
except ClientError:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return HttpResponse(f"{remote.url} timed out while fetching {package}.", status=504)
|
|
317
|
+
except (ClientError, TimeoutException):
|
|
318
|
+
log.info(f"Failed to fetch {package} simple page from {remote.url}")
|
|
319
|
+
return {}
|
|
271
320
|
|
|
272
|
-
if d.headers["content-type"] ==
|
|
321
|
+
if d.headers["content-type"] == PYPI_SIMPLE_V1_JSON:
|
|
273
322
|
page = ProjectPage.from_json_data(json.load(open(d.path, "rb")), base_url=url)
|
|
274
323
|
else:
|
|
275
324
|
page = ProjectPage.from_html(package, open(d.path, "rb").read(), base_url=url)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
325
|
+
return {
|
|
326
|
+
p.filename: parse_package(p)
|
|
327
|
+
for p in page.packages
|
|
328
|
+
if rfilter.filter_release(package, p.version)
|
|
329
|
+
}
|
|
280
330
|
|
|
281
331
|
@extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
|
|
282
332
|
def retrieve(self, request, path, package):
|
|
283
|
-
"""Retrieves the simple api html page for a package."""
|
|
333
|
+
"""Retrieves the simple api html/json page for a package."""
|
|
334
|
+
media_type = request.accepted_renderer.media_type
|
|
335
|
+
|
|
284
336
|
repo_ver, content = self.get_rvc()
|
|
285
337
|
# Should I redirect if the normalized name is different?
|
|
286
338
|
normalized = canonicalize_name(package)
|
|
339
|
+
releases = {}
|
|
287
340
|
if self.distribution.remote:
|
|
288
|
-
|
|
289
|
-
|
|
341
|
+
releases = self.pull_through_package_simple(normalized, path, self.distribution.remote)
|
|
342
|
+
elif self.should_redirect(repo_version=repo_ver):
|
|
290
343
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
|
|
291
|
-
|
|
292
|
-
content.filter(name__normalize=normalized)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
344
|
+
if content:
|
|
345
|
+
packages = content.filter(name__normalize=normalized).values(
|
|
346
|
+
"filename", "sha256", "metadata_sha256", "requires_python"
|
|
347
|
+
)
|
|
348
|
+
local_releases = {
|
|
349
|
+
p["filename"]: {
|
|
350
|
+
**p,
|
|
351
|
+
"url": urljoin(self.base_content_url, f"{path}/{p['filename']}"),
|
|
352
|
+
}
|
|
353
|
+
for p in packages
|
|
354
|
+
}
|
|
355
|
+
releases.update(local_releases)
|
|
356
|
+
if not releases:
|
|
357
|
+
return HttpResponseNotFound(f"{normalized} does not exist.")
|
|
358
|
+
|
|
359
|
+
media_type = request.accepted_renderer.media_type
|
|
360
|
+
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
361
|
+
|
|
362
|
+
if media_type == PYPI_SIMPLE_V1_JSON:
|
|
363
|
+
detail_data = write_simple_detail_json(normalized, releases.values())
|
|
364
|
+
return Response(detail_data, headers=headers)
|
|
300
365
|
else:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return StreamingHttpResponse(write_simple_detail(name, releases, streamed=True))
|
|
366
|
+
detail_data = write_simple_detail(normalized, releases.values())
|
|
367
|
+
kwargs = {"content_type": media_type, "headers": headers}
|
|
368
|
+
return HttpResponse(detail_data, **kwargs)
|
|
305
369
|
|
|
306
370
|
@extend_schema(
|
|
307
371
|
request=PackageUploadSerializer,
|
|
@@ -281,6 +281,11 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
281
281
|
default="",
|
|
282
282
|
help_text=_("The SHA256 digest of this package."),
|
|
283
283
|
)
|
|
284
|
+
metadata_sha256 = serializers.CharField(
|
|
285
|
+
required=False,
|
|
286
|
+
allow_null=True,
|
|
287
|
+
help_text=_("The SHA256 digest of the package's METADATA file."),
|
|
288
|
+
)
|
|
284
289
|
|
|
285
290
|
def deferred_validate(self, data):
|
|
286
291
|
"""
|
|
@@ -364,6 +369,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
364
369
|
"packagetype",
|
|
365
370
|
"python_version",
|
|
366
371
|
"sha256",
|
|
372
|
+
"metadata_sha256",
|
|
367
373
|
)
|
|
368
374
|
model = python_models.PythonPackageContent
|
|
369
375
|
|
|
@@ -457,6 +463,13 @@ class MultipleChoiceArrayField(serializers.MultipleChoiceField):
|
|
|
457
463
|
"""Converts set to list."""
|
|
458
464
|
return list(super().to_internal_value(data))
|
|
459
465
|
|
|
466
|
+
def to_representation(self, value):
|
|
467
|
+
"""Converts set to list for JSON serialization."""
|
|
468
|
+
result = super().to_representation(value)
|
|
469
|
+
if isinstance(result, set):
|
|
470
|
+
result = list(result)
|
|
471
|
+
return result
|
|
472
|
+
|
|
460
473
|
|
|
461
474
|
class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
462
475
|
"""
|
|
@@ -9,6 +9,7 @@ from pulpcore.plugin import models
|
|
|
9
9
|
from pulpcore.plugin.util import get_domain
|
|
10
10
|
|
|
11
11
|
from pulp_python.app import models as python_models
|
|
12
|
+
from pulp_python.app.serializers import PythonPublicationSerializer
|
|
12
13
|
from pulp_python.app.utils import write_simple_index, write_simple_detail
|
|
13
14
|
|
|
14
15
|
|
|
@@ -36,6 +37,7 @@ def publish(repository_version_pk):
|
|
|
36
37
|
write_simple_api(pub)
|
|
37
38
|
|
|
38
39
|
log.info(_("Publication: {pk} created").format(pk=pub.pk))
|
|
40
|
+
pub = PythonPublicationSerializer(instance=pub, context={"request": None}).data
|
|
39
41
|
return pub
|
|
40
42
|
|
|
41
43
|
|
|
@@ -99,7 +101,7 @@ def write_simple_api(publication):
|
|
|
99
101
|
relative_path = release["filename"]
|
|
100
102
|
path = f"../../{relative_path}"
|
|
101
103
|
checksum = release["sha256"]
|
|
102
|
-
package_releases.append(
|
|
104
|
+
package_releases.append({"filename": relative_path, "url": path, "sha256": checksum})
|
|
103
105
|
# Write the final project's page
|
|
104
106
|
write_project_page(
|
|
105
107
|
name=canonicalize_name(current_name),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import pkginfo
|
|
2
3
|
import re
|
|
3
4
|
import shutil
|
|
4
5
|
import tempfile
|
|
6
|
+
import zipfile
|
|
5
7
|
import json
|
|
6
8
|
from collections import defaultdict
|
|
7
9
|
from django.conf import settings
|
|
@@ -16,15 +18,17 @@ PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
|
|
|
16
18
|
"""TODO This serial constant is temporary until Python repositories implements serials"""
|
|
17
19
|
PYPI_SERIAL_CONSTANT = 1000000000
|
|
18
20
|
|
|
21
|
+
SIMPLE_API_VERSION = "1.0"
|
|
22
|
+
|
|
19
23
|
simple_index_template = """<!DOCTYPE html>
|
|
20
24
|
<html>
|
|
21
25
|
<head>
|
|
22
26
|
<title>Simple Index</title>
|
|
23
|
-
<meta name="
|
|
27
|
+
<meta name="pypi:repository-version" content="{{ SIMPLE_API_VERSION }}">
|
|
24
28
|
</head>
|
|
25
29
|
<body>
|
|
26
30
|
{% for name, canonical_name in projects %}
|
|
27
|
-
|
|
31
|
+
<a href="{{ canonical_name }}/">{{ name }}</a><br/>
|
|
28
32
|
{% endfor %}
|
|
29
33
|
</body>
|
|
30
34
|
</html>
|
|
@@ -32,16 +36,16 @@ simple_index_template = """<!DOCTYPE html>
|
|
|
32
36
|
|
|
33
37
|
simple_detail_template = """<!DOCTYPE html>
|
|
34
38
|
<html>
|
|
35
|
-
<head>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
</head>
|
|
39
|
-
<body>
|
|
39
|
+
<head>
|
|
40
|
+
<title>Links for {{ project_name }}</title>
|
|
41
|
+
<meta name="pypi:repository-version" content="{{ SIMPLE_API_VERSION }}">
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
40
44
|
<h1>Links for {{ project_name }}</h1>
|
|
41
|
-
{% for
|
|
42
|
-
|
|
45
|
+
{% for pkg in project_packages %}
|
|
46
|
+
<a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal">{{ pkg.filename }}</a><br/>
|
|
43
47
|
{% endfor %}
|
|
44
|
-
</body>
|
|
48
|
+
</body>
|
|
45
49
|
</html>
|
|
46
50
|
"""
|
|
47
51
|
|
|
@@ -128,6 +132,7 @@ def parse_project_metadata(project):
|
|
|
128
132
|
# Release metadata
|
|
129
133
|
"packagetype": project.get("packagetype") or "",
|
|
130
134
|
"python_version": project.get("python_version") or "",
|
|
135
|
+
"metadata_sha256": project.get("metadata_sha256"),
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
|
|
@@ -154,10 +159,8 @@ def parse_metadata(project, version, distribution):
|
|
|
154
159
|
package["version"] = version
|
|
155
160
|
package["url"] = distribution.get("url") or ""
|
|
156
161
|
package["sha256"] = distribution.get("digests", {}).get("sha256") or ""
|
|
157
|
-
package["python_version"] = distribution.get("python_version") or
|
|
158
|
-
package["requires_python"] = distribution.get("requires_python") or
|
|
159
|
-
"requires_python"
|
|
160
|
-
) # noqa: E501
|
|
162
|
+
package["python_version"] = distribution.get("python_version") or ""
|
|
163
|
+
package["requires_python"] = distribution.get("requires_python") or ""
|
|
161
164
|
|
|
162
165
|
return package
|
|
163
166
|
|
|
@@ -175,6 +178,7 @@ def get_project_metadata_from_file(filename):
|
|
|
175
178
|
packagetype = DIST_EXTENSIONS[extensions[pkg_type_index]]
|
|
176
179
|
|
|
177
180
|
metadata = DIST_TYPES[packagetype](filename)
|
|
181
|
+
metadata.metadata_sha256 = compute_metadata_sha256(filename)
|
|
178
182
|
metadata.packagetype = packagetype
|
|
179
183
|
if packagetype == "sdist":
|
|
180
184
|
metadata.python_version = "source"
|
|
@@ -187,6 +191,25 @@ def get_project_metadata_from_file(filename):
|
|
|
187
191
|
return metadata
|
|
188
192
|
|
|
189
193
|
|
|
194
|
+
def compute_metadata_sha256(filename: str) -> str | None:
|
|
195
|
+
"""
|
|
196
|
+
Compute SHA256 hash of the metadata file from a Python package.
|
|
197
|
+
|
|
198
|
+
Returns SHA256 hash or None if metadata cannot be extracted.
|
|
199
|
+
"""
|
|
200
|
+
if not filename.endswith(".whl"):
|
|
201
|
+
return None
|
|
202
|
+
try:
|
|
203
|
+
with zipfile.ZipFile(filename, "r") as f:
|
|
204
|
+
for file_path in f.namelist():
|
|
205
|
+
if file_path.endswith(".dist-info/METADATA"):
|
|
206
|
+
metadata_content = f.read(file_path)
|
|
207
|
+
return hashlib.sha256(metadata_content).hexdigest()
|
|
208
|
+
except (zipfile.BadZipFile, KeyError, OSError):
|
|
209
|
+
pass
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
190
213
|
def artifact_to_python_content_data(filename, artifact, domain=None):
|
|
191
214
|
"""
|
|
192
215
|
Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
|
|
@@ -403,17 +426,65 @@ def python_content_to_download_info(content, base_path, domain=None):
|
|
|
403
426
|
def write_simple_index(project_names, streamed=False):
|
|
404
427
|
"""Writes the simple index."""
|
|
405
428
|
simple = Template(simple_index_template)
|
|
406
|
-
context = {
|
|
429
|
+
context = {
|
|
430
|
+
"SIMPLE_API_VERSION": SIMPLE_API_VERSION,
|
|
431
|
+
"projects": ((x, canonicalize_name(x)) for x in project_names),
|
|
432
|
+
}
|
|
407
433
|
return simple.stream(**context) if streamed else simple.render(**context)
|
|
408
434
|
|
|
409
435
|
|
|
410
436
|
def write_simple_detail(project_name, project_packages, streamed=False):
|
|
411
437
|
"""Writes the simple detail page of a package."""
|
|
412
438
|
detail = Template(simple_detail_template)
|
|
413
|
-
context = {
|
|
439
|
+
context = {
|
|
440
|
+
"SIMPLE_API_VERSION": SIMPLE_API_VERSION,
|
|
441
|
+
"project_name": project_name,
|
|
442
|
+
"project_packages": project_packages,
|
|
443
|
+
}
|
|
414
444
|
return detail.stream(**context) if streamed else detail.render(**context)
|
|
415
445
|
|
|
416
446
|
|
|
447
|
+
def write_simple_index_json(project_names):
|
|
448
|
+
"""Writes the simple index in JSON format."""
|
|
449
|
+
return {
|
|
450
|
+
"meta": {"api-version": SIMPLE_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT},
|
|
451
|
+
"projects": [
|
|
452
|
+
{"name": name, "_last-serial": PYPI_SERIAL_CONSTANT} for name in project_names
|
|
453
|
+
],
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def write_simple_detail_json(project_name, project_packages):
|
|
458
|
+
"""Writes the simple detail page in JSON format."""
|
|
459
|
+
return {
|
|
460
|
+
"meta": {"api-version": SIMPLE_API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT},
|
|
461
|
+
"name": canonicalize_name(project_name),
|
|
462
|
+
"files": [
|
|
463
|
+
{
|
|
464
|
+
# v1.0, PEP 691
|
|
465
|
+
"filename": package["filename"],
|
|
466
|
+
"url": package["url"],
|
|
467
|
+
"hashes": {"sha256": package["sha256"]},
|
|
468
|
+
"requires-python": package["requires_python"] or None,
|
|
469
|
+
# data-dist-info-metadata is deprecated alias for core-metadata
|
|
470
|
+
"data-dist-info-metadata": (
|
|
471
|
+
{"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
|
|
472
|
+
),
|
|
473
|
+
# yanked and yanked_reason are not implemented because they are mutable
|
|
474
|
+
# TODO in the future:
|
|
475
|
+
# size, upload-time (v1.1, PEP 700)
|
|
476
|
+
# core-metadata (PEP 7.14)
|
|
477
|
+
# provenance (v1.3, PEP 740)
|
|
478
|
+
}
|
|
479
|
+
for package in project_packages
|
|
480
|
+
],
|
|
481
|
+
# TODO in the future:
|
|
482
|
+
# versions (v1.1, PEP 700)
|
|
483
|
+
# alternate-locations (v1.2, PEP 708)
|
|
484
|
+
# project-status (v1.4, PEP 792 - pypi and docs differ)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
417
488
|
class PackageIncludeFilter:
|
|
418
489
|
"""A special class to help filter Package's based on a remote's include/exclude"""
|
|
419
490
|
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_full_mirror.py
RENAMED
|
@@ -58,6 +58,24 @@ def test_pull_through_simple(python_remote_factory, python_distribution_factory,
|
|
|
58
58
|
assert PYTHON_XS_FIXTURE_CHECKSUMS[package.filename] == package.digests["sha256"]
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
@pytest.mark.parallel
|
|
62
|
+
@pytest.mark.parametrize("media_type", ["application/vnd.pypi.simple.v1+json", "text/html"])
|
|
63
|
+
def test_pull_through_simple_media_types(
|
|
64
|
+
media_type, python_remote_factory, python_distribution_factory
|
|
65
|
+
):
|
|
66
|
+
"""Tests pull-through with different media types (JSON and HTML)."""
|
|
67
|
+
remote = python_remote_factory(url=PYPI_URL, includes=["shelf-reader"])
|
|
68
|
+
distro = python_distribution_factory(remote=remote.pulp_href)
|
|
69
|
+
|
|
70
|
+
url = f"{distro.base_url}simple/shelf-reader/"
|
|
71
|
+
headers = {"Accept": media_type}
|
|
72
|
+
response = requests.get(url, headers=headers)
|
|
73
|
+
|
|
74
|
+
assert response.status_code == 200
|
|
75
|
+
assert media_type in response.headers["Content-Type"]
|
|
76
|
+
assert "X-PyPI-Last-Serial" in response.headers
|
|
77
|
+
|
|
78
|
+
|
|
61
79
|
@pytest.mark.parallel
|
|
62
80
|
def test_pull_through_filter(python_remote_factory, python_distribution_factory):
|
|
63
81
|
"""Tests that pull-through respects the includes/excludes filter on the remote."""
|
|
@@ -66,7 +84,7 @@ def test_pull_through_filter(python_remote_factory, python_distribution_factory)
|
|
|
66
84
|
|
|
67
85
|
r = requests.get(f"{distro.base_url}simple/pulpcore/")
|
|
68
86
|
assert r.status_code == 404
|
|
69
|
-
assert r.
|
|
87
|
+
assert r.text == "pulpcore does not exist."
|
|
70
88
|
|
|
71
89
|
r = requests.get(f"{distro.base_url}simple/shelf-reader/")
|
|
72
90
|
assert r.status_code == 200
|
|
@@ -86,11 +104,11 @@ def test_pull_through_filter(python_remote_factory, python_distribution_factory)
|
|
|
86
104
|
|
|
87
105
|
r = requests.get(f"{distro.base_url}simple/django/")
|
|
88
106
|
assert r.status_code == 404
|
|
89
|
-
assert r.
|
|
107
|
+
assert r.text == "django does not exist."
|
|
90
108
|
|
|
91
109
|
r = requests.get(f"{distro.base_url}simple/pulpcore/")
|
|
92
|
-
assert r.status_code ==
|
|
93
|
-
assert r.text ==
|
|
110
|
+
assert r.status_code == 404
|
|
111
|
+
assert r.text == "pulpcore does not exist."
|
|
94
112
|
|
|
95
113
|
r = requests.get(f"{distro.base_url}simple/shelf-reader/")
|
|
96
114
|
assert r.status_code == 200
|
|
@@ -138,3 +156,29 @@ def test_pull_through_with_repo(
|
|
|
138
156
|
assert r.status_code == 200
|
|
139
157
|
tasks = pulpcore_bindings.TasksApi.list(reserved_resources=repo.prn)
|
|
140
158
|
assert tasks.count == 3
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parallel
|
|
162
|
+
def test_pull_through_local_only(
|
|
163
|
+
python_remote_factory, python_distribution_factory, python_repo_with_sync
|
|
164
|
+
):
|
|
165
|
+
"""Tests that pull-through checks the repository if the package is not present on the remote."""
|
|
166
|
+
remote = python_remote_factory(url=PYPI_URL, includes=["pulpcore"])
|
|
167
|
+
repo = python_repo_with_sync(remote=remote)
|
|
168
|
+
remote2 = python_remote_factory(includes=[]) # Fixtures does not have pulpcore
|
|
169
|
+
distro = python_distribution_factory(repository=repo.pulp_href, remote=remote2.pulp_href)
|
|
170
|
+
|
|
171
|
+
url = f"{distro.base_url}simple/pulpcore/"
|
|
172
|
+
r = requests.get(url)
|
|
173
|
+
assert r.status_code == 200
|
|
174
|
+
assert "?redirect=" not in r.text
|
|
175
|
+
|
|
176
|
+
url = f"{distro.base_url}simple/shelf-reader/"
|
|
177
|
+
r = requests.get(url)
|
|
178
|
+
assert r.status_code == 200
|
|
179
|
+
assert "?redirect=" in r.text
|
|
180
|
+
|
|
181
|
+
url = f"{distro.base_url}simple/pulp_python/"
|
|
182
|
+
r = requests.get(url)
|
|
183
|
+
assert r.status_code == 404
|
|
184
|
+
assert r.text == "pulp-python does not exist."
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from urllib.parse import urljoin
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from pulp_python.tests.functional.constants import (
|
|
7
|
+
PYTHON_EGG_FILENAME,
|
|
8
|
+
PYTHON_EGG_URL,
|
|
9
|
+
PYTHON_SM_PROJECT_SPECIFIER,
|
|
10
|
+
PYTHON_WHEEL_FILENAME,
|
|
11
|
+
PYTHON_WHEEL_URL,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
API_VERSION = "1.0"
|
|
15
|
+
PYPI_SERIAL_CONSTANT = 1000000000
|
|
16
|
+
|
|
17
|
+
PYPI_TEXT_HTML = "text/html"
|
|
18
|
+
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
19
|
+
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parallel
|
|
23
|
+
def test_simple_json_index_api(
|
|
24
|
+
python_remote_factory, python_repo_with_sync, python_distribution_factory
|
|
25
|
+
):
|
|
26
|
+
remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
|
|
27
|
+
repo = python_repo_with_sync(remote)
|
|
28
|
+
distro = python_distribution_factory(repository=repo)
|
|
29
|
+
|
|
30
|
+
url = urljoin(distro.base_url, "simple/")
|
|
31
|
+
headers = {"Accept": PYPI_SIMPLE_V1_JSON}
|
|
32
|
+
|
|
33
|
+
response = requests.get(url, headers=headers)
|
|
34
|
+
assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_JSON
|
|
35
|
+
assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
|
|
36
|
+
|
|
37
|
+
data = response.json()
|
|
38
|
+
assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}
|
|
39
|
+
assert data["projects"]
|
|
40
|
+
for project in data["projects"]:
|
|
41
|
+
for i in ["_last-serial", "name"]:
|
|
42
|
+
assert i in project
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_simple_json_detail_api(
|
|
46
|
+
delete_orphans_pre,
|
|
47
|
+
monitor_task,
|
|
48
|
+
python_bindings,
|
|
49
|
+
python_content_factory,
|
|
50
|
+
python_distribution_factory,
|
|
51
|
+
python_repo_factory,
|
|
52
|
+
):
|
|
53
|
+
content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
|
|
54
|
+
content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
|
|
55
|
+
body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
|
|
56
|
+
|
|
57
|
+
repo = python_repo_factory()
|
|
58
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
59
|
+
distro = python_distribution_factory(repository=repo)
|
|
60
|
+
|
|
61
|
+
url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
|
|
62
|
+
headers = {"Accept": PYPI_SIMPLE_V1_JSON}
|
|
63
|
+
|
|
64
|
+
response = requests.get(url, headers=headers)
|
|
65
|
+
assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_JSON
|
|
66
|
+
assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
|
|
67
|
+
|
|
68
|
+
data = response.json()
|
|
69
|
+
assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}
|
|
70
|
+
assert data["name"] == "shelf-reader"
|
|
71
|
+
assert data["files"]
|
|
72
|
+
|
|
73
|
+
# Check data of a wheel
|
|
74
|
+
file_whl = next(
|
|
75
|
+
(i for i in data["files"] if i["filename"] == "shelf_reader-0.1-py2-none-any.whl"), None
|
|
76
|
+
)
|
|
77
|
+
assert file_whl is not None, "wheel file not found"
|
|
78
|
+
assert file_whl["url"]
|
|
79
|
+
assert file_whl["hashes"] == {
|
|
80
|
+
"sha256": "2eceb1643c10c5e4a65970baf63bde43b79cbdac7de81dae853ce47ab05197e9"
|
|
81
|
+
}
|
|
82
|
+
assert file_whl["requires-python"] is None
|
|
83
|
+
assert file_whl["data-dist-info-metadata"] == {
|
|
84
|
+
"sha256": "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Check data of a tarball
|
|
88
|
+
file_tar = next((i for i in data["files"] if i["filename"] == "shelf-reader-0.1.tar.gz"), None)
|
|
89
|
+
assert file_tar is not None, "tar file not found"
|
|
90
|
+
assert file_tar["url"]
|
|
91
|
+
assert file_tar["hashes"] == {
|
|
92
|
+
"sha256": "04cfd8bb4f843e35d51bfdef2035109bdea831b55a57c3e6a154d14be116398c"
|
|
93
|
+
}
|
|
94
|
+
assert file_tar["requires-python"] is None
|
|
95
|
+
assert file_tar["data-dist-info-metadata"] is False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parallel
|
|
99
|
+
@pytest.mark.parametrize(
|
|
100
|
+
"header, result",
|
|
101
|
+
[
|
|
102
|
+
(PYPI_TEXT_HTML, PYPI_TEXT_HTML),
|
|
103
|
+
(PYPI_SIMPLE_V1_HTML, PYPI_SIMPLE_V1_HTML),
|
|
104
|
+
(PYPI_SIMPLE_V1_JSON, PYPI_SIMPLE_V1_JSON),
|
|
105
|
+
# Follows defined ordering (html, pypi html, pypi json)
|
|
106
|
+
(f"{PYPI_SIMPLE_V1_JSON}, {PYPI_SIMPLE_V1_HTML}", PYPI_SIMPLE_V1_HTML),
|
|
107
|
+
# Everything else should be html
|
|
108
|
+
("", PYPI_TEXT_HTML),
|
|
109
|
+
("application/json", PYPI_TEXT_HTML),
|
|
110
|
+
("sth/else", PYPI_TEXT_HTML),
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
def test_simple_api_content_headers(
|
|
114
|
+
python_remote_factory, python_repo_with_sync, python_distribution_factory, header, result
|
|
115
|
+
):
|
|
116
|
+
remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
|
|
117
|
+
repo = python_repo_with_sync(remote)
|
|
118
|
+
distro = python_distribution_factory(repository=repo)
|
|
119
|
+
|
|
120
|
+
index_url = urljoin(distro.base_url, "simple/")
|
|
121
|
+
detail_url = f"{index_url}aiohttp"
|
|
122
|
+
|
|
123
|
+
for url in [index_url, detail_url]:
|
|
124
|
+
response = requests.get(url, headers={"Accept": header})
|
|
125
|
+
assert response.status_code == 200
|
|
126
|
+
assert result in response.headers["Content-Type"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.20.1
|
|
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.100,>=3.
|
|
23
|
+
Requires-Dist: pulpcore<3.100,>=3.85.0
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
25
|
Requires-Dist: bandersnatch<6.6,>=6.3.0
|
|
26
26
|
Requires-Dist: pypi-simple<2.0,>=1.5.0
|
|
@@ -45,6 +45,7 @@ pulp_python/app/migrations/0012_add_domain.py
|
|
|
45
45
|
pulp_python/app/migrations/0013_add_rbac_permissions.py
|
|
46
46
|
pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py
|
|
47
47
|
pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py
|
|
48
|
+
pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py
|
|
48
49
|
pulp_python/app/migrations/__init__.py
|
|
49
50
|
pulp_python/app/pypi/__init__.py
|
|
50
51
|
pulp_python/app/pypi/serializers.py
|
|
@@ -72,6 +73,7 @@ pulp_python/tests/functional/api/test_download_content.py
|
|
|
72
73
|
pulp_python/tests/functional/api/test_export_import.py
|
|
73
74
|
pulp_python/tests/functional/api/test_full_mirror.py
|
|
74
75
|
pulp_python/tests/functional/api/test_pypi_apis.py
|
|
76
|
+
pulp_python/tests/functional/api/test_pypi_simple_json_api.py
|
|
75
77
|
pulp_python/tests/functional/api/test_rbac.py
|
|
76
78
|
pulp_python/tests/functional/api/test_repair.py
|
|
77
79
|
pulp_python/tests/functional/api/test_sync.py
|
|
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pulp-python"
|
|
10
|
-
version = "3.
|
|
10
|
+
version = "3.20.1"
|
|
11
11
|
description = "pulp-python plugin for the Pulp Project"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -26,7 +26,7 @@ classifiers=[
|
|
|
26
26
|
]
|
|
27
27
|
requires-python = ">=3.11"
|
|
28
28
|
dependencies = [
|
|
29
|
-
"pulpcore>=3.
|
|
29
|
+
"pulpcore>=3.85.0,<3.100",
|
|
30
30
|
"pkginfo>=1.12.0,<1.13.0",
|
|
31
31
|
"bandersnatch>=6.3.0,<6.6", # 6.6 has breaking changes
|
|
32
32
|
"pypi-simple>=1.5.0,<2.0",
|
|
@@ -76,7 +76,7 @@ ignore = [
|
|
|
76
76
|
[tool.bumpversion]
|
|
77
77
|
# This section is managed by the plugin template. Do not edit manually.
|
|
78
78
|
|
|
79
|
-
current_version = "3.
|
|
79
|
+
current_version = "3.20.1"
|
|
80
80
|
commit = false
|
|
81
81
|
tag = false
|
|
82
82
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0003_new_sync_filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0010_update_json_field.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_auto_publish.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_content_unit.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_publications.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_crud_remotes.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_download_content.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_export_import.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.1}/pulp_python/tests/functional/api/test_pypi_apis.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|