pulp-python 3.22.2__py3-none-any.whl → 3.23.0__py3-none-any.whl

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.
@@ -10,7 +10,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
10
10
 
11
11
  name = "pulp_python.app"
12
12
  label = "python"
13
- version = "3.22.2"
13
+ version = "3.23.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -24,8 +24,14 @@ def repair_metadata(content):
24
24
  set_of_update_fields = set()
25
25
  total_repaired = 0
26
26
  for package in immediate_content.prefetch_related("_artifacts").iterator(chunk_size=1000):
27
+ # Get the main artifact
28
+ main_artifact = (
29
+ package.contentartifact_set.exclude(relative_path__endswith=".metadata")
30
+ .first()
31
+ .artifact
32
+ )
27
33
  new_data = artifact_to_python_content_data(
28
- package.filename, package._artifacts.get(), package.pulp_domain
34
+ package.filename, main_artifact, package.pulp_domain
29
35
  )
30
36
  changed = False
31
37
  for field, value in new_data.items():
@@ -0,0 +1,201 @@
1
+ # Generated manually on 2025-12-15 14:00 for creating missing metadata artifacts
2
+
3
+ from django.db import migrations
4
+
5
+ BATCH_SIZE = 1000
6
+
7
+
8
+ def pulp_hashlib_new(name, *args, **kwargs):
9
+ """
10
+ Copied and updated (to comply with migrations) from pulpcore.
11
+ """
12
+ import hashlib as the_real_hashlib
13
+ from django.conf import settings
14
+
15
+ if name not in settings.ALLOWED_CONTENT_CHECKSUMS:
16
+ return None
17
+
18
+ return the_real_hashlib.new(name, *args, **kwargs)
19
+
20
+
21
+ def init_and_validate(file, artifact_model, expected_digests):
22
+ """
23
+ Copied and updated (to comply with migrations) from pulpcore.
24
+ """
25
+ from django.conf import settings
26
+
27
+ digest_fields = []
28
+ for alg in ("sha512", "sha384", "sha256", "sha224", "sha1", "md5"):
29
+ if alg in settings.ALLOWED_CONTENT_CHECKSUMS:
30
+ digest_fields.append(alg)
31
+
32
+ if isinstance(file, str):
33
+ with open(file, "rb") as f:
34
+ hashers = {
35
+ n: hasher for n in digest_fields if (hasher := pulp_hashlib_new(n)) is not None
36
+ }
37
+ if not hashers:
38
+ return None
39
+
40
+ size = 0
41
+ while True:
42
+ chunk = f.read(1048576) # 1 megabyte
43
+ if not chunk:
44
+ break
45
+ for algorithm in hashers.values():
46
+ algorithm.update(chunk)
47
+ size = size + len(chunk)
48
+ else:
49
+ size = file.size
50
+ hashers = file.hashers
51
+
52
+ mismatched_sha256 = None
53
+ for algorithm, expected_digest in expected_digests.items():
54
+ if algorithm not in hashers:
55
+ return None
56
+ actual_digest = hashers[algorithm].hexdigest()
57
+ if expected_digest != actual_digest:
58
+ # Store the actual value for later fixing if it differs from the package value
59
+ mismatched_sha256 = actual_digest
60
+
61
+ attributes = {"size": size, "file": file}
62
+ for algorithm in digest_fields:
63
+ attributes[algorithm] = hashers[algorithm].hexdigest()
64
+
65
+ return artifact_model(**attributes), mismatched_sha256
66
+
67
+
68
+ def extract_wheel_metadata(filename):
69
+ """
70
+ Extract the metadata file content from a wheel file.
71
+ Return the raw metadata content as bytes or None if metadata cannot be extracted.
72
+ """
73
+ import zipfile
74
+
75
+ try:
76
+ with zipfile.ZipFile(filename, "r") as f:
77
+ for file_path in f.namelist():
78
+ if file_path.endswith(".dist-info/METADATA"):
79
+ return f.read(file_path)
80
+ except (zipfile.BadZipFile, KeyError, OSError):
81
+ pass
82
+ return None
83
+
84
+
85
+ def artifact_to_metadata_artifact(filename, artifact, md_digests, tmp_dir, artifact_model):
86
+ """
87
+ Create artifact for metadata from the provided wheel artifact.
88
+ Return (artifact, mismatched_sha256) on success, None on any failure.
89
+ """
90
+ import shutil
91
+ import tempfile
92
+
93
+ with tempfile.NamedTemporaryFile("wb", dir=tmp_dir, suffix=filename, delete=False) as temp_file:
94
+ temp_wheel_path = temp_file.name
95
+ artifact.file.seek(0)
96
+ shutil.copyfileobj(artifact.file, temp_file)
97
+ temp_file.flush()
98
+
99
+ metadata_content = extract_wheel_metadata(temp_wheel_path)
100
+ if not metadata_content:
101
+ return None
102
+
103
+ with tempfile.NamedTemporaryFile(
104
+ "wb", dir=tmp_dir, suffix=".metadata", delete=False
105
+ ) as temp_md:
106
+ temp_metadata_path = temp_md.name
107
+ temp_md.write(metadata_content)
108
+ temp_md.flush()
109
+
110
+ return init_and_validate(temp_metadata_path, artifact_model, md_digests)
111
+
112
+
113
+ def create_missing_metadata_artifacts(apps, schema_editor):
114
+ """
115
+ Create metadata artifacts for PythonPackageContent instances that have metadata_sha256
116
+ but are missing the corresponding metadata artifact.
117
+ """
118
+ import tempfile
119
+ from django.conf import settings
120
+ from django.db import models
121
+
122
+ PythonPackageContent = apps.get_model("python", "PythonPackageContent")
123
+ ContentArtifact = apps.get_model("core", "ContentArtifact")
124
+ Artifact = apps.get_model("core", "Artifact")
125
+
126
+ packages = (
127
+ PythonPackageContent.objects.filter(
128
+ metadata_sha256__isnull=False,
129
+ filename__endswith=".whl",
130
+ contentartifact__artifact__isnull=False,
131
+ contentartifact__relative_path=models.F("filename"),
132
+ )
133
+ .exclude(metadata_sha256="")
134
+ .prefetch_related("_artifacts")
135
+ .only("filename", "metadata_sha256")
136
+ )
137
+ artifact_batch = []
138
+ contentartifact_batch = []
139
+ packages_batch = []
140
+
141
+ with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_dir:
142
+ for package in packages:
143
+ # Get the main artifact for package
144
+ main_artifact = package._artifacts.get()
145
+
146
+ filename = package.filename
147
+ metadata_digests = {"sha256": package.metadata_sha256}
148
+ result = artifact_to_metadata_artifact(
149
+ filename, main_artifact, metadata_digests, temp_dir, Artifact
150
+ )
151
+ if result is None:
152
+ # Unset metadata_sha256 when extraction or validation fails
153
+ package.metadata_sha256 = None
154
+ packages_batch.append(package)
155
+ continue
156
+ metadata_artifact, mismatched_sha256 = result
157
+ if mismatched_sha256:
158
+ # Fix the package if its metadata_sha256 differs from the actual value
159
+ package.metadata_sha256 = mismatched_sha256
160
+ packages_batch.append(package)
161
+
162
+ contentartifact = ContentArtifact(
163
+ artifact=metadata_artifact,
164
+ content=package,
165
+ relative_path=f"{filename}.metadata",
166
+ )
167
+ artifact_batch.append(metadata_artifact)
168
+ contentartifact_batch.append(contentartifact)
169
+
170
+ if len(artifact_batch) == BATCH_SIZE:
171
+ Artifact.objects.bulk_create(artifact_batch, batch_size=BATCH_SIZE)
172
+ ContentArtifact.objects.bulk_create(contentartifact_batch, batch_size=BATCH_SIZE)
173
+ artifact_batch.clear()
174
+ contentartifact_batch.clear()
175
+ if len(packages_batch) == BATCH_SIZE:
176
+ PythonPackageContent.objects.bulk_update(
177
+ packages_batch, ["metadata_sha256"], batch_size=BATCH_SIZE
178
+ )
179
+ packages_batch.clear()
180
+
181
+ if artifact_batch:
182
+ Artifact.objects.bulk_create(artifact_batch, batch_size=BATCH_SIZE)
183
+ ContentArtifact.objects.bulk_create(contentartifact_batch, batch_size=BATCH_SIZE)
184
+ if packages_batch:
185
+ PythonPackageContent.objects.bulk_update(
186
+ packages_batch, ["metadata_sha256"], batch_size=BATCH_SIZE
187
+ )
188
+
189
+
190
+ class Migration(migrations.Migration):
191
+
192
+ dependencies = [
193
+ ("python", "0018_packageprovenance"),
194
+ ]
195
+
196
+ operations = [
197
+ migrations.RunPython(
198
+ create_missing_metadata_artifacts,
199
+ reverse_code=migrations.RunPython.noop,
200
+ ),
201
+ ]
@@ -352,8 +352,6 @@ class SimpleView(PackageUploadMixin, ViewSet):
352
352
  @extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
