summaryrefslogtreecommitdiffstats
path: root/src/cepis/errors.py
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 11:44:58 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 11:44:58 -0300
commita82459630e957a8d94c375394dae66734f3c90c1 (patch)
treed9d1b9e1249ccd8cd1d989f532c6368a70dc9cc2 /src/cepis/errors.py
downloadpython-a82459630e957a8d94c375394dae66734f3c90c1.tar.gz
Initial importv0.1.0
Diffstat (limited to 'src/cepis/errors.py')
-rw-r--r--src/cepis/errors.py91
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.
+ """