"""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 Faixa: """The municipality a CEP *would* belong to, when it does not exist. This is context for an error, **not an address**. It arrives on the 404 body of `GET /{cep}` and is exposed as :attr:`cepis.CEPNotFound.faixa`. The CEP asked for still does not exist; nothing here says otherwise, and treating a `Faixa` as if it were an :class:`Address` is the one mistake this type makes possible. It exists because a failing CEP is rarely random: it is usually someone who knows the city and guessed the number — most often the `-000` that 957 municipalities never received. Knowing which municipality was meant is enough to ask a much better question than "invalid CEP". `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually exist** in that municipality — not the bounds the Correios publish. For Areal/RJ the DNE's own start is `25845000`, which is precisely the CEP that just failed; `cep_ini` is `25845001`. The range is **sparse**. Areal's 118 CEPs are scattered across the ~4,900 numbers between `25845001` and `25849899`. `ceps` is how many there really are, and it is there so the pair is never read as "everything in between is valid": iterating the range would produce mostly CEPs that do not exist. """ localidade: str uf: str estado: str regiao: str ibge: str cep_ini: str cep_fim: str ceps: int @property def label(self) -> str: """The municipality as `Areal/RJ`, for a message shown to a person.""" return f"{self.localidade}/{self.uf}" @classmethod def from_json(cls, block: Any) -> Faixa | None: """Build from the `faixa` block of a 404 body, or return `None`. Takes whatever was under the `faixa` key — including nothing at all — because the field is **omitted, never null**, when the CEP falls in a band no municipality owns. Absent, empty, not a dict, or malformed all give the same answer: `None`. Nothing in here may raise. The caller is already on its way to raising :class:`cepis.CEPNotFound`, and turning that clean "does not exist" into a parse error over an optional courtesy field would be a strictly worse outcome than simply not having the field. """ if not isinstance(block, dict): return None try: localidade = str(block["localidade"]) if not localidade: return None return cls( localidade=localidade, uf=str(block.get("uf", "")), estado=str(block.get("estado", "")), regiao=str(block.get("regiao", "")), ibge=str(block.get("ibge", "")), cep_ini=str(block.get("cep_ini", "")), cep_fim=str(block.get("cep_fim", "")), ceps=int(block.get("ceps") or 0), ) except (KeyError, TypeError, ValueError): return None @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", "")), )