pulp-python 3.32.1__tar.gz → 3.34.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.1 → pulp_python-3.34.0}/CHANGES.md +27 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/PKG-INFO +1 -1
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/__init__.py +1 -1
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/management/commands/repair-python-metadata.py +1 -1
- pulp_python-3.34.0/pulp_python/app/migrations/0023_packageyank.py +50 -0
- pulp_python-3.34.0/pulp_python/app/migrations/0024_pythonrepository_error_on_reject.py +16 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/models.py +111 -27
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/pypi/serializers.py +21 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/pypi/views.py +93 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/serializers.py +31 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/__init__.py +1 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/sync.py +10 -0
- pulp_python-3.34.0/pulp_python/app/tasks/yank.py +64 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/urls.py +3 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/utils.py +42 -23
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/viewsets.py +68 -13
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_blocklist.py +51 -1
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +77 -2
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_domains.py +1 -1
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_full_mirror.py +1 -1
- pulp_python-3.34.0/pulp_python/tests/functional/api/test_version_specifier_filter.py +28 -0
- pulp_python-3.34.0/pulp_python/tests/functional/api/test_yank.py +306 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/PKG-INFO +1 -1
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/SOURCES.txt +5 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pyproject.toml +2 -2
- {pulp_python-3.32.1 → pulp_python-3.34.0}/COMMITMENT +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/COPYRIGHT +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/LICENSE +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/MANIFEST.in +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/README.md +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/functest_requirements.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/cache.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/exceptions.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/0022_pythonblocklistentry.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/provenance.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_simple_cache.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/requires.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/setup.cfg +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/test_requirements.txt +0 -0
- {pulp_python-3.32.1 → pulp_python-3.34.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,33 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.34.0 (2026-08-18) {: #3.34.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.34.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added an `error_on_reject` boolean field to PythonRepository (default: `True`). When `False`, packages rejected by the blocklist or package substitution policies are skipped instead of failing the entire repository version.
|
|
16
|
+
[#1278](https://github.com/pulp/pulp_python/issues/1278)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3.33.0 (2026-08-07) {: #3.33.0 }
|
|
21
|
+
|
|
22
|
+
#### Features {: #3.33.0-feature }
|
|
23
|
+
|
|
24
|
+
- Added yank support (PEP 592).
|
|
25
|
+
[#1270](https://github.com/pulp/pulp_python/issues/1270)
|
|
26
|
+
- Added `version_specifier` filter to package content API for PEP 440 version matching.
|
|
27
|
+
[#1277](https://github.com/pulp/pulp_python/issues/1277)
|
|
28
|
+
- Added support for uploading packages with Core Metadata 2.5 via twine.
|
|
29
|
+
[#1321](https://github.com/pulp/pulp_python/issues/1321)
|
|
30
|
+
|
|
31
|
+
#### Bugfixes {: #3.33.0-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.1 (2026-07-29) {: #3.32.1 }
|
|
12
39
|
|
|
13
40
|
#### Bugfixes {: #3.32.1-bugfix }
|
|
@@ -59,7 +59,7 @@ def href_prn_list_handler(value):
|
|
|
59
59
|
"""Common list parsing for a string of hrefs/prns."""
|
|
60
60
|
r = re.compile(
|
|
61
61
|
rf"""
|
|
62
|
-
(?:{settings.API_ROOT}(?:[-_a-zA-Z0-9]+/)?api/
|
|
62
|
+
(?:{settings.API_ROOT}(?:[-_a-zA-Z0-9]+/)?api/v\d+/repositories/python/python/[-a-f0-9]+/)
|
|
63
63
|
|(?:prn:python\.pythonrepository:[-a-f0-9]+)
|
|
64
64
|
""",
|
|
65
65
|
re.VERBOSE,
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
("python", "0023_packageyank"),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AddField(
|
|
12
|
+
model_name="pythonrepository",
|
|
13
|
+
name="error_on_reject",
|
|
14
|
+
field=models.BooleanField(default=True),
|
|
15
|
+
),
|
|
16
|
+
]
|
|
@@ -19,6 +19,7 @@ from pulpcore.plugin.models import (
|
|
|
19
19
|
BaseModel,
|
|
20
20
|
Content,
|
|
21
21
|
Distribution,
|
|
22
|
+
ProgressReport,
|
|
22
23
|
Publication,
|
|
23
24
|
Remote,
|
|
24
25
|
Repository,
|
|
@@ -204,8 +205,6 @@ class PythonPackageContent(Content):
|
|
|
204
205
|
sha256 = models.CharField(db_index=True, max_length=64)
|
|
205
206
|
metadata_sha256 = models.CharField(max_length=64, null=True)
|
|
206
207
|
size = models.BigIntegerField(default=0)
|
|
207
|
-
# yanked and yanked_reason are not implemented because they are mutable
|
|
208
|
-
|
|
209
208
|
# From pulpcore
|
|
210
209
|
PROTECTED_FROM_RECLAIM = False
|
|
211
210
|
TYPE = "python"
|
|
@@ -289,6 +288,32 @@ class PackageProvenance(Content):
|
|
|
289
288
|
unique_together = ("sha256", "_pulp_domain")
|
|
290
289
|
|
|
291
290
|
|
|
291
|
+
class PackageYank(Content):
|
|
292
|
+
"""
|
|
293
|
+
A marker content type indicating a package version is yanked in a repository (PEP 592).
|
|
294
|
+
|
|
295
|
+
Its presence in a repository version means all files for the matching
|
|
296
|
+
(name_normalized, version) pair are yanked. Yank/unyank operations
|
|
297
|
+
add/remove this marker, creating new repository versions.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
TYPE = "python_yank"
|
|
301
|
+
repo_key_fields = ("name_normalized", "version")
|
|
302
|
+
|
|
303
|
+
name_normalized = models.TextField()
|
|
304
|
+
version = models.TextField()
|
|
305
|
+
yanked_reason = models.TextField(default="")
|
|
306
|
+
|
|
307
|
+
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
308
|
+
|
|
309
|
+
def __str__(self):
|
|
310
|
+
return f"<{self._meta.object_name}: {self.name_normalized} [{self.version}]>"
|
|
311
|
+
|
|
312
|
+
class Meta:
|
|
313
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
314
|
+
unique_together = ("name_normalized", "version", "yanked_reason", "_pulp_domain")
|
|
315
|
+
|
|
316
|
+
|
|
292
317
|
class PythonPublication(Publication, AutoAddObjPermsMixin):
|
|
293
318
|
"""
|
|
294
319
|
A Publication for PythonContent.
|
|
@@ -364,12 +389,13 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
364
389
|
"""
|
|
365
390
|
|
|
366
391
|
TYPE = "python"
|
|
367
|
-
CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
|
|
392
|
+
CONTENT_TYPES = [PythonPackageContent, PackageProvenance, PackageYank]
|
|
368
393
|
REMOTE_TYPES = [PythonRemote]
|
|
369
394
|
PULL_THROUGH_SUPPORTED = True
|
|
370
395
|
|
|
371
396
|
autopublish = models.BooleanField(default=False)
|
|
372
397
|
allow_package_substitution = models.BooleanField(default=True)
|
|
398
|
+
error_on_reject = models.BooleanField(default=True)
|
|
373
399
|
|
|
374
400
|
class Meta:
|
|
375
401
|
default_related_name = "%(app_label)s_%(model_name)s"
|
|
@@ -399,10 +425,9 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
399
425
|
"""
|
|
400
426
|
Remove duplicate packages that have the same filename.
|
|
401
427
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
Also checks newly added content against the repository's blocklist entries.
|
|
428
|
+
Enforces package substitution and blocklist policies on newly added content.
|
|
429
|
+
When error_on_reject is True (default), a ValidationError is raised and the version
|
|
430
|
+
fails. When False, rejected packages are skipped and recorded in a progress report.
|
|
406
431
|
"""
|
|
407
432
|
if not self.allow_package_substitution:
|
|
408
433
|
self._check_for_package_substitution(new_version)
|
|
@@ -412,52 +437,111 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
412
437
|
|
|
413
438
|
def _check_for_package_substitution(self, new_version):
|
|
414
439
|
"""
|
|
415
|
-
|
|
416
|
-
|
|
440
|
+
Handle packages that would replace existing packages with the same filename but a
|
|
441
|
+
different sha256 checksum.
|
|
442
|
+
|
|
443
|
+
When error_on_reject is True, raise a ValidationError. When False, remove the
|
|
444
|
+
newly added conflicting packages from the version and record them in a progress report.
|
|
417
445
|
"""
|
|
418
446
|
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
|
|
419
447
|
duplicates = collect_duplicates(qs, ("filename",))
|
|
420
|
-
if duplicates:
|
|
448
|
+
if not duplicates:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if self.error_on_reject:
|
|
421
452
|
raise ValidationError(
|
|
422
453
|
"Found duplicate packages being added with the same filename but different "
|
|
423
454
|
"checksums. To allow this, set 'allow_package_substitution' to True on the "
|
|
424
455
|
f"repository. Conflicting packages: {duplicates}"
|
|
425
456
|
)
|
|
426
457
|
|
|
458
|
+
added_content = PythonPackageContent.objects.filter(
|
|
459
|
+
pk__in=new_version.added(base_version=new_version.base_version)
|
|
460
|
+
)
|
|
461
|
+
added_pks = {str(pkg.pk): pkg.filename for pkg in added_content.only("pk", "filename")}
|
|
462
|
+
to_remove_pks = []
|
|
463
|
+
messages = []
|
|
464
|
+
# Skip every newly added package in a conflicting filename group, including when
|
|
465
|
+
# multiple new packages share a filename and none of them remain in the version.
|
|
466
|
+
for dup in duplicates:
|
|
467
|
+
for pk in dup.duplicate_pks:
|
|
468
|
+
if pk in added_pks:
|
|
469
|
+
to_remove_pks.append(pk)
|
|
470
|
+
messages.append(f"{added_pks[pk]} ({pk})")
|
|
471
|
+
|
|
472
|
+
if to_remove_pks:
|
|
473
|
+
new_version.remove_content(PythonPackageContent.objects.filter(pk__in=to_remove_pks))
|
|
474
|
+
self._report_rejected_packages(
|
|
475
|
+
messages,
|
|
476
|
+
message="Skipping packages rejected by package substitution policy",
|
|
477
|
+
code="python.reject.substitution",
|
|
478
|
+
)
|
|
479
|
+
|
|
427
480
|
def _check_blocklist(self, new_version):
|
|
428
481
|
"""
|
|
429
482
|
Check newly added content in a repository version against the blocklist.
|
|
483
|
+
|
|
484
|
+
When error_on_reject is True, raise a ValidationError. When False, remove the
|
|
485
|
+
blocklisted packages from the version and record them in a progress report.
|
|
430
486
|
"""
|
|
431
487
|
added_content = PythonPackageContent.objects.filter(
|
|
432
|
-
pk__in=new_version.added(
|
|
433
|
-
).only("filename", "name_normalized", "version")
|
|
434
|
-
if added_content.exists():
|
|
435
|
-
|
|
488
|
+
pk__in=new_version.added(base_version=new_version.base_version)
|
|
489
|
+
).only("pk", "filename", "name_normalized", "version")
|
|
490
|
+
if not added_content.exists():
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
blocked = self.find_blocklisted_packages(added_content)
|
|
494
|
+
if not blocked:
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
if self.error_on_reject:
|
|
498
|
+
raise ValidationError(
|
|
499
|
+
"Blocklisted packages cannot be added to this repository: {}".format(
|
|
500
|
+
", ".join(pkg.filename for pkg in blocked)
|
|
501
|
+
)
|
|
502
|
+
)
|
|
436
503
|
|
|
437
|
-
|
|
504
|
+
new_version.remove_content(
|
|
505
|
+
PythonPackageContent.objects.filter(pk__in=[p.pk for p in blocked])
|
|
506
|
+
)
|
|
507
|
+
self._report_rejected_packages(
|
|
508
|
+
[f"{pkg.filename} ({pkg.pk})" for pkg in blocked],
|
|
509
|
+
message="Skipping packages rejected by blocklist policy",
|
|
510
|
+
code="python.reject.blocklist",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
def find_blocklisted_packages(self, packages):
|
|
438
514
|
"""
|
|
439
|
-
|
|
515
|
+
Return the packages from `packages` that match a blocklist entry.
|
|
440
516
|
"""
|
|
441
|
-
entries = PythonBlocklistEntry.objects.filter(repository=self)
|
|
442
|
-
if not entries
|
|
443
|
-
return
|
|
517
|
+
entries = list(PythonBlocklistEntry.objects.filter(repository=self))
|
|
518
|
+
if not entries:
|
|
519
|
+
return []
|
|
444
520
|
|
|
445
521
|
blocked = []
|
|
446
522
|
for pkg in packages:
|
|
447
523
|
for entry in entries:
|
|
448
524
|
if entry.filename and entry.filename == pkg.filename:
|
|
449
|
-
blocked.append(pkg
|
|
525
|
+
blocked.append(pkg)
|
|
450
526
|
break
|
|
451
527
|
if entry.name == pkg.name_normalized:
|
|
452
528
|
if not entry.version or entry.version == pkg.version:
|
|
453
|
-
blocked.append(pkg
|
|
529
|
+
blocked.append(pkg)
|
|
454
530
|
break
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
531
|
+
return blocked
|
|
532
|
+
|
|
533
|
+
def _report_rejected_packages(self, details, message, code):
|
|
534
|
+
"""
|
|
535
|
+
Record skipped packages in a task progress report.
|
|
536
|
+
"""
|
|
537
|
+
log.info("%s (%s package(s))", message, len(details))
|
|
538
|
+
with ProgressReport(
|
|
539
|
+
message=message,
|
|
540
|
+
code=code,
|
|
541
|
+
total=len(details),
|
|
542
|
+
suffix="; ".join(details),
|
|
543
|
+
) as pb:
|
|
544
|
+
pb.increase_by(len(details))
|
|
461
545
|
|
|
462
546
|
|
|
463
547
|
class PythonBlocklistEntry(BaseModel):
|
|
@@ -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)
|
|
@@ -73,6 +73,17 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
73
73
|
default=True,
|
|
74
74
|
required=False,
|
|
75
75
|
)
|
|
76
|
+
error_on_reject = serializers.BooleanField(
|
|
77
|
+
help_text=_(
|
|
78
|
+
"Whether to fail the entire repository version when packages are rejected by the "
|
|
79
|
+
"package substitution or blocklist policies. When True (the default), a ValidationError "
|
|
80
|
+
"is raised and no packages from the request are added. When False, rejected packages "
|
|
81
|
+
"are skipped and remaining packages are added; skipped packages are recorded in a "
|
|
82
|
+
"task progress report."
|
|
83
|
+
),
|
|
84
|
+
default=True,
|
|
85
|
+
required=False,
|
|
86
|
+
)
|
|
76
87
|
|
|
77
88
|
def get_blocklist_entries_href(self, obj):
|
|
78
89
|
repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.pk})
|
|
@@ -82,6 +93,7 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
82
93
|
fields = core_serializers.RepositorySerializer.Meta.fields + (
|
|
83
94
|
"autopublish",
|
|
84
95
|
"allow_package_substitution",
|
|
96
|
+
"error_on_reject",
|
|
85
97
|
"blocklist_entries_href",
|
|
86
98
|
)
|
|
87
99
|
model = python_models.PythonRepository
|
|
@@ -683,6 +695,25 @@ class PackageProvenanceSerializer(core_serializers.NoArtifactContentUploadSerial
|
|
|
683
695
|
model = python_models.PackageProvenance
|
|
684
696
|
|
|
685
697
|
|
|
698
|
+
class PackageYankSerializer(core_serializers.NoArtifactContentSerializer):
|
|
699
|
+
"""
|
|
700
|
+
Read-only serializer for PackageYank content units (PEP 592).
|
|
701
|
+
Used by PackageYankViewSet to expose yank markers via the Pulp REST API.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
name_normalized = serializers.CharField(read_only=True)
|
|
705
|
+
version = serializers.CharField(read_only=True)
|
|
706
|
+
yanked_reason = serializers.CharField(read_only=True)
|
|
707
|
+
|
|
708
|
+
class Meta:
|
|
709
|
+
fields = core_serializers.NoArtifactContentSerializer.Meta.fields + (
|
|
710
|
+
"name_normalized",
|
|
711
|
+
"version",
|
|
712
|
+
"yanked_reason",
|
|
713
|
+
)
|
|
714
|
+
model = python_models.PackageYank
|
|
715
|
+
|
|
716
|
+
|
|
686
717
|
class MultipleChoiceArrayField(serializers.MultipleChoiceField):
|
|
687
718
|
"""
|
|
688
719
|
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
|
+
)
|