pulp-python 3.26.1__py3-none-any.whl → 3.28.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.26.1"
13
+ version = "3.28.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -1,6 +1,5 @@
1
1
  from django.conf import settings
2
2
 
3
-
4
3
  # Access Condition methods that can be used with PyPI access policies
5
4
 
6
5
 
@@ -0,0 +1,37 @@
1
+ import re
2
+
3
+ from django.db import migrations, models, transaction
4
+
5
+
6
+ def populate_name_normalized(apps, schema_editor):
7
+ """Populate name_normalized for existing PythonPackageContent rows."""
8
+ PythonPackageContent = apps.get_model("python", "PythonPackageContent")
9
+ package_bulk = []
10
+ normalize_re = re.compile(r"[-_.]+")
11
+
12
+ for package in PythonPackageContent.objects.only("pk", "name").iterator():
13
+ package.name_normalized = normalize_re.sub("-", package.name).lower()
14
+ package_bulk.append(package)
15
+ if len(package_bulk) == 100000:
16
+ with transaction.atomic():
17
+ PythonPackageContent.objects.bulk_update(package_bulk, ["name_normalized"])
18
+ package_bulk = []
19
+ if package_bulk:
20
+ with transaction.atomic():
21
+ PythonPackageContent.objects.bulk_update(package_bulk, ["name_normalized"])
22
+
23
+
24
+ class Migration(migrations.Migration):
25
+
26
+ dependencies = [
27
+ ("python", "0019_create_missing_metadata_artifacts"),
28
+ ]
29
+
30
+ operations = [
31
+ migrations.AddField(
32
+ model_name="pythonpackagecontent",
33
+ name="name_normalized",
34
+ field=models.TextField(db_index=True, default=""),
35
+ ),
36
+ migrations.RunPython(populate_name_normalized, migrations.RunPython.noop, elidable=True),
37
+ ]
@@ -0,0 +1,16 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("python", "0020_pythonpackagecontent_name_normalized"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name="pythonrepository",
13
+ name="allow_package_substitution",
14
+ field=models.BooleanField(default=True),
15
+ ),
16
+ ]
pulp_python/app/models.py CHANGED
@@ -11,6 +11,7 @@ from django_lifecycle import (
11
11
  BEFORE_SAVE,
12
12
  hook,
13
13
  )
14
+ from rest_framework.serializers import ValidationError
14
15
  from pulpcore.plugin.models import (
15
16
  AutoAddObjPermsMixin,
16
17
  Content,
@@ -31,7 +32,11 @@ from .utils import (
31
32
  PYPI_LAST_SERIAL,
32
33
  PYPI_SERIAL_CONSTANT,
33
34
  )
34
- from pulpcore.plugin.repo_version_utils import remove_duplicates, validate_repo_version
35
+ from pulpcore.plugin.repo_version_utils import (
36
+ collect_duplicates,
37
+ remove_duplicates,
38
+ validate_repo_version,
39
+ )
35
40
  from pulpcore.plugin.util import get_domain_pk, get_domain
36
41
 
37
42
  log = getLogger(__name__)
@@ -115,7 +120,7 @@ class PythonDistribution(Distribution, AutoAddObjPermsMixin):
115
120
  if name:
116
121
  normalized = canonicalize_name(name)
117
122
  package_content = PythonPackageContent.objects.filter(
118
- pk__in=self.publication.repository_version.content, name__normalize=normalized
123
+ pk__in=self.publication.repository_version.content, name_normalized=normalized
119
124
  )
120
125
  # TODO Change this value to the Repo's serial value when implemented
121
126
  headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
@@ -136,14 +141,6 @@ class PythonDistribution(Distribution, AutoAddObjPermsMixin):
136
141
  ]
137
142
 
138
143
 
139
- class NormalizeName(models.Transform):
140
- """A transform field to normalize package names according to PEP426."""
141
-
142
- function = "REGEXP_REPLACE"
143
- template = "LOWER(%(function)s(%(expressions)s, '(\.|_|-)', '-', 'ig'))" # noqa:W605
144
- lookup_name = "normalize"
145
-
146
-
147
144
  class PythonPackageContent(Content):
148
145
  """
149
146
  A Content Type representing Python's Distribution Package.
@@ -195,6 +192,9 @@ class PythonPackageContent(Content):
195
192
  license_expression = models.TextField()
196
193
  license_file = models.JSONField(default=list)
197
194
 
195
+ # Stored normalized name for indexed lookups
196
+ name_normalized = models.TextField(db_index=True, default="")
197
+
198
198
  # Release metadata
199
199
  filename = models.TextField(db_index=True)
200
200
  packagetype = models.TextField(choices=PACKAGE_TYPES)
@@ -208,9 +208,13 @@ class PythonPackageContent(Content):
208
208
  PROTECTED_FROM_RECLAIM = False
209
209
  TYPE = "python"
210
210
  _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
211
- name.register_lookup(NormalizeName)
212
211
  repo_key_fields = ("filename",)
213
212
 
213
+ @hook(BEFORE_SAVE)
214
+ def set_name_normalized(self):
215
+ """Pre-compute the normalized package name for indexed lookups."""
216
+ self.name_normalized = canonicalize_name(self.name)
217
+
214
218
  @staticmethod
215
219
  def init_from_artifact_and_relative_path(artifact, relative_path):
216
220
  """Used when downloading package from pull-through cache."""
@@ -363,6 +367,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
363
367
  PULL_THROUGH_SUPPORTED = True
364
368
 
365
369
  autopublish = models.BooleanField(default=False)
370
+ allow_package_substitution = models.BooleanField(default=True)
366
371
 
367
372
  class Meta:
368
373
  default_related_name = "%(app_label)s_%(model_name)s"
@@ -391,6 +396,25 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
391
396
  def finalize_new_version(self, new_version):
392
397
  """
393
398
  Remove duplicate packages that have the same filename.
