pulp-python 3.33.0__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.
Files changed (103) hide show
  1. {pulp_python-3.33.0 → pulp_python-3.34.0}/CHANGES.md +9 -0
  2. {pulp_python-3.33.0 → pulp_python-3.34.0}/PKG-INFO +1 -1
  3. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/__init__.py +1 -1
  4. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/management/commands/repair-python-metadata.py +1 -1
  5. pulp_python-3.34.0/pulp_python/app/migrations/0024_pythonrepository_error_on_reject.py +16 -0
  6. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/models.py +84 -24
  7. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/serializers.py +12 -0
  8. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/viewsets.py +21 -13
  9. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_blocklist.py +51 -1
  10. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +56 -0
  11. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_domains.py +1 -1
  12. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/PKG-INFO +1 -1
  13. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/SOURCES.txt +1 -0
  14. {pulp_python-3.33.0 → pulp_python-3.34.0}/pyproject.toml +2 -2
  15. {pulp_python-3.33.0 → pulp_python-3.34.0}/COMMITMENT +0 -0
  16. {pulp_python-3.33.0 → pulp_python-3.34.0}/COPYRIGHT +0 -0
  17. {pulp_python-3.33.0 → pulp_python-3.34.0}/LICENSE +0 -0
  18. {pulp_python-3.33.0 → pulp_python-3.34.0}/MANIFEST.in +0 -0
  19. {pulp_python-3.33.0 → pulp_python-3.34.0}/README.md +0 -0
  20. {pulp_python-3.33.0 → pulp_python-3.34.0}/functest_requirements.txt +0 -0
  21. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/__init__.py +0 -0
  22. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/cache.py +0 -0
  23. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/exceptions.py +0 -0
  24. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/global_access_conditions.py +0 -0
  25. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/management/__init__.py +0 -0
  26. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/management/commands/__init__.py +0 -0
  27. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  28. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  29. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  30. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  31. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  32. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  33. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  34. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  35. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  36. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  37. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  38. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  39. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  40. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  41. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  42. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  43. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  44. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
  45. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
  46. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
  47. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
  48. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
  49. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0022_pythonblocklistentry.py +0 -0
  50. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/0023_packageyank.py +0 -0
  51. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/migrations/__init__.py +0 -0
  52. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/modelresource.py +0 -0
  53. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/provenance.py +0 -0
  54. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/pypi/__init__.py +0 -0
  55. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/pypi/serializers.py +0 -0
  56. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/pypi/views.py +0 -0
  57. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/replica.py +0 -0
  58. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/settings.py +0 -0
  59. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/__init__.py +0 -0
  60. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/publish.py +0 -0
  61. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/repair.py +0 -0
  62. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/sync.py +0 -0
  63. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/upload.py +0 -0
  64. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
  65. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/tasks/yank.py +0 -0
  66. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/urls.py +0 -0
  67. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/utils.py +0 -0
  68. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  69. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  70. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  71. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/pytest_plugin.py +0 -0
  72. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/__init__.py +0 -0
  73. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/__init__.py +0 -0
  74. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  75. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
  76. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  77. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  78. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  79. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  80. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  81. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  82. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  83. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
  84. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
  85. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  86. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
  87. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_simple_cache.py +0 -0
  88. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
  89. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
  90. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_version_specifier_filter.py +0 -0
  91. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
  92. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/api/test_yank.py +0 -0
  93. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/constants.py +0 -0
  94. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/functional/utils.py +0 -0
  95. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/unit/__init__.py +0 -0
  96. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python/tests/unit/test_models.py +0 -0
  97. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  98. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/entry_points.txt +0 -0
  99. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/requires.txt +0 -0
  100. {pulp_python-3.33.0 → pulp_python-3.34.0}/pulp_python.egg-info/top_level.txt +0 -0
  101. {pulp_python-3.33.0 → pulp_python-3.34.0}/setup.cfg +0 -0
  102. {pulp_python-3.33.0 → pulp_python-3.34.0}/test_requirements.txt +0 -0
  103. {pulp_python-3.33.0 → pulp_python-3.34.0}/unittest_requirements.txt +0 -0
@@ -8,6 +8,15 @@
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
+
11
20
  ## 3.33.0 (2026-08-07) {: #3.33.0 }
12
21
 
13
22
  #### Features {: #3.33.0-feature }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.33.0
3
+ Version: 3.34.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -12,7 +12,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
12
12
 
13
13
  name = "pulp_python.app"
14
14
  label = "python"
15
- version = "3.33.0"
15
+ version = "3.34.0"
16
16
  python_package_name = "pulp-python"
17
17
  domain_compatible = True
18
18
 
@@ -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/v3/repositories/python/python/[-a-f0-9]+/)
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
- When allow_package_substitution is False, reject any new version that would implicitly
427
- replace existing content with different checksums (content substitution).
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
- Raise a ValidationError if newly added packages would replace existing packages
440
- that have the same filename but a different sha256 checksum.
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().values_list("pk", flat=True)
457
- ).only("filename", "name_normalized", "version")
458
- if added_content.exists():
459
- self.check_blocklist_for_packages(added_content)
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
- def check_blocklist_for_packages(self, packages):
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
- Raise a ValidationError if any of the given packages match a blocklist entry.
515
+ Return the packages from `packages` that match a blocklist entry.
464
516
  """
465
- entries = PythonBlocklistEntry.objects.filter(repository=self)
466
- if not entries.exists():
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.filename)
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.filename)
529
+ blocked.append(pkg)
478
530
  break
479
- if blocked:
480
- raise ValidationError(
481
- "Blocklisted packages cannot be added to this repository: {}".format(
482
- ", ".join(blocked)
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):
@@ -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 **only** adding packages, then a
152
- package substitution check is performed to provide a quicker error response. Otherwise, the
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
- self._early_blocklist_check(repository, content_ids)
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.check_blocklist_for_packages(packages)
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.
@@ -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 PYTHON_EGG_FILENAME, PYTHON_EGG_URL
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
@@ -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/v3/" in repo.pulp_href
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.33.0
3
+ Version: 3.34.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -56,6 +56,7 @@ pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py
56
56
  pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py
57
57
  pulp_python/app/migrations/0022_pythonblocklistentry.py
58
58
  pulp_python/app/migrations/0023_packageyank.py
59
+ pulp_python/app/migrations/0024_pythonrepository_error_on_reject.py
59
60
  pulp_python/app/migrations/__init__.py
60
61
  pulp_python/app/pypi/__init__.py
61
62
  pulp_python/app/pypi/serializers.py
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-python"
10
- version = "3.33.0"
10
+ version = "3.34.0"
11
11
  description = "pulp-python plugin for the Pulp Project"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -79,7 +79,7 @@ ignore = [
79
79
  [tool.bumpversion]
80
80
  # This section is managed by the plugin template. Do not edit manually.
81
81
 
82
- current_version = "3.33.0"
82
+ current_version = "3.34.0"
83
83
  commit = false
84
84
  tag = false
85
85
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes