pulp-python 3.28.2__py3-none-any.whl → 3.30.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 +5 -3
- pulp_python/app/management/commands/repair-python-metadata.py +7 -3
- pulp_python/app/migrations/0022_pythonblocklistentry.py +48 -0
- pulp_python/app/modelresource.py +1 -0
- pulp_python/app/models.py +78 -12
- pulp_python/app/pypi/serializers.py +7 -5
- pulp_python/app/pypi/views.py +23 -24
- pulp_python/app/replica.py +3 -2
- pulp_python/app/serializers.py +136 -14
- pulp_python/app/tasks/publish.py +2 -2
- pulp_python/app/tasks/repair.py +4 -2
- pulp_python/app/tasks/sync.py +13 -15
- pulp_python/app/tasks/upload.py +7 -6
- pulp_python/app/urls.py +2 -2
- pulp_python/app/utils.py +8 -6
- pulp_python/app/viewsets.py +81 -3
- pulp_python/pytest_plugin.py +8 -6
- pulp_python/tests/functional/api/test_attestations.py +2 -2
- pulp_python/tests/functional/api/test_blocklist.py +152 -0
- pulp_python/tests/functional/api/test_consume_content.py +0 -1
- pulp_python/tests/functional/api/test_crud_content_unit.py +5 -4
- pulp_python/tests/functional/api/test_crud_publications.py +6 -5
- pulp_python/tests/functional/api/test_crud_remotes.py +3 -2
- pulp_python/tests/functional/api/test_domains.py +6 -5
- pulp_python/tests/functional/api/test_download_content.py +2 -2
- pulp_python/tests/functional/api/test_export_import.py +5 -3
- pulp_python/tests/functional/api/test_full_mirror.py +9 -9
- pulp_python/tests/functional/api/test_pypi_apis.py +5 -5
- pulp_python/tests/functional/api/test_pypi_simple_api.py +2 -2
- pulp_python/tests/functional/api/test_rbac.py +5 -4
- pulp_python/tests/functional/api/test_repair.py +2 -1
- pulp_python/tests/functional/api/test_sync.py +11 -11
- pulp_python/tests/functional/api/test_upload.py +5 -3
- pulp_python/tests/functional/utils.py +2 -2
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/METADATA +1 -1
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/RECORD +40 -38
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/WHEEL +0 -0
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/entry_points.txt +0 -0
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/licenses/LICENSE +0 -0
- {pulp_python-3.28.2.dist-info → pulp_python-3.30.0.dist-info}/top_level.txt +0 -0
pulp_python/app/__init__.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from gettext import gettext as _
|
|
2
|
+
|
|
1
3
|
from django.db.models.signals import post_migrate
|
|
4
|
+
|
|
2
5
|
from pulpcore.plugin import PulpPluginAppConfig
|
|
3
|
-
from gettext import gettext as _
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class PulpPythonPluginAppConfig(PulpPluginAppConfig):
|
|
@@ -10,7 +12,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
|
|
|
10
12
|
|
|
11
13
|
name = "pulp_python.app"
|
|
12
14
|
label = "python"
|
|
13
|
-
version = "3.
|
|
15
|
+
version = "3.30.0"
|
|
14
16
|
python_package_name = "pulp-python"
|
|
15
17
|
domain_compatible = True
|
|
16
18
|
|
|
@@ -26,7 +28,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
|
|
|
26
28
|
|
|
27
29
|
# TODO: Remove this when https://github.com/pulp/pulpcore/issues/5500 is resolved
|
|
28
30
|
def _populate_pypi_access_policies(sender, apps, verbosity, **kwargs):
|
|
29
|
-
from pulp_python.app.pypi.views import PyPIView, SimpleView, UploadView
|
|
31
|
+
from pulp_python.app.pypi.views import MetadataView, PyPIView, SimpleView, UploadView
|
|
30
32
|
|
|
31
33
|
try:
|
|
32
34
|
AccessPolicy = apps.get_model("core", "AccessPolicy")
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import os
|
|
3
|
-
|
|
2
|
+
import re
|
|
4
3
|
from gettext import gettext as _
|
|
5
4
|
|
|
6
5
|
from django.conf import settings
|
|
6
|
+
from django.core.management import BaseCommand, CommandError
|
|
7
7
|
|
|
8
8
|
from pulpcore.plugin.util import extract_pk
|
|
9
|
+
|
|
9
10
|
from pulp_python.app.models import PythonPackageContent, PythonRepository
|
|
10
11
|
from pulp_python.app.utils import artifact_to_python_content_data
|
|
11
12
|
|
|
@@ -78,7 +79,10 @@ class Command(BaseCommand):
|
|
|
78
79
|
Management command to repair metadata of PythonPackageContent.
|
|
79
80
|
"""
|
|
80
81
|
|
|
81
|
-
help = _(
|
|
82
|
+
help = _(
|
|
83
|
+
"[Deprecated] Use the repository `repair_metadata` task instead. "
|
|
84
|
+
"Repair the metadata of PythonPackageContent stored in PythonRepositories."
|
|
85
|
+
)
|
|
82
86
|
|
|
83
87
|
def add_arguments(self, parser):
|
|
84
88
|
"""Set up arguments."""
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 5.2.10 on 2026-04-16 14:00
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_lifecycle.mixins
|
|
5
|
+
import pulpcore.app.models.base
|
|
6
|
+
from django.db import migrations, models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("python", "0021_pythonrepository_upload_duplicate_filenames"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name="PythonBlocklistEntry",
|
|
18
|
+
fields=[
|
|
19
|
+
(
|
|
20
|
+
"pulp_id",
|
|
21
|
+
models.UUIDField(
|
|
22
|
+
default=pulpcore.app.models.base.pulp_uuid,
|
|
23
|
+
editable=False,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
serialize=False,
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
("pulp_created", models.DateTimeField(auto_now_add=True)),
|
|
29
|
+
("pulp_last_updated", models.DateTimeField(auto_now=True, null=True)),
|
|
30
|
+
("name", models.TextField(default=None, null=True)),
|
|
31
|
+
("version", models.TextField(default=None, null=True)),
|
|
32
|
+
("filename", models.TextField(default=None, null=True)),
|
|
33
|
+
("added_by", models.TextField(default="")),
|
|
34
|
+
(
|
|
35
|
+
"repository",
|
|
36
|
+
models.ForeignKey(
|
|
37
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
38
|
+
related_name="blocklist_entries",
|
|
39
|
+
to="python.pythonrepository",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
options={
|
|
44
|
+
"default_related_name": "%(app_label)s_%(model_name)s",
|
|
45
|
+
},
|
|
46
|
+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
|
|
47
|
+
),
|
|
48
|
+
]
|
pulp_python/app/modelresource.py
CHANGED
pulp_python/app/models.py
CHANGED
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import json
|
|
3
3
|
from logging import getLogger
|
|
4
|
+
from pathlib import PurePath
|
|
4
5
|
|
|
5
6
|
from aiohttp.web import json_response
|
|
7
|
+
from django.conf import settings
|
|
6
8
|
from django.contrib.postgres.fields import ArrayField
|
|
7
9
|
from django.core.exceptions import ObjectDoesNotExist
|
|
8
10
|
from django.db import models
|
|
9
|
-
from django.conf import settings
|
|
10
11
|
from django_lifecycle import (
|
|
11
12
|
BEFORE_SAVE,
|
|
12
13
|
hook,
|
|
13
14
|
)
|
|
14
15
|
from rest_framework.serializers import ValidationError
|
|
16
|
+
|
|
15
17
|
from pulpcore.plugin.models import (
|
|
16
18
|
AutoAddObjPermsMixin,
|
|
19
|
+
BaseModel,
|
|
17
20
|
Content,
|
|
18
|
-
Publication,
|
|
19
21
|
Distribution,
|
|
22
|
+
Publication,
|
|
20
23
|
Remote,
|
|
21
24
|
Repository,
|
|
22
25
|
)
|
|
26
|
+
from pulpcore.plugin.repo_version_utils import (
|
|
27
|
+
collect_duplicates,
|
|
28
|
+
remove_duplicates,
|
|
29
|
+
validate_repo_version,
|
|
30
|
+
)
|
|
23
31
|
from pulpcore.plugin.responses import ArtifactResponse
|
|
32
|
+
from pulpcore.plugin.util import get_domain, get_domain_pk
|
|
24
33
|
|
|
25
|
-
from pathlib import PurePath
|
|
26
34
|
from .provenance import Provenance
|
|
27
35
|
from .utils import (
|
|
28
|
-
|
|
36
|
+
PYPI_LAST_SERIAL,
|
|
37
|
+
PYPI_SERIAL_CONSTANT,
|
|
29
38
|
artifact_to_metadata_artifact,
|
|
39
|
+
artifact_to_python_content_data,
|
|
30
40
|
canonicalize_name,
|
|
31
41
|
python_content_to_json,
|
|
32
|
-
PYPI_LAST_SERIAL,
|
|
33
|
-
PYPI_SERIAL_CONSTANT,
|
|
34
|
-
)
|
|
35
|
-
from pulpcore.plugin.repo_version_utils import (
|
|
36
|
-
collect_duplicates,
|
|
37
|
-
remove_duplicates,
|
|
38
|
-
validate_repo_version,
|
|
39
42
|
)
|
|
40
|
-
from pulpcore.plugin.util import get_domain_pk, get_domain
|
|
41
43
|
|
|
42
44
|
log = getLogger(__name__)
|
|
43
45
|
|
|
@@ -399,9 +401,12 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
399
401
|
|
|
400
402
|
When allow_package_substitution is False, reject any new version that would implicitly
|
|
401
403
|
replace existing content with different checksums (content substitution).
|
|
404
|
+
|
|
405
|
+
Also checks newly added content against the repository's blocklist entries.
|
|
402
406
|
"""
|
|
403
407
|
if not self.allow_package_substitution:
|
|
404
408
|
self._check_for_package_substitution(new_version)
|
|
409
|
+
self._check_blocklist(new_version)
|
|
405
410
|
remove_duplicates(new_version)
|
|
406
411
|
validate_repo_version(new_version)
|
|
407
412
|
|
|
@@ -418,3 +423,64 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
418
423
|
"To allow this, set 'allow_package_substitution' to True on the repository. "
|
|
419
424
|
f"Conflicting packages: {duplicates}"
|
|
420
425
|
)
|
|
426
|
+
|
|
427
|
+
def _check_blocklist(self, new_version):
|
|
428
|
+
"""
|
|
429
|
+
Check newly added content in a repository version against the blocklist.
|
|
430
|
+
"""
|
|
431
|
+
added_content = PythonPackageContent.objects.filter(
|
|
432
|
+
pk__in=new_version.added().values_list("pk", flat=True)
|
|
433
|
+
).only("filename", "name_normalized", "version")
|
|
434
|
+
if added_content.exists():
|
|
435
|
+
self.check_blocklist_for_packages(added_content)
|
|
436
|
+
|
|
437
|
+
def check_blocklist_for_packages(self, packages):
|
|
438
|
+
"""
|
|
439
|
+
Raise a ValidationError if any of the given packages match a blocklist entry.
|
|
440
|
+
"""
|
|
441
|
+
entries = PythonBlocklistEntry.objects.filter(repository=self)
|
|
442
|
+
if not entries.exists():
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
blocked = []
|
|
446
|
+
for pkg in packages:
|
|
447
|
+
for entry in entries:
|
|
448
|
+
if entry.filename and entry.filename == pkg.filename:
|
|
449
|
+
blocked.append(pkg.filename)
|
|
450
|
+
break
|
|
451
|
+
if entry.name == pkg.name_normalized:
|
|
452
|
+
if not entry.version or entry.version == pkg.version:
|
|
453
|
+
blocked.append(pkg.filename)
|
|
454
|
+
break
|
|
455
|
+
if blocked:
|
|
456
|
+
raise ValidationError(
|
|
457
|
+
"Blocklisted packages cannot be added to this repository: {}".format(
|
|
458
|
+
", ".join(blocked)
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class PythonBlocklistEntry(BaseModel):
|
|
464
|
+
"""
|
|
465
|
+
An entry in a PythonRepository's package blocklist.
|
|
466
|
+
|
|
467
|
+
Blocklist entries prevent packages from being added to the repository.
|
|
468
|
+
Entries can match by package `name` (all versions), package `name` + `version`,
|
|
469
|
+
or exact `filename`. Exactly one of `name` or `filename` must be provided.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
name = models.TextField(null=True, default=None)
|
|
473
|
+
version = models.TextField(null=True, default=None)
|
|
474
|
+
filename = models.TextField(null=True, default=None)
|
|
475
|
+
added_by = models.TextField(default="")
|
|
476
|
+
repository = models.ForeignKey(
|
|
477
|
+
PythonRepository, on_delete=models.CASCADE, related_name="blocklist_entries"
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def __str__(self):
|
|
481
|
+
if self.filename:
|
|
482
|
+
return f"<{self._meta.object_name}: {self.filename}>"
|
|
483
|
+
return f"<{self._meta.object_name}: {self.name} [{self.version or 'all'}]>"
|
|
484
|
+
|
|
485
|
+
class Meta:
|
|
486
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from gettext import gettext as _
|
|
3
3
|
|
|
4
|
-
from
|
|
4
|
+
from django.db.utils import IntegrityError
|
|
5
5
|
from pydantic import TypeAdapter, ValidationError
|
|
6
|
-
from
|
|
7
|
-
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
|
|
8
8
|
from pulpcore.plugin.models import Artifact
|
|
9
9
|
from pulpcore.plugin.util import get_domain
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
from pulp_python.app.provenance import Attestation
|
|
12
|
+
from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
|
|
11
13
|
|
|
12
14
|
log = logging.getLogger(__name__)
|
|
13
15
|
|
|
@@ -110,7 +112,7 @@ class PackageUploadSerializer(serializers.Serializer):
|
|
|
110
112
|
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
|
|
111
113
|
except ValidationError as e:
|
|
112
114
|
raise serializers.ValidationError(
|
|
113
|
-
{"attestations": _("The uploaded attestations are not valid: {}".format(e)
|
|
115
|
+
{"attestations": _("The uploaded attestations are not valid: {}").format(e)}
|
|
114
116
|
)
|
|
115
117
|
|
|
116
118
|
sha256 = data.get("sha256_digest")
|
pulp_python/app/pypi/views.py
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from rest_framework.exceptions import NotAcceptable
|
|
7
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
8
|
-
from django.shortcuts import redirect
|
|
9
|
-
from datetime import datetime, timezone, timedelta
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
from itertools import chain
|
|
4
|
+
from pathlib import PurePath
|
|
5
|
+
from urllib.parse import urljoin, urlparse, urlunsplit
|
|
10
6
|
|
|
11
7
|
from django.contrib.sessions.models import Session
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
12
9
|
from django.db import transaction
|
|
13
10
|
from django.db.utils import DatabaseError
|
|
14
11
|
from django.http.response import (
|
|
15
12
|
Http404,
|
|
16
|
-
|
|
17
|
-
HttpResponseForbidden,
|
|
13
|
+
HttpResponse,
|
|
18
14
|
HttpResponseBadRequest,
|
|
15
|
+
HttpResponseForbidden,
|
|
16
|
+
HttpResponseNotFound,
|
|
19
17
|
StreamingHttpResponse,
|
|
20
|
-
HttpResponse,
|
|
21
18
|
)
|
|
19
|
+
from django.shortcuts import redirect
|
|
22
20
|
from drf_spectacular.utils import extend_schema
|
|
23
21
|
from dynaconf import settings
|
|
24
|
-
from itertools import chain
|
|
25
22
|
from packaging.utils import canonicalize_name
|
|
26
|
-
from
|
|
27
|
-
from
|
|
23
|
+
from rest_framework.exceptions import NotAcceptable
|
|
24
|
+
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, TemplateHTMLRenderer
|
|
25
|
+
from rest_framework.response import Response
|
|
26
|
+
from rest_framework.viewsets import ViewSet
|
|
28
27
|
|
|
29
|
-
from pulpcore.plugin.viewsets import OperationPostponedResponse
|
|
30
28
|
from pulpcore.plugin.tasking import dispatch
|
|
31
29
|
from pulpcore.plugin.util import get_domain, get_url
|
|
30
|
+
from pulpcore.plugin.viewsets import OperationPostponedResponse
|
|
31
|
+
|
|
32
|
+
from pulp_python.app import tasks
|
|
32
33
|
from pulp_python.app.models import (
|
|
34
|
+
PackageProvenance,
|
|
33
35
|
PythonDistribution,
|
|
34
36
|
PythonPackageContent,
|
|
35
37
|
PythonPublication,
|
|
36
|
-
PackageProvenance,
|
|
37
38
|
)
|
|
38
39
|
from pulp_python.app.pypi.serializers import (
|
|
39
|
-
SummarySerializer,
|
|
40
40
|
PackageMetadataSerializer,
|
|
41
41
|
PackageUploadSerializer,
|
|
42
42
|
PackageUploadTaskSerializer,
|
|
43
|
+
SummarySerializer,
|
|
43
44
|
)
|
|
44
45
|
from pulp_python.app.utils import (
|
|
45
|
-
write_simple_index,
|
|
46
|
-
write_simple_index_json,
|
|
47
|
-
write_simple_detail,
|
|
48
|
-
write_simple_detail_json,
|
|
49
|
-
python_content_to_json,
|
|
50
46
|
PYPI_LAST_SERIAL,
|
|
51
47
|
PYPI_SERIAL_CONSTANT,
|
|
52
48
|
get_remote_package_filter,
|
|
53
49
|
get_remote_simple_page,
|
|
50
|
+
python_content_to_json,
|
|
51
|
+
write_simple_detail,
|
|
52
|
+
write_simple_detail_json,
|
|
53
|
+
write_simple_index,
|
|
54
|
+
write_simple_index_json,
|
|
54
55
|
)
|
|
55
56
|
|
|
56
|
-
from pulp_python.app import tasks
|
|
57
|
-
|
|
58
57
|
log = logging.getLogger(__name__)
|
|
59
58
|
|
|
60
59
|
ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
|
pulp_python/app/replica.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from pulpcore.plugin.replica import Replicator
|
|
2
|
-
|
|
3
1
|
from pulp_glue.python.context import (
|
|
4
2
|
PulpPythonDistributionContext,
|
|
5
3
|
PulpPythonPublicationContext,
|
|
6
4
|
PulpPythonRepositoryContext,
|
|
7
5
|
)
|
|
6
|
+
|
|
7
|
+
from pulpcore.plugin.replica import Replicator
|
|
8
|
+
|
|
8
9
|
from pulp_python.app.models import PythonDistribution, PythonRemote, PythonRepository
|
|
9
10
|
from pulp_python.app.tasks import sync as python_sync
|
|
10
11
|
|
pulp_python/app/serializers.py
CHANGED
|
@@ -2,31 +2,34 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import tempfile
|
|
4
4
|
from gettext import gettext as _
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
5
7
|
from django.conf import settings
|
|
6
8
|
from django.db.utils import IntegrityError
|
|
7
9
|
from drf_spectacular.utils import extend_schema_serializer
|
|
8
10
|
from packaging.requirements import Requirement
|
|
9
|
-
from
|
|
10
|
-
from pypi_attestations import AttestationError
|
|
11
|
+
from packaging.version import InvalidVersion, Version
|
|
11
12
|
from pydantic import TypeAdapter, ValidationError
|
|
12
|
-
from
|
|
13
|
+
from pypi_attestations import AttestationError
|
|
14
|
+
from rest_framework import serializers
|
|
13
15
|
|
|
14
16
|
from pulpcore.plugin import models as core_models
|
|
15
17
|
from pulpcore.plugin import serializers as core_serializers
|
|
16
|
-
from pulpcore.plugin.util import get_domain, get_prn,
|
|
18
|
+
from pulpcore.plugin.util import get_current_authenticated_user, get_domain, get_prn, reverse
|
|
17
19
|
|
|
18
20
|
from pulp_python.app import models as python_models
|
|
19
21
|
from pulp_python.app.provenance import (
|
|
22
|
+
AnyPublisher,
|
|
20
23
|
Attestation,
|
|
24
|
+
AttestationBundle,
|
|
21
25
|
Provenance,
|
|
22
26
|
verify_provenance,
|
|
23
|
-
AttestationBundle,
|
|
24
|
-
AnyPublisher,
|
|
25
27
|
)
|
|
26
28
|
from pulp_python.app.utils import (
|
|
27
29
|
DIST_EXTENSIONS,
|
|
28
30
|
artifact_to_metadata_artifact,
|
|
29
31
|
artifact_to_python_content_data,
|
|
32
|
+
canonicalize_name,
|
|
30
33
|
get_project_metadata_from_file,
|
|
31
34
|
parse_project_metadata,
|
|
32
35
|
)
|
|
@@ -53,6 +56,11 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
53
56
|
default=False,
|
|
54
57
|
required=False,
|
|
55
58
|
)
|
|
59
|
+
blocklist_entries_href = serializers.SerializerMethodField(
|
|
60
|
+
help_text=_("URL to the blocklist entries for this repository."),
|
|
61
|
+
read_only=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
56
64
|
allow_package_substitution = serializers.BooleanField(
|
|
57
65
|
help_text=_(
|
|
58
66
|
"Whether to allow package substitution (replacing existing packages with packages "
|
|
@@ -65,10 +73,15 @@ class PythonRepositorySerializer(core_serializers.RepositorySerializer):
|
|
|
65
73
|
required=False,
|
|
66
74
|
)
|
|
67
75
|
|
|
76
|
+
def get_blocklist_entries_href(self, obj):
|
|
77
|
+
repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.pk})
|
|
78
|
+
return f"{repo_href}blocklist_entries/"
|
|
79
|
+
|
|
68
80
|
class Meta:
|
|
69
81
|
fields = core_serializers.RepositorySerializer.Meta.fields + (
|
|
70
82
|
"autopublish",
|
|
71
83
|
"allow_package_substitution",
|
|
84
|
+
"blocklist_entries_href",
|
|
72
85
|
)
|
|
73
86
|
model = python_models.PythonRepository
|
|
74
87
|
|
|
@@ -226,7 +239,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
226
239
|
required=False,
|
|
227
240
|
allow_blank=True,
|
|
228
241
|
help_text=_(
|
|
229
|
-
"The maintainer's name at a minimum;
|
|
242
|
+
"The maintainer's name at a minimum; additional contact information may be provided."
|
|
230
243
|
),
|
|
231
244
|
)
|
|
232
245
|
maintainer_email = serializers.CharField(
|
|
@@ -375,7 +388,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
375
388
|
else:
|
|
376
389
|
attestations = TypeAdapter(list[Attestation]).validate_python(value)
|
|
377
390
|
except ValidationError as e:
|
|
378
|
-
raise serializers.ValidationError(_("Invalid attestations: {}".format(e))
|
|
391
|
+
raise serializers.ValidationError(_("Invalid attestations: {}").format(e))
|
|
379
392
|
return attestations
|
|
380
393
|
|
|
381
394
|
def handle_attestations(self, filename, sha256, attestations, offline=True):
|
|
@@ -388,7 +401,7 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
|
|
|
388
401
|
verify_provenance(filename, sha256, provenance, offline=offline)
|
|
389
402
|
except AttestationError as e:
|
|
390
403
|
raise serializers.ValidationError(
|
|
391
|
-
{"attestations": _("Attestations failed verification: {}".format(e)
|
|
404
|
+
{"attestations": _("Attestations failed verification: {}").format(e)}
|
|
392
405
|
)
|
|
393
406
|
return provenance.model_dump(mode="json")
|
|
394
407
|
|
|
@@ -643,13 +656,13 @@ class PackageProvenanceSerializer(core_serializers.NoArtifactContentUploadSerial
|
|
|
643
656
|
data["provenance"] = provenance.model_dump(mode="json")
|
|
644
657
|
except ValidationError as e:
|
|
645
658
|
raise serializers.ValidationError(
|
|
646
|
-
_("The uploaded provenance is not valid: {}".format(e)
|
|
659
|
+
_("The uploaded provenance is not valid: {}").format(e)
|
|
647
660
|
)
|
|
648
661
|
if data.pop("verify"):
|
|
649
662
|
try:
|
|
650
663
|
verify_provenance(data["package"].filename, data["package"].sha256, provenance)
|
|
651
664
|
except AttestationError as e:
|
|
652
|
-
raise serializers.ValidationError(_("Provenance verification failed: {}".format(e))
|
|
665
|
+
raise serializers.ValidationError(_("Provenance verification failed: {}").format(e))
|
|
653
666
|
return data
|
|
654
667
|
|
|
655
668
|
def retrieve(self, validated_data):
|
|
@@ -717,7 +730,7 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
|
717
730
|
package_types = MultipleChoiceArrayField(
|
|
718
731
|
required=False,
|
|
719
732
|
help_text=_(
|
|
720
|
-
"The package types to sync for Python content. Leave blank to get
|
|
733
|
+
"The package types to sync for Python content. Leave blank to get everypackage type."
|
|
721
734
|
),
|
|
722
735
|
choices=python_models.PACKAGE_TYPES,
|
|
723
736
|
default=list,
|
|
@@ -752,7 +765,7 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
|
752
765
|
Requirement(pkg)
|
|
753
766
|
except ValueError as ve:
|
|
754
767
|
raise serializers.ValidationError(
|
|
755
|
-
_("includes specifier {} is invalid. {}".format(pkg, ve)
|
|
768
|
+
_("includes specifier {} is invalid. {}").format(pkg, ve)
|
|
756
769
|
)
|
|
757
770
|
return value
|
|
758
771
|
|
|
@@ -763,7 +776,7 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
|
763
776
|
Requirement(pkg)
|
|
764
777
|
except ValueError as ve:
|
|
765
778
|
raise serializers.ValidationError(
|
|
766
|
-
_("excludes specifier {} is invalid. {}".format(pkg, ve)
|
|
779
|
+
_("excludes specifier {} is invalid. {}").format(pkg, ve)
|
|
767
780
|
)
|
|
768
781
|
return value
|
|
769
782
|
|
|
@@ -780,6 +793,115 @@ class PythonRemoteSerializer(core_serializers.RemoteSerializer):
|
|
|
780
793
|
model = python_models.PythonRemote
|
|
781
794
|
|
|
782
795
|
|
|
796
|
+
class PythonBlocklistEntrySerializer(core_serializers.ModelSerializer):
|
|
797
|
+
"""
|
|
798
|
+
Serializer for PythonBlocklistEntry.
|
|
799
|
+
|
|
800
|
+
The `repository` is supplied by the URL (not the request body) and is injected
|
|
801
|
+
by the viewset before saving.
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
pulp_href = serializers.SerializerMethodField(
|
|
805
|
+
read_only=True,
|
|
806
|
+
help_text=_("The URL of this blocklist entry."),
|
|
807
|
+
)
|
|
808
|
+
repository = core_serializers.DetailRelatedField(
|
|
809
|
+
read_only=True,
|
|
810
|
+
view_name_pattern=r"repositories(-.*/.*)?-detail",
|
|
811
|
+
help_text=_("Repository this blocklist entry belongs to."),
|
|
812
|
+
)
|
|
813
|
+
name = serializers.CharField(
|
|
814
|
+
required=False,
|
|
815
|
+
allow_null=True,
|
|
816
|
+
default=None,
|
|
817
|
+
help_text=_(
|
|
818
|
+
"Package name to block (for all versions). Compared after PEP 503 normalization. "
|
|
819
|
+
"Required when 'filename' is not provided."
|
|
820
|
+
),
|
|
821
|
+
)
|
|
822
|
+
version = serializers.CharField(
|
|
823
|
+
required=False,
|
|
824
|
+
allow_null=True,
|
|
825
|
+
default=None,
|
|
826
|
+
help_text=_("Exact version string to block (e.g. '1.0'). Only used when 'name' is set."),
|
|
827
|
+
)
|
|
828
|
+
filename = serializers.CharField(
|
|
829
|
+
required=False,
|
|
830
|
+
allow_null=True,
|
|
831
|
+
default=None,
|
|
832
|
+
help_text=_("Exact filename to block. Required when 'name' is not provided."),
|
|
833
|
+
)
|
|
834
|
+
added_by = serializers.CharField(
|
|
835
|
+
read_only=True,
|
|
836
|
+
help_text=_("PRN of the user who added this blocklist entry."),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def get_pulp_href(self, obj):
|
|
840
|
+
repo_href = reverse("repositories-python/python-detail", kwargs={"pk": obj.repository_id})
|
|
841
|
+
return f"{repo_href}blocklist_entries/{obj.pk}/"
|
|
842
|
+
|
|
843
|
+
def validate(self, data):
|
|
844
|
+
"""
|
|
845
|
+
Validate that the blocklist entry is well-formed and not a duplicate.
|
|
846
|
+
"""
|
|
847
|
+
name = data.get("name")
|
|
848
|
+
filename = data.get("filename")
|
|
849
|
+
version = data.get("version")
|
|
850
|
+
|
|
851
|
+
if version and filename:
|
|
852
|
+
raise serializers.ValidationError(_("'version' cannot be used with 'filename'."))
|
|
853
|
+
if version and not name:
|
|
854
|
+
raise serializers.ValidationError(_("'version' requires 'name' to be provided."))
|
|
855
|
+
if bool(name) == bool(filename):
|
|
856
|
+
raise serializers.ValidationError(
|
|
857
|
+
_("Exactly one of 'name' or 'filename' must be provided.")
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
if version:
|
|
861
|
+
try:
|
|
862
|
+
Version(version)
|
|
863
|
+
except InvalidVersion:
|
|
864
|
+
raise serializers.ValidationError(
|
|
865
|
+
{"version": _("'{}' is not a valid version.").format(version)}
|
|
866
|
+
)
|
|
867
|
+
if name:
|
|
868
|
+
data["name"] = canonicalize_name(name)
|
|
869
|
+
name = data["name"]
|
|
870
|
+
|
|
871
|
+
repository = self.context.get("repository")
|
|
872
|
+
if repository:
|
|
873
|
+
qs = python_models.PythonBlocklistEntry.objects.filter(repository=repository)
|
|
874
|
+
if name and qs.filter(name=name, version=version).exists():
|
|
875
|
+
raise serializers.ValidationError(
|
|
876
|
+
_("A blocklist entry with this name and version already exists.")
|
|
877
|
+
)
|
|
878
|
+
if filename and qs.filter(filename=filename).exists():
|
|
879
|
+
raise serializers.ValidationError(
|
|
880
|
+
_("A blocklist entry with this filename already exists.")
|
|
881
|
+
)
|
|
882
|
+
data["repository"] = repository
|
|
883
|
+
|
|
884
|
+
return data
|
|
885
|
+
|
|
886
|
+
def create(self, validated_data):
|
|
887
|
+
"""
|
|
888
|
+
Create a new blocklist entry, recording the authenticated user in `added_by`.
|
|
889
|
+
"""
|
|
890
|
+
user = get_current_authenticated_user()
|
|
891
|
+
validated_data["added_by"] = get_prn(user) if user else ""
|
|
892
|
+
return super().create(validated_data)
|
|
893
|
+
|
|
894
|
+
class Meta:
|
|
895
|
+
fields = core_serializers.ModelSerializer.Meta.fields + (
|
|
896
|
+
"repository",
|
|
897
|
+
"name",
|
|
898
|
+
"version",
|
|
899
|
+
"filename",
|
|
900
|
+
"added_by",
|
|
901
|
+
)
|
|
902
|
+
model = python_models.PythonBlocklistEntry
|
|
903
|
+
|
|
904
|
+
|
|
783
905
|
class PythonBanderRemoteSerializer(serializers.Serializer):
|
|
784
906
|
"""
|
|
785
907
|
A Serializer for the initial step of creating a Python Remote from a Bandersnatch config file
|
pulp_python/app/tasks/publish.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from gettext import gettext as _
|
|
2
1
|
import logging
|
|
3
2
|
import os
|
|
3
|
+
from gettext import gettext as _
|
|
4
4
|
|
|
5
5
|
from django.core.files import File
|
|
6
6
|
from packaging.utils import canonicalize_name
|
|
@@ -10,7 +10,7 @@ from pulpcore.plugin.util import get_domain
|
|
|
10
10
|
|
|
11
11
|
from pulp_python.app import models as python_models
|
|
12
12
|
from pulp_python.app.serializers import PythonPublicationSerializer
|
|
13
|
-
from pulp_python.app.utils import
|
|
13
|
+
from pulp_python.app.utils import write_simple_detail, write_simple_index
|
|
14
14
|
|
|
15
15
|
log = logging.getLogger(__name__)
|
|
16
16
|
|
pulp_python/app/tasks/repair.py
CHANGED
|
@@ -7,6 +7,10 @@ from uuid import UUID
|
|
|
7
7
|
|
|
8
8
|
from django.db.models import Prefetch
|
|
9
9
|
from django.db.models.query import QuerySet
|
|
10
|
+
|
|
11
|
+
from pulpcore.plugin.models import ContentArtifact, ProgressReport
|
|
12
|
+
from pulpcore.plugin.util import get_domain
|
|
13
|
+
|
|
10
14
|
from pulp_python.app.models import PythonPackageContent, PythonRepository
|
|
11
15
|
from pulp_python.app.utils import (
|
|
12
16
|
artifact_to_python_content_data,
|
|
@@ -16,8 +20,6 @@ from pulp_python.app.utils import (
|
|
|
16
20
|
metadata_content_to_artifact,
|
|
17
21
|
parse_metadata,
|
|
18
22
|
)
|
|
19
|
-
from pulpcore.plugin.models import ContentArtifact, ProgressReport
|
|
20
|
-
from pulpcore.plugin.util import get_domain
|
|
21
23
|
|
|
22
24
|
log = logging.getLogger(__name__)
|
|
23
25
|
|