399
+
400
+ When allow_package_substitution is False, reject any new version that would implicitly
401
+ replace existing content with different checksums (content substitution).
394
402
  """
403
+ if not self.allow_package_substitution:
404
+ self._check_for_package_substitution(new_version)
395
405
  remove_duplicates(new_version)
396
406
  validate_repo_version(new_version)
407
+
408
+ def _check_for_package_substitution(self, new_version):
409
+ """
410
+ Raise a ValidationError if newly added packages would replace existing packages that have
411
+ the same filename but a different sha256 checksum.
412
+ """
413
+ qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
414
+ duplicates = collect_duplicates(qs, ("filename",))
415
+ if duplicates:
416
+ raise ValidationError(
417
+ "Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
418
+ "To allow this, set 'allow_package_substitution' to True on the repository. "
419
+ f"Conflicting packages: {duplicates}"
420
+ )
@@ -59,7 +59,7 @@ log = logging.getLogger(__name__)
59
59
 
60
60
  ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
61
61
  BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX)
62
- BASE_API_URL = urljoin(settings.PYPI_API_HOSTNAME, "pypi/")
62
+ BASE_API_URL = urljoin(settings.PYPI_API_HOSTNAME, settings.PYPI_PATH_PREFIX)
63
63
 
64
64
  PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
65
65
  PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
@@ -303,7 +303,12 @@ class SimpleView(PackageUploadMixin, ViewSet):
303
303
  repo_version, content = self.get_rvc()
304
304
  if self.should_redirect(repo_version=repo_version):
305
305
  return redirect(urljoin(self.base_content_url, f"{path}/simple/"))
306
- names = content.order_by("name").values_list("name", flat=True).distinct().iterator()
306
+ names = (
307
+ content.order_by("name_normalized")
308
+ .values_list("name", flat=True)
309
+ .distinct("name_normalized")
310
+ .iterator()
311
+ )
307
312
  media_type = request.accepted_renderer.media_type
308
313
  headers = {"X-PyPI-Last-Serial": str(PYPI_SERIAL_CONSTANT)}
309
314
 
@@ -360,8 +365,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
360
365
  releases = self.pull_through_package_simple(normalized, path, self.distribution.remote)
361
366
  elif self.should_redirect(repo_version=repo_ver):
362
367
  return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
363
- if content:
364
- local_packages = content.filter(name__normalize=normalized)
368
+ if content is not None:
369
+ local_packages = content.filter(name_normalized=normalized)
365
370
  packages = local_packages.values(
366
371
  "filename",
367
372
  "sha256",
@@ -454,7 +459,7 @@ class MetadataView(PyPIMixin, ViewSet):
454
459
  name = meta_path.parts[0]
455
460
  if name:
456
461
  normalized = canonicalize_name(name)
457
- package_content = content.filter(name__normalize=normalized)
462
+ package_content = content.filter(name_normalized=normalized)
458
463
  # TODO Change this value to the Repo's serial value when implemented
459
464
  headers = {PYPI_LAST_SERIAL: str(PYPI_SERIAL_CONSTANT)}
460
465
  if settings.DOMAIN_ENABLED:
@@ -541,7 +546,7 @@ class ProvenanceView(PyPIMixin, ViewSet):
541
546
  repo_ver, content = self.get_rvc()
542
547
  if content:
543
548
  package_content = content.filter(
544
- name__normalize=package, version=version, filename=filename
549
+ name_normalized=package, version=version, filename=filename
545
550
  ).first()
546
551
  if package_content:
547
552
  provenance = self.get_provenances(repo_ver).filter(package=package_content).first()
@@ -37,8 +37,7 @@ class PythonReplicator(Replicator):
37
37
  return None
38
38
 
39
39
  def repository_extra_fields(self, remote):
40
- # Use autopublish since publications result in faster serving times
41
- return {"autopublish": True}
40
+ return {"autopublish": False}
42
41
 
43
42
  def sync_params(self, repository, remote):
44
43
  return {"remote_pk": str(remote.pk), "repository_pk": str(repository.pk), "mirror": True}
@@ -4,10 +4,12 @@ import tempfile
4
4
  from gettext import gettext as _
5
5
  from django.conf import settings
6
6
  from django.db.utils import IntegrityError
7
+ from drf_spectacular.utils import extend_schema_serializer
7
8
  from packaging.requirements import Requirement
8
9
  from rest_framework import serializers
9
10
  from pypi_attestations import AttestationError
10
11
  from pydantic import TypeAdapter, ValidationError
12
+ from urllib.parse import urljoin
11
13
 
12
14
  from pulpcore.plugin import models as core_models
13
15
  from pulpcore.plugin import serializers as core_serializers
@@ -29,10 +31,15 @@ from pulp_python.app.utils import (
29
31
  parse_project_metadata,
30
32
  )
31
33
 
32
-
33
34
  log = logging.getLogger(__name__)
35
+ PYPI_BASE_URL = urljoin(settings.PYPI_API_HOSTNAME, settings.PYPI_PATH_PREFIX)
34
36
 
35
37
 
38
+ @extend_schema_serializer(
39
+ deprecate_fields=[
40
+ "autopublish",
41
+ ]
42
+ )
36
43
  class PythonRepositorySerializer(core_serializers.RepositorySerializer):
37
44
  """
38
45
  Serializer for Python Repositories.
@@ -41,17 +48,36 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
41
48
  autopublish = serializers.BooleanField(
42
49
  help_text=_(
43
50
  "Whether to automatically create publications for new repository versions, "
44
- "and update any distributions pointing to this repository."
51
+ "and update any distributions pointing to this repository. [Deprecated]"
45
52
  ),
46
53
  default=False,
47
54
  required=False,
48
55
  )
56
+ allow_package_substitution = serializers.BooleanField(
57
+ help_text=_(
58
+ "Whether to allow package substitution (replacing existing packages with packages "
59
+ "that have the same filename but a different checksum). When False, any new "
60
+ "repository version that would cause such a substitution will be rejected. This "
61
+ "applies to all repository version creation paths including uploads, modify, and "
62
+ "sync. When True (the default), package substitution is allowed."
63
+ ),
64
+ default=True,
65
+ required=False,
66
+ )
49
67
 
