3737from pulp_python .app .cache import PythonApiCache , find_base_path_cached
3838from pulp_python .app .models import (
3939 PackageProvenance ,
40+ PackageYank ,
4041 PythonDistribution ,
4142 PythonPackageContent ,
4243 PythonPublication ,
4647 PackageUploadSerializer ,
4748 PackageUploadTaskSerializer ,
4849 SummarySerializer ,
50+ YankSerializer ,
4951)
5052from pulp_python .app .utils import (
5153 PYPI_LAST_SERIAL ,
@@ -356,6 +358,8 @@ def parse_package(release_package):
356358 "upload_time" : release_package .upload_time ,
357359 "version" : release_package .version ,
358360 "provenance" : release_package .provenance_url ,
361+ "yanked" : release_package .is_yanked ,
362+ "yanked_reason" : release_package .yanked_reason or "" ,
359363 }
360364
361365 rfilter = get_remote_package_filter (remote )
@@ -408,6 +412,11 @@ def retrieve(self, request, path, package):
408412 "version" ,
409413 "has_provenance" ,
410414 )
415+ yank_markers = dict (
416+ PackageYank .objects .filter (
417+ pk__in = repo_ver .content , name_normalized = normalized
418+ ).values_list ("version" , "yanked_reason" )
419+ )
411420 local_releases = {
412421 p ["filename" ]: {
413422 ** p ,
@@ -418,6 +427,8 @@ def retrieve(self, request, path, package):
418427 if p ["has_provenance" ]
419428 else None
420429 ),
430+ "yanked" : p ["version" ] in yank_markers ,
431+ "yanked_reason" : yank_markers .get (p ["version" ], "" ),
421432 }
422433 for p in packages
423434 }
@@ -493,12 +504,18 @@ def retrieve(self, request, path, meta):
493504 headers = {PYPI_LAST_SERIAL : str (PYPI_SERIAL_CONSTANT )}
494505 if settings .DOMAIN_ENABLED :
495506 domain = get_domain ()
507+ yank_markers = dict (
508+ PackageYank .objects .filter (
509+ pk__in = repo_ver .content , name_normalized = normalized
510+ ).values_list ("version" , "yanked_reason" )
511+ )
496512 json_body = python_content_to_json (
497513 path ,
498514 package_content ,
499515 version = version ,
500516 domain = domain ,
501517 repository_version = repo_ver ,
518+ yank_markers = yank_markers ,
502519 )
503520 if json_body :
504521 return Response (data = json_body , headers = headers )
@@ -586,3 +603,70 @@ def retrieve(self, request, path, package, version, filename):
586603 if provenance :
587604 return Response (data = provenance .provenance )
588605 return HttpResponseNotFound (f"{ package } { version } { filename } provenance does not exist." )
606+
607+
608+ class YankView (PyPIMixin , ViewSet ):
609+ """View for yank/unyank requests (PEP 592)."""
610+
611+ endpoint_name = "yank"
612+ DEFAULT_ACCESS_POLICY = {
613+ "statements" : [
614+ {
615+ "action" : ["yank" , "unyank" ],
616+ "principal" : "authenticated" ,
617+ "effect" : "allow" ,
618+ "condition" : "index_has_repo_perm:python.modify_pythonrepository" ,
619+ },
620+ ],
621+ }
622+
623+ @extend_schema (request = YankSerializer , summary = "Yank a package version" )
624+ def yank (self , request , path ):
625+ """Yank a package version, marking all its files with data-yanked."""
626+ repo = self .distribution .repository
627+ if not repo :
628+ return HttpResponseBadRequest (reason = "Index is not pointing to a repository" )
629+
630+ serializer = YankSerializer (data = request .data )
631+ serializer .is_valid (raise_exception = True )
632+
633+ normalized = canonicalize_name (serializer .validated_data ["name" ])
634+ version = serializer .validated_data ["version" ]
635+ repo_ver = self .get_repository_version (self .distribution )
636+ if not PythonPackageContent .objects .filter (
637+ pk__in = repo_ver .content , name_normalized = normalized , version = version
638+ ).exists ():
639+ return HttpResponseNotFound (f"{ normalized } =={ version } not found in repository" )
640+
641+ result = dispatch (
642+ tasks .ayank_package ,
643+ exclusive_resources = [repo ],
644+ kwargs = {
645+ "repository_pk" : str (repo .pk ),
646+ "name" : serializer .validated_data ["name" ],
647+ "version" : serializer .validated_data ["version" ],
648+ "yanked_reason" : serializer .validated_data .get ("yanked_reason" , "" ),
649+ },
650+ )
651+ return OperationPostponedResponse (result , request )
652+
653+ @extend_schema (request = YankSerializer , summary = "Unyank a package version" )
654+ def unyank (self , request , path ):
655+ """Unyank a package version, unmarking all its files with data-yanked."""
656+ repo = self .distribution .repository
657+ if not repo :
658+ return HttpResponseBadRequest (reason = "Index is not pointing to a repository" )
659+
660+ serializer = YankSerializer (data = request .data )
661+ serializer .is_valid (raise_exception = True )
662+
663+ result = dispatch (
664+ tasks .aunyank_package ,
665+ exclusive_resources = [repo ],
666+ kwargs = {
667+ "repository_pk" : str (repo .pk ),
668+ "name" : serializer .validated_data ["name" ],
669+ "version" : serializer .validated_data ["version" ],
670+ },
671+ )
672+ return OperationPostponedResponse (result , request )
0 commit comments