diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ccea02 --- /dev/null +++ b/README.md @@ -0,0 +1,259 @@ +# cepis + +Fortran client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. + +```fortran +program demo + use cepis + implicit none + + type(cepis_address) :: address + character(len=:), allocatable :: message + integer :: status + + call cepis_lookup('01001-000', address, status, message) + if (status == CEPIS_OK) then + print *, address%logradouro, address%localidade, address%uf + end if +end program demo +``` + +## Installation + +There is no package manager here. Build the static library and link it: + +```sh +git clone https://git.cep.is/fortran.git cepis-fortran +cd cepis-fortran +make build +sudo make install # modules under $PREFIX/include/cepis, libcepis.a under $PREFIX/lib +``` + +```sh +gfortran -I/usr/local/include/cepis demo.f90 -L/usr/local/lib -lcepis -lcurl -o demo +``` + +Requires **Fortran 2018** (gfortran 9 or newer), a C compiler, and **libcurl +with its development headers** — `libcurl4-openssl-dev` on Debian and Ubuntu, +`curl-devel` on Fedora, `curl` on Alpine and OpenBSD. Fortran has no HTTP +client and no sockets, so the transport is libcurl bound through +`iso_c_binding`; the interface blocks are written by hand in +`src/cepis_http.f90` rather than vendoring a third-party Fortran-curl wrapper, +because the twelve entry points this library needs are a fraction of what such +a wrapper brings, and one file with no dependency is easier to audit than one +more dependency. + +## Usage + +`use cepis` brings in everything. Every fallible call returns an `integer` +status and an allocatable `message`; nothing stops the program. + +### A single lookup + +`lookup` accepts the CEP masked or unmasked: + +```fortran +type(cepis_address) :: a +character(len=:), allocatable :: message +integer :: status + +call cepis_lookup('01310100', a, status, message) + +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. +Every character component is allocated after a successful call, so none of them +needs an `allocated` check. + +### Coordinates + +About 57% of CEPs have a coordinate. `has_geo` says whether this one does: + +```fortran +if (a%has_geo) then + print *, a%geo%lat, a%geo%lon ! -23.567202 -46.649659 + print *, a%geo%raio_m ! 1084 + print *, a%geo%precisao ! 'logradouro' +end if +``` + +**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'`: + +```fortran +if (a%has_geo .and. a%geo%is_precise()) then + call shipping_from(a%geo%lat, a%geo%lon) +else + call shipping_from_municipality(a%ibge) +end if +``` + +### Many CEPs at once + +Do not loop over `lookup`. Use `lookup_many`, which resolves up to 500 CEPs per +request: + +```fortran +type(cepis_address), allocatable :: found(:) +character(len=8) :: ceps(3) = ['01001000', '99999999', '20040020'] +integer :: i + +call cepis_lookup_many(ceps, found, status, message) + +do i = 1, size(found) + if (found(i)%found) then + print *, ceps(i), found(i)%localidade + else + print *, ceps(i), 'not found' + end if +end do +``` + +The result has **the same length and the same order** as the input. A CEP that +does not exist, or is malformed, has `found = .false.` at its position — it does +not vanish, and it is not an error. Trailing blanks in each element are ignored, +so a fixed-length array literal works as written. + +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 `has_geo = .false.`, +> 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: + +```fortran +type(cepis_health) :: h + +call cepis_check_health(h, status, message) + +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 is not an error — that arrives in `h%status`. + +### From the command line + +`make build` also produces `build/cepis`: + +```sh +build/cepis 01001000 +build/cepis --base-url http://localhost:8080 01001000 20040020 +build/cepis --health +``` + +## Configuration + +For anything beyond the defaults, build a `cepis_client` and reuse it. It keeps +no state between calls: + +```fortran +type(cepis_client) :: client + +call cepis_client_init(client, status, message, & + base_url='https://cep.is', & ! point at another instance + timeout=10.0d0, & ! seconds per socket operation + max_attempts=3, & ! counting the first; 1 disables retrying + backoff_base=0.5d0, & ! base of the exponential wait + backoff_max=8.0d0, & ! cap on each wait, Retry-After included + total_timeout=0.0d0) ! cap on the whole call; 0 means none + +call client%lookup('01001000', address, status, message) +``` + +Every argument after `message` is optional. Retries cover 429, 5xx and network +failures, with exponential backoff and jitter; 400 and 404 are never retried. + +`lookup`, `lookup_many` and `health` also accept two optional `intent(out)` +arguments, `http_status` and `retry_after`, for callers that want to reschedule +rate-limited work. + +The module-level `cepis_lookup`, `cepis_lookup_many` and `cepis_check_health` +use one shared default client pointed at `https://cep.is`. It is not safe to use +from two threads at once; build your own client per thread if you need that. + +### Pointing at a local instance + +`base_url` is the only setting that usually changes between environments. The +API repo brings a local instance up with `just up`: + +```fortran +call cepis_client_init(client, status, message, base_url='http://localhost:8080') +``` + +## Errors + +`status` is one of these. The split is by **the decision you have to make**, not +by the HTTP status that caused it: + +| status | when | does retrying help? | +| --- | --- | --- | +| `CEPIS_OK` | the call succeeded | — | +| `CEPIS_ERR_INVALID_CEP` | not 8 digits | no | +| `CEPIS_ERR_NOT_FOUND` | well-formed, not in the database | no | +| `CEPIS_ERR_RATE_LIMITED` | 429 | yes, later | +| `CEPIS_ERR_SERVER` | 5xx | yes, later | +| `CEPIS_ERR_NETWORK` | DNS, connection, timeout, TLS | yes, later | +| `CEPIS_ERR_INVALID_RESPONSE` | unintelligible response | probably not | +| `CEPIS_ERR_CONFIG` | a configuration value is out of range | no | + +```fortran +call cepis_lookup(cep_from_form, address, status, message) +select case (status) +case (CEPIS_OK) + call use_it(address) +case (CEPIS_ERR_NOT_FOUND, CEPIS_ERR_INVALID_CEP) + call ask_again() +case default + call reschedule() ! transient failure +end select +``` + +`cepis_status_name(status)` gives a short, stable English name for logging. + +`CEPIS_ERR_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 + +```sh +make # list the targets +make test # build and run the suite against a local HTTP server +make lint # warning-free rebuild + fprettify --diff, touches nothing +make format # apply fprettify (writes) +make build # libcepis.a, the modules and the cepis CLI +make check # lint, then test, then build +make sanitize # rebuild and run the suite under ASan + UBSan +``` + +The suite starts a real HTTP server on a loopback port (`test/http_server.c`, +compiled with the tests) and points `base_url` at it, so nothing is mocked. +`make lint` and `make format` need [fprettify]; `lint` skips the style check +when it is not installed. + +[fprettify]: https://github.com/fortran-lang/fprettify + +## Licence + +ISC. See [LICENSE](LICENSE). |