pulp-python 3.27.1__tar.gz → 3.28.1__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.
- {pulp_python-3.27.1 → pulp_python-3.28.1}/CHANGES.md +19 -2
- {pulp_python-3.27.1 → pulp_python-3.28.1}/PKG-INFO +2 -2
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.28.1/pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py +37 -0
- pulp_python-3.28.1/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py +16 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/models.py +35 -11
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/pypi/views.py +11 -6
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/serializers.py +20 -4
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/settings.py +1 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/publish.py +3 -3
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/urls.py +3 -2
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/viewsets.py +63 -1
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_content_unit.py +125 -23
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/PKG-INFO +2 -2
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/SOURCES.txt +2 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/requires.txt +1 -1
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pyproject.toml +3 -3
- {pulp_python-3.27.1 → pulp_python-3.28.1}/COMMITMENT +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/COPYRIGHT +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/LICENSE +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/MANIFEST.in +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/README.md +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/functest_requirements.txt +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/provenance.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/utils.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_attestations.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_upload.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/constants.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/setup.cfg +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/test_requirements.txt +0 -0
- {pulp_python-3.27.1 → pulp_python-3.28.1}/unittest_requirements.txt +0 -0
|
@@ -8,14 +8,31 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
-
## 3.
|
|
11
|
+
## 3.28.1 (2026-04-01) {: #3.28.1 }
|
|
12
12
|
|
|
13
|
-
#### Bugfixes {: #3.
|
|
13
|
+
#### Bugfixes {: #3.28.1-bugfix }
|
|
14
14
|
|
|
15
15
|
- Support "atomic" replications in pulpcore 3.107
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## 3.28.0 (2026-03-27) {: #3.28.0 }
|
|
20
|
+
|
|
21
|
+
#### Features {: #3.28.0-feature }
|
|
22
|
+
|
|
23
|
+
- Added the name_normalized field to PythonPackageContent model with a database index to replace
|
|
24
|
+
runtime regex normalization, reducing database load for package name lookups.
|
|
25
|
+
[#1159](https://github.com/pulp/pulp_python/issues/1159)
|
|
26
|
+
- Added a new `allow_package_substitution` boolean field to PythonRepository (default: True).
|
|
27
|
+
When set to False, any new repository version that would implicitly replace existing content
|
|
28
|
+
with content that has the same filename but a different sha256 checksum is rejected. This
|
|
29
|
+
applies to all repository version creation paths including uploads, modify, and sync. Content
|
|
30
|
+
with a matching checksum is allowed through idempotently.
|
|
31
|
+
[#1162](https://github.com/pulp/pulp_python/issues/1162)
|
|
32
|
+
- Added new setting PYPI_PATH_PREFIX to allow for customizing the path prefix for the PyPI API.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
19
36
|
## 3.27.0 (2026-03-17) {: #3.27.0 }
|
|
20
37
|
|
|
21
38
|
#### Bugfixes {: #3.27.0-bugfix }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.28.1
|
|
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,7 +20,7 @@ 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.115,>=3.
|
|
23
|
+
Requires-Dist: pulpcore<3.115,>=3.105.0
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
25
|
Requires-Dist: bandersnatch<6.7,>=6.6.0
|
|
26
26
|
Requires-Dist: pypi-simple<2.0,>=1.8.0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models, transaction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def populate_name_normalized(apps, schema_editor):
|
|
7
|
+
"""Populate name_normalized for existing PythonPackageContent rows."""
|
|
8
|
+
PythonPackageContent = apps.get_model("python", "PythonPackageContent")
|
|
9
|
+
package_bulk = []
|
|
10
|
+
normalize_re = re.compile(r"[-_.]+")
|
|
11
|
+
|
|
12
|
+
for package in PythonPackageContent.objects.only("pk", "name").iterator():
|
|
13
|
+
package.name_normalized = normalize_re.sub("-", package.name).lower()
|
|
14
|
+
package_bulk.append(package)
|
|
15
|
+
if len(package_bulk) == 100000:
|
|
16
|
+
with transaction.atomic():
|
|
17
|
+
PythonPackageContent.objects.bulk_update(package_bulk, ["name_normalized"])
|
|
18
|
+
package_bulk = []
|
|
19
|
+
if package_bulk:
|
|
20
|
+
with transaction.atomic():
|
|
21
|
+
PythonPackageContent.objects.bulk_update(package_bulk, ["name_normalized"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Migration(migrations.Migration):
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
("python", "0019_create_missing_metadata_artifacts"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
operations = [
|
|
31
|
+
migrations.AddField(
|
|
32
|
+
model_name="pythonpackagecontent",
|
|
33
|
+
name="name_normalized",
|
|
34
|
+
field=models.TextField(db_index=True, default=""),
|
|
35
|
+
),
|
|
36
|
+
migrations.RunPython(populate_name_normalized, migrations.RunPython.noop, elidable=True),
|
|
37
|
+
]
|
pulp_python-3.28.1/pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
("python", "0020_pythonpackagecontent_name_normalized"),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AddField(
|
|
12
|
+
model_name="pythonrepository",
|
|
13
|
+
name="allow_package_substitution",
|
|
14
|
+
field=models.BooleanField(default=True),
|
|
15
|
+
),
|
|
16
|
+
]
|
|
@@ -11,6 +11,7 @@ from django_lifecycle import (
|
|
|
11
11
|
BEFORE_SAVE,
|
|
12
12
|
hook,
|
|
13
13
|
)
|
|
14
|
+
from rest_framework.serializers import ValidationError
|
|
14
15
|
from pulpcore.plugin.models import (
|
|
15
16
|
AutoAddObjPermsMixin,
|
|
16
17
|
Content,
|
|
@@ -31,7 +32,11 @@ from .utils import (
|
|
|
31
32
|
PYPI_LAST_SERIAL,
|
|
32
33
|
PYPI_SERIAL_CONSTANT,
|
|
33
34
|
)
|
|
34
|
-
from pulpcore.plugin.repo_version_utils import
|
|
35
|
+
from pulpcore.plugin.repo_version_utils import (
|
|
36
|
+
collect_duplicates,
|
|
37
|
+
remove_duplicates,
|
|
38
|
+
validate_repo_version,
|
|
39
|
+
)
|
|
35
40
|
from pulpcore.plugin.util import get_domain_pk, get_domain
|
|
36
41
|
|
|
37
42
|
log = getLogger(__name__)
|
|
@@ -115,7 +120,7 @@ class PythonDistribution(Distribution, AutoAddObjPermsMixin):
|
|
|
115
120
|
if name:
|
|
116
121
|
normalized = canonicalize_name(name)
|
|
117
122
|
package_content = PythonPackageContent.objects.filter(
|
|
118
|
-
pk__in=self.publication.repository_version.content,
|
|
123
|
+
pk__in=self.publication.repository_version.content, name_normalized=normalized
|
|
119
124
|
)
|
|
120
125
|
# TODO Change this value to the Repo's serial value when implemented
|
|
121
126
|
headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
|
|
@@ -136,14 +141,6 @@ class PythonDistribution(Distribution, AutoAddObjPermsMixin):
|
|
|
136
141
|
]
|
|
137
142
|
|
|
138
143
|
|
|
139
|
-
class NormalizeName(models.Transform):
|
|
140
|
-
"""A transform field to normalize package names according to PEP426."""
|
|
141
|
-
|
|
142
|
-
function = "REGEXP_REPLACE"
|
|
143
|
-
template = "LOWER(%(function)s(%(expressions)s, '(\.|_|-)', '-', 'ig'))" # noqa:W605
|
|
144
|
-
lookup_name = "normalize"
|
|
145
|
-
|
|
146
|
-
|
|
147
144
|
class PythonPackageContent(Content):
|
|
148
145
|
"""
|
|
149
146
|
A Content Type representing Python's Distribution Package.
|
|
@@ -195,6 +192,9 @@ class PythonPackageContent(Content):
|
|
|
195
192
|
license_expression = models.TextField()
|
|
196
193
|
license_file = models.JSONField(default=list)
|
|
197
194
|
|
|
195
|
+
# Stored normalized name for indexed lookups
|
|
196
|
+
name_normalized = models.TextField(db_index=True, default="")
|
|
197
|
+
|
|
198
198
|
# Release metadata
|
|
199
199
|
filename = models.TextField(db_index=True)
|
|
200
200
|
packagetype = models.TextField(choices=PACKAGE_TYPES)
|
|
@@ -208,9 +208,13 @@ class PythonPackageContent(Content):
|
|
|
208
208
|
PROTECTED_FROM_RECLAIM = False
|
|
209
209
|
TYPE = "python"
|
|
210
210
|
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
211
|
-
name.register_lookup(NormalizeName)
|
|
212
211
|
repo_key_fields = ("filename",)
|
|
213
212
|
|
|
213
|
+
@hook(BEFORE_SAVE)
|
|
214
|
+
def set_name_normalized(self):
|
|
215
|
+
"""Pre-compute the normalized package name for indexed lookups."""
|
|
216
|
+
self.name_normalized = canonicalize_name(self.name)
|
|
217
|
+
|
|
214
218
|
@staticmethod
|
|
215
219
|
def init_from_artifact_and_relative_path(artifact, relative_path):
|
|
216
220
|
"""Used when downloading package from pull-through cache."""
|
|
@@ -363,6 +367,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
363
367
|
PULL_THROUGH_SUPPORTED = True
|
|
364
368
|
|
|
365
369
|
autopublish = models.BooleanField(default=False)
|
|
370
|
+
allow_package_substitution = models.BooleanField(default=True)
|
|
366
371
|
|
|
367
372
|
class Meta:
|
|
368
373
|
default_related_name = "%(app_label)s_%(model_name)s"
|
|
@@ -391,6 +396,25 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
391
396
|
def finalize_new_version(self, new_version):
|
|
392
397
|
"""
|
|
393
398
|
Remove duplicate packages that have the same filename.
|
|
399
|
+
|
|
400
|
+
When allow_package_substitution is False, reject any new version that would implicitly
|
|
401
|
+
replace existing content with different checksums (content substitution).
|
|
394
402
|
"""
|
|
403
|
+
if not self.allow_package_substitution:
|
|
404
|
+
self._check_for_package_substitution(new_version)
|
|
395
405
|
remove_duplicates(new_version)
|
|
396
406
|
validate_repo_version(new_version)
|
|
407
|
+
|
|
408
|
+
def _check_for_package_substitution(self, new_version):
|
|
409
|
+
"""
|
|
410
|
+
Raise a ValidationError if newly added packages would replace existing packages that have
|
|
411
|
+
the same filename but a different sha256 checksum.
|
|
412
|
+
"""
|
|
413
|
+
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
|
|
414
|
+
duplicates = collect_duplicates(qs, ("filename",))
|
|
415
|
+
if duplicates:
|
|
416
|
+
raise ValidationError(
|
|
417
|
+
"Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
|
|
418
|
+
"To allow this, set 'allow_package_substitution' to True on the repository. "
|
|
419
|
+
f"Conflicting packages: {duplicates}"
|
|
420
|
+
)
|
|
@@ -59,7 +59,7 @@ log = logging.getLogger(__name__)
|
|
|
59
59
|
|
|
60
60
|
ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
|
|
61
61
|
BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX)
|
|
62
|
-
BASE_API_URL = urljoin(settings.PYPI_API_HOSTNAME,
|
|
62
|
+
BASE_API_URL = urljoin(settings.PYPI_API_HOSTNAME, settings.PYPI_PATH_PREFIX)
|
|
63
63
|
|
|
64
64
|
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
65
65
|
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
@@ -303,7 +303,12 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
303
303
|
repo_version, content = self.get_rvc()
|
|
304
304
|
if self.should_redirect(repo_version=repo_version):
|
|
305
305
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/"))
|
|
306
|
-
names =
|
|
306
|
+
names = (
|
|
307
|
+
content.order_by("name_normalized")
|
|
308
|
+
.values_list("name", flat=True)
|
|
309
|
+
.distinct("name_normalized")
|
|
310
|
+
.iterator()
|
|
311
|
+
)
|
|
307
312
|
media_type = request.accepted_renderer.media_type
|
|
308
313
|
headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
|
|
309
314
|
|
|
@@ -360,8 +365,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
360
365
|
releases = self.pull_through_package_simple(normalized, path, self.distribution.remote)
|
|
361
366
|
elif self.should_redirect(repo_version=repo_ver):
|
|
362
367
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
|
|
363
|
-
if content:
|
|
364
|
-
local_packages = content.filter(
|
|
368
|
+
if content is not None:
|
|
369
|
+
local_packages = content.filter(name_normalized=normalized)
|
|
365
370
|
packages = local_packages.values(
|
|
366
371
|
"filename",
|
|
367
372
|
"sha256",
|
|
@@ -454,7 +459,7 @@ class MetadataView(PyPIMixin, ViewSet):
|
|
|
454
459
|
name = meta_path.parts[0]
|
|
455
460
|
if name:
|
|
456
461
|
normalized = canonicalize_name(name)
|
|
457
|
-
package_content = content.filter(
|
|
462
|
+
package_content = content.filter(name_normalized=normalized)
|
|
458
463
|
# TODO Change this value to the Repo's serial value when implemented
|
|
459
464
|
headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
|
|
460
465
|
if settings.DOMAIN_ENABLED:
|
|
@@ -541,7 +546,7 @@ class ProvenanceView(PyPIMixin, ViewSet):
|
|
|
541
546
|
repo_ver, content = self.get_rvc()
|
|
542
547
|
if content:
|
|
543
548
|
package_content = content.filter(
|
|
544
|
-
|
|
549
|
+
name_normalized=package, version=version, filename=filename
|
|
545
550
|
).first()
|
|
546
551
|
if package_content:
|
|
547
552
|
provenance = self.get_provenances(repo_ver).filter(package=package_content).first()
|
|
@@ -9,6 +9,7 @@ from packaging.requirements import Requirement
|
|
|
9
9
|
from rest_framework import serializers
|
|
10
10
|
from pypi_attestations import AttestationError
|
|
11
11
|
from pydantic import TypeAdapter, ValidationError
|
|
12
|
+
from urllib.parse import urljoin
|
|
12
13
|
|
|
13
14
|
from pulpcore.plugin import models as core_models
|
|
14
15
|
from pulpcore.plugin import serializers as core_serializers
|
|
@@ -31,6 +32,7 @@ from pulp_python.app.utils import (
|
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
log = logging.getLogger(__name__)
|
|
35
|
+
PYPI_BASE_URL = urljoin(settings.PYPI_API_HOSTNAME, settings.PYPI_PATH_PREFIX)
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
@extend_schema_serializer(
|
|
@@ -51,9 +53,23 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
51
53
|
default=False,
|
|
52
54
|
required=False,
|
|
53
55
|
)
|
|
56
|
+
allow_package_substitution = serializers.BooleanField(
|
|
57
|
+
help_text=_(
|
|
58
|
+
"Whether to allow package substitution (replacing existing packages with packages "
|
|
59
|
+
"that have the same filename but a different checksum). When False, any new "
|
|
60
|
+
"repository version that would cause such a substitution will be rejected. This "
|
|
61
|
+
"applies to all repository version creation paths including uploads, modify, and "
|
|
62
|
+
"sync. When True (the default), package substitution is allowed."
|
|
63
|
+
),
|
|
64
|
+
default=True,
|
|
65
|
+
required=False,
|
|
66
|
+
)
|
|
54
67
|
|
|
55
68
|
class Meta:
|
|
56
|
-
fields = core_serializers.RepositorySerializer.Meta.fields + (
|
|
69
|
+
fields = core_serializers.RepositorySerializer.Meta.fields + (
|
|
70
|
+
"autopublish",
|
|
71
|
+
"allow_package_substitution",
|
|
72
|
+
)
|
|
57
73
|
model = python_models.PythonRepository
|
|
58
74
|
|
|
59
75
|
|
|
@@ -92,8 +108,8 @@ class PythonDistributionSerializer(core_serializers.DistributionSerializer):
|
|
|
92
108
|
def get_base_url(self, obj):
|
|
93
109
|
"""Gets the base url."""
|
|
94
110
|
if settings.DOMAIN_ENABLED:
|
|
95
|
-
return f"{
|
|
96
|
-
return f"{
|
|
111
|
+
return urljoin(PYPI_BASE_URL, f"{get_domain().name}/{obj.base_path}/")
|
|
112
|
+
return urljoin(PYPI_BASE_URL, f"{obj.base_path}/")
|
|
97
113
|
|
|
98
114
|
class Meta:
|
|
99
115
|
fields = core_serializers.DistributionSerializer.Meta.fields + (
|
|
@@ -446,7 +462,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
446
462
|
content = super().create(validated_data)
|
|
447
463
|
if provenance:
|
|
448
464
|
prov_sha256 = python_models.PackageProvenance.calculate_sha256(provenance)
|
|
449
|
-
prov_model,
|
|
465
|
+
prov_model, _created = python_models.PackageProvenance.objects.get_or_create(
|
|
450
466
|
sha256=prov_sha256,
|
|
451
467
|
_pulp_domain=get_domain(),
|
|
452
468
|
defaults={"package": content, "provenance": provenance},
|
|
@@ -60,9 +60,9 @@ def write_simple_api(publication):
|
|
|
60
60
|
python_models.PythonPackageContent.objects.filter(
|
|
61
61
|
pk__in=publication.repository_version.content, _pulp_domain=domain
|
|
62
62
|
)
|
|
63
|
-
.order_by("
|
|
63
|
+
.order_by("name_normalized")
|
|
64
64
|
.values_list("name", flat=True)
|
|
65
|
-
.distinct("
|
|
65
|
+
.distinct("name_normalized")
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
# write the root index, which lists all of the projects for which there is a package available
|
|
@@ -81,7 +81,7 @@ def write_simple_api(publication):
|
|
|
81
81
|
packages = python_models.PythonPackageContent.objects.filter(
|
|
82
82
|
pk__in=publication.repository_version.content, _pulp_domain=domain
|
|
83
83
|
)
|
|
84
|
-
releases = packages.order_by("
|
|
84
|
+
releases = packages.order_by("name_normalized").values("name", "filename", "sha256")
|
|
85
85
|
|
|
86
86
|
ind = 0
|
|
87
87
|
current_name = canonicalize_name(project_names[ind])
|
|
@@ -10,9 +10,10 @@ from pulp_python.app.pypi.views import (
|
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
if settings.DOMAIN_ENABLED:
|
|
13
|
-
PYPI_API_URL = "
|
|
13
|
+
PYPI_API_URL = "/<slug:pulp_domain>/<path:path>/"
|
|
14
14
|
else:
|
|
15
|
-
PYPI_API_URL = "
|
|
15
|
+
PYPI_API_URL = "/<path:path>/"
|
|
16
|
+
PYPI_API_URL = settings.PYPI_PATH_PREFIX.strip("/") + PYPI_API_URL
|
|
16
17
|
# TODO: Implement remaining PyPI endpoints
|
|
17
18
|
# path("project/", PackageProject.as_view()), # Endpoints to nicely see contents of index
|
|
18
19
|
# path("search/", PackageSearch.as_view()),
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
from bandersnatch.configuration import BandersnatchConfig
|
|
2
2
|
from django.db import transaction
|
|
3
|
+
from django_filters import CharFilter
|
|
4
|
+
from django_filters.rest_framework import filters as drf_filters
|
|
3
5
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
|
6
|
+
from packaging.utils import canonicalize_name
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
from rest_framework import status
|
|
6
9
|
from rest_framework.decorators import action
|
|
7
10
|
from rest_framework.response import Response
|
|
11
|
+
from rest_framework.serializers import ValidationError
|
|
8
12
|
|
|
9
13
|
from pulpcore.plugin import viewsets as core_viewsets
|
|
10
14
|
from pulpcore.plugin.actions import ModifyRepositoryActionMixin
|
|
11
15
|
from pulpcore.plugin.models import RepositoryVersion
|
|
12
16
|
from pulpcore.plugin.serializers import (
|
|
13
17
|
AsyncOperationResponseSerializer,
|
|
18
|
+
RepositoryAddRemoveContentSerializer,
|
|
14
19
|
RepositorySyncURLSerializer,
|
|
15
20
|
)
|
|
16
21
|
from pulpcore.plugin.tasking import check_content, dispatch
|
|
22
|
+
from pulpcore.plugin.util import extract_pk
|
|
17
23
|
|
|
18
24
|
from pulp_python.app import models as python_models
|
|
19
25
|
from pulp_python.app import serializers as python_serializers
|
|
@@ -124,6 +130,43 @@ class PythonRepositoryViewSet(
|
|
|
124
130
|
"python.pythonrepository_viewer": ["python.view_pythonrepository"],
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
@extend_schema(
|
|
134
|
+
description="Trigger an asynchronous task to create a new repository version.",
|
|
135
|
+
summary="Modify Repository Content",
|
|
136
|
+
responses={202: AsyncOperationResponseSerializer},
|
|
137
|
+
)
|
|
138
|
+
@action(detail=True, methods=["post"], serializer_class=RepositoryAddRemoveContentSerializer)
|
|
139
|
+
def modify(self, request, pk):
|
|
140
|
+
"""
|
|
141
|
+
Queues a task that creates a new RepositoryVersion by adding and removing content units.
|
|
142
|
+
|
|
143
|
+
If allow_package_substitution is False and the request is **only** adding packages, then a
|
|
144
|
+
package substitution check is performed to provide a quicker error response. Otherwise, the
|
|
145
|
+
check is delegated to the task.
|
|
146
|
+
"""
|
|
147
|
+
repository = self.get_object()
|
|
148
|
+
if not repository.allow_package_substitution:
|
|
149
|
+
remove_content_units = request.data.get("remove_content_units", [])
|
|
150
|
+
if remove_content_units or "base_version" in request.data:
|
|
151
|
+
return super().modify(request, pk)
|
|
152
|
+
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
|
+
packages = (
|
|
156
|
+
python_models.PythonPackageContent.objects.filter(pk__in=content_ids)
|
|
157
|
+
.exclude(pk__in=rvc)
|
|
158
|
+
.values("filename")
|
|
159
|
+
)
|
|
160
|
+
conflicting_packages = python_models.PythonPackageContent.objects.filter(
|
|
161
|
+
filename__in=packages, pk__in=rvc
|
|
162
|
+
)
|
|
163
|
+
if conflicting_packages.exists():
|
|
164
|
+
raise ValidationError(
|
|
165
|
+
"Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
|
|
166
|
+
f"Existing conflicting packages: {conflicting_packages.values('filename', 'sha256', 'pk')}" # noqa: E501
|
|
167
|
+
)
|
|
168
|
+
return super().modify(request, pk)
|
|
169
|
+
|
|
127
170
|
@extend_schema(
|
|
128
171
|
summary="Repair metadata",
|
|
129
172
|
responses={202: AsyncOperationResponseSerializer},
|
|
@@ -329,15 +372,34 @@ class PythonDistributionViewSet(core_viewsets.DistributionViewSet, core_viewsets
|
|
|
329
372
|
}
|
|
330
373
|
|
|
331
374
|
|
|
375
|
+
class NormalizedNameFilter(CharFilter):
|
|
376
|
+
"""Filter that normalizes the input and queries name_normalized."""
|
|
377
|
+
|
|
378
|
+
def filter(self, qs, value):
|
|
379
|
+
if value:
|
|
380
|
+
if isinstance(value, list):
|
|
381
|
+
value = [canonicalize_name(v) for v in value]
|
|
382
|
+
else:
|
|
383
|
+
value = canonicalize_name(value)
|
|
384
|
+
return super().filter(qs, value)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class NormalizedNameInFilter(drf_filters.BaseInFilter, NormalizedNameFilter):
|
|
388
|
+
"""In-filter that normalizes each input value and queries name_normalized."""
|
|
389
|
+
|
|
390
|
+
|
|
332
391
|
class PythonPackageContentFilter(core_viewsets.ContentFilter):
|
|
333
392
|
"""
|
|
334
393
|
FilterSet for PythonPackageContent.
|
|
335
394
|
"""
|
|
336
395
|
|
|
396
|
+
name = NormalizedNameFilter(field_name="name_normalized", lookup_expr="exact")
|
|
397
|
+
name__in = NormalizedNameInFilter(field_name="name_normalized", lookup_expr="in")
|
|
398
|
+
name__contains = CharFilter(field_name="name", lookup_expr="contains")
|
|
399
|
+
|
|
337
400
|
class Meta:
|
|
338
401
|
model = python_models.PythonPackageContent
|
|
339
402
|
fields = {
|
|
340
|
-
"name": ["exact", "in", "contains"],
|
|
341
403
|
"author": ["exact", "in", "contains"],
|
|
342
404
|
"packagetype": ["exact", "in"],
|
|
343
405
|
"requires_python": ["exact", "in", "contains"],
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_content_unit.py
RENAMED
|
@@ -144,44 +144,41 @@ def test_content_create_new_metadata(
|
|
|
144
144
|
assert getattr(content, k) == v
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
def get_package_url(package, filename):
|
|
148
|
+
with PyPISimple() as client:
|
|
149
|
+
page = client.get_project_page(package)
|
|
150
|
+
for package in page.packages:
|
|
151
|
+
if package.filename == filename:
|
|
152
|
+
return package.url
|
|
153
|
+
raise ValueError(f"Package {filename} not found")
|
|
154
|
+
|
|
155
|
+
|
|
147
156
|
@pytest.mark.parallel
|
|
148
157
|
def test_upload_metadata_23_spec(python_content_factory):
|
|
149
158
|
"""Test that packages using metadata spec 2.3 can be uploaded to pulp."""
|
|
150
159
|
filename = "urllib3-2.2.2-py3-none-any.whl"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if package.filename == filename:
|
|
155
|
-
content = python_content_factory(filename, url=package.url)
|
|
156
|
-
assert content.metadata_version == "2.3"
|
|
157
|
-
break
|
|
160
|
+
url = get_package_url("urllib3", filename)
|
|
161
|
+
content = python_content_factory(filename, url=url)
|
|
162
|
+
assert content.metadata_version == "2.3"
|
|
158
163
|
|
|
159
164
|
|
|
160
165
|
@pytest.mark.parallel
|
|
161
166
|
def test_upload_requires_python(python_content_factory):
|
|
162
167
|
filename = "pip-24.3.1-py3-none-any.whl"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if package.filename == filename:
|
|
167
|
-
content = python_content_factory(filename, url=package.url)
|
|
168
|
-
assert content.requires_python == ">=3.8"
|
|
169
|
-
break
|
|
168
|
+
url = get_package_url("pip", filename)
|
|
169
|
+
content = python_content_factory(filename, url=url)
|
|
170
|
+
assert content.requires_python == ">=3.8"
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
@pytest.mark.parallel
|
|
173
174
|
def test_upload_metadata_24_spec(python_content_factory):
|
|
174
175
|
"""Test that packages using metadata spec 2.4 can be uploaded to pulp."""
|
|
175
176
|
filename = "setuptools-80.9.0.tar.gz"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
assert content.metadata_version == "2.4"
|
|
182
|
-
assert content.license_expression == "MIT"
|
|
183
|
-
assert content.license_file == '["LICENSE"]'
|
|
184
|
-
break
|
|
177
|
+
url = get_package_url("setuptools", filename)
|
|
178
|
+
content = python_content_factory(filename, url=url)
|
|
179
|
+
assert content.metadata_version == "2.4"
|
|
180
|
+
assert content.license_expression == "MIT"
|
|
181
|
+
assert content.license_file == '["LICENSE"]'
|
|
185
182
|
|
|
186
183
|
|
|
187
184
|
@pytest.mark.parallel
|
|
@@ -203,3 +200,108 @@ def test_package_creation_with_metadata(
|
|
|
203
200
|
ensure_metadata(
|
|
204
201
|
pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME, "shelf-reader", "0.1"
|
|
205
202
|
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parallel
|
|
206
|
+
def test_disallow_package_substitution(
|
|
207
|
+
monitor_task,
|
|
208
|
+
python_bindings,
|
|
209
|
+
python_repo_factory,
|
|
210
|
+
):
|
|
211
|
+
"""
|
|
212
|
+
When allow_package_substitution=False, any new repository version that would substitute
|
|
213
|
+
existing content (same filename, different sha256) is rejected. This applies to both
|
|
214
|
+
content uploads and repository modify operations. Re-adding content with a matching
|
|
215
|
+
sha256 succeeds idempotently.
|
|
216
|
+
"""
|
|
217
|
+
repo = python_repo_factory(allow_package_substitution=False)
|
|
218
|
+
msg1 = "Found duplicate packages being added with the same filename but different checksums."
|
|
219
|
+
msg2 = "To allow this, set 'allow_package_substitution' to True on the repository."
|
|
220
|
+
|
|
221
|
+
# First upload succeeds
|
|
222
|
+
content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
|
|
223
|
+
response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
|
|
224
|
+
task = monitor_task(response.task)
|
|
225
|
+
content = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
|
|
226
|
+
assert content.filename == PYTHON_EGG_FILENAME
|
|
227
|
+
|
|
228
|
+
# Re-upload same artifact with same filename — should succeed (idempotent)
|
|
229
|
+
response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
|
|
230
|
+
task = monitor_task(response.task)
|
|
231
|
+
assert content.pulp_href in task.created_resources
|
|
232
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
233
|
+
assert repo.latest_version_href.endswith("/1/")
|
|
234
|
+
|
|
235
|
+
# Upload a different artifact with the same filename — should be rejected
|
|
236
|
+
second_filename = "pip-26.0.1.tar.gz"
|
|
237
|
+
second_url = get_package_url("pip", second_filename)
|
|
238
|
+
content_body2 = {"relative_path": PYTHON_EGG_FILENAME, "file_url": second_url}
|
|
239
|
+
with pytest.raises(PulpTaskError) as exc:
|
|
240
|
+
response = python_bindings.ContentPackagesApi.create(
|
|
241
|
+
repository=repo.pulp_href, **content_body2
|
|
242
|
+
)
|
|
243
|
+
monitor_task(response.task)
|
|
244
|
+
assert msg1 in exc.value.task.error["description"]
|
|
245
|
+
assert msg2 in exc.value.task.error["description"]
|
|
246
|
+
|
|
247
|
+
# Also create the conflicting content without a repo, then try to add via modify
|
|
248
|
+
response = python_bindings.ContentPackagesApi.create(**content_body2)
|
|
249
|
+
task = monitor_task(response.task)
|
|
250
|
+
content2 = python_bindings.ContentPackagesApi.read(task.created_resources[0])
|
|
251
|
+
# When body only contains add_content_units, the request will be rejected
|
|
252
|
+
body = {"add_content_units": [content2.pulp_href]}
|
|
253
|
+
with pytest.raises(python_bindings.ApiException) as exc:
|
|
254
|
+
python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body)
|
|
255
|
+
assert msg1 in exc.value.body
|
|
256
|
+
# Else when body contains other fields, the check will be delegated to the task
|
|
257
|
+
body = {"add_content_units": [content2.pulp_href], "base_version": repo.latest_version_href}
|
|
258
|
+
with pytest.raises(PulpTaskError) as exc:
|
|
259
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
260
|
+
assert msg1 in exc.value.task.error["description"]
|
|
261
|
+
assert msg2 in exc.value.task.error["description"]
|
|
262
|
+
|
|
263
|
+
# Verify the repository still has only the original content
|
|
264
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
265
|
+
assert repo.latest_version_href.endswith("/1/")
|
|
266
|
+
# Check that you can remove the conflicting content and add the new content
|
|
267
|
+
body = {"remove_content_units": [content.pulp_href], "add_content_units": [content2.pulp_href]}
|
|
268
|
+
response = python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body)
|
|
269
|
+
task = monitor_task(response.task)
|
|
270
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
271
|
+
assert repo.latest_version_href.endswith("/2/")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parallel
|
|
275
|
+
def test_package_substitution_allowed_by_default(
|
|
276
|
+
monitor_task,
|
|
277
|
+
python_bindings,
|
|
278
|
+
python_repo_factory,
|
|
279
|
+
):
|
|
280
|
+
"""
|
|
281
|
+
By default (allow_package_substitution=True), uploading a file with the same filename
|
|
282
|
+
but different sha256 replaces the existing content in the repository.
|
|
283
|
+
"""
|
|
284
|
+
repo = python_repo_factory()
|
|
285
|
+
assert repo.allow_package_substitution is True
|
|
286
|
+
|
|
287
|
+
content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
|
|
288
|
+
response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
|
|
289
|
+
task = monitor_task(response.task)
|
|
290
|
+
content1 = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
|
|
291
|
+
|
|
292
|
+
# Upload a different artifact with the same filename — should succeed and replace
|
|
293
|
+
second_filename = "pip-26.0.tar.gz"
|
|
294
|
+
second_url = get_package_url("pip", second_filename)
|
|
295
|
+
content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": second_url}
|
|
296
|
+
response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
|
|
297
|
+
task = monitor_task(response.task)
|
|
298
|
+
content2 = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
|
|
299
|
+
assert content2.pulp_href != content1.pulp_href
|
|
300
|
+
|
|
301
|
+
# Verify the repo has only the new content
|
|
302
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
303
|
+
content_list = python_bindings.ContentPackagesApi.list(
|
|
304
|
+
repository_version=repo.latest_version_href
|
|
305
|
+
)
|
|
306
|
+
assert content_list.count == 1
|
|
307
|
+
assert content_list.results[0].sha256 == content2.sha256
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.28.1
|
|
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,7 +20,7 @@ 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.115,>=3.
|
|
23
|
+
Requires-Dist: pulpcore<3.115,>=3.105.0
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
25
|
Requires-Dist: bandersnatch<6.7,>=6.6.0
|
|
26
26
|
Requires-Dist: pypi-simple<2.0,>=1.8.0
|
|
@@ -50,6 +50,8 @@ pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py
|
|
|
50
50
|
pulp_python/app/migrations/0017_pythonpackagecontent_size.py
|
|
51
51
|
pulp_python/app/migrations/0018_packageprovenance.py
|
|
52
52
|
pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py
|
|
53
|
+
pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py
|
|
54
|
+
pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py
|
|
53
55
|
pulp_python/app/migrations/__init__.py
|
|
54
56
|
pulp_python/app/pypi/__init__.py
|
|
55
57
|
pulp_python/app/pypi/serializers.py
|
|
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pulp-python"
|
|
10
|
-
version = "3.
|
|
10
|
+
version = "3.28.1"
|
|
11
11
|
description = "pulp-python plugin for the Pulp Project"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -26,7 +26,7 @@ classifiers=[
|
|
|
26
26
|
]
|
|
27
27
|
requires-python = ">=3.11"
|
|
28
28
|
dependencies = [
|
|
29
|
-
"pulpcore>=3.
|
|
29
|
+
"pulpcore>=3.105.0,<3.115",
|
|
30
30
|
"pkginfo>=1.12.0,<1.13.0",
|
|
31
31
|
"bandersnatch>=6.6.0,<6.7",
|
|
32
32
|
"pypi-simple>=1.8.0,<2.0",
|
|
@@ -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.
|
|
82
|
+
current_version = "3.28.1"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0003_new_sync_filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0010_update_json_field.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/app/migrations/0018_packageprovenance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_attestations.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_auto_publish.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_consume_content.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_publications.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_crud_remotes.py
RENAMED
|
File without changes
|
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_download_content.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_export_import.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_full_mirror.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_pypi_apis.py
RENAMED
|
File without changes
|
{pulp_python-3.27.1 → pulp_python-3.28.1}/pulp_python/tests/functional/api/test_pypi_simple_api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|