3737 PythonDistribution ,
3838 PythonPackageContent ,
3939 PythonPublication ,
40+ PackageProvenance ,
4041)
4142from pulp_python .app .pypi .serializers import (
4243 SummarySerializer ,
6162
6263ORIGIN_HOST = settings .CONTENT_ORIGIN if settings .CONTENT_ORIGIN else settings .PYPI_API_HOSTNAME
6364BASE_CONTENT_URL = urljoin (ORIGIN_HOST , settings .CONTENT_PATH_PREFIX )
65+ BASE_API_URL = urljoin (settings .PYPI_API_HOSTNAME , "pypi/" )
6466
6567PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
6668PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
@@ -120,6 +122,11 @@ def get_content(repository_version):
120122 """Returns queryset of the content in this repository version."""
121123 return PythonPackageContent .objects .filter (pk__in = repository_version .content )
122124
125+ @staticmethod
126+ def get_provenances (repository_version ):
127+ """Returns queryset of the provenance for this repository version."""
128+ return PackageProvenance .objects .filter (pk__in = repository_version .content )
129+
123130 def should_redirect (self , repo_version = None ):
124131 """Checks if there is a publication the content app can serve."""
125132 if self .distribution .publication :
@@ -139,10 +146,13 @@ def get_rvc(self):
139146 def initial (self , request , * args , ** kwargs ):
140147 """Perform common initialization tasks for PyPI endpoints."""
141148 super ().initial (request , * args , ** kwargs )
149+ domain_name = get_domain ().name
142150 if settings .DOMAIN_ENABLED :
143- self .base_content_url = urljoin (BASE_CONTENT_URL , f"{ get_domain ().name } /" )
151+ self .base_content_url = urljoin (BASE_CONTENT_URL , f"{ domain_name } /" )
152+ self .base_api_url = urljoin (BASE_API_URL , f"{ domain_name } /" )
144153 else :
145154 self .base_content_url = BASE_CONTENT_URL
155+ self .base_api_url = BASE_API_URL
146156
147157 @classmethod
148158 def urlpattern (cls ):
@@ -273,6 +283,13 @@ def get_renderers(self):
273283 else :
274284 return [JSONRenderer (), BrowsableAPIRenderer ()]
275285
286+ def get_provenance_url (self , package , version , filename ):
287+ """Gets the provenance url for a package."""
288+ base_path = self .distribution .base_path
289+ return urljoin (
290+ self .base_api_url , f"{ base_path } /integrity/{ package } /{ version } /{ filename } /provenance/"
291+ )
292+
276293 @extend_schema (summary = "Get index simple page" )
277294 def list (self , request , path ):
278295 """Gets the simple api html page for the index."""
@@ -308,6 +325,7 @@ def parse_package(release_package):
308325 "size" : release_package .size ,
309326 "upload_time" : release_package .upload_time ,
310327 "version" : release_package .version ,
328+ "provenance" : release_package .provenance_url ,
311329 }
312330
313331 rfilter = get_remote_package_filter (remote )
@@ -348,7 +366,8 @@ def retrieve(self, request, path, package):
348366 elif self .should_redirect (repo_version = repo_ver ):
349367 return redirect (urljoin (self .base_content_url , f"{ path } /simple/{ normalized } /" ))
350368 if content :
351- packages = content .filter (name__normalize = normalized ).values (
369+ local_packages = content .filter (name__normalize = normalized )
370+ packages = local_packages .values (
352371 "filename" ,
353372 "sha256" ,
354373 "metadata_sha256" ,
@@ -357,11 +376,19 @@ def retrieve(self, request, path, package):
357376 "pulp_created" ,
358377 "version" ,
359378 )
379+ provenances = PackageProvenance .objects .filter (package__in = local_packages ).values_list (
380+ "package__filename" , flat = True
381+ )
360382 local_releases = {
361383 p ["filename" ]: {
362384 ** p ,
363385 "url" : urljoin (self .base_content_url , f"{ path } /{ p ['filename' ]} " ),
364386 "upload_time" : p ["pulp_created" ],
387+ "provenance" : (
388+ self .get_provenance_url (normalized , p ["version" ], p ["filename" ])
389+ if p ["filename" ] in provenances
390+ else None
391+ ),
365392 }
366393 for p in packages
367394 }
@@ -497,3 +524,32 @@ def create(self, request, path):
497524 This is the endpoint that tools like Twine and Poetry use for their upload commands.
498525 """
499526 return self .upload (request , path )
527+
528+
529+ class ProvenanceView (PyPIMixin , ViewSet ):
530+ """View for the PyPI provenance endpoint."""
531+
532+ endpoint_name = "integrity"
533+ DEFAULT_ACCESS_POLICY = {
534+ "statements" : [
535+ {
536+ "action" : ["retrieve" ],
537+ "principal" : "*" ,
538+ "effect" : "allow" ,
539+ },
540+ ],
541+ }
542+
543+ @extend_schema (summary = "Get package provenance" )
544+ def retrieve (self , request , path , package , version , filename ):
545+ """Gets the provenance for a package."""
546+ repo_ver , content = self .get_rvc ()
547+ if content :
548+ package_content = content .filter (
549+ name__normalize = package , version = version , filename = filename
550+ ).first ()
551+ if package_content :
552+ provenance = PackageProvenance .objects .filter (package = package_content ).first ()
553+ if provenance :
554+ return Response (data = provenance .provenance )
555+ return HttpResponseNotFound (f"{ package } { version } { filename } provenance does not exist." )
0 commit comments