diff --git a/ciel/__init__.py b/ciel/__init__.py index 22c4904..f702da2 100644 --- a/ciel/__init__.py +++ b/ciel/__init__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. from .manage import ( - VersionNotFound, enable, get, fetch, @@ -27,3 +26,16 @@ ) from .build import build from .__version__ import __version__ +from .exceptions import ( + InvalidPDKError, + UnknownLibraryError, + VersionNotFoundError, + VersionNotFound, # backward-compatible alias + VersionNotInstalledError, + DownloadError, + UnpackError, + NoVersionsFoundError, + InvalidResponseError, + ToolMetadataError, + MissingCredentialsError, +) diff --git a/ciel/build/__init__.py b/ciel/build/__init__.py index 18918c2..6f5b7be 100644 --- a/ciel/build/__init__.py +++ b/ciel/build/__init__.py @@ -46,6 +46,10 @@ arg_version, ) from ..families import Family +from ..exceptions import ( + InvalidPDKError, + MissingCredentialsError, +) def build( @@ -65,7 +69,7 @@ def build( use_repos[name] = os.path.abspath(path) if pdk_family not in Family.by_name: - raise Exception(f"Unsupported PDK family '{pdk_family}'.") + raise InvalidPDKError(f"Unsupported PDK family '{pdk_family}'.") kwargs = { "pdk_root": pdk_root, @@ -132,7 +136,7 @@ def push( session = GitHubSession() if session.github_token is None: - raise TypeError("No GitHub token was provided.") + raise MissingCredentialsError("No GitHub token was provided.") console = Console() diff --git a/ciel/common.py b/ciel/common.py index 8cd6703..065748e 100644 --- a/ciel/common.py +++ b/ciel/common.py @@ -24,6 +24,11 @@ from typing import Optional, List from .families import Family +from .exceptions import ( + InvalidPDKError, + VersionNotInstalledError, + ToolMetadataError, +) # -- Assorted Helper Functions ISO8601_FMT = "%Y-%m-%dT%H:%M:%SZ" @@ -86,7 +91,7 @@ def resolve_pdk_family(selector: Optional[str]): Starting Ciel 3.0.0, supplying None will no longer work and the selector will be a string. - If the selector is invalid, a ValueError will be raised. "ihp_sg13g2" + If the selector is invalid, an InvalidPDKError will be raised. "ihp_sg13g2" will resolve to "ihp-sg13g2" however for some semblance of backwards compatibility with previous versions of Ciel/Volare. """ @@ -111,7 +116,7 @@ def resolve_pdk_family(selector: Optional[str]): if selector in pdk_family.variants: return pdk_family.name - raise ValueError(f"'{selector}' is not a valid PDK family or variant.") + raise InvalidPDKError(f"'{selector}' is not a valid PDK family or variant.") def resolve_pdk_variant(selector: Optional[str]): @@ -124,7 +129,7 @@ def resolve_pdk_variant(selector: Optional[str]): If selector is None, the PDK environment variables is used as a fallback. If all are None, the function will simply return None. - If the selector is invalid, a ValueError will be raised. + If the selector is invalid, an InvalidPDKError will be raised. """ selector = selector or os.getenv("PDK") if selector is None: @@ -137,7 +142,7 @@ def resolve_pdk_variant(selector: Optional[str]): if selector in pdk_family.variants: return selector - raise ValueError(f"'{selector}' is not a valid PDK family or variant.") + raise InvalidPDKError(f"'{selector}' is not a valid PDK family or variant.") @dataclass @@ -181,7 +186,7 @@ def unset_current(self, pdk_root: str): def uninstall(self, pdk_root: str): if not self.is_installed(pdk_root): - raise ValueError( + raise VersionNotInstalledError( f"Version {self.name} of the {self.pdk} PDK is not installed." ) @@ -253,7 +258,7 @@ def resolve_version( open_pdks_list = [tool for tool in tool_metadata if tool["name"] == "open_pdks"] if len(open_pdks_list) < 1: - raise ValueError("No entry for open_pdks found in tool_metadata.yml") + raise ToolMetadataError("No entry for open_pdks found in tool_metadata.yml") version = open_pdks_list[0]["commit"] diff --git a/ciel/exceptions.py b/ciel/exceptions.py new file mode 100644 index 0000000..e578de6 --- /dev/null +++ b/ciel/exceptions.py @@ -0,0 +1,66 @@ +# Copyright 2026 The American University in Cairo +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Ciel-specific exception classes. + +All exceptions inherit from their appropriate standard-library base class so +that existing code that catches ``ValueError``, ``RuntimeError``, etc. continues +to work without modification. New callers can catch the ciel-specific +sub-classes for finer-grained error handling. +""" + + +class InvalidPDKError(ValueError): + """Raised when a PDK family name or variant selector is not recognised.""" + + +class UnknownLibraryError(ValueError): + """Raised when a library name is not part of a PDK family.""" + + +class VersionNotFoundError(RuntimeError): + """Raised when a requested PDK version cannot be located (locally or remotely).""" + + +class VersionNotInstalledError(ValueError): + """Raised when an operation requires a version to be installed but it is not.""" + + +class DownloadError(RuntimeError): + """Raised when a remote download fails with an unexpected HTTP error.""" + + +class UnpackError(IOError): + """Raised when extracting a downloaded tarball fails.""" + + +class NoVersionsFoundError(ValueError): + """Raised when a data source returns no versions for the requested PDK.""" + + +class InvalidResponseError(ValueError): + """Raised when a remote server returns an unexpected or malformed response.""" + + +class ToolMetadataError(ValueError): + """Raised when ``tool_metadata.yml`` is missing a required entry.""" + + +class MissingCredentialsError(TypeError): + """Raised when a required credential (e.g. a GitHub token) was not supplied.""" + + +# Backward-compatible alias – previously defined in manage.py and exported +# from the top-level package. New code should use VersionNotFoundError. +VersionNotFound = VersionNotFoundError diff --git a/ciel/families.py b/ciel/families.py index e9b825e..f518f63 100644 --- a/ciel/families.py +++ b/ciel/families.py @@ -14,6 +14,8 @@ from dataclasses import dataclass from typing import Iterable, List, Dict, Optional, Set, ClassVar +from .exceptions import UnknownLibraryError + from .github import RepoInfo, opdks_repo, ihp_repo @@ -51,7 +53,7 @@ def resolve_libraries( elif element in self.all_libraries: final_set.add(element) else: - raise ValueError(f"Unknown library {element} for PDK {self.name}") + raise UnknownLibraryError(f"Unknown library {element} for PDK {self.name}") return final_set diff --git a/ciel/github.py b/ciel/github.py index 0ce8ada..fd488f1 100644 --- a/ciel/github.py +++ b/ciel/github.py @@ -26,6 +26,7 @@ import httpx import ssl from .__version__ import __version__ +from .exceptions import InvalidResponseError @dataclass @@ -144,7 +145,7 @@ def api( try: return req.json() except ValueError as e: - raise ValueError(f"Request {req.url} returned invalid JSON: {e}") from None + raise InvalidResponseError(f"Request {req.url} returned invalid JSON: {e}") from None @classmethod def get_user_agent(Self) -> str: diff --git a/ciel/manage.py b/ciel/manage.py index aa3f124..101bf44 100644 --- a/ciel/manage.py +++ b/ciel/manage.py @@ -40,10 +40,13 @@ from .build import build, push from .families import Family from .source import DataSource - - -class VersionNotFound(Exception): - pass +from .exceptions import ( + InvalidPDKError, + UnknownLibraryError, + VersionNotFoundError, + DownloadError, + UnpackError, +) def print_installed_list( @@ -140,7 +143,7 @@ def fetch( pdk_family = Family.by_name.get(pdk) if pdk_family is None: - raise ValueError(f"Unsupported PDK family '{pdk}'.") + raise InvalidPDKError(f"Unsupported PDK family '{pdk}'.") library_set = pdk_family.resolve_libraries(include_libraries) @@ -154,7 +157,7 @@ def fetch( for library in library_set: if library not in pdk_family.all_libraries: - raise RuntimeError(f"Unknown library {library}.") + raise UnknownLibraryError(f"Unknown library {library}.") found = False for variant in variants: lib_path = os.path.join(version_directory, variant, "libs.ref", library) @@ -218,7 +221,7 @@ def fetch( mkdirp(final_dir) io = tf.extractfile(file) if io is None: - raise IOError( + raise UnpackError( f"Failed to unpack file in {asset.filename}'s tarball: {file.name}." ) with open(final_path, "wb") as f: @@ -226,7 +229,7 @@ def fetch( except httpx.HTTPStatusError as e: if e.response is not None and e.response.status_code == 404: if not build_if_not_found: - raise RuntimeError(f"Version {version} not found remotely.") + raise VersionNotFoundError(f"Version {version} not found remotely.") console.print( f"Version {version} not found remotely, attempting to build…" ) @@ -249,11 +252,11 @@ def fetch( ) else: if e.response is not None: - raise RuntimeError( + raise DownloadError( f"Failed to obtain {version} remotely: {e.response}." ) else: - raise RuntimeError(f"Failed to request {version} from server: {e}.") + raise DownloadError(f"Failed to request {version} from server: {e}.") except KeyboardInterrupt as e: console.print("Interrupted.") for path in affected_paths: @@ -305,7 +308,7 @@ def enable( pdk_family = Family.by_name.get(pdk) if pdk_family is None: - raise ValueError(f"Unsupported PDK family '{pdk}'.") + raise InvalidPDKError(f"Unsupported PDK family '{pdk}'.") variants = pdk_family.variants version_paths = [os.path.join(version_directory, variant) for variant in variants] diff --git a/ciel/source.py b/ciel/source.py index f208aab..31de5f3 100644 --- a/ciel/source.py +++ b/ciel/source.py @@ -25,6 +25,10 @@ from .github import GitHubSession, RepoInfo from .common import Version, date_from_iso8601 +from .exceptions import ( + NoVersionsFoundError, + InvalidResponseError, +) @dataclass @@ -103,7 +107,7 @@ def get_available_versions(self, pdk: str) -> List[Version]: versions.sort(reverse=True) if len(versions) == 0: - raise ValueError( + raise NoVersionsFoundError( f"No versions found for '{pdk}' on github.com/{self.repo.id}" ) return versions @@ -142,7 +146,7 @@ def get_available_versions(self, pdk: str) -> List[Version]: req.raise_for_status() except httpx.HTTPStatusError as e: if e.response.status_code == 404: - raise ValueError( + raise NoVersionsFoundError( f"No versions found for '{pdk}' at '{self.base_url}'" ) from None else: @@ -150,7 +154,7 @@ def get_available_versions(self, pdk: str) -> List[Version]: try: manifest = req.json() except ValueError as e: - raise ValueError(f"Request {req.url} returned invalid JSON: {e}") from None + raise InvalidResponseError(f"Request {req.url} returned invalid JSON: {e}") from None versions = [] for version in manifest["versions"]: @@ -164,7 +168,7 @@ def get_available_versions(self, pdk: str) -> List[Version]: versions.sort(reverse=True) if len(versions) == 0: - raise ValueError(f"No versions found for '{pdk}' on '{self.base_url}'") + raise NoVersionsFoundError(f"No versions found for '{pdk}' on '{self.base_url}'") return versions def get_downloads_for_version( @@ -177,7 +181,7 @@ def get_downloads_for_version( req.raise_for_status() except httpx.HTTPStatusError as e: if e.response.status_code == 404: - raise ValueError( + raise NoVersionsFoundError( f"Manifest for '{version.pdk}/{version.name}' at '{self.base_url}'" ) from None else: @@ -185,7 +189,7 @@ def get_downloads_for_version( try: manifest = req.json() except ValueError as e: - raise ValueError(f"Request {req.url} returned invalid JSON: {e}") from None + raise InvalidResponseError(f"Request {req.url} returned invalid JSON: {e}") from None assets = [] for asset in manifest["assets"]: