Skip to content

Commit c6b507c

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

9 files changed

Lines changed: 103 additions & 18 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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from gettext import gettext as _
2+
3+
from pulpcore.plugin.exceptions import PulpException
4+
5+
6+
class AttestationVerificationError(PulpException):
7+
"""
8+
Raised when attestation verification fails.
9+
"""
10+
11+
error_code = "PYT0002"
12+
13+
def __init__(self, message):
14+
super().__init__()
15+
self.message = message
16+
17+
def __str__(self):
18+
return f"[{self.error_code}] " + _("Attestation verification failed: {message}").format(
19+
message=self.message
20+
)
21+
22+
23+
class UnsupportedProtocolError(PulpException):
24+
"""
25+
Raised when an unsupported protocol is used for syncing.
26+
"""
27+
28+
error_code = "PYT0004"
29+
30+
def __init__(self, protocol):
31+
super().__init__()
32+
self.protocol = protocol
33+
34+
def __str__(self):
35+
return f"[{self.error_code}] " + _(
36+
"Only HTTP(S) is supported for python syncing, got: {protocol}"
37+
).format(protocol=self.protocol)
38+
39+
40+
class RemoteFetchError(PulpException):
41+
"""
42+
Raised when fetching metadata from all remotes fails.
43+
"""
44+
45+
error_code = "PYT0008"
46+
47+
def __init__(self, url):
48+
super().__init__()
49+
self.url = url
50+
51+
def __str__(self):
52+
return f"[{self.error_code}] " + _("Failed to fetch {url} from any remote.").format(
53+
url=self.url
54+
)
55+
56+
57+
class InvalidAttestationsError(PulpException):
58+
"""
59+
Raised when attestation data cannot be validated.
60+
"""
61+
62+
error_code = "PYT0009"
63+
64+
def __init__(self, message):
65+
super().__init__()
66+
self.message = message
67+
68+
def __str__(self):
69+
return f"[{self.error_code}] " + _("Invalid attestations: {message}").format(
70+
message=self.message
71+
)

pulp_python/app/models.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,16 @@ 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:
421421
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}"
422+
"Found duplicate packages being added with the same filename but different "
423+
"checksums. To allow this, set 'allow_package_substitution' to True on the "
424+
f"repository. Conflicting packages: {duplicates}"
425425
)
426426

427427
def _check_blocklist(self, new_version):
@@ -436,7 +436,7 @@ def _check_blocklist(self, new_version):
436436

437437
def check_blocklist_for_packages(self, packages):
438438
"""
439-
Raise a ValidationError if any of the given packages match a blocklist entry.
439+
Raise a BlocklistedPackageError if any of the given packages match a blocklist entry.
440440
"""
441441
entries = PythonBlocklistEntry.objects.filter(repository=self)
442442
if not entries.exists():

pulp_python/app/serializers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
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

@@ -387,7 +388,7 @@ def validate_attestations(self, value):
387388
attestations = TypeAdapter(list[Attestation]).validate_json(value)
388389
else:
389390
attestations = TypeAdapter(list[Attestation]).validate_python(value)
390-
except ValidationError as e:
391+
except PydanticValidationError as e:
391392
raise serializers.ValidationError(_("Invalid attestations: {}").format(e))
392393
return attestations
393394

@@ -654,7 +655,7 @@ def deferred_validate(self, data):
654655
try:
655656
provenance = Provenance.model_validate_json(data["file"].read())
656657
data["provenance"] = provenance.model_dump(mode="json")
657-
except ValidationError as e:
658+
except PydanticValidationError as e:
658659
raise serializers.ValidationError(
659660
_("The uploaded provenance is not valid: {}").format(e)
660661
)

pulp_python/app/tasks/sync.py

Lines changed: 5 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,
@@ -23,6 +22,7 @@
2322
Stage,
2423
)
2524

25+
from pulp_python.app.exceptions import UnsupportedProtocolError
2626
from pulp_python.app.models import (
2727
PackageProvenance,
2828
PythonPackageContent,
@@ -52,7 +52,7 @@ def sync(remote_pk, repository_pk, mirror):
5252
repository = Repository.objects.get(pk=repository_pk)
5353

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

5757
first_stage = PythonBanderStage(remote)
5858
DeclarativeVersion(first_stage, repository, mirror).create()
@@ -115,7 +115,8 @@ async def run(self):
115115
url = self.remote.url.rstrip("/")
116116
downloader = self.remote.get_downloader(url=url)
117117
if not isinstance(downloader, HttpDownloader):
118-
raise ValueError("Only HTTP(S) is supported for python syncing")
118+
protocol = type(downloader).__name__
119+
raise UnsupportedProtocolError(protocol)
119120

120121
async with Master(url, allow_non_https=True) as master:
121122
# 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)

pulp_python/app/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from pulpcore.plugin.models import Artifact, Remote
2323
from pulpcore.plugin.util import get_domain
2424

25+
from pulp_python.app.exceptions import RemoteFetchError
26+
2527
log = logging.getLogger(__name__)
2628

2729

@@ -356,7 +358,7 @@ def fetch_json_release_metadata(name: str, version: str, remotes: set[Remote]) -
356358
json_data = json.load(file)
357359
return json_data
358360
else:
359-
raise Exception(f"Failed to fetch {url} from any remote.")
361+
raise RemoteFetchError(url=url)
360362

361363

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

pulp_python/tests/functional/api/test_attestations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_verify_provenance(python_bindings, twine_package, python_content_factor
6969
with pytest.raises(PulpTaskError) as e:
7070
monitor_task(provenance.task)
7171
assert e.value.task.state == "failed"
72-
assert "twine-6.2.0-py3-none-any.whl != twine-6.2.0.tar.gz" in e.value.task.error["description"]
72+
assert "Provenance verification failed" in e.value.task.error["description"]
7373

7474
# Test creating a provenance without verifying
7575
provenance = python_bindings.ContentProvenanceApi.create(

pulp_python/tests/functional/api/test_crud_content_unit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def test_content_crud(
113113
with pytest.raises(PulpTaskError) as e:
114114
response = python_bindings.ContentPackagesApi.create(**content_body)
115115
monitor_task(response.task)
116-
msg = "The uploaded artifact's sha256 checksum does not match the one provided"
116+
msg = "[PLP0003]"
117117
assert msg in e.value.task.error["description"]
118118

119119

0 commit comments

Comments
 (0)