diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d25149 --- /dev/null +++ b/README.md @@ -0,0 +1,239 @@ +# Cepis + +C# client for the [public CEP API at cep.is](https://cep.is). Official Correios +data (DNE), refreshed every two weeks. + +```csharp +using Cepis; + +Address address = await CepisClient.Default.LookupAsync("01001-000"); +Console.WriteLine($"{address.Logradouro} {address.Localidade} {address.UF}"); +// Praça da Sé São Paulo SP +``` + +## Installation + +Requires .NET 8 or newer. There are no runtime dependencies: HTTP comes from +`System.Net.Http`, JSON from `System.Text.Json`, and the CSV reader behind the +batch endpoint is in the library because the BCL has none. + +```sh +git clone https://git.cep.is/csharp.git cepis-csharp +cd cepis-csharp +make pack +``` + +That writes `artifacts/Cepis.0.1.0.nupkg`; add it from a local feed, or +reference `src/Cepis/Cepis.csproj` directly. + +## Usage + +### A single lookup + +`LookupAsync` accepts the CEP masked or unmasked and returns an `Address`: + +```csharp +Address a = await client.LookupAsync("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" +``` + +The data fields keep the API's own Portuguese names, with only the casing +adapted; a `JsonPropertyName` attribute maps each one to its wire name: + +| wire | property | wire | property | +| --- | --- | --- | --- | +| `cep` | `Cep` | `regiao` | `Regiao` | +| `logradouro` | `Logradouro` | `ibge` | `Ibge` | +| `complemento` | `Complemento` | `gia` | `Gia` | +| `unidade` | `Unidade` | `ddd` | `Ddd` | +| `bairro` | `Bairro` | `siafi` | `Siafi` | +| `localidade` | `Localidade` | `geo.lat` / `geo.lon` | `Geo.Lat` / `Geo.Lon` | +| `uf` | `UF` | `geo.raio_m` | `Geo.RaioM` | +| `estado` | `Estado` | `geo.precisao` | `Geo.Precisao` | + +Fields the database does not have for that CEP come back as empty strings, +never `null`. A field the API grows that this version does not model lands in +`a.Extra`, so nothing is silently dropped. + +### Coordinates + +About 57% of CEPs have a coordinate. When they do not, `Geo` is `null`: + +```csharp +if (a.Geo is { } geo) +{ + geo.Lat; // -23.567202 + geo.Lon; // -46.649659 + geo.RaioM; // 1084 + 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"`: + +```csharp +decimal cost = a.Geo is { IsPrecise: true } geo + ? ShippingFrom(geo.Lat, geo.Lon) + : ShippingFromMunicipality(a.Ibge); +``` + +### Many CEPs at once + +Do not loop over `LookupAsync`. Use `LookupManyAsync`, which resolves up to 500 +CEPs per request: + +```csharp +string[] ceps = ["01001000", "99999999", "20040020"]; +IReadOnlyList<Address?> result = await client.LookupManyAsync(ceps); + +for (int i = 0; i < ceps.Length; i++) +{ + Console.WriteLine(result[i] is null + ? $"{ceps[i]}: not found" + : $"{ceps[i]}: {result[i]!.Localidade}"); +} +``` + +The returned list has **the same length and the same order** as the input. A +CEP that does not exist, or is malformed, becomes `null` at its position — it +does not vanish, and it does not throw. + +Inputs 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 `LookupManyAsync` has `Geo == null`, +> including for CEPs that do have one. Need the coordinate? Use `LookupAsync`, +> one at a time. + +### API health + +Useful for two things: checking the API answers, and knowing **which version of +the database** is live: + +```csharp +Health h = await client.HealthAsync(); +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 `CepisClient` and reuse it. It keeps +no state between calls, holds one pooled `HttpClient`, and is safe to share +across threads: + +```csharp +using var client = new CepisClient(new CepisClientOptions +{ + BaseUrl = "https://cep.is", // point at another instance + Timeout = TimeSpan.FromSeconds(10), // per attempt, connection included + MaxAttempts = 3, // counting the first; 1 disables retrying + BackoffBase = TimeSpan.FromMilliseconds(500), // base of the exponential wait + BackoffMax = TimeSpan.FromSeconds(8), // cap on each wait + TotalTimeout = TimeSpan.FromSeconds(15), // cap on the whole call; unset by default +}); + +Address address = await client.LookupAsync("01001000"); +``` + +Retries cover 429, 5xx and network failures, with exponential backoff and +jitter; 400 and 404 are never retried. Every setting is documented on +`CepisClientOptions`. + +`CepisClient.Default` is a shared client with the default configuration, built +on first use. It applies process-wide, including to third-party libraries that +also use this package, so do not dispose it; need a different configuration? +Build your own. + +To take the `HttpClient` from `IHttpClientFactory` or a container, pass it in — +it is then neither reconfigured nor disposed by this client: + +```csharp +var client = new CepisClient(httpClientFactory.CreateClient("cepis")); +``` + +### 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`: + +```csharp +using var client = new CepisClient(new CepisClientOptions +{ + BaseUrl = "http://localhost:8080", +}); +``` + +## Errors + +Everything derives from `CepisException`. 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; carries `RetryAfter` | yes, later | +| `ServerErrorException` | 5xx; carries `Status` | yes, later | +| `NetworkException` | DNS, connection, timeout, TLS | yes, later | +| `InvalidResponseException` | unintelligible response | probably not | + +```csharp +try +{ + Address address = await client.LookupAsync(cepFromForm); +} +catch (CepNotFoundException) +{ + // ask the user for another CEP +} +catch (CepisException) +{ + // network, server, rate limit: reschedule +} +``` + +C# allows one base class, so these are not also `ArgumentException` or +`KeyNotFoundException`: one base type that catches every failure is worth more. +A cancellation you asked for is not a `CepisException` — it surfaces as +`OperationCanceledException`, untouched. + +`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` and the .NET SDK: + +```sh +make # list the targets +make test # run the suite against a local HTTP server +make lint # dotnet format --verify-no-changes + build -warnaserror, touches nothing +make format # apply dotnet format +make build # build the solution +make pack # the NuGet package into artifacts/ +make check # lint, then test, then build +``` + +## Licence + +ISC. See [LICENSE](LICENSE). |