Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion ciel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
8 changes: 6 additions & 2 deletions ciel/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
arg_version,
)
from ..families import Family
from ..exceptions import (
InvalidPDKError,
MissingCredentialsError,
)


def build(
Expand All @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
17 changes: 11 additions & 6 deletions ciel/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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]):
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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."
)

Expand Down Expand Up @@ -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"]

Expand Down
66 changes: 66 additions & 0 deletions ciel/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion ciel/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down
3 changes: 2 additions & 1 deletion ciel/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import httpx
import ssl
from .__version__ import __version__
from .exceptions import InvalidResponseError


@dataclass
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 14 additions & 11 deletions ciel/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -218,15 +221,15 @@ 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:
f.write(io.read())
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…"
)
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
16 changes: 10 additions & 6 deletions ciel/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@

from .github import GitHubSession, RepoInfo
from .common import Version, date_from_iso8601
from .exceptions import (
NoVersionsFoundError,
InvalidResponseError,
)


@dataclass
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -142,15 +146,15 @@ 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:
raise e from None
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"]:
Expand All @@ -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(
Expand All @@ -177,15 +181,15 @@ 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:
raise e from None
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"]:
Expand Down