summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:38:22 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:38:22 -0300
commit8e719fc715eb7ae9aa1c842e633f15b5c6cd3688 (patch)
tree4afda3d13e1b772f2c9a5cf9ceea5e84c647ebdd /README.md
downloaddart-8e719fc715eb7ae9aa1c842e633f15b5c6cd3688.tar.gz
Initial importv0.1.0
Diffstat (limited to 'README.md')
-rw-r--r--README.md233
1 files changed, 233 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ffb4cac
--- /dev/null
+++ b/README.md
@@ -0,0 +1,233 @@
+# cepis
+
+Dart client for the [public CEP API at cep.is](https://cep.is). Official
+Correios data (DNE), refreshed every two weeks. Pure Dart, so the same code runs
+on the VM, in AOT binaries and everywhere Flutter runs — Android, iOS, desktop
+and Web.
+
+```dart
+import 'package:cepis/cepis.dart' as cepis;
+
+final address = await cepis.lookup('01001-000');
+print('${address.logradouro} ${address.localidade} ${address.uf}');
+// Praça da Sé São Paulo SP
+```
+
+## Installation
+
+```sh
+dart pub add cepis
+# or, in a Flutter app: flutter pub add cepis
+```
+
+Requires Dart 3.6 or newer. One dependency, `package:http`: the SDK's own
+`HttpClient` does not exist on Flutter Web, and a client that breaks on one of
+Flutter's platforms is not a Flutter client.
+
+## Usage
+
+### A single lookup
+
+`lookup` accepts the CEP masked or unmasked and resolves to an `Address`:
+
+```dart
+final a = await 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.formattedCep; // '01310-100'
+```
+
+Fields the database does not have for that CEP come back as empty strings, never
+`null`. `a.raw` reads the response as it arrived, including a field the API grew
+after this release.
+
+The data fields keep the API's own Portuguese names; everything else — methods,
+types, errors — is English. Only the casing changes, where Dart demands it: the
+API's `raio_m` is `geo.raioM` and its `base_gerada_em` is `health.baseGeradaEm`.
+
+### Coordinates
+
+About 57% of CEPs have a coordinate. When they do not, `geo` is `null`:
+
+```dart
+final a = await cepis.lookup('01310100');
+
+final geo = a.geo;
+if (geo != null) {
+ geo.lat; // -23.567202
+ geo.lon; // -46.649659
+ geo.raioM; // 1084 (the API's `raio_m`)
+ 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 |
+
+`geo.isPrecise` is the shorthand for `precisao == 'logradouro'`:
+
+```dart
+final geo = a.geo;
+final cost = geo != null && geo.isPrecise
+ ? shippingFrom(geo.lat, geo.lon)
+ : shippingFromMunicipality(a.ibge);
+```
+
+### Many CEPs at once
+
+Do not loop over `lookup`. Use `lookupMany`, which resolves up to 500 CEPs per
+request:
+
+```dart
+final ceps = <String>['01001000', '99999999', '20040020'];
+final result = await cepis.lookupMany(ceps);
+
+for (var i = 0; i < ceps.length; i++) {
+ final address = result[i];
+ print('${ceps[i]}: ${address == null ? 'not found' : address.localidade}');
+}
+```
+
+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 throw.
+
+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 `lookupMany` has a `null` `geo`,
+> 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:
+
+```dart
+final h = await cepis.health();
+
+h.ok; // true
+h.dne; // '26071' identifier of the fortnightly database
+h.ceps; // 1596168
+h.baseGeradaEm; // '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 `Client` and reuse it. It keeps no
+state between calls:
+
+```dart
+final client = cepis.Client(
+ baseUrl: 'https://cep.is', // point at another instance
+ timeout: const Duration(seconds: 10), // per attempt, body read included
+ maxAttempts: 3, // counting the first; 1 disables retrying
+ backoffBase: const Duration(milliseconds: 500),
+ backoffMax: const Duration(seconds: 8), // cap on each wait
+ totalTimeout: const Duration(seconds: 15), // cap on the whole call
+);
+
+final address = await client.lookup('01001000');
+client.close();
+```
+
+`timeout` bounds a single attempt — request and body read together. The cap on
+all the attempts and the waits between them is `totalTimeout`.
+
+Retries cover 429, 5xx and network failures, with exponential backoff and full
+jitter; 400 and 404 are never retried.
+
+Every snippet here assumes the `import ... as cepis;` above; a plain
+`import 'package:cepis/cepis.dart';` puts `Client`, `Address` and the rest in
+scope unprefixed.
+
+`Client` also takes an `httpClient`, for sharing a connection pool or for a
+platform-native transport such as `cupertino_http` or `cronet_http`. A client
+passed that way is yours to close; `close()` leaves it alone.
+
+The top-level functions (`cepis.lookup`, `cepis.lookupMany`, `cepis.health`) use
+a shared default client, reachable as `cepis.defaultClient`. It applies
+process-wide, third-party packages included. Need a different configuration?
+Build your own.
+
+### Pointing at a local instance
+
+`baseUrl` is the only setting that usually changes between environments. The API
+repo brings a local instance up with `just up`:
+
+```dart
+final client = cepis.Client(baseUrl: 'http://localhost:8080');
+```
+
+From the command line, to confirm quickly that it answers:
+
+```sh
+dart run cepis --base-url http://localhost:8080 01001000
+```
+
+## Errors
+
+Everything derives from `CepisException`, which is `sealed` — a `switch` over it
+is checked for exhaustiveness. 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? |
+| --- | --- | --- |
+| `InvalidCepException` | not 8 digits | no |
+| `CepNotFoundException` | well-formed, not in the database | no |
+| `RateLimitedException` | 429 | yes, later |
+| `ServerException` | 5xx | yes, later |
+| `NetworkException` | DNS, connection, timeout, TLS | yes, later |
+| `InvalidResponseException` | unintelligible response | probably not |
+
+```dart
+try {
+ final address = await cepis.lookup(cepFromForm);
+} on cepis.CepNotFoundException {
+ // ask the user for another CEP
+} on cepis.CepisException {
+ // transient failure: reschedule
+}
+```
+
+`RateLimitedException` exposes `retryAfter`, `ServerException` exposes `status`,
+and `NetworkException` exposes `cause`.
+
+`InvalidCepException` also implements `FormatException`, so code already
+handling malformed input during validation keeps working without knowing this
+library exists.
+
+`InvalidResponseException` 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 shells out to `dart`:
+
+```sh
+make # list the targets
+make test # dart test
+make lint # dart format --set-exit-if-changed + dart analyze, touches nothing
+make format # apply dart format
+make build # compile the CLI to build/cepis
+make check # lint, then test, then build
+```
+
+The suite stands a real `HttpServer` up on a loopback port and points `baseUrl`
+at it: no network, and nothing about the HTTP layer is stubbed.
+
+## Licence
+
+ISC. See [LICENSE](LICENSE).