pulp-python 3.25.0__tar.gz → 3.26.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 (93) hide show
  1. {pulp_python-3.25.0 → pulp_python-3.26.0}/CHANGES.md +23 -0
  2. {pulp_python-3.25.0 → pulp_python-3.26.0}/PKG-INFO +1 -1
  3. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/__init__.py +1 -1
  4. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/provenance.py +1 -1
  5. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/serializers.py +1 -1
  6. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/repair.py +122 -7
  7. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/utils.py +6 -3
  8. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +3 -1
  9. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_pypi_apis.py +3 -1
  10. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_repair.py +119 -2
  11. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_sync.py +3 -1
  12. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_upload.py +5 -2
  13. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/utils.py +3 -2
  14. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/PKG-INFO +1 -1
  15. {pulp_python-3.25.0 → pulp_python-3.26.0}/pyproject.toml +4 -2
  16. {pulp_python-3.25.0 → pulp_python-3.26.0}/COMMITMENT +0 -0
  17. {pulp_python-3.25.0 → pulp_python-3.26.0}/COPYRIGHT +0 -0
  18. {pulp_python-3.25.0 → pulp_python-3.26.0}/LICENSE +0 -0
  19. {pulp_python-3.25.0 → pulp_python-3.26.0}/MANIFEST.in +0 -0
  20. {pulp_python-3.25.0 → pulp_python-3.26.0}/README.md +0 -0
  21. {pulp_python-3.25.0 → pulp_python-3.26.0}/functest_requirements.txt +0 -0
  22. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/__init__.py +0 -0
  23. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/global_access_conditions.py +0 -0
  24. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/management/__init__.py +0 -0
  25. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/management/commands/__init__.py +0 -0
  26. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  27. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  28. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  29. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  30. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  31. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  32. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  33. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  34. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  35. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  36. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  37. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  38. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  39. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  40. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  41. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  42. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  43. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  44. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
  45. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
  46. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -0
  47. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/migrations/__init__.py +0 -0
  48. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/modelresource.py +0 -0
  49. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/models.py +0 -0
  50. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/pypi/__init__.py +0 -0
  51. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/pypi/serializers.py +0 -0
  52. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/pypi/views.py +0 -0
  53. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/replica.py +0 -0
  54. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/settings.py +0 -0
  55. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/__init__.py +0 -0
  56. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/publish.py +0 -0
  57. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/sync.py +0 -0
  58. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/upload.py +0 -0
  59. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/tasks/vulnerability_report.py +0 -0
  60. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/urls.py +0 -0
  61. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/viewsets.py +0 -0
  62. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  63. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  64. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  65. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/pytest_plugin.py +0 -0
  66. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/__init__.py +0 -0
  67. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/__init__.py +0 -0
  68. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  69. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_attestations.py +0 -0
  70. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  71. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  72. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  73. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  74. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
  75. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  76. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  77. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  78. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_pypi_simple_api.py +0 -0
  79. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  80. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
  81. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -0
  82. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -0
  83. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/functional/constants.py +0 -0
  84. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/unit/__init__.py +0 -0
  85. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python/tests/unit/test_models.py +0 -0
  86. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/SOURCES.txt +0 -0
  87. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  88. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/entry_points.txt +0 -0
  89. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/requires.txt +0 -0
  90. {pulp_python-3.25.0 → pulp_python-3.26.0}/pulp_python.egg-info/top_level.txt +0 -0
  91. {pulp_python-3.25.0 → pulp_python-3.26.0}/setup.cfg +0 -0
  92. {pulp_python-3.25.0 → pulp_python-3.26.0}/test_requirements.txt +0 -0
  93. {pulp_python-3.25.0 → pulp_python-3.26.0}/unittest_requirements.txt +0 -0
