summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 11:44:58 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 11:44:58 -0300
commita82459630e957a8d94c375394dae66734f3c90c1 (patch)
treed9d1b9e1249ccd8cd1d989f532c6368a70dc9cc2 /README.md
downloadpython-a82459630e957a8d94c375394dae66734f3c90c1.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md206
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).