Skip to content

Commit f3c5557

Browse files
aKlimauclaude
andcommitted
Add more Pulp Exceptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 91a0530 commit f3c5557

11 files changed

Lines changed: 228 additions & 45 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add more Pulp Exceptions.

pulp_python/app/exceptions.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from gettext import gettext as _
2+
3+
from pulpcore.plugin.exceptions import PulpException
4+
5+
6+
class ProvenanceVerificationError(PulpException):
7+
"""
8+
Raised when provenance verification fails.
9+
"""
10+
11+
error_code = "PYT0001"
12+
13+
def __init__(self, message):
14+
super().__init__()
15+
"""
16+
:param message: Description of the provenance verification error
17+
:type message: str
18+
"""
19+
self.message = message
20+
21+
def __str__(self):
22+
return f"[{self.error_code}] " + _("Provenance verification failed: {message}").format(
23+
message=self.message
24+
)
25+
26+
27+
class AttestationVerificationError(PulpException):
28+
"""
29+
Raised when attestation verification fails.
30+
"""
31+
32+
error_code = "PYT0002"
33+
34+
def __init__(self, message):
35+
super().__init__()
36+
"""
37+
:param message: Description of the attestation verification error
38+
:type message: str
39+
"""
40+
self.message = message
41+
42+
def __str__(self):
43+
return f"[{self.error_code}] " + _("Attestation verification failed: {message}").format(
44+
message=self.message
45+
)
46+
47+
48+
class PackageSubstitutionError(PulpException):
49+
"""
50+
Raised when packages with the same filename but different checksums are being added.
51+
"""
52+
53+
error_code = "PYT0003"
54+
55+
def __init__(self, duplicates):
56+
super().__init__()
57+
"""
58+
:param duplicates: Description of duplicate packages
59+
:type duplicates: str
60+
"""
61+
self.duplicates = duplicates
62+
63+
def __str__(self):
64+
return f"[{self.error_code}] " + _(
65+
"Found duplicate packages being added with the same filename but different "
66+
"checksums. To allow this, set 'allow_package_substitution' to True on the "
67+
"repository. Conflicting packages: {duplicates}"
68+
).format(duplicates=self.duplicates)
69+
70+
71+
class MissingRelativePathError(PulpException):
72+
"""
73+
Raised when relative_path field is missing during package upload.
74+
"""
75+
76+
error_code = "PYT0005"
77+
78+
def __str__(self):
79+
return f"[{self.error_code}] " + _("This field is required: relative_path")
80+
81+
82+
class InvalidPythonExtensionError(PulpException):
83+
"""
84+
Raised when a file has an invalid Python package extension.
85+
"""
86+
87+
error_code = "PYT0006"
88+
89+
def __init__(self, filename):
90+
super().__init__()
91+
"""
92+
:param filename: The filename with invalid extension
93+
:type filename: str
94+
"""
95+
self.filename = filename
96+
97+
def __str__(self):
98+
return f"[{self.error_code}] " + _(
99+
"Extension on {filename} is not a valid python extension "
100+
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
101+
).format(filename=self.filename)
102+
103+
104+
class InvalidProvenanceError(PulpException):
105+
"""
106+
Raised when uploaded provenance data is invalid.
107+
"""
108+
109+
error_code = "PYT0007"
110+
111+
def __init__(self, message):
112+
super().__init__()
113+
"""
114+
:param message: Description of the provenance validation error
115+
:type message: str
116+
"""
117+
self.message = message
118+
119+
def __str__(self):
120+
return f"[{self.error_code}] " + _(
121+
"The uploaded provenance is not valid: {message}"
122+
).format(message=self.message)
123+
124+
125+
class RemoteFetchError(PulpException):
126+
"""
127+
Raised when fetching metadata from all remotes fails.
128+
"""
129+
130+
error_code = "PYT0008"
131+
132+
def __init__(self, url):
133+
super().__init__()
134+
self.url = url
135+
136+
def __str__(self):
137+
return f"[{self.error_code}] " + _("Failed to fetch {url} from any remote.").format(
138+
url=self.url
139+
)
140+
141+
142+
class InvalidAttestationsError(PulpException):
143+
"""
144+
Raised when attestation data cannot be validated.
145+
"""
146+
147+
error_code = "PYT0009"
148+
149+
def __init__(self, message):
150+
super().__init__()
151+
self.message = message
152+
153+
def __str__(self):
154+
return f"[{self.error_code}] " + _("Invalid attestations: {message}").format(
155+
message=self.message
156+
)
157+
158+
159+
class BlocklistedPackageError(PulpException):
160+
"""
161+
Raised when packages matching a blocklist entry are added to a repository.
162+
"""
163+
164+
error_code = "PYT0010"
165+
166+
def __init__(self, blocked):
167+
super().__init__()
168+
self.blocked = blocked
169+
170+
def __str__(self):
171+
return f"[{self.error_code}] " + _(
172+
"Blocklisted packages cannot be added to this repository: {blocked}"
173+
).format(blocked=", ".join(self.blocked))

