diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 11:44:58 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 11:44:58 -0300 |
| commit | a82459630e957a8d94c375394dae66734f3c90c1 (patch) | |
| tree | d9d1b9e1249ccd8cd1d989f532c6368a70dc9cc2 /src/cepis/errors.py | |
| download | python-a82459630e957a8d94c375394dae66734f3c90c1.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'src/cepis/errors.py')
| -rw-r--r-- | src/cepis/errors.py | 91 |
1 files changed, 91 insertions, 0 deletions
diff --git a/src/cepis/errors.py b/src/cepis/errors.py new file mode 100644 index 0000000..1698972 --- /dev/null +++ b/src/cepis/errors.py @@ -0,0 +1,91 @@ +"""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. + """ |