# 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. ### Platform support and the calling convention Built and tested on **Linux x86-64 with gfortran**. Other platforms are plausible but unverified — including other compilers, `flang` and `ifx` among them. One limit is worth knowing before you port this: `curl_easy_setopt` and `curl_easy_getinfo` are **variadic** in C, and Fortran cannot express that. `src/cepis_http.f90` binds them with a single pointer-sized third argument and passes integers through `transfer`, which is what the **SysV AMD64** and **AAPCS64** calling conventions ask for — in both, the first several variadic arguments travel in registers exactly as fixed ones do. That assumption does **not** hold on every ABI. On one that passes variadic arguments on the stack while passing fixed arguments in registers — **Apple ARM64 is the case to watch** — the binding as written would put the value in the wrong place, and the failure would be silent corruption of the option value rather than a compile error. Porting there means giving the variadic calls their own interface blocks per option type, or routing them through a small C shim. ## 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).