pulp_python/app/models.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
BEFORE_SAVE,
1313
hook,
1414
)
15-
from rest_framework.serializers import ValidationError
1615

1716
from pulpcore.plugin.models import (
1817
AutoAddObjPermsMixin,
@@ -31,6 +30,7 @@
3130
from pulpcore.plugin.responses import ArtifactResponse
3231
from pulpcore.plugin.util import get_domain, get_domain_pk
3332

33+
from .exceptions import BlocklistedPackageError, PackageSubstitutionError
3434
from .provenance import Provenance
3535
from .utils import (
3636
PYPI_LAST_SERIAL,
@@ -412,17 +412,13 @@ def finalize_new_version(self, new_version):
412412

413413
def _check_for_package_substitution(self, new_version):
414414
"""
415-
Raise a ValidationError if newly added packages would replace existing packages that have
416-
the same filename but a different sha256 checksum.
415+
Raise a PackageSubstitutionError if newly added packages would replace existing packages
416+
that have the same filename but a different sha256 checksum.
417417
"""
418418
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
419419
duplicates = collect_duplicates(qs, ("filename",))
420420
if duplicates:
421-
raise ValidationError(
422-
"Found duplicate packages being added with the same filename but different checksums. " # noqa: E501
423-
"To allow this, set 'allow_package_substitution' to True on the repository. "
424-
f"Conflicting packages: {duplicates}"
425-
)
421+
raise PackageSubstitutionError(duplicates)
426422

427423
def _check_blocklist(self, new_version):
428424
"""
@@ -436,7 +432,7 @@ def _check_blocklist(self, new_version):
436432

437433
def check_blocklist_for_packages(self, packages):
438434
"""
439-
Raise a ValidationError if any of the given packages match a blocklist entry.
435+
Raise a BlocklistedPackageError if any of the given packages match a blocklist entry.
440436
"""
441437
entries = PythonBlocklistEntry.objects.filter(repository=self)
442438
if not entries.exists():
@@ -453,11 +449,7 @@ def check_blocklist_for_packages(self, packages):
453449
blocked.append(pkg.filename)
454450
break
455451
if blocked:
456-
raise ValidationError(
457-
"Blocklisted packages cannot be added to this repository: {}".format(
458-
", ".join(blocked)
459-
)
460-
)
452+
raise BlocklistedPackageError(blocked)
461453

462454

463455
class PythonBlocklistEntry(BaseModel):

pulp_python/app/serializers.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,23 @@
99
from drf_spectacular.utils import extend_schema_serializer
1010
from packaging.requirements import Requirement
1111
from packaging.version import InvalidVersion, Version
12-
from pydantic import TypeAdapter, ValidationError
12+
from pydantic import TypeAdapter
13+
from pydantic import ValidationError as PydanticValidationError
1314
from pypi_attestations import AttestationError
1415
from rest_framework import serializers
1516

1617
from pulpcore.plugin import models as core_models
1718
from pulpcore.plugin import serializers as core_serializers
19+
from pulpcore.plugin.exceptions import DigestValidationError
1820
from pulpcore.plugin.util import get_current_authenticated_user, get_domain, get_prn, reverse
1921

2022
from pulp_python.app import models as python_models
23+
from pulp_python.app.exceptions import (
24+
InvalidProvenanceError,
25+
InvalidPythonExtensionError,
26+
MissingRelativePathError,
27+
ProvenanceVerificationError,
28+
)
2129
from pulp_python.app.provenance import (
2230
AnyPublisher,
2331
Attestation,
@@ -387,7 +395,7 @@ def validate_attestations(self, value):
387395
attestations = TypeAdapter(list[Attestation]).validate_json(value)
388396
else:
389397
attestations = TypeAdapter(list[Attestation]).validate_python(value)
390-
except ValidationError as e:
398+
except PydanticValidationError as e:
391399
raise serializers.ValidationError(_("Invalid attestations: {}").format(e))
392400
return attestations
393401

@@ -421,26 +429,18 @@ def deferred_validate(self, data):
421429
try:
422430
filename = data["relative_path"]
423431
except KeyError:
424-
raise serializers.ValidationError(detail={"relative_path": _("This field is required")})
432+
raise MissingRelativePathError()
425433

426434
artifact = data["artifact"]
427435
try:
428436
_data = artifact_to_python_content_data(filename, artifact, domain=get_domain())
429437
except ValueError:
430-
raise serializers.ValidationError(
431-
_(
432-
"Extension on {} is not a valid python extension "
433-
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
434-
).format(filename)
435-
)
438+
raise InvalidPythonExtensionError(filename)
436439

437440
if data.get("sha256") and data["sha256"] != artifact.sha256:
438-
raise serializers.ValidationError(
439-
detail={
440-
"sha256": _(
441-
"The uploaded artifact's sha256 checksum does not match the one provided"
442-
)
443-
}
441+
raise DigestValidationError(
442+
actual=artifact.sha256,
443+
expected=data["sha256"],
444444
)
445445

446446
data.update(_data)
@@ -654,15 +654,13 @@ def deferred_validate(self, data):
654654
try:
655655
provenance = Provenance.model_validate_json(data["file"].read())
656656
data["provenance"] = provenance.model_dump(mode="json")
657-
except ValidationError as e:
658-
raise serializers.ValidationError(
659-
_("The uploaded provenance is not valid: {}").format(e)
660-
)
657+
except PydanticValidationError as e:
658+
raise InvalidProvenanceError(str(e))
661659
if data.pop("verify"):
662660
try:
663661
verify_provenance(data["package"].filename, data["package"].sha256, provenance)
664662
except AttestationError as e:
665-
raise serializers.ValidationError(_("Provenance verification failed: {}").format(e))
663+
raise ProvenanceVerificationError(str(e))
666664
return data
667665

668666
def retrieve(self, validated_data):

pulp_python/app/tasks/sync.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import asyncio
22
import logging
33
from functools import partial
4-
from gettext import gettext as _
54
from urllib.parse import urljoin
65

76
from aiohttp import ClientError, ClientResponseError
@@ -12,9 +11,9 @@
1211
from packaging.requirements import Requirement
1312
from pypi_attestations import Provenance
1413
from pypi_simple import IndexPage
15-
from rest_framework import serializers
1614

1715
from pulpcore.plugin.download import HttpDownloader
16+
from pulpcore.plugin.exceptions import SyncError
1817
from pulpcore.plugin.models import Artifact, ProgressReport, Remote, Repository
1918
from pulpcore.plugin.stages import (
2019
DeclarativeArtifact,
@@ -52,7 +51,7 @@ def sync(remote_pk, repository_pk, mirror):
5251
repository = Repository.objects.get(pk=repository_pk)
5352

5453
if not remote.url:
55-
raise serializers.ValidationError(detail=_("A remote must have a url attribute to sync."))
54+
raise SyncError("A remote must have a url attribute to sync.")
5655

5756
first_stage = PythonBanderStage(remote)
5857
DeclarativeVersion(first_stage, repository, mirror).create()
@@ -115,7 +114,8 @@ async def run(self):
115114
url = self.remote.url.rstrip("/")
116115
downloader = self.remote.get_downloader(url=url)
117116
if not isinstance(downloader, HttpDownloader):
118-
raise ValueError("Only HTTP(S) is supported for python syncing")
117+
protocol = type(downloader).__name__
118+
raise SyncError(f"Only HTTP(S) is supported for python syncing, got: {protocol}")
119119

120120
async with Master(url, allow_non_https=True) as master:
121121
# Replace the session with the remote's downloader session

pulp_python/app/tasks/upload.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
from django.contrib.sessions.models import Session
55
from django.db import transaction
66
from pydantic import TypeAdapter
7+
from pydantic import ValidationError as PydanticValidationError
8+
from pypi_attestations import AttestationError
79

810
from pulpcore.plugin.models import Artifact, Content, ContentArtifact, CreatedResource
911
from pulpcore.plugin.util import get_current_authenticated_user, get_domain, get_prn
1012

13+
from pulp_python.app.exceptions import AttestationVerificationError, InvalidAttestationsError
1114
from pulp_python.app.models import PackageProvenance, PythonPackageContent, PythonRepository
1215
from pulp_python.app.provenance import (
1316
AnyPublisher,
@@ -123,13 +126,19 @@ def create_provenance(package, attestations, domain):
123126
Returns:
124127
the newly created PackageProvenance
125128
"""
126-
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
129+
try:
130+
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
131+
except PydanticValidationError as e:
132+
raise InvalidAttestationsError(str(e))
127133

128134
user = get_current_authenticated_user()
129135
publisher = AnyPublisher(kind="Pulp User", prn=get_prn(user))
130136
att_bundle = AttestationBundle(publisher=publisher, attestations=attestations)
131137
provenance = Provenance(attestation_bundles=[att_bundle])
132-
verify_provenance(package.filename, package.sha256, provenance)
138+
try:
139+
verify_provenance(package.filename, package.sha256, provenance)
140+
except AttestationError as e:
141+
raise AttestationVerificationError(str(e))
133142
provenance_json = provenance.model_dump(mode="json")
134143

135144
prov_sha256 = PackageProvenance.calculate_sha256(provenance_json)

0 commit comments

Comments
 (0)