1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
"""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", "")),
)
|