1919 BaseModel ,
2020 Content ,
2121 Distribution ,
22+ ProgressReport ,
2223 Publication ,
2324 Remote ,
2425 Repository ,
@@ -370,6 +371,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
370371
371372 autopublish = models .BooleanField (default = False )
372373 allow_package_substitution = models .BooleanField (default = True )
374+ error_on_reject = models .BooleanField (default = True )
373375
374376 class Meta :
375377 default_related_name = "%(app_label)s_%(model_name)s"
@@ -400,7 +402,8 @@ def finalize_new_version(self, new_version):
400402 Remove duplicate packages that have the same filename.
401403
402404 When allow_package_substitution is False, reject any new version that would implicitly
403- replace existing content with different checksums (content substitution).
405+ replace existing content with different checksums (content substitution), unless
406+ error_on_reject is False, in which case the conflicting packages are skipped.
404407
405408 Also checks newly added content against the repository's blocklist entries.
406409 """
@@ -412,53 +415,144 @@ def finalize_new_version(self, new_version):
412415
413416 def _check_for_package_substitution (self , new_version ):
414417 """
415- Raise a ValidationError if newly added packages would replace existing packages
416- that have the same filename but a different sha256 checksum.
418+ Handle packages that would replace existing packages with the same filename but a
419+ different sha256 checksum.
420+
421+ When error_on_reject is True, raise a ValidationError. When False, remove the
422+ newly added conflicting packages from the version and record them in a progress report.
417423 """
418424 qs = PythonPackageContent .objects .filter (pk__in = new_version .content )
419425 duplicates = collect_duplicates (qs , ("filename" ,))
420- if duplicates :
426+ if not duplicates :
427+ return
428+
429+ if self .error_on_reject :
421430 raise ValidationError (
422431 "Found duplicate packages being added with the same filename but different "
423432 "checksums. To allow this, set 'allow_package_substitution' to True on the "
424433 f"repository. Conflicting packages: { duplicates } "
425434 )
426435
436+ added_pks = {
437+ str (pk )
438+ for pk in new_version .added (base_version = new_version .base_version ).values_list (
439+ "pk" , flat = True
440+ )
441+ }
442+ to_remove_pks = []
443+ messages = []
444+ for dup in duplicates :
445+ filename = dup .keyset_value [0 ]
446+ pkgs = {
447+ str (pkg .pk ): pkg
448+ for pkg in PythonPackageContent .objects .filter (pk__in = dup .duplicate_pks ).only (
449+ "pk" , "filename" , "sha256"
450+ )
451+ }
452+ existing = [pkgs [pk ] for pk in dup .duplicate_pks if pk not in added_pks ]
453+ added = [pkgs [pk ] for pk in dup .duplicate_pks if pk in added_pks ]
454+ if existing :
455+ kept_sha256 = existing [0 ].sha256
456+ for pkg in added :
457+ to_remove_pks .append (pkg .pk )
458+ messages .append (
459+ f"{ filename } (existing sha256={ kept_sha256 } , rejected sha256={ pkg .sha256 } )"
460+ )
461+ elif len (added ) > 1 :
462+ kept = added [0 ]
463+ for pkg in added [1 :]:
464+ to_remove_pks .append (pkg .pk )
465+ messages .append (
466+ f"{ filename } (kept sha256={ kept .sha256 } , rejected sha256={ pkg .sha256 } )"
467+ )
468+
469+ if to_remove_pks :
470+ new_version .remove_content (PythonPackageContent .objects .filter (pk__in = to_remove_pks ))
471+ self ._report_rejected_packages (
472+ messages ,
473+ message = "Skipping packages rejected by package substitution policy" ,
474+ code = "python.reject.substitution" ,
475+ )
476+
427477 def _check_blocklist (self , new_version ):
428478 """
429479 Check newly added content in a repository version against the blocklist.
480+
481+ When error_on_reject is True, raise a ValidationError. When False, remove the
482+ blocklisted packages from the version and record them in a progress report.
430483 """
431484 added_content = PythonPackageContent .objects .filter (
432485 pk__in = new_version .added ().values_list ("pk" , flat = True )
433- ).only ("filename" , "name_normalized" , "version" )
434- if added_content .exists ():
435- self . check_blocklist_for_packages ( added_content )
486+ ).only ("pk" , " filename" , "name_normalized" , "version" )
487+ if not added_content .exists ():
488+ return
436489
437- def check_blocklist_for_packages (self , packages ):
490+ blocked = self .find_blocklisted_packages (added_content )
491+ if not blocked :
492+ return
493+
494+ if self .error_on_reject :
495+ raise ValidationError (
496+ "Blocklisted packages cannot be added to this repository: {}" .format (
497+ ", " .join (pkg .filename for pkg in blocked )
498+ )
499+ )
500+
501+ new_version .remove_content (
502+ PythonPackageContent .objects .filter (pk__in = [p .pk for p in blocked ])
503+ )
504+ self ._report_rejected_packages (
505+ [pkg .filename for pkg in blocked ],
506+ message = "Skipping packages rejected by blocklist policy" ,
507+ code = "python.reject.blocklist" ,
508+ )
509+
510+ def find_blocklisted_packages (self , packages ):
438511 """
439- Raise a ValidationError if any of the given packages match a blocklist entry.
512+ Return the packages from `` packages`` that match a blocklist entry.
440513 """
441- entries = PythonBlocklistEntry .objects .filter (repository = self )
442- if not entries . exists () :
443- return
514+ entries = list ( PythonBlocklistEntry .objects .filter (repository = self ) )
515+ if not entries :
516+ return []
444517
445518 blocked = []
446519 for pkg in packages :
447520 for entry in entries :
448521 if entry .filename and entry .filename == pkg .filename :
449- blocked .append (pkg . filename )
522+ blocked .append (pkg )
450523 break
451524 if entry .name == pkg .name_normalized :
452525 if not entry .version or entry .version == pkg .version :
453- blocked .append (pkg . filename )
526+ blocked .append (pkg )
454527 break
528+ return blocked
529+
530+ def check_blocklist_for_packages (self , packages ):
531+ """
532+ Raise a ValidationError if any of the given packages match a blocklist entry.
533+ """
534+ blocked = self .find_blocklisted_packages (packages )
455535 if blocked :
456536 raise ValidationError (
457537 "Blocklisted packages cannot be added to this repository: {}" .format (
458- ", " .join (blocked )
538+ ", " .join (pkg . filename for pkg in blocked )
459539 )
460540 )
461541
542+ def _report_rejected_packages (self , details , message , code ):
543+ """
544+ Record skipped packages in a task progress report.
545+ """
546+ suffix = "; " .join (details )
547+ log .info ("%s: %s" , message , suffix )
548+ with ProgressReport (
549+ message = message ,
550+ code = code ,
551+ total = len (details ),
552+ suffix = suffix ,
553+ ) as pb :
554+ pb .increase_by (len (details ))
555+
462556
463557class PythonBlocklistEntry (BaseModel ):
464558 """
0 commit comments