"""The data the API returns, as objects. **Field names are the API's own, in Portuguese** — `logradouro`, `bairro`, `localidade`. Everything else in this library is English; the fields are the one deliberate exception. The reason is fidelity: what you read in the JSON at https://cep.is/01001000 is exactly what you access on the object. Translating to `street`/`city` would mean maintaining a field-by-field mapping and forcing anyone reading the API docs alongside this library to translate back in their head. The values are Portuguese either way — they come from the Correios database. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any @dataclass(frozen=True) class Geo: """The CEP's coordinate, when the database has one. The point is the **median** of the coordinates of that CEP's addresses, not the mean: one badly geocoded address 40 km away shifts a mean and does not move a median. Always check `precisao` before using `lat`/`lon` for distance: - `"logradouro"`: the point is a stretch of street. `raio_m` lands around 100–150 m. Good for maps and for distance. - `"localidade"`: the CEP *is* the whole municipality (a city-wide general CEP), and the point is its centroid. `raio_m` runs from 7 km to 40 km. Computing shipping from this is wrong by tens of kilometres. `raio_m` is the radius containing 90% of that CEP's addresses. It exists precisely so that error cannot pass unnoticed. """ lat: float lon: float raio_m: int precisao: str @property def is_precise(self) -> bool: """`True` when the coordinate points at a street, not a municipality. Shorthand for `precisao == "logradouro"`, which is the check to make before using the point to measure distance. """ return self.precisao == "logradouro" @dataclass(frozen=True) class Address: """A CEP and everything the database knows about it. Fields the database does not have for that CEP come back as empty strings, never `None` — that is what the API returns, and changing it here would create a difference between reading the JSON and reading this library. `geo` is the exception: it is `None` when there is no coordinate. There the difference is real, because the API **omits** the whole field in that case (about 43% of CEPs) rather than sending null. `raw` holds the response exactly as it arrived, uninterpreted, so a field the API grows before this library is updated is not lost. """ cep: str logradouro: str complemento: str unidade: str bairro: str localidade: str uf: str estado: str regiao: str ibge: str gia: str ddd: str siafi: str geo: Geo | None = None raw: dict[str, Any] = field(default_factory=dict, repr=False, compare=False) @property def formatted_cep(self) -> str: """The CEP with its mask: `01001-000`. The native contract always returns it unmasked; formatting is presentation, and it lives here so it does not become a repeated f-string in every project that uses this. """ return f"{self.cep[:5]}-{self.cep[5:]}" @classmethod def from_json(cls, data: dict[str, Any]) -> Address: """Build from the dict the API returned. Unknown fields are ignored during construction but preserved in `raw`. A new field in the API therefore cannot break this library — an outdated client should fail as "I cannot read that yet", never `TypeError`. A malformed `geo` block yields `geo=None` rather than propagating the error: it is not worth losing the whole address over, since the rest of the response is still useful. """ geo = None block = data.get("geo") if isinstance(block, dict): try: geo = Geo( lat=float(block["lat"]), lon=float(block["lon"]), raio_m=int(block["raio_m"]), precisao=str(block["precisao"]), ) except (KeyError, TypeError, ValueError): geo = None return cls( cep=str(data.get("cep", "")), logradouro=str(data.get("logradouro", "")), complemento=str(data.get("complemento", "")), unidade=str(data.get("unidade", "")), bairro=str(data.get("bairro", "")), localidade=str(data.get("localidade", "")), uf=str(data.get("uf", "")), estado=str(data.get("estado", "")), regiao=str(data.get("regiao", "")), ibge=str(data.get("ibge", "")), gia=str(data.get("gia", "")), ddd=str(data.get("ddd", "")), siafi=str(data.get("siafi", "")), geo=geo, raw=dict(data), ) @dataclass(frozen=True) class Health: """The response of `GET /health`. Good for two things: checking that the API answers, and knowing **which version of the database** is live. The DNE database changes every two weeks; `dne` identifies it. """ status: str dne: str ceps: int geo: int base_gerada_em: str @property def ok(self) -> bool: """`True` when the API reports itself healthy.""" return self.status == "ok" @classmethod def from_json(cls, data: dict[str, Any]) -> Health: """Build from the dict the API returned.""" return cls( status=str(data.get("status", "")), dne=str(data.get("dne", "")), ceps=int(data.get("ceps") or 0), geo=int(data.get("geo") or 0), base_gerada_em=str(data.get("base_gerada_em", "")), )