diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..64baf53 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# cepis + +R client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```r +library(cepis) + +address <- cep_lookup("01001-000") +cat(address$logradouro, address$localidade, address$uf) +#> Praça da Sé São Paulo SP +``` + +## Installation + +```r +remotes::install_git("https://git.cep.is/r.git") +``` + +Requires R 4.0 or newer, plus two packages: **curl**, because base R cannot +report a response status code and this client has to tell a 404 from a 400 +from a 5xx, and **jsonlite** to read the JSON. Both are the ecosystem +standard and neither pulls a stack behind it. CSV is parsed with +`utils::read.csv()`. + +## Usage + +### A single lookup + +`cep_lookup()` accepts the CEP masked or unmasked and returns a +`cepis_address`, a plain list: + +```r +a <- cep_lookup("01310-100") + +a$logradouro # "Avenida Paulista" +a$localidade # "São Paulo" +a$uf # "SP" +a$ibge # "3550308" +a$cep # "01310100" (always unmasked) +formatted_cep(a) # "01310-100" +``` + +Fields the database does not have for that CEP come back as empty strings, +never `NA`. `a$raw` holds the response as it arrived, including any field the +API grew after this release. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `NULL`: + +```r +a <- cep_lookup("01310100") + +if (!is.null(a$geo)) { + a$geo$lat # -23.567202 + a$geo$lon # -46.649659 + a$geo$raio_m # 1084 + 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"`: + +```r +if (!is.null(a$geo) && 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 `cep_lookup()`. Use `cep_lookup_many()`, which resolves up +to 500 CEPs per request: + +```r +ceps <- c("01001000", "99999999", "20040020") +result <- cep_lookup_many(ceps) + +for (i in seq_along(ceps)) { + if (is.null(result[[i]])) { + cat(ceps[i], "not found\n") + } else { + cat(ceps[i], result[[i]]$localidade, "\n") + } +} +``` + +The returned list has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, is `NULL` at its position — it does +not vanish, and it does not raise. A list rather than a `data.frame` because +`NULL` is the only unambiguous "no such CEP" in R: a row of `NA`s reads as an +address with unknown fields, which is a different statement. + +Vectors 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 `cep_lookup_many()` has +> `geo = NULL`, including for CEPs that do have one. Need `geo`? Use +> `cep_lookup()`, one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version +of the database** is live: + +```r +h <- cep_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 with `cepis_client()` and +pass it along. It holds configuration only, keeps no state between calls and +is safe to reuse: + +```r +client <- cepis_client( + base_url = "https://cep.is", # point at another instance + timeout = 10, # seconds for one attempt + max_attempts = 3L, # counting the first; 1 disables retrying + backoff_base = 0.5, # base of the exponential wait + backoff_max = 8, # cap on each wait + total_timeout = NULL # cap on the whole call, attempts included +) + +address <- cep_lookup("01001000", client) +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. See `?cepis_client` for the details of +each parameter. + +Omitting `client` uses a shared default one, built with the defaults above. + +### 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`: + +```r +client <- cepis_client(base_url = "http://localhost:8080") +``` + +## Errors + +Failures are classed conditions. Everything inherits `cepis_error` (and +`error`), so one handler catches all of them. Below that, the split is by +**the decision you have to make**, not by the HTTP status that caused it: + +| class | when | does retrying help? | +| --- | --- | --- | +| `cepis_invalid_cep` | not 8 digits | no | +| `cepis_not_found` | well-formed, not in the database | no | +| `cepis_rate_limited` | 429; carries `retry_after` | yes, later | +| `cepis_server_error` | 5xx; carries `status` | yes, later | +| `cepis_network_error` | DNS, connection, timeout, TLS | yes, later | +| `cepis_invalid_response` | unintelligible response | probably not | + +```r +tryCatch( + cep_lookup(cep_from_form), + cepis_not_found = function(e) NULL, # ask the user for another CEP + cepis_error = function(e) reschedule(e) # transient failure: retry later +) +``` + +`cepis_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`: + +```sh +make # list the targets +make test # install into check/lib and run the suite +make lint # parse + codetools, touches nothing +make document # regenerate man/ and NAMESPACE with roxygen2 +make build # build the source tarball +make check # lint, then test, then build +make check-cran # build, then R CMD check the tarball +``` + +The suite stands up a real HTTP server on loopback and points `base_url` at +it, so no test touches the network. + +## Licence + +ISC. See [LICENSE](LICENSE). |