Skip to content

Commit 005810f

Browse files
committed
Add more Pulp Exceptions.
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e1e023c commit 005810f

9 files changed

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

pulp_python/app/models.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
BEFORE_SAVE,
1212
hook,
1313
)
14-
from rest_framework.serializers import ValidationError
1514
from pulpcore.plugin.models import (
1615
AutoAddObjPermsMixin,
1716
Content,
@@ -23,6 +22,7 @@
2322
from pulpcore.plugin.responses import ArtifactResponse
2423

2524
from pathlib import PurePath
25+
from .exceptions import PackageSubstitutionError
2626
from .provenance import Provenance
2727
from .utils import (
2828
artifact_to_python_content_data,
@@ -407,14 +407,10 @@ def finalize_new_version(self, new_version):
407407

408408
def _check_for_package_substitution(self, new_version):
409409
"""
410-
Raise a ValidationError if newly added packages would replace existing packages that have
411-
the same filename but a different sha256 checksum.
410+
Raise a PackageSubstitutionError if newly added packages would replace existing packages
411+
that have the same filename but a different sha256 checksum.
412412
"""
413413
qs = PythonPackageContent.objects.filter(pk__in=new_version.content)
414414
duplicates = collect_duplicates(qs, ("filename",))
415415
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-
)
416+
raise PackageSubstitutionError(duplicates)

pulp_python/app/serializers.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@
88
from packaging.requirements import Requirement
99
from rest_framework import serializers
1010
from pypi_attestations import AttestationError
11-
from pydantic import TypeAdapter, ValidationError
11+
from pydantic import TypeAdapter, ValidationError as PydanticValidationError
1212
from urllib.parse import urljoin
1313

14+
from pulpcore.plugin.exceptions import DigestValidationError
1415
from pulpcore.plugin import models as core_models
1516
from pulpcore.plugin import serializers as core_serializers
1617
from pulpcore.plugin.util import get_domain, get_prn, get_current_authenticated_user
1718

1819
from pulp_python.app import models as python_models
20+
from pulp_python.app.exceptions import (
21+
AttestationVerificationError,
22+
InvalidProvenanceError,
23+
InvalidPythonExtensionError,
24+
MissingRelativePathError,
25+
ProvenanceVerificationError,
26+
)
1927
from pulp_python.app.provenance import (
2028
Attestation,
2129
Provenance,
@@ -374,7 +382,7 @@ def validate_attestations(self, value):
374382
attestations = TypeAdapter(list[Attestation]).validate_json(value)
375383
else:
376384
attestations = TypeAdapter(list[Attestation]).validate_python(value)
377-
except ValidationError as e:
385+
except PydanticValidationError as e:
378386
raise serializers.ValidationError(_("Invalid attestations: {}".format(e)))
379387
return attestations
380388

@@ -387,9 +395,7 @@ def handle_attestations(self, filename, sha256, attestations, offline=True):
387395
try:
388396
verify_provenance(filename, sha256, provenance, offline=offline)
389397
except AttestationError as e:
390-
raise serializers.ValidationError(
391-
{"attestations": _("Attestations failed verification: {}".format(e))}
392-
)
398+
raise AttestationVerificationError(str(e))
393399
return provenance.model_dump(mode="json")
394400

395401
def deferred_validate(self, data):
@@ -408,26 +414,18 @@ def deferred_validate(self, data):
408414
try:
409415
filename = data["relative_path"]
410416
except KeyError:
411-
raise serializers.ValidationError(detail={"relative_path": _("This field is required")})
417+
raise MissingRelativePathError()
412418

413419
artifact = data["artifact"]
414420
try:
415421
_data = artifact_to_python_content_data(filename, artifact, domain=get_domain())
416422
except ValueError:
417-
raise serializers.ValidationError(
418-
_(
419-
"Extension on {} is not a valid python extension "
420-
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
421-
).format(filename)
422-
)
423+
raise InvalidPythonExtensionError(filename)
423424

424425
if data.get("sha256") and data["sha256"] != artifact.sha256:
425-
raise serializers.ValidationError(
426-
detail={
427-
"sha256": _(
428-
"The uploaded artifact's sha256 checksum does not match the one provided"
429-
)
430-
}
426+
raise DigestValidationError(
427+
actual=artifact.sha256,
428+
expected=data["sha256"],
431429
)
432430

433431
data.update(_data)
@@ -641,15 +639,13 @@ def deferred_validate(self, data):
641639
try:
642640
provenance = Provenance.model_validate_json(data["file"].read())
643641
data["provenance"] = provenance.model_dump(mode="json")
644-
except ValidationError as e:
645-
raise serializers.ValidationError(
646-
_("The uploaded provenance is not valid: {}".format(e))
647-
)
642+
except PydanticValidationError as e:
643+
raise InvalidProvenanceError(str(e))
648644
if data.pop("verify"):
649645
try:
650646
verify_provenance(data["package"].filename, data["package"].sha256, provenance)
651647
except AttestationError as e:
652-
raise serializers.ValidationError(_("Provenance verification failed: {}".format(e)))
648+
raise ProvenanceVerificationError(str(e))
653649
return data
654650

655651
def retrieve(self, validated_data):

pulp_python/app/tasks/sync.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33

44
from aiohttp import ClientResponseError, ClientError
55
from lxml.etree import LxmlError
6-
from gettext import gettext as _
76
from functools import partial
87

9-
from rest_framework import serializers
10-
8+
from pulpcore.plugin.exceptions import SyncError
119
from pulpcore.plugin.download import HttpDownloader
1210
from pulpcore.plugin.models import Artifact, ProgressReport, Remote, Repository
1311
from pulpcore.plugin.stages import (
@@ -17,6 +15,7 @@
1715
Stage,
1816
)
1917

18+
from pulp_python.app.exceptions import UnsupportedProtocolError
2019
from pulp_python.app.models import (
2120
PythonPackageContent,
2221
PythonRemote,
@@ -54,7 +53,7 @@ def sync(remote_pk, repository_pk, mirror):
5453
repository = Repository.objects.get(pk=repository_pk)
5554

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

5958
first_stage = PythonBanderStage(remote)
6059
DeclarativeVersion(first_stage, repository, mirror).create()
@@ -117,7 +116,8 @@ async def run(self):
117116
url = self.remote.url.rstrip("/")
118117
downloader = self.remote.get_downloader(url=url)
119118
if not isinstance(downloader, HttpDownloader):
120-
raise ValueError("Only HTTP(S) is supported for python syncing")
119+
protocol = type(downloader).__name__
120+
raise UnsupportedProtocolError(protocol)
121121

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

pulp_python/app/tasks/upload.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from datetime import datetime, timezone
44
from django.db import transaction
55
from django.contrib.sessions.models import Session
6-
from pydantic import TypeAdapter
6+
from pydantic import TypeAdapter, ValidationError as PydanticValidationError
7+
from pypi_attestations import AttestationError
78
from pulpcore.plugin.models import Artifact, CreatedResource, Content, ContentArtifact
89
from pulpcore.plugin.util import get_domain, get_current_authenticated_user, get_prn
910

11+
from pulp_python.app.exceptions import AttestationVerificationError, InvalidAttestationsError
1012
from pulp_python.app.models import PythonPackageContent, PythonRepository, PackageProvenance
1113
from pulp_python.app.provenance import (
1214
Attestation,
@@ -122,13 +124,19 @@ def create_provenance(package, attestations, domain):
122124
Returns:
123125
the newly created PackageProvenance
124126
"""
125-
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
127+
try:
128+
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
129+
except PydanticValidationError as e:
130+
raise InvalidAttestationsError(str(e))
126131

127132
user = get_current_authenticated_user()
128133
publisher = AnyPublisher(kind="Pulp User", prn=get_prn(user))
129134
att_bundle = AttestationBundle(publisher=publisher, attestations=attestations)
130135
provenance = Provenance(attestation_bundles=[att_bundle])
131-
verify_provenance(package.filename, package.sha256, provenance)
136+
try:
137+
verify_provenance(package.filename, package.sha256, provenance)
138+
except AttestationError as e:
139+
raise AttestationVerificationError(str(e))
132140
provenance_json = provenance.model_dump(mode="json")
133141

134142
prov_sha256 = PackageProvenance.calculate_sha256(provenance_json)

pulp_python/app/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from pulpcore.plugin.exceptions import TimeoutException
2121
from pulpcore.plugin.util import get_domain
2222

23+
from pulp_python.app.exceptions import RemoteFetchError
24+
2325
log = logging.getLogger(__name__)
2426

2527

@@ -325,7 +327,7 @@ def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -
325327
json_data = json.load(file)
326328
return json_data
327329
else:
328-
raise Exception(f"Failed to fetch {url} from any remote.")
330+
raise RemoteFetchError(url=url)
329331

330332

331333
def python_content_to_json(base_path, content_query, version=None, domain=None):

0 commit comments

Comments
 (0)