diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:28 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:28 -0300 |
| commit | ae9ad8a4c61b793f6859c84dac80b1c1b04b7b68 (patch) | |
| tree | 62271e7672dfc58f59dda2ea4dc539c89a1ea5ca /README.md | |
| download | lisp-ae9ad8a4c61b793f6859c84dac80b1c1b04b7b68.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..4baa530 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# cepis + +Common Lisp client for the [public CEP API at cep.is](https://cep.is). +Official Correios data (DNE), refreshed every two weeks. Developed and tested +on SBCL. + +```lisp +(let ((address (cepis:lookup "01001-000"))) + (format t "~a ~a ~a~%" + (cepis:address-logradouro address) + (cepis:address-localidade address) + (cepis:address-uf address))) +;; Praça da Sé São Paulo SP +``` + +## Installation + +Clone it where ASDF can find it, for instance `~/common-lisp/`, then load it +with Quicklisp so the dependencies come along: + +```sh +git clone https://git.cep.is/lisp.git ~/common-lisp/cepis +``` + +```lisp +(ql:quickload "cepis") +``` + +Three dependencies: +[`dexador`](https://github.com/fukamachi/dexador) for HTTP, because the +standard has no client and dexador is the maintained, synchronous, TLS-capable +one; [`com.inuoe.jzon`](https://github.com/Zulu-Inuoe/jzon) for JSON, because +it is the strictest of the current parsers and does not invent its own reader +syntax; and [`babel`](https://github.com/cl-babel/babel) to decode UTF-8 +bodies. The batch endpoint's CSV is read by a small RFC 4180 reader included +here — less total weight than pulling a CSV library in for one call. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns an `address`: + +```lisp +(defvar *a* (cepis:lookup "01310-100")) + +(cepis:address-logradouro *a*) ; "Avenida Paulista" +(cepis:address-localidade *a*) ; "São Paulo" +(cepis:address-uf *a*) ; "SP" +(cepis:address-ibge *a*) ; "3550308" +(cepis:address-cep *a*) ; "01310100" (always unmasked) +(cepis:address-formatted-cep *a*) ; "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never `nil`. The slot names are the API's own, in Portuguese, with Lisp's +hyphenation where the JSON has an underscore: `raio_m` is read with +`geo-raio-m`. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `address-geo` is `nil`: + +```lisp +(let ((geo (cepis:address-geo *a*))) + (when geo + (list (cepis:geo-lat geo) ; -23.567202d0 + (cepis:geo-lon geo) ; -46.649659d0 + (cepis:geo-raio-m geo) ; 1084 + (cepis:geo-precisao geo)))) ; "logradouro" +``` + +**Check `geo-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 | + +`geo-precise-p` is the shorthand for `precisao` being `"logradouro"`: + +```lisp +(let ((geo (cepis:address-geo *a*))) + (if (and geo (cepis:geo-precise-p geo)) + (shipping-from (cepis:geo-lat geo) (cepis:geo-lon geo)) + (shipping-from-municipality (cepis:address-ibge *a*)))) +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookup-many`, which resolves up to 500 CEPs +per request: + +```lisp +(let ((ceps '("01001000" "99999999" "20040020"))) + (loop for cep in ceps + for address in (cepis:lookup-many ceps) + do (format t "~a: ~a~%" + cep + (if address (cepis:address-localidade address) "not found")))) +``` + +The returned list has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, is `nil` at its position — it does +not vanish, and it does not signal. + +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 +> `address-geo` of `nil`, including for CEPs that do have one. Need the +> coordinate? 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: + +```lisp +(let ((h (cepis:health))) + (cepis:health-ok-p h) ; T + (cepis:health-dne h) ; "26071" identifier of the fortnightly database + (cepis:health-ceps h) ; 1596168 + (cepis:health-base-gerada-em h)); "2026-07-24T03:48:28Z" +``` + +An API reporting itself degraded does not signal — that arrives in +`health-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: + +```lisp +(defvar *cep* + (cepis:make-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 nil)) ; cap on the whole call, attempts included + +(cepis:lookup "01001000" :client *cep*) +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. `timeout` bounds each socket operation, +not the whole call — that is what `total-timeout` is for. See the docstring of +`make-client` for the details of each parameter. + +`lookup`, `lookup-many` and `health` use the shared client in `cepis:*client*` +when no `:client` is given. Do not mutate it: it applies process-wide, +including to third-party libraries that also use this package. 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`: + +```lisp +(cepis:make-client :base-url "http://localhost:8080") +``` + +## Errors + +Everything derives from `cepis-error`, so one handler catches any failure. +Below that, the split is by **the decision you have to make**, not by the HTTP +status that caused it: + +| condition | when | does retrying help? | +| --- | --- | --- | +| `invalid-cep` | not 8 digits | no | +| `cep-not-found` | well-formed, not in the database | no | +| `rate-limited` | 429 | yes, later | +| `server-error` | 5xx | yes, later | +| `network-error` | DNS, connection, timeout, TLS | yes, later | +| `invalid-response` | unintelligible response | probably not | + +```lisp +(handler-case (cepis:lookup cep-from-form) + (cepis:cep-not-found () + :ask-for-another-cep) + (cepis:cepis-error () + :reschedule)) +``` + +`error-message` returns the description of any of them; `server-error-status` +and `rate-limited-retry-after` carry the extra facts their callers need. + +`invalid-cep` is also a `parse-error` and `cep-not-found` a `cell-error`, so +code already handling those standard types during input validation keeps +working without knowing this library exists — and `cell-error-name` gives back +the CEP that was not found. + +`invalid-response` 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 drives SBCL in script mode. Quicklisp +must be installed and loaded from your `~/.sbclrc`: + +```sh +make # list the targets +make test # run the suite +make lint # recompile from scratch, failing on any warning +make build # compile and load the library +make check # lint, then test, then build +``` + +The suite stands up a real HTTP server on a loopback port and points a client +at it with `:base-url`. Nothing is stubbed and no test touches the network. + +## Licence + +ISC. See [LICENSE](LICENSE). |