353
353
  def retrieve(self, request, path, package):
354
354
  """Retrieves the simple api html/json page for a package."""
355
- media_type = request.accepted_renderer.media_type
356
-
357
355
  repo_ver, content = self.get_rvc()
358
356
  # Should I redirect if the normalized name is different?
359
357
  normalized = canonicalize_name(package)
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import tempfile
3
4
  from gettext import gettext as _
4
5
  from django.conf import settings
5
6
  from django.db.utils import IntegrityError
@@ -22,6 +23,7 @@ from pulp_python.app.provenance import (
22
23
  )
23
24
  from pulp_python.app.utils import (
24
25
  DIST_EXTENSIONS,
26
+ artifact_to_metadata_artifact,
25
27
  artifact_to_python_content_data,
26
28
  get_project_metadata_from_file,
27
29
  parse_project_metadata,
@@ -93,11 +95,31 @@ class PythonDistributionSerializer(core_serializers.DistributionSerializer):
93
95
  model = python_models.PythonDistribution
94
96
 
95
97
 
98
+ class PythonSingleContentArtifactField(core_serializers.SingleContentArtifactField):
99
+ """
100
+ Custom field with overridden get_attribute method. Meant to be used only in
101
+ PythonPackageContentSerializer to handle possible existence of metadata artifact.
102
+ """
103
+
104
+ def get_attribute(self, instance):
105
+ # When content has multiple artifacts (wheel + metadata), return the main one
106
+ if instance._artifacts.count() > 1:
107
+ for ca in instance.contentartifact_set.all():
108
+ if not ca.relative_path.endswith(".metadata"):
109
+ return ca.artifact
110
+
111
+ return super().get_attribute(instance)
112
+
113
+
96
114
  class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploadSerializer):
97
115
  """
98
116
  A Serializer for PythonPackageContent.
99
117
  """
100
118
 
119
+ artifact = PythonSingleContentArtifactField(
120
+ help_text=_("Artifact file representing the physical content"),
121
+ )
122
+
101
123
  # Core metadata
102
124
  # Version 1.0