50
68
  class Meta:
51
- fields = core_serializers.RepositorySerializer.Meta.fields + ("autopublish",)
69
+ fields = core_serializers.RepositorySerializer.Meta.fields + (
70
+ "autopublish",
71
+ "allow_package_substitution",
72
+ )
52
73
  model = python_models.PythonRepository
53
74
 
54
75
 
76
+ @extend_schema_serializer(
77
+ deprecate_fields=[
78
+ "publication",
79
+ ]
80
+ )
55
81
  class PythonDistributionSerializer(core_serializers.DistributionSerializer):
56
82
  """
57
83
  Serializer for Pulp distributions for the Python type.
@@ -59,7 +85,7 @@ class PythonDistributionSerializer(core_serializers.DistributionSerializer):
59
85
 
60
86
  publication = core_serializers.DetailRelatedField(
61
87
  required=False,
62
- help_text=_("Publication to be served"),
88
+ help_text=_("Publication to be served. [Deprecated]"),
63
89
  view_name_pattern=r"publications(-.*/.*)?-detail",
64
90
  queryset=core_models.Publication.objects.exclude(complete=False),
65
91
  allow_null=True,
@@ -82,8 +108,8 @@ class PythonDistributionSerializer(core_serializers.DistributionSerializer):
82
108
  def get_base_url(self, obj):
83
109
  """Gets the base url."""
84
110
  if settings.DOMAIN_ENABLED:
85
- return f"{settings.PYPI_API_HOSTNAME}/pypi/{get_domain().name}/{obj.base_path}/"
86
- return f"{settings.PYPI_API_HOSTNAME}/pypi/{obj.base_path}/"
111
+ return urljoin(PYPI_BASE_URL, f"{get_domain().name}/{obj.base_path}/")
112
+ return urljoin(PYPI_BASE_URL, f"{obj.base_path}/")
87
113
 
88
114
  class Meta:
89
115
  fields = core_serializers.DistributionSerializer.Meta.fields + (
@@ -436,7 +462,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
436
462
  content = super().create(validated_data)
437
463
  if provenance:
438
464
  prov_sha256 = python_models.PackageProvenance.calculate_sha256(provenance)
439
- prov_model, _ = python_models.PackageProvenance.objects.get_or_create(
465
+ prov_model, _created = python_models.PackageProvenance.objects.get_or_create(
440
466
  sha256=prov_sha256,
441
467
  _pulp_domain=get_domain(),
442
468
  defaults={"package": content, "provenance": provenance},
@@ -2,6 +2,7 @@ import socket
2
2
 
3
3
  PYTHON_GROUP_UPLOADS = False
4
4
  PYPI_API_HOSTNAME = "https://" + socket.getfqdn()
5
+ PYPI_PATH_PREFIX = "/pypi/"
5
6
 
6
7
  DRF_ACCESS_POLICY = {
7
8
  "dynaconf_merge_unique": True,
@@ -12,7 +12,6 @@ from pulp_python.app import models as python_models
12
12
  from pulp_python.app.serializers import PythonPublicationSerializer
13
13
  from pulp_python.app.utils import write_simple_index, write_simple_detail
14
14
 
15
-
16
15
  log = logging.getLogger(__name__)
17
16
 
18
17
 
@@ -61,9 +60,9 @@ def write_simple_api(publication):
61
60
  python_models.PythonPackageContent.objects.filter(
62
61
  pk__in=publication.repository_version.content, _pulp_domain=domain
63
62
  )
64
- .order_by("name__normalize")
63
+ .order_by("name_normalized")
65
64
  .values_list("name", flat=True)
66
- .distinct("name__normalize")
65
+ .distinct("name_normalized")
67
66
  )
68
67
 
69
68
  # write the root index, which lists all of the projects for which there is a package available
@@ -82,7 +81,7 @@ def write_simple_api(publication):
82
81
  packages = python_models.PythonPackageContent.objects.filter(
83
82
  pk__in=publication.repository_version.content, _pulp_domain=domain
84
83
  )
85
- releases = packages.order_by("name__normalize").values("name", "filename", "sha256")
84
+ releases = packages.order_by("name_normalized").values("name", "filename", "sha256")
86
85
 
87
86
  ind = 0
88
87
  current_name = canonicalize_name(project_names[ind])
pulp_python/app/urls.py CHANGED
@@ -10,9 +10,10 @@ from pulp_python.app.pypi.views import (
10
10
  )
11
11
 
12
12
  if settings.DOMAIN_ENABLED:
13
- PYPI_API_URL = "pypi/<slug:pulp_domain>/<path:path>/"
13
+ PYPI_API_URL = "/<slug:pulp_domain>/<path:path>/"
14
14
  else:
15
- PYPI_API_URL = "pypi/<path:path>/"
15
+ PYPI_API_URL = "/<path:path>/"
16
+ PYPI_API_URL = settings.PYPI_PATH_PREFIX.strip("/") + PYPI_API_URL
16
17
  # TODO: Implement remaining PyPI endpoints
17
18
  # path("project/", PackageProject.as_view()), # Endpoints to nicely see contents of index
18
19
  # path("search/", PackageSearch.as_view()),
pulp_python/app/utils.py CHANGED
@@ -20,7 +20,6 @@ from pulpcore.plugin.models import Artifact, Remote
20
20
  from pulpcore.plugin.exceptions import TimeoutException
21
21
  from pulpcore.plugin.util import get_domain
22
22
 
23
-
24
23
  log = logging.getLogger(__name__)
25
24
 
26
25
 
@@ -1,19 +1,25 @@
1
1
  from bandersnatch.configuration import BandersnatchConfig
2
2
  from django.db import transaction
3
- from drf_spectacular.utils import extend_schema
3
+ from django_filters import CharFilter
4
+ from django_filters.rest_framework import filters as drf_filters
5
+ from drf_spectacular.utils import extend_schema, extend_schema_view
6
+ from packaging.utils import canonicalize_name
4
7
  from pathlib import Path
5
8
  from rest_framework import status
6
9
  from rest_framework.decorators import action
7
10
  from rest_framework.response import Response
11
+ from rest_framework.serializers import ValidationError
8
12
 
9
13
  from pulpcore.plugin import viewsets as core_viewsets
10
14
  from pulpcore.plugin.actions import ModifyRepositoryActionMixin
11
15
  from pulpcore.plugin.models import RepositoryVersion
12
16
  from pulpcore.plugin.serializers import (
13
17
  AsyncOperationResponseSerializer,
18
+ RepositoryAddRemoveContentSerializer,
14
19
  RepositorySyncURLSerializer,
15
20
  )
16
21
  from pulpcore.plugin.tasking import check_content, dispatch
22
+ from pulpcore.plugin.util import extract_pk
17
23
 
18
24
  from pulp_python.app import models as python_models
19
25
  from pulp_python.app import serializers as python_serializers
@@ -124,6 +130,43 @@ class PythonRepositoryViewSet(
124
130
  "python.pythonrepository_viewer": ["python.view_pythonrepository"],
125
131
  }
126
132
 
133
+ @extend_schema(
134
+ description="Trigger an asynchronous task to create a new repository version.",
135
+ summary="Modify Repository Content",
136
+ responses={202: AsyncOperationResponseSerializer},
137
+ )
138
+ @action(detail=True, methods=["post"], serializer_class=RepositoryAddRemoveContentSerializer)
139
+ def modify(self, request, pk):
140
+ """
141
+ Queues a task that creates a new RepositoryVersion by adding and removing content units.
142
+
143
+ If allow_package_substitution is False and the request is **only** adding packages, then a
144
+ package substitution check is performed to provide a quicker error response. Otherwise, the
145
+ check is delegated to the task.
146
+ """
147
+ repository = self.get_object()
148
+ if not repository.allow_package_substitution:
149
+ remove_content_units = request.data.get("remove_content_units", [])
150
+ if remove_content_units or "base_version" in request.data:
151
+ return super().modify(request, pk)
152
+ rvc = repository.latest_version().content
153
+ add_content_units = request.data.get("add_content_units", [])
154
+ content_ids = [extract_pk(x) for x in add_content_units]
155
+ packages = (
156
+ python_models.PythonPackageContent.objects.filter(pk__in=content_ids)
157
+ .exclude(pk__in=rvc)
158
+ .values("filename")
159
+ )
160
+ conflicting_packages = python_models.PythonPackageContent.objects.filter(
161
+ filename__in=packages, pk__in=rvc
162
+ )
163
+ if conflicting_packages.exists():
164
+ raise ValidationError(
165
+ "Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
166
+ f"Existing conflicting packages: {conflicting_packages.values('filename', 'sha256', 'pk')}" # noqa: E501
167
+ )
168
+ return super().modify(request, pk)
169
+
127
170
  @extend_schema(
128
171
  summary="Repair metadata",
129
172
  responses={202: AsyncOperationResponseSerializer},
@@ -329,15 +372,34 @@ class PythonDistributionViewSet(core_viewsets.DistributionViewSet, core_viewsets
329
372
  }
330
373
 
331
374
 
375
+ class NormalizedNameFilter(CharFilter):
376
+ """Filter that normalizes the input and queries name_normalized."""
377
+
378
+ def filter(self, qs, value):
379
+ if value:
380
+ if isinstance(value, list):
381
+ value = [canonicalize_name(v) for v in value]
382
+ else:
383
+ value = canonicalize_name(value)
384
+ return super().filter(qs, value)
385
+
386
+
387
+ class NormalizedNameInFilter(drf_filters.BaseInFilter, NormalizedNameFilter):
388
+ """In-filter that normalizes each input value and queries name_normalized."""
389
+
390
+
332
391
  class PythonPackageContentFilter(core_viewsets.ContentFilter):
333
392
  """
