diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
| commit | 0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch) | |
| tree | 86f73108f0432fd8330edcaab8b8ae7018322dee /README.md | |
| download | perl-0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..b799f45 --- /dev/null +++ b/README.md @@ -0,0 +1,293 @@ +# Cepis + +Perl client for the [public CEP API at cep.is](https://cep.is). Official +Correios data (DNE), refreshed every two weeks. No non-core dependencies: +`HTTP::Tiny`, `JSON::PP` and `Encode` ship with Perl, and the CSV reader is part +of the distribution. + +```perl +use Cepis; + +my $address = Cepis::lookup('01001-000'); +printf "%s %s %s\n", $address->logradouro, $address->localidade, $address->uf; +# Praça da Sé São Paulo SP +``` + +## Installation + +```sh +perl Makefile.PL +make +make test +make install +``` + +Requires Perl 5.14 or newer — that is when `HTTP::Tiny` and `JSON::PP` entered +the core. + +### HTTPS needs two modules that Perl does not ship + +The library itself needs nothing from CPAN, but **reaching `https://cep.is` +does**. `HTTP::Tiny` speaks TLS only when both of these are installed: + +| module | minimum version | +| --- | --- | +| `IO::Socket::SSL` | 1.968 | +| `Net::SSLeay` | 1.49 | + +Neither is a core module. Without them every call against the default +`base_url` fails, so install them alongside this distribution: + +```sh +cpanm IO::Socket::SSL Net::SSLeay +# or, from your distribution's packages: +# Debian/Ubuntu apt install libio-socket-ssl-perl libnet-ssleay-perl +# Arch pacman -S perl-io-socket-ssl perl-net-ssleay +# Alpine apk add perl-io-socket-ssl perl-net-ssleay +# OpenBSD pkg_add p5-IO-Socket-SSL p5-Net-SSLeay +``` + +The client checks for them before it opens a socket and, when they are missing, +throws `Cepis::Error::Network` immediately with a message naming both — rather +than letting a bare `HTTP::Tiny` internal error surface: + +``` +cannot reach https://cep.is/01001000: HTTP::Tiny has no HTTPS support on this +Perl. It needs IO::Socket::SSL 1.968 or later and Net::SSLeay 1.49 or later, +and neither of those ships with Perl. Install both (...), or point base_url at +an http:// instance. HTTP::Tiny reports: ... +``` + +To check without making a request: + +```perl +my ( $ok, $why ) = Cepis::Client::tls_available(); +``` + +Plain HTTP — a local instance, a test server — works on a stock Perl with +nothing installed. + +## Usage + +### A single lookup + +`lookup` accepts the CEP masked or unmasked and returns a `Cepis::Address`: + +```perl +my $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 +`undef`. `$a->field('name')` reads any field of the raw response, including one +the API grew after this release. + +Perl does not distinguish a number from a string, so keep CEPs in strings: the +literal `01001000` loses its leading zero before this library ever sees it. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `geo` is `undef`: + +```perl +my $a = Cepis::lookup('01310100'); + +if ( my $geo = $a->geo ) { + printf "%s %s\n", $geo->lat, $geo->lon; # -23.567202 -46.649659 + print $geo->raio_m, "\n"; # 1084 + print $geo->precisao, "\n"; # "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 | + +`$geo->is_precise` is the shorthand for `precisao eq 'logradouro'`: + +```perl +my $cost + = $a->geo && $a->geo->is_precise + ? shipping_from( $a->geo->lat, $a->geo->lon ) + : 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: + +```perl +my @ceps = qw(01001000 99999999 20040020); +my $result = Cepis::lookup_many( \@ceps ); + +for my $i ( 0 .. $#ceps ) { + print defined $result->[$i] + ? "$ceps[$i]: " . $result->[$i]->localidade . "\n" + : "$ceps[$i]: not found\n"; +} +``` + +The returned arrayref has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, is `undef` at its position — it does +not vanish, and it does not throw. + +A Perl array holds `undef` at any position without shortening, so nothing is +dropped and nothing shifts: `$result->[$i]` always corresponds to `$ceps[$i]`. +That is worth stating because it is not true everywhere — in Lua, for instance, +a nil in a table truncates it. Here `scalar @$result == scalar @ceps` always +holds, and `grep { defined } @$result` is how you discard the misses when the +pairing no longer matters. + +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 `Cepis::Address` from `lookup_many` has `geo` set +> to `undef`, 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: + +```perl +my $h = Cepis::health(); + +$h->ok; # 1 +$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 throw — that arrives in `$h->status`. + +## Configuration + +For anything beyond the defaults, build a `Cepis::Client` and reuse it. It keeps +no state between calls: + +```perl +use Cepis::Client; + +my $client = Cepis::Client->new( + 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 => undef, # cap on the whole call, attempts included + user_agent => undef, # defaults to cepis-perl/<version> +); + +my $address = $client->lookup('01001000'); +``` + +`timeout` bounds each socket operation, not the whole call — that cap is +`total_timeout`, and without it the worst case is `max_attempts * timeout` plus +the sum of the backoffs. + +Retries cover 429, 5xx and network failures, with exponential backoff and full +jitter; 400 and 404 are never retried. + +The module-level functions (`Cepis::lookup`, and so on) share one default +client, reachable as `Cepis::default_client()`. It applies process-wide, +including to third-party libraries that also use this module. Need a different +configuration? Build your own. + +Each client holds one `HTTP::Tiny`, which is not shared between threads. Give +each thread its own client. + +### 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`: + +```perl +my $client = Cepis::Client->new( base_url => 'http://localhost:8080' ); +``` + +## Errors + +Every failure is thrown as an object under `Cepis::Error`, so one test catches +the lot. 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? | +| --- | --- | --- | +| `Cepis::Error::InvalidCEP` | not 8 digits | no | +| `Cepis::Error::CEPNotFound` | well-formed, not in the database | no | +| `Cepis::Error::RateLimited` | 429 | yes, later | +| `Cepis::Error::ServerError` | 5xx | yes, later | +| `Cepis::Error::Network` | DNS, connection, timeout, TLS | yes, later | +| `Cepis::Error::InvalidResponse` | unintelligible response | probably not | + +```perl +my $address = eval { Cepis::lookup($cep_from_form) }; +if ( !$address ) { + my $err = $@; + if ( ref $err && $err->isa('Cepis::Error::CEPNotFound') ) { + # ask the user for another CEP + } + elsif ( ref $err && $err->isa('Cepis::Error') ) { + # transient failure: reschedule + } + else { + die $err; + } +} +``` + +Errors stringify to their message, so `warn $err` and `"$err"` do the obvious +thing. `$err->retry_after` carries what the server asked for, in seconds, and +`$err->status` the HTTP status of the last attempt. + +`Cepis::Error::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. + +A bad argument to `Cepis::Client->new` is a `croak`, not a `Cepis::Error`: that +is a programming error, not a failed lookup. + +## Development + +Everything runs through `make` and the `perl` on your `PATH`. There is nothing +to install: the distribution and its suite use only core modules, and the tests +bring up a real HTTP server on loopback. + +```sh +make # list the targets +make test # run the suite with prove +make lint # perl -c over lib, t and Makefile.PL +make build # build Cepis-0.1.0.tar.gz +make check # lint, then test, then build +``` + +`prove -l t/` works too. + +Perl ships no linter or formatter in its core, and this distribution depends on +neither. `make lint` is therefore a `perl -c` compile check; if `perlcritic` +happens to be installed it is run as well, and `make format` applies `perltidy` +when that is installed. Neither is installed here, so `make lint` reports the +syntax checks only. + +The `Makefile` in the repository is for development only and is not shipped. +The tarball carries `Makefile.PL`, so installing from it is the usual +`perl Makefile.PL && make && make test && make install`. + +Documentation lives in POD on the public API: `perldoc lib/Cepis.pm`, +`perldoc lib/Cepis/Client.pm`. + +## Licence + +ISC. See [LICENSE](LICENSE). |