103
125
  author = serializers.CharField(
@@ -386,8 +408,21 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
386
408
  if attestations := data.pop("attestations", None):
387
409
  data["provenance"] = self.handle_attestations(filename, data["sha256"], attestations)
388
410
 
411
+ # Create metadata artifact for wheel files
412
+ if filename.endswith(".whl"):
413
+ if metadata_artifact := artifact_to_metadata_artifact(filename, artifact):
414
+ data["metadata_artifact"] = metadata_artifact
415
+ data["metadata_sha256"] = metadata_artifact.sha256
416
+
389
417
  return data
390
418
 
419
+ def get_artifacts(self, validated_data):
420
+ artifacts = super().get_artifacts(validated_data)
421
+ if metadata_artifact := validated_data.pop("metadata_artifact", None):
422
+ relative_path = f"{validated_data['filename']}.metadata"
423
+ artifacts[relative_path] = metadata_artifact
424
+ return artifacts
425
+
391
426
  def retrieve(self, validated_data):
392
427
  content = python_models.PythonPackageContent.objects.filter(
393
428
  sha256=validated_data["sha256"], _pulp_domain=get_domain()
@@ -419,6 +454,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
419
454
 
420
455
  class Meta:
421
456
  fields = core_serializers.SingleArtifactContentUploadSerializer.Meta.fields + (
457
+ "artifact",
422
458
  "author",
423
459
  "author_email",
424
460
  "description",
@@ -514,6 +550,15 @@ class PythonPackageContentUploadSerializer(PythonPackageContentSerializer):
514
550
  data["provenance"] = self.handle_attestations(
515
551
  filename, data["sha256"], attestations, offline=True
516
552
  )
553
+ # Create metadata artifact for wheel files
554
+ if filename.endswith(".whl"):
555
+ with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_dir:
556
+ if metadata_artifact := artifact_to_metadata_artifact(
557
+ filename, artifact, tmp_dir=temp_dir
558
+ ):
559
+ data["metadata_artifact"] = metadata_artifact
560
+ data["metadata_sha256"] = metadata_artifact.sha256
561
+
517
562
  return data
518
563
 
519
564
  class Meta(PythonPackageContentSerializer.Meta):
@@ -95,9 +95,13 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
95
95
  progress_report.save()
96
96
  with progress_report:
97
97
  for package in progress_report.iter(immediate_content.iterator(chunk_size=BULK_SIZE)):
98
- new_data = artifact_to_python_content_data(
99
- package.filename, package._artifacts.get(), domain
98
+ # Get the main artifact
99
+ main_artifact = (
100
+ package.contentartifact_set.exclude(relative_path__endswith=".metadata")
101
+ .first()
102
+ .artifact
100
103
  )
104
+ new_data = artifact_to_python_content_data(package.filename, main_artifact, domain)
101
105
  total_repaired += update_package_if_needed(
102
106
  package, new_data, batch, set_of_update_fields
103
107
  )
@@ -113,7 +117,11 @@ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[s
113
117
  grouped_by_url = defaultdict(list)
114
118
 
115
119
  for package in group_set:
116
- for ra in package.contentartifact_set.get().remoteartifact_set.all():
120
+ for ra in (
121
+ package.contentartifact_set.exclude(relative_path__endswith=".metadata")
122
+ .first()
123
+ .remoteartifact_set.all()
124
+ ):
117
125
  grouped_by_url[ra.remote.url].append((package, ra))
118
126
 
119
127
  # Prioritize the URL that can serve the most packages
@@ -229,11 +229,15 @@ class PulpMirror(Mirror):
229
229
  create a Content Unit to put into the pipeline
230
230
  """
231
231
  declared_contents = {}
232
+ page = await aget_remote_simple_page(pkg.name, self.remote)
233
+ upstream_pkgs = {pkg.filename: pkg for pkg in page.packages}
234
+
232
235
  for version, dists in pkg.releases.items():
233
236
  for package in dists:
234
237
  entry = parse_metadata(pkg.info, version, package)
235
238
  url = entry.pop("url")
236
239
  size = package["size"] or None
240
+ d_artifacts = []
237
241
 
238
242
  artifact = Artifact(sha256=entry["sha256"], size=size)
239
243
  package = PythonPackageContent(**entry)
@@ -245,11 +249,29 @@ class PulpMirror(Mirror):
245
249
  remote=self.remote,
246
250
  deferred_download=self.deferred_download,
247
251
  )
248
- dc = DeclarativeContent(content=package, d_artifacts=[da])
252
+ d_artifacts.append(da)
253
+
254
+ if upstream_pkg := upstream_pkgs.get(entry["filename"]):
255
+ if upstream_pkg.has_metadata:
256
+ url = upstream_pkg.metadata_url
257
+ md_sha256 = upstream_pkg.metadata_digests.get("sha256")
258
+ package.metadata_sha256 = md_sha256
259
+ artifact = Artifact(sha256=md_sha256)
260
+
261
+ metadata_artifact = DeclarativeArtifact(
262
+ artifact=artifact,
263
+ url=url,
264
+ relative_path=f"{entry['filename']}.metadata",
265
+ remote=self.remote,
266
+ deferred_download=self.deferred_download,
267
+ )
268
+ d_artifacts.append(metadata_artifact)
269
+
270
+ dc = DeclarativeContent(content=package, d_artifacts=d_artifacts)
249
271
  declared_contents[entry["filename"]] = dc
250
272
  await self.python_stage.put(dc)
251
273
 
252
- if pkg.releases and (page := await aget_remote_simple_page(pkg.name, self.remote)):
274
+ if pkg.releases and page:
253
275
  if self.remote.provenance:
254
276
  await self.sync_provenance(page, declared_contents)
255
277
 
@@ -15,7 +15,7 @@ from pulp_python.app.provenance import (
15
15
  Provenance,
16
16
  verify_provenance,
17
17
  )
18
- from pulp_python.app.utils import artifact_to_python_content_data
18
+ from pulp_python.app.utils import artifact_to_metadata_artifact, artifact_to_python_content_data
19
19
 
20
20
 
21
21
  def upload(artifact_sha256, filename, attestations=None, repository_pk=None):
@@ -97,6 +97,11 @@ def create_content(artifact_sha256, filename, domain):
97
97
  def create():
98
98
  content = PythonPackageContent.objects.create(**data)
99
99
  ContentArtifact.objects.create(artifact=artifact, content=content, relative_path=filename)
100
+
101
+ if metadata_artifact := artifact_to_metadata_artifact(filename, artifact):
102
+ ContentArtifact.objects.create(
103
+ artifact=metadata_artifact, content=content, relative_path=f"{filename}.metadata"
104
+ )
100
105
  return content
101
106
 
102
107
  new_content = create()
pulp_python/app/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import hashlib
2
+ import logging
2
3
  import pkginfo
3
4
  import re
4
5
  import shutil
@@ -7,15 +8,20 @@ import zipfile
7
8
  import json
8
9
  from aiohttp.client_exceptions import ClientError
9
10
  from collections import defaultdict
11
+ from datetime import timezone
10
12
  from django.conf import settings
11
- from django.utils import timezone
13
+ from django.db.utils import IntegrityError
12
14
  from jinja2 import Template
13
15
  from packaging.utils import canonicalize_name
14
16
  from packaging.requirements import Requirement
15
17
  from packaging.version import parse, InvalidVersion
16
18
  from pypi_simple import ACCEPT_JSON_PREFERRED, ProjectPage
17
- from pulpcore.plugin.models import Remote
19
+ from pulpcore.plugin.models import Artifact, Remote
18
20
  from pulpcore.plugin.exceptions import TimeoutException
21
+ from pulpcore.plugin.util import get_domain
22
+
23
+
24
+ log = logging.getLogger(__name__)
19
25
 
20
26
 
21
27
  PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
@@ -41,6 +47,7 @@ simple_index_template = """<!DOCTYPE html>
41
47
  </html>
42
48
  """
43
49
 
50
+ # TODO in the future: data-requires-python (PEP 503)
44
51
  simple_detail_template = """<!DOCTYPE html>
45
52
  <html>
46
53
  <head>
@@ -49,10 +56,12 @@ simple_detail_template = """<!DOCTYPE html>
49
56
  </head>
50
57
  <body>
51
58
  <h1>Links for {{ project_name }}</h1>
52
- {% for pkg in project_packages %}
53
- <a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal" {% if pkg.provenance -%}
59
+ {%- for pkg in project_packages %}
60
+ <a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
61
+ {%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}"
62
+ {%- endif %} {% if pkg.provenance -%}
54
63
  data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
55
- {% endfor %}
64
+ {%- endfor %}
56
65
  </body>
57
66
  </html>
58
67
  """
@@ -200,11 +209,11 @@ def get_project_metadata_from_file(filename):
200
209
  return metadata
201
210
 
202
211
 
203
- def compute_metadata_sha256(filename: str) -> str | None:
212
+ def extract_wheel_metadata(filename: str) -> bytes | None:
204
213
  """
205
- Compute SHA256 hash of the metadata file from a Python package.
214
+ Extract the metadata file content from a wheel file.
206
215
 
207
- Returns SHA256 hash or None if metadata cannot be extracted.
216
+ Returns the raw metadata content as bytes or None if metadata cannot be extracted.
208
217
  """
209
218
  if not filename.endswith(".whl"):
210
219
  return None
@@ -212,13 +221,22 @@ def compute_metadata_sha256(filename: str) -> str | None:
212
221
  with zipfile.ZipFile(filename, "r") as f:
213
222
  for file_path in f.namelist():
214
223
  if file_path.endswith(".dist-info/METADATA"):
215
- metadata_content = f.read(file_path)
216
- return hashlib.sha256(metadata_content).hexdigest()
217
- except (zipfile.BadZipFile, KeyError, OSError):
218
- pass
224
+ return f.read(file_path)
225
+ except (zipfile.BadZipFile, KeyError, OSError) as e:
226
+ log.warning(f"Failed to extract metadata file from {filename}: {e}")
219
227
  return None
220
228
 
221
229
 
230
+ def compute_metadata_sha256(filename: str) -> str | None:
231
+ """
232
+ Compute SHA256 hash of the metadata file from a Python package.
233
+
234
+ Returns SHA256 hash or None if metadata cannot be extracted.
235
+ """
236
+ metadata_content = extract_wheel_metadata(filename)
237
+ return hashlib.sha256(metadata_content).hexdigest() if metadata_content else None
238
+
239
+
222
240
  def artifact_to_python_content_data(filename, artifact, domain=None):
223
241
  """
224
242
  Takes the artifact/filename and returns the metadata needed to create a PythonPackageContent.
@@ -227,6 +245,7 @@ def artifact_to_python_content_data(filename, artifact, domain=None):
227
245
  # because pkginfo validates that the filename has a valid extension before
228
246
  # reading it
229
247
  with tempfile.NamedTemporaryFile("wb", dir=".", suffix=filename) as temp_file:
248
+ artifact.file.seek(0)
230
249
  shutil.copyfileobj(artifact.file, temp_file)
231
250
  temp_file.flush()
232
251
  metadata = get_project_metadata_from_file(temp_file.name)
@@ -239,6 +258,42 @@ def artifact_to_python_content_data(filename, artifact, domain=None):
239
258
  return data
240
259
 
241
260
 
261
+ def artifact_to_metadata_artifact(
262
+ filename: str, artifact: Artifact, tmp_dir: str = "."
263
+ ) -> Artifact | None:
264
+ """
265
+ Creates artifact for metadata from the provided wheel artifact.
266
+ """
267
+ if not filename.endswith(".whl"):
268
+ return None
269
+
270
+ with tempfile.NamedTemporaryFile("wb", dir=tmp_dir, suffix=filename, delete=False) as temp_file:
271
+ temp_wheel_path = temp_file.name
272
+ artifact.file.seek(0)
273
+ shutil.copyfileobj(artifact.file, temp_file)
274
+ temp_file.flush()
275
+
276
+ metadata_content = extract_wheel_metadata(temp_wheel_path)
277
+ if not metadata_content:
278
+ return None
279
+
280
+ with tempfile.NamedTemporaryFile(
281
+ "wb", dir=tmp_dir, suffix=".metadata", delete=False
282
+ ) as temp_md:
283
+ temp_metadata_path = temp_md.name
284
+ temp_md.write(metadata_content)
285
+ temp_md.flush()
286
+
287
+ metadata_artifact = Artifact.init_and_validate(temp_metadata_path)
288
+ try:
289
+ metadata_artifact.save()
290
+ except IntegrityError:
291
+ metadata_artifact = Artifact.objects.get(
292
+ sha256=metadata_artifact.sha256, pulp_domain=get_domain()
293
+ )
294
+ return metadata_artifact
295
+
296
+
242
297
  def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -> dict:
243
298
  """
244
299
  Fetches metadata for a specific release from PyPI's JSON API. A release can contain
@@ -402,7 +457,9 @@ def python_content_to_download_info(content, base_path, domain=None):
402
457
  _art = models.RemoteArtifact.objects.filter(content_artifact=content_artifact).first()
403
458
  return _art
404
459
 
405
- content_artifact = content.contentartifact_set.first()
460
+ content_artifact = content.contentartifact_set.exclude(
461
+ relative_path__endswith=".metadata"
462
+ ).first()
406
463
  artifact = find_artifact()
407
464
  origin = settings.CONTENT_ORIGIN or settings.PYPI_API_HOSTNAME or ""
408
465
  origin = origin.strip("/")
@@ -10,7 +10,10 @@ from pulp_python.tests.functional.constants import (
10
10
  PYTHON_EGG_FILENAME,
11
11
  PYTHON_EGG_URL,
12
12
  PYTHON_SM_FIXTURE_CHECKSUMS,
13
+ PYTHON_WHEEL_FILENAME,
14
+ PYTHON_WHEEL_URL,
13
15
  )
16
+ from pulp_python.tests.functional.utils import ensure_metadata
14
17
 
15
18
 
16
19
  def test_content_crud(
@@ -179,3 +182,22 @@ def test_upload_metadata_24_spec(python_content_factory):
179
182
  assert content.license_expression == "MIT"
180
183
  assert content.license_file == '["LICENSE"]'
181
184
  break
185
+
186
+
187
+ @pytest.mark.parallel
188
+ def test_package_creation_with_metadata(
189
+ pulp_content_url,
190
+ python_content_factory,
191
+ python_distribution_factory,
192
+ python_repo,
193
+ ):
194
+ """
195
+ Test that the creation of a Python wheel package creates a metadata artifact.
196
+ """
197
+ python_content_factory(
198
+ repository=python_repo, relative_path=PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL
199
+ )
200
+ distro = python_distribution_factory(repository=python_repo)
201
+
202
+ # Test that metadata is accessible
203
+ ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
@@ -16,7 +16,6 @@ from pulp_python.tests.functional.constants import (
16
16
 
17
17
 
18
18
  pytestmark = [
19
- pytest.mark.skipif(settings.DOMAIN_ENABLED, reason="Domains do not support export."),
20
19
  pytest.mark.skipif(
21
20
  "/tmp" not in settings.ALLOWED_EXPORT_PATHS,
22
21
  reason="Cannot run export-tests unless /tmp is in ALLOWED_EXPORT_PATHS "
@@ -5,22 +5,19 @@ import subprocess
5
5
  from urllib.parse import urljoin
6
6
 
7
7
  from pulp_python.tests.functional.constants import (
8
- PYTHON_SM_PROJECT_SPECIFIER,
9
- PYTHON_SM_FIXTURE_RELEASES,
10
- PYTHON_SM_FIXTURE_CHECKSUMS,
8
+ PYPI_SERIAL_CONSTANT,
11
9
  PYTHON_MD_PROJECT_SPECIFIER,
12
10
  PYTHON_MD_PYPI_SUMMARY,
13
11
  PYTHON_EGG_FILENAME,
14
12
  PYTHON_EGG_SHA256,
13
+ PYTHON_WHEEL_FILENAME,
15
14
  PYTHON_WHEEL_SHA256,
16
15
  SHELF_PYTHON_JSON,
17
16
  )
18
-
19
- from pulp_python.tests.functional.utils import ensure_simple
17
+ from pulp_python.tests.functional.utils import ensure_metadata
20
18
 
21
19
 
22
20
  PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
23
- PYPI_SERIAL_CONSTANT = 1000000000
24
21
 
25
22
 
26
23
  @pytest.mark.parallel
@@ -142,6 +139,35 @@ def test_package_upload_simple(
142
139
  assert summary.added["python.python"]["count"] == 1
143
140
 
144
141
 
142
+ @pytest.mark.parallel
143
+ def test_package_upload_with_metadata(
144
+ monitor_task,
145
+ pulp_content_url,
146
+ python_content_summary,
147
+ python_empty_repo_distro,
148
+ python_package_dist_directory,
149
+ ):
150
+ """
151
+ Test that the upload of a Python wheel package creates a metadata artifact.
152
+ """
153
+ repo, distro = python_empty_repo_distro()
154
+ url = urljoin(distro.base_url, "simple/")
155
+ dist_dir, egg_file, wheel_file = python_package_dist_directory
156
+ response = requests.post(
157
+ url,
158
+ data={"sha256_digest": PYTHON_WHEEL_SHA256},
159
+ files={"content": open(wheel_file, "rb")},
160
+ auth=("admin", "password"),
161
+ )
162
+ assert response.status_code == 202
163
+ monitor_task(response.json()["task"])
164
+ summary = python_content_summary(repository=repo)
165
+ assert summary.added["python.python"]["count"] == 1
166
+
167
+ # Test that metadata is accessible
168
+ ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
169
+
170
+
145
171
  @pytest.mark.parallel
146
172
  def test_twine_upload(
147
173
  pulpcore_bindings,
@@ -213,22 +239,6 @@ def test_simple_redirect_with_publications(
213
239
  assert response.url == str(urljoin(pulp_content_url, f"{distro.base_path}/simple/"))
214
240
 
215
241
 
216
- @pytest.mark.parallel
217
- def test_simple_correctness_live(
218
- python_remote_factory, python_repo_with_sync, python_distribution_factory
219
- ):
220
- """Checks that the simple api on live distributions are correct."""
221
- remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
222
- repo = python_repo_with_sync(remote)
223
- distro = python_distribution_factory(repository=repo)
224
- proper, msgs = ensure_simple(
225
- urljoin(distro.base_url, "simple/"),
226
- PYTHON_SM_FIXTURE_RELEASES,
227
- sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS,
228
- )
229
- assert proper is True, msgs
230
-
231
-
232
242
  @pytest.mark.parallel
233
243
  def test_pypi_json(python_remote_factory, python_repo_with_sync, python_distribution_factory):
234
244
  """Checks the data of `pypi/{package_name}/json` endpoint."""
@@ -4,21 +4,85 @@ import pytest
4
4
  import requests
5
5
 
6
6
  from pulp_python.tests.functional.constants import (
7
+ PYPI_SERIAL_CONSTANT,
7
8
  PYTHON_EGG_FILENAME,
9
+ PYTHON_EGG_SHA256,
8
10
  PYTHON_EGG_URL,
11
+ PYTHON_SM_FIXTURE_CHECKSUMS,
12
+ PYTHON_SM_FIXTURE_RELEASES,
9
13
  PYTHON_SM_PROJECT_SPECIFIER,
10
14
  PYTHON_WHEEL_FILENAME,
15
+ PYTHON_WHEEL_METADATA_SHA256,
16
+ PYTHON_WHEEL_SHA256,
11
17
  PYTHON_WHEEL_URL,
18
+ PYTHON_XS_FIXTURE_CHECKSUMS,
12
19
  )
20
+ from pulp_python.tests.functional.utils import ensure_simple
13
21
 
14
22
  API_VERSION = "1.1"
15
- PYPI_SERIAL_CONSTANT = 1000000000
16
23
 
17
24
  PYPI_TEXT_HTML = "text/html"
18
25
  PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
19
26
  PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
20
27
 
21
28
 
29
+ @pytest.mark.parallel
30
+ def test_simple_html_index_api(
31
+ python_remote_factory, python_repo_with_sync, python_distribution_factory
32
+ ):
33
+ remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
34
+ repo = python_repo_with_sync(remote)
35
+ distro = python_distribution_factory(repository=repo)
36
+
37
+ url = urljoin(distro.base_url, "simple/")
38
+ headers = {"Accept": PYPI_SIMPLE_V1_HTML}
39
+
40
+ response = requests.get(url, headers=headers)
41
+ assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML
42
+ assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
43
+
44
+ proper, msgs = ensure_simple(
45
+ url, PYTHON_SM_FIXTURE_RELEASES, sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS
46
+ )
47
+ assert proper, f"Simple API validation failed: {msgs}"
48
+
49
+
50
+ def test_simple_html_detail_api(
51
+ delete_orphans_pre,
52
+ monitor_task,
53
+ python_bindings,
54
+ python_content_factory,
55
+ python_distribution_factory,
56
+ python_repo_factory,
57
+ ):
58
+ content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
59
+ content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
60
+ body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
61
+
62
+ repo = python_repo_factory()
63
+ monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
64
+ distro = python_distribution_factory(repository=repo)
65
+
66
+ url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
67
+ headers = {"Accept": PYPI_SIMPLE_V1_HTML}
68
+
69
+ response = requests.get(url, headers=headers)
70
+ assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML
71
+ assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
72
+
73
+ metadata_sha_digests = {
74
+ PYTHON_WHEEL_FILENAME: PYTHON_WHEEL_METADATA_SHA256,
75
+ PYTHON_EGG_FILENAME: None, # egg files should not have metadata
76
+ }
77
+ proper, msgs = ensure_simple(
78
+ urljoin(distro.base_url, "simple/"),
79
+ {"shelf-reader": [PYTHON_WHEEL_FILENAME, PYTHON_EGG_FILENAME]},
80
+ sha_digests=PYTHON_XS_FIXTURE_CHECKSUMS,
81
+ metadata_sha_digests=metadata_sha_digests,
82
+ )
83
+ assert proper, f"Simple API validation failed: {msgs}"
84
+
85
+
22
86
  @pytest.mark.parallel
23
87
  def test_simple_json_index_api(
24
88
  python_remote_factory, python_repo_with_sync, python_distribution_factory
@@ -72,27 +136,19 @@ def test_simple_json_detail_api(
72
136
  assert data["versions"] == ["0.1"]
73
137
 
74
138
  # Check data of a wheel
75
- file_whl = next(
76
- (i for i in data["files"] if i["filename"] == "shelf_reader-0.1-py2-none-any.whl"), None
77
- )
139
+ file_whl = next((i for i in data["files"] if i["filename"] == PYTHON_WHEEL_FILENAME), None)
78
140
  assert file_whl is not None, "wheel file not found"
79
141
  assert file_whl["url"]
80
- assert file_whl["hashes"] == {
81
- "sha256": "2eceb1643c10c5e4a65970baf63bde43b79cbdac7de81dae853ce47ab05197e9"
82
- }
142
+ assert file_whl["hashes"] == {"sha256": PYTHON_WHEEL_SHA256}
83
143
  assert file_whl["requires-python"] is None
84
- assert file_whl["data-dist-info-metadata"] == {
85
- "sha256": "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350"
86
- }
144
+ assert file_whl["data-dist-info-metadata"] == {"sha256": PYTHON_WHEEL_METADATA_SHA256}
87
145
  assert file_whl["size"] == 22455
88
146
  assert file_whl["upload-time"] is not None
89
147
  # Check data of a tarball
90
- file_tar = next((i for i in data["files"] if i["filename"] == "shelf-reader-0.1.tar.gz"), None)
148
+ file_tar = next((i for i in data["files"] if i["filename"] == PYTHON_EGG_FILENAME), None)
91
149
  assert file_tar is not None, "tar file not found"
92
150
  assert file_tar["url"]
93
- assert file_tar["hashes"] == {
94
- "sha256": "04cfd8bb4f843e35d51bfdef2035109bdea831b55a57c3e6a154d14be116398c"
95
- }
151
+ assert file_tar["hashes"] == {"sha256": PYTHON_EGG_SHA256}
96
152
  assert file_tar["requires-python"] is None
97
153
  assert file_tar["data-dist-info-metadata"] is False
98
154
  assert file_tar["size"] == 19097
@@ -18,6 +18,7 @@ from pulp_python.tests.functional.constants import (
18
18
  DJANGO_LATEST_3,
19
19
  SCIPY_COUNTS,
20
20
  )
21
+ from pulp_python.tests.functional.utils import ensure_metadata
21
22
 
22
23
 
23
24
  @pytest.mark.parallel
@@ -336,3 +337,21 @@ def test_sync_provenance(python_repo_with_sync, python_remote_factory, python_co
336
337
  summary = python_content_summary(repository_version=repo.latest_version_href)
337
338
  assert summary.present["python.python"]["count"] == 2
338
339
  assert summary.present["python.provenance"]["count"] == 2
340
+
341
+
342
+ @pytest.mark.parallel
343
+ def test_package_sync_with_metadata(
344
+ pulp_content_url,
345
+ python_distribution_factory,
346
+ python_remote_factory,
347
+ python_repo_with_sync,
348
+ ):
349
+ """
350
+ Test that the sync of a Python wheel package creates a metadata artifact.
351
+ """
352
+ remote = python_remote_factory(includes=["pytz"])
353
+ repo = python_repo_with_sync(remote)
354
+ distro = python_distribution_factory(repository=repo)
355
+
356
+ # Test that metadata is accessible
357
+ ensure_metadata(pulp_content_url, distro.base_path, "pytz-2023.2-py2.py3-none-any.whl")
@@ -8,6 +8,7 @@ from pulp_python.tests.functional.constants import (
8
8
  PYTHON_EGG_SHA256,
9
9
  PYTHON_WHEEL_SHA256,
10
10
  )
11
+ from pulp_python.tests.functional.utils import ensure_metadata
11
12
  from urllib.parse import urljoin
12
13
 
13
14
 
@@ -48,6 +49,30 @@ def test_synchronous_package_upload(
48
49
  assert ctx.value.status == 403
49
50
 
50
51
 
52
+ @pytest.mark.parallel
53
+ def test_synchronous_package_upload_with_metadata(
54
+ download_python_file,
55
+ monitor_task,
56
+ pulp_content_url,
57
+ python_bindings,
58
+ python_distribution_factory,
59
+ python_repo,
60
+ ):
61
+ """
62
+ Test that the synchronous upload of a Python wheel package creates a metadata artifact.
63
+ """
64
+ python_file = download_python_file(PYTHON_WHEEL_FILENAME, PYTHON_WHEEL_URL)
65
+ content_body = {"file": python_file}
66
+ content = python_bindings.ContentPackagesApi.upload(**content_body)
67
+
68
+ body = {"add_content_units": [content.pulp_href]}
69
+ monitor_task(python_bindings.RepositoriesPythonApi.modify(python_repo.pulp_href, body).task)
70
+ distro = python_distribution_factory(repository=python_repo)
71
+
72
+ # Test that metadata is accessible
73
+ ensure_metadata(pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME)
74
+
75
+
51
76
  @pytest.mark.parallel
52
77
  def test_legacy_upload_invalid_protocol_version(
53
78
  python_empty_repo_distro, python_package_dist_directory
@@ -150,6 +150,8 @@ PYTHON_WHEEL_FILENAME = "shelf_reader-0.1-py2-none-any.whl"
150
150
  PYTHON_WHEEL_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), PYTHON_WHEEL_FILENAME)
151
151
  PYTHON_WHEEL_SHA256 = "2eceb1643c10c5e4a65970baf63bde43b79cbdac7de81dae853ce47ab05197e9"
152
152
 
153
+ PYTHON_WHEEL_METADATA_SHA256 = "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350"
154
+
153
155
  PYTHON_XS_FIXTURE_CHECKSUMS = {
154
156
  PYTHON_EGG_FILENAME: PYTHON_EGG_SHA256,
155
157
  PYTHON_WHEEL_FILENAME: PYTHON_WHEEL_SHA256,
@@ -353,3 +355,5 @@ VULNERABILITY_REPORT_TEST_PACKAGE_NAME = "Django"
353
355
  VULNERABILITY_REPORT_TEST_PACKAGES = [
354
356
  "django==5.2.1",
355
357
  ]
358
+
359
+ PYPI_SERIAL_CONSTANT = 1000000000
@@ -4,7 +4,29 @@ from urllib.parse import urljoin
4
4
  from lxml import html
5
5
 
6
6
 
7
- def ensure_simple(simple_url, packages, sha_digests=None):
7
+ def _validate_metadata_sha_digest(link, filename, metadata_sha_digests):
8
+ """
9
+ Validate data-dist-info-metadata attribute for a release link.
10
+ """
11
+ data_dist_info_metadata = link.get("data-dist-info-metadata")
12
+
13
+ if expected_metadata_sha := metadata_sha_digests.get(filename):
14
+ expected_attr = f"sha256={expected_metadata_sha}"
15
+ if data_dist_info_metadata != expected_attr:
16
+ return (
17
+ f"\nFile {filename} has incorrect data-dist-info-metadata: "
18
+ f"expected '{expected_attr}', got '{data_dist_info_metadata}'"
19
+ )
20
+ else:
21
+ if data_dist_info_metadata:
22
+ return (
23
+ f"\nFile {filename} should not have data-dist-info-metadata "
24
+ f"but has '{data_dist_info_metadata}'"
25
+ )
26
+ return ""
27
+
28
+
29
+ def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=None):
8
30
  """
9
31
  Tests that the simple api at `url` matches the packages supplied.
10
32
  `packages`: dictionary of form {package_name: [release_filenames]}
@@ -28,6 +50,9 @@ def ensure_simple(simple_url, packages, sha_digests=None):
28
50
  links_found[link.text] = True
29
51
  if link.get("href"):
30
52
  legit_found_links.append(link.get("href"))
53
+ # Check metadata SHA digest if provided
54
+ if metadata_sha_digests and page_name == "release":
55
+ msgs += _validate_metadata_sha_digest(link, link.text, metadata_sha_digests)
31
56
  else:
32
57
  msgs += f"\nFound {page_name} link without href {link.text}"
33
58
  else:
@@ -62,3 +87,15 @@ def ensure_simple(simple_url, packages, sha_digests=None):
62
87
  )
63
88
  )
64
89
  return len(msgs) == 0, msgs
90
+
91
+
92
+ def ensure_metadata(pulp_content_url, distro_base_path, filename):
93
+ """
94
+ Tests that metadata is accessible for a given wheel package filename.
95
+ """
96
+ relative_path = f"{distro_base_path}/{filename}.metadata"
97
+ metadata_url = urljoin(pulp_content_url, relative_path)
98
+ metadata_response = requests.get(metadata_url)
99
+ assert metadata_response.status_code == 200
100
+ assert len(metadata_response.content) > 0
101
+ assert "Name: " in metadata_response.text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.22.2
3
+ Version: 3.23.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -20,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.100,>=3.85.3
23
+ Requires-Dist: pulpcore<3.115,>=3.85.3
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
@@ -1,19 +1,19 @@
1
1
  pulp_python/__init__.py,sha256=GIuTLoBTc-07dSLJUh8xrZPRz8x-jJ61pfR0J1IjnzI,65
2
2
  pulp_python/pytest_plugin.py,sha256=r6-flyyEed8lmIQii_zBWPqKN7tKcdr_sGvjixW3N98,8605
3
- pulp_python/app/__init__.py,sha256=Agq2pXFfURN9UHGBqyIVgGg0QBSPMgwYOk9HPN2eoQU,2488
3
+ pulp_python/app/__init__.py,sha256=bHaa_LrvOhqEPGpv63B3fDtl4wSfZsPU0OkblEAklfM,2488
4
4
  pulp_python/app/global_access_conditions.py,sha256=y8NwdgAoaB5iY7EzSBoCQOgVopSHpVMc55FKJmCGGlI,1081
5
5
  pulp_python/app/modelresource.py,sha256=dogoBWibBmQyFpcV-Hp1lu7D2WwSECa5PEShWSIg7mg,1248
6
6
  pulp_python/app/models.py,sha256=VvcjwrVTw7p1LVUS7pGhZ1U7iGAacf8B3EGKyw6wxqg,12789
7
7
  pulp_python/app/provenance.py,sha256=UgNuCP7gXo4jGibUT_y0WkWDUi6Bm3BPYPN-ZLHErK4,2353
8
8
  pulp_python/app/replica.py,sha256=DaBq5biQARMePmoHuAZbYSn3oRR-VOHztzuw8MCxQA0,1877
9
- pulp_python/app/serializers.py,sha256=hzOYsPq0NfNFUjRqemy_TZU6V1w1NNPiiiiBBPEQ6p0,26474
9
+ pulp_python/app/serializers.py,sha256=eqopeRI1z1Fvdb5iVgyx6tqAZDg9aXU16cWPfMRf078,28428
10
10
  pulp_python/app/settings.py,sha256=HXJK3rr0LVTOv1xBS5cZvVtz6j4SvFZl0PH3sLTcu2w,227
11
11
  pulp_python/app/urls.py,sha256=dw6SQ7N6SIxHALAjTdGg3dPL3-54MVoDzUKfqXA2epI,1328
12
- pulp_python/app/utils.py,sha256=RWL0hzm4GqfLtpy1flcUi4VWQ_pIfnwR96Jv26MAbDU,23378
12
+ pulp_python/app/utils.py,sha256=Tq1P_eZ_N0GsY1Jdpsds472cMgPb28TlBl_EbZx3Xr4,25276
13
13
  pulp_python/app/viewsets.py,sha256=zXi4ZIt2FBsv9phU9qz1_1pSUxGQ3Np4JwhQvJDu-Ew,27688
14
14
  pulp_python/app/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  pulp_python/app/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- pulp_python/app/management/commands/repair-python-metadata.py,sha256=zYvyKV9Ge-w6q_zj-0_U1iyPGzbRLKqzXkwTij5r7aU,4272
16
+ pulp_python/app/management/commands/repair-python-metadata.py,sha256=i-mC3UYxIiHJoknRV4xZZ7cQ44lyqWshpbaeYi7as8E,4457
17
17
  pulp_python/app/migrations/0001_initial.py,sha256=xbEeZoLAUhdVrySf22Uz-e9Jm32rbY_HjxuNcEar6EE,6843
18
18
  pulp_python/app/migrations/0001_squashed_0010_update_json_field.py,sha256=hFo1OD0JS_vn8bl49nF3Qz9QZBkVc5uDdNRnporvuyY,10455
19
19
  pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py,sha256=2WMNRVfjbkVfI_cTu8VNQRlPXK4n-JuFRsBZFcm04AE,427
@@ -33,48 +33,49 @@ pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py,sha256=Ox2
33
33
  pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py,sha256=fLn0Ci-QlIV7vS1wf7s7UhC143e5JmxphsO0R7aGbg4,434
34
34
  pulp_python/app/migrations/0017_pythonpackagecontent_size.py,sha256=jly-NWXl82HqkIHEToMjxt27GXv46XAHSmj2ghZpsVQ,1737
35
35
  pulp_python/app/migrations/0018_packageprovenance.py,sha256=FU67aw6u77TjlPuzxRrRyq76rXa7b14Dk-c_8rlemnI,1940
36
+ pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py,sha256=5OHt2v8dA9OKFFLsAfgtpHxXeIBu1RvanTs9yK5POkg,7007
36
37
  pulp_python/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  pulp_python/app/pypi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
39
  pulp_python/app/pypi/serializers.py,sha256=w6_doLpaO8Dnzur57MYrTIqEP4I3LI8biu9ipLC66fk,5028
39
- pulp_python/app/pypi/views.py,sha256=RpWjxvW6xsROKA-ekK1dkqUMfZ3c8054cUHUz-yFD24,21065
40
+ pulp_python/app/pypi/views.py,sha256=ksQflLQSwcsmoJtQG6B440rQ8CPF_bSTdWXpYJFEAg4,21006
40
41
  pulp_python/app/tasks/__init__.py,sha256=lTFpVvpDKbqv9RC0b2RYU8Bo6svDjrA-djt16pADFr8,284
41
42
  pulp_python/app/tasks/publish.py,sha256=b0JwHZvnIsJ8gEc_GJm6lUKQC3Po1lxW1TcP2q59WXA,4335
42
- pulp_python/app/tasks/repair.py,sha256=igAbZH5usPdorvRmQEIUqm0tpg3bEtMVTZ1uT-r2gio,6986
43
- pulp_python/app/tasks/sync.py,sha256=dg_RzKuLboIu1YxzBsixqaBItR7e4Oux0DlnDywc-l4,11922
44
- pulp_python/app/tasks/upload.py,sha256=V80nZvLOFf4hfMFpUciA3IiwKhZmefdB2U1hTzuD0IE,5426
43
+ pulp_python/app/tasks/repair.py,sha256=yZo7tSOXxz47NEgtr-NlRR4lNii-psPLrx6aLUjSCk4,7294
44
+ pulp_python/app/tasks/sync.py,sha256=e0X8cLHp9FUjb53OTRBPAYCiBCs26x7nG-DS1vdSwMM,12895
45
+ pulp_python/app/tasks/upload.py,sha256=12E9ihDqbe9Ihij9o6p_yuV6WF1Yyt6zPeOI2dzCEms,5697
45
46
  pulp_python/app/tasks/vulnerability_report.py,sha256=0cyxNb4048HFUdUlGBA6wYsg-hEMjSfE8mtw05Ct9BQ,1126
46
47
  pulp_python/app/webserver_snippets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
48
  pulp_python/app/webserver_snippets/apache.conf,sha256=3frHSl2YV_8pJPscaFxMVo7HmxGJdb8XVmfdLtCxzoA,97
48
49
  pulp_python/app/webserver_snippets/nginx.conf,sha256=gMqZGFefsTJVVx9YRxpHVS7NMEll9CzOseYdtLr3Avc,344
49
50
  pulp_python/tests/__init__.py,sha256=4Yz43a8s-KyhdHFb5eEhIIvH72807Y84uAHnG5bO5y0,31
50
51
  pulp_python/tests/functional/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- pulp_python/tests/functional/constants.py,sha256=-_Vkvs5enXdo6_Oh215uwoAwDe8-weI_m2WZSvD10kA,12560
52
- pulp_python/tests/functional/utils.py,sha256=WCLlxlXQHoi79WmWHb9da0OoB-xq1hMuK0DrPc0_iLU,2756
52
+ pulp_python/tests/functional/constants.py,sha256=UgIDeqQpfeFi8c6SqVc-9gZDaCjo6GkMZB3ac99Oc4s,12694
53
+ pulp_python/tests/functional/utils.py,sha256=TW2gaMbQJ72yZuqXdxUjOCIv3fBnXbyc_k4XHASh1Vc,4311
53
54
  pulp_python/tests/functional/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
55
  pulp_python/tests/functional/api/test_attestations.py,sha256=_opUZT65c5H2j_8ous-nKGHmDOw8EMjxdooTIQhiXQ8,8640
55
56
  pulp_python/tests/functional/api/test_auto_publish.py,sha256=uCIt4LsO61oMk3bDs3LMQDJI8zkKqvY0b1uX16bTxzM,1747
56
57
  pulp_python/tests/functional/api/test_consume_content.py,sha256=QUOZ_bQ_Ortzitc7sjlMEJzRhPm00wayxjZvEeK18jI,1000
57
- pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=mCG-55IKByH1EeMhx-qPSKU5d-Hlbjs57BysG8bfwKs,8033
58
+ pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=0EslrBURc55_zrHbieu4IecqC-immnWB_LIfrCWHuKw,8719
58
59
  pulp_python/tests/functional/api/test_crud_publications.py,sha256=uoTGHbhKq_mYOYtNV8Tt9cyhzY0zsoG1DhYrfIdYcMc,5646
59
60
  pulp_python/tests/functional/api/test_crud_remotes.py,sha256=uRo51X3MDWIls4Fdxw5unvU47_JyyLEg2w41ByvDcmY,5811
60
61
  pulp_python/tests/functional/api/test_domains.py,sha256=RtQWpp78Oq0Vq1V_-sbBBIo9E8jR_lkcKMjgH6--lWI,10453
61
62
  pulp_python/tests/functional/api/test_download_content.py,sha256=msnsKpU6TGT8eNu6e0uFa4fte6hj85OTexwOHdf66-c,4950
62
- pulp_python/tests/functional/api/test_export_import.py,sha256=DAMuWF2vtqXtf7Ap-6EiNx2KoLvW9zUm9OqcmpnES-4,4603
63
+ pulp_python/tests/functional/api/test_export_import.py,sha256=A_LkhvnoXzm1-F7Qy8dZZcpDo2BPngnhjf55T0bH20E,4513
63
64
  pulp_python/tests/functional/api/test_full_mirror.py,sha256=uLsnWkvYxh-KY-Zp6KjJW0MRb8KGTM1faC7jAv8Yrho,7254
64
- pulp_python/tests/functional/api/test_pypi_apis.py,sha256=fTM82z92-NqYMNgxsJH3zlvVnlDvsMrBvZ5aXWfOcZI,11365
65
- pulp_python/tests/functional/api/test_pypi_simple_json_api.py,sha256=NM-ycrHlY0sn1OLssjBHYh1MdGvr2bbqkF9OMHtbY5c,4797
65
+ pulp_python/tests/functional/api/test_pypi_apis.py,sha256=ovp0DKUJbp7GUQxCXNERv3emqKIXWpTzc-rp9vx0O4I,11655
66
+ pulp_python/tests/functional/api/test_pypi_simple_api.py,sha256=y_vHEVJeGPsgAyLPQ2sXpZRnGlUcXxXxtLw41KV3ak0,7003
66
67
  pulp_python/tests/functional/api/test_rbac.py,sha256=cy1RQHvWKbE4f4aPu33ZdIUBiJBnW8aOXKlDHHcCuxo,10512
67
68
  pulp_python/tests/functional/api/test_repair.py,sha256=iiR4pcEv_YAJVCAZfp8IvZXHOoqgE5NlL9e_6AGYPuM,7866
68
- pulp_python/tests/functional/api/test_sync.py,sha256=mT6w1tlf-MU7Co52Qibl34Hyd_lHLhp1NE-nknLWcpA,13154
69
- pulp_python/tests/functional/api/test_upload.py,sha256=XVrppsgk60QO3jBWA8q6s101dZh0Kxxs78hEbYCjYkM,4793
69
+ pulp_python/tests/functional/api/test_sync.py,sha256=qLdmMUhGyXfr7psFw2E-xCUB-QCWDDpCnIWTIDR2QiE,13769
70
+ pulp_python/tests/functional/api/test_upload.py,sha256=9j7lFIuQ7poJyo1T7CcYKGqlzZKheQrhQ-6EscKz4HQ,5708
70
71
  pulp_python/tests/functional/api/test_vulnerability_report.py,sha256=Rv492Wrvu1FY7O_moo9DTB6OkI-OZURj_fuTKbENLh8,1730
71
72
  pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation,sha256=_Ygrz7523z55s4uPmQWYxF29jr0NqJxVm_d-Z8KvrZ0,10363
72
73
  pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation,sha256=muTQ8dqYSSdx76DlaPjB1REcNIS-aak-Na0TkASxu8M,10426
73
74
  pulp_python/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
75
  pulp_python/tests/unit/test_models.py,sha256=TBI0yKsrdbnJSPeBFfxSqhXK7zaNvR6qg5JehGH3Pds,229
75
- pulp_python-3.22.2.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
76
- pulp_python-3.22.2.dist-info/METADATA,sha256=CDL43k1FZGgHQVEpPyxOcYjsP1a6kmmO2_TGEyk3-mU,1743
77
- pulp_python-3.22.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- pulp_python-3.22.2.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
79
- pulp_python-3.22.2.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
80
- pulp_python-3.22.2.dist-info/RECORD,,
76
+ pulp_python-3.23.0.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
77
+ pulp_python-3.23.0.dist-info/METADATA,sha256=8ObO_b-jBoh1gxDcJcscLZA5rjgzIC5NPFT_ubgJskQ,1743
78
+ pulp_python-3.23.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
+ pulp_python-3.23.0.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
80
+ pulp_python-3.23.0.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
81
+ pulp_python-3.23.0.dist-info/RECORD,,