pulp-python 3.32.0__tar.gz → 3.33.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.32.0 → pulp_python-3.33.0}/CHANGES.md +27 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/PKG-INFO +1 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/__init__.py +1 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/cache.py +5 -4
- pulp_python-3.33.0/pulp_python/app/migrations/0023_packageyank.py +50 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/models.py +27 -3
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/pypi/serializers.py +21 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/pypi/views.py +93 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/serializers.py +19 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/__init__.py +1 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/sync.py +10 -0
- pulp_python-3.33.0/pulp_python/app/tasks/yank.py +64 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/urls.py +3 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/utils.py +42 -23
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/viewsets.py +47 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/pytest_plugin.py +14 -12
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_attestations.py +14 -16
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +21 -2
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_full_mirror.py +1 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_pypi_apis.py +14 -6
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_simple_cache.py +41 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_upload.py +3 -3
- pulp_python-3.33.0/pulp_python/tests/functional/api/test_version_specifier_filter.py +28 -0
- pulp_python-3.33.0/pulp_python/tests/functional/api/test_yank.py +306 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/PKG-INFO +1 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/SOURCES.txt +4 -2
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pyproject.toml +3 -2
- pulp_python-3.32.0/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -1
- pulp_python-3.32.0/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -1
- {pulp_python-3.32.0 → pulp_python-3.33.0}/COMMITMENT +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/COPYRIGHT +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/LICENSE +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/MANIFEST.in +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/README.md +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/functest_requirements.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/exceptions.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/0022_pythonblocklistentry.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/provenance.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_blocklist.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/requires.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/setup.cfg +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/test_requirements.txt +0 -0
- {pulp_python-3.32.0 → pulp_python-3.33.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,33 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.33.0 (2026-08-07) {: #3.33.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.33.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added yank support (PEP 592).
|
|
16
|
+
[#1270](https://github.com/pulp/pulp_python/issues/1270)
|
|
17
|
+
- Added `version_specifier` filter to package content API for PEP 440 version matching.
|
|
18
|
+
[#1277](https://github.com/pulp/pulp_python/issues/1277)
|
|
19
|
+
- Added support for uploading packages with Core Metadata 2.5 via twine.
|
|
20
|
+
[#1321](https://github.com/pulp/pulp_python/issues/1321)
|
|
21
|
+
|
|
22
|
+
#### Bugfixes {: #3.33.0-bugfix }
|
|
23
|
+
|
|
24
|
+
- Fixed Simple API cache incorrectly serving JSON responses for HTML requests when clients used `?format=json`.
|
|
25
|
+
[#1302](https://github.com/pulp/pulp_python/issues/1302)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 3.32.1 (2026-07-29) {: #3.32.1 }
|
|
30
|
+
|
|
31
|
+
#### Bugfixes {: #3.32.1-bugfix }
|
|
32
|
+
|
|
33
|
+
- Fixed Simple API cache incorrectly serving JSON responses for HTML requests when clients used `?format=json`.
|
|
34
|
+
[#1302](https://github.com/pulp/pulp_python/issues/1302)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
11
38
|
## 3.32.0 (2026-07-21) {: #3.32.0 }
|
|
12
39
|
|
|
13
40
|
#### Features {: #3.32.0-feature }
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
from pulpcore.plugin.cache import CacheKeys, SyncContentCache
|
|
2
2
|
from pulpcore.plugin.util import cache_key
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
MEDIA_TYPE_KEY = "media_type"
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class PythonApiCache(SyncContentCache):
|
|
8
8
|
"""
|
|
9
9
|
Cache for the Simple API.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Keys on the negotiated media type so HTML and JSON are cached separately,
|
|
12
|
+
including when the same Accept header is used with `?format=json`.
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
def __init__(self, base_key=None):
|
|
15
|
-
keys = (CacheKeys.path, CacheKeys.method,
|
|
16
|
+
keys = (CacheKeys.path, CacheKeys.method, MEDIA_TYPE_KEY)
|
|
16
17
|
super().__init__(base_key=base_key, keys=keys)
|
|
17
18
|
|
|
18
19
|
def make_key(self, request):
|
|
19
20
|
all_keys = {
|
|
20
21
|
CacheKeys.path: request.path,
|
|
21
22
|
CacheKeys.method: request.method,
|
|
22
|
-
|
|
23
|
+
MEDIA_TYPE_KEY: getattr(request, "accepted_media_type", "") or "",
|
|
23
24
|
}
|
|
24
25
|
return ":".join(all_keys[k] for k in self.keys)
|
|
25
26
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated by Django 5.2.16 on 2026-07-30 10:44
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
import pulpcore.app.util
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("python", "0022_pythonblocklistentry"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="PackageYank",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"content_ptr",
|
|
21
|
+
models.OneToOneField(
|
|
22
|
+
auto_created=True,
|
|
23
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
24
|
+
parent_link=True,
|
|
25
|
+
primary_key=True,
|
|
26
|
+
serialize=False,
|
|
27
|
+
to="core.content",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
("name_normalized", models.TextField()),
|
|
31
|
+
("version", models.TextField()),
|
|
32
|
+
("yanked_reason", models.TextField(default="")),
|
|
33
|
+
(
|
|
34
|
+
"_pulp_domain",
|
|
35
|
+
models.ForeignKey(
|
|
36
|
+
default=pulpcore.app.util.get_domain_pk,
|
|
37
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
38
|
+
to="core.domain",
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
options={
|
|
43
|
+
"default_related_name": "%(app_label)s_%(model_name)s",
|
|
44
|
+
"unique_together": {
|
|
45
|
+
("name_normalized", "version", "yanked_reason", "_pulp_domain")
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
bases=("core.content",),
|
|
49
|
+
),
|
|
50
|
+
]
|
|
@@ -204,8 +204,6 @@ class PythonPackageContent(Content):
|
|
|
204
204
|
sha256 = models.CharField(db_index=True, max_length=64)
|
|
205
205
|
metadata_sha256 = models.CharField(max_length=64, null=True)
|
|
206
206
|
size = models.BigIntegerField(default=0)
|
|
207
|
-
# yanked and yanked_reason are not implemented because they are mutable
|
|
208
|
-
|
|
209
207
|
# From pulpcore
|
|
210
208
|
PROTECTED_FROM_RECLAIM = False
|
|
211
209
|
TYPE = "python"
|
|
@@ -289,6 +287,32 @@ class PackageProvenance(Content):
|
|
|
289
287
|
unique_together = ("sha256", "_pulp_domain")
|
|
290
288
|
|
|
291
289
|
|
|
290
|
+
class PackageYank(Content):
|
|
291
|
+
"""
|
|
292
|
+
A marker content type indicating a package version is yanked in a repository (PEP 592).
|
|
293
|
+
|
|
294
|
+
Its presence in a repository version means all files for the matching
|
|
295
|
+
(name_normalized, version) pair are yanked. Yank/unyank operations
|
|
296
|
+
add/remove this marker, creating new repository versions.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
TYPE = "python_yank"
|
|
300
|
+
repo_key_fields = ("name_normalized", "version")
|
|
301
|
+
|
|
302
|
+
name_normalized = models.TextField()
|
|
303
|
+
version = models.TextField()
|
|
304
|
+
yanked_reason = models.TextField(default="")
|
|
305
|
+
|
|
306
|
+
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
307
|
+
|
|
308
|
+
def __str__(self):
|
|
309
|
+
return f"<{self._meta.object_name}: {self.name_normalized} [{self.version}]>"
|
|
310
|
+
|
|
311
|
+
class Meta:
|
|
312
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
313
|
+
unique_together = ("name_normalized", "version", "yanked_reason", "_pulp_domain")
|
|
314
|
+
|
|
315
|
+
|
|
292
316
|
class PythonPublication(Publication, AutoAddObjPermsMixin):
|
|
293
317
|
"""
|
|
294
318
|
A Publication for PythonContent.
|
|
@@ -364,7 +388,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
364
388
|
"""
|
|
365
389
|
|
|
366
390
|
TYPE = "python"
|
|
367
|
-
CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
|
|
391
|
+
CONTENT_TYPES = [PythonPackageContent, PackageProvenance, PackageYank]
|
|
368
392
|
REMOTE_TYPES = [PythonRemote]
|
|
369
393
|
PULL_THROUGH_SUPPORTED = True
|
|
370
394
|
|
|
@@ -136,3 +136,24 @@ class PackageUploadTaskSerializer(serializers.Serializer):
|
|
|
136
136
|
session = serializers.CharField(allow_null=True)
|
|
137
137
|
task = serializers.CharField()
|
|
138
138
|
task_start_time = serializers.DateTimeField(allow_null=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class YankSerializer(serializers.Serializer):
|
|
142
|
+
"""
|
|
143
|
+
A Serializer for yank/unyank requests (PEP 592).
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
name = serializers.CharField(
|
|
147
|
+
help_text=_("The name of the package to yank or unyank."),
|
|
148
|
+
required=True,
|
|
149
|
+
)
|
|
150
|
+
version = serializers.CharField(
|
|
151
|
+
help_text=_("The version of the package to yank or unyank."),
|
|
152
|
+
required=True,
|
|
153
|
+
)
|
|
154
|
+
yanked_reason = serializers.CharField(
|
|
155
|
+
help_text=_("The reason for yanking the package version."),
|
|
156
|
+
required=False,
|
|
157
|
+
allow_blank=True,
|
|
158
|
+
default="",
|
|
159
|
+
)
|
|
@@ -29,6 +29,7 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, Templat
|
|
|
29
29
|
from rest_framework.response import Response
|
|
30
30
|
from rest_framework.viewsets import ViewSet
|
|
31
31
|
|
|
32
|
+
from pulpcore.plugin.serializers import AsyncOperationResponseSerializer
|
|
32
33
|
from pulpcore.plugin.tasking import dispatch
|
|
33
34
|
from pulpcore.plugin.util import get_domain, get_url
|
|
34
35
|
from pulpcore.plugin.viewsets import OperationPostponedResponse
|
|
@@ -37,6 +38,7 @@ from pulp_python.app import tasks
|
|
|
37
38
|
from pulp_python.app.cache import PythonApiCache, find_base_path_cached
|
|
38
39
|
from pulp_python.app.models import (
|
|
39
40
|
PackageProvenance,
|
|
41
|
+
PackageYank,
|
|
40
42
|
PythonDistribution,
|
|
41
43
|
PythonPackageContent,
|
|
42
44
|
PythonPublication,
|
|
@@ -46,6 +48,7 @@ from pulp_python.app.pypi.serializers import (
|
|
|
46
48
|
PackageUploadSerializer,
|
|
47
49
|
PackageUploadTaskSerializer,
|
|
48
50
|
SummarySerializer,
|
|
51
|
+
YankSerializer,
|
|
49
52
|
)
|
|
50
53
|
from pulp_python.app.utils import (
|
|
51
54
|
PYPI_LAST_SERIAL,
|
|
@@ -356,6 +359,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
356
359
|
"upload_time": release_package.upload_time,
|
|
357
360
|
"version": release_package.version,
|
|
358
361
|
"provenance": release_package.provenance_url,
|
|
362
|
+
"yanked": release_package.is_yanked,
|
|
363
|
+
"yanked_reason": release_package.yanked_reason or "",
|
|
359
364
|
}
|
|
360
365
|
|
|
361
366
|
rfilter = get_remote_package_filter(remote)
|
|
@@ -408,6 +413,11 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
408
413
|
"version",
|
|
409
414
|
"has_provenance",
|
|
410
415
|
)
|
|
416
|
+
yank_markers = dict(
|
|
417
|
+
PackageYank.objects.filter(
|
|
418
|
+
pk__in=repo_ver.content, name_normalized=normalized
|
|
419
|
+
).values_list("version", "yanked_reason")
|
|
420
|
+
)
|
|
411
421
|
local_releases = {
|
|
412
422
|
p["filename"]: {
|
|
413
423
|
**p,
|
|
@@ -418,6 +428,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
418
428
|
if p["has_provenance"]
|
|
419
429
|
else None
|
|
420
430
|
),
|
|
431
|
+
"yanked": p["version"] in yank_markers,
|
|
432
|
+
"yanked_reason": yank_markers.get(p["version"], ""),
|
|
421
433
|
}
|
|
422
434
|
for p in packages
|
|
423
435
|
}
|
|
@@ -493,12 +505,18 @@ class MetadataView(PyPIMixin, ViewSet):
|
|
|
493
505
|
headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
|
|
494
506
|
if settings.DOMAIN_ENABLED:
|
|
495
507
|
domain = get_domain()
|
|
508
|
+
yank_markers = dict(
|
|
509
|
+
PackageYank.objects.filter(
|
|
510
|
+
pk__in=repo_ver.content, name_normalized=normalized
|
|
511
|
+
).values_list("version", "yanked_reason")
|
|
512
|
+
)
|
|
496
513
|
json_body = python_content_to_json(
|
|
497
514
|
path,
|
|
498
515
|
package_content,
|
|
499
516
|
version=version,
|
|
500
517
|
domain=domain,
|
|
501
518
|
repository_version=repo_ver,
|
|
519
|
+
yank_markers=yank_markers,
|
|
502
520
|
)
|
|
503
521
|
if json_body:
|
|
504
522
|
return Response(data=json_body, headers=headers)
|
|
@@ -586,3 +604,78 @@ class ProvenanceView(PyPIMixin, ViewSet):
|
|
|
586
604
|
if provenance:
|
|
587
605
|
return Response(data=provenance.provenance)
|
|
588
606
|
return HttpResponseNotFound(f"{package} {version} {filename} provenance does not exist.")
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class YankView(PyPIMixin, ViewSet):
|
|
610
|
+
"""View for yank/unyank requests (PEP 592)."""
|
|
611
|
+
|
|
612
|
+
endpoint_name = "yank"
|
|
613
|
+
DEFAULT_ACCESS_POLICY = {
|
|
614
|
+
"statements": [
|
|
615
|
+
{
|
|
616
|
+
"action": ["yank", "unyank"],
|
|
617
|
+
"principal": "authenticated",
|
|
618
|
+
"effect": "allow",
|
|
619
|
+
"condition": "index_has_repo_perm:python.modify_pythonrepository",
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@extend_schema(
|
|
625
|
+
request=YankSerializer,
|
|
626
|
+
responses={202: AsyncOperationResponseSerializer},
|
|
627
|
+
summary="Yank a package version",
|
|
628
|
+
)
|
|
629
|
+
def yank(self, request, path):
|
|
630
|
+
"""Yank a package version, marking all its files with data-yanked."""
|
|
631
|
+
repo = self.distribution.repository
|
|
632
|
+
if not repo:
|
|
633
|
+
return HttpResponseBadRequest(reason="Index is not pointing to a repository")
|
|
634
|
+
|
|
635
|
+
serializer = YankSerializer(data=request.data)
|
|
636
|
+
serializer.is_valid(raise_exception=True)
|
|
637
|
+
|
|
638
|
+
normalized = canonicalize_name(serializer.validated_data["name"])
|
|
639
|
+
version = serializer.validated_data["version"]
|
|
640
|
+
repo_ver = self.get_repository_version(self.distribution)
|
|
641
|
+
if not PythonPackageContent.objects.filter(
|
|
642
|
+
pk__in=repo_ver.content, name_normalized=normalized, version=version
|
|
643
|
+
).exists():
|
|
644
|
+
return HttpResponseNotFound(f"{normalized}=={version} not found in repository")
|
|
645
|
+
|
|
646
|
+
result = dispatch(
|
|
647
|
+
tasks.ayank_package,
|
|
648
|
+
exclusive_resources=[repo],
|
|
649
|
+
kwargs={
|
|
650
|
+
"repository_pk": str(repo.pk),
|
|
651
|
+
"name": serializer.validated_data["name"],
|
|
652
|
+
"version": serializer.validated_data["version"],
|
|
653
|
+
"yanked_reason": serializer.validated_data.get("yanked_reason", ""),
|
|
654
|
+
},
|
|
655
|
+
)
|
|
656
|
+
return OperationPostponedResponse(result, request)
|
|
657
|
+
|
|
658
|
+
@extend_schema(
|
|
659
|
+
request=YankSerializer,
|
|
660
|
+
responses={202: AsyncOperationResponseSerializer},
|
|
661
|
+
summary="Unyank a package version",
|
|
662
|
+
)
|
|
663
|
+
def unyank(self, request, path):
|
|
664
|
+
"""Unyank a package version, unmarking all its files with data-yanked."""
|
|
665
|
+
repo = self.distribution.repository
|
|
666
|
+
if not repo:
|
|
667
|
+
return HttpResponseBadRequest(reason="Index is not pointing to a repository")
|
|
668
|
+
|
|
669
|
+
serializer = YankSerializer(data=request.data)
|
|
670
|
+
serializer.is_valid(raise_exception=True)
|
|
671
|
+
|
|
672
|
+
result = dispatch(
|
|
673
|
+
tasks.aunyank_package,
|
|
674
|
+
exclusive_resources=[repo],
|
|
675
|
+
kwargs={
|
|
676
|
+
"repository_pk": str(repo.pk),
|
|
677
|
+
"name": serializer.validated_data["name"],
|
|
678
|
+
"version": serializer.validated_data["version"],
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
return OperationPostponedResponse(result, request)
|
|
@@ -683,6 +683,25 @@ class PackageProvenanceSerializer(core_serializers.NoArtifactContentUploadSerial
|
|
|
683
683
|
model = python_models.PackageProvenance
|
|
684
684
|
|
|
685
685
|
|
|
686
|
+
class PackageYankSerializer(core_serializers.NoArtifactContentSerializer):
|
|
687
|
+
"""
|
|
688
|
+
Read-only serializer for PackageYank content units (PEP 592).
|
|
689
|
+
Used by PackageYankViewSet to expose yank markers via the Pulp REST API.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
name_normalized = serializers.CharField(read_only=True)
|
|
693
|
+
version = serializers.CharField(read_only=True)
|
|
694
|
+
yanked_reason = serializers.CharField(read_only=True)
|
|
695
|
+
|
|
696
|
+
class Meta:
|
|
697
|
+
fields = core_serializers.NoArtifactContentSerializer.Meta.fields + (
|
|
698
|
+
"name_normalized",
|
|
699
|
+
"version",
|
|
700
|
+
"yanked_reason",
|
|
701
|
+
)
|
|
702
|
+
model = python_models.PackageYank
|
|
703
|
+
|
|
704
|
+
|
|
686
705
|
class MultipleChoiceArrayField(serializers.MultipleChoiceField):
|
|
687
706
|
"""
|
|
688
707
|
A wrapper to make sure this DRF serializer works properly with ArrayFields.
|
|
@@ -24,6 +24,7 @@ from pulpcore.plugin.stages import (
|
|
|
24
24
|
from pulp_python.app.exceptions import UnsupportedProtocolError
|
|
25
25
|
from pulp_python.app.models import (
|
|
26
26
|
PackageProvenance,
|
|
27
|
+
PackageYank,
|
|
27
28
|
PythonPackageContent,
|
|
28
29
|
PythonRemote,
|
|
29
30
|
)
|
|
@@ -265,6 +266,15 @@ class PulpMirror(Mirror):
|
|
|
265
266
|
)
|
|
266
267
|
d_artifacts.append(metadata_artifact)
|
|
267
268
|
|
|
269
|
+
if upstream_pkg.is_yanked:
|
|
270
|
+
yank_marker = PackageYank(
|
|
271
|
+
name_normalized=pkg.name,
|
|
272
|
+
version=version,
|
|
273
|
+
yanked_reason=upstream_pkg.yanked_reason or "",
|
|
274
|
+
)
|
|
275
|
+
yank_dc = DeclarativeContent(content=yank_marker, d_artifacts=[])
|
|
276
|
+
await self.python_stage.put(yank_dc)
|
|
277
|
+
|
|
268
278
|
dc = DeclarativeContent(content=package, d_artifacts=d_artifacts)
|
|
269
279
|
declared_contents[entry["filename"]] = dc
|
|
270
280
|
await self.python_stage.put(dc)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from packaging.utils import canonicalize_name
|
|
2
|
+
|
|
3
|
+
from pulpcore.plugin.exceptions import ValidationError
|
|
4
|
+
from pulpcore.plugin.tasking import aadd_and_remove
|
|
5
|
+
|
|
6
|
+
from pulp_python.app.models import PackageYank, PythonPackageContent, PythonRepository
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def ayank_package(repository_pk, name, version, yanked_reason=""):
|
|
10
|
+
"""
|
|
11
|
+
Yank a package version in a repository by adding a PackageYank marker.
|
|
12
|
+
Creates a new repository version with the yank marker added.
|
|
13
|
+
"""
|
|
14
|
+
normalized = canonicalize_name(name)
|
|
15
|
+
repository = await PythonRepository.objects.aget(pk=repository_pk)
|
|
16
|
+
latest = await repository.alatest_version()
|
|
17
|
+
|
|
18
|
+
exists = await PythonPackageContent.objects.filter(
|
|
19
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
20
|
+
).aexists()
|
|
21
|
+
if not exists:
|
|
22
|
+
raise ValidationError(f"Package {name}=={version} not found in repository")
|
|
23
|
+
|
|
24
|
+
existing_yank = await PackageYank.objects.filter(
|
|
25
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
26
|
+
).afirst()
|
|
27
|
+
if existing_yank and existing_yank.yanked_reason == yanked_reason:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
yank_marker, _ = await PackageYank.objects.aget_or_create(
|
|
31
|
+
name_normalized=normalized,
|
|
32
|
+
version=version,
|
|
33
|
+
yanked_reason=yanked_reason,
|
|
34
|
+
_pulp_domain_id=repository.pulp_domain_id,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
await aadd_and_remove(
|
|
38
|
+
repository_pk=repository.pk,
|
|
39
|
+
add_content_units=[yank_marker.pk],
|
|
40
|
+
remove_content_units=[],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def aunyank_package(repository_pk, name, version):
|
|
45
|
+
"""
|
|
46
|
+
Unyank a package version in a repository by removing its PackageYank marker.
|
|
47
|
+
Creates a new repository version with the yank marker removed.
|
|
48
|
+
"""
|
|
49
|
+
normalized = canonicalize_name(name)
|
|
50
|
+
repository = await PythonRepository.objects.aget(pk=repository_pk)
|
|
51
|
+
latest = await repository.alatest_version()
|
|
52
|
+
|
|
53
|
+
yank_marker = await PackageYank.objects.filter(
|
|
54
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
55
|
+
).afirst()
|
|
56
|
+
|
|
57
|
+
if yank_marker is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
await aadd_and_remove(
|
|
61
|
+
repository_pk=repository.pk,
|
|
62
|
+
add_content_units=[],
|
|
63
|
+
remove_content_units=[yank_marker.pk],
|
|
64
|
+
)
|
|
@@ -7,6 +7,7 @@ from pulp_python.app.pypi.views import (
|
|
|
7
7
|
PyPIView,
|
|
8
8
|
SimpleView,
|
|
9
9
|
UploadView,
|
|
10
|
+
YankView,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
if settings.DOMAIN_ENABLED:
|
|
@@ -40,5 +41,7 @@ urlpatterns = [
|
|
|
40
41
|
SimpleView.as_view({"get": "list", "post": "create"}),
|
|
41
42
|
name="simple-detail",
|
|
42
43
|
),
|
|
44
|
+
path(PYPI_API_URL + "yank/", YankView.as_view({"post": "yank"}), name="yank"),
|
|
45
|
+
path(PYPI_API_URL + "unyank/", YankView.as_view({"post": "unyank"}), name="unyank"),
|
|
43
46
|
path(PYPI_API_URL, PyPIView.as_view({"get": "retrieve"}), name="pypi-detail"),
|
|
44
47
|
]
|
|
@@ -30,7 +30,7 @@ log = logging.getLogger(__name__)
|
|
|
30
30
|
PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
|
|
31
31
|
"""TODO This serial constant is temporary until Python repositories implements serials"""
|
|
32
32
|
PYPI_SERIAL_CONSTANT = 1000000000
|
|
33
|
-
SUPPORTED_METADATA_VERSIONS = ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4")
|
|
33
|
+
SUPPORTED_METADATA_VERSIONS = ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5")
|
|
34
34
|
|
|
35
35
|
SIMPLE_API_VERSION = "1.1"
|
|
36
36
|
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
@@ -50,7 +50,6 @@ simple_index_template = """<!DOCTYPE html>
|
|
|
50
50
|
</html>
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
# TODO in the future: data-yanked (not implemented yet because it is mutable)
|
|
54
53
|
simple_detail_template = """<!DOCTYPE html>
|
|
55
54
|
<html>
|
|
56
55
|
<head>
|
|
@@ -62,6 +61,7 @@ simple_detail_template = """<!DOCTYPE html>
|
|
|
62
61
|
{%- for pkg in project_packages %}
|
|
63
62
|
<a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
|
|
64
63
|
{%- if pkg.requires_python %} data-requires-python="{{ pkg.requires_python }}" {%- endif %}
|
|
64
|
+
{%- if pkg.yanked %} data-yanked="{{ pkg.yanked_reason }}" {%- endif %}
|
|
65
65
|
{%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}" data-core-metadata="sha256={{ pkg.metadata_sha256 }}"
|
|
66
66
|
{%- endif %} {% if pkg.provenance -%}
|
|
67
67
|
data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
|
|
@@ -362,7 +362,12 @@ def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -
|
|
|
362
362
|
|
|
363
363
|
|
|
364
364
|
def python_content_to_json(
|
|
365
|
-
base_path,
|
|
365
|
+
base_path,
|
|
366
|
+
content_query,
|
|
367
|
+
version=None,
|
|
368
|
+
domain=None,
|
|
369
|
+
repository_version=None,
|
|
370
|
+
yank_markers=None,
|
|
366
371
|
):
|
|
367
372
|
"""
|
|
368
373
|
Converts a QuerySet of PythonPackageContent into the PyPi JSON format
|
|
@@ -375,6 +380,8 @@ def python_content_to_json(
|
|
|
375
380
|
|
|
376
381
|
Returns None if version is specified but not found within content_query
|
|
377
382
|
"""
|
|
383
|
+
if yank_markers is None:
|
|
384
|
+
yank_markers = {}
|
|
378
385
|
if repository_version:
|
|
379
386
|
content_query = content_query.annotate(
|
|
380
387
|
active_membership=FilteredRelation(
|
|
@@ -386,25 +393,32 @@ def python_content_to_json(
|
|
|
386
393
|
),
|
|
387
394
|
repo_added_time=F("active_membership__pulp_created"),
|
|
388
395
|
)
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
|
|
397
|
+
all_content = list(content_query)
|
|
398
|
+
for content in all_content:
|
|
399
|
+
content.yanked = content.version in yank_markers
|
|
400
|
+
content.yanked_reason = yank_markers.get(content.version)
|
|
401
|
+
|
|
402
|
+
latest_content = latest_content_version(all_content, version)
|
|
391
403
|
if not latest_content:
|
|
392
404
|
return None
|
|
393
|
-
|
|
394
|
-
full_metadata
|
|
395
|
-
full_metadata
|
|
405
|
+
|
|
406
|
+
full_metadata = {"last_serial": 0} # For now the serial field isn't supported by Pulp
|
|
407
|
+
full_metadata["info"] = python_content_to_info(latest_content[0])
|
|
408
|
+
full_metadata["releases"] = python_content_to_releases(all_content, base_path, domain)
|
|
409
|
+
full_metadata["urls"] = python_content_to_urls(latest_content, base_path, domain)
|
|
396
410
|
return full_metadata
|
|
397
411
|
|
|
398
412
|
|
|
399
|
-
def latest_content_version(
|
|
413
|
+
def latest_content_version(all_content, version):
|
|
400
414
|
"""
|
|
401
|
-
Walks through the content
|
|
415
|
+
Walks through the content list and finds the instances that are the latest version.
|
|
402
416
|
If 'version' is specified, the function instead tries to find content instances
|
|
403
417
|
with that version and will return an empty list if nothing is found
|
|
404
418
|
"""
|
|
405
419
|
latest_version = version
|
|
406
420
|
latest_content = []
|
|
407
|
-
for content in
|
|
421
|
+
for content in all_content:
|
|
408
422
|
if version and parse(version) == parse(content.version):
|
|
409
423
|
latest_content.append(content)
|
|
410
424
|
elif not latest_version or parse(content.version) > parse(latest_version):
|
|
@@ -462,8 +476,8 @@ def python_content_to_info(content):
|
|
|
462
476
|
"platform": content.platform or "",
|
|
463
477
|
"requires_dist": json_to_dict(content.requires_dist) or None,
|
|
464
478
|
"classifiers": json_to_dict(content.classifiers) or None,
|
|
465
|
-
"yanked":
|
|
466
|
-
"yanked_reason": None,
|
|
479
|
+
"yanked": getattr(content, "yanked", False),
|
|
480
|
+
"yanked_reason": getattr(content, "yanked_reason", None),
|
|
467
481
|
# New core metadata (Version 2.1, 2.2, 2.4)
|
|
468
482
|
"provides_extras": json_to_dict(content.provides_extras) or None,
|
|
469
483
|
"dynamic": json_to_dict(content.dynamic) or None,
|
|
@@ -472,13 +486,13 @@ def python_content_to_info(content):
|
|
|
472
486
|
}
|
|
473
487
|
|
|
474
488
|
|
|
475
|
-
def python_content_to_releases(
|
|
489
|
+
def python_content_to_releases(all_content, base_path, domain=None):
|
|
476
490
|
"""
|
|
477
|
-
Takes a
|
|
491
|
+
Takes a list of PythonPackageContent and returns a dictionary of releases
|
|
478
492
|
with each key being a version and value being a list of content for that version of the package
|
|
479
493
|
"""
|
|
480
494
|
releases = defaultdict(lambda: [])
|
|
481
|
-
for content in
|
|
495
|
+
for content in all_content:
|
|
482
496
|
releases[content.version].append(
|
|
483
497
|
python_content_to_download_info(content, base_path, domain)
|
|
484
498
|
)
|
|
@@ -535,22 +549,22 @@ def python_content_to_download_info(content, base_path, domain=None):
|
|
|
535
549
|
(getattr(content, "repo_added_time", None) or content.pulp_created).isoformat()
|
|
536
550
|
),
|
|
537
551
|
"url": url,
|
|
538
|
-
"yanked": False,
|
|
539
|
-
"yanked_reason": None,
|
|
552
|
+
"yanked": getattr(content, "yanked", False),
|
|
553
|
+
"yanked_reason": getattr(content, "yanked_reason", None),
|
|
540
554
|
}
|
|
541
555
|
|
|
542
556
|
|
|
543
|
-
def write_simple_index(project_names
|
|
557
|
+
def write_simple_index(project_names):
|
|
544
558
|
"""Writes the simple index."""
|
|
545
559
|
simple = Template(simple_index_template)
|
|
546
560
|
context = {
|
|
547
561
|
"SIMPLE_API_VERSION": SIMPLE_API_VERSION,
|
|
548
562
|
"projects": ((x, canonicalize_name(x)) for x in project_names),
|
|
549
563
|
}
|
|
550
|
-
return simple.
|
|
564
|
+
return simple.render(**context)
|
|
551
565
|
|
|
552
566
|
|
|
553
|
-
def write_simple_detail(project_name, project_packages
|
|
567
|
+
def write_simple_detail(project_name, project_packages):
|
|
554
568
|
"""Writes the simple detail page of a package."""
|
|
555
569
|
detail = Template(simple_detail_template, autoescape=True)
|
|
556
570
|
context = {
|
|
@@ -558,7 +572,7 @@ def write_simple_detail(project_name, project_packages, streamed=False):
|
|
|
558
572
|
"project_name": project_name,
|
|
559
573
|
"project_packages": project_packages,
|
|
560
574
|
}
|
|
561
|
-
return detail.
|
|
575
|
+
return detail.render(**context)
|
|
562
576
|
|
|
563
577
|
|
|
564
578
|
def write_simple_index_json(project_names):
|
|
@@ -591,7 +605,12 @@ def write_simple_detail_json(project_name, project_packages):
|
|
|
591
605
|
"core-metadata": (
|
|
592
606
|
{"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
|
|
593
607
|
),
|
|
594
|
-
#
|
|
608
|
+
# PEP 592
|
|
609
|
+
"yanked": (
|
|
610
|
+
package.get("yanked_reason")
|
|
611
|
+
if package.get("yanked") and package.get("yanked_reason")
|
|
612
|
+
else package.get("yanked", False)
|
|
613
|
+
),
|
|
595
614
|
# (v1.1, PEP 700)
|
|
596
615
|
"size": package["size"],
|
|
597
616
|
"upload-time": format_upload_time(package["upload_time"]),
|