pulp-python 3.28.1__tar.gz → 3.29.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 (97) hide show
  1. {pulp_python-3.28.1 → pulp_python-3.29.0}/CHANGES.md +57 -0
  2. {pulp_python-3.28.1 → pulp_python-3.29.0}/MANIFEST.in +2 -0
  3. {pulp_python-3.28.1 → pulp_python-3.29.0}/PKG-INFO +1 -1
  4. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/__init__.py +1 -1
  5. pulp_python-3.29.0/pulp_python/app/migrations/0022_pythonblocklistentry.py +48 -0
  6. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/models.py +64 -0
  7. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/serializers.py +122 -1
  8. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/repair.py +25 -12
  9. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/utils.py +35 -6
  10. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/viewsets.py +78 -2
  11. pulp_python-3.29.0/pulp_python/tests/functional/api/test_blocklist.py +151 -0
  12. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/PKG-INFO +1 -1
  13. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/SOURCES.txt +2 -0
  14. {pulp_python-3.28.1 → pulp_python-3.29.0}/pyproject.toml +2 -2
  15. {pulp_python-3.28.1 → pulp_python-3.29.0}/COMMITMENT +0 -0
  16. {pulp_python-3.28.1 → pulp_python-3.29.0}/COPYRIGHT +0 -0
  17. {pulp_python-3.28.1 → pulp_python-3.29.0}/LICENSE +0 -0
  18. {pulp_python-3.28.1 → pulp_python-3.29.0}/README.md +0 -0
  19. {pulp_python-3.28.1 → pulp_python-3.29.0}/functest_requirements.txt +0 -0
  20. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/__init__.py +0 -0
  21. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/global_access_conditions.py +0 -0
  22. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/__init__.py +0 -0
  23. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/commands/__init__.py +0 -0
  24. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  25. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  26. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  27. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  28. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  29. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  30. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  31. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  32. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  33. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  34. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  35. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  36. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  37. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  38. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  39. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  40. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  41. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  42. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
  43. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
  44. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
  45. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +0 -0
  46. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +0 -0
  47. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/migrations/__init__.py +0 -0
  48. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/modelresource.py +0 -0
  49. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/provenance.py +0 -0
  50. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/__init__.py +0 -0
  51. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/serializers.py +0 -0
  52. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/pypi/views.py +0 -0
  53. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/replica.py +0 -0
  54. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/settings.py +0 -0
  55. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/__init__.py +0 -0
  56. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/publish.py +0 -0
  57. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/sync.py +0 -0
  58. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/upload.py +0 -0
  59. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
  60. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/urls.py +0 -0
  61. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  62. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  63. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  64. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/pytest_plugin.py +0 -0
  65. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/__init__.py +0 -0
  66. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/__init__.py +0 -0
  67. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  68. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
  69. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  70. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  71. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
  72. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  73. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  74. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
  75. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  76. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  77. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  78. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
  79. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
  80. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  81. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
  82. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
  83. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_upload.py +0 -0
  84. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
  85. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -0
  86. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -0
  87. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/constants.py +0 -0
  88. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/functional/utils.py +0 -0
  89. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/unit/__init__.py +0 -0
  90. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python/tests/unit/test_models.py +0 -0
  91. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  92. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/entry_points.txt +0 -0
  93. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/requires.txt +0 -0
  94. {pulp_python-3.28.1 → pulp_python-3.29.0}/pulp_python.egg-info/top_level.txt +0 -0
  95. {pulp_python-3.28.1 → pulp_python-3.29.0}/setup.cfg +0 -0
  96. {pulp_python-3.28.1 → pulp_python-3.29.0}/test_requirements.txt +0 -0
  97. {pulp_python-3.28.1 → pulp_python-3.29.0}/unittest_requirements.txt +0 -0
