diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-27 20:32:01 -0300 |
| commit | b199ae044f8f9cd4b745214cd5e5495f86b5eb54 (patch) | |
| tree | a8e7a134e77381b11091126a8946208e2e6afbcb /src/cepis/client.py | |
| parent | a82459630e957a8d94c375394dae66734f3c90c1 (diff) | |
| download | python-0.2.0.tar.gz | |
A failing CEP is rarely random: it is someone who knows the city and
guessed the number, and the classic guess is the -000 that 957
municipalities never received. The API now answers those 404s with a
faixa block naming the municipality the CEP would have belonged to, so
a caller can ask "did you mean somewhere in Areal/RJ?" instead of
"invalid CEP".
CEPNotFound gains faixa, defaulting to None. It stays a 404 and stays
an exception — the field is context for the error, never a demotion of
it into a partial success. Parsing is deliberately total: anything
missing, empty or malformed under faixa yields None, because turning a
clean not-found into a parse failure over an optional courtesy field
would be strictly worse than not having the field at all.
Faixa's cep_ini/cep_fim are the smallest and largest CEP that actually
exist there, not the bounds the Correios publish, and the range in
between is sparse — hence ceps, so the pair is never read as a list of
valid numbers.
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. |