@@ -8,6 +8,29 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 3.26.0 (2026-02-26) {: #3.26.0 }
12
+
13
+ #### Features {: #3.26.0-feature }
14
+
15
+ - Added support for recreating and fixing metadata files to `repair_metadata` endpoint.
16
+ [#1099](https://github.com/pulp/pulp_python/issues/1099)
17
+
18
+ #### Bugfixes {: #3.26.0-bugfix }
19
+
20
+ - Fixed edge case where metadata file did not match wheel metadata.
21
+ [#1101](https://github.com/pulp/pulp_python/issues/1101)
22
+
23
+ ---
24
+
25
+ ## 3.25.1 (2026-02-16) {: #3.25.1 }
26
+
27
+ #### Bugfixes {: #3.25.1-bugfix }
28
+
29
+ - Fixed edge case where metadata file did not match wheel metadata.
30
+ [#1101](https://github.com/pulp/pulp_python/issues/1101)
31
+
32
+ ---
33
+
11
34
  ## 3.25.0 (2026-02-10) {: #3.25.0 }
12
35
 
13
36
  #### Features {: #3.25.0-feature }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.25.0
3
+ Version: 3.26.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.25.0"
13
+ version = "3.26.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -58,7 +58,7 @@ class Provenance(BaseModel):
58
58
  attestation_bundles: list[AttestationBundle]
59
59
 
60
60
 
61
- def verify_provenance(filename, sha256, provenance, offline=False):
61
+ def verify_provenance(filename, sha256, provenance, offline=True):
62
62
  """Verify the provenance object is valid for the package."""
63
63
  dist = Distribution(name=filename, digest=sha256)
64
64
  for bundle in provenance.attestation_bundles:
@@ -352,7 +352,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
352
352
  raise serializers.ValidationError(_("Invalid attestations: {}".format(e)))
353
353
  return attestations
354
354
 
355
- def handle_attestations(self, filename, sha256, attestations, offline=False):
355
+ def handle_attestations(self, filename, sha256, attestations, offline=True):
356
356
  """Handle converting attestations to a Provenance object."""
357
357
  user = get_current_authenticated_user()
358
358
  publisher = AnyPublisher(kind="Pulp User", prn=get_prn(user))
@@ -8,11 +8,12 @@ from django.db.models import Prefetch
8
8
  from django.db.models.query import QuerySet
9
9
  from pulp_python.app.models import PythonPackageContent, PythonRepository
10
10
  from pulp_python.app.utils import (
11
+ artifact_to_metadata_artifact,
11
12
  artifact_to_python_content_data,
12
13
  fetch_json_release_metadata,
13
14
  parse_metadata,
14
15
  )
15
- from pulpcore.plugin.models import ContentArtifact, ProgressReport
16
+ from pulpcore.plugin.models import Artifact, ContentArtifact, ProgressReport
16
17
  from pulpcore.plugin.util import get_domain
17
18
 
18
19
  log = logging.getLogger(__name__)
@@ -41,16 +42,25 @@ def repair(repository_pk: UUID) -> None:
41
42
  content_set = repository.latest_version().content.values_list("pk", flat=True)
42
43
  content = PythonPackageContent.objects.filter(pk__in=content_set)
43
44
 
44
- num_repaired, pkgs_not_repaired = repair_metadata(content)
45
+ num_repaired, pkgs_not_repaired, num_metadata_repaired, pkgs_metadata_not_repaired = (
46
+ repair_metadata(content)
47
+ )
48
+ # Convert set() to 0
49
+ if not pkgs_not_repaired:
50
+ pkgs_not_repaired = 0
51
+ if not pkgs_metadata_not_repaired:
52
+ pkgs_metadata_not_repaired = 0
53
+
45
54
  log.info(
46
55
  _(
47
56
  "{} packages' metadata repaired. Not repaired packages due to either "
48
- "inaccessible URL or mismatched sha256: {}."
49
- ).format(num_repaired, pkgs_not_repaired)
57
+ "inaccessible URL or mismatched sha256: {}. "
58
+ "{} metadata files repaired. Packages whose metadata files could not be repaired: {}."
59
+ ).format(num_repaired, pkgs_not_repaired, num_metadata_repaired, pkgs_metadata_not_repaired)
50
60
  )
51
61
 
52
62
 
53
- def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[str]]:
63
+ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[str], int, set[str]]:
54
64
  """
55
65
  Repairs metadata for a queryset of PythonPackageContent objects
56
66
  and updates the progress report.
@@ -59,9 +69,11 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
59
69
  content (QuerySet[PythonPackageContent]): The queryset of items to repair.
60
70
 
61
71
  Returns:
62
- tuple[int, set[str]]: A tuple containing:
72
+ tuple[int, set[str], int, set[str]]: A tuple containing:
63
73
  - The number of packages that were repaired.
64
74
  - A set of packages' PKs that were not repaired.
75
+ - The number of metadata files that were repaired.
76
+ - A set of packages' PKs without repaired metadata artifacts.
65
77
  """
66
78
  immediate_content = (
67
79
  content.filter(contentartifact__artifact__isnull=False)
@@ -87,6 +99,11 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
87
99
  # Keep track of on-demand packages that were not repaired
88
100
  pkgs_not_repaired = set()
89
101
 
102
+ # Metadata artifacts and content artifacts
103
+ metadata_batch = []
104
+ total_metadata_repaired = 0
105
+ pkgs_metadata_not_repaired = set()
106
+
90
107
  progress_report = ProgressReport(
91
108
  message="Repairing packages' metadata",
92
109
  code="repair.metadata",
@@ -102,6 +119,13 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
102
119
  .artifact
103
120
  )
104
121
  new_data = artifact_to_python_content_data(package.filename, main_artifact, domain)
122
+ total_metadata_repaired += update_metadata_artifact_if_needed(
123
+ package,
124
+ new_data.get("metadata_sha256"),
125
+ main_artifact,
126
+ metadata_batch,
127
+ pkgs_metadata_not_repaired,
128
+ )
105
129
  total_repaired += update_package_if_needed(
106
130
  package, new_data, batch, set_of_update_fields
107
131
  )
@@ -163,7 +187,12 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
163
187
  total_repaired += len(batch)
164
188
  PythonPackageContent.objects.bulk_update(batch, set_of_update_fields)
165
189
 
166
- return total_repaired, pkgs_not_repaired
190
+ if metadata_batch:
191
+ not_repaired = _process_metadata_batch(metadata_batch)
192
+ pkgs_metadata_not_repaired.update(not_repaired)
193
+ total_metadata_repaired += len(metadata_batch) - len(not_repaired)
194
+
195
+ return total_repaired, pkgs_not_repaired, total_metadata_repaired, pkgs_metadata_not_repaired
167
196
 
168
197
 
169
198
  def update_package_if_needed(
@@ -202,3 +231,89 @@ def update_package_if_needed(
202
231
  set_of_update_fields.clear()
203
232
 
204
233
  return total_repaired
234
+
235
+
236
+ def update_metadata_artifact_if_needed(
237
+ package: PythonPackageContent,
238
+ new_metadata_sha256: str | None,
239
+ main_artifact: Artifact,
240
+ metadata_batch: list[tuple],
241
+ pkgs_metadata_not_repaired: set[str],
242
+ ) -> int:
243
+ """
244
+ Repairs metadata artifacts for wheel packages by creating missing metadata artifacts
245
+ or updating existing ones when the metadata_sha256 differs. Only processes wheel files
246
+ that have a valid new_metadata_sha256. Queues operations for batch processing.
247
+
248
+ Args:
249
+ package: Package to check for metadata changes.
250
+ 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.
252
+ metadata_batch: List of tuples for batch processing (updated in-place).
253
+ pkgs_metadata_not_repaired: Set of package PKs that failed repair (updated in-place).
254
+
255
+ Returns:
256
+ Number of repaired metadata artifacts (only when batch is flushed at BULK_SIZE).
257
+ """
258
+ total_metadata_repaired = 0
259
+
260
+ if not package.filename.endswith(".whl") or not new_metadata_sha256:
261
+ return total_metadata_repaired
262
+
263
+ original_metadata_sha256 = package.metadata_sha256
264
+ cas = package.contentartifact_set.filter(relative_path__endswith=".metadata")
265
+
266
+ # Create missing
267
+ if not cas:
268
+ metadata_batch.append((package, main_artifact))
269
+ # Fix existing
270
+ elif new_metadata_sha256 != original_metadata_sha256:
271
+ ca = cas.first()
272
+ metadata_artifact = ca.artifact
273
+ if metadata_artifact is None or (metadata_artifact.sha256 != new_metadata_sha256):
274
+ metadata_batch.append((package, main_artifact))
275
+
276
+ if len(metadata_batch) == BULK_SIZE:
277
+ not_repaired = _process_metadata_batch(metadata_batch)
278
+ pkgs_metadata_not_repaired.update(not_repaired)
279
+ total_metadata_repaired += BULK_SIZE - len(not_repaired)
280
+ metadata_batch.clear()
281
+
282
+ return total_metadata_repaired
283
+
284
+
285
+ def _process_metadata_batch(metadata_batch: list[tuple]) -> set[str]:
286
+ """
287
+ Processes a batch of metadata repair operations by creating metadata artifacts
288
+ and their corresponding ContentArtifacts.
289
+
290
+ Args:
291
+ metadata_batch: List of (package, main_artifact) tuples.
292
+
293
+ Returns:
294
+ Set of package PKs for which metadata artifacts could not be created.
295
+ """
296
+ not_repaired = set()
297
+ content_artifacts = []
298
+
299
+ for package, main_artifact in metadata_batch:
300
+ metadata_artifact = artifact_to_metadata_artifact(package.filename, main_artifact)
301
+ if metadata_artifact:
302
+ ca = ContentArtifact(
303
+ artifact=metadata_artifact,
304
+ content=package,
305
+ relative_path=f"{package.filename}.metadata",
306
+ )
307
+ content_artifacts.append(ca)
308
+ else:
309
+ not_repaired.add(package.pk)
310
+
311
+ if content_artifacts:
312
+ ContentArtifact.objects.bulk_create(
313
+ content_artifacts,
314
+ update_conflicts=True,
315
+ update_fields=["artifact"],
316
+ unique_fields=["content", "relative_path"],
317
+ )
318
+
319
+ return not_repaired
@@ -220,9 +220,12 @@ def extract_wheel_metadata(filename: str) -> bytes | None:
220
220
  return None
221
221
  try:
222
222
  with zipfile.ZipFile(filename, "r") as f:
223
- for file_path in f.namelist():
224
- if file_path.endswith(".dist-info/METADATA"):
225
- return f.read(file_path)
223
+ metadata_paths = [p for p in f.namelist() if p.endswith("METADATA")]
224
+ sorted_paths = sorted(metadata_paths, key=lambda s: s.count("/"))
225
+ for metadata_path in sorted_paths:
226
+ file = f.read(metadata_path)
227
+ if b"Metadata-Version" in file:
228
+ return file
226
229
  except (zipfile.BadZipFile, KeyError, OSError) as e:
227
230
  log.warning(f"Failed to extract metadata file from {filename}: {e}")
228
231
  return None
@@ -200,4 +200,6 @@ def test_package_creation_with_metadata(
200
200
  distro = python_distribution_factory(repository=python_repo)
201
201
 
202
202
  # Test that metadata is accessible
203
- ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
203
+ ensure_metadata(
204
+ pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME, "shelf-reader", "0.1"
205
+ )
@@ -165,7 +165,9 @@ def test_package_upload_with_metadata(
165
165
  assert summary.added["python.python"]["count"] == 1
166
166
 
167
167
  # Test that metadata is accessible
168
- ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
168
+ ensure_metadata(
169
+ pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME, "shelf-reader", "0.1"
170
+ )
169
171
 
170
172
 
171
173
  @pytest.mark.parallel
@@ -10,7 +10,7 @@ from pulp_python.tests.functional.constants import (
10
10
 
11
11
  @pytest.fixture
12
12
  def create_content_direct(python_bindings):
13
- def _create(artifact_filename, content_data):
13
+ def _create(artifact_filename, content_data, metadata_artifact_filename=None):
14
14
  commands = (
15
15
  "from pulpcore.plugin.models import Artifact, ContentArtifact; "
16
16
  "from pulpcore.plugin.util import get_url; "
@@ -21,8 +21,16 @@ def create_content_direct(python_bindings):
21
21
  "c.save(); "
22
22
  f"ca = ContentArtifact(artifact=a, content=c, relative_path=c.filename); "
23
23
  "ca.save(); "
24
- "print(get_url(c))"
25
24
  )
25
+ if metadata_artifact_filename:
26
+ commands += (
27
+ f"a2 = Artifact.init_and_validate('{metadata_artifact_filename}'); "
28
+ "a2.save(); "
29
+ "ca2_filename = c.filename + '.metadata'; "
30
+ f"ca2 = ContentArtifact(artifact=a2, content=c, relative_path=ca2_filename); "
31
+ "ca2.save(); "
32
+ )
33
+ commands += "print(get_url(c))"
26
34
  process = subprocess.run(["pulpcore-manager", "shell", "-c", commands], capture_output=True)
27
35
 
28
36
  assert process.returncode == 0
@@ -214,3 +222,112 @@ def test_metadata_repair_endpoint(
214
222
  assert new_content.author == author
215
223
  assert new_content.packagetype == packagetype
216
224
  assert new_content.requires_python == requires_python
225
+
226
+
227
+ def test_metadata_artifact_repair_endpoint(
228
+ create_content_direct,
229
+ delete_orphans_pre,
230
+ download_python_file,
231
+ monitor_task,
232
+ move_to_repository,
233
+ pulpcore_bindings,
234
+ python_bindings,
235
+ python_repo_factory,
236
+ ):
237
+ """
238
+ Test repairing of PythonPackageContent's metadata_sha256 and its metadata Artifact
239
+ and ContentArtifact via `Repositories.repair_metadata` endpoint.
240
+ """
241
+ # 1. Setup tested data
242
+ python_repo = python_repo_factory()
243
+
244
+ # missing metadata_sha256, missing metadata Artifact + ContentArtifact
245
+ filename_1 = "scipy-1.1.0-cp27-none-win_amd64.whl"
246
+ metadata_1 = None
247
+ url_1 = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), filename_1)
248
+ file_1 = download_python_file(filename_1, url_1)
249
+
250
+ # correct metadata_sha256, missing metadata Artifact + ContentArtifact
251
+ filename_2 = "scipy-1.1.0-cp27-cp27m-manylinux1_x86_64.whl"
252
+ metadata_2 = "7f303850d9be88fff27eaeb393c2fd3a6c1a130e21758b8294fc5bb2f38e02f6"
253
+ url_2 = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), filename_2)
254
+ file_2 = download_python_file(filename_2, url_2)
255
+
256
+ # wrong metadata_sha256, missing metadata Artifact + ContentArtifact
257
+ filename_3 = "scipy-1.1.0-cp34-none-win32.whl"
258
+ metadata_3 = "1234"
259
+ url_3 = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), filename_3)
260
+ file_3 = download_python_file(filename_3, url_3)
261
+
262
+ # wrong metadata_sha256, wrong metadata Artifact, correct metadata ContentArtifact
263
+ filename_4 = "scipy-1.1.0-cp35-none-win32.whl"
264
+ metadata_4 = "5678"
265
+ url_4 = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), filename_4)
266
+ file_4 = download_python_file(filename_4, url_4)
267
+ metadata_file_4 = download_python_file(
268
+ f"{filename_1}.metadata",
269
+ urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), f"{filename_1}.metadata"),
270
+ )
271
+
272
+ # Build PythonPackageContent data
273
+ filenames = [filename_1, filename_2, filename_3, filename_4]
274
+ metadata_sha256s = [metadata_1, metadata_2, metadata_3, metadata_4]
275
+ data_1, data_2, data_3, data_4 = [
276
+ {"name": "scipy", "version": "1.1.0", "filename": f, "metadata_sha256": m}
277
+ for f, m in zip(filenames, metadata_sha256s)
278
+ ]
279
+
280
+ # 2. Create content
281
+ content_1 = create_content_direct(file_1, data_1)
282
+ content_2 = create_content_direct(file_2, data_2)
283
+ content_3 = create_content_direct(file_3, data_3)
284
+ content_4 = create_content_direct(file_4, data_4, metadata_file_4)
285
+
286
+ content_hrefs = {}
287
+ for data, content in [
288
+ (data_1, content_1),
289
+ (data_2, content_2),
290
+ (data_3, content_3),
291
+ (data_4, content_4),
292
+ ]:
293
+ for field, test_value in data.items():
294
+ assert getattr(content, field) == test_value
295
+ content_hrefs[data["filename"]] = content.pulp_href
296
+ move_to_repository(python_repo.pulp_href, list(content_hrefs.values()))
297
+
298
+ # 3. Repair metadata and metadata files
299
+ response = python_bindings.RepositoriesPythonApi.repair_metadata(python_repo.pulp_href)
300
+ monitor_task(response.task)
301
+
302
+ # 4. Check new metadata and metadata files
303
+ main_artifact_hrefs = set()
304
+ metadata_artifact_hrefs = set()
305
+ new_data = [
306
+ (filename_1, "15ae132303b2774a0d839d01c618cf99fc92716adfaaa2bc1267142ab2b76b98"),
307
+ (filename_2, "7f303850d9be88fff27eaeb393c2fd3a6c1a130e21758b8294fc5bb2f38e02f6"),
308
+ # filename_3 and filename_4 have the same metadata file
309
+ (filename_3, "747d24e500308067c4e5fd0e20fb2d4fd6595a3fb7b1d2ffa717217fb6a53364"),
310
+ (filename_4, "747d24e500308067c4e5fd0e20fb2d4fd6595a3fb7b1d2ffa717217fb6a53364"),
311
+ ]
312
+ for filename, metadata_sha256 in new_data:
313
+ content = pulpcore_bindings.ContentApi.list(pulp_href__in=[content_hrefs[filename]]).results
314
+ assert content
315
+ artifacts = content[0].artifacts
316
+ assert len(artifacts) == 2
317
+
318
+ main_artifact_href = artifacts.get(filename)
319
+ main_artifact_hrefs.add(main_artifact_href)
320
+ main_artifact = pulpcore_bindings.ArtifactsApi.read(main_artifact_href)
321
+
322
+ metadata_artifact_href = artifacts.get(f"{filename}.metadata")
323
+ metadata_artifact_hrefs.add(metadata_artifact_href)
324
+ metadata_artifact = pulpcore_bindings.ArtifactsApi.read(metadata_artifact_href)
325
+
326
+ pkg = python_bindings.ContentPackagesApi.read(content_hrefs[filename])
327
+ assert pkg.metadata_sha256 == metadata_sha256
328
+ assert main_artifact.sha256 == pkg.sha256
329
+ assert metadata_artifact.sha256 == pkg.metadata_sha256
330
+
331
+ # Check deduplication
332
+ assert len(main_artifact_hrefs) == 4
333
+ assert len(metadata_artifact_hrefs) == 3
@@ -354,4 +354,6 @@ def test_package_sync_with_metadata(
354
354
  distro = python_distribution_factory(repository=repo)
355
355
 
356
356
  # Test that metadata is accessible
357
- ensure_metadata(pulp_content_url, distro.base_path, "pytz-2023.2-py2.py3-none-any.whl")
357
+ ensure_metadata(
358
+ pulp_content_url, distro.base_path, "pytz-2023.2-py2.py3-none-any.whl", "pytz", "2023.2"
359
+ )
@@ -3,6 +3,7 @@ import requests
3
3
  from pulp_python.tests.functional.constants import (
4
4
  PYTHON_EGG_FILENAME,
5
5
  PYTHON_EGG_URL,
6
+ PYTHON_FIXTURES_URL,
6
7
  PYTHON_WHEEL_FILENAME,
7
8
  PYTHON_WHEEL_URL,
8
9
  PYTHON_EGG_SHA256,
@@ -61,7 +62,9 @@ def test_synchronous_package_upload_with_metadata(
61
62
  """
62
63
  Test that the synchronous upload of a Python wheel package creates a metadata artifact.
63
64
  """
64
- python_file = download_python_file(PYTHON_WHEEL_FILENAME, PYTHON_WHEEL_URL)
65
+ wheel_filename = "setuptools-80.9.0-py3-none-any.whl"
66
+ wheel_url = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), wheel_filename)
67
+ python_file = download_python_file(wheel_filename, wheel_url)
65
68
  content_body = {"file": python_file}
66
69
  content = python_bindings.ContentPackagesApi.upload(**content_body)
67
70
 
@@ -70,7 +73,7 @@ def test_synchronous_package_upload_with_metadata(
70
73
  distro = python_distribution_factory(repository=python_repo)
71
74
 
72
75
  # Test that metadata is accessible
73
- ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
76
+ ensure_metadata(pulp_content_url, distro.base_path, wheel_filename, "setuptools", "80.9.0")
74
77
 
75
78
 
76
79
  @pytest.mark.parallel
@@ -123,7 +123,7 @@ def ensure_simple(
123
123
  return len(msgs) == 0, msgs
124
124
 
125
125
 
126
- def ensure_metadata(pulp_content_url, distro_base_path, filename):
126
+ def ensure_metadata(pulp_content_url, distro_base_path, filename, name, version):
127
127
  """
128
128
  Tests that metadata is accessible for a given wheel package filename.
129
129
  """
@@ -132,4 +132,5 @@ def ensure_metadata(pulp_content_url, distro_base_path, filename):
132
132
  metadata_response = requests.get(metadata_url)
133
133
  assert metadata_response.status_code == 200
134
134
  assert len(metadata_response.content) > 0
135
- assert "Name: " in metadata_response.text
135
+ assert f"Name: {name}" in metadata_response.text
136
+ assert f"Version: {version}" in metadata_response.text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.25.0
3
+ Version: 3.26.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
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-python"
10
- version = "3.25.0"
10
+ version = "3.26.0"
11
11
  description = "pulp-python plugin for the Pulp Project"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -61,7 +61,9 @@ underlines = ["", "", ""]
61
61
 
62
62
  [tool.check-manifest]
63
63
  ignore = [
64
+ "AGENTS.md",
64
65
  "CHANGES/**",
66
+ "CLAUDE.md",
65
67
  "dev_requirements.txt",
66
68
  "doc_requirements.txt",
67
69
  "docs/**",
@@ -77,7 +79,7 @@ ignore = [
77
79
  [tool.bumpversion]
78
80
  # This section is managed by the plugin template. Do not edit manually.
79
81
 
80
- current_version = "3.25.0"
82
+ current_version = "3.26.0"
81
83
  commit = false
82
84
  tag = false
83
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