334
393
  FilterSet for PythonPackageContent.
335
394
  """
336
395
 
396
+ name = NormalizedNameFilter(field_name="name_normalized", lookup_expr="exact")
397
+ name__in = NormalizedNameInFilter(field_name="name_normalized", lookup_expr="in")
398
+ name__contains = CharFilter(field_name="name", lookup_expr="contains")
399
+
337
400
  class Meta:
338
401
  model = python_models.PythonPackageContent
339
402
  fields = {
340
- "name": ["exact", "in", "contains"],
341
403
  "author": ["exact", "in", "contains"],
342
404
  "packagetype": ["exact", "in"],
343
405
  "requires_python": ["exact", "in", "contains"],
@@ -605,11 +667,21 @@ class PythonRemoteViewSet(core_viewsets.RemoteViewSet, core_viewsets.RolesMixin)
605
667
  return Response(remote.data, status=status.HTTP_201_CREATED, headers=headers)
606
668
 
607
669
 
670
+ @extend_schema_view(
671
+ list=extend_schema(deprecated=True),
672
+ add_role=extend_schema(deprecated=True),
673
+ remove_role=extend_schema(deprecated=True),
674
+ list_roles=extend_schema(deprecated=True),
675
+ my_permissions=extend_schema(deprecated=True),
676
+ )
608
677
  class PythonPublicationViewSet(core_viewsets.PublicationViewSet, core_viewsets.RolesMixin):
609
678
  """
610
- <!-- User-facing documentation, rendered as html-->
611
679
  Python Publications refer to the Python Package content in a repository version, and include
612
- metadata about that content.
680
+ metadata about that content. [Deprecated] See
681
+ https://pulpproject.org/pulp_python/docs/user/guides/host/#migrating-off-publications for more
682
+ information.
683
+
684
+ Use a repository or repository-version to serve content instead.
613
685
 
614
686
  """
615
687
 
@@ -677,7 +749,7 @@ class PythonPublicationViewSet(core_viewsets.PublicationViewSet, core_viewsets.R
677
749
  "python.pythonpublication_viewer": ["python.view_pythonpublication"],
678
750
  }
679
751
 
680
- @extend_schema(responses={202: AsyncOperationResponseSerializer})
752
+ @extend_schema(responses={202: AsyncOperationResponseSerializer}, deprecated=True)
681
753
  def create(self, request):
682
754
  """
