"""Exception hierarchy. Everything derives from :class:`CepisError`, so callers who only care that *something* went wrong can catch one type. Below that, the split is by **the decision the caller has to make**, not by the HTTP status that caused it: - :class:`InvalidCEP` and :class:`CEPNotFound` are problems with the *request*. Retrying is pointless — the same CEP will fail again. - :class:`NetworkError`, :class:`ServerError` and :class:`RateLimited` are problems with *transport*. Retrying may help, and the client already retried on its own before raising any of them. That split is why `CEPNotFound` is not "the HTTP 404 exception": the client also raises it without making any request at all, when the CEP is malformed before it ever leaves the machine. """ from __future__ import annotations class CepisError(Exception): """Base of every error. Catch this to handle any failure.""" class InvalidCEP(CepisError, ValueError): """The CEP is not 8 digits. Deliberately also a :class:`ValueError`: this is an argument error, and code that already handles `ValueError` while validating input keeps working without knowing this library exists. Usually raised **without** a request ever being made — the client checks the format before going to the network. """ class CEPNotFound(CepisError, KeyError): """The CEP is well-formed but does not exist in the DNE database. Also a :class:`KeyError`, by the same reasoning as `InvalidCEP`: looking a CEP up is conceptually a key lookup. `KeyError.__str__` wraps the message in quotes (inherited from dict behaviour), which is why `__str__` is overridden here — otherwise every message would come out with spurious quotes around it. """ def __str__(self) -> str: return self.args[0] if self.args else "" class NetworkError(CepisError): """The request never completed: DNS, refused connection, timeout, TLS. Raised only after every configured attempt has failed. """ class ServerError(CepisError): """The API answered 5xx on every attempt. `status` holds the HTTP status seen on the last attempt. """ def __init__(self, message: str, status: int) -> None: super().__init__(message) self.status = status class RateLimited(CepisError): """The API (or the edge in front of it) answered 429. `retry_after` is what the server asked for, in seconds, or `None` if it did not say. The client already honoured that value across the attempts it made; it is exposed here for callers who want to reschedule the work. """ def __init__(self, message: str, retry_after: float | None = None) -> None: super().__init__(message) self.retry_after = retry_after class InvalidResponse(CepisError): """A success status came back with a body that cannot be parsed. In practice this means something between you and the API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error page. Useful as a diagnosis: the problem is not in your code. """