pulp-python 3.32.1__py3-none-any.whl → 3.33.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.
- pulp_python/app/__init__.py +1 -1
- pulp_python/app/migrations/0023_packageyank.py +50 -0
- pulp_python/app/models.py +27 -3
- pulp_python/app/pypi/serializers.py +21 -0
- pulp_python/app/pypi/views.py +93 -0
- pulp_python/app/serializers.py +19 -0
- pulp_python/app/tasks/__init__.py +1 -0
- pulp_python/app/tasks/sync.py +10 -0
- pulp_python/app/tasks/yank.py +64 -0
- pulp_python/app/urls.py +3 -0
- pulp_python/app/utils.py +42 -23
- pulp_python/app/viewsets.py +47 -0
- pulp_python/tests/functional/api/test_crud_content_unit.py +21 -2
- pulp_python/tests/functional/api/test_full_mirror.py +1 -1
- pulp_python/tests/functional/api/test_version_specifier_filter.py +28 -0
- pulp_python/tests/functional/api/test_yank.py +306 -0
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/METADATA +1 -1
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/RECORD +22 -18
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/WHEEL +0 -0
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/entry_points.txt +0 -0
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/licenses/LICENSE +0 -0
- {pulp_python-3.32.1.dist-info → pulp_python-3.33.0.dist-info}/top_level.txt +0 -0
pulp_python/app/__init__.py
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated by Django 5.2.16 on 2026-07-30 10:44
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
import pulpcore.app.util
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("python", "0022_pythonblocklistentry"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="PackageYank",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"content_ptr",
|
|
21
|
+
models.OneToOneField(
|
|
22
|
+
auto_created=True,
|
|
23
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
24
|
+
parent_link=True,
|
|
25
|
+
primary_key=True,
|
|
26
|
+
serialize=False,
|
|
27
|
+
to="core.content",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
("name_normalized", models.TextField()),
|
|
31
|
+
("version", models.TextField()),
|
|
32
|
+
("yanked_reason", models.TextField(default="")),
|
|
33
|
+
(
|
|
34
|
+
"_pulp_domain",
|
|
35
|
+
models.ForeignKey(
|
|
36
|
+
default=pulpcore.app.util.get_domain_pk,
|
|
37
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
38
|
+
to="core.domain",
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
options={
|
|
43
|
+
"default_related_name": "%(app_label)s_%(model_name)s",
|
|
44
|
+
"unique_together": {
|
|
45
|
+
("name_normalized", "version", "yanked_reason", "_pulp_domain")
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
bases=("core.content",),
|
|
49
|
+
),
|
|
50
|
+
]
|
pulp_python/app/models.py
CHANGED
|
@@ -204,8 +204,6 @@ class PythonPackageContent(Content):
|
|
|
204
204
|
sha256 = models.CharField(db_index=True, max_length=64)
|
|
205
205
|
metadata_sha256 = models.CharField(max_length=64, null=True)
|
|
206
206
|
size = models.BigIntegerField(default=0)
|
|
207
|
-
# yanked and yanked_reason are not implemented because they are mutable
|
|
208
|
-
|
|
209
207
|
# From pulpcore
|
|
210
208
|
PROTECTED_FROM_RECLAIM = False
|
|
211
209
|
TYPE = "python"
|
|
@@ -289,6 +287,32 @@ class PackageProvenance(Content):
|
|
|
289
287
|
unique_together = ("sha256", "_pulp_domain")
|
|
290
288
|
|
|
291
289
|
|
|
290
|
+
class PackageYank(Content):
|
|
291
|
+
"""
|
|
292
|
+
A marker content type indicating a package version is yanked in a repository (PEP 592).
|
|
293
|
+
|
|
294
|
+
Its presence in a repository version means all files for the matching
|
|
295
|
+
(name_normalized, version) pair are yanked. Yank/unyank operations
|
|
296
|
+
add/remove this marker, creating new repository versions.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
TYPE = "python_yank"
|
|
300
|
+
repo_key_fields = ("name_normalized", "version")
|
|
301
|
+
|
|
302
|
+
name_normalized = models.TextField()
|
|
303
|
+
version = models.TextField()
|
|
304
|
+
yanked_reason = models.TextField(default="")
|
|
305
|
+
|
|
306
|
+
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
307
|
+
|
|
308
|
+
def __str__(self):
|
|
309
|
+
return f"<{self._meta.object_name}: {self.name_normalized} [{self.version}]>"
|
|
310
|
+
|
|
311
|
+
class Meta:
|
|
312
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
313
|
+
unique_together = ("name_normalized", "version", "yanked_reason", "_pulp_domain")
|
|
314
|
+
|
|
315
|
+
|
|
292
316
|
class PythonPublication(Publication, AutoAddObjPermsMixin):
|
|
293
317
|
"""
|
|
294
318
|
A Publication for PythonContent.
|
|
@@ -364,7 +388,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
364
388
|
"""
|
|
365
389
|
|
|
366
390
|
TYPE = "python"
|
|
367
|
-
CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
|
|
391
|
+
CONTENT_TYPES = [PythonPackageContent, PackageProvenance, PackageYank]
|
|
368
392
|
REMOTE_TYPES = [PythonRemote]
|
|
369
393
|
PULL_THROUGH_SUPPORTED = True
|
|
370
394
|
|
|
@@ -136,3 +136,24 @@ class PackageUploadTaskSerializer(serializers.Serializer):
|
|
|
136
136
|
session = serializers.CharField(allow_null=True)
|
|
137
137
|
task = serializers.CharField()
|
|
138
138
|
task_start_time = serializers.DateTimeField(allow_null=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class YankSerializer(serializers.Serializer):
|
|
142
|
+
"""
|
|
143
|
+
A Serializer for yank/unyank requests (PEP 592).
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
name = serializers.CharField(
|
|
147
|
+
help_text=_("The name of the package to yank or unyank."),
|
|
148
|
+
required=True,
|
|
149
|
+
)
|
|
150
|
+
version = serializers.CharField(
|
|
151
|
+
help_text=_("The version of the package to yank or unyank."),
|
|
152
|
+
required=True,
|
|
153
|
+
)
|
|
154
|
+
yanked_reason = serializers.CharField(
|
|
155
|
+
help_text=_("The reason for yanking the package version."),
|
|
156
|
+
required=False,
|
|
157
|
+
allow_blank=True,
|
|
158
|
+
default="",
|
|
159
|
+
)
|
pulp_python/app/pypi/views.py
CHANGED
|
@@ -29,6 +29,7 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, Templat
|
|
|
29
29
|
from rest_framework.response import Response
|
|
30
30
|
from rest_framework.viewsets import ViewSet
|
|
31
31
|
|
|
32
|
+
from pulpcore.plugin.serializers import AsyncOperationResponseSerializer
|
|
32
33
|
from pulpcore.plugin.tasking import dispatch
|
|
33
34
|
from pulpcore.plugin.util import get_domain, get_url
|
|
34
35
|
from pulpcore.plugin.viewsets import OperationPostponedResponse
|
|
@@ -37,6 +38,7 @@ from pulp_python.app import tasks
|
|
|
37
38
|
from pulp_python.app.cache import PythonApiCache, find_base_path_cached
|
|
38
39
|
from pulp_python.app.models import (
|
|
39
40
|
PackageProvenance,
|
|
41
|
+
PackageYank,
|
|
40
42
|
PythonDistribution,
|
|
41
43
|
PythonPackageContent,
|
|
42
44
|
PythonPublication,
|
|
@@ -46,6 +48,7 @@ from pulp_python.app.pypi.serializers import (
|
|
|
46
48
|
PackageUploadSerializer,
|
|
47
49
|
PackageUploadTaskSerializer,
|
|
48
50
|
SummarySerializer,
|
|
51
|
+
YankSerializer,
|
|
49
52
|
)
|
|
50
53
|
from pulp_python.app.utils import (
|
|
51
54
|
PYPI_LAST_SERIAL,
|
|
@@ -356,6 +359,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
356
359
|
"upload_time": release_package.upload_time,
|
|
357
360
|
"version": release_package.version,
|
|
358
361
|
"provenance": release_package.provenance_url,
|
|
362
|
+
"yanked": release_package.is_yanked,
|
|
363
|
+
"yanked_reason": release_package.yanked_reason or "",
|
|
359
364
|
}
|
|
360
365
|
|
|
361
366
|
rfilter = get_remote_package_filter(remote)
|
|
@@ -408,6 +413,11 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
408
413
|
"version",
|
|
409
414
|
"has_provenance",
|
|
410
415
|
)
|
|
416
|
+
yank_markers = dict(
|
|
417
|
+
PackageYank.objects.filter(
|
|
418
|
+
pk__in=repo_ver.content, name_normalized=normalized
|
|
419
|
+
).values_list("version", "yanked_reason")
|
|
420
|
+
)
|
|
411
421
|
local_releases = {
|
|
412
422
|
p["filename"]: {
|
|
413
423
|
**p,
|
|
@@ -418,6 +428,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
418
428
|
if p["has_provenance"]
|
|
419
429
|
else None
|
|
420
430
|
),
|
|
431
|
+
"yanked": p["version"] in yank_markers,
|
|
432
|
+
"yanked_reason": yank_markers.get(p["version"], ""),
|
|
421
433
|
}
|
|
422
434
|
for p in packages
|
|
423
435
|
}
|
|
@@ -493,12 +505,18 @@ class MetadataView(PyPIMixin, ViewSet):
|
|
|
493
505
|
headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
|
|
494
506
|
if settings.DOMAIN_ENABLED:
|
|
495
507
|
domain = get_domain()
|
|
508
|
+
yank_markers = dict(
|
|
509
|
+
PackageYank.objects.filter(
|
|
510
|
+
pk__in=repo_ver.content, name_normalized=normalized
|
|
511
|
+
).values_list("version", "yanked_reason")
|
|
512
|
+
)
|
|
496
513
|
json_body = python_content_to_json(
|
|
497
514
|
path,
|
|
498
515
|
package_content,
|
|
499
516
|
version=version,
|
|
500
517
|
domain=domain,
|
|
501
518
|
repository_version=repo_ver,
|
|
519
|
+
yank_markers=yank_markers,
|
|
502
520
|
)
|
|
503
521
|
if json_body:
|
|
504
522
|
return Response(data=json_body, headers=headers)
|
|
@@ -586,3 +604,78 @@ class ProvenanceView(PyPIMixin, ViewSet):
|
|
|
586
604
|
if provenance:
|
|
587
605
|
return Response(data=provenance.provenance)
|
|
588
606
|
return HttpResponseNotFound(f"{package} {version} {filename} provenance does not exist.")
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class YankView(PyPIMixin, ViewSet):
|
|
610
|
+
"""View for yank/unyank requests (PEP 592)."""
|
|
611
|
+
|
|
612
|
+
endpoint_name = "yank"
|
|
613
|
+
DEFAULT_ACCESS_POLICY = {
|
|
614
|
+
"statements": [
|
|
615
|
+
{
|
|
616
|
+
"action": ["yank", "unyank"],
|
|
617
|
+
"principal": "authenticated",
|
|
618
|
+
"effect": "allow",
|
|
619
|
+
"condition": "index_has_repo_perm:python.modify_pythonrepository",
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@extend_schema(
|
|
625
|
+
request=YankSerializer,
|
|
626
|
+
responses={202: AsyncOperationResponseSerializer},
|
|
627
|
+
summary="Yank a package version",
|
|
628
|
+
)
|
|
629
|
+
def yank(self, request, path):
|
|
630
|
+
"""Yank a package version, marking all its files with data-yanked."""
|
|
631
|
+
repo = self.distribution.repository
|
|
632
|
+
if not repo:
|
|
633
|
+
return HttpResponseBadRequest(reason="Index is not pointing to a repository")
|
|
634
|
+
|
|
635
|
+
serializer = YankSerializer(data=request.data)
|
|
636
|
+
serializer.is_valid(raise_exception=True)
|
|
637
|
+
|
|
638
|
+
normalized = canonicalize_name(serializer.validated_data["name"])
|
|
639
|
+
version = serializer.validated_data["version"]
|
|
640
|
+
repo_ver = self.get_repository_version(self.distribution)
|
|
641
|
+
if not PythonPackageContent.objects.filter(
|
|
642
|
+
pk__in=repo_ver.content, name_normalized=normalized, version=version
|
|
643
|
+
).exists():
|
|
644
|
+
return HttpResponseNotFound(f"{normalized}=={version} not found in repository")
|
|
645
|
+
|
|
646
|
+
result = dispatch(
|
|
647
|
+
tasks.ayank_package,
|
|
648
|
+
exclusive_resources=[repo],
|
|
649
|
+
kwargs={
|
|
650
|
+
"repository_pk": str(repo.pk),
|
|
651
|
+
"name": serializer.validated_data["name"],
|
|
652
|
+
"version": serializer.validated_data["version"],
|
|
653
|
+
"yanked_reason": serializer.validated_data.get("yanked_reason", ""),
|
|
654
|
+
},
|
|
655
|
+
)
|
|
656
|
+
return OperationPostponedResponse(result, request)
|
|
657
|
+
|
|
658
|
+
@extend_schema(
|
|
659
|
+
request=YankSerializer,
|
|
660
|
+
responses={202: AsyncOperationResponseSerializer},
|
|
661
|
+
summary="Unyank a package version",
|
|
662
|
+
)
|
|
663
|
+
def unyank(self, request, path):
|
|
664
|
+
"""Unyank a package version, unmarking all its files with data-yanked."""
|
|
665
|
+
repo = self.distribution.repository
|
|
666
|
+
if not repo:
|
|
667
|
+
return HttpResponseBadRequest(reason="Index is not pointing to a repository")
|
|
668
|
+
|
|
669
|
+
serializer = YankSerializer(data=request.data)
|
|
670
|
+
serializer.is_valid(raise_exception=True)
|
|
671
|
+
|
|
672
|
+
result = dispatch(
|
|
673
|
+
tasks.aunyank_package,
|
|
674
|
+
exclusive_resources=[repo],
|
|
675
|
+
kwargs={
|
|
676
|
+
"repository_pk": str(repo.pk),
|
|
677
|
+
"name": serializer.validated_data["name"],
|
|
678
|
+
"version": serializer.validated_data["version"],
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
return OperationPostponedResponse(result, request)
|
pulp_python/app/serializers.py
CHANGED
|
@@ -683,6 +683,25 @@ class PackageProvenanceSerializer(core_serializers.NoArtifactContentUploadSerial
|
|
|
683
683
|
model = python_models.PackageProvenance
|
|
684
684
|
|
|
685
685
|
|
|
686
|
+
class PackageYankSerializer(core_serializers.NoArtifactContentSerializer):
|
|
687
|
+
"""
|
|
688
|
+
Read-only serializer for PackageYank content units (PEP 592).
|
|
689
|
+
Used by PackageYankViewSet to expose yank markers via the Pulp REST API.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
name_normalized = serializers.CharField(read_only=True)
|
|
693
|
+
version = serializers.CharField(read_only=True)
|
|
694
|
+
yanked_reason = serializers.CharField(read_only=True)
|
|
695
|
+
|
|
696
|
+
class Meta:
|
|
697
|
+
fields = core_serializers.NoArtifactContentSerializer.Meta.fields + (
|
|
698
|
+
"name_normalized",
|
|
699
|
+
"version",
|
|
700
|
+
"yanked_reason",
|
|
701
|
+
)
|
|
702
|
+
model = python_models.PackageYank
|
|
703
|
+
|
|
704
|
+
|
|
686
705
|
class MultipleChoiceArrayField(serializers.MultipleChoiceField):
|
|
687
706
|
"""
|
|
688
707
|
A wrapper to make sure this DRF serializer works properly with ArrayFields.
|
pulp_python/app/tasks/sync.py
CHANGED
|
@@ -24,6 +24,7 @@ from pulpcore.plugin.stages import (
|
|
|
24
24
|
from pulp_python.app.exceptions import UnsupportedProtocolError
|
|
25
25
|
from pulp_python.app.models import (
|
|
26
26
|
PackageProvenance,
|
|
27
|
+
PackageYank,
|
|
27
28
|
PythonPackageContent,
|
|
28
29
|
PythonRemote,
|
|
29
30
|
)
|
|
@@ -265,6 +266,15 @@ class PulpMirror(Mirror):
|
|
|
265
266
|
)
|
|
266
267
|
d_artifacts.append(metadata_artifact)
|
|
267
268
|
|
|
269
|
+
if upstream_pkg.is_yanked:
|
|
270
|
+
yank_marker = PackageYank(
|
|
271
|
+
name_normalized=pkg.name,
|
|
272
|
+
version=version,
|
|
273
|
+
yanked_reason=upstream_pkg.yanked_reason or "",
|
|
274
|
+
)
|
|
275
|
+
yank_dc = DeclarativeContent(content=yank_marker, d_artifacts=[])
|
|
276
|
+
await self.python_stage.put(yank_dc)
|
|
277
|
+
|
|
268
278
|
dc = DeclarativeContent(content=package, d_artifacts=d_artifacts)
|
|
269
279
|
declared_contents[entry["filename"]] = dc
|
|
270
280
|
await self.python_stage.put(dc)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from packaging.utils import canonicalize_name
|
|
2
|
+
|
|
3
|
+
from pulpcore.plugin.exceptions import ValidationError
|
|
4
|
+
from pulpcore.plugin.tasking import aadd_and_remove
|
|
5
|
+
|
|
6
|
+
from pulp_python.app.models import PackageYank, PythonPackageContent, PythonRepository
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def ayank_package(repository_pk, name, version, yanked_reason=""):
|
|
10
|
+
"""
|
|
11
|
+
Yank a package version in a repository by adding a PackageYank marker.
|
|
12
|
+
Creates a new repository version with the yank marker added.
|
|
13
|
+
"""
|
|
14
|
+
normalized = canonicalize_name(name)
|
|
15
|
+
repository = await PythonRepository.objects.aget(pk=repository_pk)
|
|
16
|
+
latest = await repository.alatest_version()
|
|
17
|
+
|
|
18
|
+
exists = await PythonPackageContent.objects.filter(
|
|
19
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
20
|
+
).aexists()
|
|
21
|
+
if not exists:
|
|
22
|
+
raise ValidationError(f"Package {name}=={version} not found in repository")
|
|
23
|
+
|
|
24
|
+
existing_yank = await PackageYank.objects.filter(
|
|
25
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
26
|
+
).afirst()
|
|
27
|
+
if existing_yank and existing_yank.yanked_reason == yanked_reason:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
yank_marker, _ = await PackageYank.objects.aget_or_create(
|
|
31
|
+
name_normalized=normalized,
|
|
32
|
+
version=version,
|
|
33
|
+
yanked_reason=yanked_reason,
|
|
34
|
+
_pulp_domain_id=repository.pulp_domain_id,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
await aadd_and_remove(
|
|
38
|
+
repository_pk=repository.pk,
|
|
39
|
+
add_content_units=[yank_marker.pk],
|
|
40
|
+
remove_content_units=[],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def aunyank_package(repository_pk, name, version):
|
|
45
|
+
"""
|
|
46
|
+
Unyank a package version in a repository by removing its PackageYank marker.
|
|
47
|
+
Creates a new repository version with the yank marker removed.
|
|
48
|
+
"""
|
|
49
|
+
normalized = canonicalize_name(name)
|
|
50
|
+
repository = await PythonRepository.objects.aget(pk=repository_pk)
|
|
51
|
+
latest = await repository.alatest_version()
|
|
52
|
+
|
|
53
|
+
yank_marker = await PackageYank.objects.filter(
|
|
54
|
+
pk__in=latest.content, name_normalized=normalized, version=version
|
|
55
|
+
).afirst()
|
|
56
|
+
|
|
57
|
+
if yank_marker is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
await aadd_and_remove(
|
|
61
|
+
repository_pk=repository.pk,
|
|
62
|
+
add_content_units=[],
|
|
63
|
+
remove_content_units=[yank_marker.pk],
|
|
64
|
+
)
|
pulp_python/app/urls.py
CHANGED
|
@@ -7,6 +7,7 @@ from pulp_python.app.pypi.views import (
|
|
|
7
7
|
PyPIView,
|
|
8
8
|
SimpleView,
|
|
9
9
|
UploadView,
|
|
10
|
+
YankView,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
if settings.DOMAIN_ENABLED:
|
|
@@ -40,5 +41,7 @@ urlpatterns = [
|
|
|
40
41
|
SimpleView.as_view({"get": "list", "post": "create"}),
|
|
41
42
|
name="simple-detail",
|
|
42
43
|
),
|
|
44
|
+
path(PYPI_API_URL + "yank/", YankView.as_view({"post": "yank"}), name="yank"),
|
|
45
|
+
path(PYPI_API_URL + "unyank/", YankView.as_view({"post": "unyank"}), name="unyank"),
|
|
43
46
|
path(PYPI_API_URL, PyPIView.as_view({"get": "retrieve"}), name="pypi-detail"),
|
|
44
47
|
]
|
pulp_python/app/utils.py
CHANGED
|
@@ -30,7 +30,7 @@ log = logging.getLogger(__name__)
|
|
|
30
30
|
PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
|
|
31
31
|
"""TODO This serial constant is temporary until Python repositories implements serials"""
|
|
32
32
|
PYPI_SERIAL_CONSTANT = 1000000000
|
|
33
|
-
SUPPORTED_METADATA_VERSIONS = ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4")
|
|
33
|
+
SUPPORTED_METADATA_VERSIONS = ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5")
|
|
34
34
|
|
|
35
35
|
SIMPLE_API_VERSION = "1.1"
|
|
36
36
|
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
@@ -50,7 +50,6 @@ simple_index_template = """<!DOCTYPE html>
|
|
|
50
50
|
</html>
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
# TODO in the future: data-yanked (not implemented yet because it is mutable)
|
|
54
53
|
simple_detail_template = """<!DOCTYPE html>
|
|
55
54
|
<html>
|
|
56
55
|
<head>
|
|
@@ -62,6 +61,7 @@ simple_detail_template = """<!DOCTYPE html>
|
|
|
62
61
|
{%- for pkg in project_packages %}
|
|
63
62
|
<a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
|
|
64
63
|
{%- if pkg.requires_python %} data-requires-python="{{ pkg.requires_python }}" {%- endif %}
|
|
64
|
+
{%- if pkg.yanked %} data-yanked="{{ pkg.yanked_reason }}" {%- endif %}
|
|
65
65
|
{%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}" data-core-metadata="sha256={{ pkg.metadata_sha256 }}"
|
|
66
66
|
{%- endif %} {% if pkg.provenance -%}
|
|
67
67
|
data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
|
|
@@ -362,7 +362,12 @@ def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -
|
|
|
362
362
|
|
|
363
363
|
|
|
364
364
|
def python_content_to_json(
|
|
365
|
-
base_path,
|
|
365
|
+
base_path,
|
|
366
|
+
content_query,
|
|
367
|
+
version=None,
|
|
368
|
+
domain=None,
|
|
369
|
+
repository_version=None,
|
|
370
|
+
yank_markers=None,
|
|
366
371
|
):
|
|
367
372
|
"""
|
|
368
373
|
Converts a QuerySet of PythonPackageContent into the PyPi JSON format
|
|
@@ -375,6 +380,8 @@ def python_content_to_json(
|
|
|
375
380
|
|
|
376
381
|
Returns None if version is specified but not found within content_query
|
|
377
382
|
"""
|
|
383
|
+
if yank_markers is None:
|
|
384
|
+
yank_markers = {}
|
|
378
385
|
if repository_version:
|
|
379
386
|
content_query = content_query.annotate(
|
|
380
387
|
active_membership=FilteredRelation(
|
|
@@ -386,25 +393,32 @@ def python_content_to_json(
|
|
|
386
393
|
),
|
|
387
394
|
repo_added_time=F("active_membership__pulp_created"),
|
|
388
395
|
)
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
|
|
397
|
+
all_content = list(content_query)
|
|
398
|
+
for content in all_content:
|
|
399
|
+
content.yanked = content.version in yank_markers
|
|
400
|
+
content.yanked_reason = yank_markers.get(content.version)
|
|
401
|
+
|
|
402
|
+
latest_content = latest_content_version(all_content, version)
|
|
391
403
|
if not latest_content:
|
|
392
404
|
return None
|
|
393
|
-
|
|
394
|
-
full_metadata
|
|
395
|
-
full_metadata
|
|
405
|
+
|
|
406
|
+
full_metadata = {"last_serial": 0} # For now the serial field isn't supported by Pulp
|
|
407
|
+
full_metadata["info"] = python_content_to_info(latest_content[0])
|
|
408
|
+
full_metadata["releases"] = python_content_to_releases(all_content, base_path, domain)
|
|
409
|
+
full_metadata["urls"] = python_content_to_urls(latest_content, base_path, domain)
|
|
396
410
|
return full_metadata
|
|
397
411
|
|
|
398
412
|
|
|
399
|
-
def latest_content_version(
|
|
413
|
+
def latest_content_version(all_content, version):
|
|
400
414
|
"""
|
|
401
|
-
Walks through the content
|
|
415
|
+
Walks through the content list and finds the instances that are the latest version.
|
|
402
416
|
If 'version' is specified, the function instead tries to find content instances
|
|
403
417
|
with that version and will return an empty list if nothing is found
|
|
404
418
|
"""
|
|
405
419
|
latest_version = version
|
|
406
420
|
latest_content = []
|
|
407
|
-
for content in
|
|
421
|
+
for content in all_content:
|
|
408
422
|
if version and parse(version) == parse(content.version):
|
|
409
423
|
latest_content.append(content)
|
|
410
424
|
elif not latest_version or parse(content.version) > parse(latest_version):
|
|
@@ -462,8 +476,8 @@ def python_content_to_info(content):
|
|
|
462
476
|
"platform": content.platform or "",
|
|
463
477
|
"requires_dist": json_to_dict(content.requires_dist) or None,
|
|
464
478
|
"classifiers": json_to_dict(content.classifiers) or None,
|
|
465
|
-
"yanked":
|
|
466
|
-
"yanked_reason": None,
|
|
479
|
+
"yanked": getattr(content, "yanked", False),
|
|
480
|
+
"yanked_reason": getattr(content, "yanked_reason", None),
|
|
467
481
|
# New core metadata (Version 2.1, 2.2, 2.4)
|
|
468
482
|
"provides_extras": json_to_dict(content.provides_extras) or None,
|
|
469
483
|
"dynamic": json_to_dict(content.dynamic) or None,
|
|
@@ -472,13 +486,13 @@ def python_content_to_info(content):
|
|
|
472
486
|
}
|
|
473
487
|
|
|
474
488
|
|
|
475
|
-
def python_content_to_releases(
|
|
489
|
+
def python_content_to_releases(all_content, base_path, domain=None):
|
|
476
490
|
"""
|
|
477
|
-
Takes a
|
|
491
|
+
Takes a list of PythonPackageContent and returns a dictionary of releases
|
|
478
492
|
with each key being a version and value being a list of content for that version of the package
|
|
479
493
|
"""
|
|
480
494
|
releases = defaultdict(lambda: [])
|
|
481
|
-
for content in
|
|
495
|
+
for content in all_content:
|
|
482
496
|
releases[content.version].append(
|
|
483
497
|
python_content_to_download_info(content, base_path, domain)
|
|
484
498
|
)
|
|
@@ -535,22 +549,22 @@ def python_content_to_download_info(content, base_path, domain=None):
|
|
|
535
549
|
(getattr(content, "repo_added_time", None) or content.pulp_created).isoformat()
|
|
536
550
|
),
|
|
537
551
|
"url": url,
|
|
538
|
-
"yanked": False,
|
|
539
|
-
"yanked_reason": None,
|
|
552
|
+
"yanked": getattr(content, "yanked", False),
|
|
553
|
+
"yanked_reason": getattr(content, "yanked_reason", None),
|
|
540
554
|
}
|
|
541
555
|
|
|
542
556
|
|
|
543
|
-
def write_simple_index(project_names
|
|
557
|
+
def write_simple_index(project_names):
|
|
544
558
|
"""Writes the simple index."""
|
|
545
559
|
simple = Template(simple_index_template)
|
|
546
560
|
context = {
|
|
547
561
|
"SIMPLE_API_VERSION": SIMPLE_API_VERSION,
|
|
548
562
|
"projects": ((x, canonicalize_name(x)) for x in project_names),
|
|
549
563
|
}
|
|
550
|
-
return simple.
|
|
564
|
+
return simple.render(**context)
|
|
551
565
|
|
|
552
566
|
|
|
553
|
-
def write_simple_detail(project_name, project_packages
|
|
567
|
+
def write_simple_detail(project_name, project_packages):
|
|
554
568
|
"""Writes the simple detail page of a package."""
|
|
555
569
|
detail = Template(simple_detail_template, autoescape=True)
|
|
556
570
|
context = {
|
|
@@ -558,7 +572,7 @@ def write_simple_detail(project_name, project_packages, streamed=False):
|
|
|
558
572
|
"project_name": project_name,
|
|
559
573
|
"project_packages": project_packages,
|
|
560
574
|
}
|
|
561
|
-
return detail.
|
|
575
|
+
return detail.render(**context)
|
|
562
576
|
|
|
563
577
|
|
|
564
578
|
def write_simple_index_json(project_names):
|
|
@@ -591,7 +605,12 @@ def write_simple_detail_json(project_name, project_packages):
|
|
|
591
605
|
"core-metadata": (
|
|
592
606
|
{"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
|
|
593
607
|
),
|
|
594
|
-
#
|
|
608
|
+
# PEP 592
|
|
609
|
+
"yanked": (
|
|
610
|
+
package.get("yanked_reason")
|
|
611
|
+
if package.get("yanked") and package.get("yanked_reason")
|
|
612
|
+
else package.get("yanked", False)
|
|
613
|
+
),
|
|
595
614
|
# (v1.1, PEP 700)
|
|
596
615
|
"size": package["size"],
|
|
597
616
|
"upload-time": format_upload_time(package["upload_time"]),
|
pulp_python/app/viewsets.py
CHANGED
|
@@ -5,6 +5,7 @@ from django.db import transaction
|
|
|
5
5
|
from django_filters import CharFilter
|
|
6
6
|
from django_filters.rest_framework import filters as drf_filters
|
|
7
7
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
|
8
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
8
9
|
from packaging.utils import canonicalize_name
|
|
9
10
|
from rest_framework import status
|
|
10
11
|
from rest_framework.decorators import action
|
|
@@ -466,6 +467,25 @@ class NormalizedNameInFilter(drf_filters.BaseInFilter, NormalizedNameFilter):
|
|
|
466
467
|
"""In-filter that normalizes each input value and queries name_normalized."""
|
|
467
468
|
|
|
468
469
|
|
|
470
|
+
class VersionSpecifierFilter(CharFilter):
|
|
471
|
+
"""Filter that matches versions against a PEP 440 specifier string."""
|
|
472
|
+
|
|
473
|
+
def filter(self, qs, value):
|
|
474
|
+
if not value:
|
|
475
|
+
return qs
|
|
476
|
+
try:
|
|
477
|
+
spec = SpecifierSet(value, prereleases=True)
|
|
478
|
+
except InvalidSpecifier:
|
|
479
|
+
raise ValidationError(
|
|
480
|
+
{"version_specifier": f"Invalid PEP 440 version specifier: {value}"}
|
|
481
|
+
)
|
|
482
|
+
matching_pks = []
|
|
483
|
+
for pk, version in qs.values_list("pk", self.field_name):
|
|
484
|
+
if spec.contains(version):
|
|
485
|
+
matching_pks.append(pk)
|
|
486
|
+
return qs.filter(pk__in=matching_pks)
|
|
487
|
+
|
|
488
|
+
|
|
469
489
|
class PythonPackageContentFilter(core_viewsets.ContentFilter):
|
|
470
490
|
"""
|
|
471
491
|
FilterSet for PythonPackageContent.
|
|
@@ -474,6 +494,10 @@ class PythonPackageContentFilter(core_viewsets.ContentFilter):
|
|
|
474
494
|
name = NormalizedNameFilter(field_name="name_normalized", lookup_expr="exact")
|
|
475
495
|
name__in = NormalizedNameInFilter(field_name="name_normalized", lookup_expr="in")
|
|
476
496
|
name__contains = CharFilter(field_name="name", lookup_expr="contains")
|
|
497
|
+
version_specifier = VersionSpecifierFilter(
|
|
498
|
+
field_name="version",
|
|
499
|
+
help_text="Filter by PEP 440 version specifier (e.g., >=2.4,<3.0 or ~=1.26)",
|
|
500
|
+
)
|
|
477
501
|
|
|
478
502
|
class Meta:
|
|
479
503
|
model = python_models.PythonPackageContent
|
|
@@ -615,6 +639,29 @@ class PackageProvenanceViewSet(core_viewsets.NoArtifactContentUploadViewSet):
|
|
|
615
639
|
}
|
|
616
640
|
|
|
617
641
|
|
|
642
|
+
class PackageYankViewSet(core_viewsets.ReadOnlyContentViewSet):
|
|
643
|
+
"""
|
|
644
|
+
Read-only viewset for PackageYank content units (PEP 592).
|
|
645
|
+
PackageYank markers indicate that a package version has been yanked in a repository.
|
|
646
|
+
Use the /yank/ and /unyank/ PyPI endpoints to create or remove these markers.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
endpoint_name = "yanks"
|
|
650
|
+
queryset = python_models.PackageYank.objects.all()
|
|
651
|
+
serializer_class = python_serializers.PackageYankSerializer
|
|
652
|
+
|
|
653
|
+
DEFAULT_ACCESS_POLICY = {
|
|
654
|
+
"statements": [
|
|
655
|
+
{
|
|
656
|
+
"action": ["list", "retrieve"],
|
|
657
|
+
"principal": "authenticated",
|
|
658
|
+
"effect": "allow",
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
"queryset_scoping": {"function": "scope_queryset"},
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
618
665
|
class PythonRemoteViewSet(core_viewsets.RemoteViewSet, core_viewsets.RolesMixin):
|
|
619
666
|
"""
|
|
620
667
|
<!-- User-facing documentation, rendered as html-->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from urllib.parse import urljoin
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
import requests
|
|
4
5
|
from pypi_simple import PyPISimple
|
|
5
6
|
|
|
6
7
|
from pulpcore.tests.functional.utils import PulpTaskError
|
|
@@ -174,14 +175,32 @@ def test_upload_requires_python(python_content_factory):
|
|
|
174
175
|
@pytest.mark.parallel
|
|
175
176
|
def test_upload_metadata_24_spec(python_content_factory):
|
|
176
177
|
"""Test that packages using metadata spec 2.4 can be uploaded to pulp."""
|
|
177
|
-
filename = "
|
|
178
|
-
url = get_package_url("
|
|
178
|
+
filename = "attrs-26.1.0.tar.gz"
|
|
179
|
+
url = get_package_url("attrs", filename)
|
|
179
180
|
content = python_content_factory(filename, url=url)
|
|
180
181
|
assert content.metadata_version == "2.4"
|
|
181
182
|
assert content.license_expression == "MIT"
|
|
182
183
|
assert content.license_file == '["LICENSE"]'
|
|
183
184
|
|
|
184
185
|
|
|
186
|
+
@pytest.mark.parallel
|
|
187
|
+
def test_legacy_upload_metadata_25(python_empty_repo_distro, python_package_dist_directory):
|
|
188
|
+
"""Test that metadata_version 2.5 (PEP 794) is accepted via the legacy upload endpoint."""
|
|
189
|
+
flit_url = get_package_url("flit", "flit-4.0.2-py3-none-any.whl")
|
|
190
|
+
_, flit_file = python_package_dist_directory(flit_url)
|
|
191
|
+
_, distro = python_empty_repo_distro()
|
|
192
|
+
url = urljoin(distro.base_url, "legacy/")
|
|
193
|
+
sha256 = "7b0b0038cd91a7f04e2e287d958dc5f25e68ffeed0b93e613bd50aec99a0d9ca"
|
|
194
|
+
with open(flit_file, "rb") as f:
|
|
195
|
+
response = requests.post(
|
|
196
|
+
url,
|
|
197
|
+
data={"sha256_digest": sha256, "metadata_version": "2.5"},
|
|
198
|
+
files={"content": f},
|
|
199
|
+
auth=("admin", "password"),
|
|
200
|
+
)
|
|
201
|
+
assert response.status_code == 202
|
|
202
|
+
|
|
203
|
+
|
|
185
204
|
@pytest.mark.parallel
|
|
186
205
|
def test_package_creation_with_metadata(
|
|
187
206
|
pulp_content_url,
|
|
@@ -32,7 +32,7 @@ def test_pull_through_install(
|
|
|
32
32
|
# Perform pull-through install
|
|
33
33
|
host = urlsplit(distro.base_url).hostname
|
|
34
34
|
url = f"{distro.base_url}simple/"
|
|
35
|
-
cmd = ("pip", "install", "--trusted-host", host, "-i", url, PACKAGE)
|
|
35
|
+
cmd = ("pip", "install", "--no-deps", "--trusted-host", host, "-i", url, PACKAGE)
|
|
36
36
|
subprocess.run(cmd, check=True)
|
|
37
37
|
|
|
38
38
|
stdout = subprocess.run(("pip", "list"), capture_output=True).stdout.decode("utf-8")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pulp_python.tests.functional.constants import PYTHON_SM_PROJECT_SPECIFIER
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parallel
|
|
7
|
+
def test_version_specifier_filter(python_bindings, python_repo_with_sync, python_remote_factory):
|
|
8
|
+
"""Test filtering content by PEP 440 version specifier."""
|
|
9
|
+
remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
|
|
10
|
+
repo = python_repo_with_sync(remote=remote)
|
|
11
|
+
|
|
12
|
+
result = python_bindings.ContentPackagesApi.list(
|
|
13
|
+
repository_version=repo.latest_version_href,
|
|
14
|
+
name="Django",
|
|
15
|
+
version_specifier=">=1.10.2,<1.10.4",
|
|
16
|
+
)
|
|
17
|
+
versions = {c.version for c in result.results}
|
|
18
|
+
assert "1.10.2" in versions
|
|
19
|
+
assert "1.10.3" in versions
|
|
20
|
+
assert "1.10.4" not in versions
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parallel
|
|
24
|
+
def test_version_specifier_filter_invalid(python_bindings):
|
|
25
|
+
"""Test that an invalid specifier returns a 400 error."""
|
|
26
|
+
with pytest.raises(python_bindings.ApiException) as exc:
|
|
27
|
+
python_bindings.ContentPackagesApi.list(version_specifier=">=invalid!version")
|
|
28
|
+
assert exc.value.status == 400
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from urllib.parse import urljoin
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from pulp_python.tests.functional.constants import (
|
|
7
|
+
PYPI_SIMPLE_V1_JSON,
|
|
8
|
+
PYTHON_FIXTURES_URL,
|
|
9
|
+
TWINE_EGG_FILENAME,
|
|
10
|
+
TWINE_EGG_URL,
|
|
11
|
+
TWINE_WHEEL_FILENAME,
|
|
12
|
+
TWINE_WHEEL_URL,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
TWINE_NAME = "twine"
|
|
16
|
+
TWINE_VERSION = "5.1.0"
|
|
17
|
+
|
|
18
|
+
TWINE_500_WHEEL_FILENAME = "twine-5.0.0-py3-none-any.whl"
|
|
19
|
+
TWINE_500_WHEEL_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), TWINE_500_WHEEL_FILENAME)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_yank_and_unyank(
|
|
23
|
+
delete_orphans_pre,
|
|
24
|
+
monitor_task,
|
|
25
|
+
python_bindings,
|
|
26
|
+
python_content_factory,
|
|
27
|
+
python_content_summary,
|
|
28
|
+
python_distribution_factory,
|
|
29
|
+
python_repo_factory,
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Yank and unyank lifecycle including idempotency and reason update checks.
|
|
33
|
+
|
|
34
|
+
Every yank/unyank that changes state creates a new repo version.
|
|
35
|
+
Repeating the same operation with the same reason is a no-op (no new version).
|
|
36
|
+
Re-yanking with a different reason updates the reason and creates a new version.
|
|
37
|
+
"""
|
|
38
|
+
content_sdist = python_content_factory(TWINE_EGG_FILENAME, url=TWINE_EGG_URL)
|
|
39
|
+
content_whl = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
|
|
40
|
+
repo = python_repo_factory()
|
|
41
|
+
body = {"add_content_units": [content_sdist.pulp_href, content_whl.pulp_href]}
|
|
42
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
43
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
44
|
+
distro = python_distribution_factory(repository=repo)
|
|
45
|
+
version_1 = repo.latest_version_href
|
|
46
|
+
|
|
47
|
+
# 1. Yank
|
|
48
|
+
response = python_bindings.PypiYankApi.yank(
|
|
49
|
+
path=distro.base_path,
|
|
50
|
+
yank={"name": TWINE_NAME, "version": TWINE_VERSION, "yanked_reason": "broken"},
|
|
51
|
+
)
|
|
52
|
+
monitor_task(response.task)
|
|
53
|
+
|
|
54
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
55
|
+
version_2 = repo.latest_version_href
|
|
56
|
+
assert version_2 != version_1
|
|
57
|
+
summary = python_content_summary(repository=repo, version=2)
|
|
58
|
+
assert summary.added["python.python_yank"]["count"] == 1
|
|
59
|
+
|
|
60
|
+
# Check Simple API JSON - yanked files should have yanked reason
|
|
61
|
+
simple_url = urljoin(distro.base_url, f"simple/{TWINE_NAME}")
|
|
62
|
+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
|
|
63
|
+
assert response.json()["files"][0]["yanked"] == "broken"
|
|
64
|
+
assert response.json()["files"][1]["yanked"] == "broken"
|
|
65
|
+
|
|
66
|
+
# Check Simple API HTML - data-yanked attribute should be present
|
|
67
|
+
response = requests.get(simple_url)
|
|
68
|
+
assert response.text.count('data-yanked="broken"') == 2
|
|
69
|
+
|
|
70
|
+
# Check PyPI Metadata API - yanked info in metadata
|
|
71
|
+
pypi_url = urljoin(distro.base_url, f"pypi/{TWINE_NAME}/json")
|
|
72
|
+
response = requests.get(pypi_url)
|
|
73
|
+
data = response.json()
|
|
74
|
+
assert data["info"]["yanked"] is True
|
|
75
|
+
assert data["info"]["yanked_reason"] == "broken"
|
|
76
|
+
for f in data["releases"][TWINE_VERSION]:
|
|
77
|
+
assert f["yanked"] is True
|
|
78
|
+
assert f["yanked_reason"] == "broken"
|
|
79
|
+
for f in data["urls"]:
|
|
80
|
+
assert f["yanked"] is True
|
|
81
|
+
assert f["yanked_reason"] == "broken"
|
|
82
|
+
|
|
83
|
+
# Yank again with same reason - idempotent, no new repo version
|
|
84
|
+
response = python_bindings.PypiYankApi.yank(
|
|
85
|
+
path=distro.base_path,
|
|
86
|
+
yank={"name": TWINE_NAME, "version": TWINE_VERSION, "yanked_reason": "broken"},
|
|
87
|
+
)
|
|
88
|
+
monitor_task(response.task)
|
|
89
|
+
|
|
90
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
91
|
+
assert repo.latest_version_href == version_2
|
|
92
|
+
|
|
93
|
+
# Yank again with different reason - updates the reason
|
|
94
|
+
response = python_bindings.PypiYankApi.yank(
|
|
95
|
+
path=distro.base_path,
|
|
96
|
+
yank={"name": TWINE_NAME, "version": TWINE_VERSION, "yanked_reason": "security fix"},
|
|
97
|
+
)
|
|
98
|
+
monitor_task(response.task)
|
|
99
|
+
|
|
100
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
101
|
+
version_3 = repo.latest_version_href
|
|
102
|
+
assert version_3 != version_2
|
|
103
|
+
|
|
104
|
+
# Simple API should show updated reason
|
|
105
|
+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
|
|
106
|
+
assert response.json()["files"][0]["yanked"] == "security fix"
|
|
107
|
+
assert response.json()["files"][1]["yanked"] == "security fix"
|
|
108
|
+
|
|
109
|
+
response = requests.get(simple_url)
|
|
110
|
+
assert response.text.count('data-yanked="security fix"') == 2
|
|
111
|
+
|
|
112
|
+
# 2. Unyank
|
|
113
|
+
response = python_bindings.PypiUnyankApi.unyank(
|
|
114
|
+
path=distro.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
115
|
+
)
|
|
116
|
+
monitor_task(response.task)
|
|
117
|
+
|
|
118
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
119
|
+
version_4 = repo.latest_version_href
|
|
120
|
+
assert version_4 != version_3
|
|
121
|
+
summary = python_content_summary(repository=repo, version=4)
|
|
122
|
+
assert summary.removed["python.python_yank"]["count"] == 1
|
|
123
|
+
|
|
124
|
+
# Check Simple API JSON - yanked should be False after unyank
|
|
125
|
+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
|
|
126
|
+
assert response.json()["files"][0]["yanked"] is False
|
|
127
|
+
assert response.json()["files"][1]["yanked"] is False
|
|
128
|
+
|
|
129
|
+
# Check Simple API HTML - data-yanked attribute should not be present
|
|
130
|
+
response = requests.get(simple_url)
|
|
131
|
+
assert "data-yanked" not in response.text
|
|
132
|
+
|
|
133
|
+
# Check PyPI Metadata API - yanked should be False after unyank
|
|
134
|
+
response = requests.get(pypi_url)
|
|
135
|
+
data = response.json()
|
|
136
|
+
assert data["info"]["yanked"] is False
|
|
137
|
+
assert data["info"]["yanked_reason"] is None
|
|
138
|
+
for f in data["releases"][TWINE_VERSION]:
|
|
139
|
+
assert f["yanked"] is False
|
|
140
|
+
assert f["yanked_reason"] is None
|
|
141
|
+
for f in data["urls"]:
|
|
142
|
+
assert f["yanked"] is False
|
|
143
|
+
assert f["yanked_reason"] is None
|
|
144
|
+
|
|
145
|
+
# Unyank again - idempotent, no new repo version
|
|
146
|
+
response = python_bindings.PypiUnyankApi.unyank(
|
|
147
|
+
path=distro.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
148
|
+
)
|
|
149
|
+
monitor_task(response.task)
|
|
150
|
+
|
|
151
|
+
repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
|
|
152
|
+
assert repo.latest_version_href == version_4
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_yank_sync(
|
|
156
|
+
delete_orphans_pre,
|
|
157
|
+
python_remote_factory,
|
|
158
|
+
python_repo_with_sync,
|
|
159
|
+
python_content_summary,
|
|
160
|
+
python_distribution_factory,
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Syncing a yanked package from upstream creates a PackageYank marker.
|
|
164
|
+
"""
|
|
165
|
+
remote = python_remote_factory(includes=[f"{TWINE_NAME}=={TWINE_VERSION}"])
|
|
166
|
+
repo = python_repo_with_sync(remote)
|
|
167
|
+
distro = python_distribution_factory(repository=repo)
|
|
168
|
+
|
|
169
|
+
# Sync should have created a yank marker for twine 5.1.0
|
|
170
|
+
summary = python_content_summary(repository=repo, version=1)
|
|
171
|
+
assert summary.added["python.python"]["count"] == 2
|
|
172
|
+
assert summary.added["python.python_yank"]["count"] == 1
|
|
173
|
+
|
|
174
|
+
# Check Simple API JSON - yanked files should have yanked reason
|
|
175
|
+
simple_url = urljoin(distro.base_url, f"simple/{TWINE_NAME}")
|
|
176
|
+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
|
|
177
|
+
assert response.json()["files"][0]["yanked"] == "https://github.com/pypa/twine/issues/1125"
|
|
178
|
+
assert response.json()["files"][1]["yanked"] == "https://github.com/pypa/twine/issues/1125"
|
|
179
|
+
|
|
180
|
+
# Check Simple API HTML - data-yanked attribute should be present
|
|
181
|
+
response = requests.get(simple_url)
|
|
182
|
+
assert response.text.count("data-yanked=") == 2
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parallel
|
|
186
|
+
def test_partial_yank(
|
|
187
|
+
monitor_task,
|
|
188
|
+
python_bindings,
|
|
189
|
+
python_content_factory,
|
|
190
|
+
python_content_summary,
|
|
191
|
+
python_distribution_factory,
|
|
192
|
+
python_repo_factory,
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
Yanking one version does not affect other versions of the same package.
|
|
196
|
+
"""
|
|
197
|
+
content_510 = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
|
|
198
|
+
content_500 = python_content_factory(TWINE_500_WHEEL_FILENAME, url=TWINE_500_WHEEL_URL)
|
|
199
|
+
|
|
200
|
+
repo = python_repo_factory()
|
|
201
|
+
body = {"add_content_units": [content_510.pulp_href, content_500.pulp_href]}
|
|
202
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
|
|
203
|
+
distro = python_distribution_factory(repository=repo)
|
|
204
|
+
|
|
205
|
+
response = python_bindings.PypiYankApi.yank(
|
|
206
|
+
path=distro.base_path,
|
|
207
|
+
yank={"name": TWINE_NAME, "version": TWINE_VERSION, "yanked_reason": "broken 5.1.0"},
|
|
208
|
+
)
|
|
209
|
+
monitor_task(response.task)
|
|
210
|
+
|
|
211
|
+
summary = python_content_summary(repository=repo, version=2)
|
|
212
|
+
assert summary.added["python.python_yank"]["count"] == 1
|
|
213
|
+
assert summary.present["python.python_yank"]["count"] == 1
|
|
214
|
+
|
|
215
|
+
# Check Simple API JSON - yanked files should have yanked reason
|
|
216
|
+
simple_url = urljoin(distro.base_url, f"simple/{TWINE_NAME}")
|
|
217
|
+
response = requests.get(simple_url, headers={"Accept": PYPI_SIMPLE_V1_JSON})
|
|
218
|
+
data = response.json()
|
|
219
|
+
file_510 = next(f for f in data["files"] if f["filename"] == TWINE_WHEEL_FILENAME)
|
|
220
|
+
file_500 = next(f for f in data["files"] if f["filename"] == TWINE_500_WHEEL_FILENAME)
|
|
221
|
+
assert file_510["yanked"] == "broken 5.1.0"
|
|
222
|
+
assert file_500["yanked"] is False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@pytest.mark.parallel
|
|
226
|
+
def test_yank_isolation_across_repositories(
|
|
227
|
+
monitor_task,
|
|
228
|
+
python_bindings,
|
|
229
|
+
python_content_factory,
|
|
230
|
+
python_content_summary,
|
|
231
|
+
python_distribution_factory,
|
|
232
|
+
python_repo_factory,
|
|
233
|
+
):
|
|
234
|
+
"""
|
|
235
|
+
Yanking in one repo does not affect another repo with the same content.
|
|
236
|
+
"""
|
|
237
|
+
content = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
|
|
238
|
+
|
|
239
|
+
repo_a = python_repo_factory()
|
|
240
|
+
repo_b = python_repo_factory()
|
|
241
|
+
body = {"add_content_units": [content.pulp_href]}
|
|
242
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo_a.pulp_href, body).task)
|
|
243
|
+
monitor_task(python_bindings.RepositoriesPythonApi.modify(repo_b.pulp_href, body).task)
|
|
244
|
+
|
|
245
|
+
distro_a = python_distribution_factory(repository=repo_a)
|
|
246
|
+
distro_b = python_distribution_factory(repository=repo_b)
|
|
247
|
+
|
|
248
|
+
# Yank in repo A only
|
|
249
|
+
response = python_bindings.PypiYankApi.yank(
|
|
250
|
+
path=distro_a.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
251
|
+
)
|
|
252
|
+
monitor_task(response.task)
|
|
253
|
+
|
|
254
|
+
# Repo A should have a yank marker, repo B should not
|
|
255
|
+
summary_a = python_content_summary(repository=repo_a, version=2)
|
|
256
|
+
assert summary_a.present["python.python_yank"]["count"] == 1
|
|
257
|
+
summary_b = python_content_summary(repository=repo_b, version=1)
|
|
258
|
+
assert "python.python_yank" not in summary_b.present
|
|
259
|
+
|
|
260
|
+
# Yank in repo B too, then unyank only in repo A
|
|
261
|
+
response = python_bindings.PypiYankApi.yank(
|
|
262
|
+
path=distro_b.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
263
|
+
)
|
|
264
|
+
monitor_task(response.task)
|
|
265
|
+
|
|
266
|
+
response = python_bindings.PypiUnyankApi.unyank(
|
|
267
|
+
path=distro_a.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
268
|
+
)
|
|
269
|
+
monitor_task(response.task)
|
|
270
|
+
|
|
271
|
+
# Repo A should be not-yanked, repo B should remain yanked
|
|
272
|
+
summary_a = python_content_summary(repository=repo_a, version=3)
|
|
273
|
+
assert "python.python_yank" not in summary_a.present
|
|
274
|
+
summary_b = python_content_summary(repository=repo_b, version=2)
|
|
275
|
+
assert summary_b.present["python.python_yank"]["count"] == 1
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.parallel
|
|
279
|
+
def test_yank_nonexistent_package(
|
|
280
|
+
python_bindings, python_repo_factory, python_distribution_factory
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
Yanking a package not in the repo should return 404.
|
|
284
|
+
"""
|
|
285
|
+
repo = python_repo_factory()
|
|
286
|
+
distro = python_distribution_factory(repository=repo)
|
|
287
|
+
|
|
288
|
+
with pytest.raises(python_bindings.ApiException) as exc:
|
|
289
|
+
python_bindings.PypiYankApi.yank(
|
|
290
|
+
path=distro.base_path, yank={"name": "nonexistent-package", "version": "99.99.99"}
|
|
291
|
+
)
|
|
292
|
+
assert exc.value.status == 404
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.parallel
|
|
296
|
+
def test_yank_no_repository(python_bindings, python_distribution_factory):
|
|
297
|
+
"""
|
|
298
|
+
Yanking on a distribution with no repository should return 400.
|
|
299
|
+
"""
|
|
300
|
+
distro = python_distribution_factory()
|
|
301
|
+
|
|
302
|
+
with pytest.raises(python_bindings.ApiException) as exc:
|
|
303
|
+
python_bindings.PypiYankApi.yank(
|
|
304
|
+
path=distro.base_path, yank={"name": TWINE_NAME, "version": TWINE_VERSION}
|
|
305
|
+
)
|
|
306
|
+
assert exc.value.status == 400
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
pulp_python/__init__.py,sha256=GIuTLoBTc-07dSLJUh8xrZPRz8x-jJ61pfR0J1IjnzI,65
|
|
2
2
|
pulp_python/pytest_plugin.py,sha256=8Ko-O87JGPC79h8-Ecw2gJLxa6q1Ng8wD6WxbinOnVs,8573
|
|
3
|
-
pulp_python/app/__init__.py,sha256=
|
|
3
|
+
pulp_python/app/__init__.py,sha256=bjr3cUnummu5g439G8dE0t3bIvggT7lgQOXuhuMM_IM,2490
|
|
4
4
|
pulp_python/app/cache.py,sha256=Zl2nb4uKyPYp5a3FBrdRZ_-cXfofNMQmIRaQ6dUZZ1g,1037
|
|
5
5
|
pulp_python/app/exceptions.py,sha256=mPNcWyuzF0XeOPybm-G6oDKtRqvfj4jyp41iaYqcqkA,1314
|
|
6
6
|
pulp_python/app/global_access_conditions.py,sha256=MZJtyoVsr-4hRaty6mKDqh3caOHd5UKJjEWLV2crOLs,1080
|
|
7
7
|
pulp_python/app/modelresource.py,sha256=4SFAdqk6lozi_cZz4uqDIqhqPAZF-7l5jJwPn-xGZFs,1249
|
|
8
|
-
pulp_python/app/models.py,sha256=
|
|
8
|
+
pulp_python/app/models.py,sha256=jTt3Ulk8KeX2QEzQdquYNszwjjeRWYdwJ-OoMIFi3rU,18557
|
|
9
9
|
pulp_python/app/provenance.py,sha256=iyhkuNahHiTDK0Djrd4-PlgErA5SJVI0uQOIPj46tEI,2352
|
|
10
10
|
pulp_python/app/replica.py,sha256=qiWRP7tM_v4yP_XLIQfumfGolru-Jt6ZA0KVb-9g2cA,1882
|
|
11
|
-
pulp_python/app/serializers.py,sha256=
|
|
11
|
+
pulp_python/app/serializers.py,sha256=F_MrhMaxkA1dNnQhRAmefrsbOkHoWu8i3DqHa2ndmIw,34519
|
|
12
12
|
pulp_python/app/settings.py,sha256=Cyc_p6U0HQjKpyrRL6JFrK3R7RMQJ9MAgNMJCfzPEiA,255
|
|
13
|
-
pulp_python/app/urls.py,sha256=
|
|
14
|
-
pulp_python/app/utils.py,sha256=
|
|
15
|
-
pulp_python/app/viewsets.py,sha256=
|
|
13
|
+
pulp_python/app/urls.py,sha256=p784I54JxOFXOg9dcXXimbnXQWvYfSLwjw3Qopib9Pw,1573
|
|
14
|
+
pulp_python/app/utils.py,sha256=13jUDe8BwfuN1m3-pAJ8Kip0vixQTHAiZZIn7xl0YUo,27786
|
|
15
|
+
pulp_python/app/viewsets.py,sha256=WUn9iOneAF61tHT8r4iTDGjIBY6Vis71fczVX78czW4,35858
|
|
16
16
|
pulp_python/app/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
pulp_python/app/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
pulp_python/app/management/commands/repair-python-metadata.py,sha256=knvTPcPwyfgCH0-KNNCK1Mj0mQKzXUdYEwo-v6F94XU,4548
|
|
@@ -39,16 +39,18 @@ pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py,sha256=glHe
|
|
|
39
39
|
pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py,sha256=n-gtLKmhDiydgkM5Fv8OisoFZYPPOhOa8-9nExCCq14,1317
|
|
40
40
|
pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py,sha256=Yx-Cgk8DR9J9e9ht00xy0s6ogh2Rh5JS7AbdzSngKLY,384
|
|
41
41
|
pulp_python/app/migrations/0022_pythonblocklistentry.py,sha256=EbtjZuN65myTAHVWrJYbt4mWgWL7EElEYs1XulHB6Bo,1732
|
|
42
|
+
pulp_python/app/migrations/0023_packageyank.py,sha256=357fqSNK-D1OvI9RynnyMMrXJtdcjoEg5j4GteqiESU,1590
|
|
42
43
|
pulp_python/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
44
|
pulp_python/app/pypi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
|
-
pulp_python/app/pypi/serializers.py,sha256=
|
|
45
|
-
pulp_python/app/pypi/views.py,sha256=
|
|
46
|
-
pulp_python/app/tasks/__init__.py,sha256=
|
|
45
|
+
pulp_python/app/pypi/serializers.py,sha256=VDV4REMltZr98bxulgOGoGLnBxUtDod0t1fg_lDFpcs,5601
|
|
46
|
+
pulp_python/app/pypi/views.py,sha256=m5_b_MLoSNhwZ64UpQKOzmlaAA7nCfKU_4r1RFY3jnA,26252
|
|
47
|
+
pulp_python/app/tasks/__init__.py,sha256=JplOj2JtE_i4sQYqV7JnDY7zkraU_KlsCaNnYYaZ1x4,346
|
|
47
48
|
pulp_python/app/tasks/publish.py,sha256=bjsJzqJbLu7TF5rLb-UsZMmlNnc_LKw-sdHX9Gcatbw,4334
|
|
48
49
|
pulp_python/app/tasks/repair.py,sha256=5InzdbjW8y3AC4Vj2PsNLm3wGGTr8D3LcfPw_WA2Fks,12257
|
|
49
|
-
pulp_python/app/tasks/sync.py,sha256=
|
|
50
|
+
pulp_python/app/tasks/sync.py,sha256=pgiw6R142ynN-lVocyOYDmokklMJFWeH9xAgwXan9a8,13363
|
|
50
51
|
pulp_python/app/tasks/upload.py,sha256=HBOknlsAb0mlE-2dJsalH_8JUZwrKk4AsVibmVJf2aQ,6102
|
|
51
52
|
pulp_python/app/tasks/vulnerability_report.py,sha256=0cyxNb4048HFUdUlGBA6wYsg-hEMjSfE8mtw05Ct9BQ,1126
|
|
53
|
+
pulp_python/app/tasks/yank.py,sha256=VU16uLkA2-CUJe7q_u87AdkDH6gqWCwsHyc36wH35rc,2200
|
|
52
54
|
pulp_python/app/webserver_snippets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
55
|
pulp_python/app/webserver_snippets/apache.conf,sha256=3frHSl2YV_8pJPscaFxMVo7HmxGJdb8XVmfdLtCxzoA,97
|
|
54
56
|
pulp_python/app/webserver_snippets/nginx.conf,sha256=gMqZGFefsTJVVx9YRxpHVS7NMEll9CzOseYdtLr3Avc,344
|
|
@@ -61,13 +63,13 @@ pulp_python/tests/functional/api/test_attestations.py,sha256=7tXJY4b48ww8Sdc37Vo
|
|
|
61
63
|
pulp_python/tests/functional/api/test_auto_publish.py,sha256=uCIt4LsO61oMk3bDs3LMQDJI8zkKqvY0b1uX16bTxzM,1747
|
|
62
64
|
pulp_python/tests/functional/api/test_blocklist.py,sha256=IWzlazoQbrfyHNq9bNAa9FcbShWqzC2Zhq8qrN7HBwg,5994
|
|
63
65
|
pulp_python/tests/functional/api/test_consume_content.py,sha256=-7b6KOrIT3NLSUafeu1tzXEXRPddDbS5VeMHxMIy2n8,999
|
|
64
|
-
pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=
|
|
66
|
+
pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=g2G_Rxd9e8-m5n_Tv3J527nDQ4UgsDNKJzIdn2hshfI,14548
|
|
65
67
|
pulp_python/tests/functional/api/test_crud_publications.py,sha256=3cuSDC9C9M1opt0lh5ObGj8t7gq5jUW-RWj6MrA4Teo,5647
|
|
66
68
|
pulp_python/tests/functional/api/test_crud_remotes.py,sha256=clFFhgG5udyKzWOLwxpmwjhiVz8r5JEPAQHkztcAf9w,5812
|
|
67
69
|
pulp_python/tests/functional/api/test_domains.py,sha256=uEA7dIBXaXah3WQ6g6xSIjdXOR_hrsQnSFrfbdtz8bg,10421
|
|
68
70
|
pulp_python/tests/functional/api/test_download_content.py,sha256=5IuaHXyLakPkjm5sLTxtTl7Dq0yl7N36HpObnIa0Sks,4950
|
|
69
71
|
pulp_python/tests/functional/api/test_export_import.py,sha256=rHns9wdaeP-vtfW_qoGHU9EVuJ4YuWE0-rjl_8otAgU,4530
|
|
70
|
-
pulp_python/tests/functional/api/test_full_mirror.py,sha256=
|
|
72
|
+
pulp_python/tests/functional/api/test_full_mirror.py,sha256=v0nSlGKGSycFqZM4opCTHVtbmCfo_Y5y3k_6Rm7eBF8,12058
|
|
71
73
|
pulp_python/tests/functional/api/test_pypi_apis.py,sha256=iZnoSN2wCczXh14KJDH8pSDTygjs0kgZsYIcIYS5TP8,13647
|
|
72
74
|
pulp_python/tests/functional/api/test_pypi_simple_api.py,sha256=tOUl8WXgJkMoxGyLbA-WorCxIRKfGWM-27_dK_Jc0Q4,7171
|
|
73
75
|
pulp_python/tests/functional/api/test_rbac.py,sha256=-xNWqvKKU-v1uC6tCRGBK0aWaff25K9C-AfzQkdHhhI,10513
|
|
@@ -75,12 +77,14 @@ pulp_python/tests/functional/api/test_repair.py,sha256=4FR7jx_LA2K-pnRJXKkhq-1uU
|
|
|
75
77
|
pulp_python/tests/functional/api/test_simple_cache.py,sha256=kJ9mwJKf9A-RthLzPZi0rjRQlygEkWsV6jAaAR8aYgc,4944
|
|
76
78
|
pulp_python/tests/functional/api/test_sync.py,sha256=TTHR1CpZeRoD97RKxj2pZPsP0OS9ibYKGPOh1WfKISg,13801
|
|
77
79
|
pulp_python/tests/functional/api/test_upload.py,sha256=f21Li9agJ5fdBqErVg1j6v5FDVdazB5mNClmGOjLUq8,5981
|
|
80
|
+
pulp_python/tests/functional/api/test_version_specifier_filter.py,sha256=nmnBbhwxCEmcHOyFbS1uvemar67sONBGM9SsNn79LCc,1063
|
|
78
81
|
pulp_python/tests/functional/api/test_vulnerability_report.py,sha256=Rv492Wrvu1FY7O_moo9DTB6OkI-OZURj_fuTKbENLh8,1730
|
|
82
|
+
pulp_python/tests/functional/api/test_yank.py,sha256=qyZbAJ6bJO-4LPkVfVxKuWFjypJ8BmBrCnLo0luAhVk,11913
|
|
79
83
|
pulp_python/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
84
|
pulp_python/tests/unit/test_models.py,sha256=TBI0yKsrdbnJSPeBFfxSqhXK7zaNvR6qg5JehGH3Pds,229
|
|
81
|
-
pulp_python-3.
|
|
82
|
-
pulp_python-3.
|
|
83
|
-
pulp_python-3.
|
|
84
|
-
pulp_python-3.
|
|
85
|
-
pulp_python-3.
|
|
86
|
-
pulp_python-3.
|
|
85
|
+
pulp_python-3.33.0.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
|
|
86
|
+
pulp_python-3.33.0.dist-info/METADATA,sha256=7sbTkC494WCjyKIVjtdp9drQ29M8gzlsgiiyg5fZfYg,1744
|
|
87
|
+
pulp_python-3.33.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
88
|
+
pulp_python-3.33.0.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
|
|
89
|
+
pulp_python-3.33.0.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
|
|
90
|
+
pulp_python-3.33.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|