summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md239
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).