@@ -8,6 +8,30 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 3.29.0 (2026-04-17) {: #3.29.0 }
12
+
13
+ #### Features {: #3.29.0-feature }
14
+
15
+ - Added repository-specific package blocklist.
16
+ [#1166](https://github.com/pulp/pulp_python/issues/1166)
17
+
18
+ #### Bugfixes {: #3.29.0-bugfix }
19
+
20
+ - Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
21
+ [#1188](https://github.com/pulp/pulp_python/issues/1188)
22
+ - Support "atomic" replications in pulpcore 3.107
23
+
24
+ ---
25
+
26
+ ## 3.28.2 (2026-04-14) {: #3.28.2 }
27
+
28
+ #### Bugfixes {: #3.28.2-bugfix }
29
+
30
+ - Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
31
+ [#1188](https://github.com/pulp/pulp_python/issues/1188)
32
+
33
+ ---
34
+
11
35
  ## 3.28.1 (2026-04-01) {: #3.28.1 }
12
36
 
13
37
  #### Bugfixes {: #3.28.1-bugfix }
@@ -33,6 +57,23 @@
33
57
 
34
58
  ---
35
59
 
60
+ ## 3.27.2 (2026-04-14) {: #3.27.2 }
61
+
62
+ #### Bugfixes {: #3.27.2-bugfix }
63
+
64
+ - Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
65
+ [#1188](https://github.com/pulp/pulp_python/issues/1188)
66
+
67
+ ---
68
+
69
+ ## 3.27.1 (2026-04-01) {: #3.27.1 }
70
+
71
+ #### Bugfixes {: #3.27.1-bugfix }
72
+
73
+ - Support "atomic" replications in pulpcore 3.107
74
+
75
+ ---
76
+
36
77
  ## 3.27.0 (2026-03-17) {: #3.27.0 }
37
78
 
38
79
  #### Bugfixes {: #3.27.0-bugfix }
@@ -340,6 +381,14 @@
340
381
 
341
382
  ---
342
383
 
384
+ ## 3.13.6 (2026-04-01) {: #3.13.6 }
385
+
386
+ #### Bugfixes {: #3.13.6-bugfix }
387
+
388
+ - Support "atomic" replications in pulpcore 3.107
389
+
390
+ ---
391
+
343
392
  ## 3.13.5 (2025-04-23) {: #3.13.5 }
344
393
 
345
394
  No significant changes.
@@ -406,6 +455,14 @@ No significant changes.
406
455
 
407
456
  ---
408
457
 
458
+ ## 3.12.9 (2026-04-01) {: #3.12.9 }
459
+
460
+ #### Bugfixes {: #3.12.9-bugfix }
461
+
462
+ - Support "atomic" replications in pulpcore 3.107
463
+
464
+ ---
465
+
409
466
  ## 3.12.8 (2025-11-18) {: #3.12.8 }
410
467
 
411
468
  No significant changes.
@@ -11,3 +11,5 @@ include unittest_requirements.txt
11
11
  include pulp_python/app/webserver_snippets/*
12
12
  include pulp_python/tests/functional/assets/*
13
13
  exclude releasing.md
14
+
15
+ exclude .gitleaks.toml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.28.1
3
+ Version: 3.29.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
@@ -10,7 +10,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
10
10
 
11
11
  name = "pulp_python.app"
12
12
  label = "python"
13
- version = "3.28.1"
13
+ version = "3.29.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -0,0 +1,48 @@
1
+ # Generated by Django 5.2.10 on 2026-04-16 14:00
2
+
3
+ import django.db.models.deletion
4
+ import django_lifecycle.mixins
5
+ import pulpcore.app.models.base
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ("python", "0021_pythonrepository_upload_duplicate_filenames"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="PythonBlocklistEntry",
18
+ fields=[
19
+ (
20
+ "pulp_id",
21
+ models.UUIDField(
22
+ default=pulpcore.app.models.base.pulp_uuid,
23
+ editable=False,
24
+ primary_key=True,
25
+ serialize=False,
26
+ ),
27
+ ),
28
+ ("pulp_created", models.DateTimeField(auto_now_add=True)),
29
+ ("pulp_last_updated", models.DateTimeField(auto_now=True, null=True)),
30
+ ("name", models.TextField(default=None, null=True)),
31
+ ("version", models.TextField(default=None, null=True)),
32
+ ("filename", models.TextField(default=None, null=True)),
33
+ ("added_by", models.TextField(default="")),
34
+ (
35
+ "repository",
36
+ models.ForeignKey(
37
+ on_delete=django.db.models.deletion.CASCADE,
38
+ related_name="blocklist_entries",
39
+ to="python.pythonrepository",
40
+ ),
41
+ ),
42
+ ],
43
+ options={
44
+ "default_related_name": "%(app_label)s_%(model_name)s",
45
+ },
46
+ bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
47
+ ),
48
+ ]
@@ -14,6 +14,7 @@ from django_lifecycle import (
14
14
  from rest_framework.serializers import ValidationError
15
15
  from pulpcore.plugin.models import (
16
16
  AutoAddObjPermsMixin,
17
+ BaseModel,
17
18
  Content,
18
19
  Publication,
19
20
  Distribution,
@@ -399,9 +400,12 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
399
400
 
400
401
  When allow_package_substitution is False, reject any new version that would implicitly
401
402
  replace existing content with different checksums (content substitution).
403
+
404
+ Also checks newly added content against the repository's blocklist entries.
402
405
  """
403
406
  if not self.allow_package_substitution:
404
407
  self._check_for_package_substitution(new_version)
408
+ self._check_blocklist(new_version)
405
409
  remove_duplicates(new_version)
406
410
  validate_repo_version(new_version)
407
411
 
@@ -418,3 +422,63 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
418
422
  "To allow this, set 'allow_package_substitution' to True on the repository. "
419
423
  f"Conflicting packages: {duplicates}"
420
424
  )
425
+
426
+ def _check_blocklist(self, new_version):
427
+ """
428
+ Check newly added content in a repository version against the blocklist.
429
+ """
430
+ added_content = PythonPackageContent.objects.filter(
431
+ pk__in=new_version.added().values_list("pk", flat=True)
432
+ ).only("filename", "name_normalized", "version")
433
+ if added_content.exists():
434
+ self.check_blocklist_for_packages(added_content)
435
+
436
+ def check_blocklist_for_packages(self, packages):
437
+ """
438
+ Raise a ValidationError if any of the given packages match a blocklist entry.
439
+ """
440
+ entries = PythonBlocklistEntry.objects.filter(repository=self)
441
+ if not entries.exists():
442
+ return
443
+
444
+ blocked = []
445
+ for pkg in packages:
446
+ for entry in entries:
447
+ if entry.filename and entry.filename == pkg.filename:
448
+ blocked.append(pkg.filename)
449
+ break
450
+ if entry.name == pkg.name_normalized:
451
+ if not entry.version or entry.version == pkg.version:
452
+ blocked.append(pkg.filename)
453
+ break
454
+ if blocked:
455
+ raise ValidationError(
456
+ "Blocklisted packages cannot be added to this repository: "
457
+ "{}".format(", ".join(blocked))
458
+ )
459
+
460
+
461
+ class PythonBlocklistEntry(BaseModel):
462
+ """
463
+ An entry in a PythonRepository's package blocklist.
464
+
465
+ Blocklist entries prevent packages from being added to the repository.
466
+ Entries can match by package `name` (all versions), package `name` + `version`,
467
+ or exact `filename`. Exactly one of `name` or `filename` must be provided.
468
+ """
469
+
470
+ name = models.TextField(null=True, default=None)
471
+ version = models.TextField(null=True, default=None)
472
+ filename = models.TextField(null=True, default=None)
473
+ added_by = models.TextField(default="")
474
+ repository = models.ForeignKey(
475
+ PythonRepository, on_delete=models.CASCADE, related_name="blocklist_entries"
476
+ )
477
+
478
+ def __str__(self):
479
+ if self.filename:
480
+ return f"<{self._meta.object_name}: {self.filename}>"
481
+ return f"<{self._meta.object_name}: {self.name} [{self.version or 'all'}]>"
482
+
483
+ class Meta:
484
+ default_related_name = "%(app_label)s_%(model_name)s"
@@ -6,6 +6,7 @@ from django.conf import settings
6
6
  from django.db.utils import IntegrityError
7
7
  from drf_spectacular.utils import extend_schema_serializer
8
8
  from packaging.requirements import Requirement
9
+ from packaging.version import Version, InvalidVersion
9
10
  from rest_framework import serializers
10
11
  from pypi_attestations import AttestationError
11
12
  from pydantic import TypeAdapter, ValidationError
@@ -13,9 +14,10 @@ from urllib.parse import urljoin
13
14
 
14
15
  from pulpcore.plugin import models as core_models
15
16
  from pulpcore.plugin import serializers as core_serializers
16
- from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user
17
+ from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user, reverse
17
18
 
18
19
  from pulp_python.app import models as python_models
20
+ from pulp_python.app.utils import canonicalize_name
19
21
  from pulp_python.app.provenance import (
20
22
  Attestation,
21
23
  Provenance,
@@ -53,6 +55,11 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
53
55
  default=False,
54
56
  required=False,
55
57
  )
58
+ blocklist_entries_href = serializers.SerializerMethodField(
59
+ help_text=_("URL to the blocklist entries for this repository."),
60
+ read_only=True,
61
+ )
62
+
56
63
  allow_package_substitution = serializers.BooleanField(
57
64
  help_text=_(
58
65
  "Whether to allow package substitution (replacing existing packages with packages "
@@ -65,10 +72,15 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
65
72
  required=False,
66
73
  )
67
74
 
75
+ def get_blocklist_entries_href(self, obj):
76
+ repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.pk})
77
+ return f"{repo_href}blocklist_entries/"
78
+
68
79
  class Meta:
69
80
  fields = core_serializers.RepositorySerializer.Meta.fields + (
70
81
  "autopublish",
71
82
  "allow_package_substitution",
83
+ "blocklist_entries_href",
72
84
  )
73
85
  model = python_models.PythonRepository
74
86
 
@@ -780,6 +792,115 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
780
792
  model = python_models.PythonRemote
781
793
 
782
794
 
795
+ class PythonBlocklistEntrySerializer(core_serializers.ModelSerializer):
796
+ """
797
+ Serializer for PythonBlocklistEntry.
798
+
799
+ The `repository` is supplied by the URL (not the request body) and is injected
800
+ by the viewset before saving.
801
+ """
802
+
803
+ pulp_href = serializers.SerializerMethodField(
804
+ read_only=True,
805
+ help_text=_("The URL of this blocklist entry."),
806
+ )
807
+ repository = core_serializers.DetailRelatedField(
808
+ read_only=True,
809
+ view_name_pattern=r"repositories(-.*/.*)?-detail",
810
+ help_text=_("Repository this blocklist entry belongs to."),
811
+ )
812
+ name = serializers.CharField(
813
+ required=False,
814
+ allow_null=True,
815
+ default=None,
816
+ help_text=_(
817
+ "Package name to block (for all versions). Compared after PEP 503 normalization. "
818
+ "Required when 'filename' is not provided."
819
+ ),
820
+ )
821
+ version = serializers.CharField(
822
+ required=False,
823
+ allow_null=True,
824
+ default=None,
825
+ help_text=_("Exact version string to block (e.g. '1.0'). Only used when 'name' is set."),
826
+ )
827
+ filename = serializers.CharField(
828
+ required=False,
829
+ allow_null=True,
830
+ default=None,
831
+ help_text=_("Exact filename to block. Required when 'name' is not provided."),
832
+ )
833
+ added_by = serializers.CharField(
834
+ read_only=True,
835
+ help_text=_("PRN of the user who added this blocklist entry."),
836
+ )
837
+
838
+ def get_pulp_href(self, obj):
839
+ repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.repository_id})
840
+ return f"{repo_href}blocklist_entries/{obj.pk}/"
841
+
842
+ def validate(self, data):
843
+ """
844
+ Validate that the blocklist entry is well-formed and not a duplicate.
845
+ """
846
+ name = data.get("name")
847
+ filename = data.get("filename")
848
+ version = data.get("version")
849
+
850
+ if version and filename:
851
+ raise serializers.ValidationError(_("'version' cannot be used with 'filename'."))
852
+ if version and not name:
853
+ raise serializers.ValidationError(_("'version' requires 'name' to be provided."))
854
+ if bool(name) == bool(filename):
855
+ raise serializers.ValidationError(
856
+ _("Exactly one of 'name' or 'filename' must be provided.")
857
+ )
858
+
859
+ if version:
860
+ try:
861
+ Version(version)
862
+ except InvalidVersion:
863
+ raise serializers.ValidationError(
864
+ {"version": _("'{}' is not a valid version.").format(version)}
865
+ )
866
+ if name:
867
+ data["name"] = canonicalize_name(name)
868
+ name = data["name"]
869
+
870
+ repository = self.context.get("repository")
871
+ if repository:
872
+ qs = python_models.PythonBlocklistEntry.objects.filter(repository=repository)
873
+ if name and qs.filter(name=name, version=version).exists():
874
+ raise serializers.ValidationError(
875
+ _("A blocklist entry with this name and version already exists.")
876
+ )
877
+ if filename and qs.filter(filename=filename).exists():
878
+ raise serializers.ValidationError(
879
+ _("A blocklist entry with this filename already exists.")
880
+ )
881
+ data["repository"] = repository
882
+
883
+ return data
884
+
885
+ def create(self, validated_data):
886
+ """
887
+ Create a new blocklist entry, recording the authenticated user in `added_by`.
888
+ """
889
+ user = get_current_authenticated_user()
890
+ validated_data["added_by"] = get_prn(user) if user else ""
891
+ return super().create(validated_data)
892
+
893
+ class Meta:
894
+ fields = core_serializers.ModelSerializer.Meta.fields + (
895
+ "repository",
896
+ "name",
897
+ "version",
898
+ "filename",
899
+ "added_by",
900
+ )
901
+ model = python_models.PythonBlocklistEntry
902
+
903
+
783
904
  class PythonBanderRemoteSerializer(serializers.Serializer):
784
905
  """
785
906
  A Serializer for the initial step of creating a Python Remote from a Bandersnatch config file
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  from collections import defaultdict
3
4
  from gettext import gettext as _
4
5
  from itertools import groupby
@@ -8,18 +9,20 @@ from django.db.models import Prefetch
8
9
  from django.db.models.query import QuerySet
9
10
  from pulp_python.app.models import PythonPackageContent, PythonRepository
10
11
  from pulp_python.app.utils import (
11
- artifact_to_metadata_artifact,
12
12
  artifact_to_python_content_data,
13
+ copy_artifact_to_temp_file,
14
+ extract_wheel_metadata,
13
15
  fetch_json_release_metadata,
16
+ metadata_content_to_artifact,
14
17
  parse_metadata,
15
18
  )
16
- from pulpcore.plugin.models import Artifact, ContentArtifact, ProgressReport
19
+ from pulpcore.plugin.models import ContentArtifact, ProgressReport
17
20
  from pulpcore.plugin.util import get_domain
18
21
 
19
22
  log = logging.getLogger(__name__)
20
23
 
21
24
 
22
- BULK_SIZE = 1000
25
+ BULK_SIZE = 250
23
26
 
24
27
 
25
28
  def repair(repository_pk: UUID) -> None:
@@ -118,11 +121,21 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
118
121
  .first()
119
122
  .artifact
120
123
  )
121
- new_data = artifact_to_python_content_data(package.filename, main_artifact, domain)
124
+ # Copy artifact to temp file once, extract both content data and metadata
125
+ temp_path = copy_artifact_to_temp_file(main_artifact, package.filename)
126
+ try:
127
+ new_data = artifact_to_python_content_data(
128
+ package.filename, main_artifact, domain, temp_path=temp_path
129
+ )
130
+ metadata_content = (
131
+ extract_wheel_metadata(temp_path) if package.filename.endswith(".whl") else None
132
+ )
133
+ finally:
134
+ os.unlink(temp_path)
122
135
  total_metadata_repaired += update_metadata_artifact_if_needed(
123
136
  package,
124
137
  new_data.get("metadata_sha256"),
125
- main_artifact,
138
+ metadata_content,
126
139
  metadata_batch,
127
140
  pkgs_metadata_not_repaired,
128
141
  )
@@ -236,7 +249,7 @@ def update_package_if_needed(
236
249
  def update_metadata_artifact_if_needed(
237
250
  package: PythonPackageContent,
238
251
  new_metadata_sha256: str | None,
239
- main_artifact: Artifact,
252
+ metadata_content: bytes | None,
240
253
  metadata_batch: list[tuple],
241
254
  pkgs_metadata_not_repaired: set[str],
242
255
  ) -> int:
@@ -248,7 +261,7 @@ def update_metadata_artifact_if_needed(
248
261
  Args:
249
262
  package: Package to check for metadata changes.
250
263
  new_metadata_sha256: The correct metadata_sha256 extracted from the main artifact, or None.
251
- main_artifact: The main package artifact used to generate metadata.
264
+ metadata_content: Raw metadata bytes extracted from the wheel, or None.
252
265
  metadata_batch: List of tuples for batch processing (updated in-place).
253
266
  pkgs_metadata_not_repaired: Set of package PKs that failed repair (updated in-place).
254
267
 
@@ -265,13 +278,13 @@ def update_metadata_artifact_if_needed(
265
278
 
266
279
  # Create missing
267
280
  if not cas:
268
- metadata_batch.append((package, main_artifact))
281
+ metadata_batch.append((package, metadata_content))
269
282
  # Fix existing
270
283
  elif new_metadata_sha256 != original_metadata_sha256:
271
284
  ca = cas.first()
272
285
  metadata_artifact = ca.artifact
273
286
  if metadata_artifact is None or (metadata_artifact.sha256 != new_metadata_sha256):
274
- metadata_batch.append((package, main_artifact))
287
+ metadata_batch.append((package, metadata_content))
275
288
 
276
289
  if len(metadata_batch) == BULK_SIZE:
277
290
  not_repaired = _process_metadata_batch(metadata_batch)
@@ -288,7 +301,7 @@ def _process_metadata_batch(metadata_batch: list[tuple]) -> set[str]:
288
301
  and their corresponding ContentArtifacts.
289
302
 
290
303
  Args:
291
- metadata_batch: List of (package, main_artifact) tuples.
304
+ metadata_batch: List of (package, metadata_content) tuples.
292
305
 
293
306
  Returns:
294
307
  Set of package PKs for which metadata artifacts could not be created.
@@ -296,8 +309,8 @@ def _process_metadata_batch(metadata_batch: list[tuple]) -> set[str]:
296
309
  not_repaired = set()
297
310
  content_artifacts = []
298
311
 
299
- for package, main_artifact in metadata_batch:
300
- metadata_artifact = artifact_to_metadata_artifact(package.filename, main_artifact)
312
+ for package, metadata_content in metadata_batch:
313
+ metadata_artifact = metadata_content_to_artifact(metadata_content)
301
314
  if metadata_artifact:
302
315
  ca = ContentArtifact(
303
316
  artifact=metadata_artifact,
@@ -240,18 +240,37 @@ def compute_metadata_sha256(filename: str) -> str | None:
240
240
  return hashlib.sha256(metadata_content).hexdigest() if metadata_content else None
241
241
 
242
242
 
243
- def artifact_to_python_content_data(filename, artifact, domain=None):
243
+ def copy_artifact_to_temp_file(artifact, filename, tmp_dir="."):
244
+ """
245
+ Copy an artifact's file to a temporary file on disk.
246
+
247
+ Returns the path to the temp file. The caller is responsible for cleanup.
248
+ """
249
+ temp_file = tempfile.NamedTemporaryFile("wb", dir=tmp_dir, suffix=filename, delete=False)
250
+ artifact.file.seek(0)
251
+ shutil.copyfileobj(artifact.file, temp_file)
252
+ temp_file.flush()
253
+ temp_file.close()
254
+ return temp_file.name
255
+
256
+
257
+ def artifact_to_python_content_data(filename, artifact, domain=None, temp_path=None):
244
258
  """
245
259
  Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
260
+
261
+ If temp_path is provided, uses it instead of copying the artifact to a new temp file.
246
262
  """
247
263
  # Copy file to a temp directory under the user provided filename, we do this
248
264
  # because pkginfo validates that the filename has a valid extension before
249
265
  # reading it
250
- with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file:
251
- artifact.file.seek(0)
252
- shutil.copyfileobj(artifact.file, temp_file)
253
- temp_file.flush()
254
- metadata = get_project_metadata_from_file(temp_file.name)
266
+ if temp_path:
267
+ metadata = get_project_metadata_from_file(temp_path)
268
+ else:
269
+ with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file:
270
+ artifact.file.seek(0)
271
+ shutil.copyfileobj(artifact.file, temp_file)
272
+ temp_file.flush()
273
+ metadata = get_project_metadata_from_file(temp_file.name)
255
274
  data = parse_project_metadata(vars(metadata))
256
275
  data["sha256"] = artifact.sha256
257
276
  data["size"] = artifact.size
@@ -280,6 +299,16 @@ def artifact_to_metadata_artifact(
280
299
  if not metadata_content:
281
300
  return None
282
301
 
302
+ return metadata_content_to_artifact(metadata_content, tmp_dir)
303
+
304
+
305
+ def metadata_content_to_artifact(metadata_content: bytes, tmp_dir: str = ".") -> Artifact | None:
306
+ """
307
+ Creates an Artifact from raw metadata content bytes.
308
+ """
309
+ if not metadata_content:
310
+ return None
311
+
283
312
  with tempfile.NamedTemporaryFile(
284
313
  "wb", dir=tmp_dir, suffix=".metadata", delete=False
285
314
  ) as temp_md:
@@ -7,6 +7,12 @@ from packaging.utils import canonicalize_name
7
7
  from pathlib import Path
8
8
  from rest_framework import status
9
9
  from rest_framework.decorators import action
10
+ from rest_framework.mixins import (
11
+ CreateModelMixin,
12
+ DestroyModelMixin,
13
+ ListModelMixin,
14
+ RetrieveModelMixin,
15
+ )
10
16
  from rest_framework.response import Response
11
17
  from rest_framework.serializers import ValidationError
12
18
 
@@ -143,15 +149,20 @@ class PythonRepositoryViewSet(
143
149
  If allow_package_substitution is False and the request is **only** adding packages, then a
144
150
  package substitution check is performed to provide a quicker error response. Otherwise, the
145
151
  check is delegated to the task.
152
+
153
+ Also performs an early blocklist check on added packages.
146
154
  """
147
155
  repository = self.get_object()
156
+ add_content_units = request.data.get("add_content_units", [])
157
+ content_ids = [extract_pk(x) for x in add_content_units]
158
+
159
+ self._early_blocklist_check(repository, content_ids)
160
+
148
161
  if not repository.allow_package_substitution:
149
162
  remove_content_units = request.data.get("remove_content_units", [])
150
163
  if remove_content_units or "base_version" in request.data:
151
164
  return super().modify(request, pk)
152
165
  rvc = repository.latest_version().content
153
- add_content_units = request.data.get("add_content_units", [])
154
- content_ids = [extract_pk(x) for x in add_content_units]
155
166
  packages = (
156
167
  python_models.PythonPackageContent.objects.filter(pk__in=content_ids)
157
168
  .exclude(pk__in=rvc)
@@ -167,6 +178,17 @@ class PythonRepositoryViewSet(
167
178
  )
168
179
  return super().modify(request, pk)
169
180
 
181
+ def _early_blocklist_check(self, repository, content_ids):
182
+ """
183
+ Raise early if any added packages match a blocklist entry.
184
+ """
185
+ if not content_ids:
186
+ return
187
+ packages = python_models.PythonPackageContent.objects.filter(pk__in=content_ids).only(
188
+ "filename", "name_normalized", "version"
189
+ )
190
+ repository.check_blocklist_for_packages(packages)
191
+
170
192
  @extend_schema(
171
193
  summary="Repair metadata",
172
194
  responses={202: AsyncOperationResponseSerializer},
@@ -216,6 +238,60 @@ class PythonRepositoryViewSet(
216
238
  return core_viewsets.OperationPostponedResponse(result, request)
217
239
 
218
240
 
241
+ class PythonBlocklistEntryViewSet(
242
+ core_viewsets.NamedModelViewSet,
243
+ CreateModelMixin,
244
+ RetrieveModelMixin,
245
+ ListModelMixin,
246
+ DestroyModelMixin,
247
+ ):
248
+ """
249
+ ViewSet for managing blocklist entries on a PythonRepository.
250
+
251
+ Blocklist entries prevent packages from being added to the repository.
252
+ Entries can match by package `name` (all versions), package `name` + `version`,
253
+ or exact `filename`. Exactly one of `name` or `filename` must be provided.
254
+ """
255
+
256
+ endpoint_name = "blocklist_entries"
257
+ router_lookup = "pythonblocklistentry"
258
+ parent_viewset = PythonRepositoryViewSet
259
+ parent_lookup_kwargs = {"repository_pk": "repository__pk"}
260
+ serializer_class = python_serializers.PythonBlocklistEntrySerializer
261
+ queryset = python_models.PythonBlocklistEntry.objects.all()
262
+ ordering = ("-pulp_created",)
263
+
264
+ DEFAULT_ACCESS_POLICY = {
265
+ "statements": [
266
+ {
267
+ "action": ["list", "retrieve"],
268
+ "principal": "authenticated",
269
+ "effect": "allow",
270
+ "condition": "has_repository_model_or_domain_or_obj_perms:python.view_pythonrepository", # noqa: E501
271
+ },
272
+ {
273
+ "action": ["create", "destroy"],
274
+ "principal": "authenticated",
275
+ "effect": "allow",
276
+ "condition": [
277
+ "has_repository_model_or_domain_or_obj_perms:python.modify_pythonrepository",
278
+ "has_repository_model_or_domain_or_obj_perms:python.view_pythonrepository",
279
+ ],
280
+ },
281
+ ],
282
+ }
283
+
284
+ def get_serializer_context(self):
285
+ """
286
+ Inject the parent repository into the serializer context so that `validate()` can check for
287
+ duplicate entries. The guard on `repository_pk` prevents errors during schema generation.
288
+ """
289
+ context = super().get_serializer_context()
290
+ if self.kwargs.get("repository_pk"):
291
+ context["repository"] = self.get_parent_object()
292
+ return context
293
+
294
+
219
295
  class PythonRepositoryVersionViewSet(core_viewsets.RepositoryVersionViewSet):
220
296
  """
221
297
  PythonRepositoryVersion represents a single Python repository version.
@@ -0,0 +1,151 @@
1
+ import pytest
2
+
3
+ from pulpcore.tests.functional.utils import PulpTaskError
4
+ from pulp_python.tests.functional.constants import PYTHON_EGG_FILENAME, PYTHON_EGG_URL
5
+
6
+ CONTENT_BODY = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
7
+ BLOCKED_MSG = "Blocklisted packages cannot be added to this repository"
8
+
9
+
10
+ @pytest.mark.parallel
11
+ def test_crd_entry(python_bindings, python_repo):
12
+ """
13
+ CRD operations on blocklist entries return correct fields and update the entry count.
14
+ """
15
+ entries_data = [
16
+ ({"name": "shelf-reader"}, "shelf-reader", None, None),
17
+ ({"name": "shelf-reader", "version": "0.1"}, "shelf-reader", "0.1", None),
18
+ ({"filename": PYTHON_EGG_FILENAME}, None, None, PYTHON_EGG_FILENAME),
19
+ ]
20
+ for body_kwargs, name, version, filename in entries_data:
21
+ entry = python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
22
+ python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
23
+ )
24
+ assert entry.name == name
25
+ assert entry.version == version
26
+ assert entry.filename == filename
27
+ assert entry.added_by == "prn:auth.user:1"
28
+ assert entry.pulp_href is not None
29
+ assert entry.prn is not None
30
+
31
+ result = python_bindings.RepositoriesPythonBlocklistEntriesApi.list(python_repo.pulp_href)
32
+ assert result.count == 3
33
+
34
+ entry = result.results[0]
35
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.read(entry.pulp_href)
36
+
37
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.delete(entry.pulp_href)
38
+ result = python_bindings.RepositoriesPythonBlocklistEntriesApi.list(python_repo.pulp_href)
39
+ assert result.count == 2
40
+
41
+
42
+ @pytest.mark.parallel
43
+ @pytest.mark.parametrize(
44
+ "body_kwargs, expected_msg",
45
+ [
46
+ ({"name": "shelf-reader"}, "this name and version already exists"),
47
+ ({"name": "shelf-reader", "version": "0.1"}, "this name and version already exists"),
48
+ ({"filename": PYTHON_EGG_FILENAME}, "this filename already exists"),
49
+ ],
50
+ )
51
+ def test_duplicate_entry_rejected(python_bindings, python_repo, body_kwargs, expected_msg):
52
+ """
53
+ Creating a duplicate entry should fail.
54
+ """
55
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
56
+ python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
57
+ )
58
+ with pytest.raises(python_bindings.ApiException) as ctx:
59
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
60
+ python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
61
+ )
62
+ assert ctx.value.status == 400
63
+ assert expected_msg in ctx.value.body
64
+
65
+
66
+ @pytest.mark.parallel
67
+ @pytest.mark.parametrize(
68
+ "body_kwargs, expected_msg",
69
+ [
70
+ ({"version": "0.1", "filename": PYTHON_EGG_FILENAME}, "version' cannot be used with"),
71
+ ({"version": "0.1"}, "version' requires 'name'"),
72
+ ({"name": "shelf-reader", "filename": PYTHON_EGG_FILENAME}, "Exactly one of"),
73
+ ({}, "Exactly one of"),
74
+ ({"name": "shelf-reader", "version": "not-a-version"}, "not a valid version"),
75
+ ],
76
+ )
77
+ def test_invalid_entry_rejected(python_bindings, python_repo, body_kwargs, expected_msg):
78
+ """
79
+ Creating an entry with invalid data should fail.
80
+ """
81
+ with pytest.raises(python_bindings.ApiException) as ctx:
82
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
83
+ python_repo.pulp_href, python_bindings.PythonPythonBlocklistEntry(**body_kwargs)
84
+ )
85
+ assert ctx.value.status == 400
86
+ assert expected_msg in ctx.value.body
87
+
88
+
89
+ @pytest.mark.parallel
90
+ def test_upload_blocked(monitor_task, python_bindings, python_repo):
91
+ """
92
+ Uploading a package matching a blocklist entry is rejected.
93
+ """
94
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
95
+ python_repo.pulp_href,
96
+ python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="0.1"),
97
+ )
98
+
99
+ with pytest.raises(PulpTaskError) as exc:
100
+ response = python_bindings.ContentPackagesApi.create(
101
+ repository=python_repo.pulp_href, **CONTENT_BODY
102
+ )
103
+ monitor_task(response.task)
104
+ assert BLOCKED_MSG in exc.value.task.error["description"]
105
+
106
+ repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
107
+ assert repo.latest_version_href.endswith("/0/")
108
+
109
+
110
+ @pytest.mark.parallel
111
+ def test_upload_allowed(monitor_task, python_bindings, python_repo):
112
+ """
113
+ Uploading a package is allowed when the blocklist entry targets a different version.
114
+ """
115
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
116
+ python_repo.pulp_href,
117
+ python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="9.9"),
118
+ )
119
+
120
+ response = python_bindings.ContentPackagesApi.create(
121
+ repository=python_repo.pulp_href, **CONTENT_BODY
122
+ )
123
+ monitor_task(response.task)
124
+
125
+ repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
126
+ assert repo.latest_version_href.endswith("/1/")
127
+
128
+
129
+ @pytest.mark.parallel
130
+ def test_modify_blocked(monitor_task, python_bindings, python_repo):
131
+ """
132
+ Adding a blocklisted package via repository modify is rejected.
133
+ """
134
+ python_bindings.RepositoriesPythonBlocklistEntriesApi.create(
135
+ python_repo.pulp_href,
136
+ python_bindings.PythonPythonBlocklistEntry(name="shelf-reader", version="0.1"),
137
+ )
138
+
139
+ response = python_bindings.ContentPackagesApi.create(**CONTENT_BODY)
140
+ task = monitor_task(response.task)
141
+ content = python_bindings.ContentPackagesApi.read(task.created_resources[0])
142
+
143
+ with pytest.raises(python_bindings.ApiException) as exc:
144
+ python_bindings.RepositoriesPythonApi.modify(
145
+ python_repo.pulp_href, {"add_content_units": [content.pulp_href]}
146
+ )
147
+ assert exc.value.status == 400
148
+ assert BLOCKED_MSG in exc.value.body
149
+
150
+ repo = python_bindings.RepositoriesPythonApi.read(python_repo.pulp_href)
151
+ assert repo.latest_version_href.endswith("/0/")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.28.1
3
+ Version: 3.29.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
@@ -52,6 +52,7 @@ pulp_python/app/migrations/0018_packageprovenance.py
52
52
  pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py
53
53
  pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py
54
54
  pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py
55
+ pulp_python/app/migrations/0022_pythonblocklistentry.py
55
56
  pulp_python/app/migrations/__init__.py
56
57
  pulp_python/app/pypi/__init__.py
57
58
  pulp_python/app/pypi/serializers.py
@@ -72,6 +73,7 @@ pulp_python/tests/functional/utils.py
72
73
  pulp_python/tests/functional/api/__init__.py
73
74
  pulp_python/tests/functional/api/test_attestations.py
74
75
  pulp_python/tests/functional/api/test_auto_publish.py
76
+ pulp_python/tests/functional/api/test_blocklist.py
75
77
  pulp_python/tests/functional/api/test_consume_content.py
76
78
  pulp_python/tests/functional/api/test_crud_content_unit.py
77
79
  pulp_python/tests/functional/api/test_crud_publications.py
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-python"
10
- version = "3.28.1"
10
+ version = "3.29.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.28.1"
82
+ current_version = "3.29.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