1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
#' Look up a single CEP
#'
#' @description
#' Accepts the CEP masked or unmasked and returns everything the DNE database
#' has for it, including the coordinate when there is one.
#'
#' @param cep A length-one character vector: `"01001-000"` or `"01001000"`.
#' @param client A [cepis_client()]. Defaults to a shared client built with
#' the default configuration.
#'
#' @return A [cepis_address].
#'
#' @section Conditions:
#' \describe{
#' \item{`cepis_invalid_cep`}{the CEP is not 8 digits. No request is made.}
#' \item{`cepis_not_found`}{well-formed, but not in the database.}
#' \item{`cepis_rate_limited`}{429 on every attempt.}
#' \item{`cepis_server_error`}{5xx on every attempt.}
#' \item{`cepis_network_error`}{the request never completed.}
#' \item{`cepis_invalid_response`}{a success status with an unreadable
#' body.}
#' }
#'
#' All of them inherit `cepis_error`; see [cepis_conditions].
#'
#' @examples
#' \donttest{
#' address <- cep_lookup("01001-000")
#' address$localidade
#' if (!is.null(address$geo) && address$geo$is_precise) {
#' c(address$geo$lat, address$geo$lon)
#' }
#' }
#'
#' @export
cep_lookup <- function(cep, client = default_client()) {
number <- normalize_cep(cep)
response <- cepis_fetch(client, paste0("/", number))
if (response$status == 404L) {
stop(not_found_error(
paste0("CEP ", number, " does not exist in the DNE database"),
cep = number
))
}
if (response$status == 400L) {
stop(invalid_cep_error(
api_message(response$body, paste0("CEP ", number, " rejected")),
value = cep
))
}
if (response$status != 200L) {
stop(server_error(
paste0("the API answered ", response$status, " for CEP ", number),
status = response$status
))
}
new_address(json_object(response$body))
}
#' Look up many CEPs in one request
#'
#' @description
#' Resolves up to 500 CEPs per request, chunking longer inputs automatically.
#' Do not loop over [cep_lookup()]: a loop of 500 calls pays 500 TLS
#' handshakes, the batch pays one.
#'
#' @param ceps A character vector of CEPs, masked or unmasked.
#' @param client A [cepis_client()]. Defaults to a shared client built with
#' the default configuration.
#'
#' @return A list of **the same length and in the same order** as `ceps`.
#' Each element is a [cepis_address], or `NULL` where the CEP does not
#' exist or is malformed — it does not vanish from the list, and it does not
#' signal a condition. That is the right difference from [cep_lookup()]: in
#' a batch of a thousand, one bad CEP must not cost the other nine hundred
#' and ninety-nine. Use [normalize_cep()] beforehand if you need to know
#' *which* input was malformed.
#'
#' A list rather than a `data.frame` because `NULL` is the only unambiguous
#' "no such CEP" in R: a row of `NA`s reads as an address with unknown
#' fields, which is a different statement, and an empty row cannot be told
#' apart from a CEP the database knows only by number. Rectangle it with
#' `do.call(rbind, lapply(result, function(a) as.data.frame(a[1:13])))` when
#' a `data.frame` is what you want.
#'
#' @section Batches carry no coordinate:
#' The endpoint behind them is CSV, and the CSV has no `geo` column. Every
#' address returned here has `geo = NULL`, including for CEPs that do have
#' one. Need `geo`? Use [cep_lookup()], one at a time.
#'
#' @examples
#' \donttest{
#' ceps <- c("01001000", "99999999", "20040020")
#' result <- cep_lookup_many(ceps)
#' vapply(result, function(a) if (is.null(a)) NA_character_ else a$localidade,
#' character(1))
#' }
#'
#' @export
cep_lookup_many <- function(ceps, client = default_client()) {
if (length(ceps) == 0L) {
return(list())
}
ceps <- as.character(ceps)
result <- list()
starts <- seq(1L, length(ceps), by = MAX_BATCH)
for (start in starts) {
chunk <- ceps[start:min(start + MAX_BATCH - 1L, length(ceps))]
result <- c(result, lookup_chunk(client, chunk))
}
result
}
lookup_chunk <- function(client, ceps) {
query <- utils::URLencode(paste(ceps, collapse = ","), reserved = TRUE)
response <- cepis_fetch(client, paste0("/csv?ceps=", query))
if (response$status == 400L) {
stop(invalid_cep_error(
api_message(response$body, "the API rejected the batch"),
value = ceps
))
}
if (response$status != 200L) {
stop(server_error(
paste0("the API answered ", response$status, " for a batch of ",
length(ceps), " CEP(s)"),
status = response$status
))
}
table <- csv_rows(response$body)
rows <- vector("list", nrow(table))
for (index in seq_len(nrow(table))) {
row <- as.list(table[index, , drop = FALSE])
localidade <- row[["localidade"]]
if (is.null(localidade) || !nzchar(trimws(localidade))) {
next
}
rows[[index]] <- new_address(row)
}
rows[seq_along(ceps)]
}
#' Ask the API how it is, and which database it is serving
#'
#' @description
#' Useful for two things: checking that the API answers, and knowing which
#' version of the DNE database is live. The database changes every two weeks.
#'
#' @param client A [cepis_client()]. Defaults to a shared client built with
#' the default configuration.
#'
#' @return A [cepis_health].
#'
#' @section Conditions:
#' An API reporting itself degraded is not a failure here — that arrives in
#' `status`, and checking health is exactly when you want the diagnosis
#' rather than an error. Network failures still signal `cepis_network_error`.
#'
#' @examples
#' \donttest{
#' health <- cep_health()
#' health$ok
#' health$dne
#' }
#'
#' @export
cep_health <- function(client = default_client()) {
response <- cepis_fetch(client, "/health", accepted_status = c(200L, 503L))
if (!(response$status %in% c(200L, 503L))) {
stop(server_error(
paste0("the API answered ", response$status, " at /health"),
status = response$status
))
}
new_health(json_object(response$body))
}
|