pulp-python 3.19.1__tar.gz → 3.20.0__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.0}/CHANGES.md +16 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/PKG-INFO +1 -1
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.20.0/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +18 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/models.py +2 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/pypi/views.py +89 -10
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/serializers.py +13 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/tasks/publish.py +3 -1
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/utils.py +87 -16
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_consume_content.py +1 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_domains.py +1 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_full_mirror.py +20 -2
- pulp_python-3.20.0/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +126 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/PKG-INFO +1 -1
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/SOURCES.txt +2 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pyproject.toml +2 -2
- {pulp_python-3.19.1 → pulp_python-3.20.0}/COMMITMENT +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/COPYRIGHT +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/LICENSE +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/MANIFEST.in +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/README.md +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/functest_requirements.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/urls.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/viewsets.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/requires.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/setup.cfg +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/test_requirements.txt +0 -0
- {pulp_python-3.19.1 → pulp_python-3.20.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.20.0 (2025-11-07) {: #3.20.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.20.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added JSON-based Simple API (PEP 691).
|
|
16
|
+
[#625](https://github.com/pulp/pulp_python/issues/625)
|
|
17
|
+
- Updated tasks to always return JSON-serializable value.
|
|
18
|
+
[#972](https://github.com/pulp/pulp_python/issues/972)
|
|
19
|
+
|
|
20
|
+
#### Bugfixes {: #3.20.0-bugfix }
|
|
21
|
+
|
|
22
|
+
- Fixed publication error when package's dists contain differing versions of its name.
|
|
23
|
+
[#907](https://github.com/pulp/pulp_python/issues/907)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
11
27
|
## 3.19.1 (2025-09-14) {: #3.19.1 }
|
|
12
28
|
|
|
13
29
|
#### Bugfixes {: #3.19.1-bugfix }
|
|
@@ -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
|
|
@@ -43,7 +45,9 @@ from pulp_python.app.pypi.serializers import (
|
|
|
43
45
|
)
|
|
44
46
|
from pulp_python.app.utils import (
|
|
45
47
|
write_simple_index,
|
|
48
|
+
write_simple_index_json,
|
|
46
49
|
write_simple_detail,
|
|
50
|
+
write_simple_detail_json,
|
|
47
51
|
python_content_to_json,
|
|
48
52
|
PYPI_LAST_SERIAL,
|
|
49
53
|
PYPI_SERIAL_CONSTANT,
|
|
@@ -57,6 +61,17 @@ log = logging.getLogger(__name__)
|
|
|
57
61
|
ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
|
|
58
62
|
BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX)
|
|
59
63
|
|
|
64
|
+
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
65
|
+
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PyPISimpleHTMLRenderer(TemplateHTMLRenderer):
|
|
69
|
+
media_type = PYPI_SIMPLE_V1_HTML
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PyPISimpleJSONRenderer(JSONRenderer):
|
|
73
|
+
media_type = PYPI_SIMPLE_V1_JSON
|
|
74
|
+
|
|
60
75
|
|
|
61
76
|
class PyPIMixin:
|
|
62
77
|
"""Mixin to get index specific info."""
|
|
@@ -235,6 +250,25 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
235
250
|
],
|
|
236
251
|
}
|
|
237
252
|
|
|
253
|
+
def perform_content_negotiation(self, request, force=False):
|
|
254
|
+
"""
|
|
255
|
+
Uses standard content negotiation, defaulting to HTML if no acceptable renderer is found.
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
return super().perform_content_negotiation(request, force)
|
|
259
|
+
except NotAcceptable:
|
|
260
|
+
return TemplateHTMLRenderer(), TemplateHTMLRenderer.media_type # text/html
|
|
261
|
+
|
|
262
|
+
def get_renderers(self):
|
|
263
|
+
"""
|
|
264
|
+
Uses custom renderers for PyPI Simple API endpoints, defaulting to standard ones.
|
|
265
|
+
"""
|
|
266
|
+
if self.action in ["list", "retrieve"]:
|
|
267
|
+
# Ordered by priority if multiple content types are present
|
|
268
|
+
return [TemplateHTMLRenderer(), PyPISimpleHTMLRenderer(), PyPISimpleJSONRenderer()]
|
|
269
|
+
else:
|
|
270
|
+
return [JSONRenderer(), BrowsableAPIRenderer()]
|
|
271
|
+
|
|
238
272
|
@extend_schema(summary="Get index simple page")
|
|
239
273
|
def list(self, request, path):
|
|
240
274
|
"""Gets the simple api html page for the index."""
|
|
@@ -242,9 +276,18 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
242
276
|
if self.should_redirect(repo_version=repo_version):
|
|
243
277
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/"))
|
|
244
278
|
names = content.order_by("name").values_list("name", flat=True).distinct().iterator()
|
|
245
|
-
|
|
279
|
+
media_type = request.accepted_renderer.media_type
|
|
280
|
+
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
281
|
+
|
|
282
|
+
if media_type == PYPI_SIMPLE_V1_JSON:
|
|
283
|
+
index_data = write_simple_index_json(names)
|
|
284
|
+
return Response(index_data, headers=headers)
|
|
285
|
+
else:
|
|
286
|
+
index_data = write_simple_index(names, streamed=True)
|
|
287
|
+
kwargs = {"content_type": media_type, "headers": headers}
|
|
288
|
+
return StreamingHttpResponse(index_data, **kwargs)
|
|
246
289
|
|
|
247
|
-
def pull_through_package_simple(self, package, path, remote):
|
|
290
|
+
def pull_through_package_simple(self, package, path, remote, media_type):
|
|
248
291
|
"""Gets the package's simple page from remote."""
|
|
249
292
|
|
|
250
293
|
def parse_package(release_package):
|
|
@@ -252,7 +295,13 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
252
295
|
stripped_url = urlunsplit(chain(parsed[:3], ("", "")))
|
|
253
296
|
redirect_path = f"{path}/{release_package.filename}?redirect={stripped_url}"
|
|
254
297
|
d_url = urljoin(self.base_content_url, redirect_path)
|
|
255
|
-
return
|
|
298
|
+
return {
|
|
299
|
+
"filename": release_package.filename,
|
|
300
|
+
"url": d_url,
|
|
301
|
+
"sha256": release_package.digests.get("sha256", ""),
|
|
302
|
+
"requires_python": release_package.requires_python,
|
|
303
|
+
"metadata_sha256": (release_package.metadata_digests or {}).get("sha256"),
|
|
304
|
+
}
|
|
256
305
|
|
|
257
306
|
rfilter = get_remote_package_filter(remote)
|
|
258
307
|
if not rfilter.filter_project(package):
|
|
@@ -269,28 +318,40 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
269
318
|
except TimeoutException:
|
|
270
319
|
return HttpResponse(f"{remote.url} timed out while fetching {package}.", status=504)
|
|
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
325
|
packages = [
|
|
277
326
|
parse_package(p) for p in page.packages if rfilter.filter_release(package, p.version)
|
|
278
327
|
]
|
|
279
|
-
|
|
328
|
+
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
329
|
+
|
|
330
|
+
if media_type == PYPI_SIMPLE_V1_JSON:
|
|
331
|
+
detail_data = write_simple_detail_json(package, packages)
|
|
332
|
+
return Response(detail_data, headers=headers)
|
|
333
|
+
else:
|
|
334
|
+
detail_data = write_simple_detail(package, packages)
|
|
335
|
+
kwargs = {"content_type": media_type, "headers": headers}
|
|
336
|
+
return HttpResponse(detail_data, **kwargs)
|
|
280
337
|
|
|
281
338
|
@extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
|
|
282
339
|
def retrieve(self, request, path, package):
|
|
283
|
-
"""Retrieves the simple api html page for a package."""
|
|
340
|
+
"""Retrieves the simple api html/json page for a package."""
|
|
341
|
+
media_type = request.accepted_renderer.media_type
|
|
342
|
+
|
|
284
343
|
repo_ver, content = self.get_rvc()
|
|
285
344
|
# Should I redirect if the normalized name is different?
|
|
286
345
|
normalized = canonicalize_name(package)
|
|
287
346
|
if self.distribution.remote:
|
|
288
|
-
return self.pull_through_package_simple(
|
|
347
|
+
return self.pull_through_package_simple(
|
|
348
|
+
normalized, path, self.distribution.remote, media_type
|
|
349
|
+
)
|
|
289
350
|
if self.should_redirect(repo_version=repo_ver):
|
|
290
351
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
|
|
291
352
|
packages = (
|
|
292
353
|
content.filter(name__normalize=normalized)
|
|
293
|
-
.values_list("filename", "sha256", "name")
|
|
354
|
+
.values_list("filename", "sha256", "name", "metadata_sha256", "requires_python")
|
|
294
355
|
.iterator()
|
|
295
356
|
)
|
|
296
357
|
try:
|
|
@@ -300,8 +361,26 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
300
361
|
else:
|
|
301
362
|
packages = chain([present], packages)
|
|
302
363
|
name = present[2]
|
|
303
|
-
releases = (
|
|
304
|
-
|
|
364
|
+
releases = (
|
|
365
|
+
{
|
|
366
|
+
"filename": filename,
|
|
367
|
+
"url": urljoin(self.base_content_url, f"{path}/{filename}"),
|
|
368
|
+
"sha256": sha256,
|
|
369
|
+
"metadata_sha256": metadata_sha256,
|
|
370
|
+
"requires_python": requires_python,
|
|
371
|
+
}
|
|
372
|
+
for filename, sha256, _, metadata_sha256, requires_python in packages
|
|
373
|
+
)
|
|
374
|
+
media_type = request.accepted_renderer.media_type
|
|
375
|
+
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
376
|
+
|
|
377
|
+
if media_type == PYPI_SIMPLE_V1_JSON:
|
|
378
|
+
detail_data = write_simple_detail_json(name, releases)
|
|
379
|
+
return Response(detail_data, headers=headers)
|
|
380
|
+
else:
|
|
381
|
+
detail_data = write_simple_detail(name, releases, streamed=True)
|
|
382
|
+
kwargs = {"content_type": media_type, "headers": headers}
|
|
383
|
+
return StreamingHttpResponse(detail_data, **kwargs)
|
|
305
384
|
|
|
306
385
|
@extend_schema(
|
|
307
386
|
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.0}/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 == "404 Not Found"
|
|
70
88
|
|
|
71
89
|
r = requests.get(f"{distro.base_url}simple/shelf-reader/")
|
|
72
90
|
assert r.status_code == 200
|
|
@@ -86,7 +104,7 @@ 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 == "404 Not Found"
|
|
90
108
|
|
|
91
109
|
r = requests.get(f"{distro.base_url}simple/pulpcore/")
|
|
92
110
|
assert r.status_code == 502
|
|
@@ -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"]
|
|
@@ -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.0"
|
|
11
11
|
description = "pulp-python plugin for the Pulp Project"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -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.0"
|
|
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.0}/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.0}/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.0}/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.0}/pulp_python/tests/functional/api/test_auto_publish.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_content_unit.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_publications.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_crud_remotes.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_download_content.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/pulp_python/tests/functional/api/test_export_import.py
RENAMED
|
File without changes
|
{pulp_python-3.19.1 → pulp_python-3.20.0}/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
|
|
File without changes
|