pulp-python 3.33.0__tar.gz → 3.35.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.33.0 → pulp_python-3.35.0}/CHANGES.md +18 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/PKG-INFO +1 -1
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/__init__.py +1 -1
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/management/commands/repair-python-metadata.py +1 -1
- pulp_python-3.35.0/pulp_python/app/migrations/0024_pythonrepository_error_on_reject.py +16 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/models.py +84 -24
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/pypi/views.py +22 -5
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/serializers.py +12 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/viewsets.py +21 -13
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_blocklist.py +51 -1
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +56 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_domains.py +1 -1
- pulp_python-3.35.0/pulp_python/tests/functional/api/test_simple_cache.py +314 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/PKG-INFO +1 -1
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/SOURCES.txt +1 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pyproject.toml +2 -2
- pulp_python-3.33.0/pulp_python/tests/functional/api/test_simple_cache.py +0 -141
- {pulp_python-3.33.0 → pulp_python-3.35.0}/COMMITMENT +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/COPYRIGHT +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/LICENSE +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/MANIFEST.in +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/README.md +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/functest_requirements.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/cache.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/exceptions.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0022_pythonblocklistentry.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/0023_packageyank.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/provenance.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/tasks/yank.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/urls.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/utils.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_version_specifier_filter.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_yank.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/requires.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/setup.cfg +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/test_requirements.txt +0 -0
- {pulp_python-3.33.0 → pulp_python-3.35.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.35.0 (2026-08-26) {: #3.35.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.35.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added conditional request support (`Last-Modified` / `If-Modified-Since` and `ETag` / `If-None-Match`) to the Simple API and JSON Metadata API for improved cache efficiency.
|
|
16
|
+
[#1338](https://github.com/pulp/pulp_python/issues/1338)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3.34.0 (2026-08-18) {: #3.34.0 }
|
|
21
|
+
|
|
22
|
+
#### Features {: #3.34.0-feature }
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
[#1278](https://github.com/pulp/pulp_python/issues/1278)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
11
29
|
## 3.33.0 (2026-08-07) {: #3.33.0 }
|
|
12
30
|
|
|
13
31
|
#### Features {: #3.33.0-feature }
|
|
@@ -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,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,
|
|
@@ -394,6 +395,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
394
395
|
|
|
395
396
|
autopublish = models.BooleanField(default=False)
|
|
396
397
|
allow_package_substitution = models.BooleanField(default=True)
|
|
398
|
+
error_on_reject = models.BooleanField(default=True)
|
|
397
399
|
|
|
398
400
|
class Meta:
|
|
399
401
|
default_related_name = "%(app_label)s_%(model_name)s"
|
|
@@ -423,10 +425,9 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
423
425
|
"""
|
|
424
426
|
Remove duplicate packages that have the same filename.
|
|
425
427
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
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.
|
|
430
431
|
"""
|
|
431
432
|
if not self.allow_package_substitution:
|
|
432
433
|
self._check_for_package_substitution(new_version)
|
|
@@ -436,52 +437,111 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
436
437
|
|
|
437
438
|
def _check_for_package_substitution(self, new_version):
|
|
438
439
|
"""
|
|
439
|
-
|
|
440
|
-
|
|
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.
|
|
441
445
|
"""
|
|
442
446
|
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
|
|
443
447
|
duplicates = collect_duplicates(qs, ("filename",))
|
|
444
|
-
if duplicates:
|
|
448
|
+
if not duplicates:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if self.error_on_reject:
|
|
445
452
|
raise ValidationError(
|
|
446
453
|
"Found duplicate packages being added with the same filename but different "
|
|
447
454
|
"checksums. To allow this, set 'allow_package_substitution' to True on the "
|
|
448
455
|
f"repository. Conflicting packages: {duplicates}"
|
|
449
456
|
)
|
|
450
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
|
+
|
|
451
480
|
def _check_blocklist(self, new_version):
|
|
452
481
|
"""
|
|
453
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.
|
|
454
486
|
"""
|
|
455
487
|
added_content = PythonPackageContent.objects.filter(
|
|
456
|
-
pk__in=new_version.added(
|
|
457
|
-
).only("filename", "name_normalized", "version")
|
|
458
|
-
if added_content.exists():
|
|
459
|
-
|
|
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
|
+
)
|
|
460
503
|
|
|
461
|
-
|
|
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):
|
|
462
514
|
"""
|
|
463
|
-
|
|
515
|
+
Return the packages from `packages` that match a blocklist entry.
|
|
464
516
|
"""
|
|
465
|
-
entries = PythonBlocklistEntry.objects.filter(repository=self)
|
|
466
|
-
if not entries
|
|
467
|
-
return
|
|
517
|
+
entries = list(PythonBlocklistEntry.objects.filter(repository=self))
|
|
518
|
+
if not entries:
|
|
519
|
+
return []
|
|
468
520
|
|
|
469
521
|
blocked = []
|
|
470
522
|
for pkg in packages:
|
|
471
523
|
for entry in entries:
|
|
472
524
|
if entry.filename and entry.filename == pkg.filename:
|
|
473
|
-
blocked.append(pkg
|
|
525
|
+
blocked.append(pkg)
|
|
474
526
|
break
|
|
475
527
|
if entry.name == pkg.name_normalized:
|
|
476
528
|
if not entry.version or entry.version == pkg.version:
|
|
477
|
-
blocked.append(pkg
|
|
529
|
+
blocked.append(pkg)
|
|
478
530
|
break
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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))
|
|
485
545
|
|
|
486
546
|
|
|
487
547
|
class PythonBlocklistEntry(BaseModel):
|
|
@@ -72,17 +72,32 @@ PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
|
72
72
|
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
"""
|
|
75
|
+
def _get_repo_version(path):
|
|
76
|
+
"""Resolve path to a RepositoryVersion, or None if not found."""
|
|
77
77
|
try:
|
|
78
78
|
distro = PyPIMixin.get_distribution(path)
|
|
79
|
-
|
|
79
|
+
return PyPIMixin.get_repository_version(distro)
|
|
80
80
|
except Http404:
|
|
81
81
|
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _etag_func(request, path, **kwargs):
|
|
85
|
+
"""Compute unquoted ETag for the condition decorator. Returns None if no repo."""
|
|
86
|
+
repo_ver = _get_repo_version(path)
|
|
87
|
+
if repo_ver is None:
|
|
88
|
+
return None
|
|
82
89
|
raw = f"{repo_ver.number}:{repo_ver.pulp_created.isoformat()}"
|
|
83
90
|
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
84
91
|
|
|
85
92
|
|
|
93
|
+
def _last_modified_func(request, path, **kwargs):
|
|
94
|
+
"""Return the repository version creation timestamp for Last-Modified."""
|
|
95
|
+
repo_ver = _get_repo_version(path)
|
|
96
|
+
if repo_ver is None:
|
|
97
|
+
return None
|
|
98
|
+
return repo_ver.pulp_created
|
|
99
|
+
|
|
100
|
+
|
|
86
101
|
class PyPISimpleHTMLRenderer(TemplateHTMLRenderer):
|
|
87
102
|
media_type = PYPI_SIMPLE_V1_HTML
|
|
88
103
|
|
|
@@ -317,7 +332,7 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
317
332
|
|
|
318
333
|
@extend_schema(summary="Get index simple page")
|
|
319
334
|
@method_decorator(cache_control(max_age=600, public=True))
|
|
320
|
-
@method_decorator(condition(etag_func=_etag_func))
|
|
335
|
+
@method_decorator(condition(etag_func=_etag_func, last_modified_func=_last_modified_func))
|
|
321
336
|
@PythonApiCache(base_key=find_base_path_cached)
|
|
322
337
|
def list(self, request, path):
|
|
323
338
|
"""Gets the simple api html page for the index."""
|
|
@@ -379,7 +394,7 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
379
394
|
|
|
380
395
|
@extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
|
|
381
396
|
@method_decorator(cache_control(max_age=600, public=True))
|
|
382
|
-
@method_decorator(condition(etag_func=_etag_func))
|
|
397
|
+
@method_decorator(condition(etag_func=_etag_func, last_modified_func=_last_modified_func))
|
|
383
398
|
@PythonApiCache(base_key=find_base_path_cached)
|
|
384
399
|
def retrieve(self, request, path, package):
|
|
385
400
|
"""Retrieves the simple api html/json page for a package."""
|
|
@@ -482,6 +497,8 @@ class MetadataView(PyPIMixin, ViewSet):
|
|
|
482
497
|
responses={200: PackageMetadataSerializer},
|
|
483
498
|
summary="Get package metadata",
|
|
484
499
|
)
|
|
500
|
+
@method_decorator(cache_control(max_age=900, public=True))
|
|
501
|
+
@method_decorator(condition(etag_func=_etag_func, last_modified_func=_last_modified_func))
|
|
485
502
|
def retrieve(self, request, path, meta):
|
|
486
503
|
"""
|
|
487
504
|
Retrieves the package's core-metadata specified by
|
|
@@ -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
|
|
@@ -144,23 +144,25 @@ class PythonRepositoryViewSet(
|
|
|
144
144
|
responses={202: AsyncOperationResponseSerializer},
|
|
145
145
|
)
|
|
146
146
|
@action(detail=True, methods=["post"], serializer_class=RepositoryAddRemoveContentSerializer)
|
|
147
|
-
def modify(self, request, pk):
|
|
147
|
+
def modify(self, request, pk, **kwargs):
|
|
148
148
|
"""
|
|
149
149
|
Queues a task that creates a new RepositoryVersion by adding and removing content units.
|
|
150
150
|
|
|
151
|
-
If allow_package_substitution is False and the request is
|
|
152
|
-
package substitution check is performed to provide a
|
|
153
|
-
check is delegated to the task.
|
|
151
|
+
If allow_package_substitution is False, error_on_reject is True, and the request is
|
|
152
|
+
**only** adding packages, then a package substitution check is performed to provide a
|
|
153
|
+
quicker error response. Otherwise, the check is delegated to the task.
|
|
154
154
|
|
|
155
|
-
Also performs an early blocklist check on added packages.
|
|
155
|
+
Also performs an early blocklist check on added packages when error_on_reject is True.
|
|
156
|
+
When error_on_reject is False, rejected packages are skipped during task finalization.
|
|
156
157
|
"""
|
|
157
158
|
repository = self.get_object()
|
|
158
159
|
add_content_units = request.data.get("add_content_units", [])
|
|
159
160
|
content_ids = [extract_pk(x) for x in add_content_units]
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
if repository.error_on_reject:
|
|
163
|
+
self._early_blocklist_check(repository, content_ids)
|
|
162
164
|
|
|
163
|
-
if not repository.allow_package_substitution:
|
|
165
|
+
if not repository.allow_package_substitution and repository.error_on_reject:
|
|
164
166
|
remove_content_units = request.data.get("remove_content_units", [])
|
|
165
167
|
if remove_content_units or "base_version" in request.data:
|
|
166
168
|
return super().modify(request, pk)
|
|
@@ -189,14 +191,20 @@ class PythonRepositoryViewSet(
|
|
|
189
191
|
packages = python_models.PythonPackageContent.objects.filter(pk__in=content_ids).only(
|
|
190
192
|
"filename", "name_normalized", "version"
|
|
191
193
|
)
|
|
192
|
-
repository.
|
|
194
|
+
blocked = repository.find_blocklisted_packages(packages)
|
|
195
|
+
if blocked:
|
|
196
|
+
raise ValidationError(
|
|
197
|
+
"Blocklisted packages cannot be added to this repository: {}".format(
|
|
198
|
+
", ".join(pkg.filename for pkg in blocked)
|
|
199
|
+
)
|
|
200
|
+
)
|
|
193
201
|
|
|
194
202
|
@extend_schema(
|
|
195
203
|
summary="Repair metadata",
|
|
196
204
|
responses={202: AsyncOperationResponseSerializer},
|
|
197
205
|
)
|
|
198
206
|
@action(detail=True, methods=["post"], serializer_class=None)
|
|
199
|
-
def repair_metadata(self, request, pk):
|
|
207
|
+
def repair_metadata(self, request, pk, **kwargs):
|
|
200
208
|
"""
|
|
201
209
|
Trigger an asynchronous task to repair Python metadata. This task will repair metadata
|
|
202
210
|
of all packages for the specified `Repository`, without creating a new `RepositoryVersion`.
|
|
@@ -212,7 +220,7 @@ class PythonRepositoryViewSet(
|
|
|
212
220
|
|
|
213
221
|
@extend_schema(summary="Sync from remote", responses={202: AsyncOperationResponseSerializer})
|
|
214
222
|
@action(detail=True, methods=["post"], serializer_class=RepositorySyncURLSerializer)
|
|
215
|
-
def sync(self, request, pk):
|
|
223
|
+
def sync(self, request, pk, **kwargs):
|
|
216
224
|
"""
|
|
217
225
|
<!-- User-facing documentation, rendered as html-->
|
|
218
226
|
Trigger an asynchronous task to sync python content. The sync task will retrieve Python
|
|
@@ -576,7 +584,7 @@ class PythonPackageSingleArtifactContentUploadViewSet(
|
|
|
576
584
|
methods=["post"],
|
|
577
585
|
serializer_class=python_serializers.PythonPackageContentUploadSerializer,
|
|
578
586
|
)
|
|
579
|
-
def upload(self, request):
|
|
587
|
+
def upload(self, request, **kwargs):
|
|
580
588
|
"""
|
|
581
589
|
Create a Python package.
|
|
582
590
|
"""
|
|
@@ -748,7 +756,7 @@ class PythonRemoteViewSet(core_viewsets.RemoteViewSet, core_viewsets.RolesMixin)
|
|
|
748
756
|
methods=["post"],
|
|
749
757
|
serializer_class=python_serializers.PythonBanderRemoteSerializer,
|
|
750
758
|
)
|
|
751
|
-
def from_bandersnatch(self, request):
|
|
759
|
+
def from_bandersnatch(self, request, **kwargs):
|
|
752
760
|
"""
|
|
753
761
|
<!-- User-facing documentation, rendered as html-->
|
|
754
762
|
Takes the fields specified in the Bandersnatch config and creates a Python Remote from it.
|
|
@@ -875,7 +883,7 @@ class PythonPublicationViewSet(core_viewsets.PublicationViewSet, core_viewsets.R
|
|
|
875
883
|
}
|
|
876
884
|
|
|
877
885
|
@extend_schema(responses={202: AsyncOperationResponseSerializer}, deprecated=True)
|
|
878
|
-
def create(self, request):
|
|
886
|
+
def create(self, request, **kwargs):
|
|
879
887
|
"""
|
|
880
888
|
<!-- User-facing documentation, rendered as html-->
|
|
881
889
|
Dispatches a publish task, which generates metadata that will be used by pip.
|
{pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_blocklist.py
RENAMED
|
@@ -2,7 +2,12 @@ import pytest
|
|
|
2
2
|
|
|
3
3
|
from pulpcore.tests.functional.utils import PulpTaskError
|
|
4
4
|
|
|
5
|
-
from pulp_python.tests.functional.constants import
|
|
5
|
+
from pulp_python.tests.functional.constants import (
|
|
6
|
+
PYTHON_EGG_FILENAME,
|
|
7
|
+
PYTHON_EGG_URL,
|
|
8
|
+
PYTHON_WHEEL_FILENAME,
|
|
9
|
+
PYTHON_WHEEL_URL,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
12
|
CONTENT_BODY = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
|
|
8
13
|
BLOCKED_MSG = "Blocklisted packages cannot be added to this repository"
|
|
@@ -150,3 +155,48 @@ def test_modify_blocked(monitor_task, python_bindings, python_repo):
|
|
|
150
155
|
|
|
151
156
|
repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
|
|
152
157
|
assert repo.latest_version_href.endswith("/0/")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parallel
|
|
161
|
+
def test_error_on_reject_false_skips_blocklisted(
|
|
162
|
+
monitor_task, python_bindings, python_repo_factory
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
When error_on_reject=False, blocklisted packages in a batch modify are skipped while
|
|
166
|
+
non-blocklisted packages are still added.
|
|
167
|
+
"""
|
|
168
|
+
repo = python_repo_factory(error_on_reject=False)
|
|
169
|
+
python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
|
|
170
|
+
repo.pulp_href,
|
|
171
|
+
python_bindings.PythonPythonBlocklistEntry(filename=PYTHON_EGG_FILENAME),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
response = python_bindings.ContentPackagesApi.create(**CONTENT_BODY)
|
|
175
|
+
blocked = python_bindings.ContentPackagesApi.read(
|
|
176
|
+
monitor_task(response.task).created_resources[0]
|
|
177
|
+
)
|
|
178
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
179
|
+
relative_path=PYTHON_WHEEL_FILENAME, file_url=PYTHON_WHEEL_URL
|
|
180
|
+
)
|
|
181
|
+
allowed = python_bindings.ContentPackagesApi.read(
|
|
182
|
+
monitor_task(response.task).created_resources[0]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
body = {"add_content_units": [blocked.pulp_href, allowed.pulp_href]}
|
|
186
|
+
task = monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
187
|
+
|
|
188
|
+
reports = {report.code: report for report in task.progress_reports}
|
|
189
|
+
assert "python.reject.blocklist" in reports
|
|
190
|
+
report = reports["python.reject.blocklist"]
|
|
191
|
+
assert report.done == 1
|
|
192
|
+
assert PYTHON_EGG_FILENAME in report.suffix
|
|
193
|
+
assert blocked.prn.split(":")[-1] in report.suffix
|
|
194
|
+
|
|
195
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
196
|
+
content_list = python_bindings.ContentPackagesApi.list(
|
|
197
|
+
repository_version=repo.latest_version_href
|
|
198
|
+
)
|
|
199
|
+
hrefs = {c.pulp_href for c in content_list.results}
|
|
200
|
+
assert allowed.pulp_href in hrefs
|
|
201
|
+
assert blocked.pulp_href not in hrefs
|
|
202
|
+
assert content_list.count == 1
|
{pulp_python-3.33.0 → pulp_python-3.35.0}/pulp_python/tests/functional/api/test_crud_content_unit.py
RENAMED
|
@@ -323,3 +323,59 @@ def test_package_substitution_allowed_by_default(
|
|
|
323
323
|
)
|
|
324
324
|
assert content_list.count == 1
|
|
325
325
|
assert content_list.results[0].sha256 == content2.sha256
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_error_on_reject_false_skips_substitution(
|
|
329
|
+
monitor_task,
|
|
330
|
+
python_bindings,
|
|
331
|
+
python_repo_factory,
|
|
332
|
+
):
|
|
333
|
+
"""
|
|
334
|
+
When error_on_reject=False and allow_package_substitution=False, conflicting packages
|
|
335
|
+
in a batch modify are skipped while non-conflicting packages are still added.
|
|
336
|
+
"""
|
|
337
|
+
repo = python_repo_factory(allow_package_substitution=False, error_on_reject=False)
|
|
338
|
+
assert repo.error_on_reject is False
|
|
339
|
+
|
|
340
|
+
content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
|
|
341
|
+
response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
|
|
342
|
+
task = monitor_task(response.task)
|
|
343
|
+
original = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
|
|
344
|
+
|
|
345
|
+
# Conflicting package: same filename, different checksum
|
|
346
|
+
conflict_filename = "pip-26.0.1.tar.gz"
|
|
347
|
+
conflict_url = get_package_url("pip", conflict_filename)
|
|
348
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
349
|
+
relative_path=PYTHON_EGG_FILENAME, file_url=conflict_url
|
|
350
|
+
)
|
|
351
|
+
conflict = python_bindings.ContentPackagesApi.read(
|
|
352
|
+
monitor_task(response.task).created_resources[0]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Non-conflicting package
|
|
356
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
357
|
+
relative_path=PYTHON_WHEEL_FILENAME, file_url=PYTHON_WHEEL_URL
|
|
358
|
+
)
|
|
359
|
+
wheel = python_bindings.ContentPackagesApi.read(
|
|
360
|
+
monitor_task(response.task).created_resources[0]
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
body = {"add_content_units": [conflict.pulp_href, wheel.pulp_href]}
|
|
364
|
+
task = monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
365
|
+
|
|
366
|
+
reports = {report.code: report for report in task.progress_reports}
|
|
367
|
+
assert "python.reject.substitution" in reports
|
|
368
|
+
report = reports["python.reject.substitution"]
|
|
369
|
+
assert report.done == 1
|
|
370
|
+
assert PYTHON_EGG_FILENAME in report.suffix
|
|
371
|
+
assert conflict.prn.split(":")[-1] in report.suffix
|
|
372
|
+
|
|
373
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
374
|
+
content_list = python_bindings.ContentPackagesApi.list(
|
|
375
|
+
repository_version=repo.latest_version_href
|
|
376
|
+
)
|
|
377
|
+
hrefs = {c.pulp_href for c in content_list.results}
|
|
378
|
+
assert original.pulp_href in hrefs
|
|
379
|
+
assert wheel.pulp_href in hrefs
|
|
380
|
+
assert conflict.pulp_href not in hrefs
|
|
381
|
+
assert content_list.count == 2
|
|
@@ -29,7 +29,7 @@ def test_domain_object_creation(
|
|
|
29
29
|
domain_name = domain.name
|
|
30
30
|
|
|
31
31
|
repo = python_repo_factory(pulp_domain=domain_name)
|
|
32
|
-
assert f"{domain_name}/api/
|
|
32
|
+
assert f"{domain_name}/api/" in repo.pulp_href
|
|
33
33
|
|
|
34
34
|
repos = python_bindings.RepositoriesPythonApi.list(pulp_domain=domain_name)
|
|
35
35
|
assert repos.count == 1
|