683
755
  <!-- User-facing documentation, rendered as html-->
@@ -698,3 +770,11 @@ class PythonPublicationViewSet(core_viewsets.PublicationViewSet, core_viewsets.R
698
770
  kwargs={"repository_version_pk": str(repository_version.pk)},
699
771
  )
700
772
  return core_viewsets.OperationPostponedResponse(result, request)
773
+
774
+ @extend_schema(deprecated=True)
775
+ def retrieve(self, request, *args, **kwargs):
776
+ return super().retrieve(request, *args, **kwargs)
777
+
778
+ @extend_schema(deprecated=True)
779
+ def destroy(self, request, *args, **kwargs):
780
+ return super().destroy(request, *args, **kwargs)
@@ -13,7 +13,6 @@ from pulp_python.tests.functional.constants import (
13
13
  PYTHON_WHEEL_FILENAME,
14
14
  )
15
15
 
16
-
17
16
  # Bindings API Fixtures
18
17
 
19
18
 
@@ -144,44 +144,41 @@ def test_content_create_new_metadata(
144
144
  assert getattr(content, k) == v
145
145
 
146
146
 
147
+ def get_package_url(package, filename):
148
+ with PyPISimple() as client:
149
+ page = client.get_project_page(package)
150
+ for package in page.packages:
151
+ if package.filename == filename:
152
+ return package.url
153
+ raise ValueError(f"Package {filename} not found")
154
+
155
+
147
156
  @pytest.mark.parallel
148
157
  def test_upload_metadata_23_spec(python_content_factory):
149
158
  """Test that packages using metadata spec 2.3 can be uploaded to pulp."""
150
159
  filename = "urllib3-2.2.2-py3-none-any.whl"
151
- with PyPISimple() as client:
152
- page = client.get_project_page("urllib3")
153
- for package in page.packages:
154
- if package.filename == filename:
155
- content = python_content_factory(filename, url=package.url)
156
- assert content.metadata_version == "2.3"
157
- break
160
+ url = get_package_url("urllib3", filename)
161
+ content = python_content_factory(filename, url=url)
162
+ assert content.metadata_version == "2.3"
158
163
 
159
164
 
160
165
  @pytest.mark.parallel
161
166
  def test_upload_requires_python(python_content_factory):
162
167
  filename = "pip-24.3.1-py3-none-any.whl"
163
- with PyPISimple() as client:
164
- page = client.get_project_page("pip")
165
- for package in page.packages:
166
- if package.filename == filename:
167
- content = python_content_factory(filename, url=package.url)
168
- assert content.requires_python == ">=3.8"
169
- break
168
+ url = get_package_url("pip", filename)
169
+ content = python_content_factory(filename, url=url)
170
+ assert content.requires_python == ">=3.8"
170
171
 
171
172
 
172
173
  @pytest.mark.parallel
173
174
  def test_upload_metadata_24_spec(python_content_factory):
174
175
  """Test that packages using metadata spec 2.4 can be uploaded to pulp."""
175
176
  filename = "setuptools-80.9.0.tar.gz"
176
- with PyPISimple() as client:
177
- page = client.get_project_page("setuptools")
178
- for package in page.packages:
179
- if package.filename == filename:
180
- content = python_content_factory(filename, url=package.url)
181
- assert content.metadata_version == "2.4"
182
- assert content.license_expression == "MIT"
183
- assert content.license_file == '["LICENSE"]'
184
- break
177
+ url = get_package_url("setuptools", filename)
178
+ content = python_content_factory(filename, url=url)
179
+ assert content.metadata_version == "2.4"
180
+ assert content.license_expression == "MIT"
181
+ assert content.license_file == '["LICENSE"]'
185
182
 
186
183
 
187
184
  @pytest.mark.parallel
@@ -203,3 +200,108 @@ def test_package_creation_with_metadata(
203
200
  ensure_metadata(
204
201
  pulp_content_url, distro.base_path, PYTHON_WHEEL_FILENAME, "shelf-reader", "0.1"
205
202
  )
203
+
204
+
205
+ @pytest.mark.parallel
206
+ def test_disallow_package_substitution(
207
+ monitor_task,
208
+ python_bindings,
209
+ python_repo_factory,
210
+ ):
211
+ """
212
+ When allow_package_substitution=False, any new repository version that would substitute
213
+ existing content (same filename, different sha256) is rejected. This applies to both
214
+ content uploads and repository modify operations. Re-adding content with a matching
215
+ sha256 succeeds idempotently.
216
+ """
217
+ repo = python_repo_factory(allow_package_substitution=False)
218
+ msg1 = "Found duplicate packages being added with the same filename but different checksums."
219
+ msg2 = "To allow this, set 'allow_package_substitution' to True on the repository."
220
+
221
+ # First upload succeeds
222
+ content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
223
+ response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
224
+ task = monitor_task(response.task)
225
+ content = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
226
+ assert content.filename == PYTHON_EGG_FILENAME
227
+
228
+ # Re-upload same artifact with same filename — should succeed (idempotent)
229
+ response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
230
+ task = monitor_task(response.task)
231
+ assert content.pulp_href in task.created_resources
232
+ repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
233
+ assert repo.latest_version_href.endswith("/1/")
234
+
235
+ # Upload a different artifact with the same filename — should be rejected
236
+ second_filename = "pip-26.0.1.tar.gz"
237
+ second_url = get_package_url("pip", second_filename)
238
+ content_body2 = {"relative_path": PYTHON_EGG_FILENAME, "file_url": second_url}
239
+ with pytest.raises(PulpTaskError) as exc:
240
+ response = python_bindings.ContentPackagesApi.create(
241
+ repository=repo.pulp_href, **content_body2
242
+ )
243
+ monitor_task(response.task)
244
+ assert msg1 in exc.value.task.error["description"]
245
+ assert msg2 in exc.value.task.error["description"]
246
+
247
+ # Also create the conflicting content without a repo, then try to add via modify
248
+ response = python_bindings.ContentPackagesApi.create(**content_body2)
249
+ task = monitor_task(response.task)
250
+ content2 = python_bindings.ContentPackagesApi.read(task.created_resources[0])
251
+ # When body only contains add_content_units, the request will be rejected
252
+ body = {"add_content_units": [content2.pulp_href]}
253
+ with pytest.raises(python_bindings.ApiException) as exc:
254
+ python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body)
255
+ assert msg1 in exc.value.body
256
+ # Else when body contains other fields, the check will be delegated to the task
257
+ body = {"add_content_units": [content2.pulp_href], "base_version": repo.latest_version_href}
258
+ with pytest.raises(PulpTaskError) as exc:
259
+ monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
260
+ assert msg1 in exc.value.task.error["description"]
261
+ assert msg2 in exc.value.task.error["description"]
262
+
263
+ # Verify the repository still has only the original content
264
+ repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
265
+ assert repo.latest_version_href.endswith("/1/")
266
+ # Check that you can remove the conflicting content and add the new content
267
+ body = {"remove_content_units": [content.pulp_href], "add_content_units": [content2.pulp_href]}
268
+ response = python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body)
269
+ task = monitor_task(response.task)
270
+ repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
271
+ assert repo.latest_version_href.endswith("/2/")
272
+
273
+
274
+ @pytest.mark.parallel
275
+ def test_package_substitution_allowed_by_default(
276
+ monitor_task,
277
+ python_bindings,
278
+ python_repo_factory,
279
+ ):
280
+ """
281
+ By default (allow_package_substitution=True), uploading a file with the same filename
282
+ but different sha256 replaces the existing content in the repository.
283
+ """
284
+ repo = python_repo_factory()
285
+ assert repo.allow_package_substitution is True
286
+
287
+ content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": PYTHON_EGG_URL}
288
+ response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
289
+ task = monitor_task(response.task)
290
+ content1 = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
291
+
292
+ # Upload a different artifact with the same filename — should succeed and replace
293
+ second_filename = "pip-26.0.tar.gz"
294
+ second_url = get_package_url("pip", second_filename)
295
+ content_body = {"relative_path": PYTHON_EGG_FILENAME, "file_url": second_url}
296
+ response = python_bindings.ContentPackagesApi.create(repository=repo.pulp_href, **content_body)
297
+ task = monitor_task(response.task)
298
+ content2 = python_bindings.ContentPackagesApi.read(task.created_resources[-1])
299
+ assert content2.pulp_href != content1.pulp_href
300
+
301
+ # Verify the repo has only the new content
302
+ repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
303
+ content_list = python_bindings.ContentPackagesApi.list(
304
+ repository_version=repo.latest_version_href
305
+ )
306
+ assert content_list.count == 1
307
+ assert content_list.results[0].sha256 == content2.sha256
@@ -12,7 +12,6 @@ from pulp_python.tests.functional.constants import (
12
12
  )
