diff options
Diffstat (limited to 'src/cepis/client.py')
| -rw-r--r-- | src/cepis/client.py | 38 |
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. |