diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 11:44:58 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 11:44:58 -0300 |
| commit | a82459630e957a8d94c375394dae66734f3c90c1 (patch) | |
| tree | d9d1b9e1249ccd8cd1d989f532c6368a70dc9cc2 /README.md | |
| download | python-a82459630e957a8d94c375394dae66734f3c90c1.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..dba6aa2 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# 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). |