13
13
  from urllib.parse import urlsplit
14
14
 
15
-
16
15
  pytestmark = pytest.mark.skipif(not settings.DOMAIN_ENABLED, reason="Domain not enabled")
17
16
 
18
17
 
@@ -182,7 +181,6 @@ def test_domain_content_replication(
182
181
  python_bindings.ContentPackagesApi,
183
182
  python_bindings.RepositoriesPythonApi,
184
183
  python_bindings.RemotesPythonApi,
185
- python_bindings.PublicationsPypiApi,
186
184
  python_bindings.DistributionsPypiApi,
187
185
  ):
188
186
  result = api_client.list(pulp_domain=replica_domain.name)
@@ -204,9 +202,6 @@ def test_domain_content_replication(
204
202
 
205
203
  response = python_bindings.ContentPackagesApi.list(pulp_domain=replica_domain.name)
206
204
  assert PYTHON_SM_PACKAGE_COUNT + 1 == response.count
207
- response = python_bindings.PublicationsPypiApi.list(pulp_domain=replica_domain.name)
208
- assert 2 == response.count
209
- add_to_cleanup(python_bindings.PublicationsPypiApi, response.results[0])
210
205
  assert 1 == python_bindings.RepositoriesPythonApi.list(pulp_domain=replica_domain.name).count
211
206
  assert 1 == python_bindings.DistributionsPypiApi.list(pulp_domain=replica_domain.name).count
212
207
  assert 1 == python_bindings.RemotesPythonApi.list(pulp_domain=replica_domain.name).count
@@ -14,7 +14,6 @@ from pulp_python.tests.functional.constants import (
14
14
  PYTHON_SM_PROJECT_SPECIFIER,
15
15
  )
16
16
 
17
-
18
17
  pytestmark = [
19
18
  pytest.mark.skipif(
20
19
  "/tmp" not in settings.ALLOWED_EXPORT_PATHS,
@@ -16,7 +16,6 @@ from pulp_python.tests.functional.constants import (
16
16
  )
17
17
  from pulp_python.tests.functional.utils import ensure_metadata
18
18
 
19
-
20
19
  PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
21
20
 
22
21
 
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  from urllib.parse import urljoin
3
3
 
4
-
5
4
  PULP_FIXTURES_BASE_URL = os.environ.get(
6
5
  "REMOTE_FIXTURES_ORIGIN", "https://fixtures.pulpproject.org/"
7
6
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.26.1
3
+ Version: 3.28.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.115,>=3.85.3
23
+ Requires-Dist: pulpcore<3.115,>=3.105.0
24
24
  Requires-Dist: pkginfo<1.13.0,>=1.12.0
25
25
  Requires-Dist: bandersnatch<6.7,>=6.6.0
26
26
  Requires-Dist: pypi-simple<2.0,>=1.8.0
@@ -1,16 +1,16 @@
1
1
  pulp_python/__init__.py,sha256=GIuTLoBTc-07dSLJUh8xrZPRz8x-jJ61pfR0J1IjnzI,65
2
- pulp_python/pytest_plugin.py,sha256=r6-flyyEed8lmIQii_zBWPqKN7tKcdr_sGvjixW3N98,8605
3
- pulp_python/app/__init__.py,sha256=6mh2SzpYY028Oh_Bts692hnCpyAKVYil7zdzIOj1vXM,2488
4
- pulp_python/app/global_access_conditions.py,sha256=y8NwdgAoaB5iY7EzSBoCQOgVopSHpVMc55FKJmCGGlI,1081
2
+ pulp_python/pytest_plugin.py,sha256=EBivPJa39ZE1QaDWea9candeeJL_W_cBk6Q7Onhw4bE,8604
3
+ pulp_python/app/__init__.py,sha256=VQR1pE7NwTGjpbsrocrfmt0jkvIahx4QbL0lV5knq3A,2488
4
+ pulp_python/app/global_access_conditions.py,sha256=MZJtyoVsr-4hRaty6mKDqh3caOHd5UKJjEWLV2crOLs,1080
5
5
  pulp_python/app/modelresource.py,sha256=dogoBWibBmQyFpcV-Hp1lu7D2WwSECa5PEShWSIg7mg,1248
6
- pulp_python/app/models.py,sha256=j2U__78AMhyYIiRKyA_o5_55EvqkZSJVh4QWmpGtX-A,14055
6
+ pulp_python/app/models.py,sha256=954pFPP05zbwRQmicA0NxFT3VIEur17HFOxPGSYBgp0,15218
7
7
  pulp_python/app/provenance.py,sha256=iyhkuNahHiTDK0Djrd4-PlgErA5SJVI0uQOIPj46tEI,2352
8
- pulp_python/app/replica.py,sha256=DaBq5biQARMePmoHuAZbYSn3oRR-VOHztzuw8MCxQA0,1877
9
- pulp_python/app/serializers.py,sha256=GODYeagBCeSi3VuI87c2ELw4dQPqjZDXb70cch_bDpw,28427
10
- pulp_python/app/settings.py,sha256=HXJK3rr0LVTOv1xBS5cZvVtz6j4SvFZl0PH3sLTcu2w,227
11
- pulp_python/app/urls.py,sha256=dw6SQ7N6SIxHALAjTdGg3dPL3-54MVoDzUKfqXA2epI,1328
12
- pulp_python/app/utils.py,sha256=Y0lYDMCsGaIh8MKU6Wu3n57LuNzN3zKn0rVP_XZKPVE,25777
13
- pulp_python/app/viewsets.py,sha256=BI18m7a1Yo5AkKf0KSgvWowavT3hslADdgDBuR_V0Hw,27712
8
+ pulp_python/app/replica.py,sha256=GHSLLKUVg4SRMEsMnesxI2xgB0Z83AzrWY1-UbACF0g,1802
9
+ pulp_python/app/serializers.py,sha256=67D1fGg-GxD3iQlEo-TI-3JOyaI2-VHqU_UNn9RDha8,29433
10
+ pulp_python/app/settings.py,sha256=Cyc_p6U0HQjKpyrRL6JFrK3R7RMQJ9MAgNMJCfzPEiA,255
11
+ pulp_python/app/urls.py,sha256=3-UG9-bkPW_bs8362XBnxbpGqQetyuMOfPaayadMYFo,1387
12
+ pulp_python/app/utils.py,sha256=RrKEqVnsorf2UICSjR3WnH8X1m-XjoVIiZs8u0JrhfM,25776
13
+ pulp_python/app/viewsets.py,sha256=yAXvuHNLdOkLznwiqIPSQPKf8-ttPGsmWy-mG1cbO38,31410
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
16
  pulp_python/app/management/commands/repair-python-metadata.py,sha256=i-mC3UYxIiHJoknRV4xZZ7cQ44lyqWshpbaeYi7as8E,4457
@@ -34,12 +34,14 @@ pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py,sha256=f
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
36
  pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py,sha256=glHecwu0X17RdRHo1MvjDRvrqpJ7EgxvYXy1FOXJvn4,817
37
+ pulp_python/app/migrations/0020_pythonpackagecontent_name_normalized.py,sha256=n-gtLKmhDiydgkM5Fv8OisoFZYPPOhOa8-9nExCCq14,1317
38
+ pulp_python/app/migrations/0021_pythonrepository_upload_duplicate_filenames.py,sha256=Yx-Cgk8DR9J9e9ht00xy0s6ogh2Rh5JS7AbdzSngKLY,384
37
39
  pulp_python/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
40
  pulp_python/app/pypi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
41
  pulp_python/app/pypi/serializers.py,sha256=w6_doLpaO8Dnzur57MYrTIqEP4I3LI8biu9ipLC66fk,5028
40
- pulp_python/app/pypi/views.py,sha256=ksQflLQSwcsmoJtQG6B440rQ8CPF_bSTdWXpYJFEAg4,21006
42
+ pulp_python/app/pypi/views.py,sha256=s53NgE_LiWKVJJdLMOwj8XG23vvKObmSI95cLklkXtw,21127
41
43
  pulp_python/app/tasks/__init__.py,sha256=lTFpVvpDKbqv9RC0b2RYU8Bo6svDjrA-djt16pADFr8,284
42
- pulp_python/app/tasks/publish.py,sha256=b0JwHZvnIsJ8gEc_GJm6lUKQC3Po1lxW1TcP2q59WXA,4335
44
+ pulp_python/app/tasks/publish.py,sha256=_1SymLwmJsZepBLdecb69ijnnTeqiH054I6n4HuSIVo,4334
43
45
  pulp_python/app/tasks/repair.py,sha256=_SACezEsnal8VnSGR91yTI-kJ6axZaEIBrhS1hkO8JA,11723
44
46
  pulp_python/app/tasks/sync.py,sha256=hBA4iyY9HFbLZMFWqVJ4W3Q3NAN122QVaOSEGSSnMtc,12868
45
47
  pulp_python/app/tasks/upload.py,sha256=12E9ihDqbe9Ihij9o6p_yuV6WF1Yyt6zPeOI2dzCEms,5697
@@ -49,20 +51,20 @@ pulp_python/app/webserver_snippets/apache.conf,sha256=3frHSl2YV_8pJPscaFxMVo7Hmx
49
51
  pulp_python/app/webserver_snippets/nginx.conf,sha256=gMqZGFefsTJVVx9YRxpHVS7NMEll9CzOseYdtLr3Avc,344
50
52
  pulp_python/tests/__init__.py,sha256=4Yz43a8s-KyhdHFb5eEhIIvH72807Y84uAHnG5bO5y0,31
51
53
  pulp_python/tests/functional/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- pulp_python/tests/functional/constants.py,sha256=lUkMLsPUOeF7duMB_epr65gDMybLu-JZDWNFKT2Ybio,13820
54
+ pulp_python/tests/functional/constants.py,sha256=SvzlxGQz1I-p3JEBSqNz9qoAYfq5yhGsqtYoJpCOioU,13819
53
55
  pulp_python/tests/functional/utils.py,sha256=ZmOVSa94o0KvH7l42YwuUAcNJo8Eb733QBH2G6nGkeQ,5901
54
56
  pulp_python/tests/functional/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
57
  pulp_python/tests/functional/api/test_attestations.py,sha256=_opUZT65c5H2j_8ous-nKGHmDOw8EMjxdooTIQhiXQ8,8640
56
58
  pulp_python/tests/functional/api/test_auto_publish.py,sha256=uCIt4LsO61oMk3bDs3LMQDJI8zkKqvY0b1uX16bTxzM,1747
57
59
  pulp_python/tests/functional/api/test_consume_content.py,sha256=QUOZ_bQ_Ortzitc7sjlMEJzRhPm00wayxjZvEeK18jI,1000
58
- pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=GbCDRY5iaLSRvPAvhz1a3mrYu_WN2lOEnFLltT6yL54,8756
60
+ pulp_python/tests/functional/api/test_crud_content_unit.py,sha256=IjkyVC761ZTOhdM3ksT_5zvvWhMk326BSJlUyql0vFM,13793
59
61
  pulp_python/tests/functional/api/test_crud_publications.py,sha256=uoTGHbhKq_mYOYtNV8Tt9cyhzY0zsoG1DhYrfIdYcMc,5646
60
62
  pulp_python/tests/functional/api/test_crud_remotes.py,sha256=uRo51X3MDWIls4Fdxw5unvU47_JyyLEg2w41ByvDcmY,5811
61
- pulp_python/tests/functional/api/test_domains.py,sha256=RtQWpp78Oq0Vq1V_-sbBBIo9E8jR_lkcKMjgH6--lWI,10453
63
+ pulp_python/tests/functional/api/test_domains.py,sha256=owbvFxOKDuAVX2PUpsO0olAmQmIKFThBqnQtjfKbS8Y,10210
62
64
  pulp_python/tests/functional/api/test_download_content.py,sha256=msnsKpU6TGT8eNu6e0uFa4fte6hj85OTexwOHdf66-c,4950
63
- pulp_python/tests/functional/api/test_export_import.py,sha256=A_LkhvnoXzm1-F7Qy8dZZcpDo2BPngnhjf55T0bH20E,4513
65
+ pulp_python/tests/functional/api/test_export_import.py,sha256=QciJ7Pdv6HSv3mKGqKSNW5g9M8e9tkWcapRQ1PUxabU,4512
64
66
  pulp_python/tests/functional/api/test_full_mirror.py,sha256=npBj0IR10JUmjZyBY99NAGc8t5Xjbzl-y__69X9Eh1E,12045
65
- pulp_python/tests/functional/api/test_pypi_apis.py,sha256=aQuZy_OO767fT0arc0syE6qtYkFjh0nEmp8Gqo9RDj4,11692
67
+ pulp_python/tests/functional/api/test_pypi_apis.py,sha256=sdovsmCb88aDaEqTe_RKXiBJDaAx5BRRaT1oNPbIAfo,11691
66
68
  pulp_python/tests/functional/api/test_pypi_simple_api.py,sha256=UzO3K3GMqiDUsAMWlxy9xM3kYtnD6e2WYNtwlJWYm-U,7251
67
69
  pulp_python/tests/functional/api/test_rbac.py,sha256=cy1RQHvWKbE4f4aPu33ZdIUBiJBnW8aOXKlDHHcCuxo,10512
68
70
  pulp_python/tests/functional/api/test_repair.py,sha256=8tW4jgR4pAsAswsblZM2NKdGAKnh1zuH7572k2GO4ag,12908
@@ -73,9 +75,9 @@ pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation,
73
75
  pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation,sha256=muTQ8dqYSSdx76DlaPjB1REcNIS-aak-Na0TkASxu8M,10426
74
76
  pulp_python/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
77
  pulp_python/tests/unit/test_models.py,sha256=TBI0yKsrdbnJSPeBFfxSqhXK7zaNvR6qg5JehGH3Pds,229
76
- pulp_python-3.26.1.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
77
- pulp_python-3.26.1.dist-info/METADATA,sha256=17f-Cq_Rg-n3Nq4H1B0fbY5nEyvZrOZOPYOrTkmnsHk,1743
78
- pulp_python-3.26.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
79
- pulp_python-3.26.1.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
80
- pulp_python-3.26.1.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
81
- pulp_python-3.26.1.dist-info/RECORD,,
78
+ pulp_python-3.28.0.dist-info/licenses/LICENSE,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
79
+ pulp_python-3.28.0.dist-info/METADATA,sha256=WvS2V5FUZUE3BYiWfMg9SBMxgC__4zGZHi5rDh4orLU,1744
80
+ pulp_python-3.28.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
81
+ pulp_python-3.28.0.dist-info/entry_points.txt,sha256=HvqLEXjw_dS5jqAwnE5JiRZFE6f-y5SRtitKLPml2To,115
82
+ pulp_python-3.28.0.dist-info/top_level.txt,sha256=X0hXgXc_bpbiKqVrkt8jD5_QEiQviKbHDwveQcOcJjo,12
83
+ pulp_python-3.28.0.dist-info/RECORD,,