# cepis Python client for the [public CEP API at cep.is](https://cep.is). Official Correios data (DNE), refreshed every two weeks. ```python import cepis address = cepis.lookup("01001-000") print(address.logradouro, address.localidade, address.uf) # Praça da Sé São Paulo SP ``` ## Installation ```sh pip install git+https://git.cep.is/python.git ``` Requires Python 3.9 or newer. ## Usage ### A single lookup `lookup` accepts the CEP masked or unmasked and returns an `Address`: ```python import cepis a = cepis.lookup("01310-100") a.logradouro # 'Avenida Paulista' a.localidade # 'São Paulo' a.uf # 'SP' a.ibge # '3550308' a.cep # '01310100' (always unmasked) a.formatted_cep # '01310-100' ``` Fields the database does not have for that CEP come back as empty strings, never `None`. ### Coordinates About 57% of CEPs have a coordinate. When they do not, `geo` is `None`: ```python a = cepis.lookup("01310100") if a.geo: print(a.geo.lat, a.geo.lon) # -23.567202 -46.649659 print(a.geo.raio_m) # 1084 print(a.geo.precisao) # 'logradouro' ``` **Check `precisao` before computing distance.** There are two kinds of point, and the difference between them is tens of kilometres: | `precisao` | typical radius | what the point is | | --- | --- | --- | | `logradouro` | 110–140 m | the stretch of street — good for maps and distance | | `localidade` | 7–40 km | the municipality centroid: the CEP *is* the whole city | `a.geo.is_precise` is the shorthand for `precisao == "logradouro"`: ```python if a.geo and a.geo.is_precise: cost = shipping_from(a.geo.lat, a.geo.lon) else: cost = shipping_from_municipality(a.ibge) ``` ### Many CEPs at once Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per request: ```python ceps = ["01001000", "99999999", "20040020"] result = cepis.lookup_many(ceps) for requested, address in zip(ceps, result): if address is None: print(f"{requested}: not found") else: print(f"{requested}: {address.localidade}") ``` The returned list has **the same length and the same order** as the input. A CEP that does not exist, or is malformed, becomes `None` at its position — it does not vanish, and it does not raise. Lists longer than 500 are chunked automatically. > **Batches carry no coordinate.** The endpoint behind them is CSV, and the > CSV has no `geo` column. Every `Address` from `lookup_many` has `geo=None`, > including for CEPs that do have one. Need `geo`? Use `lookup`, one at a time. ### API health Useful for two things: checking the API answers, and knowing **which version of the database** is live: ```python h = cepis.health() h.ok # True h.dne # '26071' identifier of the fortnightly database h.ceps # 1596168 h.base_gerada_em # '2026-07-24T03:48:28Z' ``` An API reporting itself degraded does not raise — that arrives in `h.status`. ## Configuration For anything beyond the defaults, build a `Client` and reuse it. It keeps no state between calls and is safe to share across threads: ```python from cepis import Client client = Client( base_url="https://cep.is", # point at another instance timeout=10.0, # seconds per socket operation max_attempts=3, # counting the first; 1 disables retrying backoff_base=0.5, # base of the exponential wait backoff_max=8.0, # cap on each wait total_timeout=None, # cap on the whole call, attempts included ) address = client.lookup("01001000") ``` Retries cover 429, 5xx and network failures, with exponential backoff and jitter; 400 and 404 are never retried. See `help(cepis.Client)` for the details of each parameter. The module-level functions (`cepis.lookup`, etc.) use a shared default client. Do not mutate it: it applies process-wide, including to third-party libraries that also use this module. Need a different configuration? Build your own. ### Pointing at a local instance `base_url` is the only parameter that usually changes between environments. The API repo brings a local instance up with `just up`: ```python client = Client(base_url="http://localhost:8080") ``` From the command line, to confirm quickly that it answers: ```sh python -m cepis --base-url http://localhost:8080 01001000 ``` ## Errors Everything derives from `CepisError`. Below that, the split is by **the decision you have to make**, not by the HTTP status that caused it: | error | when | does retrying help? | | --- | --- | --- | | `InvalidCEP` | not 8 digits | no | | `CEPNotFound` | well-formed, not in the database | no | | `RateLimited` | 429 | yes, later | | `ServerError` | 5xx | yes, later | | `NetworkError` | DNS, connection, timeout, TLS | yes, later | | `InvalidResponse` | unintelligible response | probably not | ```python from cepis import CEPNotFound, CepisError try: address = cepis.lookup(cep_from_form) except CEPNotFound: ... # ask the user for another CEP except CepisError: ... # transient failure: reschedule ``` `InvalidCEP` also derives from `ValueError` and `CEPNotFound` from `KeyError`, so code already handling those types during input validation keeps working without knowing this library exists. `InvalidResponse` almost always means something between you and the API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error page. Useful as a diagnosis: the problem is not in your code. ## Development Everything runs through `make`, which shells out to `uv` — there is nothing to install first and no virtualenv to activate: ```sh make # list the targets make test # run the suite make lint # ruff check + format check, touches nothing make build # wheel + sdist into dist/ make check # lint, then test, then build ``` ## Licence ISC. See [LICENSE](LICENSE).