#' 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)) }