summaryrefslogtreecommitdiffstats
path: root/src/cepis/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/cepis/client.py')
-rw-r--r--src/cepis/client.py38
1 files changed, 30 insertions, 8 deletions
diff --git a/src/cepis/client.py b/src/cepis/client.py
index bebc6e6..ffd1332 100644
--- a/src/cepis/client.py
+++ b/src/cepis/client.py
@@ -35,7 +35,7 @@ from .errors import (
RateLimited,
ServerError,
)
-from .models import Address, Health
+from .models import Address, Faixa, Health
DEFAULT_URL = "https://cep.is"
@@ -160,6 +160,16 @@ class Client:
API's own message: the format already passed local validation, so the
server knows something this client does not.
+ A 404 may carry the municipality the CEP would have belonged to, in
+ `CEPNotFound.faixa` (see :class:`cepis.Faixa`) — still a 404, still an
+ error, but usually enough to ask a better question than "invalid CEP"::
+
+ try:
+ address = client.lookup(cep)
+ except CEPNotFound as exc:
+ if exc.faixa:
+ print(f"no such CEP in {exc.faixa.label}")
+
:raises InvalidCEP: the CEP is not 8 digits (no request is made).
:raises CEPNotFound: well-formed, but not in the database.
:raises RateLimited: 429 after every attempt.
@@ -170,7 +180,11 @@ class Client:
status, body, _ = self._request(f"/{number}")
if status == 404:
- raise CEPNotFound(f"CEP {number} does not exist in the DNE database")
+ faixa = Faixa.from_json(self._error_json(body).get("faixa"))
+ message = f"CEP {number} does not exist in the DNE database"
+ if faixa is not None:
+ message = f"{message} (it would be in {faixa.label})"
+ raise CEPNotFound(message, faixa=faixa)
if status == 400:
raise InvalidCEP(self._error_message(body, f"CEP {number} rejected"))
@@ -258,15 +272,23 @@ class Client:
return data
@staticmethod
- def _error_message(body: bytes, fallback: str) -> str:
- """Pull `mensagem` out of an error body, falling back to a default."""
+ def _error_json(body: bytes) -> dict[str, Any]:
+ """Decode an error body, or return `{}`.
+
+ Unlike :meth:`_json` this never raises. An error response is already
+ the bad news; whether its body could be parsed only decides how much
+ detail the exception carries, never whether one is raised.
+ """
try:
data = json.loads(body.decode("utf-8"))
- if isinstance(data, dict) and data.get("mensagem"):
- return str(data["mensagem"])
except (ValueError, UnicodeDecodeError):
- pass
- return fallback
+ return {}
+ return data if isinstance(data, dict) else {}
+
+ def _error_message(self, body: bytes, fallback: str) -> str:
+ """Pull `mensagem` out of an error body, falling back to a default."""
+ message = self._error_json(body).get("mensagem")
+ return str(message) if message else fallback
def _backoff(self, attempt: int, retry_after: float | None) -> float:
"""How long to wait before the next attempt.