pulp-python 3.20.1__tar.gz → 3.22.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 (94) hide show
  1. {pulp_python-3.20.1 → pulp_python-3.22.0}/CHANGES.md +50 -0
  2. {pulp_python-3.20.1 → pulp_python-3.22.0}/MANIFEST.in +1 -0
  3. {pulp_python-3.20.1 → pulp_python-3.22.0}/PKG-INFO +5 -4
  4. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/__init__.py +1 -1
  5. pulp_python-3.22.0/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +50 -0
  6. pulp_python-3.22.0/pulp_python/app/migrations/0018_packageprovenance.py +59 -0
  7. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/models.py +47 -1
  8. pulp_python-3.22.0/pulp_python/app/provenance.py +71 -0
  9. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/serializers.py +47 -5
  10. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/views.py +92 -26
  11. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/serializers.py +151 -1
  12. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/__init__.py +1 -0
  13. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/sync.py +43 -6
  14. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/upload.py +60 -13
  15. pulp_python-3.22.0/pulp_python/app/tasks/vulnerability_report.py +30 -0
  16. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/urls.py +12 -1
  17. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/utils.py +65 -7
  18. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/viewsets.py +63 -2
  19. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/pytest_plugin.py +31 -1
  20. pulp_python-3.22.0/pulp_python/tests/functional/api/test_attestations.py +242 -0
  21. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_pypi_apis.py +5 -30
  22. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +7 -2
  23. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_sync.py +12 -0
  24. pulp_python-3.22.0/pulp_python/tests/functional/api/test_upload.py +124 -0
  25. pulp_python-3.22.0/pulp_python/tests/functional/api/test_vulnerability_report.py +48 -0
  26. pulp_python-3.22.0/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +1 -0
  27. pulp_python-3.22.0/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +1 -0
  28. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/constants.py +13 -5
  29. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/PKG-INFO +5 -4
  30. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/SOURCES.txt +8 -0
  31. pulp_python-3.22.0/pulp_python.egg-info/requires.txt +5 -0
  32. {pulp_python-3.20.1 → pulp_python-3.22.0}/pyproject.toml +6 -5
  33. pulp_python-3.20.1/pulp_python/tests/functional/api/test_upload.py +0 -44
  34. pulp_python-3.20.1/pulp_python.egg-info/requires.txt +0 -4
  35. {pulp_python-3.20.1 → pulp_python-3.22.0}/COMMITMENT +0 -0
  36. {pulp_python-3.20.1 → pulp_python-3.22.0}/COPYRIGHT +0 -0
  37. {pulp_python-3.20.1 → pulp_python-3.22.0}/LICENSE +0 -0
  38. {pulp_python-3.20.1 → pulp_python-3.22.0}/README.md +0 -0
  39. {pulp_python-3.20.1 → pulp_python-3.22.0}/functest_requirements.txt +0 -0
  40. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/__init__.py +0 -0
  41. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/global_access_conditions.py +0 -0
  42. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/__init__.py +0 -0
  43. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/commands/__init__.py +0 -0
  44. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  45. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  46. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  47. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  48. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  49. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  50. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  51. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  52. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  53. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  54. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  55. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  56. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  57. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  58. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  59. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  60. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  61. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  62. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/__init__.py +0 -0
  63. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/modelresource.py +0 -0
  64. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/__init__.py +0 -0
  65. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/replica.py +0 -0
  66. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/settings.py +0 -0
  67. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/publish.py +0 -0
  68. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/repair.py +0 -0
  69. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  70. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  71. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  72. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/__init__.py +0 -0
  73. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/__init__.py +0 -0
  74. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  75. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  76. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  77. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
  78. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  79. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  80. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
  81. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  82. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  83. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  84. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  85. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
  86. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/utils.py +0 -0
  87. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/unit/__init__.py +0 -0
  88. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/unit/test_models.py +0 -0
  89. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  90. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/entry_points.txt +0 -0
  91. {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/top_level.txt +0 -0
  92. {pulp_python-3.20.1 → pulp_python-3.22.0}/setup.cfg +0 -0
  93. {pulp_python-3.20.1 → pulp_python-3.22.0}/test_requirements.txt +0 -0
  94. {pulp_python-3.20.1 → pulp_python-3.22.0}/unittest_requirements.txt +0 -0
@@ -8,6 +8,38 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 3.22.0 (2025-12-09) {: #3.22.0 }
12
+
13
+ #### Features {: #3.22.0-feature }
14
+
15
+ - Added attestations field to package upload that will create a PEP 740 Provenance object for that content.
16
+ [#984](https://github.com/pulp/pulp_python/issues/984)
17
+ - Added the ability to upload PEP 740 Provenance files to repositories.
18
+
19
+ #### Bugfixes {: #3.22.0-bugfix }
20
+
21
+ - Added some sanity validation checks to twine upload endpoint.
22
+
23
+ ---
24
+
25
+ ## 3.21.0 (2025-11-18) {: #3.21.0 }
26
+
27
+ #### Features {: #3.21.0-feature }
28
+
29
+ - Added ability to serve a specific repository version of a PyPI index.
30
+ [#982](https://github.com/pulp/pulp_python/issues/982)
31
+ - Implemented PEP 700 support, adding `versions`, `size` and `upload-time` to the Simple JSON API.
32
+ [#996](https://github.com/pulp/pulp_python/issues/996)
33
+ - Added the new /scan endpoint to the RepositoryVersion viewset to generate vulnerability reports.
34
+ [#1012](https://github.com/pulp/pulp_python/issues/1012)
35
+
36
+ #### Bugfixes {: #3.21.0-bugfix }
37
+
38
+ - Fixed pull-through caching not checking the repository if package was not present on remote.
39
+ [#1004](https://github.com/pulp/pulp_python/issues/1004)
40
+
41
+ ---
42
+
11
43
  ## 3.20.1 (2025-11-18) {: #3.20.1 }
12
44
 
13
45
  #### Bugfixes {: #3.20.1-bugfix }
@@ -205,6 +237,12 @@ No significant changes.
205
237
 
206
238
  ---
207
239
 
240
+ ## 3.12.8 (2025-11-18) {: #3.12.8 }
241
+
242
+ No significant changes.
243
+
244
+ ---
245
+
208
246
  ## 3.12.7 (2025-07-23) {: #3.12.7 }
209
247
 
210
248
  No significant changes.
@@ -291,6 +329,12 @@ No significant changes.
291
329
 
292
330
  ---
293
331
 
332
+ ## 3.11.7 (2025-11-18) {: #3.11.7 }
333
+
334
+ No significant changes.
335
+
336
+ ---
337
+
294
338
  ## 3.11.6 (2025-07-23) {: #3.11.6 }
295
339
 
296
340
  No significant changes.
@@ -355,6 +399,12 @@ No significant changes.
355
399
 
356
400
  ---
357
401
 
402
+ ## 3.10.2 (2025-11-18) {: #3.10.2 }
403
+
404
+ No significant changes
405
+
406
+ ---
407
+
358
408
  ## 3.10.1 (2025-07-23) {: #3.10.1 }
359
409
 
360
410
  ### Bugfixes
@@ -9,4 +9,5 @@ include functest_requirements.txt
9
9
  include test_requirements.txt
10
10
  include unittest_requirements.txt
11
11
  include pulp_python/app/webserver_snippets/*
12
+ include pulp_python/tests/functional/assets/*
12
13
  exclude releasing.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.20.1
3
+ Version: 3.22.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
@@ -20,10 +20,11 @@ Classifier: Programming Language :: Python :: 3.13
20
20
  Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: pulpcore<3.100,>=3.85.0
23
+ Requires-Dist: pulpcore<3.100,>=3.85.3
24
24
  Requires-Dist: pkginfo<1.13.0,>=1.12.0
25
- Requires-Dist: bandersnatch<6.6,>=6.3.0
26
- Requires-Dist: pypi-simple<2.0,>=1.5.0
25
+ Requires-Dist: bandersnatch<6.7,>=6.6.0
26
+ Requires-Dist: pypi-simple<2.0,>=1.8.0
27
+ Requires-Dist: pypi-attestations==0.0.28
27
28
  Dynamic: license-file
28
29
 
29
30
  # pulp_python
@@ -10,7 +10,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
10
10
 
11
11
  name = "pulp_python.app"
12
12
  label = "python"
13
- version = "3.20.1"
13
+ version = "3.22.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -0,0 +1,50 @@
1
+ # Generated by Django 4.2.26 on 2025-11-11 21:43
2
+
3
+ from django.db import migrations, models, transaction
4
+
5
+
6
+ def add_size_to_current_models(apps, schema_editor):
7
+ """Adds the size to current PythonPackageContent models."""
8
+ PythonPackageContent = apps.get_model("python", "PythonPackageContent")
9
+ RemoteArtifact = apps.get_model("core", "RemoteArtifact")
10
+ package_bulk = []
11
+ for python_package in PythonPackageContent.objects.only("pk", "size").iterator():
12
+ content_artifact = python_package.contentartifact_set.first()
13
+ if content_artifact.artifact:
14
+ artifact = content_artifact.artifact
15
+ else:
16
+ artifact = RemoteArtifact.objects.filter(content_artifact=content_artifact).first()
17
+ python_package.size = artifact.size or 0
18
+ package_bulk.append(python_package)
19
+ if len(package_bulk) == 100000:
20
+ with transaction.atomic():
21
+ PythonPackageContent.objects.bulk_update(
22
+ package_bulk,
23
+ [
24
+ "size",
25
+ ],
26
+ )
27
+ package_bulk = []
28
+ with transaction.atomic():
29
+ PythonPackageContent.objects.bulk_update(
30
+ package_bulk,
31
+ [
32
+ "size",
33
+ ],
34
+ )
35
+
36
+
37
+ class Migration(migrations.Migration):
38
+
39
+ dependencies = [
40
+ ("python", "0016_pythonpackagecontent_metadata_sha256"),
41
+ ]
42
+
43
+ operations = [
44
+ migrations.AddField(
45
+ model_name="pythonpackagecontent",
46
+ name="size",
47
+ field=models.BigIntegerField(default=0),
48
+ ),
49
+ migrations.RunPython(add_size_to_current_models, migrations.RunPython.noop, elidable=True),
50
+ ]
@@ -0,0 +1,59 @@
1
+ # Generated by Django 4.2.26 on 2025-12-01 19:49
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+ import pulpcore.app.util
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("python", "0017_pythonpackagecontent_size"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name="PackageProvenance",
17
+ fields=[
18
+ (
19
+ "content_ptr",
20
+ models.OneToOneField(
21
+ auto_created=True,
22
+ on_delete=django.db.models.deletion.CASCADE,
23
+ parent_link=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ to="core.content",
27
+ ),
28
+ ),
29
+ ("provenance", models.JSONField()),
30
+ ("sha256", models.CharField(max_length=64)),
31
+ (
32
+ "_pulp_domain",
33
+ models.ForeignKey(
34
+ default=pulpcore.app.util.get_domain_pk,
35
+ on_delete=django.db.models.deletion.PROTECT,
36
+ to="core.domain",
37
+ ),
38
+ ),
39
+ (
40
+ "package",
41
+ models.ForeignKey(
42
+ on_delete=django.db.models.deletion.CASCADE,
43
+ related_name="provenances",
44
+ to="python.pythonpackagecontent",
45
+ ),
46
+ ),
47
+ ],
48
+ options={
49
+ "default_related_name": "%(app_label)s_%(model_name)s",
50
+ "unique_together": {("sha256", "_pulp_domain")},
51
+ },
52
+ bases=("core.content",),
53
+ ),
54
+ migrations.AddField(
55
+ model_name="pythonremote",
56
+ name="provenance",
57
+ field=models.BooleanField(default=False),
58
+ ),
59
+ ]
@@ -1,3 +1,5 @@
1
+ import hashlib
2
+ import json
1
3
  from logging import getLogger
2
4
 
3
5
  from aiohttp.web import json_response
@@ -5,6 +7,10 @@ from django.contrib.postgres.fields import ArrayField
5
7
  from django.core.exceptions import ObjectDoesNotExist
6
8
  from django.db import models
7
9
  from django.conf import settings
10
+ from django_lifecycle import (
11
+ BEFORE_SAVE,
12
+ hook,
13
+ )
8
14
  from pulpcore.plugin.models import (
9
15
  AutoAddObjPermsMixin,
10
16
  Content,
@@ -16,6 +22,7 @@ from pulpcore.plugin.models import (
16
22
  from pulpcore.plugin.responses import ArtifactResponse
17
23
 
18
24
  from pathlib import PurePath
25
+ from .provenance import Provenance
19
26
  from .utils import (
20
27
  artifact_to_python_content_data,
21
28
  canonicalize_name,
@@ -193,6 +200,7 @@ class PythonPackageContent(Content):
193
200
  python_version = models.TextField()
194
201
  sha256 = models.CharField(db_index=True, max_length=64)
195
202
  metadata_sha256 = models.CharField(max_length=64, null=True)
203
+ size = models.BigIntegerField(default=0)
196
204
  # yanked and yanked_reason are not implemented because they are mutable
197
205
 
198
206
  # From pulpcore
@@ -234,6 +242,43 @@ class PythonPackageContent(Content):
234
242
  ]
235
243
 
236
244
 
245
+ class PackageProvenance(Content):
246
+ """
247
+ PEP 740 provenance objects.
248
+ """
249
+
250
+ TYPE = "provenance"
251
+ repo_key_fields = ("package_id",)
252
+
253
+ package = models.ForeignKey(
254
+ PythonPackageContent, on_delete=models.CASCADE, related_name="provenances"
255
+ )
256
+ provenance = models.JSONField(null=False)
257
+ sha256 = models.CharField(max_length=64, null=False)
258
+
259
+ _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
260
+
261
+ @staticmethod
262
+ def calculate_sha256(provenance):
263
+ """Calculates the sha256 from the provenance."""
264
+ provenance_json = json.dumps(provenance, sort_keys=True).encode("utf-8")
265
+ hasher = hashlib.sha256(provenance_json)
266
+ return hasher.hexdigest()
267
+
268
+ @hook(BEFORE_SAVE)
269
+ def set_sha256_hook(self):
270
+ """Ensure that sha256 is set before saving."""
271
+ self.sha256 = self.calculate_sha256(self.provenance)
272
+
273
+ @property
274
+ def as_model(self):
275
+ return Provenance.model_validate(self.provenance)
276
+
277
+ class Meta:
278
+ default_related_name = "%(app_label)s_%(model_name)s"
279
+ unique_together = ("sha256", "_pulp_domain")
280
+
281
+
237
282
  class PythonPublication(Publication, AutoAddObjPermsMixin):
238
283
  """
239
284
  A Publication for PythonContent.
@@ -269,6 +314,7 @@ class PythonRemote(Remote, AutoAddObjPermsMixin):
269
314
  exclude_platforms = ArrayField(
270
315
  models.CharField(max_length=10, blank=True), choices=PLATFORMS, default=list
271
316
  )
317
+ provenance = models.BooleanField(default=False)
272
318
 
273
319
  def get_remote_artifact_url(self, relative_path=None, request=None):
274
320
  """Get url for remote_artifact"""
@@ -294,7 +340,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
294
340
  """
295
341
 
296
342
  TYPE = "python"
297
- CONTENT_TYPES = [PythonPackageContent]
343
+ CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
298
344
  REMOTE_TYPES = [PythonRemote]
299
345
  PULL_THROUGH_SUPPORTED = True
300
346
 
@@ -0,0 +1,71 @@
1
+ from typing import Annotated, Literal, Union, get_args
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+ from pydantic.alias_generators import to_snake
5
+ from pypi_attestations import (
6
+ Attestation,
7
+ Distribution,
8
+ Publisher,
9
+ )
10
+
11
+
12
+ class _PermissivePolicy:
13
+ """A permissive verification policy that always succeeds."""
14
+
15
+ def verify(self, cert):
16
+ """Succeed regardless of the publisher's identity."""
17
+ pass
18
+
19
+
20
+ class AnyPublisher(BaseModel):
21
+ """A fallback publisher for any kind not matching other publisher types."""
22
+
23
+ model_config = ConfigDict(alias_generator=to_snake, extra="allow")
24
+
25
+ kind: str
26
+
27
+ def _as_policy(self):
28
+ """Return a permissive policy that always succeed."""
29
+ return _PermissivePolicy()
30
+
31
+
32
+ # Get the underlying Union type of the original Publisher
33
+ # Publisher is Annotated[Union[...], Field(discriminator="kind")]
34
+ _OriginalPublisherTypes = get_args(Publisher.__origin__)
35
+ # Add AnyPublisher to the list of original publisher types
36
+ _ExtendedPublisherTypes = (*_OriginalPublisherTypes, AnyPublisher)
37
+ _ExtendedPublisherUnion = Union[_ExtendedPublisherTypes]
38
+ # Create a new type that fallbacks to AnyPublisher
39
+ ExtendedPublisher = Annotated[_ExtendedPublisherUnion, Field(union_mode="left_to_right")]
40
+
41
+
42
+ class AttestationBundle(BaseModel):
43
+ """
44
+ AttestationBundle object as defined in PEP740.
45
+
46
+ PyPI only accepts attestations from TrustedPublishers (GitHub, GitLab, Google), but we will
47
+ accept from any user.
48
+ """
49
+
50
+ publisher: ExtendedPublisher
51
+ attestations: list[Attestation]
52
+
53
+
54
+ class Provenance(BaseModel):
55
+ """Provenance object as defined in PEP740."""
56
+
57
+ version: Literal[1] = 1
58
+ attestation_bundles: list[AttestationBundle]
59
+
60
+
61
+ def verify_provenance(filename, sha256, provenance, offline=False):
62
+ """Verify the provenance object is valid for the package."""
63
+ dist = Distribution(name=filename, digest=sha256)
64
+ for bundle in provenance.attestation_bundles:
65
+ publisher = bundle.publisher
66
+ policy = publisher._as_policy()
67
+ for attestation in bundle.attestations:
68
+ sig_bundle = attestation.to_bundle()
69
+ checkpoint = sig_bundle.log_entry._inner.inclusion_proof.checkpoint
70
+ staging = "sigstage.dev" in checkpoint.envelope
71
+ attestation.verify(policy, dist, staging=staging, offline=offline)
@@ -2,7 +2,9 @@ import logging
2
2
  from gettext import gettext as _
3
3
 
4
4
  from rest_framework import serializers
5
- from pulp_python.app.utils import DIST_EXTENSIONS
5
+ from pydantic import TypeAdapter, ValidationError
6
+ from pulp_python.app.provenance import Attestation
7
+ from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
6
8
  from pulpcore.plugin.models import Artifact
7
9
  from pulpcore.plugin.util import get_domain
8
10
  from django.db.utils import IntegrityError
@@ -54,6 +56,27 @@ class PackageUploadSerializer(serializers.Serializer):
54
56
  min_length=64,
55
57
  max_length=64,
56
58
  )
59
+ protocol_version = serializers.ChoiceField(
60
+ help_text=_("Protocol version to use for the upload. Only version 1 is supported."),
61
+ required=False,
62
+ choices=(1,),
63
+ default=1,
64
+ )
65
+ filetype = serializers.ChoiceField(
66
+ help_text=_("Type of artifact to upload."),
67
+ required=False,
68
+ choices=("bdist_wheel", "sdist"),
69
+ )
70
+ metadata_version = serializers.ChoiceField(
71
+ help_text=_("Metadata version of the uploaded package."),
72
+ required=False,
73
+ choices=SUPPORTED_METADATA_VERSIONS,
74
+ )
75
+ attestations = serializers.JSONField(
76
+ required=False,
77
+ help_text=_("A JSON list containing attestations for the package."),
78
+ write_only=True,
79
+ )
57
80
 
58
81
  def validate(self, data):
59
82
  """Validates the request."""
@@ -63,14 +86,33 @@ class PackageUploadSerializer(serializers.Serializer):
63
86
  file = data.get("content")
64
87
  for ext, packagetype in DIST_EXTENSIONS.items():
65
88
  if file.name.endswith(ext):
89
+ if (filetype := data.get("filetype")) and filetype != packagetype:
90
+ raise serializers.ValidationError(
91
+ {
92
+ "filetype": _(
93
+ "filetype {} does not match found filetype {} for file {}"
94
+ ).format(filetype, packagetype, file.name)
95
+ }
96
+ )
66
97
  break
67
98
  else:
68
99
  raise serializers.ValidationError(
69
- _(
70
- "Extension on {} is not a valid python extension "
71
- "(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
72
- ).format(file.name)
100
+ {
101
+ "content": _(
102
+ "Extension on {} is not a valid python extension "
103
+ "(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
104
+ ).format(file.name)
105
+ }
73
106
  )
107
+
108
+ if attestations := data.get("attestations"):
109
+ try:
110
+ attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
111
+ except ValidationError as e:
112
+ raise serializers.ValidationError(
113
+ {"attestations": _("The uploaded attestations are not valid: {}".format(e))}
114
+ )
115
+
74
116
  sha256 = data.get("sha256_digest")
75
117
  digests = {"sha256": sha256} if sha256 else None
76
118
  artifact = Artifact.init_and_validate(file, expected_digests=digests)