From ba1608127fc4a1ef6a48a6ac4e1c9a2eb2c7b359 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 18 Feb 2026 12:25:02 +1100 Subject: [PATCH 01/70] Updates to taxonomic and metadata help files --- NEWS.md | 2 +- R/dplyr-filter.R | 11 ++++++++--- R/taxonomic-searches.R | 4 ++-- R/tidyr-unnest.R | 15 +++++++++++++-- man/filter.data_request.Rd | 11 ++++++++--- man/taxonomic_searches.Rd | 4 ++-- man/unnest.Rd | 15 +++++++++++++-- 7 files changed, 47 insertions(+), 15 deletions(-) diff --git a/NEWS.md b/NEWS.md index dba0a332..754c8584 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,7 +12,7 @@ * `dplyr::glimpse()` can be used to see what the resulting query will look like. * New functions `capture()` and `compound()` as prequels to `collapse()` (#278). -## Changes to metadata functions +### Changes to metadata functions * All metadata requests now accept `select()`. * Metadata types that support `unnest()` now also support `filter()`, even when `unnest()` is not supplied. * All `show_all()` and `search_all()` functions gain an `all_fields` argument. diff --git a/R/dplyr-filter.R b/R/dplyr-filter.R index cf2a0443..518ed734 100644 --- a/R/dplyr-filter.R +++ b/R/dplyr-filter.R @@ -63,10 +63,15 @@ #' filter(doi = "a-long-doi-string") |> #' collect()} #' -#' For taxonomic metadata, the `taxa` field is valid: +#' For metadata that support [unnest()], the lhs of the equation sets the `type`, +#' while the rhs sets the value: #' \preformatted{request_metadata() |> -#' filter(taxa == "Chordata") |> -#' unnest()} +#' filter(field == "basisOfRecord") |> +#' unnest() |> +#' collect()} +#' This can be used for types `list`, `field`, `profile` and `taxa`, +#' noting that for `taxa` it requires a valid `taxonConceptID`; if +#' you have a taxonomic name, use [identify()] instead. #' #' For building taxonomic trees, the `rank` field is valid: #' \preformatted{request_data() |> diff --git a/R/taxonomic-searches.R b/R/taxonomic-searches.R index fc0c9509..8fbe3d31 100644 --- a/R/taxonomic-searches.R +++ b/R/taxonomic-searches.R @@ -25,8 +25,8 @@ #' identifiers. #' #' Note that when taxonomic look-up is required within a pipe, the equivalent -#' to [search_taxa()] is \code{\link[=identify.data_request]{identify()}} (or -#' [galah_identify()]). The equivalent to [search_identifiers()] is to use +#' to [search_taxa()] is \code{\link[=identify.data_request]{identify()}}. The +#' equivalent to [search_identifiers()] is to use #' \code{\link[=filter.data_request]{filter()}} to filter by `taxonConceptId`. #' #' @details diff --git a/R/tidyr-unnest.R b/R/tidyr-unnest.R index ed068c11..c4c34135 100644 --- a/R/tidyr-unnest.R +++ b/R/tidyr-unnest.R @@ -17,16 +17,27 @@ #' filter(field == basisOfRecord) |> #' collect() #' -#' # Using `galah::unnest()` in this way is equivalent to: +#' # This is equivalent to: +#' galah_call() |> +#' distinct(basisOfRecord) |> +#' collect() +#' +#' # and also to: #' show_all(fields, "basisOfRecord") |> #' show_values() #' -#' # to add information to a species list: +#' # Add information to a species list #' request_metadata() |> #' filter(list == "dr650") |> #' select(everything()) |> #' unnest() |> #' collect() +#' +#' # Find 'child' taxa +#' request_metadata() |> +#' identify("Crinia") +#' unnest() |> +#' collect() #' } #' @export unnest <- function(.query){ diff --git a/man/filter.data_request.Rd b/man/filter.data_request.Rd index dbcde5c2..cbc89591 100644 --- a/man/filter.data_request.Rd +++ b/man/filter.data_request.Rd @@ -80,10 +80,15 @@ When requesting a data download from a DOI, the field \code{doi} is valid, i.e.: filter(doi = "a-long-doi-string") |> collect()} -For taxonomic metadata, the \code{taxa} field is valid: +For metadata that support \code{\link[=unnest]{unnest()}}, the lhs of the equation sets the \code{type}, +while the rhs sets the value: \preformatted{request_metadata() |> - filter(taxa == "Chordata") |> - unnest()} + filter(field == "basisOfRecord") |> + unnest() |> + collect()} +This can be used for types \code{list}, \code{field}, \code{profile} and \code{taxa}, +noting that for \code{taxa} it requires a valid \code{taxonConceptID}; if +you have a taxonomic name, use \code{\link[=identify]{identify()}} instead. For building taxonomic trees, the \code{rank} field is valid: \preformatted{request_data() |> diff --git a/man/taxonomic_searches.Rd b/man/taxonomic_searches.Rd index 7767ec67..959a0a40 100644 --- a/man/taxonomic_searches.Rd +++ b/man/taxonomic_searches.Rd @@ -26,8 +26,8 @@ inverse function to \code{\link[=search_taxa]{search_taxa()}}, which takes names identifiers. Note that when taxonomic look-up is required within a pipe, the equivalent -to \code{\link[=search_taxa]{search_taxa()}} is \code{\link[=identify.data_request]{identify()}} (or -\code{\link[=galah_identify]{galah_identify()}}). The equivalent to \code{\link[=search_identifiers]{search_identifiers()}} is to use +to \code{\link[=search_taxa]{search_taxa()}} is \code{\link[=identify.data_request]{identify()}}. The +equivalent to \code{\link[=search_identifiers]{search_identifiers()}} is to use \code{\link[=filter.data_request]{filter()}} to filter by \code{taxonConceptId}. } \details{ diff --git a/man/unnest.Rd b/man/unnest.Rd index a19dfa94..545fb5ef 100644 --- a/man/unnest.Rd +++ b/man/unnest.Rd @@ -31,15 +31,26 @@ request_metadata() |> filter(field == basisOfRecord) |> collect() -# Using `galah::unnest()` in this way is equivalent to: +# This is equivalent to: +galah_call() |> + distinct(basisOfRecord) |> + collect() + +# and also to: show_all(fields, "basisOfRecord") |> show_values() -# to add information to a species list: +# Add information to a species list request_metadata() |> filter(list == "dr650") |> select(everything()) |> unnest() |> collect() + +# Find 'child' taxa +request_metadata() |> + identify("Crinia") + unnest() |> + collect() } } From 1791baec0d59d86b6b6e964f91dd156e97813a8e Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 20 Feb 2026 19:27:43 +1100 Subject: [PATCH 02/70] First pass supporting event queries (#226) --- R/capture.R | 4 ++++ R/capture_events.R | 28 ++++++++++++++++++++++++++++ R/capture_occurrences_count.R | 12 ++++++------ R/collapse_events.R | 20 ++++++++++++++++++++ R/collapse_query_set.R | 2 ++ R/collect_events.R | 16 ++++++++++++++++ R/dplyr-collect.R | 2 ++ R/galah_call.R | 22 ++++++++++++---------- R/print.R | 8 ++++---- R/query_API.R | 17 +++++++++++++---- R/sysdata.rda | Bin 7386 -> 7421 bytes data-raw/node_config.csv | 2 ++ man/galah_call.Rd | 4 ++-- man/print_galah_objects.Rd | 6 +++--- tests/testthat/test-atlas_events.R | 15 +++++++++++++++ 15 files changed, 129 insertions(+), 29 deletions(-) create mode 100644 R/capture_events.R create mode 100644 R/collapse_events.R create mode 100644 R/collect_events.R create mode 100644 tests/testthat/test-atlas_events.R diff --git a/R/capture.R b/R/capture.R index 4ea1fcf1..05c79cf3 100644 --- a/R/capture.R +++ b/R/capture.R @@ -77,6 +77,8 @@ capture.data_request <- function(x, "species" = capture_species(x), "species-count" = capture_species_count(x), "distributions" = capture_distributions_data(x), + "events" = capture_events(x), + "events-count" = capture_events_count(x), cli::cli_abort("Unrecognised 'type'")) |> add_request(x) } @@ -293,6 +295,8 @@ count_switch <- function(x){ "occurrences-count" = "occurrences-count", "species" = "species-count", "species-count" = "species-count", + "events" = "events-count", + "events-count" = "events-count", "media" = cli::cli_abort("type = 'media' is not supported by `count()`"), cli::cli_abort("`count()` only supports `type = 'occurrences' or` `'species'`")) x diff --git a/R/capture_events.R b/R/capture_events.R new file mode 100644 index 00000000..15b45880 --- /dev/null +++ b/R/capture_events.R @@ -0,0 +1,28 @@ +capture_events <- function(x, + error_call = rlang::caller_env()){ + browser() +} + +capture_events_count <- function(x, + error_call = rlang::caller_env()){ + + # compile supplied arguments into a list + # honestly this is a little messy, but the alternative is to call + # [build_predicates()], which is messier as taxonomic info hasn't yet been + # parsed. Instead we call [build_predicates()] during [collapse_query()]. + predicates_info <- list(identify = x$identify, + filter = x$filter, + geolocate = x$geolocate, + group_by = x$group_by, + slice = ifelse(is.null(x$slice), + tibble::tibble(slice_n = 30, slice_called = FALSE), + x$slice), + limit = 0) + + list(type = "data/events-count", + url = url_lookup("data/events-count"), + headers = c(build_headers(), + `Content-Type` = "application/json"), + body = predicates_info) |> + as_prequery() +} \ No newline at end of file diff --git a/R/capture_occurrences_count.R b/R/capture_occurrences_count.R index c2762323..7644f821 100644 --- a/R/capture_occurrences_count.R +++ b/R/capture_occurrences_count.R @@ -20,12 +20,12 @@ capture_occurrences_count <- function(.query){ #' @keywords Internal #' @noRd capture_occurrences_count_atlas <- function(identify = NULL, - filter = NULL, - geolocate = NULL, - apply_profile = NULL, - group_by = NULL, - distinct = NULL, - slice_arrange = NULL + filter = NULL, + geolocate = NULL, + apply_profile = NULL, + group_by = NULL, + distinct = NULL, + slice_arrange = NULL ){ query <- build_query(identify, filter, diff --git a/R/collapse_events.R b/R/collapse_events.R new file mode 100644 index 00000000..9914ade7 --- /dev/null +++ b/R/collapse_events.R @@ -0,0 +1,20 @@ +#' @noRd +#' @keywords Internal +collapse_events <- function(x){ + browser() +} + +#' @noRd +#' @keywords Internal +collapse_events_count <- function(x){ + + result <- list(eventSearch = list(documents = list(total = list()))) |> + jsonlite::toJSON(auto_unbox = TRUE, + pretty = FALSE) + + x$body <- glue::glue("query list {result}") |> + stringr::str_remove_all(":\\[\\]") |> + stringr::str_remove_all(":|\"") + + x +} \ No newline at end of file diff --git a/R/collapse_query_set.R b/R/collapse_query_set.R index 22d3dc1d..c51976c0 100644 --- a/R/collapse_query_set.R +++ b/R/collapse_query_set.R @@ -5,6 +5,8 @@ collapse_query_set <- function(x, error_call = rlang::caller_env()){ switch(x$type, + "data/events" = collapse_events(x), + "data/events-count" = collapse_events_count(x), "data/occurrences" = collapse_occurrences(x), "data/occurrences-count" = { if(is_gbif()){ diff --git a/R/collect_events.R b/R/collect_events.R new file mode 100644 index 00000000..69a2acff --- /dev/null +++ b/R/collect_events.R @@ -0,0 +1,16 @@ +#' @noRd +#' @keywords Internal +collect_events <- function(.query, wait = TRUE, file = NULL){ + browser() +} + +#' @noRd +#' @keywords Internal +collect_events_count <- function(.query){ + + # get response from API + result <- query_API(.query) + + tibble::tibble(count = unlist(result)) + # purrr::pluck(result, "data", "eventSearch", "documents", "total") +} \ No newline at end of file diff --git a/R/dplyr-collect.R b/R/dplyr-collect.R index 170bfc42..1487a6af 100644 --- a/R/dplyr-collect.R +++ b/R/dplyr-collect.R @@ -112,6 +112,8 @@ collect.computed_query <- function(x, }else{ switch(x$type, "data/distributions" = collect_distributions(x), + "data/events" = collect_events(x, wait = wait, file = file), + "data/events-count" = collect_events_count(x), "data/occurrences" = collect_occurrences(x, wait = wait, file = file), "data/occurrences-count" = collect_occurrences_count(x), "data/occurrences-count-groupby" = collect_occurrences_count(x), diff --git a/R/galah_call.R b/R/galah_call.R index 262afcdc..3de6ec14 100644 --- a/R/galah_call.R +++ b/R/galah_call.R @@ -73,19 +73,21 @@ #' } #' @export galah_call <- function(type = c("occurrences", - "occurrences-count", - "occurrences-doi", - # "distributions", - "species", - "species-count" - )){ + "occurrences-count", + "occurrences-doi", + # "distributions", + "species", + "species-count", + "events", + "events-count" + )){ if(!missing(type)){ type <- match.arg(type) }else{ type <- "occurrences" } list(type = type) |> - structure(class = "data_request") + structure(class = c("data_request", "list")) } #' @rdname galah_call @@ -121,7 +123,7 @@ request_metadata <- function(type = c("fields", cli::cli_abort() } list(type = type_checked) |> - structure(class = "metadata_request") + structure(class = c("metadata_request", "list")) } #' @rdname galah_call @@ -131,5 +133,5 @@ request_files <- function( # note: option to add `...` here for consistency with `request_data()` ){ list(type = match.arg(type)) |> - structure(class = "files_request") -} + structure(class = c("files_request", "list")) +} \ No newline at end of file diff --git a/R/print.R b/R/print.R index baeb48ec..d2ca2a54 100644 --- a/R/print.R +++ b/R/print.R @@ -46,16 +46,16 @@ print.data_request <- function(x, # NOTE: use of `x` arg here is for consistency #' @rdname print_galah_objects #' @export -print.files_request <- function(x, ...){ +print.metadata_request <- function(x, ...){ format_request_text(x, - object_type = "files_request") + object_type = "metadata_request") } #' @rdname print_galah_objects #' @export -print.metadata_request <- function(x, ...){ +print.files_request <- function(x, ...){ format_request_text(x, - object_type = "metadata_request") + object_type = "files_request") } #' Internal function to format text in `print()` diff --git a/R/query_API.R b/R/query_API.R index c15ac0e3..3262348c 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -135,10 +135,19 @@ add_headers <- function(req, headers){ #' @keywords Internal add_body <- function(req, body){ if(!is.null(body)){ - req <- req |> httr2::req_body_raw(body) - # note: this is not `req_body_json()` because - # we have already converted our list to json text - # by this point + # event datasets have to be structured differently + if(stringr::str_detect(req$url, "^https://api.ala.org.au/event")){ + req <- req |> + httr2::req_body_json(data = list(query = body)) + # this looks weird because it stores a JSON-like string (actually a graphQL query) + # within *another* JSON query. But it runs, so never mind. + }else{ + req <- req |> + httr2::req_body_raw(body) + # note: this is not `req_body_json()` because + # we have already converted our list to json text + # by this point + } } req } diff --git a/R/sysdata.rda b/R/sysdata.rda index 1a7ab2f95fb8630fea264457071c9fadd17d8642..1b74d9c537a08f5768df1414f96edf8fba8c215d 100644 GIT binary patch literal 7421 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 literal 7386 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 Mk}1N3g3W9l0JvK@W&i*H diff --git a/data-raw/node_config.csv b/data-raw/node_config.csv index 6213c730..fe0cdba7 100644 --- a/data-raw/node_config.csv +++ b/data-raw/node_config.csv @@ -1,6 +1,8 @@ atlas,type,url,functional Australia,data/distributions-id,https://api.ala.org.au/spatial-service/distribution/{id},FALSE Australia,data/distributions-taxa,https://api.ala.org.au/spatial-service/distribution/lsids/{lsid}?nowkt=false,FALSE +Australia,data/events,https://api.ala.org.au/event/download,TRUE +Australia,data/events-count,https://api.ala.org.au/event/graphql,TRUE Australia,data/occurrences,https://api.ala.org.au/occurrences/occurrences/offline/download,TRUE Australia,data/occurrences-count,https://api.ala.org.au/occurrences/occurrences/search,TRUE Australia,data/occurrences-count-groupby,https://api.ala.org.au/occurrences/occurrences/facets,TRUE diff --git a/man/galah_call.Rd b/man/galah_call.Rd index 8bece2c7..a42b4b60 100644 --- a/man/galah_call.Rd +++ b/man/galah_call.Rd @@ -9,12 +9,12 @@ \usage{ galah_call( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count") + "species-count", "events", "events-count") ) request_data( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count") + "species-count", "events", "events-count") ) request_metadata( diff --git a/man/print_galah_objects.Rd b/man/print_galah_objects.Rd index bc497739..a4cd95c5 100644 --- a/man/print_galah_objects.Rd +++ b/man/print_galah_objects.Rd @@ -3,8 +3,8 @@ \name{print_galah_objects} \alias{print_galah_objects} \alias{print.data_request} -\alias{print.files_request} \alias{print.metadata_request} +\alias{print.files_request} \alias{print.query} \alias{print.prequery} \alias{print.computed_query} @@ -14,10 +14,10 @@ \usage{ \method{print}{data_request}(x, ...) -\method{print}{files_request}(x, ...) - \method{print}{metadata_request}(x, ...) +\method{print}{files_request}(x, ...) + \method{print}{query}(x, ...) \method{print}{prequery}(x, ...) diff --git a/tests/testthat/test-atlas_events.R b/tests/testthat/test-atlas_events.R new file mode 100644 index 00000000..fa026d83 --- /dev/null +++ b/tests/testthat/test-atlas_events.R @@ -0,0 +1,15 @@ +test_that("`galah_call(type = 'events')` works", { + x <- galah_call(type = "events") + expect_equal(names(x), "type") + expect_equal(x$type, "events") + expect_s3_class(x, "data_request") +}) + +test_that("`count()` works for type = 'events'", { + x <- galah_call(type = "events") |> + count() |> + collect() + expect_equal(nrow(x), 1) + expect_equal(ncol(x), 1) + expect_true(is.integer(x$count)) +}) \ No newline at end of file From 93301ae14c22c016e4dc71eab22d5d6c9bbbd8f5 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Mon, 23 Feb 2026 15:53:07 +1100 Subject: [PATCH 03/70] First pass at including `describe()` (#291) Functionally very similar to `show_all_fields()`, but with different syntax and behaviour --- NAMESPACE | 2 + R/capture.R | 21 ++- R/capture_occurrences.R | 8 + R/check.R | 153 ++++++++++-------- R/collect_metadata.R | 2 +- R/collect_occurrences.R | 43 +++++ R/describe.R | 41 +++++ R/dplyr-collect.R | 1 + R/dplyr-glimpse.R | 2 + _pkgdown.yml | 1 + man/describe.Rd | 48 ++++++ man/glimpse.data_request.Rd | 4 + tests/testthat/test-describe.R | 43 +++++ vignettes/quick_start_guide.Rmd | 227 +++++++++++++++------------ vignettes/quick_start_guide.Rmd.orig | 82 +++++----- 15 files changed, 469 insertions(+), 209 deletions(-) create mode 100644 R/describe.R create mode 100644 man/describe.Rd create mode 100644 tests/testthat/test-describe.R diff --git a/NAMESPACE b/NAMESPACE index a24ad62f..be3d727c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -33,6 +33,7 @@ S3method(compute,prequery) S3method(compute,query) S3method(compute,query_set) S3method(count,data_request) +S3method(describe,data_request) S3method(distinct,data_request) S3method(filter,data_request) S3method(filter,files_request) @@ -80,6 +81,7 @@ export(collect_media) export(compound) export(compute) export(count) +export(describe) export(distinct) export(filter) export(galah_apply_profile) diff --git a/R/capture.R b/R/capture.R index 05c79cf3..22671289 100644 --- a/R/capture.R +++ b/R/capture.R @@ -65,6 +65,7 @@ capture.data_request <- function(x, x <- x |> check_authentication() |> check_doi() |> + check_describe() |> check_distinct_count_groupby() |> check_glimpse() |> check_slice_arrange() |> @@ -72,6 +73,7 @@ capture.data_request <- function(x, switch(x$type, "occurrences" = capture_occurrences(x, mint_doi = mint_doi), "occurrences-count" = capture_occurrences_count(x), + "occurrences-describe" = capture_occurrences_describe(x), "occurrences-doi" = capture_occurrences_doi(x), "occurrences-glimpse" = capture_occurrences_glimpse(x), "species" = capture_species(x), @@ -183,13 +185,30 @@ check_doi <- function(x){ x } +#' Internal function to check for `describe()` +#' @noRd +#' @keywords Internal +check_describe <- function(x){ + if(!is.null(x$describe)){ + if(x$type == "occurrences"){ + x$type <- "occurrences-describe" + x + }else{ + cli::cli_inform("`describe()` is only supported for `type =\"occurrences\"") + x + } + }else{ + x + } +} + #' Internal function to check behaviour of `distinct()`, `group_by()` etc. #' called by `capture()` #' @noRd #' @keywords Internal check_distinct_count_groupby <- function(x){ - if(x$type == "occurrences-doi"){ + if(x$type %in% c("occurrences-doi", "occurrences-describe")){ return(x) } diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index 333d1eb6..6f3d4b39 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -159,4 +159,12 @@ capture_occurrences_glimpse <- function(.query){ result$type <- "data/occurrences-glimpse" as_prequery(result) } +} + +#' Internal function to convert `data_request` with `type = "occurrences-decribe"` to a `query` +#' @noRd +#' @keywords Internal +capture_occurrences_describe <- function(.query){ + x <- request_metadata(type = "fields") |> capture() + update_request_object(x, type = "data/occurrences-describe") } \ No newline at end of file diff --git a/R/check.R b/R/check.R index 5205e157..e78a013f 100644 --- a/R/check.R +++ b/R/check.R @@ -444,7 +444,7 @@ check_identifiers_la <- function(.query, if(inherits(.query$url, "data.frame")){ url <- httr2::url_parse(.query$url$url[1]) }else{ - url <- httr2::url_parse(.query$url[1]) + url <- httr2::url_parse(.query$url[1]) } queries <- url$query if(!is.null(queries$fq)){ @@ -767,82 +767,20 @@ check_select <- function(.query, dimnames = list(NULL, valid_any)) |> as.data.frame() - # 2. parse groups - group_initial <- .query$request$select$group - # new step to avoid calling `show_all_assertions()` internally - group <- group_initial[group_initial != "assertions"] - if(length(group) > 0){ - group_cols <- purrr::map(group, preset_groups) |> - unlist() - group_names <- tidyselect::eval_select(dplyr::all_of(group_cols), - data = df) |> - names() - # note: technically `group_names` and `group_cols` are identical - # BUT `eval_select()` will fail if invalid columns are given - }else{ - group_names <- NULL - } - - # 3. parse quosures to get list of field names - if(length(.query$request$select$quosure) > 0){ - dot_names <- purrr::map(.query$request$select$quosure, - function(a){ - tidyselect::eval_select(a, - data = df, - error_call = error_call) |> - names() - }) |> - unlist() - }else{ - dot_names <- c() - } - - # 3a: set 'identifier' column name - id_col <- default_columns()[1] - - # 4: set behaviour depending on what names are given - # NOTE: - ## because assertions aren't fields, leaving `fields` empty means default fields are returned - ## but only when `group = assertions` and no other requests are made - ## this adds a single field (recordID) to the query to avoid this problem. - ## This problem also occurs when a single field is requested - ## under some circumstances (e.g. "images"), even when that field is - ## fully populated. - if(length(dot_names) > 1){ - individual_cols <- dot_names - }else{ - if(length(dot_names) == 1){ # i.e. a single field selected - if(length(group_names) == 0){ - individual_cols <- unique(c(id_col, dot_names)) - }else{ - individual_cols <- dot_names - } - }else{ # i.e. length(dot_names) == 0, meaning no fields selected - if(length(group_initial) <= 1 & !any(group_names == id_col)){ - individual_cols <- id_col - }else{ - individual_cols <- NULL - } - } - } + # handle supplied `select()` + field_values <- parse_select_occurrences(.query, df, error_call = error_call) - # 5. merge to create output object - # NOTE: placing `recordID` first is critical; - # having e.g. media columns _before_ `recordID` causes the download to fail - field_values <- unique(c(group_names, individual_cols)) + # warn if(is.null(field_values)){ c("No fields selected", i = "Please specify a valid set of fields in `select()`", i = "You can look up valid fields using `show_all(fields)`") |> cli::cli_abort(call = error_call) } - if(any(field_values == id_col)){ - field_values <- c(id_col, field_values[field_values != id_col]) # recordID needs to be first - } - + # 6. handle assertions is_assertion <- field_values %in% valid_assertions - if(any(group_initial == "assertions")){ + if(any(.query$request$select$group == "assertions")){ assertion_text <- "includeall" }else{ if(any(is_assertion)){ @@ -866,6 +804,85 @@ check_select <- function(.query, .query } +#' Internal function to parse out `select()` queries for occurrences +#' Called both by `check_select()` (above) and by `collect_occurrences_describe()` +#' @noRd +#' @keywords Internal +parse_select_occurrences <- function(.query, + df, + error_call = rlang::caller_env()){ + + # 2. parse groups + group_initial <- .query$request$select$group + # new step to avoid calling `show_all_assertions()` internally + group <- group_initial[group_initial != "assertions"] + if(length(group) > 0){ + group_cols <- purrr::map(group, preset_groups) |> + unlist() + group_names <- tidyselect::eval_select(dplyr::any_of(group_cols), + data = df) |> + names() + # note: technically `group_names` and `group_cols` are identical + # BUT `eval_select()` will fail if invalid columns are given + }else{ + group_names <- NULL + } + + # 3. parse quosures to get list of field names + if(length(.query$request$select$quosure) > 0){ + dot_names <- purrr::map(.query$request$select$quosure, + function(a){ + tidyselect::eval_select(a, + data = df, + error_call = error_call) |> + names() + }) |> + unlist() + }else{ + dot_names <- c() + } + + # 3a: set 'identifier' column name + id_col <- default_columns()[1] + + # 4: set behaviour depending on what names are given + # NOTE: + ## because assertions aren't fields, leaving `fields` empty means default fields are returned + ## but only when `group = assertions` and no other requests are made + ## this adds a single field (recordID) to the query to avoid this problem. + ## This problem also occurs when a single field is requested + ## under some circumstances (e.g. "images"), even when that field is + ## fully populated. + if(length(dot_names) > 1){ + individual_cols <- dot_names + }else{ + if(length(dot_names) == 1){ # i.e. a single field selected + if(length(group_names) == 0){ + individual_cols <- unique(c(id_col, dot_names)) + }else{ + individual_cols <- dot_names + } + }else{ # i.e. length(dot_names) == 0, meaning no fields selected + if(length(group_initial) <= 1 & !any(group_names == id_col)){ + individual_cols <- id_col + }else{ + individual_cols <- NULL + } + } + } + + # 5. merge to create output object + # NOTE: placing `recordID` first is critical; + # having e.g. media columns _before_ `recordID` causes the download to fail + field_values <- unique(c(group_names, individual_cols)) + + if(any(field_values == id_col)){ + field_values <- c(id_col, field_values[field_values != id_col]) # recordID needs to be first + } + field_values +} + + #' Check for valid `type` #' @noRd #' @keywords Internal diff --git a/R/collect_metadata.R b/R/collect_metadata.R index 583ca0bb..af84eada 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -287,7 +287,7 @@ collect_fields <- function(.query){ } # now mutate to required format result_df <- result_df |> - dplyr::mutate(id = result_df$name, + dplyr::mutate(id = .data$name, type = "fields") |> dplyr::rename_with(camel_to_snake_case) |> dplyr::bind_rows(galah_internal_archived$media, diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index 0d6fab68..23d02496 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -172,6 +172,49 @@ collect_occurrences_glimpse_la <- function(.query){ } } +#' collect type `data/occurrences-describe` +#' @noRd +#' @keywords Internal +collect_occurrences_describe <- function(.query){ + + if(!is.null(.query$data)){ + result <- retrieve_internal_data(.query) |> + dplyr::filter(!is.na(.data$id)) + }else{ + result <- query_API(.query) |> + dplyr::bind_rows() |> + dplyr::filter(!is.na(.data$name)) |> + # below here for consistency with `collect_fields()` + dplyr::filter(.data$stored == TRUE) |> + dplyr::mutate(id = .data$name) |> + dplyr::rename_with(camel_to_snake_case) |> + dplyr::bind_rows(galah_internal_archived$media, + galah_internal_archived$other) |> + dplyr::distinct(.data$id, .keep_all = TRUE) + + # add caching here + result_df <- update_attributes(result, type = "fields") + update_cache(fields = result_df) + } + + # create a 'fake' data.frame to run .query$select on + df <- matrix(data = NA, + nrow = 0, + ncol = nrow(result), + dimnames = list(c(), + dplyr::pull(result, .data$id))) |> + as.data.frame() + + # use this df to parse user-requested `select()` function` + keep_names <- parse_select_occurrences(.query, df) + + # keep only those rows that correspond to user-requested names + result |> + dplyr::filter(.data$id %in% keep_names) |> + dplyr::select("id", "description", "data_type") + +} + #' Download failed message #' @noRd #' @keywords Internal diff --git a/R/describe.R b/R/describe.R new file mode 100644 index 00000000..176924a6 --- /dev/null +++ b/R/describe.R @@ -0,0 +1,41 @@ +#' Describe what fields are available +#' +#' Unlike a local `tibble`, where printing to the console shows what fields +#' are available, when working remotely there is no way to know what fields +#' are present that can be queried. This function name is borrowed from SQL, +#' where it is used to give a read-out of fields that are in the source +#' database. `r lifecycle::badge("experimental")` +#' @param x An object of class `data_request` +#' @param ... Other arguments, currently ignored +#' @name describe +#' @seealso \code{\link[=glimpse.data_request]{glimpse()}} for a different +#' way to view results of a query; [show_all_fields()] for full metadata +#' on available fields; \code{\link[=distinct.data_request]{distinct()}} +#' for showing the values *within* a given field. +#' @order 1 +#' @returns A `tibble` showing the `id`, `description` and `data_type` +#' for all fields, or if `select()` is called, then those fields +#' requested by the user. +#' @examples \dontrun{ +#' # By default, this shows all fields in the source system +#' galah_call() |> +#' describe() |> +#' collect() +#' +#' # If `select()` is called, only requested fields are shown +#' galah_call() |> +#' select(group = "basic") |> +#' describe() |> +#' collect() +#' } +#' @export +describe <- function(x, ...){ + UseMethod("describe") +} + +#' @rdname describe +#' @order 2 +#' @export +describe.data_request <- function(x, ...){ + update_request_object(x, describe = TRUE) +} \ No newline at end of file diff --git a/R/dplyr-collect.R b/R/dplyr-collect.R index 1487a6af..5b8fe112 100644 --- a/R/dplyr-collect.R +++ b/R/dplyr-collect.R @@ -117,6 +117,7 @@ collect.computed_query <- function(x, "data/occurrences" = collect_occurrences(x, wait = wait, file = file), "data/occurrences-count" = collect_occurrences_count(x), "data/occurrences-count-groupby" = collect_occurrences_count(x), + "data/occurrences-describe" = collect_occurrences_describe(x), "data/occurrences-doi" = collect_occurrences_doi(x, file = file), "data/occurrences-glimpse" = collect_occurrences_glimpse(x), "data/species"= collect_species(x, file = file), diff --git a/R/dplyr-glimpse.R b/R/dplyr-glimpse.R index f71d987b..510a9b60 100644 --- a/R/dplyr-glimpse.R +++ b/R/dplyr-glimpse.R @@ -12,6 +12,8 @@ #' sent to the server, then returning a novel object class with it's own #' [print()] method. #' @name glimpse.data_request +#' @seealso [describe()] for a different way to see fields returned by a query; +#' [show_all_fields()] to see all available fields. #' @examples \dontrun{ #' galah_call() |> #' filter(year >= 2019, diff --git a/_pkgdown.yml b/_pkgdown.yml index d214d9fc..0de478e2 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -56,6 +56,7 @@ reference: - arrange.data_request - authenticate - count.data_request + - describe - distinct.data_request - filter.data_request - geolocate diff --git a/man/describe.Rd b/man/describe.Rd new file mode 100644 index 00000000..9185957c --- /dev/null +++ b/man/describe.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/describe.R +\name{describe} +\alias{describe} +\alias{describe.data_request} +\title{Describe what fields are available} +\usage{ +describe(x, ...) + +\method{describe}{data_request}(x, ...) +} +\arguments{ +\item{x}{An object of class \code{data_request}} + +\item{...}{Other arguments, currently ignored} +} +\value{ +A \code{tibble} showing the \code{id}, \code{description} and \code{data_type} +for all fields, or if \code{select()} is called, then those fields +requested by the user. +} +\description{ +Unlike a local \code{tibble}, where printing to the console shows what fields +are available, when working remotely there is no way to know what fields +are present that can be queried. This function name is borrowed from SQL, +where it is used to give a read-out of fields that are in the source +database. \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} +} +\examples{ +\dontrun{ +# By default, this shows all fields in the source system +galah_call() |> + describe() |> + collect() + +# If `select()` is called, only requested fields are shown +galah_call() |> + select(group = "basic") |> + describe() |> + collect() +} +} +\seealso{ +\code{\link[=glimpse.data_request]{glimpse()}} for a different +way to view results of a query; \code{\link[=show_all_fields]{show_all_fields()}} for full metadata +on available fields; \code{\link[=distinct.data_request]{distinct()}} +for showing the values \emph{within} a given field. +} diff --git a/man/glimpse.data_request.Rd b/man/glimpse.data_request.Rd index 8590077b..57d7701d 100644 --- a/man/glimpse.data_request.Rd +++ b/man/glimpse.data_request.Rd @@ -36,3 +36,7 @@ galah_call() |> collect() } } +\seealso{ +\code{\link[=describe]{describe()}} for a different way to see fields returned by a query; +\code{\link[=show_all_fields]{show_all_fields()}} to see all available fields. +} diff --git a/tests/testthat/test-describe.R b/tests/testthat/test-describe.R new file mode 100644 index 00000000..5cc4e253 --- /dev/null +++ b/tests/testthat/test-describe.R @@ -0,0 +1,43 @@ +test_that("`describe()` amends a query to type = 'occurrences-describe'", { + x <- galah_call() |> + describe() |> + capture() + expect_equal(x$type, "data/occurrences-describe") + expect_s3_class(x, c("query", "list")) +}) + +test_that("`describe()` ignores `count()`", { + x <- galah_call() |> + describe() |> + count() |> + capture() + expect_equal(x$type, "data/occurrences-describe") + expect_s3_class(x, c("query", "list")) +}) + +test_that("`describe()` returns all fields by default", { + x <- galah_call() |> + describe() |> + collect() + expect_gt(nrow(x), 300) + expect_s3_class(x, c("tbl", "tbl_df", "data.frame")) +}) + +test_that("`describe()` adheres to user-provided `select()`", { + x <- galah_call() |> + select(group = "basic") |> + describe() |> + collect() + y <- default_columns() + expect_true(all(x$name %in% y)) + expect_s3_class(x, c("tbl", "tbl_df", "data.frame")) +}) + +test_that("`describe()` works for `select(type = 'media')`", { + x <- galah_call() |> + select(group = "media") |> + describe() |> + collect() + expect_gt(nrow(x), 2) + expect_s3_class(x, c("tbl", "tbl_df", "data.frame")) +}) \ No newline at end of file diff --git a/vignettes/quick_start_guide.Rmd b/vignettes/quick_start_guide.Rmd index cbd901bf..b92ade7e 100644 --- a/vignettes/quick_start_guide.Rmd +++ b/vignettes/quick_start_guide.Rmd @@ -1,7 +1,7 @@ --- title: "Quick start guide" author: "Martin Westgate & Dax Kellie" -date: '2026-02-11' +date: '2026-02-23' output: rmarkdown::html_vignette vignette: > @@ -63,33 +63,116 @@ chosen organisation. These pipes differ from traditional syntax in two ways: - they begin with a function - usually `galah_call()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` -So an example query might be to find the number of records per year: +So an example query might be to find the number of records: ``` r galah_config(atlas = "Australia") galah_call() |> # open a pipe - filter(year >= 2020) |> # choose rows to keep - count(year) |> # count the number of rows + count() |> # count the number of rows collect() # retrieve query from the server ``` ``` -## # A tibble: 7 × 2 -## year count -## -## 1 2024 11889930 -## 2 2023 11007491 -## 3 2022 9430065 -## 4 2025 9142677 -## 5 2021 8695248 -## 6 2020 7311836 -## 7 2026 309836 +## # A tibble: 1 × 1 +## count +## +## 1 167207256 +``` +You can further narrow this to a single taxonomic group with `identify()`: + + +``` r +galah_call() |> + identify("Eolophus roseicapilla") |> + count() |> + collect() +``` + +``` +## # A tibble: 1 × 1 +## count +## +## 1 1410911 +``` + +To choose records using non-taxonomic criteria, you'll need to use `filter()`. +This reqires that you find out: + +- what **fields** (columns) are present in the dataset you are searching +- what **values** exist for those fields + +You can answer the first question with `describe()`: + + +``` r +galah_call() |> + describe() |> + collect() +``` + +``` +## # A tibble: 639 × 3 +## id description data_type +## +## 1 abcdTypeStatus string +## 2 acceptedNameUsage Accepted name string +## 3 acceptedNameUsageID Accepted name string +## 4 accessRights Access rights string +## 5 annotationsDoi string +## 6 annotationsUid Referenced by publication string +## 7 assertionUserId Assertions by user string +## 8 assertions Record issues string +## 9 assertionsCount int +## 10 associatedMedia Associated Media string +## # ℹ 629 more rows +``` + +Once you find a variable you are interested in, see what values it contains +using `distinct()`: + + +``` r +galah_call() |> + distinct(basisOfRecord) |> + collect() +``` + +``` +## # A tibble: 9 × 1 +## basisOfRecord +## +## 1 HUMAN_OBSERVATION +## 2 PRESERVED_SPECIMEN +## 3 OCCURRENCE +## 4 MACHINE_OBSERVATION +## 5 OBSERVATION +## 6 MATERIAL_SAMPLE +## 7 LIVING_SPECIMEN +## 8 FOSSIL_SPECIMEN +## 9 MATERIAL_CITATION +``` + +Then you can combine these within a `filter()` query: + + +``` r +galah_call() |> + filter(basisOfRecord == "PRESERVED_SPECIMEN") |> + count() |> + collect() +``` + +``` +## # A tibble: 1 × 1 +## count +## +## 1 18055082 ``` -Or to find the number of categories present in a dataset, for example how many -species are present: +These functions can be combined in a number of ways, for example to +learn how many species are present within the genus 'Crinia': ``` r @@ -107,7 +190,7 @@ galah_call() |> ## 1 17 ``` -You can 'glimpse' a data download before you run it, to check all the data +You can `glimpse()` a data download before you run it, to check all the data you need is included: @@ -122,14 +205,14 @@ galah_call() |> ``` ## Rows: 21,984 ## Columns: 8 -## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au… -## $ eventDate 1.272672e+12, 1.289002e+12, 1.291014e+12 +## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/9b4ad5… +## $ eventDate 1.285459e+12, 1.280189e+12, 1.287533e+12 ## $ scientificName "Eolophus roseicapilla", "Eolophus roseicapilla", "Eolophus roseicapilla" -## $ decimalLatitude -25.98833, -37.83032, -35.41707 -## $ decimalLongitude 152.0442, 144.9812, 138.6868 -## $ basisOfRecord "HUMAN_OBSERVATION", "HUMAN_OBSERVATION", "HUMAN_OBSERVATION" -## $ dataResourceName "BirdLife Australia, Birdata", "eBird Australia", "eBird Australia" -## $ occurrenceStatus "PRESENT", "ABSENT", "ABSENT" +## $ decimalLatitude -37.1500, -18.6998, -32.6300 +## $ decimalLongitude 143.7500, 127.6872, 138.0900 +## $ basisOfRecord "HUMAN_OBSERVATION", "MATERIAL_SAMPLE", "HUMAN_OBSERVATION" +## $ dataResourceName "BirdLife Australia, Birdata", "Australian National Wildlife Collection provider for OZCAM", "BirdLife Australia, Birdata" +## $ occurrenceStatus "PRESENT", "PRESENT", "PRESENT" ``` And, once satisfied that your parameters are correct, download the records @@ -152,13 +235,13 @@ galah_call() |> ## ## 1 NA -36.5 Eolophus roseicapilla ## 2 NA -38.2 Eolophus roseicapilla -## 3 NA -37.0 Eolophus roseicapilla -## 4 NA -37.7 Eolophus roseicapilla -## 5 NA -35.6 Eolophus roseicapilla -## 6 NA -31.1 Eolophus roseicapilla +## 3 NA -36.1 Eolophus roseicapilla +## 4 NA -38.3 Eolophus roseicapilla +## 5 NA -38.2 Eolophus roseicapilla +## 6 NA -38.3 Eolophus roseicapilla ## 7 NA -38.2 Eolophus roseicapilla ## 8 NA -38.2 Eolophus roseicapilla -## 9 NA -38.2 Eolophus roseicapilla +## 9 NA -38.3 Eolophus roseicapilla ## 10 NA -38.2 Eolophus roseicapilla ## # ℹ 21,974 more rows ``` @@ -182,6 +265,7 @@ Supported `dplyr` verbs that modify queries are as follows: Additional verbs are: - `apply_profile()` +- `describe()` - `geolocate()` or `st_crop.data_request()` - `identify.data_request()` - `unnest()` @@ -192,70 +276,6 @@ DOI for your data. See the [download data reproducibly](download-data-reproducibly.html) vignette for details. -# Finding information - -Building queries using `filter()` requires that you know two things: - -- what **fields** (columns) are present in the dataset you are searching -- what **values** exist for those fields - -Finding this information requires looking for metadata: - - -``` r -request_metadata(type = "fields") |> - collect() -``` - -``` -## # A tibble: 639 × 3 -## id description type -## -## 1 abcdTypeStatus fields -## 2 acceptedNameUsage Accepted name fields -## 3 acceptedNameUsageID Accepted name fields -## 4 accessRights Access rights fields -## 5 annotationsDoi fields -## 6 annotationsUid Referenced by publication fields -## 7 assertionUserId Assertions by user fields -## 8 assertions Record issues fields -## 9 assertionsCount fields -## 10 associatedMedia Associated Media fields -## # ℹ 629 more rows -``` - -You can browser this tibble using `View()` or search it using `filter()`. -Once you have found a field that you want to include in your query, you -can find values for that field using `unnest()`: - - -``` r -request_metadata() |> - filter(fields == "cl22") |> - unnest() |> - collect() -``` - -``` -## # A tibble: 11 × 1 -## cl22 -## -## 1 New South Wales -## 2 Victoria -## 3 Queensland -## 4 South Australia -## 5 Western Australia -## 6 Northern Territory -## 7 Tasmania -## 8 Australian Capital Territory -## 9 Macquarie Island -## 10 Coral Sea Islands -## 11 Ashmore and Cartier Islands -``` - -Different types of metadata are available; see `?request_metadata` for -a full list. - # Wrapper functions While `dplyr` syntax is very flexible, there are cases where it is easier @@ -277,7 +297,7 @@ galah_call() |> ## # A tibble: 1 × 1 ## count ## -## 1 9142677 +## 1 9238597 ``` Or occurrences: @@ -294,23 +314,22 @@ galah_call() |> ``` ## # A tibble: 2,032 × 9 -## recordID scientificName taxonConceptID decimalLatitude decimalLongitude eventDate basisOfRecord occurrenceStatus -## -## 1 0026d29f-b6ab-4… Eolophus rose… https://biodi… -35.4 149. 2000-08-07 00:00:00 HUMAN_OBSERV… PRESENT -## 2 0062d446-007b-4… Eolophus rose… https://biodi… -35.3 149. 2000-03-10 00:00:00 HUMAN_OBSERV… PRESENT -## 3 00a62ee0-1e08-4… Eolophus rose… https://biodi… -35.2 149. 2000-01-29 00:00:00 HUMAN_OBSERV… PRESENT -## 4 00ab2f4d-326f-4… Eolophus rose… https://biodi… -35.4 149. 2000-09-25 00:00:00 HUMAN_OBSERV… PRESENT -## 5 00ae4631-ea59-4… Eolophus rose… https://biodi… -35.3 149. 2000-02-12 00:00:00 HUMAN_OBSERV… PRESENT -## 6 00b6c8ec-e7b9-4… Eolophus rose… https://biodi… -35.2 149. 2000-02-05 00:00:00 HUMAN_OBSERV… PRESENT +## recordID scientificName taxonConceptID decimalLatitude decimalLongitude eventDate basisOfRecord occurrenceStatus dataResourceName +## +## 1 0026d29f-b6ab-4a1d-9c57-6ee12cfde3a0 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.4 149. 2000-08-07 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… +## 2 0062d446-007b-4164-ac4f-ae84297e2578 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.3 149. 2000-03-10 00:00:00 HUMAN_OBSERV… PRESENT BirdLife Austra… +## 3 00a62ee0-1e08-4114-b0d8-9b7905472d53 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.2 149. 2000-01-29 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… +## 4 00ab2f4d-326f-4b01-9a8a-1a10c1f77e3c Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.4 149. 2000-09-25 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… +## 5 00ae4631-ea59-44ec-b8f8-4377b6b3b3ef Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.3 149. 2000-02-12 00:00:00 HUMAN_OBSERV… PRESENT BirdLife Austra… +## 6 00b6c8ec-e7b9-4d9f-9638-d962b1b4acfa Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.2 149. 2000-02-05 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… ## # ℹ 2,026 more rows -## # ℹ 1 more variable: dataResourceName ``` `atlas_species()` replaces the need for `distinct()` call, while `atlas_media()` is a shortcut to a complex workflow that incorporates both data and metadata calls. Finally, metadata calls can be made more efficiently using the `show_all()` and `show_values()` functions. These take the same arguments as the `type` -argument in `request_metadata()`, but use non-standard evaluation, so they +argument in `request_metadata()`, but us non-standard evaluation, so they don't require quotes. They are also evaluated immediately rather than lazily: diff --git a/vignettes/quick_start_guide.Rmd.orig b/vignettes/quick_start_guide.Rmd.orig index 6d540347..3dcde669 100644 --- a/vignettes/quick_start_guide.Rmd.orig +++ b/vignettes/quick_start_guide.Rmd.orig @@ -59,21 +59,60 @@ chosen organisation. These pipes differ from traditional syntax in two ways: - they begin with a function - usually `galah_call()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` -So an example query might be to find the number of records per year: +So an example query might be to find the number of records: ```{r, count-example} galah_config(atlas = "Australia") galah_call() |> # open a pipe - filter(year >= 2020) |> # choose rows to keep - count(year) |> # count the number of rows + count() |> # count the number of rows collect() # retrieve query from the server ``` +You can further narrow this to a single taxonomic group with `identify()`: -Or to find the number of categories present in a dataset, for example how many -species are present: +```{r, identify-example} +galah_call() |> + identify("Eolophus roseicapilla") |> + count() |> + collect() +``` + +To choose records using non-taxonomic criteria, you'll need to use `filter()`. +This reqires that you find out: + +- what **fields** (columns) are present in the dataset you are searching +- what **values** exist for those fields + +You can answer the first question with `describe()`: + +```{r, describe-example} +galah_call() |> + describe() |> + collect() +``` + +Once you find a variable you are interested in, see what values it contains +using `distinct()`: ```{r, distinct-example} +galah_call() |> + distinct(basisOfRecord) |> + collect() +``` + +Then you can combine these within a `filter()` query: + +```{r, count-filter-example} +galah_call() |> + filter(basisOfRecord == "PRESERVED_SPECIMEN") |> + count() |> + collect() +``` + +These functions can be combined in a number of ways, for example to +learn how many species are present within the genus 'Crinia': + +```{r, species-count-example} galah_call() |> identify("Crinia") |> # filters by taxonomic names distinct(speciesID) |> # keep only unique values @@ -81,7 +120,7 @@ galah_call() |> collect() ``` -You can 'glimpse' a data download before you run it, to check all the data +You can `glimpse()` a data download before you run it, to check all the data you need is included: ```{r, glimpse-example} @@ -127,6 +166,7 @@ Supported `dplyr` verbs that modify queries are as follows: Additional verbs are: - `apply_profile()` +- `describe()` - `geolocate()` or `st_crop.data_request()` - `identify.data_request()` - `unnest()` @@ -137,34 +177,6 @@ DOI for your data. See the [download data reproducibly](download-data-reproducibly.html) vignette for details. -# Finding information - -Building queries using `filter()` requires that you know two things: - -- what **fields** (columns) are present in the dataset you are searching -- what **values** exist for those fields - -Finding this information requires looking for metadata: - -```{r, metadata-fields} -request_metadata(type = "fields") |> - collect() -``` - -You can browser this tibble using `View()` or search it using `filter()`. -Once you have found a field that you want to include in your query, you -can find values for that field using `unnest()`: - -```{r, metadata-fields-unnest} -request_metadata() |> - filter(fields == "cl22") |> - unnest() |> - collect() -``` - -Different types of metadata are available; see `?request_metadata` for -a full list. - # Wrapper functions While `dplyr` syntax is very flexible, there are cases where it is easier @@ -196,7 +208,7 @@ galah_call() |> is a shortcut to a complex workflow that incorporates both data and metadata calls. Finally, metadata calls can be made more efficiently using the `show_all()` and `show_values()` functions. These take the same arguments as the `type` -argument in `request_metadata()`, but use non-standard evaluation, so they +argument in `request_metadata()`, but us non-standard evaluation, so they don't require quotes. They are also evaluated immediately rather than lazily: ```{r, show-all-fields} From 59a9375f9d1ebe2423c89a4454656815b20361c8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 17 Mar 2026 15:52:09 +1100 Subject: [PATCH 04/70] Further progress on events --- R/capture.R | 7 ++++--- R/capture_events.R | 19 +++++++++++++++++++ R/check.R | 2 +- R/collapse_checks.R | 24 +++++++++++++++++++++--- R/collapse_events.R | 1 + R/collect_events.R | 21 ++++++++++++++++++++- R/collect_metadata_unnest.R | 5 +++++ R/dplyr-collect.R | 1 + tests/testthat/test-atlas_events.R | 10 ++++++++++ 9 files changed, 82 insertions(+), 8 deletions(-) diff --git a/R/capture.R b/R/capture.R index 22671289..2413110e 100644 --- a/R/capture.R +++ b/R/capture.R @@ -81,6 +81,7 @@ capture.data_request <- function(x, "distributions" = capture_distributions_data(x), "events" = capture_events(x), "events-count" = capture_events_count(x), + "events-describe" = capture_events_describe(x), cli::cli_abort("Unrecognised 'type'")) |> add_request(x) } @@ -190,11 +191,11 @@ check_doi <- function(x){ #' @keywords Internal check_describe <- function(x){ if(!is.null(x$describe)){ - if(x$type == "occurrences"){ - x$type <- "occurrences-describe" + if(x$type %in% c("occurrences", "events")){ + x$type <- glue::glue("{x$type}-describe") x }else{ - cli::cli_inform("`describe()` is only supported for `type =\"occurrences\"") + cli::cli_inform("`describe()` is only supported for `type` \"occurrences\" or \"events\" ") x } }else{ diff --git a/R/capture_events.R b/R/capture_events.R index 15b45880..f9d0596e 100644 --- a/R/capture_events.R +++ b/R/capture_events.R @@ -1,8 +1,14 @@ +#' Internal function to run `capture()` for type = "events" +#' @noRd +#' @keywords Internal capture_events <- function(x, error_call = rlang::caller_env()){ browser() } +#' Internal function to run `capture()` for type = "events-count" +#' @noRd +#' @keywords Internal capture_events_count <- function(x, error_call = rlang::caller_env()){ @@ -25,4 +31,17 @@ capture_events_count <- function(x, `Content-Type` = "application/json"), body = predicates_info) |> as_prequery() +} + +#' Internal function to run `capture()` for type = "events-describe" +#' @noRd +#' @keywords Internal +capture_events_describe <- function(x, + error_call = rlang::caller_env()){ + list(type = "data/events-describe", + url = url_lookup("data/events-count"), + headers = c(build_headers(), + `Content-Type` = "application/json"), + body = "query { __schema { types { name kind description } } }") |> + as_query() } \ No newline at end of file diff --git a/R/check.R b/R/check.R index e78a013f..c977df7e 100644 --- a/R/check.R +++ b/R/check.R @@ -41,7 +41,7 @@ check_authentication <- function(x){ if(atlas != "Australia" & !is.null(x$authenticate)){ cli::cli_warn("Authentication not supported for atlas {atlas}: skipping") - x$authenticate <- NULL + x$authenticate <- NULL } x } diff --git a/R/collapse_checks.R b/R/collapse_checks.R index 56c6f2d3..56ecbdd8 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -8,7 +8,8 @@ collapse_build_checks <- function(.query){ names_vec <- purrr::map(.query, \(a){purrr::pluck(a, "type")}) |> unlist() - data_lookup <- stringr::str_detect(names_vec, "^data") + data_lookup <- stringr::str_detect(names_vec, "^data") & + names_vec != "data/occurrences-describe" if(any(data_lookup)){ data_names <- names_vec[data_lookup] # parse any `metadata` @@ -45,7 +46,8 @@ collapse_run_checks <- function(.query, error_call = rlang::caller_env()){ # "data/" functions require pre-processing of metadata, - if(stringr::str_detect(.query$type, "^data/") & .query$type != "data/occurrences-doi"){ + if(stringr::str_detect(.query$type, "^data/") & + !(.query$type %in% c("data/occurrences-doi", "data/occurrences-describe"))){ # taxon concept ID must always be evaluated .query <- check_identifiers(.query, error_call) # login should only be evaluated for species and occurrence @@ -72,8 +74,13 @@ collapse_run_checks <- function(.query, # run remaining checks, if requested by the user if(potions::pour("package", "run_checks")) { + # events doesn't need a reason, but others do + if(.query$type %in% c("data/occurrences", "data/species")){ + .query <- .query |> + check_reason(error_call) + } + # all need fields and profiles .query <- .query |> - check_reason(error_call) |> check_fields(error_call) |> check_profiles(error_call) } @@ -84,6 +91,17 @@ collapse_run_checks <- function(.query, check_identifiers() |> check_fields() } + + # add an exception to retain to user-supplied taxon for child taxa queries + # this is useful because it allows us to retain parent-child relations + # in the resulting tibble + if(.query$type == "metadata/taxa-unnest"){ + supplied_taxon <- names(.query) == "metadata/taxa-single" + if(any(supplied_taxon)){ + names(.query)[supplied_taxon] <- "supplied_taxon" + } + } + collapse_remove_metadata(.query) # special cases: # distributions diff --git a/R/collapse_events.R b/R/collapse_events.R index 9914ade7..0f7e7efc 100644 --- a/R/collapse_events.R +++ b/R/collapse_events.R @@ -4,6 +4,7 @@ collapse_events <- function(x){ browser() } +#' Internal function to run `capture()` for type = "events" #' @noRd #' @keywords Internal collapse_events_count <- function(x){ diff --git a/R/collect_events.R b/R/collect_events.R index 69a2acff..8fb6227e 100644 --- a/R/collect_events.R +++ b/R/collect_events.R @@ -1,9 +1,11 @@ +#' Internal function for calling `collect()` on type = "events" #' @noRd #' @keywords Internal collect_events <- function(.query, wait = TRUE, file = NULL){ browser() } +#' Internal function for calling `collect()` on type = "events-count" #' @noRd #' @keywords Internal collect_events_count <- function(.query){ @@ -13,4 +15,21 @@ collect_events_count <- function(.query){ tibble::tibble(count = unlist(result)) # purrr::pluck(result, "data", "eventSearch", "documents", "total") -} \ No newline at end of file +} + +#' Internal function for calling `collect()` on type = "events-describe" +#' @noRd +#' @keywords Internal +collect_events_describe <- function(.query){ + + # get response from API + result <- query_API(.query) + + # parse to a format consistent with `collect_occurrences_describe()` + result |> + purrr::pluck("data", "__schema", "types") |> + dplyr::bind_rows() |> + dplyr::rename("id" = .data$name, + "data_type" = .data$kind) |> + dplyr::relocate(.data$description, .before = .data$data_type) +} diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index b42ba151..1f461572 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -123,4 +123,9 @@ collect_taxa_unnest <- function(.query){ dplyr::rename_with(camel_to_snake_case) |> parse_rename(.query) |> parse_select(.query) + + ## if useful to retain supplied taxon, do so here + # supplied_df <- .query$supplied_taxon |> + # dplyr::select(tidyselect::any_of(colnames(result_df))) + # dplyr::bind_rows(supplied_df, result_df) } diff --git a/R/dplyr-collect.R b/R/dplyr-collect.R index 5b8fe112..d2a88175 100644 --- a/R/dplyr-collect.R +++ b/R/dplyr-collect.R @@ -114,6 +114,7 @@ collect.computed_query <- function(x, "data/distributions" = collect_distributions(x), "data/events" = collect_events(x, wait = wait, file = file), "data/events-count" = collect_events_count(x), + "data/events-describe" = collect_events_describe(x), "data/occurrences" = collect_occurrences(x, wait = wait, file = file), "data/occurrences-count" = collect_occurrences_count(x), "data/occurrences-count-groupby" = collect_occurrences_count(x), diff --git a/tests/testthat/test-atlas_events.R b/tests/testthat/test-atlas_events.R index fa026d83..2d79efe6 100644 --- a/tests/testthat/test-atlas_events.R +++ b/tests/testthat/test-atlas_events.R @@ -12,4 +12,14 @@ test_that("`count()` works for type = 'events'", { expect_equal(nrow(x), 1) expect_equal(ncol(x), 1) expect_true(is.integer(x$count)) +}) + +test_that("`describe()` works for type = 'events'", { + x <- galah_call(type = "events") |> + describe() |> + collect() + expect_gt(nrow(x), 50) + expect_equal(ncol(x), 3) + expect_equal(colnames(x), + c("id", "description", "data_type")) }) \ No newline at end of file From 94abfbf2847c11249e6ca67da47ca698e3306ccd Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 17 Mar 2026 16:19:42 +1100 Subject: [PATCH 05/70] Ensure functions work with `galah::` syntax without first calling `library()` (#294) Solution was to keep user messages in `.onAttach` as per `R CMD check` requirements, but put `potions` dependencies in `.onLoad`. Also added `.onUnload` for completeness --- R/onload.R | 85 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/R/onload.R b/R/onload.R index caa431f5..5e30f4aa 100644 --- a/R/onload.R +++ b/R/onload.R @@ -1,42 +1,57 @@ -#' Set-up for galah during loading +#' Set up for galah when `loadNamespace` is called #' @noRd #' @keywords Internal -.onAttach <- function(libname, pkgname) { - if (pkgname == "galah") { +.onLoad <- function(libname, pkgname) { + if (pkgname == "galah") { + # set up storage of standard information via {potions} + potions::brew(.pkg = "galah") # set up caching of behaviour + quiet_config <- purrr::quietly(galah_config) + config_info <- quiet_config() # to cache defaults without raising a message + } +} - # set up storage of standard information via {potions} - potions::brew(.pkg = "galah") # set up caching of behaviour - quiet_config <- purrr::quietly(galah_config) - config_info <- quiet_config() # to cache defaults without raising a message +#' Remove galah-specific information when `unloadNamespace` is called +#' @noRd +#' @keywords Internal +.onUnload <- function(libpath) { + potions::drain_package("galah") +} + +#' Set-up for galah when `library()` is called +#' @noRd +#' @keywords Internal +.onAttach <- function(libname, pkgname) { + if (pkgname == "galah") { - # get information to display to the user - ## get the galah version, if we can - galah_version <- "version unknown" - suppressWarnings( - try(galah_version <- utils::packageDescription("galah")[["Version"]], - silent = TRUE)) + # get information to display to the user + ## get the galah version, if we can + galah_version <- "version unknown" + suppressWarnings( + try(galah_version <- utils::packageDescription("galah")[["Version"]], + silent = TRUE)) - # show currently-selected atlas - current_node <- potions::pour("atlas", .pkg = "galah") |> - purrr::pluck("acronym") - current_url <- show_all_atlases() |> - dplyr::filter(.data$acronym == current_node) |> - dplyr::pull("url") |> - stringr::str_replace("^https://", "") + # show currently-selected atlas + current_node <- potions::pour("atlas", .pkg = "galah") |> + purrr::pluck("acronym") + current_url <- show_all_atlases() |> + dplyr::filter(.data$acronym == current_node) |> + dplyr::pull("url") |> + stringr::str_replace("^https://", "") - # display a message - # NOTE: This message *must* have the following classes to enable them - # to be controlled programmatically. - # see ?packageStartupMessage (required by `check()`) - c( - glue::glue("galah version {galah_version}"), - "\n", # note: glue wipes newlines, so have to be outside - cli::col_magenta(glue::glue('This package is currently configured to query {current_node} ({current_url}).\n')), - "\n", - i = cli::col_magenta('- You can change this at any time using e.g. `galah_config(atlas = \"GBIF\")`.'), - "\n", - i = cli::col_magenta('- To see all supported organisations, run `show_all(atlases)`.') - ) |> - packageStartupMessage() - } + # display a message + # NOTE: This message *must* have the following classes to enable them + # to be controlled programmatically. + # see ?packageStartupMessage (required by `check()`) + c( + glue::glue("galah version {galah_version}"), + "\n", # note: glue wipes newlines, so have to be outside + cli::col_magenta(glue::glue('This package is currently configured to query {current_node} ({current_url}).\n')), + "\n", + i = cli::col_magenta('- You can change this at any time using e.g. `galah_config(atlas = \"GBIF\")`.'), + "\n", + i = cli::col_magenta('- To see all supported organisations, run `show_all(atlases)`.') + ) |> + packageStartupMessage() + } } + \ No newline at end of file From 72310aec23446a8b48df501c9b703838976919c8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 17 Mar 2026 16:20:16 +1100 Subject: [PATCH 06/70] Iterate version number, add skip-tests when offline --- DESCRIPTION | 2 +- tests/testthat/test-atlas_events.R | 3 +++ tests/testthat/test-describe.R | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index b8a98704..b9c76085 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: galah Type: Package Title: Biodiversity Data from the GBIF Node Network -Version: 2.2.0 +Version: 2.2.1.9999 Authors@R: c(person(given = "Martin", family = "Westgate", diff --git a/tests/testthat/test-atlas_events.R b/tests/testthat/test-atlas_events.R index 2d79efe6..c563ba22 100644 --- a/tests/testthat/test-atlas_events.R +++ b/tests/testthat/test-atlas_events.R @@ -1,4 +1,5 @@ test_that("`galah_call(type = 'events')` works", { + skip_if_offline(); skip_on_ci() x <- galah_call(type = "events") expect_equal(names(x), "type") expect_equal(x$type, "events") @@ -6,6 +7,7 @@ test_that("`galah_call(type = 'events')` works", { }) test_that("`count()` works for type = 'events'", { + skip_if_offline(); skip_on_ci() x <- galah_call(type = "events") |> count() |> collect() @@ -15,6 +17,7 @@ test_that("`count()` works for type = 'events'", { }) test_that("`describe()` works for type = 'events'", { + skip_if_offline(); skip_on_ci() x <- galah_call(type = "events") |> describe() |> collect() diff --git a/tests/testthat/test-describe.R b/tests/testthat/test-describe.R index 5cc4e253..f71d8fb0 100644 --- a/tests/testthat/test-describe.R +++ b/tests/testthat/test-describe.R @@ -1,4 +1,5 @@ test_that("`describe()` amends a query to type = 'occurrences-describe'", { + skip_if_offline(); skip_on_ci() x <- galah_call() |> describe() |> capture() @@ -7,6 +8,7 @@ test_that("`describe()` amends a query to type = 'occurrences-describe'", { }) test_that("`describe()` ignores `count()`", { + skip_if_offline(); skip_on_ci() x <- galah_call() |> describe() |> count() |> @@ -16,6 +18,7 @@ test_that("`describe()` ignores `count()`", { }) test_that("`describe()` returns all fields by default", { + skip_if_offline(); skip_on_ci() x <- galah_call() |> describe() |> collect() @@ -24,6 +27,7 @@ test_that("`describe()` returns all fields by default", { }) test_that("`describe()` adheres to user-provided `select()`", { + skip_if_offline(); skip_on_ci() x <- galah_call() |> select(group = "basic") |> describe() |> @@ -34,6 +38,7 @@ test_that("`describe()` adheres to user-provided `select()`", { }) test_that("`describe()` works for `select(type = 'media')`", { + skip_if_offline(); skip_on_ci() x <- galah_call() |> select(group = "media") |> describe() |> From 821308a2f544902d474d0966fc083c00fbd53f52 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:14:17 +1100 Subject: [PATCH 07/70] Re-add list metadata columns #293 --- R/utilities_internal.R | 4 +++- tests/testthat/test-show_all.R | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 969d0112..caaa9eea 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -100,7 +100,9 @@ lookup_select_columns <- function(type) { "list_name", "description", "list_type", - "item_count"), + "item_count", + "is_authoritative", + "is_threatened"), "lists-unnest" = c("scientific_name", "vernacular_name", "taxon_concept_id"), diff --git a/tests/testthat/test-show_all.R b/tests/testthat/test-show_all.R index 89684ce9..8ea4d0b6 100644 --- a/tests/testthat/test-show_all.R +++ b/tests/testthat/test-show_all.R @@ -57,4 +57,17 @@ test_that("`show_all_lists()` functions work correctly", { expect_equal(attributes(syntax1)$call, "lists") expect_equal(attributes(syntax1)$region, "Australia") expect_equal(nrow(limit_test), 3) -}) \ No newline at end of file +}) + +test_that("show_all(lists) returns additional metadata columns", { + skip_if_offline(); skip_on_ci() + quiet_lists <- function(...){ + list_fun <- purrr::quietly(show_all_lists) + list_fun(...) |> + purrr::pluck("result") + } + syntax1 <- quiet_lists() + limit_test <- quiet_lists(limit = 3) + expected_cols <- c("is_authoritative", "is_threatened") + expect_in(expected_cols, colnames(limit_test)) +}) From 711685d8f237a56044d8b1517d26b6e41ffa41c5 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 27 Mar 2026 14:01:26 +1100 Subject: [PATCH 08/70] First pass at supporting `from` argument in `request_data()` (#297) This requires the choice of atlas to be locked in at the `request_data()` stage and then passed to all later functions; we can't assume the global stage is true any more. So this is a bit of a messy change, but one that makes queries more complete and reliable. --- R/atlas_media.R | 20 +-- R/atlas_species.R | 2 +- R/atlas_taxonomy.R | 6 +- R/build_query.R | 29 ++-- R/capture.R | 6 +- R/capture_events.R | 21 ++- R/capture_files.R | 13 +- R/capture_metadata.R | 162 +++++++++--------- R/capture_metadata_unnest.R | 26 ++- R/capture_occurrences.R | 66 ++++---- R/capture_occurrences_count.R | 84 ++++------ R/capture_species.R | 20 ++- R/capture_species_count.R | 47 +++--- R/capture_taxa.R | 32 ++-- R/check.R | 37 ++-- R/collapse_metadata.R | 12 +- R/collapse_query_set.R | 6 +- R/collect_metadata.R | 63 ++++--- R/collect_metadata_unnest.R | 2 +- R/collect_occurrences.R | 4 +- R/collect_occurrences_count.R | 2 +- R/collect_species.R | 2 +- R/collect_taxa.R | 16 +- R/compound.R | 37 ++-- R/compute_occurrences.R | 9 +- R/dplyr-compute.R | 2 +- R/dplyr-filter.R | 2 +- R/galah_call.R | 50 +++++- R/galah_filter.R | 7 +- R/galah_select.R | 18 +- R/print.R | 25 ++- R/read_zip.R | 165 ++++++++++-------- R/search_all.R | 10 +- R/url_lookup.R | 16 +- R/utilities_caching.R | 9 +- R/utilities_internal.R | 89 +++++----- man/galah_call.Rd | 17 +- man/read_zip.Rd | 5 +- tests/testthat/test-dplyr-filter.R | 241 +++++++++++++++------------ tests/testthat/test-galah_call.R | 38 ++++- tests/testthat/test-galah_filter.R | 15 ++ tests/testthat/test-galah_identify.R | 4 +- 42 files changed, 818 insertions(+), 619 deletions(-) create mode 100644 tests/testthat/test-galah_filter.R diff --git a/R/atlas_media.R b/R/atlas_media.R index 87516696..69fb1f03 100644 --- a/R/atlas_media.R +++ b/R/atlas_media.R @@ -12,27 +12,27 @@ atlas_media <- function(request = NULL, apply_profile = NULL, all_fields = FALSE ) { - - # check media is available - media_supported() - # capture supplied arguments args <- as.list(environment()) # convert to `data_request` object .query <- check_atlas_inputs(args) .query$type <- "occurrences" # default, but in case supplied otherwise + # check media is available + media_supported(atlas = .query$atlas) + # ensure a filter is present (somewhat redundant with `collapse`) if(is.null(.query$filter)){ cli::cli_abort("You must specify a valid `filter()` to use `atlas_media()`") } + # work out which fields are most available for this atlas + present_fields <- image_fields(atlas = .query$atlas) + # ensure media columns are present in `select` if(is.null(.query$select)){ .query <- .query |> dplyr::select(group = c("basic", "media")) - present_fields <- image_fields() - present_fields <- present_fields[present_fields != "multimedia"] # check these fields for Spain query_collapse <- collapse(.query) # if `select` is present, ensure that at least one 'media' field is requested }else{ @@ -45,12 +45,8 @@ atlas_media <- function(request = NULL, strsplit(split = ",") |> purrr::pluck(!!!list(1)) - # `multimedia` should be in `select`, but not `filter` - image_select <- image_fields() - image_select <- image_select[image_select != "multimedia"] - # abort if no fields are given to `select` - if(!any(selected_fields %in% image_select)){ + if(!any(selected_fields %in% present_fields)){ selected_text <- glue::glue_collapse(selected_fields, sep = ", ") c("No media fields requested by `select()`", i = "try `galah_select({selected_text}, group = 'media')` instead") |> @@ -64,7 +60,7 @@ atlas_media <- function(request = NULL, # add media content to filters if(length(present_fields) > 0){ # do region-specific filter parsing - media_fq <- image_filters(present_fields) + media_fq <- image_filters(present_fields, atlas = atlas) # add back to source object if(length(media_fq) > 1){ media_fq <- glue::glue("({glue::glue_collapse(media_fq, ' OR ')})") diff --git a/R/atlas_species.R b/R/atlas_species.R index 178d4e24..b5cc8b36 100644 --- a/R/atlas_species.R +++ b/R/atlas_species.R @@ -32,7 +32,7 @@ atlas_species <- function(request = NULL, check_distinct_for_atlas_species <- function(.query){ if(is.null(.query$distinct)){ .query |> - distinct(species_facets(), .keep_all = TRUE) + distinct(species_facets(.query), .keep_all = TRUE) }else{ .query$distinct$keep_all <- TRUE .query diff --git a/R/atlas_taxonomy.R b/R/atlas_taxonomy.R index 055ed731..2a5cbd4e 100644 --- a/R/atlas_taxonomy.R +++ b/R/atlas_taxonomy.R @@ -20,7 +20,7 @@ atlas_taxonomy <- function(request = NULL, .query$type <- "taxonomy" # default, but in case supplied otherwise check_identify(.query) check_down_to(.query) - constrain_ids <- check_constraints(args = args, + constrain_ids <- check_constraints(.query, call = as.list(sys.call())) # extract required information from `identify` @@ -58,11 +58,11 @@ atlas_taxonomy <- function(request = NULL, #' Internal function to check whether constraints have been passed #' @noRd #' @keywords Internal -check_constraints <- function(args, call){ +check_constraints <- function(.query, call){ if(any(names(call) == "constrain_ids")){ # i.e. if user specifies an argument call$constrain_ids }else{ # if NULL only occurs because no argument is set - if(potions::pour("atlas", "region") == "Australia"){ + if(.query$atlas == "Australia"){ "biodiversity.org.au" } } diff --git a/R/build_query.R b/R/build_query.R index 0f062990..b74b370c 100644 --- a/R/build_query.R +++ b/R/build_query.R @@ -10,8 +10,9 @@ build_headers <- function(){ #' @keywords Internal build_query <- function(identify = NULL, filter = NULL, - location = NULL, - apply_profile = NULL) { + geolocate = NULL, + apply_profile = NULL, + atlas = NULL) { if(is.null(identify)) { taxa_query <- NULL } else { # assumes a tibble or data.frame has been given @@ -38,22 +39,22 @@ build_query <- function(identify = NULL, # merge query <- list(fq = c(filter_query, taxa_query)) # geographic stuff - if (!is.null(location)) { - # if location is for a point radius vs polygon/bbox - if(!is.null(names(location))){ - if(all(!is.null(location$radius))) { # `galah_radius()` will always pass radius argument + if (!is.null(geolocate)) { + # if `geolocate` is for a point radius vs polygon/bbox + if(!is.null(names(geolocate))){ + if(all(!is.null(geolocate$radius))) { # `galah_radius()` will always pass radius argument query$q <- paste0("*:*") - query$lon <- location$lon - query$lat <- location$lat - query$radius <- location$radius + query$lon <- geolocate$lon + query$lat <- geolocate$lat + query$radius <- geolocate$radius }else - query$wkt <- location + query$wkt <- geolocate } else { - query$wkt <- location + query$wkt <- geolocate } } # add profiles information (ALA only) - if(profiles_supported()){ + if(profiles_supported(atlas)){ if(!is.null(apply_profile)) { query$qualityProfile <- apply_profile } else { @@ -115,9 +116,9 @@ build_filter_query <- function(filters) { #' Sub-function to `build_query()` for taxa #' @noRd #' @keywords Internal -build_taxa_query <- function(ids) { +build_taxa_query <- function(ids, atlas) { ids <- ids[order(ids)] - if(is_gbif()){ + if(atlas == "Global"){ list(taxonKey = ids) }else{ wrapped_ids <- paste0("\"", ids, "\"") diff --git a/R/capture.R b/R/capture.R index 2413110e..01ea9889 100644 --- a/R/capture.R +++ b/R/capture.R @@ -166,7 +166,7 @@ as_prequery <- function(x){ #' @keywords Internal check_doi <- function(x){ if(x$type == "occurrences"){ - if(is_gbif()){ + if(x$atlas == "Global"){ variables <- unlist(x$filter) if(any(variables == "DOI")){ x$type <- "occurrences-doi" @@ -223,9 +223,9 @@ check_distinct_count_groupby <- function(x){ # this is clunky, but backwards compatible if(x$type == "species" & !has_distinct){ if(has_count){ - x <- x |> distinct(species_facets(), .keep_all = FALSE) + x <- x |> distinct(species_facets(x), .keep_all = FALSE) }else{ - x <- x |> distinct(species_facets(), .keep_all = TRUE) + x <- x |> distinct(species_facets(x), .keep_all = TRUE) } has_distinct <- TRUE } diff --git a/R/capture_events.R b/R/capture_events.R index f9d0596e..c64a7429 100644 --- a/R/capture_events.R +++ b/R/capture_events.R @@ -12,6 +12,8 @@ capture_events <- function(x, capture_events_count <- function(x, error_call = rlang::caller_env()){ + x$type <- "data/events-count" + # compile supplied arguments into a list # honestly this is a little messy, but the alternative is to call # [build_predicates()], which is messier as taxonomic info hasn't yet been @@ -25,8 +27,9 @@ capture_events_count <- function(x, x$slice), limit = 0) - list(type = "data/events-count", - url = url_lookup("data/events-count"), + list(type = x$type, + atlas = x$atlas, + url = url_lookup(x), headers = c(build_headers(), `Content-Type` = "application/json"), body = predicates_info) |> @@ -38,10 +41,12 @@ capture_events_count <- function(x, #' @keywords Internal capture_events_describe <- function(x, error_call = rlang::caller_env()){ - list(type = "data/events-describe", - url = url_lookup("data/events-count"), - headers = c(build_headers(), - `Content-Type` = "application/json"), - body = "query { __schema { types { name kind description } } }") |> - as_query() + x$type <- "data/events-describe" + list(type = x$type, + atlas = x$atlas, + url = url_lookup(x), + headers = c(build_headers(), + `Content-Type` = "application/json"), + body = "query { __schema { types { name kind description } } }") |> + as_query() } \ No newline at end of file diff --git a/R/capture_files.R b/R/capture_files.R index 0fe0f02b..e38d275f 100644 --- a/R/capture_files.R +++ b/R/capture_files.R @@ -6,6 +6,8 @@ capture_media_files <- function(.query, thumbnail = FALSE, error_call = rlang::caller_env() ){ + .query$type <- "files/media" + # handle filters if(is.null(.query$filter)){ cli::cli_abort("`This function requires a `filter()` argument", @@ -27,8 +29,10 @@ capture_media_files <- function(.query, } # create output - list(type = "files/media", - url = build_media_tibble(identifiers, + list(type = .query$type, + atlas = .query$atlas, + url = build_media_tibble(.query, + identifiers, df$mime_type, thumbnail), headers = build_headers()) |> @@ -39,7 +43,8 @@ capture_media_files <- function(.query, #' Internal function to create API urls AND file paths #' @noRd #' @keywords Internal -build_media_tibble <- function(identifiers, +build_media_tibble <- function(.query, + identifiers, mimetype, thumbnail = FALSE){ # create a `size` vector @@ -54,7 +59,7 @@ build_media_tibble <- function(identifiers, # return a result tibble::tibble( - url = url_lookup("files/media", + url = url_lookup(.query, id = identifiers, size = size_vec), path = build_file_path(ids = identifiers, diff --git a/R/capture_metadata.R b/R/capture_metadata.R index 2e803810..6ddad705 100644 --- a/R/capture_metadata.R +++ b/R/capture_metadata.R @@ -6,30 +6,32 @@ #' @param query_type the sting used to define that query type; see `show_all_apis()` #' @noRd #' @keywords Internal -default_query <- function(query_type){ - list(type = query_type, - url = url_lookup(query_type), +default_query <- function(x){ + list(type = x$type, + atlas = x$atlas, + url = url_lookup(x), headers = build_headers()) } #' Ditto for cache #' @noRd #' @keywords Internal -default_cache <- function(query_type){ - specific_type <- stringr::str_remove(query_type, "^metadata/") - list(type = query_type, +default_cache <- function(x){ + specific_type <- stringr::str_remove(x$type, "^metadata/") + list(type = x$type, + atlas = x$atlas, data = glue::glue("galah:::retrieve_cache(\"{specific_type}\")")) } #' Modified version of `default_query()` that supports filtering #' @noRd #' @keywords Internal -filtered_query <- function(query_type, .query){ - url <- query_type |> - url_lookup() |> +filtered_query <- function(x){ + url <- url_lookup(x) |> httr2::url_parse() - url$query <- list(q = .query$filter$value[1]) - list(type = query_type, + url$query <- list(q = x$filter$value[1]) + list(type = x$type, + atlas = x$atlas, url = httr2::url_build(url), headers = build_headers()) } @@ -41,6 +43,7 @@ filtered_query <- function(query_type, .query){ #' @keywords Internal capture_apis <- function(x){ list(type = "metadata/apis", + atlas = x$atlas, data = "galah:::node_config") |> as_query() } @@ -50,15 +53,16 @@ capture_apis <- function(x){ #' @noRd #' @keywords Internal capture_assertions <- function(x){ - query_type <- "metadata/assertions" - if(is_gbif()){ - result <- list(type = query_type, + x$type <- "metadata/assertions" + if(x$atlas == "Global"){ + result <- list(type = x$type, + atlas = x$atlas, data = "galah:::gbif_internal_archived$assertions") }else{ - if(check_if_cache_update_needed("assertions")){ - result <- default_query(query_type) + if(check_if_cache_update_needed(x, "assertions")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } } result |> @@ -69,7 +73,9 @@ capture_assertions <- function(x){ #' @noRd #' @keywords Internal capture_atlases <- function(x){ - list(type = "metadata/atlases", + x$type <- "metadata/atlases" + list(type = x$type, + atlas = x$atlas, data = "galah:::node_metadata") |> as_query() } @@ -79,20 +85,20 @@ capture_atlases <- function(x){ #' @keywords Internal capture_collections <- function(x){ # set `type` - query_type <- "metadata/collections" + x$type <- "metadata/collections" # If `filter()` is supplied, we always need a query - if(is_gbif() & !missing(x)){ + if(x$atlas == "Global" & !missing(x)){ if(!is.null(x$filter)){ - result <- filtered_query(query_type, x) + result <- filtered_query(x) }else{ - result <- default_query(query_type) + result <- default_query(x) } # If no `filter()`, check cache instead }else{ - if(check_if_cache_update_needed("collections")){ - result <- default_query(query_type) + if(check_if_cache_update_needed(x, "collections")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } } result |> @@ -105,11 +111,11 @@ capture_collections <- function(x){ #' @noRd #' @keywords Internal capture_config <- function(x){ - query_type <- "metadata/config" - if(check_if_cache_update_needed("config")){ - result <- default_query(query_type) + x$type <- "metadata/config" + if(check_if_cache_update_needed(x, "config")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } result |> as_query() @@ -120,20 +126,20 @@ capture_config <- function(x){ #' @keywords Internal capture_datasets <- function(x){ # set `type` - query_type <- "metadata/datasets" + x$type <- "metadata/datasets" # If `filter()` is supplied, we always need a query - if(is_gbif() & !missing(x)){ + if(x$atlas == "Global" & !missing(x)){ if(!is.null(x$filter)){ - result <- filtered_query(query_type, x) + result <- filtered_query(x) }else{ - result <- default_query(query_type) + result <- default_query(x) } # If no `filter()`, check cache instead }else{ - if(check_if_cache_update_needed("datasets")){ - result <- default_query(query_type) + if(check_if_cache_update_needed(x, "datasets")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } } result |> @@ -144,11 +150,11 @@ capture_datasets <- function(x){ #' @noRd #' @keywords Internal capture_fields <- function(x){ - query_type <- "metadata/fields" - if(check_if_cache_update_needed("fields")){ - default_query(query_type) |> as_query() + x$type <- "metadata/fields" + if(check_if_cache_update_needed(x, "fields")){ + default_query(x) |> as_query() }else{ - default_cache(query_type) |> as_query() + default_cache(x) |> as_query() } } @@ -156,11 +162,11 @@ capture_fields <- function(x){ #' @noRd #' @keywords Internal capture_licences <- function(x){ - query_type <- "metadata/licences" - if(check_if_cache_update_needed("licences")){ - default_query(query_type) |> as_query() + x$type <- "metadata/licences" + if(check_if_cache_update_needed(x, "licences")){ + default_query(x) |> as_query() }else{ - default_cache(query_type) |> as_query() + default_cache(x) |> as_query() } } @@ -169,20 +175,22 @@ capture_licences <- function(x){ #' @keywords Internal capture_lists <- function(x, error_call = rlang::caller_env()){ - query_type <- "metadata/lists" + x$type <- "metadata/lists" # if filter is supplied, lookup a specified list by dr number if(!is.null(x$filter)){ dr_lookup <- stringr::str_detect(x$filter$value, "^dr") if(any(dr_lookup)){ dr_values <- x$filter$value[dr_lookup] - base_url <- url_lookup(query_type) + base_url <- url_lookup(x) url <- glue::glue("{base_url}/{dr_values}") if(length(url) > 1){ - result <- list(type = query_type, + result <- list(type = x$type, + atlas = x$atlas, url = tibble::tibble(url = url), # note: tibbles are used to skip pagination in `collapse()` headers = build_headers()) }else{ - result <- list(type = query_type, + result <- list(type = x$type, + atlas = x$atlas, url = url, headers = build_headers()) } @@ -193,8 +201,8 @@ capture_lists <- function(x, } # if filter isn't supplied, check cache etc }else{ - if(check_if_cache_update_needed("lists")){ - url <- url_lookup(query_type) |> + if(check_if_cache_update_needed(x, "lists")){ + url <- url_lookup(x) |> httr2::url_parse() url$query <- list(max = 10000) if(!missing(x)){ @@ -202,11 +210,12 @@ capture_lists <- function(x, url$query <- list(max = x$slice$slice_n) } } - result <- list(type = query_type, + result <- list(type = x$type, + atlas = x$atlas, url = httr2::url_build(url), headers = build_headers()) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } } result |> @@ -246,8 +255,10 @@ capture_media_metadata <- function(.query, # cli::cli_abort("Media metadata not found in supplied tibble", # call = error_call) # } - list(type = "metadata/media", - url = tibble::tibble(url = url_lookup("metadata/media", + .query$type <- "metadata/media" + list(type = .query$type, + atlas = .query$atlas, + url = tibble::tibble(url = url_lookup(.query, id = .query$filter$value)), headers = build_headers()) |> as_query() @@ -258,11 +269,11 @@ capture_media_metadata <- function(.query, #' @noRd #' @keywords Internal capture_profiles <- function(x){ - query_type <- "metadata/profiles" - if(check_if_cache_update_needed("profiles")){ - result <- default_query(query_type) + x$type <- "metadata/profiles" + if(check_if_cache_update_needed(x, "profiles")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } result |> as_query() @@ -273,20 +284,20 @@ capture_profiles <- function(x){ #' @keywords Internal capture_providers <- function(x){ # set `type` - query_type <- "metadata/providers" + x$type <- "metadata/providers" # If `filter()` is supplied, we always need a query - if(is_gbif() & !missing(x)){ + if(x$atlas == "Global" & !missing(x)){ if(!is.null(x$filter)){ - result <- filtered_query(query_type, x) + result <- filtered_query(x) }else{ - result <- default_query(query_type) + result <- default_query(x) } # If no `filter()`, check cache instead }else{ - if(check_if_cache_update_needed("providers")){ - result <- default_query(query_type) + if(check_if_cache_update_needed(x, "providers")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } } result |> @@ -297,11 +308,11 @@ capture_providers <- function(x){ #' @noRd #' @keywords Internal capture_reasons <- function(x){ - query_type <- "metadata/reasons" - if(check_if_cache_update_needed("reasons")){ - result <- default_query(query_type) + x$type <- "metadata/reasons" + if(check_if_cache_update_needed(x, "reasons")){ + result <- default_query(x) }else{ - result <- default_cache(query_type) + result <- default_cache(x) } result |> as_query() @@ -311,13 +322,10 @@ capture_reasons <- function(x){ #' @noRd #' @keywords Internal capture_ranks <- function(x){ - if(is_gbif()){ - result <- list(type = "metadata/ranks", - data = "galah:::gbif_internal_archived$ranks") - }else{ - result <- list(type = "metadata/ranks", - data = "galah:::galah_internal_archived$ranks") - } - result |> + list(type = "metadata/ranks", + atlas = x$atlas, + data = ifelse(x$atlas == "Global", + "galah:::gbif_internal_archived$ranks", + "galah:::galah_internal_archived$ranks")) |> as_query() } \ No newline at end of file diff --git a/R/capture_metadata_unnest.R b/R/capture_metadata_unnest.R index c27188ad..aa4c9d58 100644 --- a/R/capture_metadata_unnest.R +++ b/R/capture_metadata_unnest.R @@ -3,9 +3,10 @@ #' @noRd #' @keywords Internal capture_fields_unnest <- function(.query){ - url <- url_lookup("metadata/fields-unnest") |> + .query$type <- "metadata/fields-unnest" + url <- url_lookup(.query) |> httr2::url_parse() - if(is_gbif()){ + if(.query$atlas == "Global"){ url$query <- list(limit = 0, facet = .query$filter$value[1], # note: facet (singular), not facets (plural) facetLimit = 10^4) @@ -13,7 +14,8 @@ capture_fields_unnest <- function(.query){ url$query <- list(facets = .query$filter$value[1], flimit = 10^4) } - list(type = "metadata/fields-unnest", + list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url)) |> as_prequery() } @@ -23,15 +25,17 @@ capture_fields_unnest <- function(.query){ #' @noRd #' @keywords Internal capture_lists_unnest <- function(.query){ + .query$type <- "metadata/lists-unnest" # get list lookup url - url <- url_lookup("metadata/lists-unnest", + url <- url_lookup(.query, list_id = .query$filter$value[1]) |> httr2::url_parse() # set a default query url$query <- list(max = -1, # remove max limit includeKVP = TRUE) # add name & status columns # create object - list(type = "metadata/lists-unnest", + list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url)) |> as_query() } @@ -41,8 +45,10 @@ capture_lists_unnest <- function(.query){ #' @noRd #' @keywords Internal capture_profiles_unnest <- function(.query){ - list(type = "metadata/profiles-unnest", - url = url_lookup("metadata/profiles-unnest", + .query$type <- "metadata/profiles-unnest" + list(type = .query$type, + atlas = .query$atlas, + url = url_lookup(.query, profile = .query$filter$value[1])) |> as_prequery() } @@ -52,13 +58,15 @@ capture_profiles_unnest <- function(.query){ #' @noRd #' @keywords Internal capture_taxa_unnest <- function(.query){ + .query$type <- "metadata/taxa-unnest" if(!is.null(.query$filter)){ id <- .query$filter$value[1] }else if(!is.null(.query$identify)){ id <- "`TAXON_PLACEHOLDER`" } - list(type = "metadata/taxa-unnest", - url = url_lookup("metadata/taxa-unnest", id = id), + list(type = .query$type, + atlas = .query$atlas, + url = url_lookup(.query, id = id), headers = build_headers()) |> as_prequery() } diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index 6f3d4b39..ebf2f7f7 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -10,7 +10,7 @@ capture_occurrences <- function(.query, cli::cli_abort("No filters supplied to `capture()` with `type = \"occurrences\"`", call = error_call) } - switch(potions::pour("atlas", "region"), + switch(.query$atlas, "Global" = capture_occurrences_gbif(.query, ...), capture_occurrences_la(.query, ...)) } @@ -25,10 +25,12 @@ capture_occurrences_gbif <- function(.query, username <- potions::pour("user", "username", .pkg = "galah") password <- potions::pour("user", "password", .pkg = "galah") user_string <- glue::glue("{username}:{password}") - + .query$type <- "data/occurrences" + # build object - list(type = "data/occurrences", - url = url_lookup("data/occurrences"), + list(type = .query$type, + atlas = .query$atlas, + url = url_lookup(.query), headers = list( `User-Agent` = galah_version_string(), `X-USER-AGENT` = galah_version_string(), @@ -50,29 +52,32 @@ capture_occurrences_gbif <- function(.query, #' @keywords Internal capture_occurrences_la <- function(.query, mint_doi = FALSE){ + .query$type <- "data/occurrences" # build a query query <- c(build_query(identify = .query$identify, filter = .query$filter, - location = .query$geolocate, - apply_profile = .query$apply_profile), + geolocate = .query$geolocate, + apply_profile = .query$apply_profile, + atlas = .query$atlas), fields = "`SELECT_PLACEHOLDER`", qa = "`ASSERTIONS_PLACEHOLDER`", facet = "false", - sourceTypeId = {potions::pour("atlas", "region") |> - source_type_id_lookup()}, + sourceTypeId = source_type_id_lookup(.query$atlas), reasonTypeId = potions::pour("user", "download_reason_id"), dwcHeaders = "true") |> add_email_notify() |> add_email_address(query = .query) |> - add_doi_request(mint_doi = mint_doi) + add_doi_request(mint_doi = mint_doi, + atlas = .query$atlas) # build url - url <- url_lookup("data/occurrences") |> + url <- url_lookup(.query) |> httr2::url_parse() url$query <- query # build output - list(type = "data/occurrences", + list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers()) |> as_prequery() @@ -83,6 +88,8 @@ capture_occurrences_la <- function(.query, #' @keywords Internal capture_occurrences_doi <- function(.query, error_call = rlang::caller_env()){ + .query$type <- "data/occurrences-doi" + # check for a doi filter if(is.null(.query$filter)){ cli::cli_abort("A DOI must be specified using `filter(doi == \"my-doi-here\")`.", @@ -97,8 +104,7 @@ capture_occurrences_doi <- function(.query, } # check atlas is ok - atlas <- potions::pour("atlas", "acronym") - if(!(atlas %in% c("ALA", "GBIF"))){ + if(!(.query$atlas %in% c("Australia", "Global"))){ c( "DOI downloads not supported by selected atlas.", i = "`request_data(type = \"occurrences-doi\")` has only been implemented for ALA & GBIF") |> @@ -115,7 +121,7 @@ capture_occurrences_doi <- function(.query, # extract useful part of DOI - if(atlas == "ALA"){ + if(.query$atlas == "Australia"){ doi <- stringr::str_split(doi, "ala.")[[1]][2] if(is.na(doi)){ c("DOI has not been generated by the ALA.", @@ -125,8 +131,9 @@ capture_occurrences_doi <- function(.query, } # build an object - list(type = "data/occurrences-doi", - url = url_lookup("data/occurrences-doi", + list(type = .query$type, + atlas = .query$atlas, + url = url_lookup(.query, doi_string = doi), headers = build_headers(), download = TRUE) |> @@ -137,26 +144,27 @@ capture_occurrences_doi <- function(.query, #' @noRd #' @keywords Internal capture_occurrences_glimpse <- function(.query){ - if(is_gbif()){ + .query$type <- "data/occurrences-glimpse" + + if(.query$atlas == "Global"){ result <- capture_occurrences_count(.query) result$body$limit <- 3 - result$type <- "data/occurrences-glimpse" as_prequery(result) }else{ result <- capture_occurrences_la(.query) url <- httr2::url_parse(result$url) # replace path with count API - url$path <- url_lookup("data/occurrences-count") |> + url$path <- list(type = "data/occurrences-count", + atlas = .query$atlas) |> + structure(class = c("data_request", "list")) |> + url_lookup() |> httr2::url_parse() |> - purrr::pluck("path") - - # add a pageSize arg - url$query$pageSize <- 3 + purrr::pluck("path") + url$query$pageSize <- 3 # add a pageSize arg + result$url <- httr2::url_build(url) # rebuild - # rebuild and ship - result$url <- httr2::url_build(url) - result$type <- "data/occurrences-glimpse" + # return as_prequery(result) } } @@ -165,6 +173,8 @@ capture_occurrences_glimpse <- function(.query){ #' @noRd #' @keywords Internal capture_occurrences_describe <- function(.query){ - x <- request_metadata(type = "fields") |> capture() - update_request_object(x, type = "data/occurrences-describe") + .query$type <- "data/occurrences-describe" + x <- request_metadata(type = "fields", from = .query$atlas) |> + capture() + update_request_object(x, type = .query$type) } \ No newline at end of file diff --git a/R/capture_occurrences_count.R b/R/capture_occurrences_count.R index 7644f821..a9d43d15 100644 --- a/R/capture_occurrences_count.R +++ b/R/capture_occurrences_count.R @@ -3,53 +3,43 @@ #' @param .query an object of class `data_request` #' @noRd capture_occurrences_count <- function(.query){ - # NOTE: This is quite weird syntax; consider revising - if(is_gbif()){ - function_name <- "capture_occurrences_count_gbif" - arg_names <- names(formals(capture_occurrences_count_gbif)) - }else{ - function_name <- "capture_occurrences_count_atlas" - arg_names <- names(formals(capture_occurrences_count_atlas)) + switch(.query$atlas, + "Global" = capture_occurrences_count_gbif(.query), + capture_occurrences_count_atlas(.query)) } - custom_call <- .query[names(.query) %in% arg_names] - class(custom_call) <- "data_request" - do.call(function_name, custom_call) -} #' capture() for counts on LAs #' @keywords Internal #' @noRd -capture_occurrences_count_atlas <- function(identify = NULL, - filter = NULL, - geolocate = NULL, - apply_profile = NULL, - group_by = NULL, - distinct = NULL, - slice_arrange = NULL -){ - query <- build_query(identify, - filter, - geolocate, - apply_profile = apply_profile) +capture_occurrences_count_atlas <- function(.query){ + query <- build_query(identify = .query$identify, + filter = .query$filter, + geolocate = .query$geolocate, + apply_profile = .query$apply_profile, + atlas = .query$atlas) # set behaviour depending on `group_by()` - if(is.null(group_by) & is.null(distinct)){ - url <- url_lookup("data/occurrences-count") |> + if(is.null(.query$group_by) & is.null(.query$distinct)){ + .query$type <- "data/occurrences-count" + url <- url_lookup(.query) |> httr2::url_parse() url$query <- c(query, pageSize = 0) - result <- list(type = "data/occurrences-count", + result <- list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers()) }else{ - url <- url_lookup("data/occurrences-count-groupby") |> + .query$type <- "data/occurrences-count-groupby" + url <- url_lookup(.query) |> httr2::url_parse() - if(!is.null(group_by)){ - facets <- group_by$name + if(!is.null(.query$group_by)){ + facets <- .query$group_by$name }else{ - facets <- distinct$name + facets <- .query$distinct$name } names(facets) <- rep("facets", length(facets)) - url$query <- c(query, facets, parse_slice_arrange(slice_arrange)) - result <- list(type = "data/occurrences-count-groupby", + url$query <- c(query, facets, parse_slice_arrange(.query$slice_arrange)) + result <- list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers()) } @@ -78,23 +68,18 @@ parse_slice_arrange <- function(df){ #' capture() for counts on GBIF #' @keywords Internal #' @noRd -capture_occurrences_count_gbif <- function(identify = NULL, - filter = NULL, - geolocate = NULL, - group_by = NULL, - slice = NULL # probably broken - ){ +capture_occurrences_count_gbif <- function(.query){ # compile supplied arguments into a list # honestly this is a little messy, but the alternative is to call # [build_predicates()], which is messier as taxonomic info hasn't yet been # parsed. Instead we call [build_predicates()] during [collapse_query()]. - predicates_info <- list(identify = identify, - filter = filter, - geolocate = geolocate, - group_by = group_by, - slice = ifelse(is.null(slice), + predicates_info <- list(identify = .query$identify, + filter = .query$filter, + geolocate = .query$geolocate, + group_by = .query$group_by, + slice = ifelse(is.null(.query$slice), tibble::tibble(slice_n = 30, slice_called = FALSE), - slice), + .query$slice), limit = 0) # get strings @@ -103,17 +88,18 @@ capture_occurrences_count_gbif <- function(identify = NULL, user_string <- glue::glue("{username}:{password}") # handle type - if(is.null(group_by)){ - data_type <- "data/occurrences-count" + if(is.null(.query$group_by)){ + .query$type <- "data/occurrences-count" }else{ - data_type <- "data/occurrences-count-groupby" + .query$type <- "data/occurrences-count-groupby" } # build object ## Note that unlike with other atlases, parsing of `group_by` is handled ## by `collapse()` rather than here. - list(type = data_type, - url = url_lookup("data/occurrences-count"), + list(type = .query$type, + atlas = .query$atlas, + url = url_lookup(.query), headers = list( `User-Agent` = galah_version_string(), `X-USER-AGENT` = galah_version_string(), diff --git a/R/capture_species.R b/R/capture_species.R index e323bcce..bf671379 100644 --- a/R/capture_species.R +++ b/R/capture_species.R @@ -2,7 +2,7 @@ #' @noRd #' @keywords Internal capture_species <- function(.query){ - if(is_gbif()){ + if(.query$atlas == "Global"){ result <- capture_occurrences_gbif(.query, format = "SPECIES_LIST") result$type <- "data/species" @@ -16,6 +16,8 @@ capture_species <- function(.query){ #' @noRd #' @keywords Internal capture_species_atlas <- function(.query){ + .query$type <- "data/species" + # set default columns if(is.null(.query$select)){ .query <- .query |> select(group = "taxonomy") @@ -23,7 +25,7 @@ capture_species_atlas <- function(.query){ # determine whether to use `distinct()` or `species_facets()` if(is.null(.query$distinct)){ - .query$distinct <- tibble::tibble(name = species_facets(), + .query$distinct <- tibble::tibble(name = species_facets(.query), type = "field") }else{ if(is.na(.query$distinct$name)){ @@ -37,10 +39,11 @@ capture_species_atlas <- function(.query){ # build a query query <- c( - build_query(.query$identify, - .query$filter, - .query$geolocate, - .query$apply_profile), + build_query(identify = .query$identify, + filter = .query$filter, + geolocate = .query$geolocate, + apply_profile = .query$apply_profile, + atlas = .query$atlas), sourceTypeId = 2004, reasonTypeId = potions::pour("user", "download_reason_id"), facets = .query$distinct$name, @@ -49,11 +52,12 @@ capture_species_atlas <- function(.query){ add_email_notify() # build url - url <- url_lookup("data/species") |> + url <- url_lookup(.query) |> httr2::url_parse() url$query <- query # build output - list(type = "data/species", + list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers(), download = TRUE) |> diff --git a/R/capture_species_count.R b/R/capture_species_count.R index 9c2ca51b..62be8098 100644 --- a/R/capture_species_count.R +++ b/R/capture_species_count.R @@ -4,61 +4,54 @@ #' @noRd capture_species_count <- function(.query, error_call = rlang::caller_env()){ - if(is_gbif()){ + if(.query$atlas == "Global"){ cli::cli_abort("`count()` is not supported for GBIF with type = 'species'", call = error_call) }else{ - function_name <- "capture_species_count_atlas" - arg_names <- names(formals(capture_species_count_atlas)) + capture_species_count_atlas(.query) } - custom_call <- .query[names(.query) %in% arg_names] - class(custom_call) <- "data_request" - do.call(function_name, custom_call) } #' collapse for counts on LAs #' @keywords Internal #' @noRd -capture_species_count_atlas <- function(identify = NULL, - filter = NULL, - geolocate = NULL, - apply_profile = NULL, - group_by = NULL, - distinct = NULL, - slice_arrange = NULL -){ +capture_species_count_atlas <- function(.query){ + .query$type <- "data/species-count" # determine facets - if(is.null(distinct)){ - facet_variable <- species_facets() + if(is.null(.query$distinct)){ + facet_variable <- species_facets(.query) }else{ - facet_variable <- distinct$name[[1]] + facet_variable <- .query$distinct$name[[1]] } # get url - url <- url_lookup("data/species-count") |> + url <- url_lookup(.query) |> httr2::url_parse() - query <- build_query(identify, - filter, - geolocate, - apply_profile = apply_profile) + query <- build_query(identify = .query$identify, + filter = .query$filter, + geolocate = .query$geolocate, + apply_profile = .query$apply_profile, + atlas = .query$atlas) # set behaviour depending on `group_by()` - if(is.null(group_by)){ + if(is.null(.query$group_by)){ url$query <- c(query, list(flimit = 1, facets = facet_variable)) - result <- list(type = "data/species-count", + result <- list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers()) }else{ - facets <- c(as.list(group_by$name), + facets <- c(as.list(.query$group_by$name), facet_variable) names(facets) <- rep("facets", length(facets)) url$query <- c(query, facets, - parse_slice_arrange(slice_arrange), + parse_slice_arrange(.query$slice_arrange), list(flimit = -1)) - result <- list(type = "data/species-count", + result <- list(type =.query$type, + atlas = .query$atlas, url = httr2::url_build(url), headers = build_headers()) } diff --git a/R/capture_taxa.R b/R/capture_taxa.R index b6d9a040..bf2773f9 100644 --- a/R/capture_taxa.R +++ b/R/capture_taxa.R @@ -3,7 +3,8 @@ #' @keywords Internal capture_taxa <- function(.query){ if(is.null(.query$identify)){ - result <- list(type = "metadata/taxa") + result <- list(type = "metadata/taxa", + atlas = .query$atlas) }else{ if(ncol(.query$identify) > 1 | colnames(.query$identify)[1] != "search_term"){ @@ -20,10 +21,11 @@ capture_taxa <- function(.query){ #' @noRd #' @keywords Internal capture_taxa_single <- function(.query){ + .query$type <- "metadata/taxa-single" terms <- .query$identify$search_term - list(type = "metadata/taxa-single", - url = tibble::tibble(url = url_lookup("metadata/taxa-single", - name = terms), + list(type = .query$type, + atlas = .query$atlas, + url = tibble::tibble(url = url_lookup(.query, name = terms), search_term = terms), headers = build_headers()) } @@ -32,17 +34,18 @@ capture_taxa_single <- function(.query){ #' @noRd #' @keywords Internal capture_taxa_multiple <- function(.query){ + .query$type <- "metadata/taxa-multiple" # get a data.frame, enforce use of accepted taxon levels identify_df <- .query$identify colnames(identify_df) <- colnames(identify_df) |> tolower() identify_df <- identify_df |> - dplyr::select(dplyr::any_of(accepted_ranks())) + dplyr::select(dplyr::any_of(accepted_ranks(.query))) # split into one query per row split_list <- split(.query$identify, seq_len(nrow(.query$identify))) - base_url <- url_lookup("metadata/taxa-multiple") |> + base_url <- url_lookup(.query) |> httr2::url_parse() urls <- purrr::map(split_list, function(a, base_url){ @@ -56,7 +59,8 @@ capture_taxa_multiple <- function(.query){ unlist() # build object and return - list(type = "metadata/taxa-multiple", + list(type = .query$type, + atlas = .query$atlas, url = tibble::tibble(url = urls, search_term = search_terms), headers = build_headers()) @@ -66,15 +70,16 @@ capture_taxa_multiple <- function(.query){ #' @noRd #' @keywords Internal capture_identifiers <- function(.query){ + .query$type <- "metadata/identifiers" if(is.null(.query$filter)){ - url_list <- url_lookup("metadata/identifiers") + url_list <- url_lookup(.query) names(url_list) <- "no-name-supplied" }else{ search_terms <- .query$filter$value query <- as.list(search_terms) - base_url <- url_lookup("metadata/identifiers") + base_url <- url_lookup(.query) # create query urls - if(grepl("api.gbif.org", base_url)){ + if(grepl("api.gbif.org", base_url)){ # NOTE: This is not `.query$atlas == "GBIF"` because other atlases use this API base_url <- utils::URLdecode(base_url) urls <- glue::glue(base_url, id = query) |> unlist() @@ -91,7 +96,8 @@ capture_identifiers <- function(.query){ } } # build object and return - list(type = "metadata/identifiers", + list(type = .query$type, + atlas = .query$atlas, url = tibble::tibble(url = urls, search_term = search_terms), headers = build_headers()) |> @@ -101,8 +107,8 @@ capture_identifiers <- function(.query){ #' Internal function to accept only specific taxon ranks for searching #' @noRd #' @keywords Internal -accepted_ranks <- function(){ - if(is_gbif()){ +accepted_ranks <- function(.query){ + if(.query$atlas == "Global"){ # https://techdocs.gbif.org/en/openapi/v1/species#/Searching%20names/matchNames c("kingdom", "phylum", diff --git a/R/check.R b/R/check.R index c977df7e..7f6a00d4 100644 --- a/R/check.R +++ b/R/check.R @@ -32,13 +32,14 @@ check_atlas_inputs <- function(args, #' @noRd #' @keywords Internal check_authentication <- function(x){ + # if authentication not passed in pipe, check `galah_config()` if(is.null(x$authenticate) & isTRUE(potions::pour("user", "authenticate", .pkg = "galah")) & x$type %in% c("occurrences")){ x <- x |> authenticate() } - atlas <- potions::pour("atlas", "region", .pkg = "galah") - if(atlas != "Australia" & + # reset authentication to `NULL` if requested for a non-supported atlas + if(x$atlas != "Australia" & !is.null(x$authenticate)){ cli::cli_warn("Authentication not supported for atlas {atlas}: skipping") x$authenticate <- NULL @@ -102,7 +103,7 @@ check_download_filename <- function(file, #' @keywords Internal check_email <- function(.query, call = rlang::caller_env()){ - if(is_gbif()){ + if(.query$atlas == "Global"){ # actually we check the userpwd entry here email_text <- .query$options$userpwd if(email_text == ":"){ @@ -122,7 +123,7 @@ check_email <- function(.query, FALSE } # authentication only acceptable alternative to email for ALA - if(is_ala()){ + if(.query$atlas == "Australia"){ authentication_missing <- is.null(.query$authenticate) if(email_text_missing & authentication_missing){ abort_email_missing(error_call = call) @@ -186,7 +187,7 @@ check_fields <- function(.query, error_call = rlang::caller_env()) { if(potions::pour("package", "run_checks")){ - if(is_gbif()){ + if(.query$atlas == "Global"){ if(.query$type == "data/occurrences"){ check_result <- check_fields_gbif_predicates(.query) }else{ @@ -228,7 +229,7 @@ check_field_identities <- function(df, error_call = rlang::caller_env()){ if(!is.null(.query$fields) & potions::pour("package", "run_checks", .pkg = "galah") & - potions::pour("atlas", "region", .pkg = "galah") %in% c("Australia", "Spain", "Sweden") + .query$atlas %in% c("Australia", "Spain", "Sweden") # NOTE: last line included because the remaining atlases use different # architecture which tends to mean queries are sent with non-DwC terms, # but return DwC terms. This triggers warnings that are technically @@ -426,7 +427,7 @@ check_groups <- function(group, n){ check_identifiers <- function(.query, error_call = rlang::caller_env()){ # For GBIF, which uses predicates, we 'promote' taxonomic queries to 'predicates' - if(is_gbif()){ + if(.query$atlas == "Global"){ .query$body$identify <- .query$`metadata/taxa-single` .query # otherwise we replace "(`TAXON_PLACEHOLDER`)" @@ -459,7 +460,8 @@ check_identifiers_la <- function(.query, call = error_call) } - taxa_ids <- build_taxa_query(identifiers$taxon_concept_id) + taxa_ids <- build_taxa_query(ids = identifiers$taxon_concept_id, + atlas = .query$atlas) queries$fq <- stringr::str_replace_all(queries$fq, "\\(`TAXON_PLACEHOLDER`\\)", taxa_ids) @@ -496,7 +498,7 @@ check_identifiers_la <- function(.query, check_login <- function(.query, error_call = rlang::caller_env()) { # Check for valid email for occurrences or species queries for all providers - if(is_gbif()){ + if(.query$atlas == "Global"){ if(grepl("^data", .query$type)){ check_email(.query, call = error_call) check_password(.query, call = error_call) @@ -505,7 +507,7 @@ check_login <- function(.query, if(.query$type %in% c("data/occurrences", "data/species") & is.null(.query$request$authenticate) # i.e. only validate if authenticate = FALSE ){ - switch(potions::pour("atlas", "region"), + switch(.query$atlas, "United Kingdom" = {}, check_email(.query, call = error_call)) } @@ -546,13 +548,13 @@ check_media_cols_present <- function(.query, strsplit(",") |> purrr::pluck(1) fields_check <- image_fields() %in% fields - if(!any(fields_check)){ + if(!any(fields_check)){atlas = .query$atlas c("No media fields requested.", i = "Use `select()` to specify which media fields are required.", i = "Valid fields are 'images', 'videos' and 'sounds'.") |> cli::cli_abort(call = error_call) }else{ - image_fields()[fields_check] + image_fields(atlas = .query$atlas)[fields_check] } } @@ -722,7 +724,7 @@ check_profiles <- function(.query, #' @keywords Internal check_reason <- function(.query, error_call = rlang::caller_env()){ - if(reasons_supported()) { + if(reasons_supported(atlas = .query$atlas)) { if(.query$type %in% c("data/occurrences", "data/species")){ query <- httr2::url_parse(.query$url)$query if(is.null(query$reasonTypeId)){ @@ -753,7 +755,7 @@ check_reason <- function(.query, check_select <- function(.query, error_call = rlang::caller_env()){ if(any(names(.query$request) == "select")){ - if(!(is_gbif() & stringr::str_detect(.query$type, "^data"))){ + if(!(.query$atlas == "Global" & stringr::str_detect(.query$type, "^data"))){ # cli::cli({ # cli::cli_text("Skipping `select()`.") # cli::cli_bullets(c(i = "This function is not supported by the GBIF occurrences downloads API v1.")) @@ -817,7 +819,10 @@ parse_select_occurrences <- function(.query, # new step to avoid calling `show_all_assertions()` internally group <- group_initial[group_initial != "assertions"] if(length(group) > 0){ - group_cols <- purrr::map(group, preset_groups) |> + group_cols <- purrr::map(group, + \(a){ + preset_groups(a, atlas = .query$atlas) + }) |> unlist() group_names <- tidyselect::eval_select(dplyr::any_of(group_cols), data = df) |> @@ -843,7 +848,7 @@ parse_select_occurrences <- function(.query, } # 3a: set 'identifier' column name - id_col <- default_columns()[1] + id_col <- default_columns(.query$atlas)[1] # 4: set behaviour depending on what names are given # NOTE: diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index 47655f93..8c2d3780 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -44,7 +44,7 @@ collapse_lists <- function(.query){ #' @keywords Internal get_max_n <- function(.query){ url <- httr2::url_parse(.query$url) - if(is_gbif()){ + if(.query$atlas == "Global"){ count_field <- "count" }else{ count_field <- "listCount" @@ -72,16 +72,17 @@ collapse_profile_values <- function(.query, url <- .query |> purrr::pluck("url") |> httr2::url_parse() - profile_name <- extract_profile_name(url) + profile_name <- extract_profile_name(.query, url) short_name <- profile_short_name(profile_name, error_call = error_call) - if (!potions::pour("atlas", "region") == "Spain") { + if (.query$atlas != "Spain") { path_name <- url |> purrr::pluck("path") |> dirname() url$path <- glue::glue("{path_name}/{short_name}") } list(type = .query$type, + atlas = .query$atlas, url = httr2::url_build(url)) |> as_query() } @@ -124,9 +125,8 @@ profile_short_name <- function(profile, #' for data profiles. Only used by `compute_profile_values()` #' @noRd #' @keywords Internal -extract_profile_name <- function(url) { - atlas <- potions::pour("atlas", "region") - if (atlas == "Spain") { +extract_profile_name <- function(.query, url) { + if (.query$atlas == "Spain") { profile_name <- url |> purrr::pluck("query", "profileName") } else { diff --git a/R/collapse_query_set.R b/R/collapse_query_set.R index c51976c0..6e42b8f3 100644 --- a/R/collapse_query_set.R +++ b/R/collapse_query_set.R @@ -9,14 +9,14 @@ collapse_query_set <- function(x, "data/events-count" = collapse_events_count(x), "data/occurrences" = collapse_occurrences(x), "data/occurrences-count" = { - if(is_gbif()){ + if(x$atlas == "Global"){ collapse_occurrences_count_gbif(x) }else{ collapse_occurrences_count_atlas_basic(x) } }, "data/occurrences-count-groupby" = { - if(is_gbif()){ + if(x$atlas == "Global"){ if(nrow(x$body$group_by) > 1){ collapse_occurrences_count_gbif_groupby_crossed(x) }else{ @@ -34,7 +34,7 @@ collapse_query_set <- function(x, } }, "data/occurrences-glimpse" = { - if(is_gbif()){ + if(x$atlas == "Global"){ collapse_occurrences_count_gbif(x, limit = 3) }else{ x diff --git a/R/collect_metadata.R b/R/collect_metadata.R index af84eada..b1d7d7b7 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -4,9 +4,9 @@ #' as per `galah` requirements #' @noRd #' @keywords Internal -update_attributes <- function(df, type){ +update_attributes <- function(df, type, atlas){ attr(df, "call") <- type # needed for `show_values()` to work - attr(df, "region") <- potions::pour("atlas", "region") # needed for caching to work + attr(df, "region") <- atlas # needed for caching to work df } @@ -76,7 +76,8 @@ make_nulls_safe <- function(x){ #' @keywords Internal collect_apis <- function(.query){ retrieve_internal_data(.query) |> - update_attributes(type = "apis") |> + update_attributes(type = "apis", + atlas = .query$atlas) |> parse_select(.query) } @@ -96,7 +97,8 @@ collect_assertions <- function(.query){ dplyr::rename_with(camel_to_snake_case) |> parse_rename(.query) |> dplyr::mutate(type = "assertions") |> # for consistency with `collect_fields()` - update_attributes(type = "assertions") + update_attributes(type = "assertions", + atlas = .query$atlas) update_cache(assertions = result_df) } parse_select(result_df, .query) # always evaluate `select()` last @@ -108,7 +110,8 @@ collect_assertions <- function(.query){ #' @keywords Internal collect_atlases <- function(.query){ retrieve_internal_data(.query) |> - update_attributes(type = "atlases") |> + update_attributes(type = "atlases", + atlas = .query$atlas) |> parse_select(.query) } @@ -120,7 +123,7 @@ collect_collections <- function(.query){ result_df <- retrieve_internal_data(.query) }else{ # Handle GBIF first - if(is_gbif()){ + if(.query$atlas == "Global"){ result <- query_API(.query) if(any(names(result) == "results")){ # happens when `filter()` not specified # Note: This assumes only one API call; will need more potentially @@ -130,7 +133,7 @@ collect_collections <- function(.query){ purrr::map(tidy_list_columns) |> dplyr::bind_rows() # Then France - }else if(potions::pour("atlas", "region", .pkg = "galah") == "France"){ + }else if(.query$atlas == "France"){ result <- .query |> query_API() |> purrr::pluck("_embedded", "producers") |> @@ -153,7 +156,8 @@ collect_collections <- function(.query){ result_df <- result_df |> dplyr::rename_with(camel_to_snake_case) |> parse_arrange() |> - update_attributes(type = "collections") + update_attributes(type = "collections", + atlas = .query$atlas) update_cache(collections = result_df) } @@ -180,7 +184,8 @@ collect_config <- function(.query){ }else{ result <- query_API(.query) |> tibble::as_tibble() |> - update_attributes(type = "config") + update_attributes(type = "config", + atlas = .query$atlas) update_cache(config = result) } result @@ -194,7 +199,7 @@ collect_datasets <- function(.query){ result_df <- retrieve_internal_data(.query) }else{ result <- query_API(.query) - if(is_gbif()){ + if(.query$atlas == "Global"){ if(any(names(result) == "results")){ # happens when `filter()` not specified # Note: This assumes only one API call; will need more potentially result <- purrr::pluck(result, "results") @@ -202,7 +207,7 @@ collect_datasets <- function(.query){ result_df <- result |> purrr::map(tidy_list_columns) |> dplyr::bind_rows() - }else if(potions::pour("atlas", "region", .pkg = "galah") == "France"){ + }else if(.query$atlas == "France"){ result_df <- result |> purrr::pluck("_embedded", "datasets") |> dplyr::bind_rows() @@ -215,7 +220,8 @@ collect_datasets <- function(.query){ result_df <- result_df |> dplyr::rename_with(camel_to_snake_case) |> parse_arrange() |> - update_attributes(type = "datasets") + update_attributes(type = "datasets", + atlas = .query$atlas) update_cache(datasets = result_df) } parse_select(result_df, .query) @@ -247,7 +253,9 @@ collect_distributions_metadata <- function(.query){ "label" = "area_name", "common_name" = "common_nam") |> dplyr::mutate("common_name" = trimws(.data$common_name)) # remove leading or trailing spaces - result <- update_attributes(result, type = "distributions") + result <- update_attributes(result, + type = "distributions", + atlas = .query$atlas) update_cache(distributions = result) result } @@ -262,7 +270,7 @@ collect_fields <- function(.query){ result <- query_API(.query) |> dplyr::bind_rows() - if(is_gbif()){ + if(.query$atlas == "Global"){ # we need to join some local metadata to GBIF info df <- gbif_internal_archived$search_fields |> dplyr::mutate(search_field = TRUE) @@ -298,7 +306,9 @@ collect_fields <- function(.query){ # Using `distinct()` is a safe way to ensure they are always present # but **not** duplicated. } - result_df <- update_attributes(result_df, type = "fields") + result_df <- update_attributes(result_df, + type = "fields", + atlas = .query$atlas) update_cache(fields = result_df) } result_df |> @@ -328,7 +338,9 @@ collect_licences <- function(.query){ acronym = character(), url = character()) } - result_df <- update_attributes(result_df, type = "licences") + result_df <- update_attributes(result_df, + type = "licences", + atlas = .query$atlas) update_cache(licences = result_df) } parse_select(result_df, .query) @@ -376,7 +388,8 @@ collect_lists <- function(.query){ dplyr::rename_with(camel_to_snake_case) |> parse_rename(.query) |> parse_arrange() |> - update_attributes(type = "lists") + update_attributes(type = "lists", + atlas = .query$atlas) if(should_update_cache){ update_cache(lists = result_df) @@ -421,7 +434,8 @@ collect_profiles <- function(.query){ dplyr::filter(!duplicated(.data$id)) |> dplyr::rename_with(camel_to_snake_case) |> parse_arrange() |> - update_attributes(type = "profiles") + update_attributes(type = "profiles", + atlas = .query$atlas) update_cache(profiles = result_df) } result_df |> @@ -437,7 +451,7 @@ collect_providers <- function(.query){ result_df <- retrieve_internal_data(.query) }else{ result <- query_API(.query) - if(is_gbif()){ + if(.query$atlas == "Global"){ if(any(names(result) == "results")){ # happens when `filter()` not specified # Note: This assumes only one API call; will need more potentially result <- purrr::pluck(result, "results") @@ -445,7 +459,7 @@ collect_providers <- function(.query){ result_df <- result |> purrr::map(tidy_list_columns) |> dplyr::bind_rows() - }else if(potions::pour("atlas", "region", .pkg = "galah") == "France"){ + }else if(.query$atlas == "France"){ result_df <- tibble::tibble(name = { purrr::pluck(result, "_embedded", "providers") |> unlist() @@ -462,7 +476,8 @@ collect_providers <- function(.query){ result_df <- result_df |> dplyr::rename_with(camel_to_snake_case) |> parse_arrange() |> - update_attributes(type = "providers") + update_attributes(type = "providers", + atlas = .query$atlas) update_cache(providers = result_df) } parse_select(result_df, .query) @@ -473,7 +488,8 @@ collect_providers <- function(.query){ #' @keywords Internal collect_ranks <- function(.query){ retrieve_internal_data(.query) |> - update_attributes(type = "ranks") |> + update_attributes(type = "ranks", + atlas = .query$atlas) |> parse_select(.query) } @@ -490,7 +506,8 @@ collect_reasons <- function(.query){ dplyr::filter(!.data$deprecated) |> dplyr::relocate("id", "name") |> parse_arrange() |> - update_attributes(type = "reasons") + update_attributes(type = "reasons", + atlas = .query$atlas) update_cache(reasons = result_df) } parse_select(result_df, .query) diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 1f461572..1af57650 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -8,7 +8,7 @@ collect_fields_unnest <- function(.query, purrr::pluck("url") |> httr2::url_parse() - if(is_gbif()){ + if(.query$atlas == "Global"){ # get name of facet in question facet <- purrr::pluck(facet, "query", "facet") # NOTE: "facet" (singular) check_missing_fields(facet, call = error_call) diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index 23d02496..461da9b5 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -12,7 +12,7 @@ collect_occurrences <- function(.query, wait, file = NULL, error_call = rlang::caller_env()){ - switch(potions::pour("atlas", "region"), + switch(.query$atlas, "Austria" = collect_occurrences_direct(.query, file = file, call = error_call), @@ -112,7 +112,7 @@ collect_occurrences_doi <- function(.query, #' @noRd #' @keywords Internal collect_occurrences_glimpse <- function(.query){ - if(is_gbif()){ + if(.query$atlas == "Global"){ collect_occurrences_glimpse_gbif(.query) }else{ collect_occurrences_glimpse_la(.query) diff --git a/R/collect_occurrences_count.R b/R/collect_occurrences_count.R index db6320ab..09d6d353 100644 --- a/R/collect_occurrences_count.R +++ b/R/collect_occurrences_count.R @@ -2,7 +2,7 @@ #' @noRd #' @keywords Internal collect_occurrences_count <- function(.query){ - {if(is_gbif()){ + {if(.query$atlas == "Global"){ collect_occurrences_count_gbif(.query) }else{ collect_occurrences_count_la(.query) diff --git a/R/collect_species.R b/R/collect_species.R index 045bd107..4eb92505 100644 --- a/R/collect_species.R +++ b/R/collect_species.R @@ -2,7 +2,7 @@ #' @keywords Internal #' @noRd collect_species <- function(.query, file = NULL){ - if(is_gbif()){ + if(.query$atlas == "Global"){ collect_occurrences(.query, wait = TRUE) }else{ .query$file <- check_download_filename(file, ext = "csv") diff --git a/R/collect_taxa.R b/R/collect_taxa.R index 2d4337d3..6593361c 100644 --- a/R/collect_taxa.R +++ b/R/collect_taxa.R @@ -47,7 +47,8 @@ collect_taxa_namematching <- function(.query, check_homonyms(result, error_call = error_call) # Check for invalid search terms if (galah_config()$package$verbose) { - check_search_terms(result) + check_search_terms(result, + atlas = .query$atlas) } result |> dplyr::rename_with(camel_to_snake_case) |> @@ -61,10 +62,11 @@ collect_taxa_namematching <- function(.query, collect_taxa_la <- function(.query){ search_terms <- .query$url$search_term result <- query_API(.query) |> - clean_la_taxa(search_terms = search_terms) |> + clean_la_taxa(search_terms = search_terms, + atlas = .query$atlas) |> dplyr::bind_rows() if(ncol(result) > 1){ - name <- switch(potions::pour("atlas", "region"), + name <- switch(.query$atlas, "France" = "referenceID", "Portugal" = "usageKey", "guid") @@ -118,7 +120,9 @@ clean_gbif_taxa <- function(result){ #' @param result a list from a taxonomic web service #' @noRd #' @keywords Internal -clean_la_taxa <- function(result, search_terms){ +clean_la_taxa <- function(result, + search_terms, + atlas){ purrr::map(result, function(a){ # capture results @@ -162,7 +166,6 @@ clean_la_taxa <- function(result, search_terms){ } # unlist if necessary - atlas <- potions::pour("atlas", "region") if (any(atlas %in% c("France", "Portugal"))) { list_of_results <- list_of_results |> unlist() @@ -186,7 +189,7 @@ collect_identifiers <- function(.query){ } if(!any(colnames(result) == "success")){ # GBIF doesn't indicate success - # we avoid `is_gbif()` here because other atlases use GBIF APIs + # we avoid `.query$atlas == "Global"` here because other atlases use GBIF APIs result$success <- TRUE result <- result |> dplyr::relocate("success", .before = 1) |> @@ -212,7 +215,6 @@ collect_identifiers <- function(.query){ #' @keywords Internal check_search_terms <- function(result, atlas) { if (!all(result$success)) { - atlas <- potions::pour("atlas", "region") d <- cli::cli_div(theme = list(span.bold = list("font-weight" = "bold"), span.yellow = list(color = "yellow"))) diff --git a/R/compound.R b/R/compound.R index e89b6d69..ce0b2041 100644 --- a/R/compound.R +++ b/R/compound.R @@ -86,7 +86,7 @@ compound.prequery <- function(x, mint_doi = FALSE, ...){ }else if(stringr::str_detect(x$type, "^files")){ list(capture_media_files(x, ...)) |> as_query_set() - }else if(x$type == "data/distribtions"){ + }else if(x$type == "data/distributions"){ build_query_set_distributions(x) }else{ build_query_set_data(x, mint_doi = mint_doi, ...) @@ -126,15 +126,15 @@ build_query_set_metadata <- function(x){ # handle authentication if(!is.null(x$request$authenticate)){ result <- append(result, - list(request_metadata("config") |> capture())) + list(request_metadata("config", from = x$atlas) |> capture())) } # add checks if required if(potions::pour("package", "run_checks")){ result <- append(result, switch(x$request$type, - "fields-unnest" = list(request_metadata("fields") |> capture()), - "profiles-unnest" = list(request_metadata("profiles") |> capture()), + "fields-unnest" = list(request_metadata("fields", from = x$atlas) |> capture()), + "profiles-unnest" = list(request_metadata("profiles", from = x$atlas) |> capture()), NULL)) } @@ -144,8 +144,9 @@ build_query_set_metadata <- function(x){ # identify() calls must be parsed, irrespective of `run_checks` (which is parsed above) if(!is.null(x$request$identify)){ result[[(length(result) + 1)]] <- list(type = "taxa", - identify = x$request$identify) |> - structure(class = "metadata_request") |> + atlas = x$atlas, + identify = x$request$identify) |> + structure(class = c("metadata_request", "list")) |> capture() } if(is.null(x$request$identify) & is.null(x$request$filter)){ @@ -181,10 +182,10 @@ build_query_set_data <- function(x, mint_doi, ...){ # handle authentication if( !is.null(x$request$authenticate) & - potions::pour("atlas", "region") == "Australia" + x$atlas == "Australia" ){ result <- append(result, - list(request_metadata("config") |> capture())) + list(request_metadata("config", from = x$atlas) |> capture())) } # handle `run_checks` @@ -201,44 +202,44 @@ build_query_set_data <- function(x, mint_doi, ...){ if(any(!fields_absent) | x_type %in% c("species-count", "species")) { result <- c(result, - list(request_metadata("fields") |> + list(request_metadata("fields", from = x$atlas) |> select(tidyselect::everything()) |> # needed to ensure GBIF works capture(), - request_metadata("assertions") |> capture())) + request_metadata("assertions", from = x$atlas) |> capture())) }else{ # for living atlases, we need `collapse_fields()` to check the `lsid` field # this isn't required for GBIF which doesn't use `fq` for taxon queries if(!is.null(x$request$identify) & - !is_gbif()){ + x$atlas != "Global"){ result <- c(result, - list(request_metadata("fields") |> capture())) + list(request_metadata("fields", from = x$atlas) |> capture())) } } if(x_type %in% c("occurrences", "media", "species") & - reasons_supported()) { - result[[(length(result) + 1)]] <- request_metadata("reasons") |> + reasons_supported(atlas = x$atlas)) { + result[[(length(result) + 1)]] <- request_metadata("reasons", from = x$atlas) |> capture() } }else{ # if select is required, we need fields even if `run_checks == FALSE` if(!fields_absent[["select"]] | x_type %in% c("occurrences", "species")){ result <- c(result, - list(request_metadata("fields") |> capture(), - request_metadata("assertions") |> capture())) + list(request_metadata("fields", from = x$atlas) |> capture(), + request_metadata("assertions", from = x$atlas) |> capture())) } } # handle `identify()` if(!is.null(x$request$identify) & x_type != "occurrences-doi"){ - result[[(length(result) + 1)]] <- request_metadata() |> + result[[(length(result) + 1)]] <- request_metadata(from = x$atlas) |> identify(x$request$identify) |> capture() } # handle `apply_profile()` if(!is.null(x$request$apply_profile)){ - result[[(length(result) + 1)]] <- request_metadata("profiles") |> + result[[(length(result) + 1)]] <- request_metadata("profiles", from = x$atlas) |> capture() } diff --git a/R/compute_occurrences.R b/R/compute_occurrences.R index a590409a..36dae33e 100644 --- a/R/compute_occurrences.R +++ b/R/compute_occurrences.R @@ -2,7 +2,7 @@ #' @noRd #' @keywords Internal compute_occurrences <- function(.query){ - switch(potions::pour("atlas", "region"), + switch(.query$atlas, "Austria" = compute_occurrences_la_direct(.query), "Global" = compute_occurrences_gbif(.query), compute_occurrences_la(.query)) @@ -29,7 +29,8 @@ compute_occurrences_gbif <- function(.query){ url = glue::glue("https://api.gbif.org/v1/occurrence/download/{post_result}")) |> query_API() |> check_occurrence_response() - c(list(type = "data/occurrences"), + c(list(type = "data/occurrences", + atlas = .query$atlas), status_code) |> structure(class = "computed_query") } @@ -50,7 +51,8 @@ compute_occurrences_la <- function(.query){ cli::cli_text("Request for {n_records} occurrences placed in queue.") } # return a useful object - c(list(type = "data/occurrences"), + c(list(type = "data/occurrences", + atlas = .query$atlas), status_code, list(fields = extract_fields(.query))) |> add_request(.query) |> @@ -77,6 +79,7 @@ compute_occurrences_doi <- function(.query){ .query$download <- NULL result <- query_API(.query) c(list(type = "data/occurrences-doi", + atlas = .query$atlas, url = result$downloadLink, download = TRUE), result[!(names(result) %in% c("request", "downloadLink"))]) |> diff --git a/R/dplyr-compute.R b/R/dplyr-compute.R index 650b09e9..e3e8a048 100644 --- a/R/dplyr-compute.R +++ b/R/dplyr-compute.R @@ -78,7 +78,7 @@ compute.query <- function(x, ...){ "data/occurrences" = compute_occurrences(x), "data/occurrences-doi" = compute_occurrences_doi(x), "data/species" = { - if(is_gbif()){ + if(x$atlas == "Global"){ compute_occurrences(x) }else{ as_computed_query(x) diff --git a/R/dplyr-filter.R b/R/dplyr-filter.R index 518ed734..cede13cf 100644 --- a/R/dplyr-filter.R +++ b/R/dplyr-filter.R @@ -119,7 +119,7 @@ filter.data_request <- function(.data, ...){ dots <- rlang::enquos(..., .ignore_empty = "all") check_named_input(dots) - if(is_gbif()){ + if(.data$atlas == "Global"){ filters <- parse_quosures_data_gbif(dots) # `handle_quosures_GBIF.R` }else{ filters <- parse_quosures_data(dots) # `handle_quosures.R` diff --git a/R/galah_call.R b/R/galah_call.R index 3de6ec14..c6cf5bde 100644 --- a/R/galah_call.R +++ b/R/galah_call.R @@ -7,8 +7,11 @@ #' download data, in the same way that you would wrangle data with `dplyr` and #' the `tidyverse`. It is synonymous with [request_data()]; to query other #' data types call [request_metadata()] or [request_files()]. -#' @param type string: what form of data should be returned? Acceptable values +#' @param type string: What form of data should be returned? Acceptable values #' are specified by the corresponding `request` function +#' @param from string: Which organisation should be queried. Accepts an +#' organisation name, acronym, or region (see [show_all_atlases()] for +#' admissible values) #' @details #' [galah_call()] and any of the `request_` functions are used to begin a #' piped query, which is then actioned using @@ -80,13 +83,25 @@ galah_call <- function(type = c("occurrences", "species-count", "events", "events-count" - )){ + ), + from = NULL){ + # set type if(!missing(type)){ type <- match.arg(type) }else{ type <- "occurrences" } - list(type = type) |> + + # set atlas + if(!is.null(from)){ + atlas <- configure_atlas(from)$region + }else{ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } + + # create object + list(type = type, + atlas = atlas) |> structure(class = c("data_request", "list")) } @@ -112,7 +127,9 @@ request_metadata <- function(type = c("fields", "ranks", "reasons", "taxa", - "identifiers")){ + "identifiers"), + from = NULL){ + # set tupe type_checked <- try(match.arg(type), silent = TRUE) if(inherits(type_checked, "try-error")){ @@ -122,16 +139,35 @@ request_metadata <- function(type = c("fields", x = "Can't find metadata type `{type}`.") |> cli::cli_abort() } - list(type = type_checked) |> + + # set atlas + if(!is.null(from)){ + atlas <- configure_atlas(from)$region + }else{ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } + + # create object + list(type = type_checked, + atlas = atlas) |> structure(class = c("metadata_request", "list")) } #' @rdname galah_call #' @export request_files <- function( - type = "media" + type = "media", + from = NULL # note: option to add `...` here for consistency with `request_data()` ){ - list(type = match.arg(type)) |> + # set atlas + if(!is.null(from)){ + atlas <- configure_atlas(from)$region + }else{ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } + + list(type = match.arg(type), + atlas = atlas) |> structure(class = c("files_request", "list")) } \ No newline at end of file diff --git a/R/galah_filter.R b/R/galah_filter.R index 2c22e9e5..478eed94 100644 --- a/R/galah_filter.R +++ b/R/galah_filter.R @@ -7,7 +7,7 @@ galah_filter <- function(...){ check_named_input(dots) switch(class(dots[[1]])[1], "data_request" = { - if(is_gbif()){ + if(dots[[1]]$atlas == "Global"){ filters <- parse_quosures_data_gbif(dots[-1]) # `handle_quosures_GBIF.R` }else{ filters <- parse_quosures_data(dots[-1]) # `handle_quosures.R` @@ -25,7 +25,10 @@ galah_filter <- function(...){ input$type <- parsed_dots$variable input }, - if(is_gbif()){ + # NOTE: below here is triggered if user calls `galah_filter()` without + # `galah_call()`/`request_data()` first. In this case we only have + # global settings to go on. + if(potions::pour("atlas", "region", .pkg = "galah") == "Global"){ parse_quosures_data_gbif(dots) }else{ parse_quosures_data(dots) diff --git a/R/galah_select.R b/R/galah_select.R index 0c08e0ea..9b0e2fc9 100644 --- a/R/galah_select.R +++ b/R/galah_select.R @@ -4,14 +4,14 @@ galah_select <- function(..., group = NULL){ dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() |> as.list() - if(is_gbif()){ - cli::cli_text("`select()` is not supported for GBIF occurrence downloads API v1: skipping") - if(inherits(dots[[1]], "data_request")){ - dots[[1]] - }else{ - NULL - } - }else{ + # if(is_gbif()){ + # cli::cli_text("`select()` is not supported for GBIF occurrence downloads API v1: skipping") + # if(inherits(dots[[1]], "data_request")){ + # dots[[1]] + # }else{ + # NULL + # } + # }else{ if(length(dots) < 1){ list(quosure = c(), summary = "") |> add_group(group) @@ -27,5 +27,5 @@ galah_select <- function(..., group = NULL){ summary = generate_summary(dots)) |> add_group(group) } - } + # } } \ No newline at end of file diff --git a/R/print.R b/R/print.R index d2ca2a54..aa0797a7 100644 --- a/R/print.R +++ b/R/print.R @@ -101,14 +101,18 @@ switch_slot_text <- function(x, a){ }, "select" = x[[a]]$summary, "group_by" = glue::glue_collapse(x[[a]]$name, sep = " | "), - "apply_profile" ={x[[a]][1]}, - "mint_doi" = {x[[a]][1]}, - "") + # "apply_profile" ={x[[a]][1]}, + # "mint_doi" = {x[[a]][1]}, + x[[a]][1] # default is return slot content + ) } #' @rdname print_galah_objects #' @export print.query <- function(x, ...){ + atlas <- glue::glue("\n + atlas: {x$atlas}") |> + galah_grey() if(!is.null(x$arrange)){ arrange <- galah_pale_green(glue::glue("\n arrange: {x$arrange$variable} ({x$arrange$direction})")) @@ -161,7 +165,8 @@ print.query <- function(x, ...){ } # keep only populated levels - print_list <- list(subtext, # note: need code for url tibbles + print_list <- list(atlas, + subtext, # note: need code for url tibbles select, arrange, slice) @@ -180,6 +185,9 @@ print.prequery <- print.query #' @rdname print_galah_objects #' @export print.computed_query <- function(x, ...){ + atlas <- glue::glue("\n + atlas: {x$atlas}") |> + galah_grey() # calculate arrange/slice info if(!is.null(x$arrange)){ arrange <- galah_pale_green(glue::glue("\n @@ -238,10 +246,11 @@ print.computed_query <- function(x, ...){ } # keep only populated levels - print_list <- list(id, - subtext, # note: need code for url tibbles - arrange, - slice) + print_list <- list(atlas, + id, + subtext, # note: need code for url tibbles + arrange, + slice) print_list <- print_list[!unlist(purrr::map(print_list, is.null))] # print diff --git a/R/read_zip.R b/R/read_zip.R index d9d09fd3..b9d2f434 100644 --- a/R/read_zip.R +++ b/R/read_zip.R @@ -9,6 +9,8 @@ #' a query. #' @param file (character) A file name. Must be a length-1 character ending in #' `.zip`. +#' @param source (character) Where was this file sourced from? Should be one +#' of `"LA"` (for 'Living Atlas'; the default) or `"GBIF"`. #' @examples \dontrun{ #' # set a working directory #' galah_config(directory = "data-raw", @@ -24,8 +26,39 @@ #' x <- read_zip("./data-raw/burrowing_frog_data.zip") #' } #' @export -read_zip <- function(file){ - # basic checks +read_zip <- function(file, source = c("LA", "GBIF")){ + + # FIXME: should be possible to autodetect GBIF files somehow + # Ideally we shouldn't need the user to specify this. + + # check type, file + source <- match.arg(source) + check_zip_filename(file) + + # import file without unzipping + data_files <- zip_data_file_names(file) + result <- switch(source, + "GBIF" = read_zip_gbif(file, data_files), + "LA" = read_zip_la(file, data_files)) + + # add formatted date + attr(result, "modified_date") <- file.info(file)$mtime |> + format("%e %B %Y") |> + trimws() + + # exit safely + if(is.null(result)){ + cli::cli_abort("No data loaded") + }else{ + result + } +} + +#' Internal function to check the `file` argument to `read_zip()` +#' @noRd +#' @keywords Internal +check_zip_filename <- function(file){ + # basic checks if(missing(file)){ cli::cli_abort("`file` is missing, with no default") } @@ -38,71 +71,71 @@ read_zip <- function(file){ if(!grepl(".zip$", file)){ cli::cli_abort("`file` should end in `.zip`") } - # get names of files stored in .zip +} + +#' Internal function to get file names from inside a zip file +#' @noRd +#' @keywords Internal +zip_data_file_names <- function(file){ all_files <- utils::unzip(file, list = TRUE)$Name - # zip files contain a lot of metadata that `galah` does not import - # import only those files that meet our criteria for 'data' - if(is_gbif()){ - available_files <- all_files[grepl(".csv$", all_files)] - result <- unz(description = file, # require lapply? - filename = available_files) |> - readr::read_tsv(col_types = readr::cols()) |> - suppressWarnings() - # Note: DOIs for GBIF are stored in `compute()` stage, not in the zip file - }else{ - available_files <- all_files[stringr::str_detect(all_files, ".csv$") & - !(all_files %in% c("citation.csv", "headings.csv"))] - # grepl("^data|records", all_files)] - # typically files start with data or records, - # but downloads sometimes have the name of the zip file as the csv file - - result <- purrr::map(available_files, - function(a, x){ - # create connection to a specific file within zip - conn <- unz(description = x, - filename = a, - open = "rb") - out <- readr::read_csv(conn, - col_types = readr::cols()) |> - suppressWarnings() - close(conn) - return(out) - }, x = file) |> - dplyr::bind_rows() - # # add doi when mint_doi = TRUE - if(any(all_files == "doi.txt")){ - conn <- unz(description = file, - filename = "doi.txt", - open = "rb") - attr(result, "doi") <- readr::read_file(conn) |> - sub("\\n$", "", x = _) - close(conn) - } - # look for citation in README.html - # BUT first check whether other atlases do this - if(any(all_files == "README.html")){ - conn <- unz(description = file, - filename = "README.html", - open = "rb") - readme <- xml2::read_html(conn) |> - xml2::as_list() |> - unlist() - close(conn) - cite_check <- grepl("cite", names(readme)) - if(any(cite_check)){ - attr(result, "citation") <- readme[cite_check] |> - glue::glue_collapse(sep = "") - } - } + valid_check <- stringr::str_detect(all_files, ".csv$") & + !(all_files %in% c("citation.csv", "headings.csv")) + all_files[valid_check] +} + +#' Internal function to read a zip file from GBIF +#' @noRd +#' @keywords Internal +read_zip_gbif <- function(file, data_files){ + unz(description = file, # require lapply? + filename = data_files) |> + readr::read_tsv(col_types = readr::cols()) |> + suppressWarnings() +} + +#' Internal function to read a zip file from living atlases +#' @noRd +#' @keywords Internal +read_zip_la <- function(file, data_files){ + # read data + result <- purrr::map(data_files, + function(a, x){ + # create connection to a specific file within zip + conn <- unz(description = x, + filename = a, + open = "rb") + out <- readr::read_csv(conn, + col_types = readr::cols()) |> + suppressWarnings() + close(conn) + return(out) + }, x = file) |> + dplyr::bind_rows() + + # # add doi when mint_doi = TRUE + if(any(data_files == "doi.txt")){ + conn <- unz(description = file, + filename = "doi.txt", + open = "rb") + attr(result, "doi") <- readr::read_file(conn) |> + sub("\\n$", "", x = _) + close(conn) } - # add formatted date - attr(result, "modified_date") <- file.info(file)$mtime |> - format("%e %B %Y") |> - trimws() - # exit safely - if(is.null(result)){ - cli::cli_abort("No data loaded") - }else{ - result + + # look for citation in README.html + if(any(data_files == "README.html")){ + conn <- unz(description = file, + filename = "README.html", + open = "rb") + readme <- xml2::read_html(conn) |> + xml2::as_list() |> + unlist() + close(conn) + cite_check <- grepl("cite", names(readme)) + if(any(cite_check)){ + attr(result, "citation") <- readme[cite_check] |> + glue::glue_collapse(sep = "") + } } + result } diff --git a/R/search_all.R b/R/search_all.R index 1af3beb9..3f394b2d 100644 --- a/R/search_all.R +++ b/R/search_all.R @@ -139,14 +139,8 @@ search_all <- function(type, request <- request_metadata() |> filter("media" == query) }else{ - if(is_gbif() & - type %in% c("collections", "datasets", "providers")){ # these support `q` arg in API - request <- request_metadata() |> - filter({{type}} == query) - }else{ - request <- request_metadata(type = type) - run_subsequent_query <- TRUE - } + request <- request_metadata(type = type) + run_subsequent_query <- TRUE } # add all_fields if requested diff --git a/R/url_lookup.R b/R/url_lookup.R index 5b7321ff..6a388ac8 100644 --- a/R/url_lookup.R +++ b/R/url_lookup.R @@ -11,19 +11,15 @@ #' internally. #' @noRd #' @keywords internal -url_lookup <- function(type, - ..., +url_lookup <- function(.query, + ..., quiet = FALSE, error_call = rlang::caller_env()){ dots <- list(...) - current_atlas <- potions::pour("atlas", "region") # get requested url - if(missing(type)){ - type <- dots$type - } url_string <- node_config |> - dplyr::filter(.data$type == {{type}} & - .data$atlas == {{current_atlas}}) |> + dplyr::filter(.data$type == .query$type & + .data$atlas == .query$atlas) |> dplyr::pull(url) # parse as needed if(length(url_string) > 0){ @@ -39,8 +35,8 @@ url_lookup <- function(type, if(quiet){ return(NULL) }else{ - c("No API is available for type `{type}`", - i = "Selected atlas: {current_atlas}", + c("No API is available for type `{.query$type}`", + i = "Selected atlas: {.query$atlas}", i = "Use `show_all_apis()` to list valid API calls") |> cli::cli_abort(call = error_call) } diff --git a/R/utilities_caching.R b/R/utilities_caching.R index ab5d6aa5..bc23dc7e 100644 --- a/R/utilities_caching.R +++ b/R/utilities_caching.R @@ -53,14 +53,13 @@ retrieve_cache <- function(slot_name){ #' Internal function to decide whether to update the internal cache #' @noRd #' @keywords Internal -check_if_cache_update_needed <- function(function_name){ - # get data for checking - df <- retrieve_cache(function_name) - current_atlas <- potions::pour("atlas", "region", .pkg = "galah") +check_if_cache_update_needed <- function(x, # where x is a query object + function_name + ){ # build some checks caching_disabled <- !potions::pour("package", "caching", .pkg = "galah") is_local <- !is.null(attr(df, "ARCHIVED")) - is_wrong_atlas <- attr(df, "region") != current_atlas + is_wrong_atlas <- attr(df, "region") != x$atlas is_too_short <- nrow(df) < 1 # somewhat arbitrary, but catches empty tibbles # evaluate those checks # if any are TRUE, update is needed diff --git a/R/utilities_internal.R b/R/utilities_internal.R index caaa9eea..593c870e 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -297,9 +297,10 @@ add_email_address <- function(x, query){ #' Add a DOI request #' @noRd #' @keywords Internal -add_doi_request <- function(x, mint_doi = FALSE){ - if(isTRUE(mint_doi) & - potions::pour("atlas", "region") == "Australia"){ +add_doi_request <- function(x, + mint_doi = FALSE, + atlas = "Australia"){ + if(isTRUE(mint_doi) & atlas == "Australia"){ x$mintDoi <- TRUE } x @@ -309,33 +310,22 @@ add_doi_request <- function(x, mint_doi = FALSE){ ## Functions to change behaviour depending on selected `atlas` -- ##---------------------------------------------------------------- -#' Internal function for determining if we should call GBIF or not -#' @noRd -#' @keywords Internal -is_gbif <- function(){ - potions::pour("atlas", "region") == "Global" -} - -#' Internal function for determining if we should call ALA or not -#' @noRd -#' @keywords Internal -is_ala <- function(){ - potions::pour("atlas", "region") == "Australia" -} - #' Internal function to populate `groups` arg in `select()` #' @noRd #' @keywords Internal -preset_groups <- function(group_name) { +preset_groups <- function(group_name, atlas = NULL) { + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } cols <- switch(group_name, - "basic" = default_columns(), + "basic" = default_columns(atlas), "event" = c("eventRemarks", "eventTime", "eventID", "eventDate", "samplingEffort", "samplingProtocol"), - "media" = image_fields(), + "media" = image_fields(atlas), "taxonomy" = c("kingdom", "phylum", "class", @@ -351,8 +341,10 @@ preset_groups <- function(group_name) { #' Internal function to specify 'basic' columns in `select()` #' @noRd #' @keywords Internal -default_columns <- function() { - atlas <- potions::pour("atlas", "region") +default_columns <- function(atlas = NULL) { + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } if(atlas %in% c("Austria", "Brazil", "Guatemala", @@ -398,8 +390,10 @@ default_columns <- function() { #' @noRd #' @keywords Internal -image_fields <- function() { - atlas <- potions::pour("atlas", "region") +image_fields <- function(atlas = NULL) { + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } if(atlas %in% c("Austria", "Brazil", "Guatemala", @@ -422,9 +416,11 @@ image_fields <- function() { #' @noRd #' @keywords Internal image_filters <- function(present_fields, + atlas = NULL, error_call = rlang::caller_env()){ - - atlas <- potions::pour("atlas", "region") + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } switch(atlas, "Austria" = "(all_image_url:*)", "Australia" = glue::glue("({present_fields}:*)"), @@ -446,14 +442,14 @@ image_filters <- function(present_fields, #' @noRd #' @keywords Internal -species_facets <- function(){ - atlas <- potions::pour("atlas", "region") - if(atlas %in% c("Australia", - "Flanders", - "France", - "Spain", - "Sweden", - "United Kingdom")) { +species_facets <- function(.query){ + if(.query$atlas %in% + c("Australia", + "Flanders", + "France", + "Spain", + "Sweden", + "United Kingdom")) { "speciesID" }else{ "species_guid" @@ -462,12 +458,12 @@ species_facets <- function(){ #' @noRd #' @keywords Internal -profiles_supported <- function(){ - atlas <- potions::pour("atlas", "region") - if(atlas %in% c("Australia", - "Flanders", - "Sweden", - "Spain")) { +profiles_supported <- function(atlas){ + if(atlas %in% + c("Australia", + "Flanders", + "Sweden", + "Spain")) { TRUE }else{ FALSE @@ -479,8 +475,10 @@ profiles_supported <- function(){ #' checked in `compute()`) #' @noRd #' @keywords Internal -reasons_supported <- function(){ - atlas <- potions::pour("atlas", "region") +reasons_supported <- function(atlas = NULL){ + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } supported_atlases <- request_metadata(type = "apis") |> collect() |> dplyr::filter(.data$type == "metadata/reasons") |> @@ -490,9 +488,10 @@ reasons_supported <- function(){ #' @noRd #' @keywords Internal -media_supported <- function(){ - atlas <- potions::pour("atlas", "region", - .pkg = "galah") +media_supported <- function(atlas = NULL){ + if(is.null(atlas)){ + atlas <- potions::pour("atlas", "region", .pkg = "galah") + } unsupported_atlases <- c("France", "Global") if(atlas %in% unsupported_atlases){ cli::cli_abort("`atlas_media` is not supported for atlas = {atlas}") diff --git a/man/galah_call.Rd b/man/galah_call.Rd index a42b4b60..d8fd203d 100644 --- a/man/galah_call.Rd +++ b/man/galah_call.Rd @@ -9,25 +9,32 @@ \usage{ galah_call( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", "events", "events-count") + "species-count", "events", "events-count"), + from = NULL ) request_data( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", "events", "events-count") + "species-count", "events", "events-count"), + from = NULL ) request_metadata( type = c("fields", "apis", "assertions", "atlases", "collections", "config", "datasets", "licences", "lists", "media", "profiles", "providers", "ranks", - "reasons", "taxa", "identifiers") + "reasons", "taxa", "identifiers"), + from = NULL ) -request_files(type = "media") +request_files(type = "media", from = NULL) } \arguments{ -\item{type}{string: what form of data should be returned? Acceptable values +\item{type}{string: What form of data should be returned? Acceptable values are specified by the corresponding \code{request} function} + +\item{from}{string: Which organisation should be queried. Accepts an +organisation name, acronym, or region (see \code{\link[=show_all_atlases]{show_all_atlases()}} for +admissible values)} } \value{ Each sub-function returns a different object class: diff --git a/man/read_zip.Rd b/man/read_zip.Rd index d5428cba..d379fb3c 100644 --- a/man/read_zip.Rd +++ b/man/read_zip.Rd @@ -4,11 +4,14 @@ \alias{read_zip} \title{Read downloaded data from a zip file} \usage{ -read_zip(file) +read_zip(file, source = c("LA", "GBIF")) } \arguments{ \item{file}{(character) A file name. Must be a length-1 character ending in \code{.zip}.} + +\item{source}{(character) Where was this file sourced from? Should be one +of \code{"LA"} (for 'Living Atlas'; the default) or \code{"GBIF"}.} } \description{ Living atlases supply data downloads as zip files. This function reads these diff --git a/tests/testthat/test-dplyr-filter.R b/tests/testthat/test-dplyr-filter.R index 6eb5b568..ec621cf4 100644 --- a/tests/testthat/test-dplyr-filter.R +++ b/tests/testthat/test-dplyr-filter.R @@ -1,14 +1,10 @@ # set up quiet functions for testing reasons purrr_collect <- purrr::quietly(collect.data_request) -test_that("`filter()` works for a single 'equals' argument", { - filters <- galah_filter(year == 2010) - expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) - expect_equal(nrow(filters), 1) -}) - test_that("`filter()` gives an error for single equals sign", { - expect_error(galah_filter(year = 2010)) + galah_call() |> + filter(year = 2010) |> + expect_error() }) test_that("`filter()` works with assertions", { @@ -99,63 +95,72 @@ test_that("`filter()` handles assertions and taxa", { expect_equal(problem_families$result$count[1], top_family$count) }) -test_that("`filter()` returns empty tibble when no arguments specified", { - filters <- galah_filter() - expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) - expect_equal(nrow(filters), 0) -}) - test_that("`filter()` works for two 'equals' arguments", { - filters <- galah_filter(year == 2010, basisOfRecord == "HUMAN_OBSERVATION") - expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) + filters <- request_data() |> + filter(year == 2010, basisOfRecord == "HUMAN_OBSERVATION") |> + purrr::pluck("filter") + expect_s3_class(filters, c("data_filter", "tbl_df", "tbl", "data.frame")) expect_equal(nrow(filters), 2) expect_equal(filters$variable, c("year", "basisOfRecord")) }) test_that("`filter()` works for two arguments of the same variable", { - filters <- galah_filter(year >= 2010, year <= 2015) - expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) + filters <- request_data() |> + filter(year >= 2010, year <= 2015) |> + purrr::pluck("filter") + expect_s3_class(filters, c("data_filter", "tbl_df", "tbl", "data.frame")) expect_equal(nrow(filters), 2) expect_equal(">=", filters$logical[1]) }) test_that("`filter()` works with urls", { - filters <- galah_filter( - taxonConceptID == "https://biodiversity.org.au/afd/taxa/065f1da4-53cd-40b8-a396-80fa5c74dedd") + filters <- request_data() |> + filter(taxonConceptID == "https://biodiversity.org.au/afd/taxa/065f1da4-53cd-40b8-a396-80fa5c74dedd") |> + purrr::pluck("filter") expect_true(nchar(filters$query) > 70) }) test_that("`filter()` parses '&' correctly", { - filters <- galah_filter(year >= 2010 & year < 2020) + filters <- request_data() |> + filter(year >= 2010 & year < 2020) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_equal(filters$value, "2010&2020") expect_equal(filters$query, "year:[2010 TO *] AND year:[* TO 2020] AND -(year:\"2020\")") }) test_that("`filter()` handles numeric queries for text fields", { - filters <- galah_filter(cl22 >= "Tasmania") + filters <- request_data() |> + filter(cl22 >= "Tasmania") |> + purrr::pluck("filter") expect_equal(filters$query, "cl22:[Tasmania TO *]") }) test_that("`filter()` handles OR statements", { - filters <- galah_filter(year == 2010 | year == 2020) + filters <- request_data() |> + filter(year == 2010 | year == 2020) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_equal(filters$value, "2010|2020") expect_equal(filters$query, "((year:\"2010\") OR (year:\"2020\"))") }) test_that("`filter()` handles OR statements", { - filters <- galah_filter(raw_scientificName == "Litoria jervisiensis" | - raw_scientificName == "Litoria peronii") + filters <- request_data() |> + filter(raw_scientificName == "Litoria jervisiensis" | + raw_scientificName == "Litoria peronii") |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_equal(filters$query, "((raw_scientificName:\"Litoria jervisiensis\") OR (raw_scientificName:\"Litoria peronii\"))") }) test_that("`filter()` works with 3 OR statements", { - filters <- galah_filter(basisOfRecord == "HumanObservation" | - basisOfRecord == "MachineObservation" | - basisOfRecord == "PreservedSpecimen") + filters <- request_data() |> + filter(basisOfRecord == "HumanObservation" | + basisOfRecord == "MachineObservation" | + basisOfRecord == "PreservedSpecimen") |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_equal(filters$value, "HumanObservation|MachineObservation|PreservedSpecimen") expect_equal(filters$query, @@ -163,57 +168,74 @@ test_that("`filter()` works with 3 OR statements", { }) test_that("`filter()` handles exclusion", { - filters <- galah_filter(year >= 2010, year != 2021) + filters <- request_data() |> + filter(year >= 2010, year != 2021) |> + purrr::pluck("filter") expect_equal(nrow(filters), 2) expect_equal(filters$query, c("year:[2010 TO *]", "-(year:\"2021\")")) }) test_that("`filter()` handles multiple exclusions", { - filters <- galah_filter(!(stateProvince == "Victoria" & year == 2021)) + filters <- request_data() |> + filter(!(stateProvince == "Victoria" & year == 2021)) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_equal(filters$query, "-(stateProvince:\"Victoria\") AND -(year:\"2021\")") }) test_that("`filter()` handles three terms at once", { - filters <- galah_filter( - basisOfRecord == "HumanObservation", - year >= 2010, - stateProvince == "New South Wales") + filters <- request_data() |> + filter(basisOfRecord == "HumanObservation", + year >= 2010, + stateProvince == "New South Wales") |> + purrr::pluck("filter") expect_equal(nrow(filters),3) expect_equal(filters$query, c("(basisOfRecord:\"HumanObservation\")","year:[2010 TO *]","(stateProvince:\"New South Wales\")")) }) test_that("`filter()` treats `c()` as an OR for numerics", { - filters <- galah_filter(year == c(2010, 2021)) + filters <- request_data() |> + filter(year == c(2010, 2021)) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) }) test_that("`filter()` treats `c()` as an OR for strings", { - filters <- galah_filter(multimedia == c("Image", "Sound", "Video")) + filters <- request_data() |> + filter(multimedia == c("Image", "Sound", "Video")) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_true(grepl("multimedia:\"Image\"", filters$query)) }) test_that("`filter()` can take an object as a value", { value <- "2010" - filters <- galah_filter(year == value) + filters <- request_data() |> + filter(year == value) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_match(filters$query, "(year:\"2010\")") }) test_that("`filter()` returns error when equations are passed as a string", { - expect_error(galah_filter("year == 2010")) + request_data() |> + filter("year == 2010") |> + expect_error() }) test_that("`filter()` handles taxonomic queries", { skip_if_offline(); skip_on_ci() - filters <- galah_filter(taxonConceptID == search_taxa("Animalia")$taxon_concept_id) + filters <- request_data() |> + filter(taxonConceptID == search_taxa("Animalia")$taxon_concept_id) |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_false(grepl("search_taxa", filters$query)) }) test_that("`filter()` handles taxonomic queries when passed as a string", { - filters <- galah_filter(taxonConceptID == "https://biodiversity.org.au/afd/taxa/012a1234") + filters <- request_data() |> + filter(taxonConceptID == "https://biodiversity.org.au/afd/taxa/012a1234") |> + purrr::pluck("filter") expect_equal(nrow(filters), 1) expect_false(grepl("search_taxa", filters$query)) expect_true(grepl("taxonConceptID", filters$query)) @@ -221,9 +243,10 @@ test_that("`filter()` handles taxonomic queries when passed as a string", { test_that("`filter()` handles taxonomic exclusions", { skip_if_offline(); skip_on_ci() - filters <- galah_filter( - taxonConceptID == search_taxa("Animalia")$taxon_concept_id, - taxonConceptID != search_taxa("Chordata")$taxon_concept_id) + filters <- request_data() |> + filter(taxonConceptID == search_taxa("Animalia")$taxon_concept_id, + taxonConceptID != search_taxa("Chordata")$taxon_concept_id) |> + purrr::pluck("filter") expect_equal(nrow(filters), 2) expect_false(any(grepl("search_taxa", filters$query))) }) @@ -239,52 +262,76 @@ test_that("`filter()` handles lsid as an input", { count() |> collapse() expect_s3_class(query, "query") - expect_equal(length(query), 4) - expect_equal(names(query), c("type", + expect_equal(length(query), 5) + expect_equal(names(query), c("type", + "atlas", "url", "headers", "request")) }) test_that("`filter()` handles different fields separated by OR", { - filters <- galah_filter(phylum == "Chordata" | kingdom == "Plantae") + filters <- request_data() |> + filter(phylum == "Chordata" | kingdom == "Plantae") |> + purrr::pluck("filter") expect_equal(filters$query, "((phylum:\"Chordata\") OR (kingdom:\"Plantae\"))") }) test_that("`filter()` fails when given invalid AND syntax", { - expect_error(galah_filter(year >= 2020 & 2021)) + request_data() |> + filter(year >= 2020 & 2021) |> + expect_error() }) test_that("`filter()` fails when given invalid OR syntax", { - expect_error(galah_filter(year == 2020 | 2021)) + request_data() |> + filter(year == 2020 | 2021) |> + expect_error() }) # TODOL test that `filter()` handles between() even with multiple filters # NOTE: not implemented yet test_that("`filter()` accepts an OR statement for different fields", { - filters <- galah_filter(year == 2010 | basisOfRecord == "PRESERVED_SPECIMEN") + filters <- request_data() |> + filter(year == 2010 | basisOfRecord == "PRESERVED_SPECIMEN") |> + purrr::pluck("filter") expect_true(grepl("year", filters$query) & grepl("basisOfRecord", filters$query)) }) -test_that("`filter()` handles is.na() even with multiple filters", { - filter_single <- galah_filter(is.na(eventDate)) - filter_multiple <- galah_filter(is.na(eventDate), year > 2010) - expect_equal(nrow(filter_single), 1) - expect_equal(nrow(filter_multiple), 2) - expect_true(grepl("(*:* AND -eventDate:*)", filter_single$query)) - expect_true(grepl("(*:* AND -eventDate:*)", filter_multiple$query[[1]])) +test_that("`filter()` handles is.na()", { + filters <- request_data() |> + filter(is.na(eventDate)) |> + purrr::pluck("filter") + expect_equal(nrow(filters), 1) + expect_true(grepl("(*:* AND -eventDate:*)", filters$query)) +}) + +test_that("`filter()` handles is.na() with multiple filters", { + filters <- request_data() |> + filter(is.na(eventDate), year > 2010) |> + purrr::pluck("filter") + expect_equal(nrow(filters), 2) + expect_true(grepl("(*:* AND -eventDate:*)", filters$query[[1]])) }) -test_that("`filter()` handles %in% even with multiple filters", { +test_that("`filter()` handles %in%", { list_of_years <- 2020:2022 - filter_single <- galah_filter(year %in% list_of_years) - filter_multiple <- galah_filter(year %in% list_of_years, cl22 == "Tasmania") - expect_equal(nrow(filter_single), 1) - expect_equal(nrow(filter_multiple), 2) - expect_equal("((year:\"2020\") OR (year:\"2021\") OR (year:\"2022\"))", filter_single$query[[1]]) - expect_equal("((year:\"2020\") OR (year:\"2021\") OR (year:\"2022\"))", filter_multiple$query[[1]]) + filters <- request_data() |> + filter(year %in% list_of_years) |> + purrr::pluck("filter") + expect_equal(nrow(filters), 1) + expect_equal("((year:\"2020\") OR (year:\"2021\") OR (year:\"2022\"))", filters$query[[1]]) +}) + +test_that("`filter()` handles %in% with multiple filters", { + list_of_years <- 2020:2022 + filters <- request_data() |> + filter(year %in% list_of_years, cl22 == "Tasmania") |> + purrr::pluck("filter") + expect_equal(nrow(filters), 2) + expect_equal("((year:\"2020\") OR (year:\"2021\") OR (year:\"2022\"))", filters$query[[1]]) }) test_that("`filter()` parses fields correctly with is.na()", { @@ -327,34 +374,35 @@ test_that("`filter()` handles apostrophes (') correctly", { names <- c("Australia's Virtual Herbarium", "iNaturalist observations", "iNaturalist research-grade observations") - filter <- galah_filter(datasetName %in% names)$query - query <- galah_call() |> - galah_filter(datasetName %in% names) |> - atlas_counts() - expect_equal(nrow(query), 1) # returns result - expect_gte(query$count[1], 1) - expect_match(filter, "\\(datasetName:\\\"Australia's Virtual Herbarium\\\"") + query <- request_data() |> + filter(datasetName %in% names) |> + count() + filters <- purrr::pluck(query, "filter") + result <- collect(query) + expect_equal(nrow(result), 1) # returns result + expect_gte(result$count[1], 1) + expect_match(filters$query, "\\(datasetName:\\\"Australia's Virtual Herbarium\\\"") }) test_that("`filter()` handles multiple values with brackets correctly", { skip_if_offline(); skip_on_ci() - filter <- galah_filter( - scientificName == c("Aviceda (Aviceda) subcristata", - "Todiramphus (Todiramphus) sanctus"))$query - query <- galah_call() |> - galah_filter(scientificName == c("Aviceda (Aviceda) subcristata", - "Todiramphus (Todiramphus) sanctus")) |> - atlas_counts() - expect_equal(nrow(query), 1) # returns result - expect_gte(query$count[1], 1) - expect_match(filter, "\\(scientificName:\\\"Aviceda \\(Aviceda\\) subcristata\\\"\\)") + query <- request_data() |> + filter(scientificName == c("Aviceda (Aviceda) subcristata", + "Todiramphus (Todiramphus) sanctus")) |> + count() + filters <- purrr::pluck(query, "filter") + result <- collect(query) + expect_equal(nrow(result), 1) # returns result + expect_gte(result$count[1], 1) + expect_match(filters$query, "\\(scientificName:\\\"Aviceda \\(Aviceda\\) subcristata\\\"\\)") }) test_that("`filter()` builds correct query with `!`, `%in%`, `c()` and `identify()`", { ibra_subset <- c("Brigalow Belt North", "Brigalow Belt South", "Central Mackay Coast") - query <- request_data(type = "occurrences-count") |> + query <- request_data() |> identify("Crinia signifera") |> - filter(!cl1048 %in% ibra_subset) + filter(!cl1048 %in% ibra_subset) |> + count() expect_equal(query$filter$query, c("-(cl1048:\"Brigalow Belt North\") OR -(cl1048:\"Brigalow Belt South\") OR -(cl1048:\"Central Mackay Coast\")")) }) @@ -362,46 +410,33 @@ test_that("`filter()` accepts {{}} on lhs of formula", { skip_if_offline(); skip_on_ci() field <- "species" result <- galah_call() |> - galah_filter({{field}} == "Eolophus roseicapilla") |> - atlas_counts() - expect_s3_class(result, c("tbl_df", "tbl", "data.frame")) - expect_equal(nrow(result), 1) - expect_gte(result$count[1], 100) - result2 <- request_data() |> filter({{field}} == "Eolophus roseicapilla") |> count() |> collect() - expect_equal(result, result2) + expect_s3_class(result, c("tbl_df", "tbl", "data.frame")) + expect_equal(nrow(result), 1) + expect_gte(result$count[1], 100) }) -test_that("`filter()` handles `method = 'data'` correctly", { - x <- request_data() |> - filter(year == 2010) - expect_s3_class(x, "data_request") - expect_false(is.null(x$filter)) - expect_equal(colnames(x$filter), c("variable", "logical", "value", "query")) - y <- request_data() |> filter(year == 2010) - expect_equal(x, y) -}) -test_that("`filter() handles `method = 'metadata'` correctly", { +test_that("`filter() works for class `metadata_request`", { x <- request_metadata() |> filter(field == cl22) expect_s3_class(x, "metadata_request") - expect_equal(length(x), 2) - expect_equal(names(x), c("type", "filter")) + expect_equal(length(x), 3) + expect_equal(names(x), c("type", "atlas", "filter")) expect_equal(names(x$filter), c("variable", "value")) }) -test_that("`filter() handles `method = 'files'` correctly", { +test_that("`filter()` works for class `files_request`", { x <- tibble::tibble( id = c(1, 2), images = c("1234", "5678")) y <- request_files() |> filter(media == x) expect_s3_class(y, "files_request") - expect_equal(length(y), 2) - expect_equal(names(y), c("type", "filter")) + expect_equal(length(y), 3) + expect_equal(names(y), c("type", "atlas", "filter")) expect_equal(y$filter$data, x) }) diff --git a/tests/testthat/test-galah_call.R b/tests/testthat/test-galah_call.R index 91aa349b..4048c09e 100644 --- a/tests/testthat/test-galah_call.R +++ b/tests/testthat/test-galah_call.R @@ -1,5 +1,5 @@ test_that("`galah_call()` builds objects of class 'data_request' by default", { - expect_equal(length(galah_call()), 1) + expect_equal(length(galah_call()), 2) expect_s3_class(galah_call(), "data_request") }) @@ -12,7 +12,27 @@ test_that("`request_` functions build correct object classes", { expect_true(y$type == "media") }) -test_that("`galah_call()` works with all `galah_` functions", { +test_that("request_data(from = X) sets `atlas` slot in later objects", { + query_initial <- request_data(from = "GBIF") |> + filter(year == 2010) |> + count() + expect_equal(query_initial$atlas, "Global") + query_capture <- capture(query_initial) + expect_equal(query_capture$atlas, "Global") + query_compound <- compound(query_capture) + compound_atlases <- purrr::map(query_compound, + \(a){purrr::pluck(a, "atlas")}) |> + unlist() + expect_true(all(compound_atlases == "Global")) + query_collapse <- collapse(query_compound) + expect_equal(query_collapse$atlas, "Global") + query_compute <- compute(query_collapse) + expect_equal(query_compute$atlas, "Global") + collect(query_compute) + # add test to ensure that galah_config() hasn't been updated to GBIF +}) + +test_that("`galah_call()` works with all `dplyr` functions", { skip_if_offline(); skip_on_ci() result <- galah_call() |> identify("Litoria") |> @@ -30,20 +50,20 @@ test_that("`galah_call()` works with all `galah_` functions", { # ensure content is added in same order as supplied expect_equal( names(result), - c("type", "identify", "filter", "select", "apply_profile", + c("type", "atlas", "identify", "filter", "select", "apply_profile", "geolocate", "group_by", "arrange")) }) test_that("`galah_call()` works irrespective of `galah_` function order", { skip_if_offline(); skip_on_ci() result <- galah_call() |> - galah_apply_profile(ALA) |> - galah_group_by(year, basisOfRecord) |> + apply_profile(ALA) |> + group_by(year, basisOfRecord) |> arrange(basisOfRecord) |> - galah_geolocate("POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))") |> - galah_select(year) |> - galah_filter(year == 2021, cl22 == "Tasmania") |> - galah_identify("Litoria") + geolocate("POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))") |> + select(year) |> + filter(year == 2021, cl22 == "Tasmania") |> + identify("Litoria") expect_false(any(unlist(lapply(result, is.null)))) }) diff --git a/tests/testthat/test-galah_filter.R b/tests/testthat/test-galah_filter.R new file mode 100644 index 00000000..eefdda0d --- /dev/null +++ b/tests/testthat/test-galah_filter.R @@ -0,0 +1,15 @@ +test_that("`filter()` works for a single 'equals' argument", { + filters <- galah_filter(year == 2010) + expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) + expect_equal(nrow(filters), 1) +}) + +test_that("`filter()` gives an error for single equals sign", { + expect_error(galah_filter(year = 2010)) +}) + +test_that("`filter()` returns empty tibble when no arguments specified", { + filters <- galah_filter() + expect_s3_class(filters, c("tbl_df", "tbl", "data.frame")) + expect_equal(nrow(filters), 0) +}) diff --git a/tests/testthat/test-galah_identify.R b/tests/testthat/test-galah_identify.R index be81ddb4..692a24cd 100644 --- a/tests/testthat/test-galah_identify.R +++ b/tests/testthat/test-galah_identify.R @@ -80,8 +80,8 @@ test_that("`identify()` warns when identifiers are partially returned", { test_that("`identify()` truncates unmatched list of taxa at 3 ", { skip_if_offline(); skip_on_ci() x <- galah_call() |> - galah_identify("Litoria", "blarghy", "blorp", "florp", "skorp") |> - galah_filter(year == 2020) |> + identify("Litoria", "blarghy", "blorp", "florp", "skorp") |> + filter(year == 2020) |> count() |> purrr_compute() expected_messages <- c( From 8d7f2ed2ea3220043f906c789ff1b480dc2c3522 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 1 Apr 2026 10:07:16 +1100 Subject: [PATCH 09/70] First pass at enabling authentication across atlases (#296) - new API added to `node_config.csv` - code in `queryy_API.R` amended to cache more, and check whether cached items are from the currently-requested atlas - add `authentication_supported()` function for consistency with other systems - add early testing --- R/check.R | 4 +- R/compound.R | 2 +- R/query_API.R | 46 +++++++++++++------ R/sysdata.rda | Bin 7421 -> 7432 bytes R/utilities_internal.R | 14 ++++++ data-raw/node_config.csv | 1 + tests/testthat/test-international-Flanders.R | 14 ++++++ 7 files changed, 64 insertions(+), 17 deletions(-) diff --git a/R/check.R b/R/check.R index 7f6a00d4..d2f1c436 100644 --- a/R/check.R +++ b/R/check.R @@ -39,9 +39,9 @@ check_authentication <- function(x){ x <- x |> authenticate() } # reset authentication to `NULL` if requested for a non-supported atlas - if(x$atlas != "Australia" & + if(!authentication_supported(x$atlas) & !is.null(x$authenticate)){ - cli::cli_warn("Authentication not supported for atlas {atlas}: skipping") + cli::cli_warn("Authentication not supported for atlas {x$atlas}: skipping") x$authenticate <- NULL } x diff --git a/R/compound.R b/R/compound.R index ce0b2041..792bc87f 100644 --- a/R/compound.R +++ b/R/compound.R @@ -182,7 +182,7 @@ build_query_set_data <- function(x, mint_doi, ...){ # handle authentication if( !is.null(x$request$authenticate) & - x$atlas == "Australia" + authentication_supported(x$atlas) ){ result <- append(result, list(request_metadata("config", from = x$atlas) |> capture())) diff --git a/R/query_API.R b/R/query_API.R index 3262348c..fb2bebd5 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -77,16 +77,16 @@ query_API_internal <- function(.query, if(!is.null(.query$request$authenticate) & .query$type != "metadata/config" # necessary to prevent circular problems ){ - + # check whether config data is available - auth_info <- get_auth_info() + auth_info <- get_auth_info(.query) query <- query |> httr2::req_oauth_auth_code( client = auth_info$client, auth_url = dplyr::pull(auth_info$config, "authorize_url"), scope = dplyr::pull(auth_info$config, "scopes"), pkce = TRUE, - cache_disk = FALSE) # purrr::pluck(.query, "authenticate", "cache_disk")) + cache_disk = .query$request$authenticate$cache_disk) } # then handle downloads @@ -102,7 +102,7 @@ query_API_internal <- function(.query, } else { query |> httr2::req_perform(path = .query$file, - verbosity = 0) + verbosity = 3) } # then other pings, which should resolve quickly # and can be allowed to fail otherwise @@ -165,31 +165,49 @@ add_options <- function(req, options){ #' get a client, and if it doesn't exist, make one #' @noRd #' @keywords Internal -get_auth_info <- function(error_call = rlang::caller_env()){ - x <- retrieve_cache("client") # this is cached by build_auth_client() - auth_config <- show_all_config() # handle download /retrieval of config info - if(is.null(x)){ - x <- build_auth_client(auth_config) - } +get_auth_info <- function(.query, + error_call = rlang::caller_env()){ + # get config info + auth_config <- request_metadata(type = "config", + from = .query$atlas) |> + collect() + # use this to get a client + auth_client <- build_auth_client(auth_config, + atlas = .query$atlas) + # note that both of the above functions check caches first, so it's not + # inefficient to run both + # if still can't get a client, you might be offline - if(is.null(x)){ + if(is.null(auth_client)){ cli::cli_abort(c("Unable to generate an authentication client", i = "You might be offline"), call = error_call) } + # otherwise, return the requested information list(config = auth_config, - client = x) + client = auth_client) } #' create a client object #' @noRd #' @keywords Internal -build_auth_client <- function(config){ +build_auth_client <- function(config, atlas){ + + # check whether a client has been previously cached for this atlas + cache_check <- retrieve_cache("client") + if(!is.null(cache_check)){ + if(attr(cache_check, "region") == atlas){ + cache_check + } + } + # otherwise, build new client result <- httr2::oauth_client( id = dplyr::pull(config, "client_id"), token_url = dplyr::pull(config, "token_url"), auth = "body", - name = "galah") + name = "galah") |> + update_attributes(type = "client", + atlas = atlas) update_cache(client = result) result } \ No newline at end of file diff --git a/R/sysdata.rda b/R/sysdata.rda index 1b74d9c537a08f5768df1414f96edf8fba8c215d..6b01887ceb9c652e05c0c489650fed09246eaf88 100644 GIT binary patch literal 7432 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 GE=9Ph^+|>R literal 7421 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 diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 593c870e..0ed848ba 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -456,6 +456,20 @@ species_facets <- function(.query){ } } +#' Internal function to test whether authentication is supported +#' @noRd +#' @keywords Internal +authentication_supported <- function(atlas){ + if(atlas %in% + c("Australia", + "Flanders")){ + TRUE +}else{ + FALSE +} +} + +#' Internal function to test whether profiles are supported #' @noRd #' @keywords Internal profiles_supported <- function(atlas){ diff --git a/data-raw/node_config.csv b/data-raw/node_config.csv index fe0cdba7..6157e748 100644 --- a/data-raw/node_config.csv +++ b/data-raw/node_config.csv @@ -86,6 +86,7 @@ Flanders,data/species-count,https://natuurdata.inbo.be/biocache-service/occurren Flanders,files/media,https://natuurdata.inbo.be/image-service/image/{id}/{size},TRUE Flanders,metadata/assertions,https://natuurdata.inbo.be/biocache-service/assertions/codes,TRUE Flanders,metadata/collections,https://natuurdata.inbo.be/collectory/ws/collection,TRUE +Flanders,metadata/config,https://natuurdata.inbo.be/galah/config,TRUE Flanders,metadata/datasets,https://natuurdata.inbo.be/collectory/ws/dataResource,TRUE Flanders,metadata/fields,https://natuurdata.inbo.be/biocache-service/index/fields,TRUE Flanders,metadata/fields-unnest,https://natuurdata.inbo.be/biocache-service/occurrence/facets,TRUE diff --git a/tests/testthat/test-international-Flanders.R b/tests/testthat/test-international-Flanders.R index 49acfe23..ff28ac8c 100644 --- a/tests/testthat/test-international-Flanders.R +++ b/tests/testthat/test-international-Flanders.R @@ -248,6 +248,20 @@ test_that("`atlas_species()` works for Flanders", { expect_s3_class(spp, c("tbl_df", "tbl", "data.frame")) }) +test_that("`authenticate()` works for Flanders", { + skip_if_offline(); skip_on_ci() + galah_config( + download_reason_id = 10, + send_email = FALSE) + x <- request_data(from = "Flanders") |> + authenticate() |> + identify("Canis") |> + filter(year == 2020) |> + compound() + y <- collapse(x) + z <- collect(y) +}) + test_that("`atlas_occurrences()` works for Flanders", { skip_if_offline(); skip_on_ci() galah_config( From 865bd807bc3b8adf8a26c9df6c1e6dc7adb4dd14 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 1 Apr 2026 12:48:45 +1100 Subject: [PATCH 10/70] Improved behaviour in `read_zip()` when source atlas is unknown (#297) Have added efficient delimiter detection to avoid needing to specify the atlas in use. This works because GBIF uses tabs and everyone else uses commas. Plus return API call verbosity to normal --- R/query_API.R | 2 +- R/read_zip.R | 45 +++++++++++++++++++++++++++++++++------------ man/read_zip.Rd | 10 +++------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/R/query_API.R b/R/query_API.R index fb2bebd5..11018598 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -102,7 +102,7 @@ query_API_internal <- function(.query, } else { query |> httr2::req_perform(path = .query$file, - verbosity = 3) + verbosity = 0) } # then other pings, which should resolve quickly # and can be allowed to fail otherwise diff --git a/R/read_zip.R b/R/read_zip.R index b9d2f434..52701e07 100644 --- a/R/read_zip.R +++ b/R/read_zip.R @@ -3,14 +3,11 @@ #' @description #' Living atlases supply data downloads as zip files. This function reads these #' data efficiently, i.e. without unzipping them first, using the `readr` -#' package. Although this function has been part of galah for some time, it was -#' previously internal to [atlas_occurrences()]. It has been exported now to -#' support easy re-importing of downloaded files, without the need to re-run +#' package. Although this function is mostly called internally, it is exported +#' to allow easy re-importing of downloaded files, without the need to re-run #' a query. #' @param file (character) A file name. Must be a length-1 character ending in #' `.zip`. -#' @param source (character) Where was this file sourced from? Should be one -#' of `"LA"` (for 'Living Atlas'; the default) or `"GBIF"`. #' @examples \dontrun{ #' # set a working directory #' galah_config(directory = "data-raw", @@ -26,20 +23,27 @@ #' x <- read_zip("./data-raw/burrowing_frog_data.zip") #' } #' @export -read_zip <- function(file, source = c("LA", "GBIF")){ +read_zip <- function(file){ # FIXME: should be possible to autodetect GBIF files somehow # Ideally we shouldn't need the user to specify this. - # check type, file - source <- match.arg(source) + # check file check_zip_filename(file) - # import file without unzipping + # find data files _within_ the zip file data_files <- zip_data_file_names(file) - result <- switch(source, - "GBIF" = read_zip_gbif(file, data_files), - "LA" = read_zip_la(file, data_files)) + + # if none, abort + if(length(data_files) < 1){ + cli::cli_abort("No data files detected") + } + + # import correctly depending on the delimiter + delim <- check_delimiter(file, data_files[1]) + result <- switch(delim, + "tsv" = read_zip_gbif(file, data_files), + "csv" = read_zip_la(file, data_files)) # add formatted date attr(result, "modified_date") <- file.info(file)$mtime |> @@ -83,6 +87,23 @@ zip_data_file_names <- function(file){ all_files[valid_check] } +#' get the delimiter for this file type +#' @noRd +#' @keywords Internal +check_delimiter <- function(file, data_file){ + # get header row of first file *only* + x <- unz(description = file, + filename = data_file) |> + readr::read_lines(n_max = 1) + n_tabs <- stringr::str_count(x, "\t") + n_commas <- stringr::str_count(x, ",") + if(n_tabs > n_commas){ + "tsv" + }else{ + "csv" + } +} + #' Internal function to read a zip file from GBIF #' @noRd #' @keywords Internal diff --git a/man/read_zip.Rd b/man/read_zip.Rd index d379fb3c..76f1a53d 100644 --- a/man/read_zip.Rd +++ b/man/read_zip.Rd @@ -4,21 +4,17 @@ \alias{read_zip} \title{Read downloaded data from a zip file} \usage{ -read_zip(file, source = c("LA", "GBIF")) +read_zip(file) } \arguments{ \item{file}{(character) A file name. Must be a length-1 character ending in \code{.zip}.} - -\item{source}{(character) Where was this file sourced from? Should be one -of \code{"LA"} (for 'Living Atlas'; the default) or \code{"GBIF"}.} } \description{ Living atlases supply data downloads as zip files. This function reads these data efficiently, i.e. without unzipping them first, using the \code{readr} -package. Although this function has been part of galah for some time, it was -previously internal to \code{\link[=atlas_occurrences]{atlas_occurrences()}}. It has been exported now to -support easy re-importing of downloaded files, without the need to re-run +package. Although this function is mostly called internally, it is exported +to allow easy re-importing of downloaded files, without the need to re-run a query. } \examples{ From c57adb253435f25d33101529531429247fab87f8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 1 Apr 2026 12:51:14 +1100 Subject: [PATCH 11/70] Support `select()` for GBIF occurrence queries Because these are post-hoc, we use the same capture approach as for metadata. Also minor bug fix for type = occurrences-glimpse, reorder printing of `galah_config()` --- R/capture.R | 17 ++++++- R/capture_occurrences.R | 3 +- R/check.R | 7 +-- R/collect_occurrences.R | 10 +++- R/compute_occurrences.R | 3 +- R/dplyr-select.R | 15 ++---- R/galah_select.R | 39 ++++++---------- R/print.R | 57 +++++++++++++++-------- R/utilities_internal.R | 10 ++++ tests/testthat/test-international-GBIF.R | 58 ++++++++++++++++-------- 10 files changed, 137 insertions(+), 82 deletions(-) diff --git a/R/capture.R b/R/capture.R index 01ea9889..49486a42 100644 --- a/R/capture.R +++ b/R/capture.R @@ -300,7 +300,12 @@ check_distinct_count_groupby <- function(x){ count_switch() |> dplyr::select(-dplyr::any_of(c("label", "i18nCode", "fq"))) }else{ - dplyr::select(x, group = "basic") # assumes type = "occurrences" + # assumes type = "occurrences" + if(x$atlas == "Global"){ + dplyr::select(x, tidyselect::everything()) # for backwards-compatibility + }else{ + dplyr::select(x, group = "basic") + } } } # end has_select } # end has_distinct @@ -416,7 +421,15 @@ enforce_select_query_metadata <- function(x){ enforce_select_query_data <- function(x){ if(is.null(x$select)){ switch(x$type, - "occurrences" = dplyr::select(x, group = "basic"), + # This section _should_ be handled above by check_distinct_count_groupby() + # probably needs better integration + "occurrences" = { + if(x$atlas == "Global"){ + dplyr::select(x, tidyselect::everything()) # for backwards-compatibility + }else{ + dplyr::select(x, group = "basic") + } + }, "occurrences-count" = dplyr::select(x, -dplyr::any_of(c("label", "i18nCode", "fq"))), "species" = dplyr::select(x, group = "taxonomy"), # NOTE: further exceptions may be needed for type = "species" diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index ebf2f7f7..d811a75d 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -144,13 +144,14 @@ capture_occurrences_doi <- function(.query, #' @noRd #' @keywords Internal capture_occurrences_glimpse <- function(.query){ - .query$type <- "data/occurrences-glimpse" if(.query$atlas == "Global"){ result <- capture_occurrences_count(.query) + result$type <- "data/occurrences-glimpse" result$body$limit <- 3 as_prequery(result) }else{ + .query$type <- "data/occurrences-glimpse" result <- capture_occurrences_la(.query) url <- httr2::url_parse(result$url) diff --git a/R/check.R b/R/check.R index d2f1c436..3a01ffa9 100644 --- a/R/check.R +++ b/R/check.R @@ -749,18 +749,13 @@ check_reason <- function(.query, } #' Check that `select()` quosures can be parsed correctly -#' NOTE: much of this content was previously in `parse_select()` (defunct) #' @noRd #' @keywords Internal check_select <- function(.query, error_call = rlang::caller_env()){ if(any(names(.query$request) == "select")){ + # NOTE: GBIF occurrence queries are excluded because they are parsed elsewhere if(!(.query$atlas == "Global" & stringr::str_detect(.query$type, "^data"))){ - # cli::cli({ - # cli::cli_text("Skipping `select()`.") - # cli::cli_bullets(c(i = "This function is not supported by the GBIF occurrences downloads API v1.")) - # }) - # }else{ # 1. build df to `select` from valid_fields <- .query[["metadata/fields"]]$id valid_assertions <- .query[["metadata/assertions"]]$id diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index 461da9b5..961fe2e2 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -41,6 +41,7 @@ collect_occurrences_direct <- function(.query, file, call){ #' @noRd #' @keywords Internal collect_occurrences_default <- function(.query, wait, file, call){ + # check queue download_response <- check_queue(.query, wait = wait) if(is.null(download_response)){ @@ -73,6 +74,12 @@ collect_occurrences_default <- function(.query, wait, file, call){ result <- result |> check_field_identities(.query, error_call = call) |> check_media_cols() # check for, and then clean, media info + + # exception for GBIF to post-process `select()` + if(.query$atlas == "Global"){ + result <- parse_select(result, .query) + } + # exception for GBIF to ensure DOIs are preserved if(!is.null(download_response$doi)){ # NOTE: GBIF documents DOIs in download response status url (it used to be automatically appended) @@ -129,7 +136,8 @@ collect_occurrences_glimpse_gbif <- function(.query){ df <- result |> purrr::pluck("results") |> purrr::map(tidy_list_columns) |> - dplyr::bind_rows() + dplyr::bind_rows() |> + parse_select(.query) attr(df, "total_n") <- result$count # assign new object for bespoke printing diff --git a/R/compute_occurrences.R b/R/compute_occurrences.R index 36dae33e..7d941420 100644 --- a/R/compute_occurrences.R +++ b/R/compute_occurrences.R @@ -31,7 +31,8 @@ compute_occurrences_gbif <- function(.query){ check_occurrence_response() c(list(type = "data/occurrences", atlas = .query$atlas), - status_code) |> + status_code[names(status_code) != "request"], + list(request = .query$request)) |> structure(class = "computed_query") } diff --git a/R/dplyr-select.R b/R/dplyr-select.R index 05c27c14..397e49d2 100644 --- a/R/dplyr-select.R +++ b/R/dplyr-select.R @@ -129,16 +129,11 @@ #' } #' @export select.data_request <- function(.data, ..., group = NULL){ - # if(is_gbif()){ - # cli::cli_text("`select()` is not supported for GBIF occurrence downloads API v1: skipping") - # .data - # }else{ - dots <- rlang::enquos(..., .ignore_empty = "all") - list(quosure = dots, - summary = generate_summary(dots)) |> - add_group(group) |> - update_request_object(.data, select = _) - # } + dots <- rlang::enquos(..., .ignore_empty = "all") + list(quosure = dots, + summary = generate_summary(dots)) |> + add_group(group) |> + update_request_object(.data, select = _) } #' @rdname select.data_request diff --git a/R/galah_select.R b/R/galah_select.R index 9b0e2fc9..32153fca 100644 --- a/R/galah_select.R +++ b/R/galah_select.R @@ -4,28 +4,19 @@ galah_select <- function(..., group = NULL){ dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() |> as.list() - # if(is_gbif()){ - # cli::cli_text("`select()` is not supported for GBIF occurrence downloads API v1: skipping") - # if(inherits(dots[[1]], "data_request")){ - # dots[[1]] - # }else{ - # NULL - # } - # }else{ - if(length(dots) < 1){ - list(quosure = c(), summary = "") |> - add_group(group) - } - else if(inherits(dots[[1]], "data_request")){ - list(quosure = dots[-1], - summary = generate_summary(dots[-1])) |> - add_group(group) |> - update_request_object(dots[[1]], - select = _) - }else{ - list(quosure = dots, - summary = generate_summary(dots)) |> - add_group(group) - } - # } + if(length(dots) < 1){ + list(quosure = c(), summary = "") |> + add_group(group) + } + else if(inherits(dots[[1]], "data_request")){ + list(quosure = dots[-1], + summary = generate_summary(dots[-1])) |> + add_group(group) |> + update_request_object(dots[[1]], + select = _) + }else{ + list(quosure = dots, + summary = generate_summary(dots)) |> + add_group(group) + } } \ No newline at end of file diff --git a/R/print.R b/R/print.R index aa0797a7..01743723 100644 --- a/R/print.R +++ b/R/print.R @@ -295,21 +295,27 @@ print.galah_config <- function(x, ...){ cli::cli_text("`galah` package configuration") cli::cli_end() cli::cli_par() - # print package settings - cli::cli_text("{galah_pink(\"Package\")}") - package_info <- purrr::pluck(x, "package") - package_logical_check <- purrr::map(package_info, is.logical) |> - unlist() - logical_values <- package_info[package_logical_check] |> - unlist() - package_settings <- names(logical_values) |> - galah_green() - names(package_settings) <- c("x", "v")[as.integer(logical_values) + 1] - package_settings <- c(package_settings, - "i" = glue::glue("{galah_green('directory')}: {galah_grey(x$package$directory)}")) |> - cli::cli_bullets() + print_config_atlas(x) + print_config_user(x) + print_config_package(x) +} + +#' Internal function to print current atlas within galah_config() +#' @noRd +#' @keywords Internal +print_config_atlas <- function(x){ + cli::cli_par() + cli::cli_text("{galah_pink(\"Atlas\")}") + atlas_text <- galah_green(x$atlas$organisation) + atlas_subtext <- galah_grey(glue::glue("({x$atlas$acronym}), {x$atlas$region}")) + cli::cli_bullets("{atlas_text} {atlas_subtext}") cli::cli_end() - # print user settings +} + +#' Internal function to print current user settings within galah_config() +#' @noRd +#' @keywords Internal +print_config_user <- function(x){ cli::cli_par() cli::cli_text("{galah_pink(\"User\")}") user_settings <- c( @@ -323,11 +329,24 @@ print.galah_config <- function(x, ...){ ifelse("v", "x") cli::cli_bullets(user_settings) cli::cli_end() - cli::cli_par() - cli::cli_text("{galah_pink(\"Atlas\")}") - atlas_text <- galah_green(x$atlas$organisation) - atlas_subtext <- galah_grey(glue::glue("({x$atlas$acronym}), {x$atlas$region}")) - cli::cli_bullets("{atlas_text} {atlas_subtext}") +} + +#' Internal function to print current package settings within galah_config() +#' @noRd +#' @keywords Internal +print_config_package <- function(x){ + cli::cli_text("{galah_pink(\"Package\")}") + package_info <- purrr::pluck(x, "package") + package_logical_check <- purrr::map(package_info, is.logical) |> + unlist() + logical_values <- package_info[package_logical_check] |> + unlist() + package_settings <- names(logical_values) |> + galah_green() + names(package_settings) <- c("x", "v")[as.integer(logical_values) + 1] + package_settings <- c(package_settings, + "i" = glue::glue("{galah_green('directory')}: {galah_grey(x$package$directory)}")) |> + cli::cli_bullets() cli::cli_end() } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 0ed848ba..3ee1a550 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -383,6 +383,16 @@ default_columns <- function(atlas = NULL) { "basisOfRecord", "occurrenceStatus", "dataResourceName") + }else if(atlas == "Global"){ + c("occurrenceId", + "scientificName", + "taxonConceptID", + "decimalLatitude", + "decimalLongitude", + "eventDate", + "basisOfRecord", + "occurrenceStatus", + "datasetName") }else{ cli::cli_abort("Unknown `atlas`") } diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index f751d684..a8804e14 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -234,9 +234,9 @@ test_that("`count()` works with `identify` for GBIF when `run_checks` = TRUE", { count() |> collapse() expect_s3_class(x, "query") - expect_equal(length(x), 6) + expect_equal(length(x), 7) expect_equal(names(x), - c("type", "url", "headers", + c("type", "atlas", "url", "headers", "options", "body", "request")) expect_equal(x$type, "data/occurrences-count") # compute @@ -251,7 +251,11 @@ test_that("`count()` works with `identify` for GBIF when `run_checks` = TRUE", { test_that("`glimpse()` works for GBIF", { skip_if_offline(); skip_on_ci() - x <- galah_call() |> + galah_config(atlas = "GBIF", + username = "atlasoflivingaustralia", + email = "ala4r@ala.org.au", + password = "galah-gbif-test-login") + x <- request_data(from = "GBIF") |> filter(year == 2025) |> glimpse() |> collect() @@ -267,13 +271,11 @@ test_that("`glimpse()` works for GBIF", { expect_equal(3) }) +# FIXME: add test for `select()` paired with `glimpse()` + test_that("`atlas_occurrences()` works for GBIF", { skip_if_offline(); skip_on_ci() - galah_config(atlas = "GBIF", - username = "atlasoflivingaustralia", - email = "ala4r@ala.org.au", - password = "galah-gbif-test-login") - x <- galah_call() |> + x <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", country == "AU") |> @@ -283,6 +285,31 @@ test_that("`atlas_occurrences()` works for GBIF", { expect_gt(ncol(x), 10) }) +test_that("`atlas_occurrences()` works with `select()` for GBIF", { + skip_if_offline(); skip_on_ci() + x <- galah_call(from = "GBIF") |> + filter(year == 1890, + classKey == "359", + country == "AU") |> + select(countryCode, species, eventDate) |> + collect() + expect_s3_class(x, c("tbl_df", "tbl", "data.frame")) + expect_gt(nrow(x), 10) + expect_equal(ncol(x), 3) +}) + +# FIXME: test not implemented yet +test_that("invalid fields are caught from `select()` for GBIF", { + skip_if_offline(); skip_on_ci() + galah_call(from = "GBIF") |> + filter(year == 1890, + classKey == "359", + country == "AU") |> # this uses `check_fields_gbif_predicates()` + select(country, species, eventDate) |> # this uses `check_fields_gbif_counts()` + collect() |> + expect_error() +}) + test_that("`atlas_occurrences()` works with `galah_polygon()` for GBIF", { skip_if_offline(); skip_on_ci() wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" @@ -315,21 +342,16 @@ test_that("`atlas_occurences()` works with `galah_radius()` for GBIF", { expect_equal(nrow(result), count$count) }) -test_that("`galah_select()` returns message for GBIF", { - expect_message({x <- galah_select(galah_call())}) - expect_true(is.null(x$select)) -}) - -test_that("atlas_species works for GBIF", { +test_that("`atlas_species()` works for GBIF", { skip_if_offline(); skip_on_ci() x <- request_data(type = "species") |> filter(year == 2010) |> identify("Litoria") |> collapse() expect_s3_class(x, "query") - expect_equal(length(x), 6) + expect_equal(length(x), 7) expect_equal(names(x), - c("type", "url", "headers", "options", "body", "request")) + c("type", "atlas", "url", "headers", "options", "body", "request")) expect_equal(x$type, "data/species") y <- compute(x) expect_s3_class(y, "computed_query") @@ -344,7 +366,7 @@ test_that("atlas_species works for GBIF", { # expect_equal(z, species) }) -test_that("atlas_media fails for GBIF", { +test_that("`atlas_media()` fails for GBIF", { skip_if_offline(); skip_on_ci() expect_error({galah_call() |> galah_identify("perameles") |> @@ -366,7 +388,7 @@ test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences # NOTE: the above query should return 72 records (tested 2025-06-10) expect_s3_class(x, "query") expect_equal(names(x), - c("type", "url", "headers", "options", "body", "request")) + c("type", "atlas", "url", "headers", "options", "body", "request")) expect_equal(x$type, "data/occurrences") # compute y <- compute(x) From ca18f32ae3a1f93cf476c9fcd3981a9ae82e1c73 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 1 Apr 2026 14:00:51 +1100 Subject: [PATCH 12/70] move older `galah_` functions to `superseded` These will be maintained, but current best practice is to use the `dplyr` versions; this documentation change merely reinforces that. --- R/{galah_apply_profile.R => apply_profile.R} | 3 +- R/atlas_counts.R | 2 +- R/atlas_distributions.R | 4 +- R/atlas_occurrences.R | 12 +++--- R/dplyr-filter.R | 8 +++- R/galah_filter.R | 4 +- R/galah_group_by.R | 3 +- R/galah_select.R | 3 +- R/{galah_geolocate.R => geolocate.R} | 4 +- R/{galah_identify.R => graphics-identify.R} | 4 +- R/superseded-functions.R | 22 ++++++++++ _pkgdown.yml | 1 + man/apply_profile.Rd | 5 +-- man/atlas_.Rd | 14 +++---- man/filter.data_request.Rd | 13 +++--- man/geolocate.Rd | 7 +--- man/group_by.data_request.Rd | 5 +-- man/identify.data_request.Rd | 5 +-- man/select.data_request.Rd | 5 +-- man/superseded_functions.Rd | 44 ++++++++++++++++++++ 20 files changed, 114 insertions(+), 54 deletions(-) rename R/{galah_apply_profile.R => apply_profile.R} (98%) rename R/{galah_geolocate.R => geolocate.R} (99%) rename R/{galah_identify.R => graphics-identify.R} (98%) create mode 100644 R/superseded-functions.R create mode 100644 man/superseded_functions.Rd diff --git a/R/galah_apply_profile.R b/R/apply_profile.R similarity index 98% rename from R/galah_apply_profile.R rename to R/apply_profile.R index 7d0a42f9..5542f406 100644 --- a/R/galah_apply_profile.R +++ b/R/apply_profile.R @@ -39,7 +39,8 @@ apply_profile <- function(.data, ...){ apply_profile = result) } -#' @rdname apply_profile +#' @rdname superseded_functions +#' @order 1 #' @export galah_apply_profile <- function(...){ dots <- rlang::enquos(..., .ignore_empty = "all") |> diff --git a/R/atlas_counts.R b/R/atlas_counts.R index 4521d0f3..dcb1b2a0 100644 --- a/R/atlas_counts.R +++ b/R/atlas_counts.R @@ -1,6 +1,6 @@ #' @rdname atlas_ #' @order 2 -#' @param group_by `tibble`: generated by a call to [galah_group_by()]. +#' @param group_by `r lifecycle::badge("superseded")` `tibble`: generated by a call to [galah_group_by()]. #' @param limit `numeric`: maximum number of categories to return, defaulting to 100. #' If limit is NULL, all results are returned. For some categories this will #' take a while. diff --git a/R/atlas_distributions.R b/R/atlas_distributions.R index 8116d570..b06d5d68 100644 --- a/R/atlas_distributions.R +++ b/R/atlas_distributions.R @@ -4,9 +4,9 @@ #' @param request optional `data_request` object: generated by a call to #' [galah_call()]. #' @param identify `data.frame`: generated by a call to -#' [galah_identify()]. +#' identify(). #' @param filter `data.frame`: generated by a call to -#' [galah_filter()] +#' filter() #' @return An object of classes `sf`, `tbl`, `tbl_df` and `data.frame` #' (aka a Simple feature collection) of distribution maps, with one column per #' map, and spatial data stored in the `geometry` column. diff --git a/R/atlas_occurrences.R b/R/atlas_occurrences.R index 349577ad..2f858095 100644 --- a/R/atlas_occurrences.R +++ b/R/atlas_occurrences.R @@ -11,7 +11,7 @@ #' #' * They have the ability to accept `filter`, `select` etc as arguments, #' rather than within a pipe; but **only** when using the `galah_` forms of -#' those functions (e.g. [galah_filter()]). +#' those functions (see [superseded_functions]). #' * `atlas_` functions do not require you to specify the `method` or `type` #' arguments to [galah_call()], as they are more specific in what data are #' being requested. @@ -20,11 +20,11 @@ #' @order 1 #' @param request optional `data_request` object: generated by a call to #' [galah_call()]. -#' @param identify `tibble`: generated by a call to [galah_identify()]. -#' @param filter `tibble`: generated by a call to [galah_filter()] -#' @param geolocate `string`: generated by a call to [galah_geolocate()] -#' @param apply_profile `string`: generated by a call to [galah_apply_profile()] -#' @param select `tibble`: generated by a call to [galah_select()] +#' @param identify `r lifecycle::badge("superseded")` `tibble`: generated by a call to [galah_identify()]. +#' @param filter `r lifecycle::badge("superseded")` `tibble`: generated by a call to [galah_filter()] +#' @param geolocate `r lifecycle::badge("superseded")` `string`: generated by a call to [galah_geolocate()] +#' @param apply_profile `r lifecycle::badge("superseded")` `string`: generated by a call to [galah_apply_profile()] +#' @param select `r lifecycle::badge("superseded")` `tibble`: generated by a call to [galah_select()] #' @param mint_doi `logical`: by default no DOI will be generated. Set to #' `TRUE` if you intend to use the data in a publication or similar. #' @param doi `string`: (Optional) DOI to download. If provided overrides diff --git a/R/dplyr-filter.R b/R/dplyr-filter.R index cede13cf..da7d2b3f 100644 --- a/R/dplyr-filter.R +++ b/R/dplyr-filter.R @@ -9,12 +9,16 @@ #' @name filter.data_request #' @order 1 #' @param .data An object of class `data_request`, `metadata_request` -#' or `files_request`, created using [galah_call()] or related functions. +#' or `files_request`, created using [request_data()] or related functions. #' @param ... Expressions that return a logical value, and are defined in terms #' of the variables in the selected atlas (and checked using `show_all(fields)`. #' If multiple expressions are included, they are combined with the & operator. #' Only rows for which all conditions evaluate to `TRUE` are kept. -#' @return A tibble containing filter values. +#' @return An object of the same class as supplied, but containing a populated +#' `filter` slot. The slot itself will contain an object of class `data_filter` +#' or `metadata_filter` if called for a living atlas query; these formats are +#' `tibble`-like. For GBIF it will be an object of class `predicates_filter`, +#' which is `list`-like. #' @seealso \code{\link[=select.data_request]{select()}}, #' \code{\link[=group_by.data_request]{group_by()}} and [geolocate()] for #' other ways to amend the information returned by [atlas_()] functions. Use diff --git a/R/galah_filter.R b/R/galah_filter.R index 478eed94..b73d8c7c 100644 --- a/R/galah_filter.R +++ b/R/galah_filter.R @@ -1,5 +1,5 @@ -#' @rdname filter.data_request -#' @order 4 +#' @rdname superseded_functions +#' @order 2 #' @export galah_filter <- function(...){ dots <- rlang::enquos(..., .ignore_empty = "all") |> diff --git a/R/galah_group_by.R b/R/galah_group_by.R index b2a5878a..104287e1 100644 --- a/R/galah_group_by.R +++ b/R/galah_group_by.R @@ -1,4 +1,5 @@ -#' @rdname group_by.data_request +#' @rdname superseded_functions +#' @order 4 #' @export galah_group_by <- function(...){ dots <- rlang::enquos(..., .ignore_empty = "all") |> diff --git a/R/galah_select.R b/R/galah_select.R index 32153fca..1d9de3da 100644 --- a/R/galah_select.R +++ b/R/galah_select.R @@ -1,4 +1,5 @@ -#' @rdname select.data_request +#' @rdname superseded_functions +#' @order 6 #' @export galah_select <- function(..., group = NULL){ dots <- rlang::enquos(..., .ignore_empty = "all") |> diff --git a/R/galah_geolocate.R b/R/geolocate.R similarity index 99% rename from R/galah_geolocate.R rename to R/geolocate.R index 7f81a5e6..56880882 100644 --- a/R/galah_geolocate.R +++ b/R/geolocate.R @@ -143,8 +143,8 @@ geolocate <- function(..., type = c("polygon", "bbox", "radius")) { ) } -#' @rdname geolocate -#' @order 2 +#' @rdname superseded_functions +#' @order 3 #' @export galah_geolocate <- geolocate diff --git a/R/galah_identify.R b/R/graphics-identify.R similarity index 98% rename from R/galah_identify.R rename to R/graphics-identify.R index 2239b72a..b6cea238 100644 --- a/R/galah_identify.R +++ b/R/graphics-identify.R @@ -65,8 +65,8 @@ identify.metadata_request <- function(x, ...){ x } -#' @rdname identify.data_request -#' @order 3 +#' @rdname superseded_functions +#' @order 5 #' @export galah_identify <- function(...) { dots_initial <- list(...) diff --git a/R/superseded-functions.R b/R/superseded-functions.R new file mode 100644 index 00000000..83e53cf6 --- /dev/null +++ b/R/superseded-functions.R @@ -0,0 +1,22 @@ +#' Superseded functions +#' +#' These functions are still valid, but have been superseded by more recent +#' versions. They are designed to be used to supply arguments within +#' functions from the [atlas_] family. Instead you should consider using +#' piped functions for this same functionality. +#' @name superseded_functions +#' @rdname superseded_functions +#' @aliases galah_filter +#' @aliases galah_identify +#' @details +#' Replacements are as follows: +#' +#' \itemize{ +#' \item \code{\link[=apply_profile]{apply_profile()}} instead of `galah_apply_profile()` +#' \item \code{\link[=filter.data_request]{filter()}} instead of `galah_filter()` +#' \item \code{\link[=geolocate.data_request]{geolocate()}} instead of `galah_geolocate()` +#' \item \code{\link[=group_by.data_request]{select()}} instead of `galah_group_by()` +#' \item \code{\link[=identify.data_request]{select()}} instead of `galah_identify()` +#' \item \code{\link[=select.data_request]{select()}} instead of `galah_select()` +#' } +NULL \ No newline at end of file diff --git a/_pkgdown.yml b/_pkgdown.yml index 0de478e2..4109e560 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -101,3 +101,4 @@ reference: - atlas_citation - as_data_filter - print_galah_objects + - superseded_functions diff --git a/man/apply_profile.Rd b/man/apply_profile.Rd index db289227..1b146d97 100644 --- a/man/apply_profile.Rd +++ b/man/apply_profile.Rd @@ -1,13 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/galah_apply_profile.R +% Please edit documentation in R/apply_profile.R \name{apply_profile} \alias{apply_profile} -\alias{galah_apply_profile} \title{Apply a data quality profile} \usage{ apply_profile(.data, ...) - -galah_apply_profile(...) } \arguments{ \item{.data}{An object of class \code{data_request}} diff --git a/man/atlas_.Rd b/man/atlas_.Rd index 5980ccb8..e461933b 100644 --- a/man/atlas_.Rd +++ b/man/atlas_.Rd @@ -62,15 +62,15 @@ atlas_taxonomy( \item{request}{optional \code{data_request} object: generated by a call to \code{\link[=galah_call]{galah_call()}}.} -\item{identify}{\code{tibble}: generated by a call to \code{\link[=galah_identify]{galah_identify()}}.} +\item{identify}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{tibble}: generated by a call to \code{\link[=galah_identify]{galah_identify()}}.} -\item{filter}{\code{tibble}: generated by a call to \code{\link[=galah_filter]{galah_filter()}}} +\item{filter}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{tibble}: generated by a call to \code{\link[=galah_filter]{galah_filter()}}} -\item{geolocate}{\code{string}: generated by a call to \code{\link[=galah_geolocate]{galah_geolocate()}}} +\item{geolocate}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{string}: generated by a call to \code{\link[=galah_geolocate]{galah_geolocate()}}} -\item{apply_profile}{\code{string}: generated by a call to \code{\link[=galah_apply_profile]{galah_apply_profile()}}} +\item{apply_profile}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{string}: generated by a call to \code{\link[=galah_apply_profile]{galah_apply_profile()}}} -\item{select}{\code{tibble}: generated by a call to \code{\link[=galah_select]{galah_select()}}} +\item{select}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{tibble}: generated by a call to \code{\link[=galah_select]{galah_select()}}} \item{mint_doi}{\code{logical}: by default no DOI will be generated. Set to \code{TRUE} if you intend to use the data in a publication or similar.} @@ -82,7 +82,7 @@ all other arguments. Only available for the ALA.} \code{data} with date and time added. The file path (directory) is always given by \code{galah_config()$package$directory}.} -\item{group_by}{\code{tibble}: generated by a call to \code{\link[=galah_group_by]{galah_group_by()}}.} +\item{group_by}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} \code{tibble}: generated by a call to \code{\link[=galah_group_by]{galah_group_by()}}.} \item{limit}{\code{numeric}: maximum number of categories to return, defaulting to 100. If limit is NULL, all results are returned. For some categories this will @@ -121,7 +121,7 @@ but \code{atlas_} functions differ in two ways: \itemize{ \item They have the ability to accept \code{filter}, \code{select} etc as arguments, rather than within a pipe; but \strong{only} when using the \code{galah_} forms of -those functions (e.g. \code{\link[=galah_filter]{galah_filter()}}). +those functions (see \link{superseded_functions}). \item \code{atlas_} functions do not require you to specify the \code{method} or \code{type} arguments to \code{\link[=galah_call]{galah_call()}}, as they are more specific in what data are being requested. diff --git a/man/filter.data_request.Rd b/man/filter.data_request.Rd index cbc89591..704e65c4 100644 --- a/man/filter.data_request.Rd +++ b/man/filter.data_request.Rd @@ -1,10 +1,9 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/dplyr-filter.R, R/galah_filter.R +% Please edit documentation in R/dplyr-filter.R \name{filter.data_request} \alias{filter.data_request} \alias{filter.metadata_request} \alias{filter.files_request} -\alias{galah_filter} \title{Keep rows that match a condition} \usage{ \method{filter}{data_request}(.data, ...) @@ -12,12 +11,10 @@ \method{filter}{metadata_request}(.data, ...) \method{filter}{files_request}(.data, ...) - -galah_filter(...) } \arguments{ \item{.data}{An object of class \code{data_request}, \code{metadata_request} -or \code{files_request}, created using \code{\link[=galah_call]{galah_call()}} or related functions.} +or \code{files_request}, created using \code{\link[=request_data]{request_data()}} or related functions.} \item{...}{Expressions that return a logical value, and are defined in terms of the variables in the selected atlas (and checked using \code{show_all(fields)}. @@ -25,7 +22,11 @@ If multiple expressions are included, they are combined with the & operator. Only rows for which all conditions evaluate to \code{TRUE} are kept.} } \value{ -A tibble containing filter values. +An object of the same class as supplied, but containing a populated +\code{filter} slot. The slot itself will contain an object of class \code{data_filter} +or \code{metadata_filter} if called for a living atlas query; these formats are +\code{tibble}-like. For GBIF it will be an object of class \code{predicates_filter}, +which is \code{list}-like. } \description{ The \code{filter()} function is used to subset a data, retaining all rows that diff --git a/man/geolocate.Rd b/man/geolocate.Rd index d497b709..99394b7d 100644 --- a/man/geolocate.Rd +++ b/man/geolocate.Rd @@ -1,9 +1,8 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/galah_geolocate.R, R/galah_polygon.R, -% R/galah_bbox.R, R/galah_radius.R +% Please edit documentation in R/geolocate.R, R/galah_polygon.R, R/galah_bbox.R, +% R/galah_radius.R \name{geolocate} \alias{geolocate} -\alias{galah_geolocate} \alias{galah_polygon} \alias{galah_bbox} \alias{galah_radius} @@ -12,8 +11,6 @@ \usage{ geolocate(..., type = c("polygon", "bbox", "radius")) -galah_geolocate(..., type = c("polygon", "bbox", "radius")) - galah_polygon(...) galah_bbox(...) diff --git a/man/group_by.data_request.Rd b/man/group_by.data_request.Rd index 3b9aa8e9..c3410539 100644 --- a/man/group_by.data_request.Rd +++ b/man/group_by.data_request.Rd @@ -1,13 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/dplyr-group_by.R, R/galah_group_by.R +% Please edit documentation in R/dplyr-group_by.R \name{group_by.data_request} \alias{group_by.data_request} -\alias{galah_group_by} \title{Group by one or more variables} \usage{ \method{group_by}{data_request}(.data, ...) - -galah_group_by(...) } \arguments{ \item{.data}{An object of class \code{data_request}} diff --git a/man/identify.data_request.Rd b/man/identify.data_request.Rd index 9bb5cfbd..f73032c4 100644 --- a/man/identify.data_request.Rd +++ b/man/identify.data_request.Rd @@ -1,16 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/galah_identify.R +% Please edit documentation in R/graphics-identify.R \name{identify.data_request} \alias{identify.data_request} \alias{identify.metadata_request} -\alias{galah_identify} \title{Narrow a query by passing taxonomic identifiers} \usage{ \method{identify}{data_request}(x, ...) \method{identify}{metadata_request}(x, ...) - -galah_identify(...) } \arguments{ \item{x}{An object of class \code{data_request} or \code{metadata_request}.} diff --git a/man/select.data_request.Rd b/man/select.data_request.Rd index 1fb94815..eaa1bfad 100644 --- a/man/select.data_request.Rd +++ b/man/select.data_request.Rd @@ -1,16 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/dplyr-select.R, R/galah_select.R +% Please edit documentation in R/dplyr-select.R \name{select.data_request} \alias{select.data_request} \alias{select.metadata_request} -\alias{galah_select} \title{Keep or drop columns using their names} \usage{ \method{select}{data_request}(.data, ..., group = NULL) \method{select}{metadata_request}(.data, ...) - -galah_select(..., group = NULL) } \arguments{ \item{.data}{An object of class \code{data_request}, created using \code{\link[=galah_call]{galah_call()}}.} diff --git a/man/superseded_functions.Rd b/man/superseded_functions.Rd new file mode 100644 index 00000000..c8358116 --- /dev/null +++ b/man/superseded_functions.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/apply_profile.R, R/galah_filter.R, +% R/geolocate.R, R/galah_group_by.R, R/graphics-identify.R, R/galah_select.R, +% R/superseded-functions.R +\name{galah_apply_profile} +\alias{galah_apply_profile} +\alias{galah_filter} +\alias{galah_geolocate} +\alias{galah_group_by} +\alias{galah_identify} +\alias{galah_select} +\alias{superseded_functions} +\title{Superseded functions} +\usage{ +galah_apply_profile(...) + +galah_filter(...) + +galah_geolocate(..., type = c("polygon", "bbox", "radius")) + +galah_group_by(...) + +galah_identify(...) + +galah_select(..., group = NULL) +} +\description{ +These functions are still valid, but have been superseded by more recent +versions. They are designed to be used to supply arguments within +functions from the \link{atlas_} family. Instead you should consider using +piped functions for this same functionality. +} +\details{ +Replacements are as follows: + +\itemize{ +\item \code{\link[=apply_profile]{apply_profile()}} instead of \code{galah_apply_profile()} +\item \code{\link[=filter.data_request]{filter()}} instead of \code{galah_filter()} +\item \code{\link[=geolocate.data_request]{geolocate()}} instead of \code{galah_geolocate()} +\item \code{\link[=group_by.data_request]{select()}} instead of \code{galah_group_by()} +\item \code{\link[=identify.data_request]{select()}} instead of \code{galah_identify()} +\item \code{\link[=select.data_request]{select()}} instead of \code{galah_select()} +} +} From 652dce51abadcf57e3976391b7f687b3e4f93fc8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 10 Apr 2026 08:30:54 +1000 Subject: [PATCH 13/70] Minor update to docs --- R/capture.R | 2 +- R/compound.R | 14 ++++++-------- man/capture.data_request.Rd | 2 +- man/compound.Rd | 14 ++++++-------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/R/capture.R b/R/capture.R index 49486a42..7520552a 100644 --- a/R/capture.R +++ b/R/capture.R @@ -1,4 +1,4 @@ -#' Capture a request +#' Convert a request into a query #' #' @description #' The first step in evaluating a request is to capture and parse the diff --git a/R/compound.R b/R/compound.R index 792bc87f..cba9b89f 100644 --- a/R/compound.R +++ b/R/compound.R @@ -1,13 +1,11 @@ -#' Force evaluation of a database query +#' Create a list of all API calls needed to evaluate a request #' #' @description -#' [compound()] shows the full set of queries -#' required to properly evaluate the user's request, run prior to [collapse()]. -#' -#' The number of total queries to send for a single data request is often broader -#' than the single query returned by [collapse()]. If, for example, -#' the user's query includes a call to -#' \code{\link[=identify.data_request]{identify()}}, then a taxonomic query +#' [compound()] shows the full set of queries required to properly evaluate +#' the user's request, run prior to [collapse()]. The number of total queries +#' to send for a single data request is often greater than the single query +#' returned by [collapse()]. If, for example, the user's query includes a call +#' to \code{\link[=identify.data_request]{identify()}}, then a taxonomic query #' is required to run _before_ the 'final' query is attempted. In relation to #' other functions that manipulate `_request` objects, [compound()] is called #' within \code{\link[=collapse.data_request]{collapse()}}, and itself diff --git a/man/capture.data_request.Rd b/man/capture.data_request.Rd index db7e2247..0b1619b2 100644 --- a/man/capture.data_request.Rd +++ b/man/capture.data_request.Rd @@ -6,7 +6,7 @@ \alias{capture.metadata_request} \alias{capture.files_request} \alias{capture.list} -\title{Capture a request} +\title{Convert a request into a query} \usage{ capture(x, ...) diff --git a/man/compound.Rd b/man/compound.Rd index 3b23dc04..d458e727 100644 --- a/man/compound.Rd +++ b/man/compound.Rd @@ -8,7 +8,7 @@ \alias{compound.prequery} \alias{compound.query} \alias{compound.query_set} -\title{Force evaluation of a database query} +\title{Create a list of all API calls needed to evaluate a request} \usage{ compound(x, ...) @@ -40,13 +40,11 @@ listed in the order in which they will be evaluated, meaning the query that the user has actually requested will be placed last. } \description{ -\code{\link[=compound]{compound()}} shows the full set of queries -required to properly evaluate the user's request, run prior to \code{\link[=collapse]{collapse()}}. - -The number of total queries to send for a single data request is often broader -than the single query returned by \code{\link[=collapse]{collapse()}}. If, for example, -the user's query includes a call to -\code{\link[=identify.data_request]{identify()}}, then a taxonomic query +\code{\link[=compound]{compound()}} shows the full set of queries required to properly evaluate +the user's request, run prior to \code{\link[=collapse]{collapse()}}. The number of total queries +to send for a single data request is often greater than the single query +returned by \code{\link[=collapse]{collapse()}}. If, for example, the user's query includes a call +to \code{\link[=identify.data_request]{identify()}}, then a taxonomic query is required to run \emph{before} the 'final' query is attempted. In relation to other functions that manipulate \verb{_request} objects, \code{\link[=compound]{compound()}} is called within \code{\link[=collapse.data_request]{collapse()}}, and itself From f4c693157ad2796a7a42f1a8a7a02b93b6b4b34f Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 22 Apr 2026 16:06:35 +1000 Subject: [PATCH 14/70] Re-arrange `authenticate()` to handle all JWT- and email-based authentication First pass, but when complete, will mean all query parameters can be passed in pipe. Extends solutions to issues #296 and #297 --- R/authenticate.R | 106 ++++++++++++++++++++++++++++++---- R/capture.R | 25 +++++++- R/check.R | 124 ---------------------------------------- R/collapse_checks.R | 4 -- R/compute_occurrences.R | 2 +- R/galah_config.R | 10 +++- R/messages.R | 17 ++---- R/query_API.R | 2 +- R/utilities_internal.R | 22 +++++-- man/authenticate.Rd | 33 ++++++++--- 10 files changed, 177 insertions(+), 168 deletions(-) diff --git a/R/authenticate.R b/R/authenticate.R index 208d59d2..551f537c 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -1,14 +1,20 @@ #' Set up authentication #' -#' Add an authentication slot to a query. That slot is then used by later -#' code to determine whether to add an OAuth workflow. It is triggered -#' automatically within [capture()] if the `authenticate` argument -#' of [galah_config()] is set to `TRUE`, but only for occurrence queries -#' to the Atlas of Living Australia. +#' Authenticate a request, either by sending a registered email address +#' (and, for GBIF, password and username); or by loggin in via the +#' browser to generate a JWT token. Note that while handling this manually +#' in-pipe is the most transparent approach, this function is also used to +#' pass cached information stored via [galah_config()] to a query. #' `r lifecycle::badge("experimental")`. #' @param .data An object of class `data_request` or `metadata_request` -#' @param cache_disk (logical) Should JWT tokens be cached to disk? Defaults +#' @param email (string) Email address registered with the selected organisation +#' @param username (string) Registered username (GBIF only) +#' @param password (string) Registered password (GBIF only) +#' @param use_jwt (logical) Should an OAuth workflow be used for authentication? +#' Only supported for Flemish and Australian atlases. Defaults to `FALSE` +#' @param cache_jwt (logical) Should JWT tokens be cached to disk? Defaults #' to `FALSE` +#' @param ... Other arguments, currently ignored #' @returns An object of the same class as supplied, but with an added #' `authenticate` slot. #' @examples \dontrun{ @@ -27,9 +33,89 @@ #' } #' @export authenticate <- function(.data, - cache_disk = FALSE){ + email = NULL, + username = NULL, + password = NULL, + use_jwt = FALSE, + cache_jwt = FALSE, + ...){ + + # handle GBIF first + if(.data$atlas == "Global"){ + # only occurrence queries require authentication for this atlas + if(authentication_required(.data$type)){ + # ignore token requests + if(isTRUE(use_jwt)){ + cli::cli_warn("JWT-based authentication not supported for GBIF: skipping") + } + authenticate_with_email_gbif(.data, + email = email, + username = username, + password = password) + }else{ + .data + } + # Now handle living atlases + # behaviour here differs from gbif - support authentication wherever possible + # specifically, call JWT if authentication is possible, and either + # 1. user has specifically requested it, or + # 2. no email is provided + }else{ + if( + (isTRUE(use_jwt) | is.null(email)) & authentication_supported(.data$atlas) + ){ + update_request_object(.data, + authenticate = list(email = NULL, + use_jwt = TRUE, + use_apikey = FALSE, # not supported yet + cache_disk = cache_jwt)) + }else{ + authenticate_with_email(.data, email = email) + } + } +} + +#' Internal function to build a valid email authentication for GBIF +#' @noRd +#' @keywords Internal +authenticate_with_email <- function(.data, + email = NULL, + call = rlang::caller_env()){ + if(is.null(email)){ + email <- potions::pour("user", "email", .pkg = "galah") + } + if(is.null(email)){ + abort_email_missing(call = call) + } update_request_object(.data, - authenticate = list(use_jwt = TRUE, - use_apikey = FALSE, # not supported yet - cache_disk = cache_disk)) + authenticate = list(use_jwt = FALSE, + email = email)) +} + +#' Internal function to build a valid email authentication for GBIF +#' @noRd +#' @keywords Internal +authenticate_with_email_gbif <- function(.data, + email = NULL, + username = NULL, + password = NULL, + call = rlang::caller_env()){ + # then look for local email etc + email_args <- c(email, username, password) + if(any(is.null(email_args))){ + cached_user <- potions::pour("user", .pkg = "galah") + email_args <- unlist(cached_user)[c("email", "username", "password")] + } + # run some checks + email_args <- enforce_character(email_args) + if(any(is.null(email_args))){ + cli::cli_abort("atlas `GBIF` requires email, username, and password for occurrence queries", + call = call) + } + # save out + update_request_object(.data, + authenticate = list(use_jwt = FALSE, + email = email, + username = username, + password = password)) } \ No newline at end of file diff --git a/R/capture.R b/R/capture.R index 7520552a..679669b1 100644 --- a/R/capture.R +++ b/R/capture.R @@ -91,7 +91,6 @@ capture.data_request <- function(x, #' @export capture.metadata_request <- function(x, ...){ x <- x |> - check_authentication() |> enforce_select_query() switch(x$type, "apis" = capture_apis(x), @@ -161,6 +160,30 @@ as_prequery <- function(x){ structure(x, class = c("prequery", "list")) } +# Below here are check functions that are specific to `capture()` + +#' Internal function to lookup requests for authentication +#' This is necessary, because only in `capture()` can we assess what to do when +#' `authenticate()` hasn't been previously called. This is important for e.g. +#' enforcing `authenticate()` where it is required, or pulling information from +#' `galah_config()` to supply that information +#' @noRd +#' @keywords Internal +check_authentication <- function(x){ + if(!is.null(x$authenticate)){ + x + }else{ + if(authentication_required(x$type)){ + config_options <- c(list(x), + potions::pour("user", .pkg = "galah")) + names(config_options)[c(1, 2)] <- c(".data", "use_jwt") + do.call(authenticate, config_options) + }else{ + x + } + } +} + #' Internal function to ensure that DOIs are parsed properly #' @noRd #' @keywords Internal diff --git a/R/check.R b/R/check.R index 3a01ffa9..e3d69583 100644 --- a/R/check.R +++ b/R/check.R @@ -26,27 +26,6 @@ check_atlas_inputs <- function(args, request_obj } -#' Internal function to lookup requests for authentication -#' Note this is currently only called on `data_request` objects, and -#' then only before parsing -#' @noRd -#' @keywords Internal -check_authentication <- function(x){ - # if authentication not passed in pipe, check `galah_config()` - if(is.null(x$authenticate) & - isTRUE(potions::pour("user", "authenticate", .pkg = "galah")) & - x$type %in% c("occurrences")){ - x <- x |> authenticate() - } - # reset authentication to `NULL` if requested for a non-supported atlas - if(!authentication_supported(x$atlas) & - !is.null(x$authenticate)){ - cli::cli_warn("Authentication not supported for atlas {x$atlas}: skipping") - x$authenticate <- NULL - } - x -} - #' Internal function to check that the specified path exists, and if not, #' to create it. Called by `galah_config()` #' @param x a path to a directory, or NULL @@ -98,45 +77,6 @@ check_download_filename <- function(file, # check_path()? # currently commented out in check.R } -#' Subfunction to `check_login()` -#' @noRd -#' @keywords Internal -check_email <- function(.query, - call = rlang::caller_env()){ - if(.query$atlas == "Global"){ - # actually we check the userpwd entry here - email_text <- .query$options$userpwd - if(email_text == ":"){ - abort_email_missing(error_call = call) - } - }else{ - # use purrr::pluck() to search for named slots - # base parsing captures `email_notify` and is therefore unrelable - email_text <- httr2::url_parse(.query$url) |> - purrr::pluck("query", "email") - # set criteria for missingness - email_text_missing <- if(is.null(email_text)){ - TRUE - }else if(email_text == ""){ - TRUE - }else{ - FALSE - } - # authentication only acceptable alternative to email for ALA - if(.query$atlas == "Australia"){ - authentication_missing <- is.null(.query$authenticate) - if(email_text_missing & authentication_missing){ - abort_email_missing(error_call = call) - } - }else{ - if(email_text_missing){ - abort_email_missing(error_call = call) - } - } - } - .query -} - #' Check files are filtered properly #' @noRd #' @keywords Internal @@ -491,30 +431,6 @@ check_identifiers_la <- function(.query, .query } -#' Internal function to confirm requisite login information has been provided -#' Called by `compute()` -#' @noRd -#' @keywords Internal -check_login <- function(.query, - error_call = rlang::caller_env()) { - # Check for valid email for occurrences or species queries for all providers - if(.query$atlas == "Global"){ - if(grepl("^data", .query$type)){ - check_email(.query, call = error_call) - check_password(.query, call = error_call) - } - }else{ - if(.query$type %in% c("data/occurrences", "data/species") & - is.null(.query$request$authenticate) # i.e. only validate if authenticate = FALSE - ){ - switch(.query$atlas, - "United Kingdom" = {}, - check_email(.query, call = error_call)) - } - } - .query -} - #' Internal function to convert multi-value media fields to list-columns #' @param .query A tibble() returned by atlas_occurrences #' @noRd @@ -654,46 +570,6 @@ check_occurrence_status <- function(.query){ check_occurrence_response() } -#' Internal function to expand a url -#' Proposed to spin out multiple urls to paginate when n is high -#' -#' Note: this needs to be in the compute stage of multiple APIs: ie. from `request_data()` and `request_metadata()` -#' Also requires something like `check_facet_count()` to know what the max value is. -#' @noRd -#' @keywords Internal -# check_pagination <- function(){} - -#' Subfunction to `check_login()` -#' @noRd -#' @keywords Internal -check_password <- function(.query, - call = rlang::caller_env()){ - if (.query$options$userpwd == ":") { - cli::cli_abort("GBIF requires a username and password to download occurrences or species.", - call = call) - } -} - -# Internal function to create a valid filename for download -# Note this is most commonly used when galah defaults are in place; i.e. -# downloads are sent to a temporary directory. -# Called by `query_API()` -# check_path <- function(.query){ -# if(is.null(.query$path)){ -# if(.query$type == "species"){ -# ext <- "csv" -# }else{ -# ext <- "zip" -# } -# cache_file <- pour("package", "directory") -# .query$path <- paste0(cache_dir, "/temp_file.", ext) -# } else { -# dirname(x) |> check_directory() # errors if path doesn't exist -# # NOTE: it might make sense here to check that a supplied filename is valid -# } -# .query -# } - #' Internal function to check a supplied profile is valid #' @noRd #' @keywords Internal diff --git a/R/collapse_checks.R b/R/collapse_checks.R index 56ecbdd8..fbdf51b5 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -50,10 +50,6 @@ collapse_run_checks <- function(.query, !(.query$type %in% c("data/occurrences-doi", "data/occurrences-describe"))){ # taxon concept ID must always be evaluated .query <- check_identifiers(.query, error_call) - # login should only be evaluated for species and occurrence - if(.query$type %in% c("data/occurrences", "data/species")){ - .query <- check_login(.query, error_call) - } # check_select() is specifically for parsing fields into urls, # should only be called for occurrences diff --git a/R/compute_occurrences.R b/R/compute_occurrences.R index 7d941420..71029f76 100644 --- a/R/compute_occurrences.R +++ b/R/compute_occurrences.R @@ -46,7 +46,7 @@ compute_occurrences_la <- function(.query){ check_occurrence_response() if(potions::pour("package", "verbose")){ n_records <- status_code$total_records - if(!is.null(.query$request$authenticate)){ + if(isTRUE(.query$request$authenticate$use_jwt)){ cli::cli_text("Query sent including JWT token") } cli::cli_text("Request for {n_records} occurrences placed in queue.") diff --git a/R/galah_config.R b/R/galah_config.R index 9f2ab221..0401a6f6 100644 --- a/R/galah_config.R +++ b/R/galah_config.R @@ -243,13 +243,19 @@ enforce_exists <- function(value, #' @keywords Internal enforce_character <- function(value, error_call = rlang::caller_env()){ + # check type if (!is.character(value)) { c("Invalid type", i = "Value must be entered as a string.") |> cli::cli_abort(call = error_call) - }else{ - value } + # check length + if(nchar(value) < 1){ + c("Invalid type", + i = "Strings must contain one or more characters") |> + cli::cli_abort(call = error_call) + } + value } #' Ensure download reason is valid diff --git a/R/messages.R b/R/messages.R index 5ebc40b3..9ef4d6af 100644 --- a/R/messages.R +++ b/R/messages.R @@ -1,4 +1,4 @@ -#' Internal function called by `check_login()` +#' Internal function called by `authenticate()` #' @noRd #' @keywords Internal abort_email_missing <- function(error_call = rlang::caller_env()){ @@ -7,20 +7,11 @@ abort_email_missing <- function(error_call = rlang::caller_env()){ i = "To download occurrence records, species lists, or (for GBIF) occurrence counts, \\ you must provide a valid email address registered with the selected atlas.", - i = "Provide your email address using `galah_config(email = )`.") |> + i = "Provide your email address using `galah_config(email = )` or `authenticate(email = )`.") |> cli::cli_abort(call = error_call) } - -# Internal function called by `check_login()` -# @noRd -# @keywords Internal -# abort_api_key_missing <- function(error_call = caller_env()){ -# bullets <- c("API key has not been specified for the ALA", -# i = "log on to your profile at `https://ala.org.au` to retrieve one", -# i = "use `galah_config(api_key = 'my_key_here') to fix this problem" -# ) -# abort(bullets, call = error_call) -# } +# generalise this to discuss API keys, use of `authenticate()` as in-pipe alternative +# to `galah_config()` #' System-wide, generic failure message #' @noRd diff --git a/R/query_API.R b/R/query_API.R index 11018598..1dcf2610 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -74,7 +74,7 @@ query_API_internal <- function(.query, add_body(.query$body) # NOTE: adding `body` converts from GET to POST # set authentication behaviour - if(!is.null(.query$request$authenticate) & + if(isTRUE(.query$request$authenticate$use_jwt) & .query$type != "metadata/config" # necessary to prevent circular problems ){ diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 3ee1a550..56fefb1c 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -288,8 +288,8 @@ add_email_notify <- function(x) { #' @noRd #' @keywords Internal add_email_address <- function(x, query){ - if(is.null(query$authenticate)){ - x$email <- potions::pour("user", "email") + if(isFALSE(query$authenticate$use_jwt)){ + x$email <- query$authenticate$email } x } @@ -473,10 +473,22 @@ authentication_supported <- function(atlas){ if(atlas %in% c("Australia", "Flanders")){ - TRUE -}else{ - FALSE + TRUE + }else{ + FALSE + } } + +#' Internal function to test whether authentication is required +#' @noRd +#' @keywords Internal +authentication_required <- function(type){ + if(type %in% + c("occurrences", "species")){ + TRUE + }else{ + FALSE + } } #' Internal function to test whether profiles are supported diff --git a/man/authenticate.Rd b/man/authenticate.Rd index e46788e6..12543911 100644 --- a/man/authenticate.Rd +++ b/man/authenticate.Rd @@ -4,24 +4,43 @@ \alias{authenticate} \title{Set up authentication} \usage{ -authenticate(.data, cache_disk = FALSE) +authenticate( + .data, + email = NULL, + username = NULL, + password = NULL, + use_jwt = FALSE, + cache_jwt = FALSE, + ... +) } \arguments{ \item{.data}{An object of class \code{data_request} or \code{metadata_request}} -\item{cache_disk}{(logical) Should JWT tokens be cached to disk? Defaults +\item{email}{(string) Email address registered with the selected organisation} + +\item{username}{(string) Registered username (GBIF only)} + +\item{password}{(string) Registered password (GBIF only)} + +\item{use_jwt}{(logical) Should an OAuth workflow be used for authentication? +Only supported for Flemish and Australian atlases. Defaults to \code{FALSE}} + +\item{cache_jwt}{(logical) Should JWT tokens be cached to disk? Defaults to \code{FALSE}} + +\item{...}{Other arguments, currently ignored} } \value{ An object of the same class as supplied, but with an added \code{authenticate} slot. } \description{ -Add an authentication slot to a query. That slot is then used by later -code to determine whether to add an OAuth workflow. It is triggered -automatically within \code{\link[=capture]{capture()}} if the \code{authenticate} argument -of \code{\link[=galah_config]{galah_config()}} is set to \code{TRUE}, but only for occurrence queries -to the Atlas of Living Australia. +Authenticate a request, either by sending a registered email address +(and, for GBIF, password and username); or by loggin in via the +browser to generate a JWT token. Note that while handling this manually +in-pipe is the most transparent approach, this function is also used to +pass cached information stored via \code{\link[=galah_config]{galah_config()}} to a query. \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}}. } \examples{ From 46b9fae420375efaa0d702f53ea705992340bf18 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 23 Apr 2026 10:40:17 +1000 Subject: [PATCH 15/70] Fix bug in `read_zip()` preventing storage of citation info (#297) --- R/authenticate.R | 4 +- R/galah_config.R | 60 ++++++++++----------- R/messages.R | 4 +- R/read_zip.R | 69 +++++++++++++++---------- tests/testthat/test-atlas_occurrences.R | 14 +++-- 5 files changed, 83 insertions(+), 68 deletions(-) diff --git a/R/authenticate.R b/R/authenticate.R index 551f537c..1ed34913 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -84,7 +84,7 @@ authenticate_with_email <- function(.data, if(is.null(email)){ email <- potions::pour("user", "email", .pkg = "galah") } - if(is.null(email)){ + if(any(is.null(email) | nchar(email) < 1 | is.na(email))){ abort_email_missing(call = call) } update_request_object(.data, @@ -108,7 +108,7 @@ authenticate_with_email_gbif <- function(.data, } # run some checks email_args <- enforce_character(email_args) - if(any(is.null(email_args))){ + if(any(is.null(email_args) | nchar(email_args) < 1 | is.na(email_args), na.rm = TRUE)){ cli::cli_abort("atlas `GBIF` requires email, username, and password for occurrence queries", call = call) } diff --git a/R/galah_config.R b/R/galah_config.R index 0401a6f6..8904d948 100644 --- a/R/galah_config.R +++ b/R/galah_config.R @@ -170,7 +170,7 @@ default_config <- function(){ #' @noRd #' @keywords Internal restructure_config <- function(dots, - error_call= rlang::caller_env()){ + call = rlang::caller_env()){ # NOTE: we use `lapply()` here rather than `purrr::map()` ON PURPOSE # It prevents error messages being prefaced with: # Error in `purrr::map()` at galah-R/R/galah_config.R:171:3: @@ -179,7 +179,7 @@ restructure_config <- function(dots, result <- lapply(names(dots), \(a){validate_config(a, dots[[a]], - error_call = error_call)}) + call = call)}) names(result) <- names(dots) result } @@ -189,25 +189,25 @@ restructure_config <- function(dots, #' @keywords Internal validate_config <- function(name, value, - error_call = rlang::caller_env()) { + call = rlang::caller_env()) { result <- switch(name, "atlas" = { value <- configure_atlas(value) # see whether atlases have changed, and if so, give a message check_atlas(potions::pour("atlas"), value) }, - "authenticate" = enforce_logical(value), - "caching" = enforce_logical(value), + "authenticate" = enforce_logical(value, call = call), + "caching" = enforce_logical(value, call = call), "directory" = check_directory(value), - "download_reason_id" = enforce_download_reason(value), - "email" = enforce_character(value), - "password" = enforce_character(value), - "run_checks" = enforce_logical(value), - "send_email" = enforce_logical(value), - "username" = enforce_character(value), - "verbose" = enforce_logical(value), + "download_reason_id" = enforce_download_reason(value, call = call), + "email" = enforce_character(value, call = call), + "password" = enforce_character(value, call = call), + "run_checks" = enforce_logical(value, call = call), + "send_email" = enforce_logical(value, call = call), + "username" = enforce_character(valu, call = calle), + "verbose" = enforce_logical(value, call = call), enforce_invalid_name(name, - error_call = error_call)) + call = call)) result } @@ -215,10 +215,10 @@ validate_config <- function(name, #' @noRd #' @keywords Internal enforce_logical <- function(value, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ if (!is.logical(value)) { cli::cli_abort("Supplied value must be TRUE or FALSE.", - call = error_call) + call = call) }else{ value } @@ -228,11 +228,11 @@ enforce_logical <- function(value, #' @noRd #' @keywords Internal enforce_exists <- function(value, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ if (!dir.exists(value)) { c("Cache directory does not exist.", i = "Does the directory entered exist?") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) }else{ value } @@ -242,18 +242,12 @@ enforce_exists <- function(value, #' @noRd #' @keywords Internal enforce_character <- function(value, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ # check type - if (!is.character(value)) { + if (!(is.character(value) | is.na(value))) { c("Invalid type", - i = "Value must be entered as a string.") |> - cli::cli_abort(call = error_call) - } - # check length - if(nchar(value) < 1){ - c("Invalid type", - i = "Strings must contain one or more characters") |> - cli::cli_abort(call = error_call) + i = "Value must be entered as a string or `NA`") |> + cli::cli_abort(call = call) } value } @@ -262,7 +256,7 @@ enforce_character <- function(value, #' @noRd #' @keywords Internal enforce_download_reason <- function(value, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ # first ensure API is available. Currently missing for Brazil, for example. reasons_df <- show_all_reasons() |> @@ -278,7 +272,7 @@ enforce_download_reason <- function(value, c("Invalid download reason ID.", i = "Use `show_all(reasons)` to see all valid reasons.", x = "{value} does not match an existing reason ID.") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) } else if(is.character(value) & !(value %in% reasons_df$name)) { bullets <- c( "Invalid download reason name.", @@ -303,18 +297,18 @@ enforce_download_reason <- function(value, #' @noRd #' @keywords Internal enforce_invalid_name <- function(name, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ c("Invalid option name.", i = "See `?galah_config()` for valid options.", x = "\"{name}\" is not a valid option name.") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) } #' Set behaviour for deriving correct atlas information #' @noRd #' @keywords Internal configure_atlas <- function(query, - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ comparison <- do.call(c, node_metadata) comparison <- comparison[!is.na(comparison)] |> @@ -327,7 +321,7 @@ configure_atlas <- function(query, c("Unsupported atlas provided.", i = "Use `show_all(atlases)` to see supported atlases.", x = "\"{query}\" is not a valid atlas.") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) }else{ selected_entry <- comparison[which(lookup == min(lookup))][[1]] diff --git a/R/messages.R b/R/messages.R index 9ef4d6af..de99d0fb 100644 --- a/R/messages.R +++ b/R/messages.R @@ -1,14 +1,14 @@ #' Internal function called by `authenticate()` #' @noRd #' @keywords Internal -abort_email_missing <- function(error_call = rlang::caller_env()){ +abort_email_missing <- function(call = rlang::caller_env()){ c( "No user email was found.", i = "To download occurrence records, species lists, or (for GBIF) occurrence counts, \\ you must provide a valid email address registered with the selected atlas.", i = "Provide your email address using `galah_config(email = )` or `authenticate(email = )`.") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) } # generalise this to discuss API keys, use of `authenticate()` as in-pipe alternative # to `galah_config()` diff --git a/R/read_zip.R b/R/read_zip.R index 52701e07..ba5642b3 100644 --- a/R/read_zip.R +++ b/R/read_zip.R @@ -45,16 +45,13 @@ read_zip <- function(file){ "tsv" = read_zip_gbif(file, data_files), "csv" = read_zip_la(file, data_files)) - # add formatted date - attr(result, "modified_date") <- file.info(file)$mtime |> - format("%e %B %Y") |> - trimws() - # exit safely if(is.null(result)){ cli::cli_abort("No data loaded") }else{ - result + result |> + append_citation(file) |> + append_date(file) } } @@ -112,6 +109,7 @@ read_zip_gbif <- function(file, data_files){ filename = data_files) |> readr::read_tsv(col_types = readr::cols()) |> suppressWarnings() + # FIXME: check whether possible to support DOIs here (currently in `collect_occurrences()`) } #' Internal function to read a zip file from living atlases @@ -119,32 +117,40 @@ read_zip_gbif <- function(file, data_files){ #' @keywords Internal read_zip_la <- function(file, data_files){ # read data - result <- purrr::map(data_files, - function(a, x){ - # create connection to a specific file within zip - conn <- unz(description = x, - filename = a, - open = "rb") - out <- readr::read_csv(conn, - col_types = readr::cols()) |> - suppressWarnings() - close(conn) - return(out) - }, x = file) |> + purrr::map(data_files, + function(a, x){ + # create connection to a specific file within zip + conn <- unz(description = x, + filename = a, + open = "rb") + out <- readr::read_csv(conn, + col_types = readr::cols()) |> + suppressWarnings() + close(conn) + return(out) + }, x = file) |> dplyr::bind_rows() +} + +#' Internal function add doi and/or citation +#' @noRd +#' @keywords Internal +append_citation <- function(df, file){ + all_files <- utils::unzip(file, list = TRUE) |> + dplyr::pull(.data$Name) - # # add doi when mint_doi = TRUE - if(any(data_files == "doi.txt")){ + # add doi when mint_doi = TRUE + if(any(all_files == "doi.txt")){ conn <- unz(description = file, filename = "doi.txt", open = "rb") - attr(result, "doi") <- readr::read_file(conn) |> + attr(df, "doi") <- readr::read_file(conn) |> sub("\\n$", "", x = _) close(conn) } - - # look for citation in README.html - if(any(data_files == "README.html")){ + + # then add citation + if(any(all_files == "README.html")){ conn <- unz(description = file, filename = "README.html", open = "rb") @@ -154,9 +160,20 @@ read_zip_la <- function(file, data_files){ close(conn) cite_check <- grepl("cite", names(readme)) if(any(cite_check)){ - attr(result, "citation") <- readme[cite_check] |> + attr(df, "citation") <- readme[cite_check] |> glue::glue_collapse(sep = "") } } - result + + df # return df } + +#' Internal function add formatted download datetime +#' @noRd +#' @keywords Internal +append_date <- function(df, file){ + attr(df, "modified_date") <- file.info(file)$mtime |> + format("%e %B %Y") |> + trimws() + df +} \ No newline at end of file diff --git a/tests/testthat/test-atlas_occurrences.R b/tests/testthat/test-atlas_occurrences.R index 9639653b..2d4c4cce 100644 --- a/tests/testthat/test-atlas_occurrences.R +++ b/tests/testthat/test-atlas_occurrences.R @@ -21,7 +21,7 @@ quiet_occurrences <- function(...){ } test_that("`atlas_occurrences()` fails nicely if no email is provided", { - galah_config(email = "", run_checks = FALSE) + galah_config(email = NA, run_checks = FALSE) expect_error({ galah_call() |> filter(year == 1900) |> @@ -51,7 +51,6 @@ test_that("collapse(type = 'occurrences') creates an object", { result <- galah_call() |> identify("Perameles") |> collapse() - # expect_equal(length(result), 4) expect_true(inherits(result, "query")) expect_equal(result$type, "data/occurrences") }) @@ -66,7 +65,7 @@ test_that("`compute(type = 'occurrences')` works", { # collapse query_collapse <- collapse(base_query) expect_true(inherits(query_collapse, "query")) - expect_equal(length(query_collapse), 4) + expect_equal(length(query_collapse), 5) expect_equal(query_collapse$type, "data/occurrences") # compute response <- quiet_compute(base_query) @@ -74,6 +73,7 @@ test_that("`compute(type = 'occurrences')` works", { expect_true(response$type == "data/occurrences") expect_equal(names(response), c("type", + "atlas", "status", "total_records", "queue_size", @@ -103,6 +103,7 @@ test_that("`atlas_occurrences()` accepts all narrowing functions inline", { x <- quiet_compute(base_query) expect_equal(names(x), c("type", + "atlas", "status", "total_records", "queue_size", @@ -161,6 +162,9 @@ test_that("`atlas_occurrences()`() and friends accept a file name", { expect_true(any(list.files(directory) == "crinia_file.zip")) # test `collect` occ2 <- base_query |> quiet_collect(file = "crinia_collect") + # these should be the same, except for citations, which will have slightly different times + attr(occ1, "citation") <- NULL + attr(occ2, "citation") <- NULL expect_equal(occ1, occ2) expect_true(any(list.files(directory) == "crinia_collect.zip")) # test DOIs @@ -199,9 +203,9 @@ test_that("`atlas_occurrences()` downloads data from a DOI", { result2 <- request_data() |> filter(doi == doi) |> quiet_collapse() - expect_equal(length(result2), 5) + expect_equal(length(result2), 6) expect_equal(names(result2), - c("type", "url", "headers", "download", "request")) + c("type", "atlas", "url", "headers", "download", "request")) expect_s3_class(result2, "query") expect_equal(result2$type, "data/occurrences-doi") result3 <- quiet_collect(result2) From ffbaf2682d8f83f09816c947ab7096315b6231db Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 23 Apr 2026 18:04:39 +1000 Subject: [PATCH 16/70] Misc bug fixes from `test()` - ensure calls withing `atlas_media()` are always sent to the correct atlas - update `authentication_required()` to return `FALSE` for count queries - ensure `print.metadata_request()` displays size for `type = "media"` - `parse_select()` errors when supplied `tibble` is `NULL` - `check_if_cache_update_needed()` now does what it should - bug fix to `search_identifiers() - add `download_reason_id` arg to `authenticate()` --- R/atlas_media.R | 8 ++- R/authenticate.R | 58 +++++++++++++------ R/capture.R | 2 +- R/capture_occurrences.R | 18 +++--- R/capture_species.R | 2 +- R/check.R | 10 ++-- R/collect_taxa.R | 2 +- R/filter_object_classes.R | 9 ++- R/galah_config.R | 2 +- R/messages.R | 2 +- R/utilities_caching.R | 5 +- R/utilities_internal.R | 21 ++++--- tests/testthat/_snaps/print.md | 55 +++++++++++------- tests/testthat/test-atlas_media.R | 29 +++++++--- tests/testthat/test-dplyr-distinct.R | 3 +- tests/testthat/test-international-Austria.R | 2 +- tests/testthat/test-international-Brazil.R | 2 +- tests/testthat/test-international-Flanders.R | 2 +- tests/testthat/test-international-Sweden.R | 2 +- tests/testthat/test-print.R | 1 + tests/testthat/test-request_metadata_unnest.R | 12 ++-- tests/testthat/test-search_taxa.R | 4 +- 22 files changed, 157 insertions(+), 94 deletions(-) diff --git a/R/atlas_media.R b/R/atlas_media.R index 69fb1f03..39ae77ee 100644 --- a/R/atlas_media.R +++ b/R/atlas_media.R @@ -60,7 +60,7 @@ atlas_media <- function(request = NULL, # add media content to filters if(length(present_fields) > 0){ # do region-specific filter parsing - media_fq <- image_filters(present_fields, atlas = atlas) + media_fq <- image_filters(present_fields, atlas = .query$atlas) # add back to source object if(length(media_fq) > 1){ media_fq <- glue::glue("({glue::glue_collapse(media_fq, ' OR ')})") @@ -73,10 +73,12 @@ atlas_media <- function(request = NULL, # get occurrences, expand to one row per media entry occ <- query_collapse |> collect(wait = TRUE) |> - build_media_id(media_fields = present_fields) + build_media_id(media_fields = present_fields[present_fields != "multimedia"]) + # NOTE: `multimedia` field contains a description of which field has IDs + # _not_ IDs themselves; hence removal in above processing stage # collect media metadata - media_query <- request_metadata() |> + media_query <- request_metadata(from = .query$atlas) |> filter(media == dplyr::pull(occ, "media_id")) if(isTRUE(all_fields)){ media_query <- media_query |> diff --git a/R/authenticate.R b/R/authenticate.R index 1ed34913..2e0a2520 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -6,15 +6,17 @@ #' in-pipe is the most transparent approach, this function is also used to #' pass cached information stored via [galah_config()] to a query. #' `r lifecycle::badge("experimental")`. -#' @param .data An object of class `data_request` or `metadata_request` -#' @param email (string) Email address registered with the selected organisation -#' @param username (string) Registered username (GBIF only) -#' @param password (string) Registered password (GBIF only) +#' @param .data An object of class `data_request` or `metadata_request`. +#' @param email (string) Email address registered with the selected organisation. +#' @param username (string) Registered username (GBIF only). +#' @param password (string) Registered password (GBIF only). +#' @param download_reason_id (integer) ID for the download reason. +#' See `show_all_reasons()` for accepted values. #' @param use_jwt (logical) Should an OAuth workflow be used for authentication? #' Only supported for Flemish and Australian atlases. Defaults to `FALSE` #' @param cache_jwt (logical) Should JWT tokens be cached to disk? Defaults -#' to `FALSE` -#' @param ... Other arguments, currently ignored +#' to `FALSE`. +#' @param ... Other arguments, currently ignored. #' @returns An object of the same class as supplied, but with an added #' `authenticate` slot. #' @examples \dontrun{ @@ -36,6 +38,7 @@ authenticate <- function(.data, email = NULL, username = NULL, password = NULL, + download_reason_id = NULL, use_jwt = FALSE, cache_jwt = FALSE, ...){ @@ -43,13 +46,14 @@ authenticate <- function(.data, # handle GBIF first if(.data$atlas == "Global"){ # only occurrence queries require authentication for this atlas - if(authentication_required(.data$type)){ + if(authentication_required(.data)){ # ignore token requests if(isTRUE(use_jwt)){ cli::cli_warn("JWT-based authentication not supported for GBIF: skipping") } authenticate_with_email_gbif(.data, email = email, + download_reason_id = download_reason_id, username = username, password = password) }else{ @@ -61,16 +65,25 @@ authenticate <- function(.data, # 1. user has specifically requested it, or # 2. no email is provided }else{ + # get and check the download reason + if(is.null(download_reason_id)){ + download_reason_id <- potions::pour("user", "download_reason_id") + } + check_reason(download_reason_id) + if( (isTRUE(use_jwt) | is.null(email)) & authentication_supported(.data$atlas) ){ update_request_object(.data, authenticate = list(email = NULL, + download_reason_id = download_reason_id, use_jwt = TRUE, use_apikey = FALSE, # not supported yet cache_disk = cache_jwt)) }else{ - authenticate_with_email(.data, email = email) + authenticate_with_email(.data, + email = email, + download_reason_id = download_reason_id) } } } @@ -80,6 +93,7 @@ authenticate <- function(.data, #' @keywords Internal authenticate_with_email <- function(.data, email = NULL, + download_reason_id, call = rlang::caller_env()){ if(is.null(email)){ email <- potions::pour("user", "email", .pkg = "galah") @@ -88,8 +102,11 @@ authenticate_with_email <- function(.data, abort_email_missing(call = call) } update_request_object(.data, - authenticate = list(use_jwt = FALSE, - email = email)) + authenticate = list(email = email, + download_reason_id = download_reason_id, + use_jwt = FALSE, + use_apikey = FALSE, + cache_disk = FALSE)) } #' Internal function to build a valid email authentication for GBIF @@ -99,23 +116,30 @@ authenticate_with_email_gbif <- function(.data, email = NULL, username = NULL, password = NULL, + download_reason_id, call = rlang::caller_env()){ - # then look for local email etc + # look for local email etc email_args <- c(email, username, password) if(any(is.null(email_args))){ cached_user <- potions::pour("user", .pkg = "galah") email_args <- unlist(cached_user)[c("email", "username", "password")] } # run some checks - email_args <- enforce_character(email_args) + email_args <- purrr::map(email_args, enforce_character) |> + unlist() if(any(is.null(email_args) | nchar(email_args) < 1 | is.na(email_args), na.rm = TRUE)){ - cli::cli_abort("atlas `GBIF` requires email, username, and password for occurrence queries", - call = call) + c("GBIF requires the following information for occurrence queries:", + i = "`email`, `username`, `password` and `download_reason_id`", + i = "These can be supplied globally via {.fn galah::galah_config}, or in-pipe via {.fn galah::authenticate}.") |> + cli::cli_abort(call = call) } # save out update_request_object(.data, - authenticate = list(use_jwt = FALSE, - email = email, + authenticate = list(email = email, username = username, - password = password)) + password = password, + download_reason_id = download_reason_id, + use_jwt = FALSE, + use_apikey = FALSE, + cache_disk = FALSE)) } \ No newline at end of file diff --git a/R/capture.R b/R/capture.R index 679669b1..072158f6 100644 --- a/R/capture.R +++ b/R/capture.R @@ -173,7 +173,7 @@ check_authentication <- function(x){ if(!is.null(x$authenticate)){ x }else{ - if(authentication_required(x$type)){ + if(authentication_required(x)){ config_options <- c(list(x), potions::pour("user", .pkg = "galah")) names(config_options)[c(1, 2)] <- c(".data", "use_jwt") diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index d811a75d..1fc8c56d 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -3,12 +3,12 @@ #' @keywords Internal capture_occurrences <- function(.query, ..., - error_call = rlang::caller_env()){ + call = rlang::caller_env()){ if(is.null(.query$filter) & is.null(.query$identify) & is.null(.query$geolocate)){ cli::cli_abort("No filters supplied to `capture()` with `type = \"occurrences\"`", - call = error_call) + call = call) } switch(.query$atlas, "Global" = capture_occurrences_gbif(.query, ...), @@ -19,14 +19,14 @@ capture_occurrences <- function(.query, #' @noRd #' @keywords Internal capture_occurrences_gbif <- function(.query, - format = "SIMPLE_CSV", + format = "SIMPLE_CSV", ...){ # get user string - username <- potions::pour("user", "username", .pkg = "galah") - password <- potions::pour("user", "password", .pkg = "galah") - user_string <- glue::glue("{username}:{password}") .query$type <- "data/occurrences" - + username <- .query$authenticate$username + password <- .query$authenticate$password + user_string <- glue::glue("{username}:{password}") + # build object list(type = .query$type, atlas = .query$atlas, @@ -51,7 +51,7 @@ capture_occurrences_gbif <- function(.query, #' @noRd #' @keywords Internal capture_occurrences_la <- function(.query, - mint_doi = FALSE){ + mint_doi = FALSE){ .query$type <- "data/occurrences" # build a query query <- c(build_query(identify = .query$identify, @@ -63,7 +63,7 @@ capture_occurrences_la <- function(.query, qa = "`ASSERTIONS_PLACEHOLDER`", facet = "false", sourceTypeId = source_type_id_lookup(.query$atlas), - reasonTypeId = potions::pour("user", "download_reason_id"), + reasonTypeId = .query$authenticate$download_reason_id, dwcHeaders = "true") |> add_email_notify() |> add_email_address(query = .query) |> diff --git a/R/capture_species.R b/R/capture_species.R index bf671379..7228cfe4 100644 --- a/R/capture_species.R +++ b/R/capture_species.R @@ -45,7 +45,7 @@ capture_species_atlas <- function(.query){ apply_profile = .query$apply_profile, atlas = .query$atlas), sourceTypeId = 2004, - reasonTypeId = potions::pour("user", "download_reason_id"), + reasonTypeId = .query$authenticate$download_reason_id, facets = .query$distinct$name, parse_select_species(.query$select)) |> add_email_address(query = .query) |> diff --git a/R/check.R b/R/check.R index e3d69583..812bf96f 100644 --- a/R/check.R +++ b/R/check.R @@ -598,21 +598,21 @@ check_profiles <- function(.query, #' Internal function to check that a reason code is valid #' @noRd #' @keywords Internal -check_reason <- function(.query, +check_reason <- function(.query, error_call = rlang::caller_env()){ if(reasons_supported(atlas = .query$atlas)) { if(.query$type %in% c("data/occurrences", "data/species")){ query <- httr2::url_parse(.query$url)$query if(is.null(query$reasonTypeId)){ c("Missing a valid download reason.", - i = "See `show_all(reasons)`.", - i = "Use `galah_config(download_reason_id = ...)` to set a download reason.") |> - cli::cli_abort(call = error_call) + i = "See `show_all(reasons)`.", + i = "You can set {.arg download_reason_id} globally using `{.fn galah::galah_config}`, or in-pipe using {.fn galah::authenticate}.") |> + cli::cli_abort(call = error_call) }else{ user_reason <- query$reasonTypeId valid_reasons <- .query[["metadata/reasons"]]$id if(!(user_reason %in% valid_reasons)){ - c( + c( "Invalid download reason ID.", i = "Use `show_all(reasons)` to see all valid reasons.", x = "\"{user_reason}\" does not match an existing reason ID.") |> diff --git a/R/collect_taxa.R b/R/collect_taxa.R index 6593361c..643355df 100644 --- a/R/collect_taxa.R +++ b/R/collect_taxa.R @@ -201,7 +201,7 @@ collect_identifiers <- function(.query){ # Check for invalid search terms if (galah_config()$package$verbose) { - check_search_terms(result) + check_search_terms(result, atlas = .query$atlas) } result |> diff --git a/R/filter_object_classes.R b/R/filter_object_classes.R index e7268762..b7ce2635 100644 --- a/R/filter_object_classes.R +++ b/R/filter_object_classes.R @@ -67,7 +67,14 @@ print.predicates_filter <- function(x, ...){ #' @order 7 #' @export print.metadata_filter <- function(x, ...){ - glue::glue("Object of class `metadata_filter` with type `{x$variable}` (n = {length(x$data)} entries)") |> + if(!is.null(x$data)){ + size_text <- glue::glue("(n = {length(x$data)} entries)") + }else if(!is.null(x$filter$value)){ # type = "media" + size_text <- glue::glue("(n = {length(x$filter$value)} entries)") + }else{ + size_text <- "" + } + glue::glue("Object of class `metadata_filter` with type `{x$variable}` {size_text}") |> cat() } diff --git a/R/galah_config.R b/R/galah_config.R index 8904d948..8b9b32b9 100644 --- a/R/galah_config.R +++ b/R/galah_config.R @@ -204,7 +204,7 @@ validate_config <- function(name, "password" = enforce_character(value, call = call), "run_checks" = enforce_logical(value, call = call), "send_email" = enforce_logical(value, call = call), - "username" = enforce_character(valu, call = calle), + "username" = enforce_character(value, call = call), "verbose" = enforce_logical(value, call = call), enforce_invalid_name(name, call = call)) diff --git a/R/messages.R b/R/messages.R index de99d0fb..e0e22a82 100644 --- a/R/messages.R +++ b/R/messages.R @@ -7,7 +7,7 @@ abort_email_missing <- function(call = rlang::caller_env()){ i = "To download occurrence records, species lists, or (for GBIF) occurrence counts, \\ you must provide a valid email address registered with the selected atlas.", - i = "Provide your email address using `galah_config(email = )` or `authenticate(email = )`.") |> + i = "This can be supplied globally via {.fn galah::galah_config}, or in-pipe via {.fn galah::authenticate}.") |> cli::cli_abort(call = call) } # generalise this to discuss API keys, use of `authenticate()` as in-pipe alternative diff --git a/R/utilities_caching.R b/R/utilities_caching.R index bc23dc7e..a60457da 100644 --- a/R/utilities_caching.R +++ b/R/utilities_caching.R @@ -56,14 +56,15 @@ retrieve_cache <- function(slot_name){ check_if_cache_update_needed <- function(x, # where x is a query object function_name ){ + # first get df from cached slot + df <- retrieve_cache(function_name) # build some checks caching_disabled <- !potions::pour("package", "caching", .pkg = "galah") - is_local <- !is.null(attr(df, "ARCHIVED")) is_wrong_atlas <- attr(df, "region") != x$atlas is_too_short <- nrow(df) < 1 # somewhat arbitrary, but catches empty tibbles # evaluate those checks # if any are TRUE, update is needed - result <- is_local | is_wrong_atlas | is_too_short | caching_disabled + result <- is_wrong_atlas | is_too_short | caching_disabled if(length(result) < 1){ # bug catcher TRUE }else{ diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 56fefb1c..0c9bd24b 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -9,7 +9,12 @@ #' only happen at the end of a pipe. #' @noRd #' @keywords Internal -parse_select <- function(df, .query){ +parse_select <- function(df, + .query, + call = rlang::caller_env()){ + if(is.null(df)){ + cli::cli_abort("Unable to call `select()` without supplying a `tibble`", call = call) + } # get quosures captured by `select()` quo_list <- purrr::pluck(.query, "request", "select", "quosure") # map() over list of quosures @@ -482,13 +487,13 @@ authentication_supported <- function(atlas){ #' Internal function to test whether authentication is required #' @noRd #' @keywords Internal -authentication_required <- function(type){ - if(type %in% - c("occurrences", "species")){ - TRUE - }else{ - FALSE - } +authentication_required <- function(x){ + # only required for downloads (in current version) + x$type %in% c("occurrences", "species") & + # only required when not generating counts + is.null(x$group_by) & + is.null(x$count) & + is.null(x$glimpse) } #' Internal function to test whether profiles are supported diff --git a/tests/testthat/_snaps/print.md b/tests/testthat/_snaps/print.md index 43284d92..bd9d5b2b 100644 --- a/tests/testthat/_snaps/print.md +++ b/tests/testthat/_snaps/print.md @@ -5,6 +5,7 @@ Message Object of type data_request containing: * type occurrences + * atlas Australia # populated `data_request()` prints correctly @@ -13,6 +14,7 @@ Message Object of type data_request containing: * type occurrences + * atlas Australia * identify search_term: crinia * filter Object of class `data_filter`: year == 2025 @@ -23,6 +25,7 @@ Message Object of type data_request containing: * type occurrences + * atlas Australia # object of class `metadata-request` formats correctly with `filter() @@ -31,7 +34,8 @@ Message Object of type metadata_request containing: * type lists - * filter Object of class `metadata_filter` with type `list` (n = 0 entries) + * atlas Australia + * filter Object of class `metadata_filter` with type `list` # object of class `metadata-request` formats correctly with `identify() @@ -40,6 +44,7 @@ Message Object of type metadata_request containing: * type taxa + * atlas Australia * identify search_term: Crinia # object of class `metadata-request` formats correctly with `identify() |> unnest() @@ -49,6 +54,7 @@ Message Object of type metadata_request containing: * type taxa-unnest + * atlas Australia * identify search_term: Crinia # object of class `query` formats correctly @@ -57,6 +63,7 @@ capture(identify(request_metadata(), "Crinia")) Message Object of class query with type metadata/taxa-single + * atlas: Australia * url: https://api.ala.org.au/namematching/api/search?q=Crinia # object of class `computed_query` formats correctly @@ -65,19 +72,9 @@ x Message Object of class computed query with type metadata/taxa-single + * atlas: Australia * url: https://api.ala.org.au/namematching/api/search?q=Crinia -# object of class `query_set` formats correctly - - Code - compound(filter(galah_call(), basisOfRecord == "HUMAN_OBSERVATION")) - Message - Object of class query_set containing 3 queries: - * metadata/fields url: https://api.ala.org.au/occurrences/index/fields - * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes - * data/occurrences url: - https://api.ala.org.au/occurrences/occurrences/offline/... - # `galah_config()` formats correctly Code @@ -85,6 +82,16 @@ Message `galah` package configuration + Atlas + Atlas of Living Australia (ALA), Australia + + User + x authentication + username [Not Provided] + email + password [Not Provided] + download_reason_id 10 + Package x verbose v run_checks @@ -92,14 +99,18 @@ v caching i directory: something - User - x authentication - username [Provided] - email ala4r@ala.org.au - password [Provided] - download_reason_id 10 - - Atlas - Atlas of Living Australia (ALA), Australia - + +# object of class `query_set` formats correctly + + Code + compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), + basisOfRecord == "HUMAN_OBSERVATION")) + Message + Object of class query_set containing 5 queries: + * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig + * metadata/fields url: https://api.ala.org.au/occurrences/index/fields + * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes + * metadata/reasons url: https://api.ala.org.au/logger/service/logger/reasons + * data/occurrences url: + https://api.ala.org.au/occurrences/occurrences/offline/... diff --git a/tests/testthat/test-atlas_media.R b/tests/testthat/test-atlas_media.R index 4b2a8540..2ab5ae17 100644 --- a/tests/testthat/test-atlas_media.R +++ b/tests/testthat/test-atlas_media.R @@ -27,8 +27,8 @@ test_that("atlas_media fails when no filters are provided", { test_that("`atlas_media()` works", { skip_if_offline(); skip_on_ci() - capture_config <- purrr_config(email = "ala4r@ala.org.au") media_data <- galah_call() |> + authenticate(email = "ala4r@ala.org.au") |> identify("Microseris lanceolata") |> filter(year == 2019) |> quiet_media() @@ -37,6 +37,7 @@ test_that("`atlas_media()` works", { expect_gte(ncol(media_data), 3) # set `all_fields` = TRUE all_media_data <- galah_call() |> + authenticate(email = "ala4r@ala.org.au") |> identify("Microseris lanceolata") |> filter(year == 2019) |> quiet_media(all_fields = TRUE) @@ -92,9 +93,10 @@ test_that("`collapse()` and `collect()` work for `type = 'media'`", { filter(media == unlist(dplyr::pull(occ_collect, "images"))) |> quiet_collapse() expect_true(inherits(media_collapse, "query")) - expect_equal(length(media_collapse), 4) + expect_equal(length(media_collapse), 5) expect_equal(names(media_collapse), - c("type", + c("type", + "atlas", "url", "headers", "request")) @@ -102,9 +104,10 @@ test_that("`collapse()` and `collect()` work for `type = 'media'`", { # compute media_compute <- quiet_compute(media_collapse) expect_true(inherits(media_compute, "computed_query")) - expect_equal(length(media_compute), 4) + expect_equal(length(media_compute), 5) expect_equal(names(media_compute), - c("type", + c("type", + "atlas", "url", "headers", "request")) @@ -126,16 +129,24 @@ test_that("`collapse()` and `collect()` work for `type = 'media'`", { filter(media == df) |> quiet_collapse(thumbnail = TRUE) expect_true(inherits(files_collapse, "query")) - expect_equal(length(files_collapse), 4) + expect_equal(length(files_collapse), 5) expect_equal(names(files_collapse), - c("type", "url", "headers", "request")) + c("type", + "atlas", + "url", + "headers", + "request")) expect_equal(files_collapse$type, "files/media") # compute files_compute <- quiet_compute(files_collapse) expect_true(inherits(files_compute, "computed_query")) - expect_equal(length(files_compute), 4) + expect_equal(length(files_compute), 5) expect_equal(names(files_compute), - c("type", "url", "headers", "request")) + c("type", + "atlas", + "url", + "headers", + "request")) # collect files_collect <- quiet_collect(files_compute) expect_s3_class(files_collect, c("tbl_df", "tbl", "data.frame")) diff --git a/tests/testthat/test-dplyr-distinct.R b/tests/testthat/test-dplyr-distinct.R index fd8edb05..d6c693d1 100644 --- a/tests/testthat/test-dplyr-distinct.R +++ b/tests/testthat/test-dplyr-distinct.R @@ -10,7 +10,8 @@ test_that("`group_by()` without `distinct()` returns occurrences, not species", skip_if_offline(); skip_on_ci() query <- galah_call() |> filter(year == 2024, - genus == "Crinia") + genus == "Crinia") |> + authenticate(email = "ala4r@ala.org.au") expected_n <- query |> count() |> quiet_collect() diff --git a/tests/testthat/test-international-Austria.R b/tests/testthat/test-international-Austria.R index 917cdf1c..b6355b2d 100644 --- a/tests/testthat/test-international-Austria.R +++ b/tests/testthat/test-international-Austria.R @@ -227,7 +227,7 @@ test_that("atlas_occurrences works for Austria", { skip_if(inherits(occ_collapse, "try-error"), message = "API not available") expect_s3_class(occ_collapse, "query") expect_equal(names(occ_collapse), - c("type", "url", "headers", "request")) + c("type", "atlas", "url", "headers", "request")) expect_equal(occ_collapse$type, "data/occurrences") # compute occ_compute <- compute(occ_collapse) diff --git a/tests/testthat/test-international-Brazil.R b/tests/testthat/test-international-Brazil.R index 4dd70b8e..d017e4e8 100644 --- a/tests/testthat/test-international-Brazil.R +++ b/tests/testthat/test-international-Brazil.R @@ -194,7 +194,7 @@ test_that("`atlas_occurrences()` works for Brazil", { skip_if(inherits(occ_collapse, "try-error"), message = "API not available") expect_s3_class(occ_collapse, "query") expect_equal(names(occ_collapse), - c("type", "url", "headers", "request")) + c("type", "atlas", "url", "headers", "request")) occ_compute <- compute(occ_collapse) |> try(silent = TRUE) skip_if(inherits(occ_compute, "try-error"), message = "API not available") diff --git a/tests/testthat/test-international-Flanders.R b/tests/testthat/test-international-Flanders.R index ff28ac8c..e2d685cc 100644 --- a/tests/testthat/test-international-Flanders.R +++ b/tests/testthat/test-international-Flanders.R @@ -203,7 +203,7 @@ test_that("`count()` works with galah_identify for Flanders", { test_that("`glimpse()` works for Flanders", { skip_if_offline(); skip_on_ci() - x <- galah_call() |> + x <- galah_call(from = "Flanders") |> filter(year == 2025) |> glimpse() |> collect() diff --git a/tests/testthat/test-international-Sweden.R b/tests/testthat/test-international-Sweden.R index 817590bc..90ae1256 100644 --- a/tests/testthat/test-international-Sweden.R +++ b/tests/testthat/test-international-Sweden.R @@ -158,7 +158,7 @@ test_that("`search_taxa()` works for multiple ranks in Sweden", { expect_true(all(grepl("^[[:digit:]]+$", taxa$taxon_concept_id))) }) -test_that("s`how_values()` works fields in Sweden", { +test_that("`show_values()` works fields in Sweden", { skip_if_offline(); skip_on_ci() quiet_values <- function(...){ x <- purrr::quietly(show_values) diff --git a/tests/testthat/test-print.R b/tests/testthat/test-print.R index f24c0632..8dd5a8c4 100644 --- a/tests/testthat/test-print.R +++ b/tests/testthat/test-print.R @@ -52,6 +52,7 @@ test_that("object of class `query_set` formats correctly", { # prevent earlier caching (or lack thereof) affecting the result reset_cache() galah_call() |> + authenticate(email = "ala4r@ala.org.au") |> filter(basisOfRecord == "HUMAN_OBSERVATION") |> compound() |> expect_snapshot() diff --git a/tests/testthat/test-request_metadata_unnest.R b/tests/testthat/test-request_metadata_unnest.R index 8efc2ecc..e69f6ef7 100644 --- a/tests/testthat/test-request_metadata_unnest.R +++ b/tests/testthat/test-request_metadata_unnest.R @@ -74,7 +74,7 @@ test_that("request_metadata() |> unnest() works for type = 'lists'", { expect_s3_class(x, "query") expect_equal(x$type, "metadata/lists-unnest") expect_equal(names(x), - c("type", "url", "request")) + c("type", "atlas", "url", "request")) y <- compute(x) expect_s3_class(y, "computed_query") z <- collect(y) @@ -114,7 +114,7 @@ test_that("`request_metadata() |> unnest() |> collapse()` works for type = profi expect_s3_class(x, "query") expect_equal(x$type, "metadata/profiles-unnest") expect_equal(names(x), - c("type", "url", "request")) + c("type", "atlas", "url", "request")) }) test_that("request_metadata() |> unnest() works for type = 'profiles'", { @@ -156,8 +156,8 @@ test_that("request_metadata() |> unnest() works for type = 'taxa' using `identif unnest() |> collapse() expect_s3_class(x, "query") - expect_equal(length(x), 4) - expect_equal(names(x), c("type", "url", "headers", "request")) + expect_equal(length(x), 6) + expect_equal(names(x), c("type", "atlas", "url", "headers", "request", "supplied_taxon")) expect_equal(x$type, "metadata/taxa-unnest") y <- compute(x) expect_s3_class(y, "computed_query") @@ -175,8 +175,8 @@ test_that("request_metadata() |> unnest() works for type = 'taxa' using `filter( unnest() |> collapse() expect_s3_class(x, "query") - expect_equal(length(x), 4) - expect_equal(names(x), c("type", "url", "headers", "request")) + expect_equal(length(x), 5) + expect_equal(names(x), c("type", "atlas", "url", "headers", "request")) expect_equal(x$type, "metadata/taxa-unnest") y <- compute(x) expect_s3_class(y, "computed_query") diff --git a/tests/testthat/test-search_taxa.R b/tests/testthat/test-search_taxa.R index 5ff37992..412f9c1a 100644 --- a/tests/testthat/test-search_taxa.R +++ b/tests/testthat/test-search_taxa.R @@ -144,11 +144,11 @@ test_that("`request_metadata()` works for `type = 'taxa'`", { x <- request_metadata() |> identify("crinia") expect_s3_class(x, "metadata_request") - expect_equal(names(x), c("type", "identify")) + expect_equal(names(x), c("type", "atlas", "identify")) expect_equal(x$identify$search_term, "crinia") y <- collapse(x) expect_s3_class(y, "query") - expect_equal(names(y), c("type", "url", "headers", "request")) + expect_equal(names(y), c("type", "atlas", "url", "headers", "request")) z <- collect(y) expect_s3_class(z, c("tbl_df", "tbl", "data.frame")) expect_equal(nrow(z), 1) From c341c1b62360b10ab7204e2bc6e44b062e477ac6 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 24 Apr 2026 09:53:27 +1000 Subject: [PATCH 17/70] Temporarily deactivate events for next release (#226) Can be added again later --- R/capture_events.R | 2 +- R/galah_call.R | 4 ++-- tests/testthat/test-atlas_events.R | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/R/capture_events.R b/R/capture_events.R index c64a7429..0328b9b0 100644 --- a/R/capture_events.R +++ b/R/capture_events.R @@ -3,7 +3,7 @@ #' @keywords Internal capture_events <- function(x, error_call = rlang::caller_env()){ - browser() + x # placeholder, obvs } #' Internal function to run `capture()` for type = "events-count" diff --git a/R/galah_call.R b/R/galah_call.R index c6cf5bde..d04ee2d2 100644 --- a/R/galah_call.R +++ b/R/galah_call.R @@ -81,8 +81,8 @@ galah_call <- function(type = c("occurrences", # "distributions", "species", "species-count", - "events", - "events-count" + # "events", + # "events-count" ), from = NULL){ # set type diff --git a/tests/testthat/test-atlas_events.R b/tests/testthat/test-atlas_events.R index c563ba22..e6d25d2a 100644 --- a/tests/testthat/test-atlas_events.R +++ b/tests/testthat/test-atlas_events.R @@ -1,13 +1,18 @@ +test_that("`galah_call(type = 'events') is deactivated", { + expect_error(galah_call(type = "events")) +}) + + test_that("`galah_call(type = 'events')` works", { - skip_if_offline(); skip_on_ci() + skip("Not yet enabled") x <- galah_call(type = "events") - expect_equal(names(x), "type") + expect_equal(names(x), c("type", "atlas")) expect_equal(x$type, "events") expect_s3_class(x, "data_request") }) test_that("`count()` works for type = 'events'", { - skip_if_offline(); skip_on_ci() + skip("Not yet enabled") x <- galah_call(type = "events") |> count() |> collect() @@ -17,7 +22,7 @@ test_that("`count()` works for type = 'events'", { }) test_that("`describe()` works for type = 'events'", { - skip_if_offline(); skip_on_ci() + skip("Not yet enabled") x <- galah_call(type = "events") |> describe() |> collect() From d77235c8e46fb94712b4cd062494c4ed2191a7fc Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 24 Apr 2026 10:03:23 +1000 Subject: [PATCH 18/70] fix `glimpse` pipeline somehow broken by changes to authentication --- R/authenticate.R | 5 +++-- R/capture_occurrences.R | 1 + R/collapse_checks.R | 5 +++-- R/utilities_internal.R | 19 +++++++++++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/R/authenticate.R b/R/authenticate.R index 2e0a2520..0a15e72e 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -65,11 +65,12 @@ authenticate <- function(.data, # 1. user has specifically requested it, or # 2. no email is provided }else{ - # get and check the download reason + # get the download reason, if not supplied if(is.null(download_reason_id)){ download_reason_id <- potions::pour("user", "download_reason_id") } - check_reason(download_reason_id) + # NOTE: We don't _check_ the reason here, because that has to happen + # at `collapse()`, because we need to ping an API first if( (isTRUE(use_jwt) | is.null(email)) & authentication_supported(.data$atlas) diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index 1fc8c56d..7b9c66c3 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -164,6 +164,7 @@ capture_occurrences_glimpse <- function(.query){ purrr::pluck("path") url$query$pageSize <- 3 # add a pageSize arg result$url <- httr2::url_build(url) # rebuild + result$type <- "data/occurrences-glimpse" # return as_prequery(result) diff --git a/R/collapse_checks.R b/R/collapse_checks.R index fbdf51b5..62e2e75b 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -70,8 +70,9 @@ collapse_run_checks <- function(.query, # run remaining checks, if requested by the user if(potions::pour("package", "run_checks")) { - # events doesn't need a reason, but others do - if(.query$type %in% c("data/occurrences", "data/species")){ + # events doesn't need a reason, but others do, unless they have `-glimpse` + if(.query$type %in% c("data/occurrences", "data/species") & + .query$request$type != "occurrences-glimpse"){ .query <- .query |> check_reason(error_call) } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 0c9bd24b..8fdc0d86 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -488,12 +488,19 @@ authentication_supported <- function(atlas){ #' @noRd #' @keywords Internal authentication_required <- function(x){ - # only required for downloads (in current version) - x$type %in% c("occurrences", "species") & - # only required when not generating counts - is.null(x$group_by) & - is.null(x$count) & - is.null(x$glimpse) + # only required for downloads (in current version) + download_check <- x$type %in% c("occurrences", "species") & + # only required when not generating counts + # is.null(x$group_by) & # <- not needed, as group_by() can preface count() or distinct() + is.null(x$count) & + is.null(x$glimpse) + + # does distinct require authentication? Only if present and keep_all = TRUE + if(is.null(x$distinct)){ + download_check + }else{ + (x$distinct$keep_all | is.null(x$group_by)) & download_check + } } #' Internal function to test whether profiles are supported From 13a6e9ef183a7a27fb184fdfa03e9f8dc156b6a8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 24 Apr 2026 10:03:57 +1000 Subject: [PATCH 19/70] Fix `url_lookup()` Tests broken by changes from accepting a sting to a query --- R/check.R | 7 ++++--- R/url_lookup.R | 9 +++++++-- tests/testthat/test-url_lookup.R | 23 +++++++++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/R/check.R b/R/check.R index 812bf96f..b99551d0 100644 --- a/R/check.R +++ b/R/check.R @@ -512,10 +512,10 @@ check_occurrence_response <- function(.query, error_type <- sub("\\:.*", "", .query$message) |> stringr::str_trim() - + message <- .query$message bullets <- c( "There was a problem with your query.", - "*" = glue::glue("message: {.query$message}")) + "*" = glue::glue("message: {message}")) switch(as.character(error_type), "500" = {cli::cli_abort(bullets, @@ -549,7 +549,8 @@ check_occurrence_response <- function(.query, } # convert `key` to `status_url` if(is.null(.query$status_url) & !is.null(.query$key)){ - .query$status_url <- glue::glue("https://api.gbif.org/v1/occurrence/download/{.query$key}") + key_value <- .query$key + .query$status_url <- glue::glue("https://api.gbif.org/v1/occurrence/download/{key_value}") } # add `queue_size` if(is.null(.query$queue_size)){ diff --git a/R/url_lookup.R b/R/url_lookup.R index 6a388ac8..8d16fd14 100644 --- a/R/url_lookup.R +++ b/R/url_lookup.R @@ -35,8 +35,13 @@ url_lookup <- function(.query, if(quiet){ return(NULL) }else{ - c("No API is available for type `{.query$type}`", - i = "Selected atlas: {.query$atlas}", + # write message + type <- .query$type + atlas <- .query$atlas + # this syntax is cumbersome, but necessary, as cli >3.4.0 doesn't accect + # {.query$type} (no leading dots) + c("No API is available for type `{type}`", + i = "Selected atlas: {atlas}", i = "Use `show_all_apis()` to list valid API calls") |> cli::cli_abort(call = error_call) } diff --git a/tests/testthat/test-url_lookup.R b/tests/testthat/test-url_lookup.R index 36e8c277..e9b52665 100644 --- a/tests/testthat/test-url_lookup.R +++ b/tests/testthat/test-url_lookup.R @@ -1,30 +1,37 @@ test_that("`url_lookup()` errors for nonsense strings", { - url_lookup("something") |> + list(type = "something", atlas = "Australia") |> + as_query() |> + url_lookup() |> expect_error(label = "No API is available") }) test_that("`url_lookup()` returns a URL for a valid input", { - url_lookup("metadata/assertions") |> + list(type = "metadata/assertions", atlas = "Australia") |> + as_query() |> + url_lookup() |> stringr::str_detect("^https://api.ala.org.au") |> expect_true() }) test_that("`url_lookup()` parses named inputs correctly", { - url_lookup("metadata/taxa-single", - name = "Crinia") |> + list(type = "metadata/taxa-single", atlas = "Australia") |> + as_query() |> + url_lookup(name = "Crinia") |> stringr::str_detect("search\\?q=Crinia$") |> expect_true() }) test_that("`url_lookup()` errors for incorrect named inputs", { - url_lookup("metadata/taxa-single", - something = "Crinia") |> + list(type = "metadata/taxa-single", atlas = "Australia") |> + as_query() |> + url_lookup(something = "Crinia") |> expect_error() }) test_that("`url_lookup()` parses multiple named inputs correctly", { - url_lookup("metadata/taxa-single", - name = c("Crinia", "Limnodynastes")) |> + list(type = "metadata/taxa-single", atlas = "Australia") |> + as_query() |> + url_lookup(name = c("Crinia", "Limnodynastes")) |> length() |> expect_equal(2) }) From 2867a30b70f45b590d9442972d979a189edde1c6 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 29 Apr 2026 16:57:58 +1000 Subject: [PATCH 20/70] More bug fixes --- R/atlas_media.R | 2 +- R/capture_occurrences.R | 4 +- R/check.R | 18 ++++++++- R/collapse_checks.R | 5 +++ R/collect_occurrences.R | 4 +- R/compute_occurrences.R | 4 +- R/galah_call.R | 2 +- R/query_API.R | 1 + R/utilities_internal.R | 37 +++++++++++-------- man/authenticate.Rd | 16 +++++--- man/galah_call.Rd | 4 +- tests/testthat/test-galah_call.R | 17 +++++---- tests/testthat/test-international-Austria.R | 39 ++++++++++---------- tests/testthat/test-international-Flanders.R | 8 ++-- tests/testthat/test-international-GBIF.R | 25 +++++++++---- tests/testthat/test-international-Sweden.R | 10 ++--- tests/testthat/test-international-UK.R | 2 +- 17 files changed, 122 insertions(+), 76 deletions(-) diff --git a/R/atlas_media.R b/R/atlas_media.R index 39ae77ee..752fb25b 100644 --- a/R/atlas_media.R +++ b/R/atlas_media.R @@ -52,7 +52,7 @@ atlas_media <- function(request = NULL, i = "try `galah_select({selected_text}, group = 'media')` instead") |> cli::cli_abort() }else{ - present_fields <- selected_fields[selected_fields %in% image_select] + present_fields <- selected_fields[selected_fields %in% present_fields] query_collapse <- x } } # end `select` checks diff --git a/R/capture_occurrences.R b/R/capture_occurrences.R index 7b9c66c3..e8edd293 100644 --- a/R/capture_occurrences.R +++ b/R/capture_occurrences.R @@ -19,7 +19,7 @@ capture_occurrences <- function(.query, #' @noRd #' @keywords Internal capture_occurrences_gbif <- function(.query, - format = "SIMPLE_CSV", + format = "SIMPLE_CSV", # or "SPECIES_LIST" ...){ # get user string .query$type <- "data/occurrences" @@ -42,7 +42,7 @@ capture_occurrences_gbif <- function(.query, body = list(filter = .query$filter, identify = .query$identify, geolocate = .query$geolocate, - format = "SIMPLE_CSV")) |> + format = format)) |> as_prequery() } diff --git a/R/check.R b/R/check.R index b99551d0..eb7eeb07 100644 --- a/R/check.R +++ b/R/check.R @@ -642,7 +642,9 @@ check_select <- function(.query, as.data.frame() # handle supplied `select()` - field_values <- parse_select_occurrences(.query, df, error_call = error_call) + field_values <- .query |> + parse_select_occurrences(build_select_df_from_query(.query), + error_call = error_call) # warn if(is.null(field_values)){ @@ -678,6 +680,20 @@ check_select <- function(.query, .query } +#' internal function to build a select-able df from a .query +#' @noRd +#' @keywords Internal +build_select_df_from_query <- function(.query){ + valid_fields <- .query[["metadata/fields"]]$id + valid_assertions <- .query[["metadata/assertions"]]$id + valid_any <- c(valid_fields, valid_assertions) + matrix(data = NA, + nrow = 0, + ncol = length(valid_any), + dimnames = list(NULL, valid_any)) |> + tibble::as_tibble() +} + #' Internal function to parse out `select()` queries for occurrences #' Called both by `check_select()` (above) and by `collect_occurrences_describe()` #' @noRd diff --git a/R/collapse_checks.R b/R/collapse_checks.R index 62e2e75b..abfd2062 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -56,6 +56,11 @@ collapse_run_checks <- function(.query, if(.query$type %in% c("data/occurrences", "data/occurrences-glimpse")){ .query <- check_select(.query, error_call) } + # browser() + if(.query$type %in% c("data/species") & .query$atlas == "Global"){ # NOTE: require this for occurrences too? Do LA species queries require it? + parse_select_occurrences(.query, build_select_df_from_query(.query)) # OK this works, just need to update select() now + # probably best to move this to check_select_GBIF or similar + } # NOTE: the naming convention here is misleading; should probably be `parse_select()` # after checking, for type = "glimpse", we need to rename the fields query diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index 961fe2e2..a1b39cca 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -201,7 +201,9 @@ collect_occurrences_describe <- function(.query){ dplyr::distinct(.data$id, .keep_all = TRUE) # add caching here - result_df <- update_attributes(result, type = "fields") + result_df <- update_attributes(result, + type = "fields", + atlas = .query$atlas) update_cache(fields = result_df) } diff --git a/R/compute_occurrences.R b/R/compute_occurrences.R index 71029f76..f683b528 100644 --- a/R/compute_occurrences.R +++ b/R/compute_occurrences.R @@ -25,11 +25,11 @@ compute_occurrences_la_direct <- function(.query){ compute_occurrences_gbif <- function(.query){ post_result <- query_API(.query) # returns an id status_code <- list( - type = "data/occurrences", + type = .query$type, url = glue::glue("https://api.gbif.org/v1/occurrence/download/{post_result}")) |> query_API() |> check_occurrence_response() - c(list(type = "data/occurrences", + c(list(type = .query$type, atlas = .query$atlas), status_code[names(status_code) != "request"], list(request = .query$request)) |> diff --git a/R/galah_call.R b/R/galah_call.R index d04ee2d2..2f923bbb 100644 --- a/R/galah_call.R +++ b/R/galah_call.R @@ -80,7 +80,7 @@ galah_call <- function(type = c("occurrences", "occurrences-doi", # "distributions", "species", - "species-count", + "species-count" # "events", # "events-count" ), diff --git a/R/query_API.R b/R/query_API.R index 1dcf2610..943674c9 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -86,6 +86,7 @@ query_API_internal <- function(.query, auth_url = dplyr::pull(auth_info$config, "authorize_url"), scope = dplyr::pull(auth_info$config, "scopes"), pkce = TRUE, + redirect_uri = "http://localhost:27231/", cache_disk = .query$request$authenticate$cache_disk) } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 8fdc0d86..288c03b0 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -17,15 +17,20 @@ parse_select <- function(df, } # get quosures captured by `select()` quo_list <- purrr::pluck(.query, "request", "select", "quosure") - # map() over list of quosures - # honestly I don't know why `!!quo_list` fails here, but it does, so used this instead - pos <- purrr::map(quo_list, \(a){ - tidyselect::eval_select(expr = a, data = df) - }) |> - unlist() - # apply tidy selection to `df` - # note: this code taken from `tidyselect` documentation; it could be argued that `df[pos]` is sufficient - rlang::set_names(df[pos], names(pos)) + + if(length(quo_list) > 0){ + # map() over list of quosures + # honestly I don't know why `!!quo_list` fails here, but it does, so used this instead + pos <- purrr::map(quo_list, \(a){ + tidyselect::eval_select(expr = a, data = df) + }) |> + unlist() + # apply tidy selection to `df` + # note: this code taken from `tidyselect` documentation; it could be argued that `df[pos]` is sufficient + rlang::set_names(df[pos], names(pos)) + }else{ + df + } } #' equivalent to `parse_select()` but for filter @@ -488,14 +493,16 @@ authentication_supported <- function(atlas){ #' @noRd #' @keywords Internal authentication_required <- function(x){ - # only required for downloads (in current version) + # only required for downloads (in current version) download_check <- x$type %in% c("occurrences", "species") & - # only required when not generating counts - # is.null(x$group_by) & # <- not needed, as group_by() can preface count() or distinct() - is.null(x$count) & - is.null(x$glimpse) + # downloads never happen for `glimpse()` or `describe()` + is.null(x$glimpse) & + is.null(x$describe) & + # downloads only happen for GBIF counts, otherwise count invalidates need for auth + (is.null(x$count) | (!is.null(x$count) & x$atlas == "Global")) + # is.null(x$group_by) & # <- not needed, as group_by() can preface count() or distinct() - # does distinct require authentication? Only if present and keep_all = TRUE + # `distinct()` only requires authentication if 1. it is present and 2. keep_all = TRUE if(is.null(x$distinct)){ download_check }else{ diff --git a/man/authenticate.Rd b/man/authenticate.Rd index 12543911..a170a05d 100644 --- a/man/authenticate.Rd +++ b/man/authenticate.Rd @@ -9,27 +9,31 @@ authenticate( email = NULL, username = NULL, password = NULL, + download_reason_id = NULL, use_jwt = FALSE, cache_jwt = FALSE, ... ) } \arguments{ -\item{.data}{An object of class \code{data_request} or \code{metadata_request}} +\item{.data}{An object of class \code{data_request} or \code{metadata_request}.} -\item{email}{(string) Email address registered with the selected organisation} +\item{email}{(string) Email address registered with the selected organisation.} -\item{username}{(string) Registered username (GBIF only)} +\item{username}{(string) Registered username (GBIF only).} -\item{password}{(string) Registered password (GBIF only)} +\item{password}{(string) Registered password (GBIF only).} + +\item{download_reason_id}{(integer) ID for the download reason. +See \code{show_all_reasons()} for accepted values.} \item{use_jwt}{(logical) Should an OAuth workflow be used for authentication? Only supported for Flemish and Australian atlases. Defaults to \code{FALSE}} \item{cache_jwt}{(logical) Should JWT tokens be cached to disk? Defaults -to \code{FALSE}} +to \code{FALSE}.} -\item{...}{Other arguments, currently ignored} +\item{...}{Other arguments, currently ignored.} } \value{ An object of the same class as supplied, but with an added diff --git a/man/galah_call.Rd b/man/galah_call.Rd index d8fd203d..bb99ab28 100644 --- a/man/galah_call.Rd +++ b/man/galah_call.Rd @@ -9,13 +9,13 @@ \usage{ galah_call( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", "events", "events-count"), + "species-count", ), from = NULL ) request_data( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", "events", "events-count"), + "species-count", ), from = NULL ) diff --git a/tests/testthat/test-galah_call.R b/tests/testthat/test-galah_call.R index 4048c09e..2dfe2fa2 100644 --- a/tests/testthat/test-galah_call.R +++ b/tests/testthat/test-galah_call.R @@ -13,23 +13,26 @@ test_that("`request_` functions build correct object classes", { }) test_that("request_data(from = X) sets `atlas` slot in later objects", { - query_initial <- request_data(from = "GBIF") |> + chosen_atlas <- "Spain" + query_initial <- request_data(from = chosen_atlas) |> filter(year == 2010) |> count() - expect_equal(query_initial$atlas, "Global") + expect_equal(query_initial$atlas, chosen_atlas) query_capture <- capture(query_initial) - expect_equal(query_capture$atlas, "Global") + expect_equal(query_capture$atlas, chosen_atlas) query_compound <- compound(query_capture) compound_atlases <- purrr::map(query_compound, \(a){purrr::pluck(a, "atlas")}) |> unlist() - expect_true(all(compound_atlases == "Global")) + expect_true(all(compound_atlases == chosen_atlas)) query_collapse <- collapse(query_compound) - expect_equal(query_collapse$atlas, "Global") + expect_equal(query_collapse$atlas, chosen_atlas) query_compute <- compute(query_collapse) - expect_equal(query_compute$atlas, "Global") - collect(query_compute) + expect_equal(query_compute$atlas, chosen_atlas) + result <- collect(query_compute) # add test to ensure that galah_config() hasn't been updated to GBIF + expect_gte(result$count, 10) + expect_false(galah_config()$atlas$region == chosen_atlas) }) test_that("`galah_call()` works with all `dplyr` functions", { diff --git a/tests/testthat/test-international-Austria.R b/tests/testthat/test-international-Austria.R index b6355b2d..774a2fae 100644 --- a/tests/testthat/test-international-Austria.R +++ b/tests/testthat/test-international-Austria.R @@ -5,7 +5,7 @@ test_that("swapping to atlas = Austria works", { expect_message(galah_config(atlas = "Austria")) }) -test_that("show_all(assertions) works for Austria", { +test_that("`show_all(assertions)` works for Austria", { skip_if_offline(); skip_on_ci() x <- show_all(assertions) |> try(silent = TRUE) @@ -16,7 +16,7 @@ test_that("show_all(assertions) works for Austria", { expect_equal(x, y) }) -test_that("show_all(collections) works for Austria", { +test_that("`show_all(collections)` works for Austria", { skip_if_offline(); skip_on_ci() x <- show_all(collections) |> try(silent = TRUE) @@ -27,7 +27,7 @@ test_that("show_all(collections) works for Austria", { expect_equal(x, y) }) -test_that("show_all(datasets) works for Austria", { +test_that("`show_all(datasets)` works for Austria", { skip_if_offline(); skip_on_ci() x <- show_all(datasets) |> try(silent = TRUE) @@ -38,7 +38,7 @@ test_that("show_all(datasets) works for Austria", { expect_equal(x, y) }) -test_that("show_all(fields) works for Austria", { +test_that("`show_all(fields)` works for Austria", { skip_if_offline(); skip_on_ci() x <- show_all(fields) |> try(silent = TRUE) @@ -49,7 +49,7 @@ test_that("show_all(fields) works for Austria", { expect_equal(x, y) }) -test_that("show_all(licences) works for Austria", { +test_that("`show_all(licences)` works for Austria", { skip_if_offline(); skip_on_ci() x <- show_all(licences) |> try(silent = TRUE) @@ -98,7 +98,7 @@ test_that("show_all(profiles) fails for Austria", { expect_error(show_all(profiles)) }) -test_that("search_all(fields) works for Austria", { +test_that("`search_all(fields)` works for Austria", { skip_if_offline(); skip_on_ci() x <- search_all(fields, "year") |> try(silent = TRUE) @@ -107,7 +107,7 @@ test_that("search_all(fields) works for Austria", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("search_all(taxa) works for Austria", { +test_that("`search_all(taxa)` works for Austria", { skip_if_offline(); skip_on_ci() x <- search_all(taxa, "Vulpes vulpes") |> try(silent = TRUE) @@ -116,7 +116,7 @@ test_that("search_all(taxa) works for Austria", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("show_values works for Austria", { +test_that("`show_values()` works for Austria", { skip_if_offline(); skip_on_ci() quiet_values <- function(...){ x <- purrr::quietly(show_values) @@ -139,7 +139,7 @@ test_that("show_values works for Austria", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("atlas_counts works with type = 'occurrences' for Austria", { +test_that("`atlas_counts()` works with type = 'occurrences' for Austria", { skip_if_offline(); skip_on_ci() x <- atlas_counts() |> dplyr::pull(count) |> @@ -148,7 +148,7 @@ test_that("atlas_counts works with type = 'occurrences' for Austria", { expect_gt(x, 0) }) -test_that("atlas_counts works with type = 'species' for Austria", { +test_that("`atlas_counts()` works with type = 'species' for Austria", { skip_if_offline(); skip_on_ci() x <- atlas_counts(type = "species") |> dplyr::pull(count) |> @@ -158,11 +158,12 @@ test_that("atlas_counts works with type = 'species' for Austria", { }) ## FIXME: Only works when run_checks = TRUE -test_that("atlas_counts works with galah_identify for Austria", { +test_that("`count()` works with `identify()` for Austria", { skip_if_offline(); skip_on_ci() - result <- galah_call() |> - galah_identify("Mammalia") |> # run_checks = TRUE works - atlas_counts() |> + result <- galah_call(from = "Austria") |> + identify("Mammalia") |> # run_checks = TRUE works + count() |> + collect() |> try(silent = TRUE) skip_if(inherits(result, "try-error"), message = "API not available") expect_gt(result$count, 1) @@ -174,12 +175,12 @@ test_that("atlas_counts works with galah_identify for Austria", { 0.1) # i.e. <1% margin of error }) -test_that("atlas_counts works with group_by for Austria", { +test_that("`count()` works with group_by for Austria", { skip_if_offline(); skip_on_ci() - result <- galah_call() |> - galah_filter(year >= 2020) |> - galah_group_by(year) |> - atlas_counts() |> + result <- galah_call(from = "Austria") |> + filter(year >= 2020) |> + count(year) |> + collect() |> try(silent = TRUE) skip_if(inherits(result, "try-error"), message = "API not available") expect_gt(nrow(result), 1) diff --git a/tests/testthat/test-international-Flanders.R b/tests/testthat/test-international-Flanders.R index e2d685cc..501e81b3 100644 --- a/tests/testthat/test-international-Flanders.R +++ b/tests/testthat/test-international-Flanders.R @@ -249,12 +249,10 @@ test_that("`atlas_species()` works for Flanders", { }) test_that("`authenticate()` works for Flanders", { - skip_if_offline(); skip_on_ci() - galah_config( - download_reason_id = 10, - send_email = FALSE) + skip("authentication requires interactivity") x <- request_data(from = "Flanders") |> - authenticate() |> + authenticate(use_jwt = TRUE, + download_reason_id = 10) |> identify("Canis") |> filter(year == 2020) |> compound() diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index a8804e14..5bd58d17 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -82,7 +82,7 @@ test_that("show_all(datasets) works for GBIF", { z <- request_metadata() |> filter(dataset == "avian") |> collect() - expect_equal(nrow(z), 20) + expect_gte(nrow(z), 10) # historically this was n = 20 }) test_that("show_all(providers) works for GBIF", { @@ -347,7 +347,8 @@ test_that("`atlas_species()` works for GBIF", { x <- request_data(type = "species") |> filter(year == 2010) |> identify("Litoria") |> - collapse() + compound() + # collapse() expect_s3_class(x, "query") expect_equal(length(x), 7) expect_equal(names(x), @@ -359,17 +360,25 @@ test_that("`atlas_species()` works for GBIF", { expect_gt(nrow(z), 0) expect_gt(ncol(z), 0) expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) - # species <- galah_call() |> - # galah_filter(year == 2010) |> - # galah_identify("Litoria") |> - # atlas_species() - # expect_equal(z, species) +}) + +test_that("`distinct()` queries accept `select()` for GBIF", { + skip_if_offline(); skip_on_ci() + x <- request_data() |> + filter(year == 2010) |> + distinct(species_guid, .keep_all = TRUE) |> + identify("Crinia") |> + select(scientificName, speciesKey) |> + collect() + expect_gt(nrow(z), 10) # + expect_equal(ncol(z), 2) + expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) }) test_that("`atlas_media()` fails for GBIF", { skip_if_offline(); skip_on_ci() expect_error({galah_call() |> - galah_identify("perameles") |> + identify("perameles") |> atlas_media() }) }) diff --git a/tests/testthat/test-international-Sweden.R b/tests/testthat/test-international-Sweden.R index 90ae1256..9ab0a498 100644 --- a/tests/testthat/test-international-Sweden.R +++ b/tests/testthat/test-international-Sweden.R @@ -249,7 +249,7 @@ test_that("atlas_species works for Sweden", { email = "martinjwestgate@gmail.com", send_email = FALSE) spp <- galah_call() |> - galah_identify("Carnivora") |> + identify("Carnivora") |> atlas_species() |> try(silent = TRUE) skip_if(inherits(spp, "try-error"), message = "API not available") @@ -265,14 +265,14 @@ test_that("atlas_occurrences works for Sweden", { email = "martinjwestgate@gmail.com", send_email = FALSE) occ_collapse <- galah_call() |> - galah_identify("Mammalia") |> - galah_filter(year < 1850) |> - galah_select(group = "basic") |> # use defaults + identify("Mammalia") |> + filter(year < 1850) |> + select(group = "basic") |> # use defaults collapse() skip_if(inherits(occ_collapse, "try-error"), message = "API not available") expect_s3_class(occ_collapse, "query") expect_equal(names(occ_collapse), - c("type", "url", "headers", "request")) + c("type", "atlas", "url", "headers", "request")) expect_equal(occ_collapse$type, "data/occurrences") # compute occ_compute <- compute(occ_collapse) diff --git a/tests/testthat/test-international-UK.R b/tests/testthat/test-international-UK.R index b1a4a25d..fa8359f7 100644 --- a/tests/testthat/test-international-UK.R +++ b/tests/testthat/test-international-UK.R @@ -222,7 +222,7 @@ test_that("atlas_occurrences works for United Kingdom", { # with run checks, this gives n = 5. Without it's n = 2 expect_s3_class(occ_collapse, "query") expect_equal(names(occ_collapse), - c("type", "url", "headers", "request")) + c("type", "atlas", "url", "headers", "request")) expect_equal(occ_collapse$type, "data/occurrences") # compute # notes: From 994f5d629d904180a872a6758d413e394aec232b Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 29 Apr 2026 17:35:45 +1000 Subject: [PATCH 21/70] supersede last `galah_` piped queries Major user-facing change here is replacing spatial `galah_` functions with `geolocate_`, i.e. `galah_polygon()` is now `geolocate_polygon()`. Original functions are *not* deprecated, just superseded. --- NAMESPACE | 3 + R/apply_profile.R | 19 --- R/galah_config.R | 2 +- R/galah_filter.R | 37 ----- R/galah_group_by.R | 18 --- R/galah_select.R | 23 --- R/geolocate.R | 45 +++--- R/{galah_bbox.R => geolocate_bbox.R} | 2 +- R/{galah_polygon.R => geolocate_polygon.R} | 2 +- R/{galah_radius.R => geolocate_radius.R} | 2 +- R/graphics-identify.R | 18 --- R/superseded-functions.R | 165 ++++++++++++++++++++- man/galah_call.Rd | 4 +- man/geolocate.Rd | 50 +++---- man/superseded_functions.Rd | 35 ++++- 15 files changed, 244 insertions(+), 181 deletions(-) delete mode 100644 R/galah_filter.R delete mode 100644 R/galah_group_by.R delete mode 100644 R/galah_select.R rename R/{galah_bbox.R => geolocate_bbox.R} (99%) rename R/{galah_polygon.R => geolocate_polygon.R} (99%) rename R/{galah_radius.R => geolocate_radius.R} (99%) diff --git a/NAMESPACE b/NAMESPACE index be3d727c..3e14d4cb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -96,6 +96,9 @@ export(galah_polygon) export(galah_radius) export(galah_select) export(geolocate) +export(geolocate_bbox) +export(geolocate_polygon) +export(geolocate_radius) export(glimpse) export(group_by) export(identify) diff --git a/R/apply_profile.R b/R/apply_profile.R index 5542f406..48a82896 100644 --- a/R/apply_profile.R +++ b/R/apply_profile.R @@ -39,25 +39,6 @@ apply_profile <- function(.data, ...){ apply_profile = result) } -#' @rdname superseded_functions -#' @order 1 -#' @export -galah_apply_profile <- function(...){ - dots <- rlang::enquos(..., .ignore_empty = "all") |> - detect_request_object() - switch(class(dots[[1]])[1], - "data_request" = { - result <- parse_quosures_basic(dots[-1]) |> - parse_profile() - update_request_object(dots[[1]], - apply_profile = result) - }, - { - parse_quosures_basic(dots) |> - parse_profile() - }) -} - #' Internal parsing of `profile` args #' @noRd #' @keywords Internal diff --git a/R/galah_config.R b/R/galah_config.R index 8b9b32b9..c3ecd05a 100644 --- a/R/galah_config.R +++ b/R/galah_config.R @@ -278,7 +278,7 @@ enforce_download_reason <- function(value, "Invalid download reason name.", i = "Use `show_all(reasons)` to see all valid reasons.", x = "\"{value}\" does not match an existing reason name.") |> - cli::cli_abort(call = error_call) + cli::cli_abort(call = call) } if (is.character(value) & (value %in% reasons_df$name)) { value_id <- reasons_df |> diff --git a/R/galah_filter.R b/R/galah_filter.R deleted file mode 100644 index b73d8c7c..00000000 --- a/R/galah_filter.R +++ /dev/null @@ -1,37 +0,0 @@ -#' @rdname superseded_functions -#' @order 2 -#' @export -galah_filter <- function(...){ - dots <- rlang::enquos(..., .ignore_empty = "all") |> - detect_request_object() - check_named_input(dots) - switch(class(dots[[1]])[1], - "data_request" = { - if(dots[[1]]$atlas == "Global"){ - filters <- parse_quosures_data_gbif(dots[-1]) # `handle_quosures_GBIF.R` - }else{ - filters <- parse_quosures_data(dots[-1]) # `handle_quosures.R` - } - update_request_object(dots[[1]], - filter = filters) - }, - "metadata_request" = { - parse_quosures_metadata(dots[[1]], dots[-1]) - }, - "files_request" = { - input <- dots[[1]] - parsed_dots <- parse_quosures_files(dots[-1]) - input$filter <- parsed_dots$data - input$type <- parsed_dots$variable - input - }, - # NOTE: below here is triggered if user calls `galah_filter()` without - # `galah_call()`/`request_data()` first. In this case we only have - # global settings to go on. - if(potions::pour("atlas", "region", .pkg = "galah") == "Global"){ - parse_quosures_data_gbif(dots) - }else{ - parse_quosures_data(dots) - } - ) -} \ No newline at end of file diff --git a/R/galah_group_by.R b/R/galah_group_by.R deleted file mode 100644 index 104287e1..00000000 --- a/R/galah_group_by.R +++ /dev/null @@ -1,18 +0,0 @@ -#' @rdname superseded_functions -#' @order 4 -#' @export -galah_group_by <- function(...){ - dots <- rlang::enquos(..., .ignore_empty = "all") |> - detect_request_object() - switch(class(dots[[1]])[1], - "data_request" = { - df <- parse_quosures_basic(dots[-1]) |> - parse_group_by() - update_request_object(dots[[1]], - group_by = df) - }, - { - parse_quosures_basic(dots) |> - parse_group_by() - }) -} \ No newline at end of file diff --git a/R/galah_select.R b/R/galah_select.R deleted file mode 100644 index 1d9de3da..00000000 --- a/R/galah_select.R +++ /dev/null @@ -1,23 +0,0 @@ -#' @rdname superseded_functions -#' @order 6 -#' @export -galah_select <- function(..., group = NULL){ - dots <- rlang::enquos(..., .ignore_empty = "all") |> - detect_request_object() |> - as.list() - if(length(dots) < 1){ - list(quosure = c(), summary = "") |> - add_group(group) - } - else if(inherits(dots[[1]], "data_request")){ - list(quosure = dots[-1], - summary = generate_summary(dots[-1])) |> - add_group(group) |> - update_request_object(dots[[1]], - select = _) - }else{ - list(quosure = dots, - summary = generate_summary(dots)) |> - add_group(group) - } -} \ No newline at end of file diff --git a/R/geolocate.R b/R/geolocate.R index 56880882..17fa0034 100644 --- a/R/geolocate.R +++ b/R/geolocate.R @@ -4,12 +4,12 @@ #' Restrict results to those from a specified area. #' Areas can be specified as either polygons or bounding boxes, depending on #' `type`. Alternatively, users can call the underlying functions directly via -#' `galah_polygon()`, `galah_bbox()` or `galah_radius()`. It is possible to use +#' `geolocate_polygon()`, `geolocate_bbox()` or `geolocate_radius()`. It is possible to use #' `sf` syntax by calling `st_crop()`, which is synonymous with -#' `galah_polygon()`. +#' `geolocate_polygon()`. #' #' **Use a polygon** -#' If calling `galah_geolocate()`, the default `type` is `"polygon"`, which +#' If calling `geolocate()`, the default `type` is `"polygon"`, which #' narrows queries to within an area supplied as a `POLYGON` or `MULTIPOLYGON`. #' Polygons must be #' specified as either an `sf` object, a 'well-known text' (WKT) string, or a @@ -27,10 +27,10 @@ #' latitude/longitude coordinate numbers or as an `sf` object (`sfc_POINT`). #' Area is supplied as a `radius` in kilometres. Default radius is 10 km. #' -#' @param type `string`: one of `c("polygon", "bbox")`. Defaults to +#' @param type `string`: one of `"polygon"`, `"bbox"` or `"radius"`. Defaults to #' `"polygon"`. If `type = "polygon"`, a multipolygon will be built via -#' [galah_polygon()]. If `type = "bbox"`, a multipolygon will be built via -#' [galah_bbox()]. The multipolygon is used to narrow a query to the ALA. +#' [geolocate_polygon()]. If `type = "bbox"`, a multipolygon will be built via +#' [geolocate_bbox()]. The multipolygon is used to narrow a query to the ALA. #' @param ... For `st_crop`, additional arguments (currently ignored). Otherwise #' a single `sf` object, WKT string or shapefile. Bounding boxes can #' be supplied as a `tibble`/`data.frame` or a `bbox` @@ -72,7 +72,7 @@ #' sf::st_as_sfc() #' galah_call() |> #' identify("reptilia") |> -#' galah_polygon(location) |> +#' geolocate_polygon(location) |> #' count() |> #' collect() #' @@ -89,7 +89,7 @@ #' sf::st_as_sfc() #' galah_call() |> #' identify("vulpes") |> -#' galah_geolocate(location, type = "bbox") |> +#' geolocate(location, type = "bbox") |> #' count() |> #' collect() #' @@ -98,7 +98,7 @@ #' crs = sf::st_crs("WGS84")) #' galah_call() |> #' identify("reptilia") |> -#' galah_geolocate(b_box, type = "bbox") |> +#' geolocate(b_box, type = "bbox") |> #' count() |> #' collect() #' @@ -106,17 +106,17 @@ #' b_box <- tibble::tibble(xmin = 148, ymin = -29, xmax = 143, ymax = -21) #' galah_call() |> #' identify("vulpes") |> -#' galah_geolocate(b_box, type = "bbox") |> +#' geolocate(b_box, type = "bbox") |> #' count() |> #' collect() #' #' # Search for records within a radius around a point's coordinates #' galah_call() |> #' identify("manorina melanocephala") |> -#' galah_geolocate(lat = -33.7, -#' lon = 151.3, -#' radius = 5, -#' type = "radius") |> +#' geolocate(lat = -33.7, +#' lon = 151.3, +#' radius = 5, +#' type = "radius") |> #' count() |> #' collect() #' @@ -124,9 +124,9 @@ #' point <- sf::st_sfc(sf::st_point(c(-33.66741, 151.3174)), crs = 4326) #' galah_call() |> #' identify("manorina melanocephala") |> -#' galah_geolocate(point, -#' radius = 5, -#' type = "radius") |> +#' geolocate(point, +#' radius = 5, +#' type = "radius") |> #' count() |> #' collect() #' } @@ -137,17 +137,12 @@ geolocate <- function(..., type = c("polygon", "bbox", "radius")) { type <- match.arg(type) switch(type, - polygon = galah_polygon(...), - bbox = galah_bbox(...), - radius = galah_radius(...) + polygon = geolocate_polygon(...), + bbox = geolocate_bbox(...), + radius = geolocate_radius(...) ) } -#' @rdname superseded_functions -#' @order 3 -#' @export -galah_geolocate <- geolocate - #' @rdname geolocate #' @order 6 #' @param x An object of class `data_request`, created using [galah_call()] diff --git a/R/galah_bbox.R b/R/geolocate_bbox.R similarity index 99% rename from R/galah_bbox.R rename to R/geolocate_bbox.R index af6f954a..8ad188d1 100644 --- a/R/galah_bbox.R +++ b/R/geolocate_bbox.R @@ -1,7 +1,7 @@ #' @rdname geolocate #' @order 4 #' @export -galah_bbox <- function(...) { +geolocate_bbox <- function(...) { # check to see if any of the inputs are a data request query <- list(...) diff --git a/R/galah_polygon.R b/R/geolocate_polygon.R similarity index 99% rename from R/galah_polygon.R rename to R/geolocate_polygon.R index fb2d8f9d..9a0a7ca8 100644 --- a/R/galah_polygon.R +++ b/R/geolocate_polygon.R @@ -1,7 +1,7 @@ #' @rdname geolocate #' @order 3 #' @export -galah_polygon <- function(...){ +geolocate_polygon <- function(...){ # check to see if any of the inputs are a data request query <- list(...) if(length(query) > 1 & inherits(query[[1]], "data_request")){ diff --git a/R/galah_radius.R b/R/geolocate_radius.R similarity index 99% rename from R/galah_radius.R rename to R/geolocate_radius.R index 50f315c8..2dd410b7 100644 --- a/R/galah_radius.R +++ b/R/geolocate_radius.R @@ -1,7 +1,7 @@ #' @rdname geolocate #' @order 5 #' @export -galah_radius <- function(...){ +geolocate_radius <- function(...){ # check to see if any of the inputs are a data request query <- list(...) diff --git a/R/graphics-identify.R b/R/graphics-identify.R index b6cea238..d78af584 100644 --- a/R/graphics-identify.R +++ b/R/graphics-identify.R @@ -63,22 +63,4 @@ identify.metadata_request <- function(x, ...){ x$type <- "taxa" } x -} - -#' @rdname superseded_functions -#' @order 5 -#' @export -galah_identify <- function(...) { - dots_initial <- list(...) - if (length(dots_initial) < 1) { - cli::cli_warn("No query passed to `identify()`.") - tibble::tibble("search_term" = character()) - }else{ - if(inherits(dots_initial[[1]], "data_request")){ - do.call(identify.data_request, dots_initial) - }else{ - search_terms <- identify(galah_call(), ...)$identify - return(search_terms) - } - } } \ No newline at end of file diff --git a/R/superseded-functions.R b/R/superseded-functions.R index 83e53cf6..2b6da022 100644 --- a/R/superseded-functions.R +++ b/R/superseded-functions.R @@ -6,17 +6,174 @@ #' piped functions for this same functionality. #' @name superseded_functions #' @rdname superseded_functions +#' @param ... Arguments passed to the function in question, usually (but +#' not always) using non-standard evaluation. +#' @param type `string`: one of `"polygon"`, `"bbox"` or `"radius"`. Defaults to +#' `"polygon"`. If `type = "polygon"`, a multipolygon will be built via +#' [geolocate_polygon()]. If `type = "bbox"`, a multipolygon will be built via +#' [geolocate_bbox()]. The multipolygon is used to narrow a query to the ALA. +#' @param group `string`: (optional) name of one or more column groups to +#' include. Valid options are `"basic"`, `"event"` `"taxonomy"`, `"media"` and +#' `"assertions"`. +#' @aliases galah_apply_profile #' @aliases galah_filter +#' @aliases galah_geolocate +#' @aliases galah_polygon +#' @aliases galah_bbox +#' @aliases galah_radius +#' @aliases galah_group_by #' @aliases galah_identify +#' @aliases galah_select #' @details #' Replacements are as follows: #' #' \itemize{ #' \item \code{\link[=apply_profile]{apply_profile()}} instead of `galah_apply_profile()` #' \item \code{\link[=filter.data_request]{filter()}} instead of `galah_filter()` -#' \item \code{\link[=geolocate.data_request]{geolocate()}} instead of `galah_geolocate()` -#' \item \code{\link[=group_by.data_request]{select()}} instead of `galah_group_by()` -#' \item \code{\link[=identify.data_request]{select()}} instead of `galah_identify()` +#' \item [geolocate()] instead of `galah_geolocate()` +#' \item [geolocate_polygon()] instead of `galah_polygon()` +#' \item [geolocate_bbox()] instead of `galah_bbox()` +#' \item [geolocate_radius()] instead of `galah_radius()` +#' \item \code{\link[=group_by.data_request]{group_by()}} instead of `galah_group_by()` +#' \item \code{\link[=identify.data_request]{identify()}} instead of `galah_identify()` #' \item \code{\link[=select.data_request]{select()}} instead of `galah_select()` #' } -NULL \ No newline at end of file +NULL + +#' @rdname superseded_functions +#' @order 1 +#' @export +galah_apply_profile <- function(...){ + dots <- rlang::enquos(..., .ignore_empty = "all") |> + detect_request_object() + switch(class(dots[[1]])[1], + "data_request" = { + result <- parse_quosures_basic(dots[-1]) |> + parse_profile() + update_request_object(dots[[1]], + apply_profile = result) + }, + { + parse_quosures_basic(dots) |> + parse_profile() + }) +} + +#' @rdname superseded_functions +#' @order 2 +#' @export +galah_filter <- function(...){ + dots <- rlang::enquos(..., .ignore_empty = "all") |> + detect_request_object() + check_named_input(dots) + switch(class(dots[[1]])[1], + "data_request" = { + if(dots[[1]]$atlas == "Global"){ + filters <- parse_quosures_data_gbif(dots[-1]) # `handle_quosures_GBIF.R` + }else{ + filters <- parse_quosures_data(dots[-1]) # `handle_quosures.R` + } + update_request_object(dots[[1]], + filter = filters) + }, + "metadata_request" = { + parse_quosures_metadata(dots[[1]], dots[-1]) + }, + "files_request" = { + input <- dots[[1]] + parsed_dots <- parse_quosures_files(dots[-1]) + input$filter <- parsed_dots$data + input$type <- parsed_dots$variable + input + }, + # NOTE: below here is triggered if user calls `galah_filter()` without + # `galah_call()`/`request_data()` first. In this case we only have + # global settings to go on. + if(potions::pour("atlas", "region", .pkg = "galah") == "Global"){ + parse_quosures_data_gbif(dots) + }else{ + parse_quosures_data(dots) + } + ) +} + +#' @rdname superseded_functions +#' @order 3 +#' @export +galah_geolocate <- geolocate + +#' @rdname superseded_functions +#' @order 4 +#' @export +galah_polygon <- geolocate_polygon + +#' @rdname superseded_functions +#' @order 5 +#' @export +galah_bbox <- geolocate_bbox + +#' @rdname superseded_functions +#' @order 6 +#' @export +galah_radius <- geolocate_radius + +#' @rdname superseded_functions +#' @order 7 +#' @export +galah_group_by <- function(...){ + dots <- rlang::enquos(..., .ignore_empty = "all") |> + detect_request_object() + switch(class(dots[[1]])[1], + "data_request" = { + df <- parse_quosures_basic(dots[-1]) |> + parse_group_by() + update_request_object(dots[[1]], + group_by = df) + }, + { + parse_quosures_basic(dots) |> + parse_group_by() + }) +} + +#' @rdname superseded_functions +#' @order 8 +#' @export +galah_identify <- function(...) { + dots_initial <- list(...) + if (length(dots_initial) < 1) { + cli::cli_warn("No query passed to `identify()`.") + tibble::tibble("search_term" = character()) + }else{ + if(inherits(dots_initial[[1]], "data_request")){ + do.call(identify.data_request, dots_initial) + }else{ + search_terms <- identify(galah_call(), ...)$identify + return(search_terms) + } + } +} + +#' @rdname superseded_functions +#' @order 9 +#' @export +galah_select <- function(..., group = NULL){ + dots <- rlang::enquos(..., .ignore_empty = "all") |> + detect_request_object() |> + as.list() + if(length(dots) < 1){ + list(quosure = c(), summary = "") |> + add_group(group) + } + else if(inherits(dots[[1]], "data_request")){ + list(quosure = dots[-1], + summary = generate_summary(dots[-1])) |> + add_group(group) |> + update_request_object(dots[[1]], + select = _) + }else{ + list(quosure = dots, + summary = generate_summary(dots)) |> + add_group(group) + } +} \ No newline at end of file diff --git a/man/galah_call.Rd b/man/galah_call.Rd index bb99ab28..3bce59c1 100644 --- a/man/galah_call.Rd +++ b/man/galah_call.Rd @@ -9,13 +9,13 @@ \usage{ galah_call( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", ), + "species-count"), from = NULL ) request_data( type = c("occurrences", "occurrences-count", "occurrences-doi", "species", - "species-count", ), + "species-count"), from = NULL ) diff --git a/man/geolocate.Rd b/man/geolocate.Rd index 99394b7d..177ac463 100644 --- a/man/geolocate.Rd +++ b/man/geolocate.Rd @@ -1,21 +1,21 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/geolocate.R, R/galah_polygon.R, R/galah_bbox.R, -% R/galah_radius.R +% Please edit documentation in R/geolocate.R, R/geolocate_polygon.R, +% R/geolocate_bbox.R, R/geolocate_radius.R \name{geolocate} \alias{geolocate} -\alias{galah_polygon} -\alias{galah_bbox} -\alias{galah_radius} +\alias{geolocate_polygon} +\alias{geolocate_bbox} +\alias{geolocate_radius} \alias{st_crop.data_request} \title{Narrow a query to within a specified area} \usage{ geolocate(..., type = c("polygon", "bbox", "radius")) -galah_polygon(...) +geolocate_polygon(...) -galah_bbox(...) +geolocate_bbox(...) -galah_radius(...) +geolocate_radius(...) \method{st_crop}{data_request}(x, y, ...) } @@ -24,10 +24,10 @@ galah_radius(...) a single \code{sf} object, WKT string or shapefile. Bounding boxes can be supplied as a \code{tibble}/\code{data.frame} or a \code{bbox}} -\item{type}{\code{string}: one of \code{c("polygon", "bbox")}. Defaults to +\item{type}{\code{string}: one of \code{"polygon"}, \code{"bbox"} or \code{"radius"}. Defaults to \code{"polygon"}. If \code{type = "polygon"}, a multipolygon will be built via -\code{\link[=galah_polygon]{galah_polygon()}}. If \code{type = "bbox"}, a multipolygon will be built via -\code{\link[=galah_bbox]{galah_bbox()}}. The multipolygon is used to narrow a query to the ALA.} +\code{\link[=geolocate_polygon]{geolocate_polygon()}}. If \code{type = "bbox"}, a multipolygon will be built via +\code{\link[=geolocate_bbox]{geolocate_bbox()}}. The multipolygon is used to narrow a query to the ALA.} \item{x}{An object of class \code{data_request}, created using \code{\link[=galah_call]{galah_call()}}} @@ -44,12 +44,12 @@ If \code{type = "radius"}, Restrict results to those from a specified area. Areas can be specified as either polygons or bounding boxes, depending on \code{type}. Alternatively, users can call the underlying functions directly via -\code{galah_polygon()}, \code{galah_bbox()} or \code{galah_radius()}. It is possible to use +\code{geolocate_polygon()}, \code{geolocate_bbox()} or \code{geolocate_radius()}. It is possible to use \code{sf} syntax by calling \code{st_crop()}, which is synonymous with -\code{galah_polygon()}. +\code{geolocate_polygon()}. \strong{Use a polygon} -If calling \code{galah_geolocate()}, the default \code{type} is \code{"polygon"}, which +If calling \code{geolocate()}, the default \code{type} is \code{"polygon"}, which narrows queries to within an area supplied as a \code{POLYGON} or \code{MULTIPOLYGON}. Polygons must be specified as either an \code{sf} object, a 'well-known text' (WKT) string, or a @@ -99,7 +99,7 @@ location <- "POLYGON((142.3 -29.0,142.7 -29.1,142.7 -29.4,142.3 -29.0))" |> sf::st_as_sfc() galah_call() |> identify("reptilia") |> - galah_polygon(location) |> + geolocate_polygon(location) |> count() |> collect() @@ -116,7 +116,7 @@ location <- "POLYGON((142.3 -29.0,142.7 -29.1,142.7 -29.4,142.3 -29.0))" |> sf::st_as_sfc() galah_call() |> identify("vulpes") |> - galah_geolocate(location, type = "bbox") |> + geolocate(location, type = "bbox") |> count() |> collect() @@ -125,7 +125,7 @@ b_box <- sf::st_bbox(c(xmin = 143, xmax = 148, ymin = -29, ymax = -28), crs = sf::st_crs("WGS84")) galah_call() |> identify("reptilia") |> - galah_geolocate(b_box, type = "bbox") |> + geolocate(b_box, type = "bbox") |> count() |> collect() @@ -133,17 +133,17 @@ galah_call() |> b_box <- tibble::tibble(xmin = 148, ymin = -29, xmax = 143, ymax = -21) galah_call() |> identify("vulpes") |> - galah_geolocate(b_box, type = "bbox") |> + geolocate(b_box, type = "bbox") |> count() |> collect() # Search for records within a radius around a point's coordinates galah_call() |> identify("manorina melanocephala") |> - galah_geolocate(lat = -33.7, - lon = 151.3, - radius = 5, - type = "radius") |> + geolocate(lat = -33.7, + lon = 151.3, + radius = 5, + type = "radius") |> count() |> collect() @@ -151,9 +151,9 @@ galah_call() |> point <- sf::st_sfc(sf::st_point(c(-33.66741, 151.3174)), crs = 4326) galah_call() |> identify("manorina melanocephala") |> - galah_geolocate(point, - radius = 5, - type = "radius") |> + geolocate(point, + radius = 5, + type = "radius") |> count() |> collect() } diff --git a/man/superseded_functions.Rd b/man/superseded_functions.Rd index c8358116..df74039c 100644 --- a/man/superseded_functions.Rd +++ b/man/superseded_functions.Rd @@ -1,11 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/apply_profile.R, R/galah_filter.R, -% R/geolocate.R, R/galah_group_by.R, R/graphics-identify.R, R/galah_select.R, -% R/superseded-functions.R +% Please edit documentation in R/superseded-functions.R \name{galah_apply_profile} \alias{galah_apply_profile} \alias{galah_filter} \alias{galah_geolocate} +\alias{galah_polygon} +\alias{galah_bbox} +\alias{galah_radius} \alias{galah_group_by} \alias{galah_identify} \alias{galah_select} @@ -18,12 +19,31 @@ galah_filter(...) galah_geolocate(..., type = c("polygon", "bbox", "radius")) +galah_polygon(...) + +galah_bbox(...) + +galah_radius(...) + galah_group_by(...) galah_identify(...) galah_select(..., group = NULL) } +\arguments{ +\item{...}{Arguments passed to the function in question, usually (but +not always) using non-standard evaluation.} + +\item{type}{\code{string}: one of \code{"polygon"}, \code{"bbox"} or \code{"radius"}. Defaults to +\code{"polygon"}. If \code{type = "polygon"}, a multipolygon will be built via +\code{\link[=geolocate_polygon]{geolocate_polygon()}}. If \code{type = "bbox"}, a multipolygon will be built via +\code{\link[=geolocate_bbox]{geolocate_bbox()}}. The multipolygon is used to narrow a query to the ALA.} + +\item{group}{\code{string}: (optional) name of one or more column groups to +include. Valid options are \code{"basic"}, \code{"event"} \code{"taxonomy"}, \code{"media"} and +\code{"assertions"}.} +} \description{ These functions are still valid, but have been superseded by more recent versions. They are designed to be used to supply arguments within @@ -36,9 +56,12 @@ Replacements are as follows: \itemize{ \item \code{\link[=apply_profile]{apply_profile()}} instead of \code{galah_apply_profile()} \item \code{\link[=filter.data_request]{filter()}} instead of \code{galah_filter()} -\item \code{\link[=geolocate.data_request]{geolocate()}} instead of \code{galah_geolocate()} -\item \code{\link[=group_by.data_request]{select()}} instead of \code{galah_group_by()} -\item \code{\link[=identify.data_request]{select()}} instead of \code{galah_identify()} +\item \code{\link[=geolocate]{geolocate()}} instead of \code{galah_geolocate()} +\item \code{\link[=geolocate_polygon]{geolocate_polygon()}} instead of \code{galah_polygon()} +\item \code{\link[=geolocate_bbox]{geolocate_bbox()}} instead of \code{galah_bbox()} +\item \code{\link[=geolocate_radius]{geolocate_radius()}} instead of \code{galah_radius()} +\item \code{\link[=group_by.data_request]{group_by()}} instead of \code{galah_group_by()} +\item \code{\link[=identify.data_request]{identify()}} instead of \code{galah_identify()} \item \code{\link[=select.data_request]{select()}} instead of \code{galah_select()} } } From 5d0716811ced264ff3443de8715f12c2115eb2aa Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 30 Apr 2026 13:27:45 +1000 Subject: [PATCH 22/70] update tests for `geolocate_` functions --- ...h_apply_profile.R => test-apply_profile.R} | 0 ...galah_group_by.R => test-dplyr-group_by.R} | 0 ...est-galah_geolocate.R => test-geolocate.R} | 18 ++-- ...est-galah_bbox.R => test-geolocate_bbox.R} | 48 +++++----- ...lah_polygon.R => test-geolocate_polygon.R} | 64 ++++++++----- ...galah_radius.R => test-geolocate_radius.R} | 93 ++++++++++--------- ...{test-galah_identify.R => test-identify.R} | 0 7 files changed, 119 insertions(+), 104 deletions(-) rename tests/testthat/{test-galah_apply_profile.R => test-apply_profile.R} (100%) rename tests/testthat/{test-galah_group_by.R => test-dplyr-group_by.R} (100%) rename tests/testthat/{test-galah_geolocate.R => test-geolocate.R} (72%) rename tests/testthat/{test-galah_bbox.R => test-geolocate_bbox.R} (74%) rename tests/testthat/{test-galah_polygon.R => test-geolocate_polygon.R} (55%) rename tests/testthat/{test-galah_radius.R => test-geolocate_radius.R} (52%) rename tests/testthat/{test-galah_identify.R => test-identify.R} (100%) diff --git a/tests/testthat/test-galah_apply_profile.R b/tests/testthat/test-apply_profile.R similarity index 100% rename from tests/testthat/test-galah_apply_profile.R rename to tests/testthat/test-apply_profile.R diff --git a/tests/testthat/test-galah_group_by.R b/tests/testthat/test-dplyr-group_by.R similarity index 100% rename from tests/testthat/test-galah_group_by.R rename to tests/testthat/test-dplyr-group_by.R diff --git a/tests/testthat/test-galah_geolocate.R b/tests/testthat/test-geolocate.R similarity index 72% rename from tests/testthat/test-galah_geolocate.R rename to tests/testthat/test-geolocate.R index 8149c172..347ec121 100644 --- a/tests/testthat/test-galah_geolocate.R +++ b/tests/testthat/test-geolocate.R @@ -1,32 +1,32 @@ -purrr_geolocate <- purrr::quietly(galah_geolocate) +purrr_geolocate <- purrr::quietly(geolocate) quiet_geolocate <- function(...){purrr_geolocate(...)$result} -test_that("galah_geolocate defaults to galah_polygon", { +test_that("`geolocate()` defaults to `geolocate_polygon`(", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" - expect_match(galah_geolocate(wkt), "MULTIPOLYGON") + expect_match(geolocate(wkt), "MULTIPOLYGON") }) -test_that("galah_geolocate works when type is set to polygon", { +test_that("`geolocate()` works when type is set to polygon", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" polygon <- quiet_geolocate(wkt, type = "polygon") expect_match(polygon, "MULTIPOLYGON") }) -test_that("galah_geolocate switches to use galah_bbox when `type = bbox`", { +test_that("`geolocate()` switches to use `geolocate_bbox()` when `type = bbox`", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" obj_sf <- wkt |> sf::st_as_sfc() - galah_geolocate(wkt, type = "bbox") |> + geolocate(wkt, type = "bbox") |> expect_error(label = "input must be an sf object, data.frame or tibble") polygon <- quiet_geolocate(obj_sf, type = "bbox") expect_match(polygon, "MULTIPOLYGON") }) -test_that("galah_geolocate switches to use galah_radius when `type = radius`", { +test_that("geolocate switches to use galah_radius when `type = radius`", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" obj_sf <- wkt |> sf::st_as_sfc() - galah_geolocate(wkt, type = "radius") |> + geolocate(wkt, type = "radius") |> expect_error("Missing") - galah_geolocate(obj_sf, type = "radius") |> + geolocate(obj_sf, type = "radius") |> expect_error("Invalid spatial object") radius_obj <- quiet_geolocate(lat = -33.66741, lon = 151.3174, diff --git a/tests/testthat/test-galah_bbox.R b/tests/testthat/test-geolocate_bbox.R similarity index 74% rename from tests/testthat/test-galah_bbox.R rename to tests/testthat/test-geolocate_bbox.R index 94e1e0bf..7e8e26ce 100644 --- a/tests/testthat/test-galah_bbox.R +++ b/tests/testthat/test-geolocate_bbox.R @@ -1,15 +1,15 @@ -purrr_bbox <- purrr::quietly(galah_bbox) +purrr_bbox <- purrr::quietly(geolocate_bbox) quiet_bbox <- function(...){ purrr_bbox(...) |> purrr::pluck("result")} -test_that("`galah_bbox()` returns bbox for sf", { +test_that("`geolocate_bbox()` returns bbox for sf", { polygon_sfc <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" |> sf::st_as_sfc() polygon_bbox <- quiet_bbox(polygon_sfc) expected_bbox <- polygon_sfc |> sf::st_bbox() - galah_bbox(polygon_sfc) |> + geolocate_bbox(polygon_sfc) |> expect_message("Data returned for bounding box:") grepl("MULTIPOLYGON", quiet_bbox(polygon_sfc)) |> expect_true() @@ -17,47 +17,47 @@ test_that("`galah_bbox()` returns bbox for sf", { expect_equal(expected_bbox) }) -test_that("`galah_bbox()` returns bbox for shapefile", { +test_that("`geolocate_bbox()` returns bbox for shapefile", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") shapefile <- sf::st_read(poly_path, quiet = TRUE) shapefile_bbox <- quiet_bbox(shapefile) expected_bbox <- attributes(shapefile_bbox)$bbox - expect_message(galah_bbox(shapefile), "Data returned for bounding box:") + expect_message(geolocate_bbox(shapefile), "Data returned for bounding box:") expect_true(grepl("MULTIPOLYGON", quiet_bbox(shapefile))) expect_equal(attributes(shapefile_bbox)$bbox, expected_bbox) }) -test_that("`galah_bbox()` returns bbox for bbox", { # FIXME: not backwards compatible with bbox coords? +test_that("`geolocate_bbox()` returns bbox for bbox", { # FIXME: not backwards compatible with bbox coords? bbox <- sf::st_bbox(c(xmin = 143, xmax = 148, ymin = -29, ymax = -28), crs = sf::st_crs("WGS84")) bbox_galah <- quiet_bbox(bbox) expected_polygon <- "MULTIPOLYGON (((143 -29, 148 -29, 148 -28, 143 -28, 143 -29)))" - expect_message(galah_bbox(bbox), "Data returned for bounding box:") + expect_message(geolocate_bbox(bbox), "Data returned for bounding box:") expect_equal(quiet_bbox(bbox)[1], expected_polygon) expect_equal(attributes(bbox_galah)$bbox, bbox) }) -test_that("`galah_bbox()` returns bbox for tibble", { +test_that("`geolocate_bbox()` returns bbox for tibble", { tibble <- tibble::tibble(xmin = 143, ymin = -29, xmax = 148, ymax = -21) tibble_bbox <- quiet_bbox(tibble) expected_polygon <- "MULTIPOLYGON (((143 -29, 148 -29, 148 -21, 143 -21, 143 -29)))" expected_bbox <- sf::st_bbox(c(xmin = 143, xmax = 148, ymin = -29, ymax = -21), crs = sf::st_crs("WGS84")) - expect_message(galah_bbox(tibble), "Data returned for bounding box:") + expect_message(geolocate_bbox(tibble), "Data returned for bounding box:") expect_equal(quiet_bbox(tibble)[1], expected_polygon) expect_equal(attributes(tibble_bbox)$bbox, expected_bbox) }) -test_that("`galah_bbox()` does not accept incorrect tibbles", { +test_that("`geolocate_bbox()` does not accept incorrect tibbles", { tibble_wrong <- tibble::tibble(c1 = c("hi", "hello"), c2 = 1:2) tibble_bad_colnames <- tibble::tibble(top = 148, bottom = -29, ymin = -29, ymax = -29) tibble_invalid_bbox <- tibble::tibble(xmin = 148, ymin = -29, xmax = 143, ymax = -29) - expect_error(galah_bbox(tibble_wrong)) - expect_error(galah_bbox(tibble_bad_colnames)) - expect_error(galah_bbox(tibble_invalid_bbox)) + expect_error(geolocate_bbox(tibble_wrong)) + expect_error(geolocate_bbox(tibble_bad_colnames)) + expect_error(geolocate_bbox(tibble_invalid_bbox)) }) -test_that("`galah_bbox()` uses only first coordinates of tibble with many coordinates", { +test_that("`geolocate_bbox()` uses only first coordinates of tibble with many coordinates", { tibble_many_coords <- tibble::tibble(xmin = c(148, 145), ymin = c(-29, -42), xmax = c(143, 146), @@ -69,7 +69,7 @@ test_that("`galah_bbox()` uses only first coordinates of tibble with many coordi expect_true() }) -test_that("`galah_bbox()` checks number of inputs, uses first argument", { # FIXME +test_that("`geolocate_bbox()` checks number of inputs, uses first argument", { # FIXME wkt_1 <- glue::glue("POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 \\ -29.39064,142.36228 -29.39064,142.36228 -29.00703))") |> sf::st_as_sfc() @@ -89,35 +89,35 @@ test_that("`galah_bbox()` checks number of inputs, uses first argument", { # FIX as.character(expected_polygon)) }) -test_that("`galah_bbox()` checks inputs", { +test_that("`geolocate_bbox()` checks inputs", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" number <- 45 c_char <- c("a", "b", "c", "d") c_numbers <- c(45, 2, 45, 2) - expect_error(galah_bbox(wkt)) - expect_error(galah_bbox(number)) - expect_error(galah_bbox(c_char)) - expect_error(galah_bbox(c_numbers)) + expect_error(geolocate_bbox(wkt)) + expect_error(geolocate_bbox(number)) + expect_error(geolocate_bbox(c_char)) + expect_error(geolocate_bbox(c_numbers)) }) -test_that("`galah_bbox()` detects invalid spatial objects", { +test_that("`geolocate_bbox()` detects invalid spatial objects", { impossible_bbox <- sf::st_bbox(c(xmin = 148000, xmax = -29000, ymin = -29000, ymax = -29000), crs = sf::st_crs("WGS84")) - expect_error(galah_bbox(impossible_bbox), + expect_error(geolocate_bbox(impossible_bbox), "Invalid spatial object") }) -test_that("`galah_bbox()` converts to multipolygon", { +test_that("`geolocate_bbox()` converts to multipolygon", { wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" sf_wkt <- wkt |> sf::st_as_sfc() expect_match(quiet_bbox(sf_wkt), "MULTIPOLYGON") }) -# TODO: test that galah_bbox unnests sf object & shapefiles correctly +# TODO: test that geolocate_bbox unnests sf object & shapefiles correctly # after converting from dots rm(purrr_bbox, quiet_bbox) \ No newline at end of file diff --git a/tests/testthat/test-galah_polygon.R b/tests/testthat/test-geolocate_polygon.R similarity index 55% rename from tests/testthat/test-galah_polygon.R rename to tests/testthat/test-geolocate_polygon.R index 97506ae0..3a6cae83 100644 --- a/tests/testthat/test-galah_polygon.R +++ b/tests/testthat/test-geolocate_polygon.R @@ -1,71 +1,85 @@ -test_that("galah_polygon uses first argument", { +test_that("`geolocate_polygon()` uses first argument", { wkt_1 <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" wkt_2 <- "POLYGON((145.6765 -42.13203, 145.9652 -42.63203, 146.5425 -42.63203, 146.8312 -42.13203, 146.5425 -41.63203, 145.9652 -41.63203, 145.6765 -42.13203))" expected_polygon <- "MULTIPOLYGON (((142.3623 -29.00703, 142.7413 -29.00703, 142.7413 -29.39064, 142.3623 -29.39064, 142.3623 -29.00703)))" - warning_text <- expect_warning(galah_polygon(wkt_1, wkt_2)) + warning_text <- expect_warning(geolocate_polygon(wkt_1, wkt_2)) grepl("More than 1 spatial area provided", warning_text) |> any() |> expect_true() - quiet_polygons <- purrr::quietly(galah_polygon) + quiet_polygons <- purrr::quietly(geolocate_polygon) calculated_polygon <- quiet_polygons(wkt_1, wkt_2)$result expect_identical(calculated_polygon, - galah_polygon(wkt_1)[1], + geolocate_polygon(wkt_1)[1], expected_polygon) }) -test_that("galah_polygon checks inputs", { +test_that("`geolocate_polygon()` checks inputs", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") + sf::st_read(poly_path, quiet = TRUE) |> + geolocate_polygon() |> + expect_error() + wkt_path <- test_path("testdata", "long_act_wkt.txt") - expect_error(galah_polygon(sf::st_read(poly_path, quiet = TRUE))) - expect_error(galah_polygon(readLines(wkt_path))) + readLines(wkt_path) |> + geolocate_polygon() |> + expect_error() }) -test_that("galah_polygon finds polygon errors 1" , { +test_that("`geolocate_polygon()` finds polygon errors 1" , { skip_if_offline(); skip_on_ci() invalid_wkt <- "POLYGON((145.71622941565508 -32.17848852726597,))" - expect_error(galah_polygon(invalid_wkt)) + geolocate_polygon(invalid_wkt) |> + expect_error() # NOTE: # this code generates an extra string ('OGR: Corrupt data') # that is useful, but can't be stopped by tryCatch. Skipping on CRAN # to avoid unnecessary error messages }) -test_that("galah_polygon finds polygon errors 2", { +test_that("`geolocate_polygon()` finds polygon errors 2", { invalid_wkt <- "POLYGON((132.8 -12.72, 132.95 -12.70, 132.92 -12.57, 132.85 -12.58))" - expect_error(galah_polygon(invalid_wkt)) + geolocate_polygon(invalid_wkt) |> + expect_error() }) -test_that("galah_polygon converts WKT strings to multipolygon", { +test_that("`geolocate_polygon()` converts WKT strings to multipolygon", { wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" - expect_match(galah_polygon(wkt), "MULTIPOLYGON") + geolocate_polygon(wkt) |> + expect_match("MULTIPOLYGON") }) -test_that("galah_polygon converts WKT strings with spaces", { +test_that("`geolocate_polygon()` converts WKT strings with spaces", { wkt_with_spaces <- "POLYGON ((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" - converted_wkt_with_spaces <- build_wkt(sf::st_as_sfc(wkt_with_spaces)) + converted_wkt_with_spaces <- wkt_with_spaces |> + sf::st_as_sfc() |> + build_wkt() expect_match(converted_wkt_with_spaces, "MULTIPOLYGON \\(\\(\\(143\\.32") }) -test_that("galah_polygon converts sf object to multipolygon", { +test_that("`geolocate_polygon()` converts sf object to multipolygon", { sf_wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" |> sf::st_as_sfc() - expect_match(galah_polygon(sf_wkt), "MULTIPOLYGON") + geolocate_polygon(sf_wkt) |> + expect_match("MULTIPOLYGON") }) -test_that("galah_polygon counts vertices correctly", { - wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" - expect_equal(n_points(sf::st_as_sfc(wkt)), 4) +test_that("`geolocate_polygon()` counts vertices correctly", { + sf::st_as_sfc("POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))") |> + n_points() |> + expect_equal(4) }) -test_that("galah_polygon checks for simple polygons only", { +test_that("`geolocate_polygon()` checks for simple polygons only", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") shapefile_complex <- sf::st_read(poly_path, quiet = TRUE) shapefile_simple <- sf::st_simplify(shapefile_complex, dTolerance = 1000) - expect_error(galah_polygon(shapefile_complex), "Polygon must have 500 or fewer vertices") - expect_match(galah_polygon(shapefile_simple), "MULTIPOLYGON") + geolocate_polygon(shapefile_complex) |> + expect_error(label = "Polygon must have 500 or fewer vertices") + geolocate_polygon(shapefile_simple) |> + expect_match(regexp = "MULTIPOLYGON") }) -test_that("galah_polygon counts n vertices correctly", { +test_that("`geolocate_polygon()` counts n vertices correctly", { sf_wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" |> sf::st_as_sfc() poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") @@ -75,4 +89,4 @@ test_that("galah_polygon counts n vertices correctly", { }) -# Future: test that galah_polygon accepts nothing as an input and ignores \ No newline at end of file +# Future: test that geolocate_polygon accepts nothing as an input and ignores \ No newline at end of file diff --git a/tests/testthat/test-galah_radius.R b/tests/testthat/test-geolocate_radius.R similarity index 52% rename from tests/testthat/test-galah_radius.R rename to tests/testthat/test-geolocate_radius.R index 8191fb67..f9baac30 100644 --- a/tests/testthat/test-galah_radius.R +++ b/tests/testthat/test-geolocate_radius.R @@ -1,9 +1,9 @@ -purrr_radius <- purrr::quietly(galah_radius) +purrr_radius <- purrr::quietly(geolocate_radius) quiet_radius <- function(...){ purrr_radius(...) |> purrr::pluck("result")} -test_that("galah_radius returns list from lon/lat/radius arguments", { +test_that("`geolocate_radius()` returns list from lon/lat/radius arguments", { lon <- 151.3174 lat <- -33.66741 radius = 3 @@ -16,7 +16,7 @@ test_that("galah_radius returns list from lon/lat/radius arguments", { expect_equal(radius_object, expected_object) }) -test_that("galah_radius assigns default radius when missing argument", { +test_that("`geolocate_radius()` assigns default radius when missing argument", { lon <- 151.3174 lat <- -33.66741 radius_object <- purrr_radius(lon = lon, lat = lat) @@ -28,7 +28,7 @@ test_that("galah_radius assigns default radius when missing argument", { expect_equal(radius_object$result, expected_object) }) -test_that("galah_radius returns radius for sf_POINT object", { +test_that("`geolocate_radius()` returns radius for sf_POINT object", { point <- sf::st_sfc(sf::st_point(c(-33.66741, 151.3174)), crs = 4326) expected_object <- list(lat = -33.66741, @@ -38,63 +38,64 @@ test_that("galah_radius returns radius for sf_POINT object", { expect_equal(expected_object) }) -test_that("galah_radius errors when more complex sf objects are passed", { +test_that("`geolocate_radius()` errors when more complex sf objects are passed", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") shapefile <- sf::st_read(poly_path, quiet = TRUE) wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" obj_sf <- wkt |> sf::st_as_sfc() - expect_error(galah_radius(shapefile, radius = 3), "Invalid spatial object supplied") - expect_error(galah_radius(obj_sf, radius = 3), "Invalid spatial object supplied") + geolocate_radius(shapefile, radius = 3) |> + expect_error(label = "Invalid spatial object supplied") + geolocate_radius(obj_sf, radius = 3) |> + expect_error(label = "Invalid spatial object supplied") }) -test_that("galah_radius detects inputs", { +test_that("`geolocate_radius()` detects inputs", { lon_char <- "wrongo" lon_list <- list(lon = 151) lon_df <- data.frame(lon = 151) lat <- -33.66741 radius = 3 - expect_error(galah_radius(lon = lon_char, + expect_error(geolocate_radius(lon = lon_char, lat = lat, radius = radius), "Invalid class detected") - expect_error(galah_radius(lon = lon_list, + expect_error(geolocate_radius(lon = lon_list, lat = lat, radius = radius), "Invalid class detected") - expect_error(galah_radius(lon = lon_df, + expect_error(geolocate_radius(lon = lon_df, lat = lat, radius = radius), "Invalid class detected") }) -test_that("galah_radius detects impossible coordinates", { +test_that("`geolocate_radius()` detects impossible coordinates", { lon1 <- 182 lon2 <- -195 lat1 <- -91 lat2 = 109 - expect_error(galah_radius(lon = lon1, - lat = -32, - radius = 2), - "Point location outside of possible range") - expect_error(galah_radius(lon = lon2, - lat = -32, - radius = 2), - "Point location outside of possible range") - expect_error(galah_radius(lon = 151, - lat = lat1, - radius = 2), - "Point location outside of possible range") - expect_error(galah_radius(lon = 151, - lat = lat2, - radius = 2), - "Point location outside of possible range") + geolocate_radius(lon = lon1, + lat = -32, + radius = 2) |> + expect_error(label = "Point location outside of possible range") + geolocate_radius(lon = lon2, + lat = -32, + radius = 2) |> + expect_error(label = "Point location outside of possible range") + geolocate_radius(lon = 151, + lat = lat1, + radius = 2) |> + expect_error(label = "Point location outside of possible range") + geolocate_radius(lon = 151, + lat = lat2, + radius = 2) |> + expect_error(label = "Point location outside of possible range") }) -test_that("galah_radius messages when radius is very large", { +test_that("`geolocate_radius()` messages when radius is very large", { radius <- 1600 - expect_message( - galah_radius(lon = 151, lat = -32, radius = radius), - "Supplied radius is larger than the area of Australia") + geolocate_radius(lon = 151, lat = -32, radius = radius) |> + expect_message(label = "Supplied radius is larger than the area of Australia") }) -test_that("galah_radius only uses first arguments supplied to lon/lat/radius", { +test_that("`geolocate_radius()` only uses first arguments supplied to lon/lat/radius", { multiple_lon <- c(151, 152, 153) multiple_lat <- c(-31, -32, -33) multiple_radius <- c(3, 2, 1) @@ -102,18 +103,18 @@ test_that("galah_radius only uses first arguments supplied to lon/lat/radius", { expected_object <- list(lat = -31, lon = 151, radius = 3) - expect_warning(galah_radius(lon = multiple_lon, - lat = -31, - radius = radius), - "More than 1 spatial") - expect_warning(galah_radius(lon = 151, - lat = multiple_lat, - radius = radius), - "More than 1 spatial") - expect_warning(galah_radius(lon = 151, - lat = -31, - radius = multiple_radius), - "More than 1 radius") + geolocate_radius(lon = multiple_lon, + lat = -31, + radius = radius) |> + expect_warning(label = "More than 1 spatial") + geolocate_radius(lon = 151, + lat = multiple_lat, + radius = radius) |> + expect_warning(label = "More than 1 spatial") + geolocate_radius(lon = 151, + lat = -31, + radius = multiple_radius) |> + expect_warning(label = "More than 1 radius") quiet_radius(lon = multiple_lon, lat = -31, radius = radius) |> purrr::pluck("lon") |> @@ -126,5 +127,5 @@ test_that("galah_radius only uses first arguments supplied to lon/lat/radius", { expect_equal(expected_object$radius) }) -# TODO: (after implementing) galah_radius uses only first coordinates of tibble with many coordinates +# TODO: (after implementing) geolocate_radius uses only first coordinates of tibble with many coordinates rm(purrr_radius, quiet_radius) \ No newline at end of file diff --git a/tests/testthat/test-galah_identify.R b/tests/testthat/test-identify.R similarity index 100% rename from tests/testthat/test-galah_identify.R rename to tests/testthat/test-identify.R From f9be3fc371c6bfcc5af5c1960d44f878da2da368 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 1 May 2026 14:02:26 +1000 Subject: [PATCH 23/70] Fix tests/bugs for Kew, Spain, Sweden --- R/onload.R | 6 ++++-- R/utilities_internal.R | 8 +++----- tests/testthat/test-international-Portugal.R | 14 +++++++------- tests/testthat/test-international-Spain.R | 18 +++++++++--------- tests/testthat/test-international-Sweden.R | 6 +++--- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/R/onload.R b/R/onload.R index 5e30f4aa..c05a336a 100644 --- a/R/onload.R +++ b/R/onload.R @@ -47,9 +47,11 @@ "\n", # note: glue wipes newlines, so have to be outside cli::col_magenta(glue::glue('This package is currently configured to query {current_node} ({current_url}).\n')), "\n", - i = cli::col_magenta('- You can change this at any time using e.g. `galah_config(atlas = \"GBIF\")`.'), + i = cli::col_magenta('- You can change this setting globally using using e.g. `galah_config(atlas = \"GBIF\")`.'), "\n", - i = cli::col_magenta('- To see all supported organisations, run `show_all(atlases)`.') + i = cli::col_magenta('- Or for a single query by opening your pipe with e.g. `galah_call(from = "Spain")`.'), + "\n", + i = cli::col_magenta('- To see *all* supported organisations, run `show_all(atlases)`.') ) |> packageStartupMessage() } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 288c03b0..49f0c9ab 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -450,12 +450,9 @@ image_filters <- function(present_fields, "Kew" = "(images:*)", "Portugal" = "(all_image_url:*)", "Spain" = "(multimedia:*)", - "Sweden" = {filter_fields <- present_fields |> - stringr::str_remove("s$") |> - paste0("IDsCount") - glue::glue("{filter_fields}:[1 TO *]")}, + "Sweden" = "(multimedia:*)", "United Kingdom" = "(all_image_url:*)", - cli::cli_abort("`atlas_media` is not supported for atlas = {atlas}", + cli::cli_abort("`atlas_media()` is not supported for atlas = {atlas}", call = error_call) ) } @@ -467,6 +464,7 @@ species_facets <- function(.query){ c("Australia", "Flanders", "France", + "Kew", "Spain", "Sweden", "United Kingdom")) { diff --git a/tests/testthat/test-international-Portugal.R b/tests/testthat/test-international-Portugal.R index 670c9c77..17a8bd2b 100644 --- a/tests/testthat/test-international-Portugal.R +++ b/tests/testthat/test-international-Portugal.R @@ -92,7 +92,7 @@ test_that("show_values works for fields for Portugal", { x <- purrr::quietly(show_values) x(...)$result } - x <- search_all(fields, "basis_of_record") |> + x <- search_all(fields, "basisOfRecord") |> quiet_values() |> try(silent = TRUE) skip_if(inherits(x, "try-error"), message = "API not available") @@ -118,7 +118,7 @@ test_that("atlas_counts works with type = 'species' for Portugal", { expect_gt(x, 0) }) -test_that("atlas_counts works with galah_identify for Portugal", { +test_that("`atlas_counts()` works with galah_identify for Portugal", { skip_if_offline(); skip_on_ci() result <- galah_call() |> identify("Mammalia") |> @@ -138,30 +138,30 @@ test_that("atlas_counts works with galah_identify for Portugal", { 0.1) # i.e. <1% margin of error }) -test_that("atlas_counts works with group_by for Portugal", { +test_that("`atlas_counts()` works with group_by for Portugal", { skip_if_offline(); skip_on_ci() result <- galah_call() |> filter(year >= 2000) |> - group_by(basis_of_record) |> + group_by(basisOfRecord) |> count() |> collect() |> try(silent = TRUE) skip_if(inherits(result, "try-error"), message = "API not available") expect_gt(nrow(result), 1) - expect_equal(names(result), c("basis_of_record", "count")) + expect_equal(names(result), c("basisOfRecord", "count")) }) # NOTE: I've so far been unable to register for this atlas, # so we can't test downloads. This affects `atlas_occurrences()` and # `atlas_species()` -test_that("atlas_species returns error for Portugal", { +test_that("`atlas_species()` returns error for Portugal", { skip_if_offline(); skip_on_ci() expect_error({galah_call(type = "species") |> identify("Carnivora") |> collect() }) }) -test_that("atlas_occurrences returns error for Portugal", { +test_that("`atlas_occurrences()` returns error for Portugal", { skip_if_offline(); skip_on_ci() expect_error(atlas_occurrences( filter = galah_filter(year == 2020) diff --git a/tests/testthat/test-international-Spain.R b/tests/testthat/test-international-Spain.R index f0cbd252..b1c88804 100644 --- a/tests/testthat/test-international-Spain.R +++ b/tests/testthat/test-international-Spain.R @@ -161,7 +161,7 @@ test_that("show_values works for fields for Spain", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("atlas_counts works for Spain", { +test_that("`atlas_counts()` works for Spain", { skip_if_offline(); skip_on_ci() x <- atlas_counts() |> dplyr::pull(count) |> @@ -170,7 +170,7 @@ test_that("atlas_counts works for Spain", { expect_gt(x, 0) }) -test_that("atlas_counts works with type = 'species' for Spain", { +test_that("`atlas_counts()` works with type = 'species' for Spain", { skip_if_offline(); skip_on_ci() x <- atlas_counts(type = "species") |> dplyr::pull(count) |> @@ -179,7 +179,7 @@ test_that("atlas_counts works with type = 'species' for Spain", { expect_gt(x, 0) }) -test_that("atlas_counts works with galah_identify for Spain", { +test_that("`atlas_counts()` works with galah_identify for Spain", { skip_if_offline(); skip_on_ci() result <- galah_call() |> identify("Mammalia") |> @@ -199,7 +199,7 @@ test_that("atlas_counts works with galah_identify for Spain", { 0.1) # i.e. <1% margin of error }) -test_that("atlas_counts works with apply_profile for Spain", { +test_that("`atlas_counts()` works with apply_profile for Spain", { skip_if_offline(); skip_on_ci() without_profile <- galah_call() |> count() |> @@ -213,7 +213,7 @@ test_that("atlas_counts works with apply_profile for Spain", { expect_lt(with_profile$count, without_profile$count) }) -test_that("atlas_counts works with group_by for Spain", { +test_that("`atlas_counts()` works with group_by for Spain", { skip_if_offline(); skip_on_ci() result <- galah_call() |> filter(year >= 2000) |> @@ -226,7 +226,7 @@ test_that("atlas_counts works with group_by for Spain", { expect_equal(names(result), c("basisOfRecord", "count")) }) -test_that("atlas_species works for Spain", { +test_that("`atlas_species()` works for Spain", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Spain", @@ -242,7 +242,7 @@ test_that("atlas_species works for Spain", { expect_s3_class(spp, c("tbl_df", "tbl", "data.frame")) }) -test_that("galah_select works for Spain", { +test_that("`galah_select()` works for Spain", { skip_if_offline(); skip_on_ci() x <- galah_select() y <- galah_select(basisOfRecord) @@ -257,7 +257,7 @@ test_that("galah_select works for Spain", { expect_true(inherits(y$quosure[[1]], c("quosure", "formula"))) }) -test_that("atlas_occurrences works for Spain", { +test_that("`atlas_occurrences()` works for Spain", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Spain", @@ -276,7 +276,7 @@ test_that("atlas_occurrences works for Spain", { expect_true(inherits(occ, c("tbl_df", "tbl", "data.frame"))) }) -test_that("atlas_media() works for Spain", { +test_that("`atlas_media()` works for Spain", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Spain", diff --git a/tests/testthat/test-international-Sweden.R b/tests/testthat/test-international-Sweden.R index 9ab0a498..9a803c20 100644 --- a/tests/testthat/test-international-Sweden.R +++ b/tests/testthat/test-international-Sweden.R @@ -314,7 +314,7 @@ test_that("collect_media() works for Sweden", { x <- request_data() |> identify("Amphibia") |> filter(year == 2010, - imageIDsCount > 0) # |> # multimediaCount + !is.na(multimedia)) # get counts media_count <- x |> count() |> @@ -329,7 +329,7 @@ test_that("collect_media() works for Sweden", { skip_if(inherits(media_occ, "try-error"), message = "API not available") # get metadata media_meta <- request_metadata() |> - filter(media == media_occ) |> + filter(media == unlist(media_occ$images)) |> collect() |> try(silent = TRUE) skip_if(inherits(media_meta, "try-error"), message = "API not available") @@ -348,7 +348,7 @@ test_that("collect_media() works for Sweden", { x <- purrr::quietly(collect_media) x(...)$result } - quiet_media(x[seq_len(n_downloads), ]) + quiet_media(media_meta[seq_len(n_downloads), ]) expect_equal(length(list.files("temp", pattern = ".jpg$")), n_downloads) unlink("temp", recursive = TRUE) From cf60682c923adb72836f8e0d68e4043a55572686 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sat, 2 May 2026 16:38:54 +1000 Subject: [PATCH 24/70] Fix bugs in GBIF code & tests --- R/collapse_occurrences.R | 6 ++- tests/testthat/test-international-GBIF.R | 66 +++++++++++++----------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/R/collapse_occurrences.R b/R/collapse_occurrences.R index 5a6b6b72..696addbf 100644 --- a/R/collapse_occurrences.R +++ b/R/collapse_occurrences.R @@ -4,8 +4,10 @@ collapse_occurrences <- function(x){ if(any(names(x) == "body")){ result <- list( - creator = potions::pour("user", "username", .pkg = "galah"), - notificationAddresses = list(potions::pour("user", "email", .pkg = "galah")), + creator = x$request$authenticate$username, + notificationAddresses = list(x$request$authenticate$email), + # note that in the above line, list() *must* be kept + # as it wraps the address in [] and API calls fail without it. sendNotification = potions::pour("package", "send_email", .pkg = "galah"), format = x$body$format, predicate = build_predicates(x$body)) |> diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index 5bd58d17..b0088fdc 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -8,7 +8,7 @@ test_that("swapping to atlas = GBIF works", { password = "galah-gbif-test-login")) }) -test_that("show_all(fields) works for GBIF", { +test_that("`show_all(fields)` works for GBIF", { skip_if_offline(); skip_on_ci() # first ensure underlying syntax is valid x <- request_metadata() |> @@ -23,7 +23,7 @@ test_that("show_all(fields) works for GBIF", { expect_equal(x, y) }) -test_that("search_all(fields) works for GBIF", { +test_that("`search_all(fields)` works for GBIF", { skip_if_offline(); skip_on_ci() result <- search_all(fields, "year") expect_gte(nrow(result), 2) @@ -35,7 +35,7 @@ test_that("search_all(fields) works for GBIF", { expect_true() }) -test_that("show_values works for GBIF fields", { +test_that("`show_values()` works for GBIF fields", { skip_if_offline(); skip_on_ci() # query syntax x <- request_metadata() |> @@ -58,7 +58,7 @@ test_that("show_values works for GBIF fields", { expect_equal(x, y) }) -test_that("show_all(collections) works for GBIF", { +test_that("`show_all(collections)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- show_all(collections, limit = 10) expect_equal(nrow(x), 10) @@ -74,7 +74,7 @@ test_that("show_all(collections) works for GBIF", { expect_equal(nrow(z), 20) }) -test_that("show_all(datasets) works for GBIF", { +test_that("`show_all(datasets)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- show_all(datasets, limit = 10) expect_equal(nrow(x), 10) @@ -85,7 +85,7 @@ test_that("show_all(datasets) works for GBIF", { expect_gte(nrow(z), 10) # historically this was n = 20 }) -test_that("show_all(providers) works for GBIF", { +test_that("`show_all(providers)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- show_all(providers, limit = 10) expect_equal(nrow(x), 10) @@ -96,25 +96,25 @@ test_that("show_all(providers) works for GBIF", { expect_equal(nrow(z), 20) }) -test_that("show_all(reasons) fails for GBIF", { +test_that("`show_all(reasons)` fails for GBIF", { expect_error(show_all(reasons)) }) -test_that("show_all(assertions) works for GBIF", { +test_that("`show_all(assertions)` works for GBIF", { x <- show_all(assertions) expect_gt(nrow(x), 1) expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("show_all(profiles) fails for GBIF", { +test_that("`show_all(profiles)` fails for GBIF", { expect_error(show_all(profiles)) }) -test_that("show_all(lists) fails for GBIF", { +test_that("`show_all(lists)` fails for GBIF", { expect_error(show_all(lists)) }) -test_that("search_all(taxa) works for GBIF", { +test_that("`search_all(taxa)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- search_taxa("Mammalia") expect_equal(nrow(x), 1) @@ -123,7 +123,7 @@ test_that("search_all(taxa) works for GBIF", { expect_true(x$class == "Mammalia") }) -test_that("search_all(taxa) works using a tibble for GBIF", { +test_that("`search_all(taxa)` works using a tibble for GBIF", { skip_if_offline(); skip_on_ci() x <- search_all(taxa, data.frame(kingdom = "Animalia", @@ -134,7 +134,7 @@ test_that("search_all(taxa) works using a tibble for GBIF", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("search_all(identifiers) works for GBIF", { +test_that("`search_all(identifiers)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- search_all(identifiers, "359") |> try(silent = TRUE) @@ -145,21 +145,21 @@ test_that("search_all(identifiers) works for GBIF", { galah_config(verbose = TRUE) -test_that("search_all(datasets) works for GBIF", { +test_that("`search_all(datasets)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- search_all(datasets, "Mammals") expect_lte(nrow(x), 20) expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("search_all(collections) works for GBIF", { +test_that("`search_all(collections)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- search_all(collections, "Museum") expect_lte(nrow(x), 20) expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("search_all(providers) works for GBIF", { +test_that("`search_all(providers)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- search_all(providers, "Frog") expect_lte(nrow(x), 20) @@ -168,7 +168,7 @@ test_that("search_all(providers) works for GBIF", { galah_config(verbose = FALSE) -test_that("atlas_counts works for GBIF", { +test_that("`count()` works for GBIF", { skip_if_offline(); skip_on_ci() galah_call() |> count() |> @@ -177,13 +177,13 @@ test_that("atlas_counts works for GBIF", { expect_gt(0) }) -test_that("atlas_counts fails for GBIF when type = 'species'", { +test_that("`atlas_counts()` fails for GBIF when `type = 'species'`", { expect_error(atlas_counts(type = "species")) }) galah_config(run_checks = TRUE) -test_that("`count` works with 2 `group_by` args for GBIF", { +test_that("`count()` works with 2 `group_by` args for GBIF", { skip_if_offline(); skip_on_ci() x <- galah_call() |> filter(year >= 2020) |> @@ -310,12 +310,12 @@ test_that("invalid fields are caught from `select()` for GBIF", { expect_error() }) -test_that("`atlas_occurrences()` works with `galah_polygon()` for GBIF", { +test_that("`atlas_occurrences()` works with `geolocate_polygon()` for GBIF", { skip_if_offline(); skip_on_ci() wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" base_query <- galah_call() |> identify("Mammalia") |> - galah_polygon(wkt) + geolocate_polygon(wkt) count <- base_query |> count() |> collect() @@ -325,11 +325,11 @@ test_that("`atlas_occurrences()` works with `galah_polygon()` for GBIF", { expect_equal(nrow(result), count$count) }) -test_that("`atlas_occurences()` works with `galah_radius()` for GBIF", { +test_that("`atlas_occurences()` works with `geolocate_radius()` for GBIF", { skip_if_offline(); skip_on_ci() base_query <- galah_call() |> identify("Mammalia") |> - galah_radius(lat = -33.7, + geolocate_radius(lat = -33.7, lon = 151.3, radius = 0.5) count <- base_query |> @@ -344,11 +344,14 @@ test_that("`atlas_occurences()` works with `galah_radius()` for GBIF", { test_that("`atlas_species()` works for GBIF", { skip_if_offline(); skip_on_ci() - x <- request_data(type = "species") |> + x <- request_data(type = "species", + from = "GBIF") |> + authenticate(username = "atlasoflivingaustralia", + email = "ala4r@ala.org.au", + password = "galah-gbif-test-login") |> filter(year == 2010) |> - identify("Litoria") |> - compound() - # collapse() + identify("Crinia") |> + collapse() expect_s3_class(x, "query") expect_equal(length(x), 7) expect_equal(names(x), @@ -357,14 +360,17 @@ test_that("`atlas_species()` works for GBIF", { y <- compute(x) expect_s3_class(y, "computed_query") z <- collect(y) - expect_gt(nrow(z), 0) - expect_gt(ncol(z), 0) + expect_gt(nrow(z), 10) # n = 14 + expect_gt(ncol(z), 5) # n = 22 expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) }) test_that("`distinct()` queries accept `select()` for GBIF", { skip_if_offline(); skip_on_ci() - x <- request_data() |> + x <- request_data(from = "GBIF") |> + authenticate(username = "atlasoflivingaustralia", + email = "ala4r@ala.org.au", + password = "galah-gbif-test-login") |> filter(year == 2010) |> distinct(species_guid, .keep_all = TRUE) |> identify("Crinia") |> From a9f633051ed004cdd9693abf2b1cf62273deadc7 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sun, 3 May 2026 18:54:51 +1000 Subject: [PATCH 25/70] Update DESCRIPTION, NEWS, and QSG --- DESCRIPTION | 2 +- NEWS.md | 19 ++- R/onload.R | 2 +- data-raw/node_config.csv | 4 +- man/figures/atlases_plot.png | Bin 336204 -> 336525 bytes vignettes/quick_start_guide.Rmd | 213 +++++++++++++++------------ vignettes/quick_start_guide.Rmd.orig | 82 ++++++----- 7 files changed, 185 insertions(+), 137 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b9c76085..3fa7e996 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: galah Type: Package Title: Biodiversity Data from the GBIF Node Network -Version: 2.2.1.9999 +Version: 2.3.0 Authors@R: c(person(given = "Martin", family = "Westgate", diff --git a/NEWS.md b/NEWS.md index 754c8584..fca1533e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,20 @@ +# galah 2.3.0 + +### New functions and options +* `request_data()` and friends gain a `from` argument, allowing in-pipe atlas specification (#297). +* `authenticate()` gains arguments to allow in-pipe email- or JWT-based authentication. Support added for JWT-based authentication on VBP (#296). +* New function `describe()` to check what fields are available (#291). +* New names for spatial functions: `geolocate_polygon()`, `geolocate_bbox()` and `geolocate_radius()`. +* Now possible to call `select()` on GBIF occurrence queries. + +### Bug fixes +* `isAuthoritative` and `isThreatened` returned by default by `search_all()` (#293). +* Calling functions using `pkg::fun()` syntax work without first calling `library(galah)` (#294). + +### Lifecycle +* `galah_` prefix functions for piping formally superseded by their `dplyr` alternatives. +* Using `filter`, `select` etc as arguments to the `atlas_` family formally superseded in favor of piping. + # galah 2.2.0 ### Improved organisational support @@ -27,7 +44,7 @@ * Move to `{cli}` for `print()` calls, not `cat()`. * Reduce usage of `@importFrom` in favour of `pkg::fun()` syntax, as per R style guide. * `basisOfRecord` now included as default field (i.e. with `select(group = "basic")`). (#281) -* `query` objects now have a `request` slot showing the request that generated them . +* `query` objects now have a `request` slot showing the request that generated them. # galah 2.1.2 diff --git a/R/onload.R b/R/onload.R index c05a336a..3e4e471d 100644 --- a/R/onload.R +++ b/R/onload.R @@ -49,7 +49,7 @@ "\n", i = cli::col_magenta('- You can change this setting globally using using e.g. `galah_config(atlas = \"GBIF\")`.'), "\n", - i = cli::col_magenta('- Or for a single query by opening your pipe with e.g. `galah_call(from = "Spain")`.'), + i = cli::col_magenta('- Or for a single query by opening your pipe with e.g. `request_data(from = "Spain")`.'), "\n", i = cli::col_magenta('- To see *all* supported organisations, run `show_all(atlases)`.') ) |> diff --git a/data-raw/node_config.csv b/data-raw/node_config.csv index 6157e748..f2670743 100644 --- a/data-raw/node_config.csv +++ b/data-raw/node_config.csv @@ -1,8 +1,8 @@ atlas,type,url,functional Australia,data/distributions-id,https://api.ala.org.au/spatial-service/distribution/{id},FALSE Australia,data/distributions-taxa,https://api.ala.org.au/spatial-service/distribution/lsids/{lsid}?nowkt=false,FALSE -Australia,data/events,https://api.ala.org.au/event/download,TRUE -Australia,data/events-count,https://api.ala.org.au/event/graphql,TRUE +Australia,data/events,https://api.ala.org.au/event/download,FALSE +Australia,data/events-count,https://api.ala.org.au/event/graphql,FALSE Australia,data/occurrences,https://api.ala.org.au/occurrences/occurrences/offline/download,TRUE Australia,data/occurrences-count,https://api.ala.org.au/occurrences/occurrences/search,TRUE Australia,data/occurrences-count-groupby,https://api.ala.org.au/occurrences/occurrences/facets,TRUE diff --git a/man/figures/atlases_plot.png b/man/figures/atlases_plot.png index d5e88e4fa4d3c4547f102bdd9dee52310c456b22..7b39aad07c3e41a763ef7506193eb7c9ec3be6dd 100644 GIT binary patch delta 261865 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≪%+HLxyttSJlpQEyzT@z} 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 LJ!;?8^Sgfo0Fa;o delta 261037 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 @@ -36,55 +36,63 @@ Load the package library(galah) ``` -# Configuration -Begin by choosing which organisation you would like `galah` to query, -and providing your registration information for that organisation. - - -``` r -galah_config(atlas = "GBIF", - username = "user1", - email = "email@email.com", - password = "my_password") -``` - -The full list of supported queries by organisation is as follows: - -

- # Getting data + `galah` is a `dplyr` extension package; rather than using pipes to amend a `tibble` in your workspace, you amend a query, which is then sent to your chosen organisation. These pipes differ from traditional syntax in two ways: -- they begin with a function - usually `galah_call()` - instead of a `tibble` +- they begin with a function - usually `request_data()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` So an example query might be to find the number of records: ``` r -galah_config(atlas = "Australia") +request_data() |> # open a pipe + count() |> # count the number of rows + collect() # retrieve query from the server +``` + +``` +## # A tibble: 1 × 1 +## count +## +## 1 179728084 +``` + +By default, queries are sent to the Atlas of Living Australia, but this can +be changed using the `from` argument: -galah_call() |> # open a pipe - count() |> # count the number of rows - collect() # retrieve query from the server + +``` r +request_data(from = "Sweden") |> + count() |> + collect() ``` ``` ## # A tibble: 1 × 1 ## count ## -## 1 167207256 +## 1 173135256 ``` -You can further narrow this to a single taxonomic group with `identify()`: + +The `from` argument accepts the `acronym`, `region`, or `institution` fields from +`show_all(atlases)`. The full list of supported queries by organisation is as follows: + +
+Figure 1: Organisations (rows) and APIs (columns) supported by galah +

Figure 1: Organisations (rows) and APIs (columns) supported by galah

+
+ +# Filtering + +You can pass taxonomic filters to your query with `identify()`: ``` r -galah_call() |> +request_data() |> identify("Eolophus roseicapilla") |> count() |> collect() @@ -94,7 +102,7 @@ galah_call() |> ## # A tibble: 1 × 1 ## count ## -## 1 1410911 +## 1 1594059 ``` To choose records using non-taxonomic criteria, you'll need to use `filter()`. @@ -107,26 +115,26 @@ You can answer the first question with `describe()`: ``` r -galah_call() |> +request_data() |> describe() |> collect() ``` ``` -## # A tibble: 639 × 3 -## id description data_type -## -## 1 abcdTypeStatus string -## 2 acceptedNameUsage Accepted name string -## 3 acceptedNameUsageID Accepted name string -## 4 accessRights Access rights string -## 5 annotationsDoi string -## 6 annotationsUid Referenced by publication string -## 7 assertionUserId Assertions by user string -## 8 assertions Record issues string -## 9 assertionsCount int -## 10 associatedMedia Associated Media string -## # ℹ 629 more rows +## # A tibble: 581 × 3 +## id description data_type +## +## 1 acceptedNameUsage Accepted name string +## 2 acceptedNameUsageID Accepted name string +## 3 accessRights Access rights string +## 4 annotationsDoi string +## 5 annotationsUid Referenced by publication string +## 6 assertionUserId Assertions by user string +## 7 assertions Record issues string +## 8 assertionsCount int +## 9 associatedMedia Associated Media string +## 10 associatedOccurrences Associated Occurrences string +## # ℹ 571 more rows ``` Once you find a variable you are interested in, see what values it contains @@ -134,7 +142,7 @@ using `distinct()`: ``` r -galah_call() |> +request_data() |> distinct(basisOfRecord) |> collect() ``` @@ -158,7 +166,7 @@ Then you can combine these within a `filter()` query: ``` r -galah_call() |> +request_data() |> filter(basisOfRecord == "PRESERVED_SPECIMEN") |> count() |> collect() @@ -168,7 +176,7 @@ galah_call() |> ## # A tibble: 1 × 1 ## count ## -## 1 18055082 +## 1 18189911 ``` These functions can be combined in a number of ways, for example to @@ -176,7 +184,7 @@ learn how many species are present within the genus 'Crinia': ``` r -galah_call() |> +request_data() |> identify("Crinia") |> # filters by taxonomic names distinct(speciesID) |> # keep only unique values count() |> @@ -195,7 +203,7 @@ you need is included: ``` r -galah_call() |> +request_data() |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> glimpse() |> @@ -203,15 +211,15 @@ galah_call() |> ``` ``` -## Rows: 21,984 +## Rows: 27,556 ## Columns: 8 -## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/9b4ad5… -## $ eventDate 1.285459e+12, 1.280189e+12, 1.287533e+12 -## $ scientificName "Eolophus roseicapilla", "Eolophus roseicapilla", "Eolophus roseicapilla" -## $ decimalLatitude -37.1500, -18.6998, -32.6300 -## $ decimalLongitude 143.7500, 127.6872, 138.0900 -## $ basisOfRecord "HUMAN_OBSERVATION", "MATERIAL_SAMPLE", "HUMAN_OBSERVATION" -## $ dataResourceName "BirdLife Australia, Birdata", "Australian National Wildlife Collection provider for OZCAM", "BirdLife Australia, Birdata" +## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/a586dca0-4717-451a-90da-b65518a52ee3", "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/a38ac5ac-3049-4fde-8391-faf8bf7aeb7f" +## $ eventDate 1.267402e+12, 1.272586e+12, 1.282781e+12 +## $ scientificName "Eolophus roseicapilla roseicapilla", "Eolophus roseicapilla", "Eolophus roseicapilla albiceps" +## $ decimalLatitude -30.91667, -36.42500, -20.52361 +## $ decimalLongitude 150.2500, 144.6583, 145.3958 +## $ basisOfRecord "HUMAN_OBSERVATION", "HUMAN_OBSERVATION", "HUMAN_OBSERVATION" +## $ dataResourceName "New South Wales Bird Atlassers", "Victorian Biodiversity Atlas", "BirdLife Australia, Birdata" ## $ occurrenceStatus "PRESENT", "PRESENT", "PRESENT" ``` @@ -220,35 +228,50 @@ themselves: - ``` r -galah_call() |> +request_data() |> + authenticate(email = "registered_email@wherever.com") |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> select(eventDate, decimalLatitude, species) |> collect() ``` + +``` +## Request for 27556 occurrences placed in queue. +``` + +``` +## Current queue length: 1 ``` -## # A tibble: 21,984 × 3 + +``` +## --- +``` + +``` +## # A tibble: 27,556 × 3 ## eventDate decimalLatitude species ## -## 1 NA -36.5 Eolophus roseicapilla +## 1 NA -36.3 Eolophus roseicapilla ## 2 NA -38.2 Eolophus roseicapilla -## 3 NA -36.1 Eolophus roseicapilla -## 4 NA -38.3 Eolophus roseicapilla -## 5 NA -38.2 Eolophus roseicapilla -## 6 NA -38.3 Eolophus roseicapilla +## 3 NA -37.0 Eolophus roseicapilla +## 4 NA -37.7 Eolophus roseicapilla +## 5 NA -35.6 Eolophus roseicapilla +## 6 NA -31.1 Eolophus roseicapilla ## 7 NA -38.2 Eolophus roseicapilla -## 8 NA -38.2 Eolophus roseicapilla -## 9 NA -38.3 Eolophus roseicapilla +## 8 NA -36.8 Eolophus roseicapilla +## 9 NA -38.2 Eolophus roseicapilla ## 10 NA -38.2 Eolophus roseicapilla -## # ℹ 21,974 more rows +## # ℹ 27,546 more rows ``` +# Architecture + This works because many of the functions in `dplyr` are "generic", meaning it is possible to write extensions that apply them to new object classes. -In our case, `galah_call()` creates a new object class called a +In our case, `request_data()` creates a new object class called a `data_request` for which we have written new extensions. This means that galah will not interfere with your use of `filter()` and friends on your tibbles. Supported `dplyr` verbs that modify queries are as follows: @@ -297,7 +320,7 @@ galah_call() |> ## # A tibble: 1 × 1 ## count ## -## 1 9238597 +## 1 9913135 ``` Or occurrences: @@ -313,20 +336,20 @@ galah_call() |> ``` ``` -## # A tibble: 2,032 × 9 -## recordID scientificName taxonConceptID decimalLatitude decimalLongitude eventDate basisOfRecord occurrenceStatus dataResourceName -## -## 1 0026d29f-b6ab-4a1d-9c57-6ee12cfde3a0 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.4 149. 2000-08-07 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… -## 2 0062d446-007b-4164-ac4f-ae84297e2578 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.3 149. 2000-03-10 00:00:00 HUMAN_OBSERV… PRESENT BirdLife Austra… -## 3 00a62ee0-1e08-4114-b0d8-9b7905472d53 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.2 149. 2000-01-29 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… -## 4 00ab2f4d-326f-4b01-9a8a-1a10c1f77e3c Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.4 149. 2000-09-25 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… -## 5 00ae4631-ea59-44ec-b8f8-4377b6b3b3ef Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.3 149. 2000-02-12 00:00:00 HUMAN_OBSERV… PRESENT BirdLife Austra… -## 6 00b6c8ec-e7b9-4d9f-9638-d962b1b4acfa Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-9… -35.2 149. 2000-02-05 00:00:00 HUMAN_OBSERV… PRESENT Garden Bird Sur… -## # ℹ 2,026 more rows +## --# A tibble: 2,318 × 9 +## recordID scientificName taxonConceptID decimalLatitude decimalLongitude eventDate basisOfRecord occurrenceStatus dataResourceName +## +## 1 0026d29f-b6ab-4a1d-9c57-6ee12cfde3a0 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.4 149. 2000-08-07 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## 2 00a62ee0-1e08-4114-b0d8-9b7905472d53 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.2 149. 2000-01-29 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## 3 00ab2f4d-326f-4b01-9a8a-1a10c1f77e3c Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.4 149. 2000-09-25 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## 4 00b6c8ec-e7b9-4d9f-9638-d962b1b4acfa Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.2 149. 2000-02-05 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## 5 00e36517-6518-42d5-8b69-90c526095fef Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.4 149. 2000-01-01 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## 6 010b463f-d34f-45ec-8733-91f676a299d8 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.2 149. 2000-03-12 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys +## # ℹ 2,312 more rows ``` `atlas_species()` replaces the need for `distinct()` call, while `atlas_media()` -is a shortcut to a complex workflow that incorporates both data and metadata +is a shortcut to a more complex workflow that incorporates both data and metadata calls. Finally, metadata calls can be made more efficiently using the `show_all()` and `show_values()` functions. These take the same arguments as the `type` argument in `request_metadata()`, but us non-standard evaluation, so they @@ -338,20 +361,20 @@ show_all(fields) ``` ``` -## # A tibble: 639 × 3 -## id description type -## -## 1 abcdTypeStatus fields -## 2 acceptedNameUsage Accepted name fields -## 3 acceptedNameUsageID Accepted name fields -## 4 accessRights Access rights fields -## 5 annotationsDoi fields -## 6 annotationsUid Referenced by publication fields -## 7 assertionUserId Assertions by user fields -## 8 assertions Record issues fields -## 9 assertionsCount fields -## 10 associatedMedia Associated Media fields -## # ℹ 629 more rows +## # A tibble: 581 × 3 +## id description type +## +## 1 acceptedNameUsage Accepted name fields +## 2 acceptedNameUsageID Accepted name fields +## 3 accessRights Access rights fields +## 4 annotationsDoi fields +## 5 annotationsUid Referenced by publication fields +## 6 assertionUserId Assertions by user fields +## 7 assertions Record issues fields +## 8 assertionsCount fields +## 9 associatedMedia Associated Media fields +## 10 associatedOccurrences Associated Occurrences fields +## # ℹ 571 more rows ``` You can check the diff --git a/vignettes/quick_start_guide.Rmd.orig b/vignettes/quick_start_guide.Rmd.orig index 3dcde669..d3330e85 100644 --- a/vignettes/quick_start_guide.Rmd.orig +++ b/vignettes/quick_start_guide.Rmd.orig @@ -33,45 +33,45 @@ Load the package library(galah) ``` -# Configuration -Begin by choosing which organisation you would like `galah` to query, -and providing your registration information for that organisation. - -```{r, galah-config, eval=FALSE} -galah_config(atlas = "GBIF", - username = "user1", - email = "email@email.com", - password = "my_password") - -``` - -The full list of supported queries by organisation is as follows: - -```{r, atlas-support, echo = FALSE, out.width = "100%", fig.cap="Fig 1: Organisations and APIs supported by galah"} -knitr::include_graphics('../man/figures/atlases_plot.png') -``` - # Getting data + `galah` is a `dplyr` extension package; rather than using pipes to amend a `tibble` in your workspace, you amend a query, which is then sent to your chosen organisation. These pipes differ from traditional syntax in two ways: -- they begin with a function - usually `galah_call()` - instead of a `tibble` +- they begin with a function - usually `request_data()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` So an example query might be to find the number of records: ```{r, count-example} -galah_config(atlas = "Australia") +request_data() |> # open a pipe + count() |> # count the number of rows + collect() # retrieve query from the server +``` + +By default, queries are sent to the Atlas of Living Australia, but this can +be changed using the `from` argument: + +```{r, sbdi-example} +request_data(from = "Sweden") |> + count() |> + collect() +``` -galah_call() |> # open a pipe - count() |> # count the number of rows - collect() # retrieve query from the server +The `from` argument accepts the `acronym`, `region`, or `institution` fields from +`show_all(atlases)`. The full list of supported queries by organisation is as follows: + +```{r, atlas-support, echo = FALSE, out.width = "100%", fig.cap="Figure 1: Organisations (rows) and APIs (columns) supported by galah"} +knitr::include_graphics('../man/figures/atlases_plot.png') ``` -You can further narrow this to a single taxonomic group with `identify()`: + +# Filtering + +You can pass taxonomic filters to your query with `identify()`: ```{r, identify-example} -galah_call() |> +request_data() |> identify("Eolophus roseicapilla") |> count() |> collect() @@ -86,7 +86,7 @@ This reqires that you find out: You can answer the first question with `describe()`: ```{r, describe-example} -galah_call() |> +request_data() |> describe() |> collect() ``` @@ -95,7 +95,7 @@ Once you find a variable you are interested in, see what values it contains using `distinct()`: ```{r, distinct-example} -galah_call() |> +request_data() |> distinct(basisOfRecord) |> collect() ``` @@ -103,7 +103,7 @@ galah_call() |> Then you can combine these within a `filter()` query: ```{r, count-filter-example} -galah_call() |> +request_data() |> filter(basisOfRecord == "PRESERVED_SPECIMEN") |> count() |> collect() @@ -113,7 +113,7 @@ These functions can be combined in a number of ways, for example to learn how many species are present within the genus 'Crinia': ```{r, species-count-example} -galah_call() |> +request_data() |> identify("Crinia") |> # filters by taxonomic names distinct(speciesID) |> # keep only unique values count() |> @@ -124,7 +124,7 @@ You can `glimpse()` a data download before you run it, to check all the data you need is included: ```{r, glimpse-example} -galah_call() |> +request_data() |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> glimpse() |> @@ -134,22 +134,30 @@ galah_call() |> And, once satisfied that your parameters are correct, download the records themselves: -```{r, cache-email, echo = FALSE} -galah_config(email = "ala4r@ala.org.au", - verbose = FALSE) + +```{r, occurrence-example, eval = FALSE} +request_data() |> + authenticate(email = "registered_email@wherever.com") |> + identify("Eolophus roseicapilla") |> + filter(year == 2010) |> + select(eventDate, decimalLatitude, species) |> + collect() ``` -```{r, occurrence-example, message = FALSE} -galah_call() |> +```{r, cache-email, echo = FALSE} +request_data() |> + authenticate(email = "ala4r@ala.org.au") |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> select(eventDate, decimalLatitude, species) |> collect() ``` +# Architecture + This works because many of the functions in `dplyr` are "generic", meaning it is possible to write extensions that apply them to new object classes. -In our case, `galah_call()` creates a new object class called a +In our case, `request_data()` creates a new object class called a `data_request` for which we have written new extensions. This means that galah will not interfere with your use of `filter()` and friends on your tibbles. Supported `dplyr` verbs that modify queries are as follows: @@ -205,7 +213,7 @@ galah_call() |> ``` `atlas_species()` replaces the need for `distinct()` call, while `atlas_media()` -is a shortcut to a complex workflow that incorporates both data and metadata +is a shortcut to a more complex workflow that incorporates both data and metadata calls. Finally, metadata calls can be made more efficiently using the `show_all()` and `show_values()` functions. These take the same arguments as the `type` argument in `request_metadata()`, but us non-standard evaluation, so they From 36cf5f703619fa583e342de4cda1c1aefb9767b8 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sun, 3 May 2026 18:56:23 +1000 Subject: [PATCH 26/70] minor test updates --- tests/testthat/_snaps/print.md | 33 ++++++++++++++++---------------- tests/testthat/test-galah_call.R | 1 + 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/testthat/_snaps/print.md b/tests/testthat/_snaps/print.md index bd9d5b2b..2122c122 100644 --- a/tests/testthat/_snaps/print.md +++ b/tests/testthat/_snaps/print.md @@ -75,6 +75,19 @@ * atlas: Australia * url: https://api.ala.org.au/namematching/api/search?q=Crinia +# object of class `query_set` formats correctly + + Code + compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), + basisOfRecord == "HUMAN_OBSERVATION")) + Message + Object of class query_set containing 4 queries: + * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig + * metadata/fields url: https://api.ala.org.au/occurrences/index/fields + * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes + * data/occurrences url: + https://api.ala.org.au/occurrences/occurrences/offline/... + # `galah_config()` formats correctly Code @@ -87,9 +100,9 @@ User x authentication - username [Not Provided] - email - password [Not Provided] + username [Provided] + email ala4r@ala.org.au + password [Provided] download_reason_id 10 Package @@ -100,17 +113,3 @@ i directory: something -# object of class `query_set` formats correctly - - Code - compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), - basisOfRecord == "HUMAN_OBSERVATION")) - Message - Object of class query_set containing 5 queries: - * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig - * metadata/fields url: https://api.ala.org.au/occurrences/index/fields - * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes - * metadata/reasons url: https://api.ala.org.au/logger/service/logger/reasons - * data/occurrences url: - https://api.ala.org.au/occurrences/occurrences/offline/... - diff --git a/tests/testthat/test-galah_call.R b/tests/testthat/test-galah_call.R index 2dfe2fa2..3520bdd6 100644 --- a/tests/testthat/test-galah_call.R +++ b/tests/testthat/test-galah_call.R @@ -13,6 +13,7 @@ test_that("`request_` functions build correct object classes", { }) test_that("request_data(from = X) sets `atlas` slot in later objects", { + skip_if_offline(); skip_on_ci() chosen_atlas <- "Spain" query_initial <- request_data(from = chosen_atlas) |> filter(year == 2010) |> From f4af3389489da1b93a2d45bf01bb3c19228cbd05 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Wed, 13 May 2026 12:30:27 +1000 Subject: [PATCH 27/70] Update gitignore and Rbuildignore Adds AI stuff to ignore --- .Rbuildignore | 4 +++- .gitignore | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 4eb89fe2..da651838 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -17,4 +17,6 @@ ^data-raw/ ^data-raw$ ^cheatsheet/ -^cheatsheet$ \ No newline at end of file +^cheatsheet$ +^\.positai$ +^\.claude$ diff --git a/.gitignore b/.gitignore index fcb38c10..0fec45bb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ docs/* docs SECRETS.txt .secure-credentials -/TEST-SENSITIVE-DATA \ No newline at end of file +/TEST-SENSITIVE-DATA +.positai From 541eca1eaa3fb211c3909e8884ae3f9454b06a7e Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Wed, 13 May 2026 12:30:57 +1000 Subject: [PATCH 28/70] minor addition to `describe()` docs --- R/describe.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/describe.R b/R/describe.R index 176924a6..e658de32 100644 --- a/R/describe.R +++ b/R/describe.R @@ -15,7 +15,8 @@ #' @order 1 #' @returns A `tibble` showing the `id`, `description` and `data_type` #' for all fields, or if `select()` is called, then those fields -#' requested by the user. +#' requested by the user. See [galah_select()] for other examples of valid +#' `group` values. #' @examples \dontrun{ #' # By default, this shows all fields in the source system #' galah_call() |> From 433507d751d865e8fee29abb26087f7bc6c92c14 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Wed, 13 May 2026 12:32:29 +1000 Subject: [PATCH 29/70] Add deprecation warnings --- R/superseded-functions.R | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/R/superseded-functions.R b/R/superseded-functions.R index 2b6da022..5e80358f 100644 --- a/R/superseded-functions.R +++ b/R/superseded-functions.R @@ -44,6 +44,7 @@ NULL #' @order 1 #' @export galah_apply_profile <- function(...){ + lifecycle::deprecate_warn("2.3.0", "galah_apply_profile()", "apply_profile()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() switch(class(dots[[1]])[1], @@ -63,6 +64,7 @@ galah_apply_profile <- function(...){ #' @order 2 #' @export galah_filter <- function(...){ + lifecycle::deprecate_warn("2.3.0", "galah_filter()", "filter()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() check_named_input(dots) @@ -100,27 +102,40 @@ galah_filter <- function(...){ #' @rdname superseded_functions #' @order 3 #' @export -galah_geolocate <- geolocate +galah_geolocate <- function(...) { + lifecycle::deprecate_warn("2.3.0", "galah_geolocate()", "geolocate()") + geolocate(...) +} #' @rdname superseded_functions #' @order 4 #' @export -galah_polygon <- geolocate_polygon +galah_polygon <- function(...) { + lifecycle::deprecate_warn("2.3.0", "galah_polygon()", "geolocate_polygon()") + geolocate_polygon(...) +} #' @rdname superseded_functions #' @order 5 #' @export -galah_bbox <- geolocate_bbox +galah_bbox <- function(...) { + lifecycle::deprecate_warn("2.3.0", "galah_bbox()", "geolocate_bbox()") + geolocate_bbox(...) +} #' @rdname superseded_functions #' @order 6 #' @export -galah_radius <- geolocate_radius +galah_radius <- function(...) { + lifecycle::deprecate_warn("2.3.0", "galah_radius()", "geolocate_radius()") + geolocate_radius(...) +} #' @rdname superseded_functions #' @order 7 #' @export galah_group_by <- function(...){ + lifecycle::deprecate_warn("2.3.0", "galah_group_by()", "group_by()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() switch(class(dots[[1]])[1], @@ -140,6 +155,7 @@ galah_group_by <- function(...){ #' @order 8 #' @export galah_identify <- function(...) { + lifecycle::deprecate_warn("2.3.0", "galah_identify()", "identify()") dots_initial <- list(...) if (length(dots_initial) < 1) { cli::cli_warn("No query passed to `identify()`.") @@ -158,6 +174,7 @@ galah_identify <- function(...) { #' @order 9 #' @export galah_select <- function(..., group = NULL){ + lifecycle::deprecate_warn("2.3.0", "galah_select()", "select()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() |> as.list() From 25aa17317bca6cfb98a63d7a6b79ddf7a5304d1d Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sat, 16 May 2026 09:01:21 +0200 Subject: [PATCH 30/70] Ensure GBIF count requests get user/password from query, not potions --- R/capture_occurrences_count.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/capture_occurrences_count.R b/R/capture_occurrences_count.R index a9d43d15..cdcc7b79 100644 --- a/R/capture_occurrences_count.R +++ b/R/capture_occurrences_count.R @@ -83,8 +83,8 @@ capture_occurrences_count_gbif <- function(.query){ limit = 0) # get strings - username <- potions::pour("user", "username", .pkg = "galah") - password <- potions::pour("user", "password", .pkg = "galah") + username <- .query$authenticate$username + password <- .query$authenticate$password user_string <- glue::glue("{username}:{password}") # handle type From 2b1f6226d7905b00192cd482f61b37fb5986c477 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sat, 16 May 2026 17:00:03 +0200 Subject: [PATCH 31/70] Supersede rather than deprecate `galah_` functions Remove deprecated warnings as per advice in https://cran.r-project.org/web/packages/lifecycle/vignettes/communicate.html Also remove `galah_` functions from examples --- R/describe.R | 4 ++-- R/dplyr-select.R | 9 ++++----- R/superseded-functions.R | 31 +++++++------------------------ man/describe.Rd | 3 ++- man/select.data_request.Rd | 9 ++++----- man/superseded_functions.Rd | 6 +++--- 6 files changed, 22 insertions(+), 40 deletions(-) diff --git a/R/describe.R b/R/describe.R index e658de32..d0b01e02 100644 --- a/R/describe.R +++ b/R/describe.R @@ -15,8 +15,8 @@ #' @order 1 #' @returns A `tibble` showing the `id`, `description` and `data_type` #' for all fields, or if `select()` is called, then those fields -#' requested by the user. See [galah_select()] for other examples of valid -#' `group` values. +#' requested by the user. See \code{\link[=select.data_request]{select()}} +#' for other examples of valid `group` values. #' @examples \dontrun{ #' # By default, this shows all fields in the source system #' galah_call() |> diff --git a/R/dplyr-select.R b/R/dplyr-select.R index 397e49d2..4d4770b2 100644 --- a/R/dplyr-select.R +++ b/R/dplyr-select.R @@ -48,9 +48,8 @@ #' require thousands or millions of records at a time. To reduce time taken to #' download data, and limit complexity of the resulting `tibble`, it is sensible #' to restrict the fields returned by occurrence queries. The full list of -#' available fields can be viewed with `show_all(fields)`. Note that `select()` -#' and `galah_select()` are supported for all atlases that allow downloads, with -#' the exception of GBIF, for which all columns are returned. +#' available fields can be viewed with `show_all(fields)`. `select()` +#' is supported for all atlases that allow downloads. #' #' Calling the argument `group = "basic"` returns the following columns: #' @@ -123,8 +122,8 @@ #' # When used in a pipe, `galah_select()` and `select()` are synonymous. #' # Hence the previous example can be rewritten as: #' galah_call() |> -#' galah_identify("perameles") |> -#' galah_select(basisOfRecord, group = "basic") |> +#' identify("perameles") |> +#' select(basisOfRecord, group = "basic") |> #' collect() #' } #' @export diff --git a/R/superseded-functions.R b/R/superseded-functions.R index 5e80358f..f99f9f7b 100644 --- a/R/superseded-functions.R +++ b/R/superseded-functions.R @@ -1,9 +1,9 @@ #' Superseded functions #' #' These functions are still valid, but have been superseded by more recent -#' versions. They are designed to be used to supply arguments within -#' functions from the [atlas_] family. Instead you should consider using -#' piped functions for this same functionality. +#' versions. They were designed to be used to supply arguments within +#' functions from the [atlas_] family. Instead you should use piped +#' functions for this same functionality. #' @name superseded_functions #' @rdname superseded_functions #' @param ... Arguments passed to the function in question, usually (but @@ -44,7 +44,6 @@ NULL #' @order 1 #' @export galah_apply_profile <- function(...){ - lifecycle::deprecate_warn("2.3.0", "galah_apply_profile()", "apply_profile()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() switch(class(dots[[1]])[1], @@ -64,7 +63,6 @@ galah_apply_profile <- function(...){ #' @order 2 #' @export galah_filter <- function(...){ - lifecycle::deprecate_warn("2.3.0", "galah_filter()", "filter()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() check_named_input(dots) @@ -102,40 +100,27 @@ galah_filter <- function(...){ #' @rdname superseded_functions #' @order 3 #' @export -galah_geolocate <- function(...) { - lifecycle::deprecate_warn("2.3.0", "galah_geolocate()", "geolocate()") - geolocate(...) -} +galah_geolocate <- geolocate #' @rdname superseded_functions #' @order 4 #' @export -galah_polygon <- function(...) { - lifecycle::deprecate_warn("2.3.0", "galah_polygon()", "geolocate_polygon()") - geolocate_polygon(...) -} +galah_polygon <- geolocate_polygon #' @rdname superseded_functions #' @order 5 #' @export -galah_bbox <- function(...) { - lifecycle::deprecate_warn("2.3.0", "galah_bbox()", "geolocate_bbox()") - geolocate_bbox(...) -} +galah_bbox <- geolocate_bbox #' @rdname superseded_functions #' @order 6 #' @export -galah_radius <- function(...) { - lifecycle::deprecate_warn("2.3.0", "galah_radius()", "geolocate_radius()") - geolocate_radius(...) -} +galah_radius <- geolocate_radius #' @rdname superseded_functions #' @order 7 #' @export galah_group_by <- function(...){ - lifecycle::deprecate_warn("2.3.0", "galah_group_by()", "group_by()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() switch(class(dots[[1]])[1], @@ -155,7 +140,6 @@ galah_group_by <- function(...){ #' @order 8 #' @export galah_identify <- function(...) { - lifecycle::deprecate_warn("2.3.0", "galah_identify()", "identify()") dots_initial <- list(...) if (length(dots_initial) < 1) { cli::cli_warn("No query passed to `identify()`.") @@ -174,7 +158,6 @@ galah_identify <- function(...) { #' @order 9 #' @export galah_select <- function(..., group = NULL){ - lifecycle::deprecate_warn("2.3.0", "galah_select()", "select()") dots <- rlang::enquos(..., .ignore_empty = "all") |> detect_request_object() |> as.list() diff --git a/man/describe.Rd b/man/describe.Rd index 9185957c..8c3cfb5f 100644 --- a/man/describe.Rd +++ b/man/describe.Rd @@ -17,7 +17,8 @@ describe(x, ...) \value{ A \code{tibble} showing the \code{id}, \code{description} and \code{data_type} for all fields, or if \code{select()} is called, then those fields -requested by the user. +requested by the user. See \code{\link[=select.data_request]{select()}} +for other examples of valid \code{group} values. } \description{ Unlike a local \code{tibble}, where printing to the console shows what fields diff --git a/man/select.data_request.Rd b/man/select.data_request.Rd index eaa1bfad..a86adeaf 100644 --- a/man/select.data_request.Rd +++ b/man/select.data_request.Rd @@ -68,9 +68,8 @@ GBIF nodes store content in hundreds of different fields, and users often require thousands or millions of records at a time. To reduce time taken to download data, and limit complexity of the resulting \code{tibble}, it is sensible to restrict the fields returned by occurrence queries. The full list of -available fields can be viewed with \code{show_all(fields)}. Note that \code{select()} -and \code{galah_select()} are supported for all atlases that allow downloads, with -the exception of GBIF, for which all columns are returned. +available fields can be viewed with \code{show_all(fields)}. \code{select()} +is supported for all atlases that allow downloads. Calling the argument \code{group = "basic"} returns the following columns: \itemize{ @@ -144,8 +143,8 @@ galah_call() |> # When used in a pipe, `galah_select()` and `select()` are synonymous. # Hence the previous example can be rewritten as: galah_call() |> - galah_identify("perameles") |> - galah_select(basisOfRecord, group = "basic") |> + identify("perameles") |> + select(basisOfRecord, group = "basic") |> collect() } } diff --git a/man/superseded_functions.Rd b/man/superseded_functions.Rd index df74039c..e14d9209 100644 --- a/man/superseded_functions.Rd +++ b/man/superseded_functions.Rd @@ -46,9 +46,9 @@ include. Valid options are \code{"basic"}, \code{"event"} \code{"taxonomy"}, \co } \description{ These functions are still valid, but have been superseded by more recent -versions. They are designed to be used to supply arguments within -functions from the \link{atlas_} family. Instead you should consider using -piped functions for this same functionality. +versions. They were designed to be used to supply arguments within +functions from the \link{atlas_} family. Instead you should use piped +functions for this same functionality. } \details{ Replacements are as follows: From 9a8f5d9a88d83a4d38d4db2458ea62cf5fd23f81 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Sat, 16 May 2026 17:00:20 +0200 Subject: [PATCH 32/70] Add further test for authentication with count data in GBIF --- tests/testthat/test-international-GBIF.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index b0088fdc..af762293 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -365,6 +365,20 @@ test_that("`atlas_species()` works for GBIF", { expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) }) +test_that("`count()` queries work with `authenticate()` for GBIF", { + skip_if_offline(); skip_on_ci() + x <- request_data(from = "GBIF") |> + authenticate(username = "atlasoflivingaustralia", + email = "ala4r@ala.org.au", + password = "galah-gbif-test-login") |> + filter(year == 2010) |> + count() |> + collect() + expect_equal(nrow(x), 1) + expect_equal(ncol(x), 1) + expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) +}) + test_that("`distinct()` queries accept `select()` for GBIF", { skip_if_offline(); skip_on_ci() x <- request_data(from = "GBIF") |> From 8fa2480ac56114af453e66ced2af30c095a19502 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 27 May 2026 14:54:34 +0200 Subject: [PATCH 33/70] Fix for miscellaneous GBIF bugs (#299) tests added --- R/check.R | 16 +++++++++---- R/collect_metadata.R | 2 +- R/collect_metadata_unnest.R | 14 +++++++---- R/compound.R | 9 +++++-- R/utilities_internal.R | 5 ++-- tests/testthat/test-international-GBIF.R | 30 ++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/R/check.R b/R/check.R index eb7eeb07..62e27bf6 100644 --- a/R/check.R +++ b/R/check.R @@ -234,13 +234,21 @@ check_fields_gbif_counts <- function(.query){ filter_invalid <- glue::glue_collapse(invalid_fields, sep = ", ") } } - + # then facets - # first extract facets + # noting this differs between using this for it's original purpose (generating counts), + # and it's more recent purpose of getting field values group_by_invalid <- NA - if(!is.null(.query$body$group_by)){ + if(.query$type == "data/occurrences-count" & !is.null(.query$body$group_by)){ facets <- .query$body$group_by$name - # check for invalid facets + }else if(.query$type == "metadata/fields-unnest" & !is.null(.query$request$filter$value)){ + facets <- .query$request$filter$value[1] + }else{ + facets <- NULL + } + + # if a facet is supplied, check its' validity + if(!is.null(facets)){ valid_search_fields <- .query[["metadata/fields"]] |> dplyr::filter(.data$search_field == TRUE) |> dplyr::pull("id") diff --git a/R/collect_metadata.R b/R/collect_metadata.R index b1d7d7b7..a3ce87a7 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -269,7 +269,7 @@ collect_fields <- function(.query){ }else{ result <- query_API(.query) |> dplyr::bind_rows() - + if(.query$atlas == "Global"){ # we need to join some local metadata to GBIF info df <- gbif_internal_archived$search_fields |> diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 1af57650..bf258bb7 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -7,20 +7,26 @@ collect_fields_unnest <- function(.query, facet <- .query |> purrr::pluck("url") |> httr2::url_parse() - + if(.query$atlas == "Global"){ # get name of facet in question facet <- purrr::pluck(facet, "query", "facet") # NOTE: "facet" (singular) check_missing_fields(facet, call = error_call) # get result from API - .query |> + result <- .query |> query_API() |> - purrr::pluck(!!!list("facets", 1, "counts")) |> + purrr::pluck(!!!list("facets", 1, "counts")) + # add an error catcher here, as next step is sensitive to column missingness + # this should be caught because `pluck()` generates NULL for missing data + if(is.null(result)){ + tibble::tibble() + }else{ + result |> dplyr::bind_rows() |> dplyr::rename_with(camel_to_snake_case) |> dplyr::rename({{facet}} := "name") |> parse_select(.query) - + } }else{ facet <- purrr::pluck(facet, "query", "facets") # NOTE: "facets" (plural) check_missing_fields(facet, call = error_call) diff --git a/R/compound.R b/R/compound.R index cba9b89f..87105722 100644 --- a/R/compound.R +++ b/R/compound.R @@ -131,8 +131,13 @@ build_query_set_metadata <- function(x){ if(potions::pour("package", "run_checks")){ result <- append(result, switch(x$request$type, - "fields-unnest" = list(request_metadata("fields", from = x$atlas) |> capture()), - "profiles-unnest" = list(request_metadata("profiles", from = x$atlas) |> capture()), + "fields-unnest" = list( + request_metadata(type = "fields", from = x$atlas) |> + select(tidyselect::everything()) |> + capture()), + "profiles-unnest" = list( + request_metadata("profiles", from = x$atlas) |> + capture()), NULL)) } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 49f0c9ab..9a22b115 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -494,10 +494,11 @@ authentication_required <- function(x){ # only required for downloads (in current version) download_check <- x$type %in% c("occurrences", "species") & # downloads never happen for `glimpse()` or `describe()` - is.null(x$glimpse) & is.null(x$describe) & # downloads only happen for GBIF counts, otherwise count invalidates need for auth - (is.null(x$count) | (!is.null(x$count) & x$atlas == "Global")) + (is.null(x$count) | (!is.null(x$count) & x$atlas == "Global")) & + # ditto glimpse + (is.null(x$glimpse) | (!is.null(x$glimpse) & x$atlas == "Global")) # is.null(x$group_by) & # <- not needed, as group_by() can preface count() or distinct() # `distinct()` only requires authentication if 1. it is present and 2. keep_all = TRUE diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index af762293..553ee8e4 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -58,6 +58,13 @@ test_that("`show_values()` works for GBIF fields", { expect_equal(x, y) }) +test_that("`show_values()` fails for unsearchable GBIF fields", { + skip_if_offline(); skip_on_ci() + search_all(fields, "accessRights") |> + show_values() |> + expect_error() +}) + test_that("`show_all(collections)` works for GBIF", { skip_if_offline(); skip_on_ci() x <- show_all(collections, limit = 10) @@ -271,6 +278,29 @@ test_that("`glimpse()` works for GBIF", { expect_equal(3) }) +test_that("`glimpse()` works with `identify()` for GBIF", { + skip_if_offline(); skip_on_ci() + galah_config(atlas = "GBIF", + username = "atlasoflivingaustralia", + email = "ala4r@ala.org.au", + password = "galah-gbif-test-login") + x <- request_data(from = "GBIF") |> + identify("Eolophus roseicapilla") |> + filter(year == 2010) |> + glimpse() |> + collapse() + expect_s3_class(x, c("occurrences_glimpse", "tbl_df", "tbl", "data.frame")) + expect_equal(nrow(x), 3) # number of rows in the tibble + quiet_print <- purrr::quietly(print.occurrences_glimpse) + x_print <- strsplit(quiet_print(x)$output, "\n")[[1]] # print statement + expect_gt(length(x_print), 5) + stringr::str_detect(x_print, + "^\\$ (taxonConceptID|eventDate|decimalLatitude)") |> + which() |> + length() |> + expect_equal(3) +}) + # FIXME: add test for `select()` paired with `glimpse()` test_that("`atlas_occurrences()` works for GBIF", { From 6e5e3e684a45f08de987e641c1a3e5f1bcd026d1 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 27 May 2026 16:25:42 +0200 Subject: [PATCH 34/70] First pass at supporting ALA specieslist API v2 (#300) Doesn't support filtering to a single dr yet. Have tried to implement without breaking other living atlases, but this will require testing. --- R/capture_metadata.R | 23 +++++++++++++++-------- R/collapse_metadata.R | 30 ++++++++++++++++++++++++------ R/collect_metadata.R | 11 ++++++++--- R/sysdata.rda | Bin 7432 -> 7390 bytes data-raw/node_config.csv | 4 ++-- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/R/capture_metadata.R b/R/capture_metadata.R index 6ddad705..bd0861a8 100644 --- a/R/capture_metadata.R +++ b/R/capture_metadata.R @@ -174,7 +174,7 @@ capture_licences <- function(x){ #' @noRd #' @keywords Internal capture_lists <- function(x, - error_call = rlang::caller_env()){ + error_call = rlang::caller_env()){ x$type <- "metadata/lists" # if filter is supplied, lookup a specified list by dr number if(!is.null(x$filter)){ @@ -202,24 +202,31 @@ capture_lists <- function(x, # if filter isn't supplied, check cache etc }else{ if(check_if_cache_update_needed(x, "lists")){ - url <- url_lookup(x) |> - httr2::url_parse() - url$query <- list(max = 10000) + url <- url_lookup(x) if(!missing(x)){ + url <- url |> httr2::url_parse() if(!is.null(x$slice)){ - url$query <- list(max = x$slice$slice_n) - } + max_value <- x$slice$slice_n + switch(x$atlas, + "Australia" = {url$query <- list(pageSize = x$slice$slice_n)}, + url$query <- list(max = x$slice$slice_n)) + }else{ + switch(x$atlas, + "Australia" = {url$query <- list(pageSize = 1)}, + url$query <- list(max = 10000)) + } + url <- httr2::url_build(url) } result <- list(type = x$type, atlas = x$atlas, - url = httr2::url_build(url), + url = url, headers = build_headers()) }else{ result <- default_cache(x) } } result |> - as_query() + as_prequery() } #' Internal version of `capture()` for `request_metadata(type = "media")` diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index 8c2d3780..f87990ee 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -6,17 +6,35 @@ #' @keywords Internal collapse_lists <- function(.query){ if(is.null(.query$url)){ - .query + as_query(.query) }else if(inherits(.query$url, "tbl_df")){ - .query - }else if(stringr::str_detect(.query$url, "[:digit:]+$")){ - .query + as_query(.query) + }else if(stringr::str_detect(.query$url, "[:digit:]+$") & .query$atlas != "Australia"){ + as_query(.query) + # set up 'new' lists code (May 2026) + # rationale here is that we run a query to get the number of levels + }else if(stringr::str_detect(.query$url, "[:digit:]+$") & .query$atlas == "Australia"){ + # first handle case where levels are pre-specified + if(!is.null(x$request$slice)){ + # FIXME: This assumes that n requested is <25, which is a false assumption + # need to explicitly check this number and paginate etc accordingly + as_query(.query) + }else{ + n_lists <- .query |> + query_API() |> + purrr::pluck("listCount") + n_pages <- ceiling(n_lists * 0.04) # (i.e. 1/25) + base_url <- stringr::str_remove(.query$url, "pageSize=1$") + .query$url <- tibble::tibble(url = glue::glue("{base_url}page={seq_len(n_pages)}&pageSize=25")) + as_query(.query) + } + # below is legacy, probably still important/used, but hard to be sure }else{ url <- httr2::url_parse(.query$url) n_requested <- as.integer(url$query$max) # make decisions about how much pagination is needed if(n_requested <= 500){ # we haven't hit pagination limit - .query + as_query(.query) }else{ # more lists are requested n <- get_max_n(.query) n_pages <- ceiling(n$max_requested / n$paginate) @@ -34,8 +52,8 @@ collapse_lists <- function(.query){ }) |> unlist() .query$url <- dplyr::select(result, "url") + as_query(.query) } - .query } } diff --git a/R/collect_metadata.R b/R/collect_metadata.R index a3ce87a7..912723fe 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -353,7 +353,7 @@ collect_lists <- function(.query){ # NOTE: this function has some quite versatile behaviour, so we need to # explicitly control when caching does (and does not) happen. should_update_cache <- FALSE - + # requests for cached data use the `data` slot; check this first if(!is.null(.query$data)){ result_df <- retrieve_internal_data(.query) @@ -364,8 +364,13 @@ collect_lists <- function(.query){ # if(length(result) > 1 & is.null(names(result))){ # this old code is risky # test for pagination requests in .query instead if(inherits(.query$url, "data.frame")){ - result_df <- purrr::map(query_API(.query), - \(a){a$lists}) |> + # FIXME: haven't checked whether this breaks other living atlases + result_df <- purrr::map(result, \(a){a |> + purrr::pluck("lists") |> + purrr::map(tidy_list_columns) |> + dplyr::bind_rows()}, + .progress = potions::pour("package", "verbose", .pkg = "galah") + ) |> dplyr::bind_rows() should_update_cache <- TRUE }else{ diff --git a/R/sysdata.rda b/R/sysdata.rda index 6b01887ceb9c652e05c0c489650fed09246eaf88..9c0cf6f906b471c2dd0859e667166dd95ed05796 100644 GIT binary patch literal 7390 zcmV<493kUET4*^jL0KkKSr(Vno&X_bfBpae|NsC0|NsC0|M|cF|NBrvK>$SIN(dML z06|2B;3L=1rPSll`pCXMzVI{-wP)E?POa|<4?s1gvqR0-Oeb0%+3e>u>(5@^^Vg@N zZ*kFW%kHOpMeSQ&{S!u0(;+nUMwm4olhoT&O*I)0LI!{UN@$t{5@cyTRP{Y0Mt}es0MGyc z000^Rpa1{>14BY1G^SI@Ce%GmMwlj=WDNr#X`lcA0|)?MjGAc8O*8}401W_1q>%{H zQ_5+eVm6Qf&IMkYKr$Kt(8x5%$j|^Z1W5>*1k*s8X)>MyYE6-*1lmCco~G1jdLVj-r~}jnjXg}7 z4FCWDC)aKrgB*jv|7DTmaSIkQ7YznNK$*Ch`<^a-Eel&v86su|urfmG8#;_Amo6nJ znL!vqP-*~&+PA%NQ;zk}ny&(rH} zta09`4(-&UDZnBNB1eJJeY4DE`;TWn3JDQO%ZsP=>_22m7KF6X;uUjp@5kbyneU+8_>)k+9kT8%L6Iy!1TTle43^Go`CG__Jx8FP8Y^>PZYVkK1dxIKy zhSb#>N1fE#cU);m;JBffM>I1o_OfG5X%|xX5fc;+oj^B?wL>zwaK-kagMkE4AzCC4 zl6J9#L|RF~6Cy%19WX&wAyp$PQeOb5F%EH9o#R6eX0Q5_30_|eguLR@p9!U3W+ySS zszg86(_w*4aO$9F!a`ZnSo{5+l6&$@UXWp#q3K<_e&6x19tU;rj=dN)DEiOleP%}O zNi$$>pzOhZTy<`k8lr9WG8!b*btcQKH?>G`yKv*jrPS8rA!#h!MFb{Szw7-f z`?aO)^V3UdYSz_sh0($(Y+l-vGL1^!_j@OGLh#hym!=-j)z#k_d&8uv`Bln7r7;Bx`78A|#4~_h;?IP+TdL0y0tx9=t(zn+e8Z2k^Mi%;RVwFIXRaRGb~s{_1E^$Jee!z?KUt%+kVUC_ zK>iSKFy4n->SdER0V^Ajp5>A;e#X|Ig()3vf{jFoZxw4ofmTSPLqM`8VKHv4_-w0F zUBYvodik#}JWghuOy%QX^zX7kP0n1ep`m^h(&8f`C{pDEP?j`7B`srYHl+fF9uR6f z&`3a>ah;VPJt<{SFdHKq7OMUTT$`L2+j@4j)zsr&tRrmA3CtqsjkF`yTuG-Rp;;+@ z?X_0htCsXDp$}s_8V=ch@z^uzYrkh2dy?Q*$x!GDmr*-3XHF48jclK-5rE_VwUq6TEDqK^P zN4$w#b2T=p78bNAWB~pO9?At2tX6<6j-V#qS6{P9mClMLsNwAaE5@~9W;_pYOZ=6)v=Oh31a~7aAQ2L zF9cVnmMyiKHt8@l!C;I~QZO?IYLFXX)EXQI1BWp+P2{zSyFeX{_S{2x!pwAr5Zpiz z4WMte*qabYVZ#P6Q$rbNMP%C6ve9ZOxMnD4d=Zin!)2=xvz1tZW+9k7b#E`AB(tz( zf}=2DnKiG%;}W3Rv>@L!XrY@cJLO!#E>dp$Be^tc42g3gU2Qqi>ry6S}Hf#h}rnLqHLZU(1 z6n=|rN@CgtgdtvtJ^xl7x56)dUuWg;Q^uVBDTl71SJ!^$-SRXZx3Kyd)Ya?&c>cVE ze0D|&1BZY%RC+TQBs%8N2{rz&N0U?A4Hgi-Kt$NCc{$lC6|=W;^~YvXk2_d6GhgFK zHWwu_jaCWt0)!dBXfP3arC%1Ck2ja)`aH1*o6O@s=tWAT@#|08r^Un$} zJkP`JMjo>6rsN6P0L#&_pY``GQkj{MnVFe`LNHwON$Lich3}qKsB$Zqo=gEC!SJV?g{zRyZK$9${YTxhlP?P zHRE=9P|UOzJ`@R;K_%Focyzvq{W8Z!$4_5=FJT$QiGvfb#yNS*(ZLtgR+D74G|vZcamLJCO3YIr!VX@%`U+piie`*?1H0 zD7&1oU>peS$#-4Wu0Zir^zqF>WD(soG&Qw?*8prrkY)<#NIJ3`pd)_78#YRKpodHnG#jC*ik*qD}2?m zw~gyANiZ);jYfJIoS-s61SS~;Ry7f>-S}cVg!*;<4xd_+oJ9Jsv=KPJCAPSh41SFf zgMv2z`A&2l01YwHdXT>yXeGW^A&ax(=HJ7yworZg+P !VrWpxxM~|tkqv$T~7Xz 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 GE=9Ph^+|>R diff --git a/data-raw/node_config.csv b/data-raw/node_config.csv index f2670743..adc3cec1 100644 --- a/data-raw/node_config.csv +++ b/data-raw/node_config.csv @@ -19,8 +19,8 @@ Australia,metadata/fields,https://api.ala.org.au/occurrences/index/fields,TRUE Australia,metadata/fields-unnest,https://api.ala.org.au/occurrences/occurrences/facets,TRUE Australia,metadata/identifiers,https://api.ala.org.au/namematching/api/getByTaxonID,TRUE Australia,metadata/licences,https://api.ala.org.au/images/ws/licence,TRUE -Australia,metadata/lists,https://api.ala.org.au/specieslist/ws/speciesList,TRUE -Australia,metadata/lists-unnest,https://api.ala.org.au/specieslist/ws/speciesListItems/{list_id},TRUE +Australia,metadata/lists,https://lists-ws.ala.org.au/v2/speciesList,TRUE +Australia,metadata/lists-unnest,https://lists-ws.ala.org.au/v2/speciesListItems/{list_id},TRUE Australia,metadata/media,https://api.ala.org.au/images/ws/image/{id},TRUE Australia,metadata/profiles,https://api.ala.org.au/dqf-service/api/v1/data-profiles?enabled=true,TRUE Australia,metadata/profiles-unnest,https://api.ala.org.au/dqf-service/api/v1/data-profiles/{profile},TRUE From 0c68e5c781c3e8126639774e069ff72b51e72811 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 28 May 2026 15:18:26 +0200 Subject: [PATCH 35/70] First pass at response to Security, Vulnerability & Performance Review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate OAuth configuration domains — remote MITM can hijack auth flow - Add httr2::req_retry() to all API calls - Use httr2::req_perform_parallel() for batch/paginated requests - Fix or remove cache_disk parameter (silent contract violation) - Error instead of warn when auth is unsupported for selected atlas - Allowlist permitted curl options in add_options() - Replace blocking spinner with cli::cli_progress_step() --- R/authenticate.R | 22 +-- R/check_queue.R | 4 +- R/collapse_checks.R | 2 +- R/collect_occurrences.R | 32 +---- R/query_API.R | 197 +++++++++++++++++---------- R/read_zip.R | 6 + R/utilities_internal.R | 24 ++++ tests/testthat/test-authentication.R | 7 +- 8 files changed, 179 insertions(+), 115 deletions(-) diff --git a/R/authenticate.R b/R/authenticate.R index 0a15e72e..5210b416 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -71,16 +71,18 @@ authenticate <- function(.data, } # NOTE: We don't _check_ the reason here, because that has to happen # at `collapse()`, because we need to ping an API first - - if( - (isTRUE(use_jwt) | is.null(email)) & authentication_supported(.data$atlas) - ){ - update_request_object(.data, - authenticate = list(email = NULL, - download_reason_id = download_reason_id, - use_jwt = TRUE, - use_apikey = FALSE, # not supported yet - cache_disk = cache_jwt)) + if(isTRUE(use_jwt) | is.null(email)){ + if(authentication_supported(.data$atlas)){ + update_request_object(.data, + authenticate = list(email = NULL, + download_reason_id = download_reason_id, + use_jwt = TRUE, + use_apikey = FALSE, # not supported yet + cache_disk = cache_jwt)) + }else{ + atlas <- .data$atlas + cli::cli_abort("JWT-based authentication is not supported for atlas {atlas}") + } }else{ authenticate_with_email(.data, email = email, diff --git a/R/check_queue.R b/R/check_queue.R index 252c4aab..265b20ac 100644 --- a/R/check_queue.R +++ b/R/check_queue.R @@ -4,11 +4,11 @@ check_queue <- function(.query, wait = FALSE){ # process supplied object if(.query$status == "incomplete"){ - download_response <- c(list(type = .query$type), + download_response <- c(.query[c("type", "atlas")], check_occurrence_status(.query)) |> structure(class = "computed_query") if(wait){ - c(list(type = .query$type), + c(.query[c("type", "atlas")], check_queue_loop(.query)) |> structure(class = "computed_query") }else{ diff --git a/R/collapse_checks.R b/R/collapse_checks.R index abfd2062..d13e9842 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -56,7 +56,7 @@ collapse_run_checks <- function(.query, if(.query$type %in% c("data/occurrences", "data/occurrences-glimpse")){ .query <- check_select(.query, error_call) } - # browser() + if(.query$type %in% c("data/species") & .query$atlas == "Global"){ # NOTE: require this for occurrences too? Do LA species queries require it? parse_select_occurrences(.query, build_select_df_from_query(.query)) # OK this works, just need to update select() now # probably best to move this to check_select_GBIF or similar diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index a1b39cca..519c21da 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -51,11 +51,12 @@ collect_occurrences_default <- function(.query, wait, file, call){ # get data if(potions::pour("package", "verbose", .pkg = "galah") & download_response$status == "complete") { - scrolly_dots_message("Downloading") + cli::cli_progress_step("Downloading") } # sometimes lookup info critical, but not others - unclear when/why! if(any(names(download_response) == "download_url")){ new_object <- list(type = "data/occurrences", + atlas = .query$atlas, url = download_response$download_url, download = TRUE, file = check_download_filename(file)) |> @@ -233,31 +234,4 @@ download_failed_message <- function(call){ i = "This usually suggests a problem with the download itself, rather than the API.", i = "Consider checking that a file has been created in the expected location.") |> cli::cli_abort(call = call) -} - -#' Theatrics -#' @noRd -#' @keywords Internal -scrolly_dots_message <- function(message) { - - spinny <- cli::make_spinner( - which = "simpleDotsScrolling", - template = paste0(message, " {spin}") - ) - - # update the spinner 100 times - lapply(1:100, function(x) { - spinny$spin() - wait(.001) - }) - - # clear the spinner from the status bar - # spinny$finish() -} - -#' Wait time -#' @noRd -#' @keywords Internal -wait <- function(seconds = 1) { - Sys.sleep(seconds) -} +} \ No newline at end of file diff --git a/R/query_API.R b/R/query_API.R index 943674c9..727a8b80 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -7,72 +7,27 @@ #' @keywords Internal query_API <- function(.query, error_call = rlang::caller_env()) { - # first try situation when many urls are supplied - # this is common for living atlases, where many urls are generated for - # e.g. paginated queries, grouped counts etc + # structure for executing queries if(inherits(.query$url, "data.frame")){ - purrr::map(.x = seq_len(nrow(.query$url)), - .f = function(a){ - data_tr <- .query - data_tr$url <- .query$url$url[[a]] - if(any(names(.query$url) == "path")){ # for those that require downloads - data_tr$download <- TRUE - data_tr$file <- .query$url$path[[a]] - } - query_API_internal(data_tr, - error_call = error_call) - }, - .progress = set_progress_bar_behaviour(nrow(.query$url) > 1) - ) - # next handle multiple `body` arguments - # this is currently limited to GBIF count requests with > 1 `group_by` args + query_API_multiple_url(.query, error_call = error_call) }else if(inherits(.query$body, "data.frame")){ - purrr::map(.x = split(.query$body, - seq_len(nrow(.query$body))), - .f = function(a){ - data_tr <- .query - data_tr$body <- a$predicate[[1]] - a$result <- list(query_API_internal(data_tr, - error_call = error_call)) - a - }, - .progress = set_progress_bar_behaviour(nrow(.query$url) > 1) - ) |> - dplyr::bind_rows() - # finally, some queries are 'simple'; one `url`, one or no `body` args - # these we just run without any looping. + query_API_multiple_body(.query, error_call = error_call) }else{ - query_API_internal(.query, - error_call = error_call) + query_API_once(.query, error_call = error_call) } } -#' Internal function to run an API call using httr2 -#' @param criteria length-1 logical statement as to whether to proceed or not +#' Internal function to build an API call using httr2 #' @noRd #' @keywords Internal -set_progress_bar_behaviour <- function(criteria){ - verbose <- all( - potions::pour("package", "verbose", .pkg = "galah") & - isTRUE(criteria)) - if(verbose){ - list(name = "Querying API", - clear = TRUE) - }else{ - FALSE - } -} - -#' Internal function to run an API call using httr2 -#' @noRd -#' @keywords Internal -query_API_internal <- function(.query, - error_call = rlang::caller_env()) { +build_API_call <- function(.query, + error_call = rlang::caller_env()) { query <- httr2::request(.query$url) |> add_headers(.query$headers) |> add_options(.query$options) |> # used by GBIF - add_body(.query$body) # NOTE: adding `body` converts from GET to POST - + add_body(.query$body) |> # NOTE: adding `body` converts from GET to POST + httr2::req_retry(max_tries = 5, backoff = ~ 2) + # set authentication behaviour if(isTRUE(.query$request$authenticate$use_jwt) & .query$type != "metadata/config" # necessary to prevent circular problems @@ -90,31 +45,98 @@ query_API_internal <- function(.query, cache_disk = .query$request$authenticate$cache_disk) } - # then handle downloads + return(query) +} + +#' Internal function to run multiple url-based API calls using httr2 +#' +#' Built for situations when many urls are supplied. +#' This is common for living atlases, where many urls are generated for +#' e.g. paginated queries, grouped counts etc. +#' @noRd +#' @keywords Internal +query_API_multiple_url <- function(.query, + error_call = rlang::caller_env()){ + # get a list of requests + requests <- purrr::map(.x = seq_len(nrow(.query$url)), + .f = \(a){ + data_tr <- .query + data_tr$url <- .query$url$url[[a]] + build_API_call(data_tr) + }) + # and, for downloads, paths + paths <- purrr::map(.x = seq_len(nrow(.query$url)), + .f = \(a){ + if(any(names(.query$url) == "path")){ # for those that require downloads + .query$url$path[[a]] + }else{ + NA + } + }) |> + unlist() + + # first look for paths; if present, download + progress_object <- set_progress_bar_behaviour(nrow(.query$url) > 1) + if(all(!is.na(paths))){ + # purrr::map(unique(paths), check_directory) # necessary? + check_directory(paths[[1]]) # just check first path instead + httr2::req_perform_parallel(requests, + paths = paths, + on_error = "continue", + progress = progress_object) + }else{ # if no path, just run all queries + result <- httr2::req_perform_parallel(requests, + on_error = "continue", + progress = progress_object) + purrr::map(result, httr2::resp_body_json) + } +} + +#' Internal function to run multiple body-based API calls using httr2 +#' +#' This is currently limited to GBIF count requests with > 1 `group_by` args +#' @noRd +#' @keywords Internal +query_API_multiple_body <- function(.query, + error_call = rlang::caller_env()){ + requests <- purrr::map(.x = split(.query$body, + seq_len(nrow(.query$body))), + .f = \(a){ + data_tr <- .query + data_tr$body <- a$predicate[[1]] + build_API_call(data_tr) + }) + progress_object <- set_progress_bar_behaviour(nrow(.query$url) > 1) + result <- httr2::req_perform_parallel(requests, + on_error = "continue", + progress = progress_object) + purrr::map(result, httr2::resp_body_json) +} + +#' Internal function to run an API call using httr2 +#' @noRd +#' @keywords Internal +query_API_once <- function(.query, + error_call = rlang::caller_env()) { + # build a query + query <- build_API_call(.query) + + # handle downloads if(!is.null(.query$download)){ check_directory(.query$file) - - # handle thumbnails (which might fail if missing) - if(any(stringr::str_detect(.query$url, "thumbnail"))) { - query |> - httr2::req_error(is_error = \(resp) FALSE) |> - httr2::req_perform(path = .query$file, - verbosity = 0) - } else { - query |> - httr2::req_perform(path = .query$file, - verbosity = 0) - } + query |> + httr2::req_perform(path = .query$file, + verbosity = 0) # then other pings, which should resolve quickly # and can be allowed to fail otherwise }else{ - res <- query |> + result <- query |> httr2::req_timeout(seconds = 20) |> httr2::req_perform(verbosity = 0) if(grepl("^https://api.gbif.org/v1/occurrence/download/request", .query$url)){ - httr2::resp_body_string(res) + httr2::resp_body_string(result) }else{ - httr2::resp_body_json(res) # may not work for invalid URLs + httr2::resp_body_json(result) # may not work for invalid URLs } } } @@ -158,7 +180,8 @@ add_body <- function(req, body){ #' @keywords Internal add_options <- function(req, options){ if(!is.null(options)){ - req$options <- options + allowed <- c("httpauth", "userpwd") + req$options <- options[names(options) %in% allowed] } req } @@ -201,6 +224,18 @@ build_auth_client <- function(config, atlas){ cache_check } } + + # security + # check token url + config |> + dplyr::pull("token_url") |> + authentication_host(atlas = atlas) + + # check auth url + config |> + dplyr::pull("authorize_url") |> + authentication_host(atlas = atlas) + # otherwise, build new client result <- httr2::oauth_client( id = dplyr::pull(config, "client_id"), @@ -209,6 +244,24 @@ build_auth_client <- function(config, atlas){ name = "galah") |> update_attributes(type = "client", atlas = atlas) + + # cache and return update_cache(client = result) result +} + +#' Internal function to run an API call using httr2 +#' @param criteria length-1 logical statement as to whether to proceed or not +#' @noRd +#' @keywords Internal +set_progress_bar_behaviour <- function(criteria){ + verbose <- all( + potions::pour("package", "verbose", .pkg = "galah") & + isTRUE(criteria)) + if(verbose){ + list(name = "Querying API", + clear = TRUE) + }else{ + FALSE + } } \ No newline at end of file diff --git a/R/read_zip.R b/R/read_zip.R index ba5642b3..5168d992 100644 --- a/R/read_zip.R +++ b/R/read_zip.R @@ -92,6 +92,12 @@ check_delimiter <- function(file, data_file){ x <- unz(description = file, filename = data_file) |> readr::read_lines(n_max = 1) + if(length(x) < 1){ + c("Supplied file appears to be empty", + i = glue::glue("Zip file: {file}"), + i = glue::glue("Data file: {data_file}")) |> + cli::cli_abort(call = rlang::caller_env()) + } n_tabs <- stringr::str_count(x, "\t") n_commas <- stringr::str_count(x, ",") if(n_tabs > n_commas){ diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 9a22b115..8a2aca2c 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -487,6 +487,30 @@ authentication_supported <- function(atlas){ } } +#' Internal function to test whether authentication is provided by the expected host +#' +#' This is a security check to prevent someone intercepting the request from the network, +#' and replacing it with their own credentials. Apparently this is a 'severe' risk. I am +#' sceptical of that, but it's cheap to implement, so here we are. +#' @noRd +#' @keywords Internal +authentication_host <- function(url, + atlas, + error_call = rlang::caller_env()){ + supplied_host <- url |> + httr2::url_parse() |> + purrr::pluck("hostname") + accepted_host <- switch(atlas, + "Australia" = "auth.ala.org.au", + "Flanders" = "auth.inbo.be") + if(supplied_host != accepted_host){ + c("OAuth request returned unexpected domain", + i = glue::glue("expected: {accepted_host}"), + i = glue::glue("observed: {supplied_host}")) |> + cli::cli_abort(call = error_call) + } +} + #' Internal function to test whether authentication is required #' @noRd #' @keywords Internal diff --git a/tests/testthat/test-authentication.R b/tests/testthat/test-authentication.R index 6e48298c..7f5f6b60 100644 --- a/tests/testthat/test-authentication.R +++ b/tests/testthat/test-authentication.R @@ -1,5 +1,10 @@ quiet_config <- purrr::quietly(galah_config) +test_that("checking authentication host works", { + authentication_host("https://auth.ala.org.au/some/paths/here", "Flanders") |> + expect_error() +}) + test_that("`request_metadata()` works for type = `config`", { skip_if_offline(); skip_on_ci() result <- request_metadata(type = "config") |> @@ -44,7 +49,7 @@ test_that("`authenticate()` works in-pipe for occurrences", { skip("authentication requires interactivity") query <- galah_call() |> - authenticate() |> + authenticate(use_jwt = TRUE) |> identify("Litoria dentata") |> filter(year == 2025) |> compound() From 55b34a6b1bb8233b300db9ce74df55f1ca3e7079 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 28 May 2026 15:23:50 +0200 Subject: [PATCH 36/70] Update specieslists urls to `api.ala.org.au/specieslist/v2` (#300) Minor bug fixes, as yet incomplete. Performance a major issue for `show_all_lists()`, partly improved by changes to parallel api calls, but refactoring data very slow. --- R/capture_metadata.R | 15 +++++---------- R/collapse_metadata.R | 20 ++++++++++++++------ R/collect_metadata.R | 4 +++- R/sysdata.rda | Bin 7390 -> 7392 bytes data-raw/node_config.csv | 4 ++-- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/R/capture_metadata.R b/R/capture_metadata.R index bd0861a8..f3676588 100644 --- a/R/capture_metadata.R +++ b/R/capture_metadata.R @@ -205,16 +205,11 @@ capture_lists <- function(x, url <- url_lookup(x) if(!missing(x)){ url <- url |> httr2::url_parse() - if(!is.null(x$slice)){ - max_value <- x$slice$slice_n - switch(x$atlas, - "Australia" = {url$query <- list(pageSize = x$slice$slice_n)}, - url$query <- list(max = x$slice$slice_n)) - }else{ - switch(x$atlas, - "Australia" = {url$query <- list(pageSize = 1)}, - url$query <- list(max = 10000)) - } + # note: page size is set in `collapse()` rather than here + # as it allows more sensible assessment of pagination requirements + url$query <- switch(x$atlas, + "Australia" = {list(pageSize = 1)}, + list(max = 10000)) url <- httr2::url_build(url) } result <- list(type = x$type, diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index f87990ee..0994421d 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -15,17 +15,25 @@ collapse_lists <- function(.query){ # rationale here is that we run a query to get the number of levels }else if(stringr::str_detect(.query$url, "[:digit:]+$") & .query$atlas == "Australia"){ # first handle case where levels are pre-specified - if(!is.null(x$request$slice)){ - # FIXME: This assumes that n requested is <25, which is a false assumption - # need to explicitly check this number and paginate etc accordingly - as_query(.query) + if(!is.null(.query$request$slice)){ + n_lists <- .query$request$slice$slice_n + # otherwise calculate it }else{ n_lists <- .query |> query_API() |> purrr::pluck("listCount") - n_pages <- ceiling(n_lists * 0.04) # (i.e. 1/25) + } + # calculate urls + if(is.null(n_lists)){ + as_query(.query) + }else{ + n_pages <- ceiling(n_lists * 0.01) # (i.e. 1/100) base_url <- stringr::str_remove(.query$url, "pageSize=1$") - .query$url <- tibble::tibble(url = glue::glue("{base_url}page={seq_len(n_pages)}&pageSize=25")) + if(n_pages < 2){ + .query$url <- tibble::tibble(url = glue::glue("{base_url}pageSize={n_lists}")) + }else{ + .query$url <- tibble::tibble(url = glue::glue("{base_url}page={seq_len(n_pages)}&pageSize=100")) + } as_query(.query) } # below is legacy, probably still important/used, but hard to be sure diff --git a/R/collect_metadata.R b/R/collect_metadata.R index 912723fe..21e2a08d 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -372,7 +372,9 @@ collect_lists <- function(.query){ .progress = potions::pour("package", "verbose", .pkg = "galah") ) |> dplyr::bind_rows() - should_update_cache <- TRUE + should_update_cache <- TRUE # FIXME: this is only true when `slice_head()` ISN'T called + # this isn't even checked at present + # former reliance on tibbles etc is bad practice }else{ lists_slot <- purrr::pluck(result, "lists") # single-list queries that use `filter()` don't have a `lists` slot diff --git a/R/sysdata.rda b/R/sysdata.rda index 9c0cf6f906b471c2dd0859e667166dd95ed05796..4c49d664d15718141d35ba4726c0889e25859a4e 100644 GIT binary patch literal 7392 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 StPfQG@pmLsg$WIslEPq3*lT3~ literal 7390 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 !VrWpxxM~|tkqv$T~7Xz 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 Date: Mon, 1 Jun 2026 16:05:10 +0200 Subject: [PATCH 37/70] Bug fixes to GBIF count queries introduced by changes to `query_API()` --- R/check.R | 8 ++-- R/collapse_checks.R | 4 +- R/collect_occurrences_count.R | 53 ++++++++++-------------- R/query_API.R | 26 +++++++----- tests/testthat/test-authentication.R | 17 ++++---- tests/testthat/test-international-GBIF.R | 15 ++++--- 6 files changed, 64 insertions(+), 59 deletions(-) diff --git a/R/check.R b/R/check.R index 62e27bf6..049a94e8 100644 --- a/R/check.R +++ b/R/check.R @@ -152,10 +152,9 @@ check_fields <- function(.query, i = "Use `search_all(fields)` to find a valid field ID.", x = glue::glue("Can't find field(s) in"), glue::glue(" ", - rlang::format_error_bullets(invalid_fields_message), - call = error_call) + rlang::format_error_bullets(invalid_fields_message)) ) - cli::cli_abort(bullets) + cli::cli_abort(bullets, call = error_call) } } .query @@ -239,7 +238,8 @@ check_fields_gbif_counts <- function(.query){ # noting this differs between using this for it's original purpose (generating counts), # and it's more recent purpose of getting field values group_by_invalid <- NA - if(.query$type == "data/occurrences-count" & !is.null(.query$body$group_by)){ + if(stringr::str_detect(.query$type, "^data/occurrences-count") & # stringr used because type can end in `-groupby` + !is.null(.query$body$group_by)){ facets <- .query$body$group_by$name }else if(.query$type == "metadata/fields-unnest" & !is.null(.query$request$filter$value)){ facets <- .query$request$filter$value[1] diff --git a/R/collapse_checks.R b/R/collapse_checks.R index d13e9842..210f1ce6 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -90,8 +90,8 @@ collapse_run_checks <- function(.query, }else if(stringr::str_detect(.query$type, "-unnest$")){ # FIXME: decide which checks should be subject to `if(potions::pour("package", "run_checks"))` .query <- .query |> - check_identifiers() |> - check_fields() + check_identifiers(error_call) |> + check_fields(error_call) } # add an exception to retain to user-supplied taxon for child taxa queries diff --git a/R/collect_occurrences_count.R b/R/collect_occurrences_count.R index 09d6d353..3c67c1a9 100644 --- a/R/collect_occurrences_count.R +++ b/R/collect_occurrences_count.R @@ -1,16 +1,17 @@ #' `collect()` for `type = "data/occurrences-count"` #' @noRd #' @keywords Internal -collect_occurrences_count <- function(.query){ +collect_occurrences_count <- function(.query, + error_call = rlang::caller_env()){ {if(.query$atlas == "Global"){ - collect_occurrences_count_gbif(.query) + collect_occurrences_count_gbif(.query, error_call = error_call) }else{ collect_occurrences_count_la(.query) }} |> parse_select(.query) } -#' `collect()` for `type = "data/occurrences-count"` for gbif +#' collect a single count query #' @noRd #' @keywords Internal collect_occurrences_count_gbif <- function(.query, @@ -18,41 +19,33 @@ collect_occurrences_count_gbif <- function(.query, # get response from GBIF result <- query_API(.query) - # first handle case when there are multiple queries in a tibble - if(inherits(result, "data.frame")){ - split(result, seq_len(nrow(result))) |> - purrr::map(.f = \(a){ - tibble::tibble( - dplyr::select(a, -"predicate", -"result"), - collect_occurrences_count_gbif_single(a$result[[1]], - error_on_null = FALSE)) - }) |> - dplyr::bind_rows() - # then handle 'simple' queries + # parse results + if(inherits(result, "singlequery")){ + collect_occurrences_count_gbif_single(result) + }else if(inherits(result, "multiquery")){ + # first pull out list of filter-like facets + list1 <- .query$body |> + select(-tidyselect::last_col()) |> + split(f = seq_len(nrow(.query$body))) + # then final facets + list2 <- purrr::map(result, + collect_occurrences_count_gbif_single) + # then bind across 1. lists and 2. rows + purrr::map2(list1, list2, dplyr::bind_cols) |> + dplyr::bind_rows() }else{ - collect_occurrences_count_gbif_single(result, - error_on_null = TRUE) + cli::cli_abort("Unknown result class", call = error_call) } } - -#' collect a single count query + +#' Internal function to pick out facets where needed #' @noRd #' @keywords Internal -collect_occurrences_count_gbif_single <- function(result, - error_on_null = TRUE, - error_call = rlang::caller_env()){ - # handle obvious errors - if(is.null(result$count) & error_on_null){ - cli::cli_abort("API returned a NULL result", - call = error_call) - } - - # parse results +collect_occurrences_count_gbif_single <- function(result){ if(length(result$facets) < 1){ # first handle single values tibble::tibble(count = result$count) }else{ - purrr::map( - purrr::pluck(result, "facets"), + purrr::map(purrr::pluck(result, "facets"), \(a){ df <- a |> purrr::pluck("counts") |> diff --git a/R/query_API.R b/R/query_API.R index 727a8b80..4948bce9 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -80,16 +80,19 @@ query_API_multiple_url <- function(.query, if(all(!is.na(paths))){ # purrr::map(unique(paths), check_directory) # necessary? check_directory(paths[[1]]) # just check first path instead - httr2::req_perform_parallel(requests, - paths = paths, - on_error = "continue", - progress = progress_object) + result <- httr2::req_perform_parallel(requests, + paths = paths, + on_error = "continue", + progress = progress_object) }else{ # if no path, just run all queries result <- httr2::req_perform_parallel(requests, on_error = "continue", progress = progress_object) - purrr::map(result, httr2::resp_body_json) } + + # return multiquery + purrr::map(result, httr2::resp_body_json) |> + structure(class = c("multiquery", "list")) } #' Internal function to run multiple body-based API calls using httr2 @@ -106,11 +109,12 @@ query_API_multiple_body <- function(.query, data_tr$body <- a$predicate[[1]] build_API_call(data_tr) }) - progress_object <- set_progress_bar_behaviour(nrow(.query$url) > 1) + progress_object <- set_progress_bar_behaviour(nrow(.query$body) > 1) result <- httr2::req_perform_parallel(requests, on_error = "continue", progress = progress_object) - purrr::map(result, httr2::resp_body_json) + purrr::map(result, httr2::resp_body_json) |> + structure(class = c("multiquery", "list")) } #' Internal function to run an API call using httr2 @@ -131,12 +135,14 @@ query_API_once <- function(.query, # and can be allowed to fail otherwise }else{ result <- query |> - httr2::req_timeout(seconds = 20) |> + httr2::req_timeout(seconds = 60) |> httr2::req_perform(verbosity = 0) if(grepl("^https://api.gbif.org/v1/occurrence/download/request", .query$url)){ - httr2::resp_body_string(result) + httr2::resp_body_string(result) |> + structure(class = c("singlequery", "list")) }else{ - httr2::resp_body_json(result) # may not work for invalid URLs + httr2::resp_body_json(result)|> # may not work for invalid URLs + structure(class = c("singlequery", "list")) } } } diff --git a/tests/testthat/test-authentication.R b/tests/testthat/test-authentication.R index 7f5f6b60..fedf1589 100644 --- a/tests/testthat/test-authentication.R +++ b/tests/testthat/test-authentication.R @@ -36,12 +36,15 @@ test_that("`authenticate()` works in-pipe for metadata", { is.null(result$request$authenticate) |> expect_false() - result2 <- compound(result) - expect_equal(length(result2), 2) - purrr::map(result2, \(a){a$type}) |> - unlist() |> - expect_equal(c("metadata/config", "metadata/reasons")) + ## NOTE: "metadata/config" no longer appended, unclear why + # result2 <- compound(result) + # expect_equal(length(result2), 2) + # purrr::map(result2, \(a){a$type}) |> + # unlist() |> + # expect_equal(c("metadata/config", "metadata/reasons")) + df <- collect(result) + expect_equal(names(df), c("id", "name")) galah_config(caching = TRUE) }) @@ -66,7 +69,7 @@ test_that("`authenticate()` works in-pipe for occurrences", { y <- compute(x) inherits(y, "computed_query") |> expect_true() - any(names(y) == "authenticate") |> + y |> purrr::pluck("request", "request", "authenticate", "use_jwt") |> expect_true() z <- collect(y) @@ -89,7 +92,7 @@ test_that("setting `authentication` to `TRUE` changes data returned", { expect_equal(length(x_queryset), 5) expect_equal(x_queryset[[1]]$type, "metadata/config") - is.null(x_queryset[[5]]$authenticate) |> + is.null(x_queryset[[5]]$request$authenticate) |> expect_false() # unclear whether it is _critical_ for compound() to source `show_all_config()` here # but some use cases it probably is necessary, and for the others it is diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index 553ee8e4..112783e8 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -37,6 +37,7 @@ test_that("`search_all(fields)` works for GBIF", { test_that("`show_values()` works for GBIF fields", { skip_if_offline(); skip_on_ci() + galah_config(atlas = "GBIF") # query syntax x <- request_metadata() |> filter(fields == "gbifRegion") |> @@ -60,8 +61,10 @@ test_that("`show_values()` works for GBIF fields", { test_that("`show_values()` fails for unsearchable GBIF fields", { skip_if_offline(); skip_on_ci() - search_all(fields, "accessRights") |> - show_values() |> + request_metadata(from = "GBIF") |> + filter(field == "accessRights") |> + unnest() |> + collapse() |> expect_error() }) @@ -288,7 +291,7 @@ test_that("`glimpse()` works with `identify()` for GBIF", { identify("Eolophus roseicapilla") |> filter(year == 2010) |> glimpse() |> - collapse() + collect() expect_s3_class(x, c("occurrences_glimpse", "tbl_df", "tbl", "data.frame")) expect_equal(nrow(x), 3) # number of rows in the tibble quiet_print <- purrr::quietly(print.occurrences_glimpse) @@ -420,9 +423,9 @@ test_that("`distinct()` queries accept `select()` for GBIF", { identify("Crinia") |> select(scientificName, speciesKey) |> collect() - expect_gt(nrow(z), 10) # - expect_equal(ncol(z), 2) - expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) + expect_gt(nrow(x), 10) # + expect_equal(ncol(x), 2) + expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) test_that("`atlas_media()` fails for GBIF", { From ddba12c04860155b4cf4371670ee1f11dc67d3e9 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 2 Jun 2026 13:00:01 +0200 Subject: [PATCH 38/70] Support auto-population of `q` parameter where possible (#301) --- R/build_query.R | 38 ++++++++++++++++++++-------- R/collapse_checks.R | 6 ++--- R/collapse_occurrences_count_atlas.R | 23 +---------------- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/R/build_query.R b/R/build_query.R index b74b370c..3f53a6e8 100644 --- a/R/build_query.R +++ b/R/build_query.R @@ -13,6 +13,7 @@ build_query <- function(identify = NULL, geolocate = NULL, apply_profile = NULL, atlas = NULL) { + # get a taxonomic query if(is.null(identify)) { taxa_query <- NULL } else { # assumes a tibble or data.frame has been given @@ -22,6 +23,7 @@ build_query <- function(identify = NULL, taxa_query <- "`TAXON_PLACEHOLDER`" } } + # validate filters if (is.null(filter)) { filter_query <- NULL @@ -32,12 +34,22 @@ build_query <- function(identify = NULL, if (nrow(filter) == 0) { filter_query <- NULL } else { - queries <- unique(filter$query) - filter_query <- paste0(queries, collapse = " AND ") + filter_query <- unique(filter$query) } } + # merge - query <- list(fq = c(filter_query, taxa_query)) + combined_query <- c(filter_query, taxa_query) + n_queries <- length(combined_query) + if(n_queries < 1L){ + query <- list(q = "*:*") + }else if(n_queries == 1L){ + query <- list(q = combined_query) + }else{ + query <- list(q = combined_query[1], + fq = glue::glue_collapse(combined_query[-1], sep = " AND ")) + } + # geographic stuff if (!is.null(geolocate)) { # if `geolocate` is for a point radius vs polygon/bbox @@ -47,12 +59,14 @@ build_query <- function(identify = NULL, query$lon <- geolocate$lon query$lat <- geolocate$lat query$radius <- geolocate$radius - }else - query$wkt <- geolocate + } else { + query$wkt <- geolocate + } } else { - query$wkt <- geolocate + query$wkt <- geolocate } } + # add profiles information (ALA only) if(profiles_supported(atlas)){ if(!is.null(apply_profile)) { @@ -61,6 +75,8 @@ build_query <- function(identify = NULL, query$disableAllQualityFilters <- "true" } } + + # clean and return build_single_fq(query) } @@ -69,7 +85,7 @@ build_query <- function(identify = NULL, #' @noRd build_single_fq <- function(query){ if(any(names(query) == "fq")){ - # ensure all arguments from galah_filter are enclosed in brackets + # ensure all arguments from `filter()` are enclosed in brackets # EXCEPT for assertions fq <- query$fq missing_brackets <- @@ -79,14 +95,16 @@ build_single_fq <- function(query){ if(any(missing_brackets)){ fq[missing_brackets] <- glue::glue("({fq[missing_brackets]})") } - # add brackets to non-negative AND statements + # add brackets to non-negative 'AND' statements # (adding additional brackets to negative statements breaks them) if(any(!grepl("^-\\(", fq))) { fq_single <- glue::glue_collapse(glue::glue("{fq}"), "AND") } else { - fq_single <- glue::glue_collapse(glue::glue("({fq})"), "AND") + fq_single <- glue::glue_collapse(glue::glue("{fq}"), "AND") } - c(fq = fq_single, query[names(query) != "fq"]) + c(query[names(query) == "q"], + fq = fq_single, + query[!(names(query) %in% c("fq", "q"))]) }else{ query } diff --git a/R/collapse_checks.R b/R/collapse_checks.R index 210f1ce6..424a2ac6 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -58,10 +58,10 @@ collapse_run_checks <- function(.query, } if(.query$type %in% c("data/species") & .query$atlas == "Global"){ # NOTE: require this for occurrences too? Do LA species queries require it? - parse_select_occurrences(.query, build_select_df_from_query(.query)) # OK this works, just need to update select() now + parse_select_occurrences(.query, + build_select_df_from_query(.query)) # OK this works, just need to update select() now # probably best to move this to check_select_GBIF or similar } - # NOTE: the naming convention here is misleading; should probably be `parse_select()` # after checking, for type = "glimpse", we need to rename the fields query if(.query$type == "data/occurrences-glimpse"){ @@ -88,7 +88,6 @@ collapse_run_checks <- function(.query, } # as do `unnest()`/`show_values()` functions }else if(stringr::str_detect(.query$type, "-unnest$")){ - # FIXME: decide which checks should be subject to `if(potions::pour("package", "run_checks"))` .query <- .query |> check_identifiers(error_call) |> check_fields(error_call) @@ -105,6 +104,7 @@ collapse_run_checks <- function(.query, } collapse_remove_metadata(.query) + # special cases: # distributions # if(.query$type == "data/distributions" & diff --git a/R/collapse_occurrences_count_atlas.R b/R/collapse_occurrences_count_atlas.R index 0364580b..4da0b716 100644 --- a/R/collapse_occurrences_count_atlas.R +++ b/R/collapse_occurrences_count_atlas.R @@ -171,6 +171,7 @@ collapse_occurrences_count_atlas_groupby_crossed <- function(.query, check_facet_count <- function(.query, warn = TRUE, error_call = rlang::caller_env()){ + url <- httr2::url_parse(.query$url) current_limit <- url$query$flimit @@ -193,26 +194,4 @@ check_facet_count <- function(.query, }else{ purrr::map(result, function(a){a$count}) |> unlist() } - # if(inherits(result, "data.frame")){ # group_by arg present - # n_available <- result$count - # if(length(n_available) > 1){ # is this correct? What about multiple fields? - # n_available - # }else{ - # n_available <- n_available[[1]] - # if(warn & - # pour("package", "verbose") & - # n_requested < n_available & - # !.query$arrange$slice_called & - # .query$arrange$direction == "descending" # for ascending, n_requested is always zero (TRUE?) - # ){ - # bullets <- c( - # glue("This query will return {n_requested} rows, but there are {n_available} rows in total."), - # i = "Use `slice_head()` to set a higher number of rows to return.") - # inform(bullets) - # } - # n_available - # } - # }else{ - # NA - # } } From 319511483be5b043f126968316df197d75dba2cc Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 2 Jun 2026 14:03:07 +0200 Subject: [PATCH 39/70] Fix minor bug affecting media downloads attempting to parse result as JSON, when this isn't required --- R/query_API.R | 18 ++++++--- tests/testthat/test-international-Austria.R | 4 +- tests/testthat/test-international-Flanders.R | 10 +---- tests/testthat/test-international-Spain.R | 4 +- tests/testthat/test-international-Sweden.R | 41 +++++--------------- 5 files changed, 28 insertions(+), 49 deletions(-) diff --git a/R/query_API.R b/R/query_API.R index 4948bce9..34e64302 100644 --- a/R/query_API.R +++ b/R/query_API.R @@ -57,6 +57,7 @@ build_API_call <- function(.query, #' @keywords Internal query_API_multiple_url <- function(.query, error_call = rlang::caller_env()){ + # get a list of requests requests <- purrr::map(.x = seq_len(nrow(.query$url)), .f = \(a){ @@ -75,8 +76,11 @@ query_API_multiple_url <- function(.query, }) |> unlist() - # first look for paths; if present, download + # set progress bar behaviour + # note this is pretty basic rn progress_object <- set_progress_bar_behaviour(nrow(.query$url) > 1) + + # first look for paths; if present, download if(all(!is.na(paths))){ # purrr::map(unique(paths), check_directory) # necessary? check_directory(paths[[1]]) # just check first path instead @@ -84,15 +88,17 @@ query_API_multiple_url <- function(.query, paths = paths, on_error = "continue", progress = progress_object) - }else{ # if no path, just run all queries + # no `return()` call needed here + + # if no `path`, just run all queries + }else{ result <- httr2::req_perform_parallel(requests, on_error = "continue", progress = progress_object) + # return multiquery + purrr::map(result, httr2::resp_body_json) |> + structure(class = c("multiquery", "list")) } - - # return multiquery - purrr::map(result, httr2::resp_body_json) |> - structure(class = c("multiquery", "list")) } #' Internal function to run multiple body-based API calls using httr2 diff --git a/tests/testthat/test-international-Austria.R b/tests/testthat/test-international-Austria.R index 774a2fae..baa15118 100644 --- a/tests/testthat/test-international-Austria.R +++ b/tests/testthat/test-international-Austria.R @@ -254,7 +254,7 @@ test_that("atlas_media() works for Austria", { send_email = FALSE) x <- request_data() |> identify("Mammalia") |> - filter(year == 2010) |> # !is.na(all_image_url) + filter(year == 2010) |> atlas_media() |> try(silent = TRUE) skip_if(inherits(x, "try-error"), message = "API not available") @@ -267,7 +267,7 @@ test_that("atlas_media() works for Austria", { x <- purrr::quietly(collect_media) x(...)$result } - n_downloads <- 5 + n_downloads <- 3 quiet_media(x[seq_len(n_downloads), ]) expect_equal(length(list.files("temp", pattern = ".jpg$")), n_downloads) diff --git a/tests/testthat/test-international-Flanders.R b/tests/testthat/test-international-Flanders.R index 501e81b3..8fd65dfb 100644 --- a/tests/testthat/test-international-Flanders.R +++ b/tests/testthat/test-international-Flanders.R @@ -297,13 +297,7 @@ test_that("`atlas_media()` works for Flanders", { x <- request_data() |> identify("Vulpes") |> filter(year == 2025, - basisOfRecord == "HUMAN_OBSERVATION", - # month == 6, - !is.na(images) - ) |> - # group_by(year) |> - # count() |> - # collect() + basisOfRecord == "HUMAN_OBSERVATION") |> atlas_media() |> try(silent = TRUE) skip_if(inherits(x, "try-error"), message = "API not available") @@ -316,7 +310,7 @@ test_that("`atlas_media()` works for Flanders", { x <- purrr::quietly(collect_media) x(...)$result } - n_downloads <- 5 + n_downloads <- 3 quiet_media(x[seq_len(n_downloads), ]) expect_equal(length(list.files("temp", pattern = ".jpg$")), n_downloads) diff --git a/tests/testthat/test-international-Spain.R b/tests/testthat/test-international-Spain.R index b1c88804..f9249333 100644 --- a/tests/testthat/test-international-Spain.R +++ b/tests/testthat/test-international-Spain.R @@ -289,7 +289,7 @@ test_that("`atlas_media()` works for Spain", { filter(year >= 2023) |> atlas_media() |> try(silent = TRUE) - skip_if(inherits(x, "try-error"), message = "API not available") # FIXME: failing here + skip_if(inherits(x, "try-error"), message = "API not available") expect_s3_class(x, c("tbl_df", "tbl", "data.frame")) expect_gte(nrow(x), 1) expect_equal(colnames(x)[1:2], @@ -299,7 +299,7 @@ test_that("`atlas_media()` works for Spain", { x <- purrr::quietly(collect_media) x(...)$result } - n_downloads <- 5 + n_downloads <- 3 quiet_media(x[seq_len(n_downloads), ]) expect_equal(length(list.files("temp", pattern = ".jpg$")), n_downloads) diff --git a/tests/testthat/test-international-Sweden.R b/tests/testthat/test-international-Sweden.R index 9a803c20..bf1449bb 100644 --- a/tests/testthat/test-international-Sweden.R +++ b/tests/testthat/test-international-Sweden.R @@ -313,42 +313,21 @@ test_that("collect_media() works for Sweden", { send_email = FALSE) x <- request_data() |> identify("Amphibia") |> - filter(year == 2010, - !is.na(multimedia)) - # get counts - media_count <- x |> - count() |> - collect() |> - try(silent = TRUE) - skip_if(inherits(media_count, "try-error"), message = "API not available") - # get occurrences - media_occ <- x |> - select(group = c("basic", "media")) |> - collect(wait = TRUE) |> - try(silent = TRUE) - skip_if(inherits(media_occ, "try-error"), message = "API not available") - # get metadata - media_meta <- request_metadata() |> - filter(media == unlist(media_occ$images)) |> - collect() |> + filter(year == 2010) |> + atlas_media() |> try(silent = TRUE) - skip_if(inherits(media_meta, "try-error"), message = "API not available") - expect_gt(nrow(media_meta), 0) - # get files - galah_config(directory = "temp") - n_downloads <- 3 - request_files() |> - filter(media == media_meta[seq_len(n_downloads), ]) |> - collect(thumbnail = TRUE) - expect_equal(length(list.files("temp", pattern = ".jpg$")), - n_downloads) - unlink("temp", recursive = TRUE) - # try with collect_media() + skip_if(inherits(x, "try-error"), message = "API not available") # FIXME: failing here + expect_s3_class(x, c("tbl_df", "tbl", "data.frame")) + expect_gte(nrow(x), 1) + expect_equal(colnames(x)[1:2], + c("media_id", "media_type")) + # download a subset quiet_media <- function(...){ x <- purrr::quietly(collect_media) x(...)$result } - quiet_media(media_meta[seq_len(n_downloads), ]) + n_downloads <- 3 + quiet_media(x[seq_len(n_downloads), ]) expect_equal(length(list.files("temp", pattern = ".jpg$")), n_downloads) unlink("temp", recursive = TRUE) From ba3bc4e4f237999b2f1e0c8f95df29450a081d81 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 2 Jun 2026 14:05:27 +0200 Subject: [PATCH 40/70] Update behaviour for lists v2 on ALA - new select headings - adjust tests --- R/collect_metadata.R | 9 +++------ R/utilities_internal.R | 5 ++++- tests/testthat/test-request_metadata_select.R | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/R/collect_metadata.R b/R/collect_metadata.R index 21e2a08d..e4fdbcfe 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -360,10 +360,8 @@ collect_lists <- function(.query){ }else{ # here we run and parse an API call result <- query_API(.query) # this when `url` is a single value or a tibble - # pagination returns long lists - handle using `map()` - # if(length(result) > 1 & is.null(names(result))){ # this old code is risky - # test for pagination requests in .query instead - if(inherits(.query$url, "data.frame")){ + + if(inherits(result, "multiquery")){ # FIXME: haven't checked whether this breaks other living atlases result_df <- purrr::map(result, \(a){a |> purrr::pluck("lists") |> @@ -380,8 +378,7 @@ collect_lists <- function(.query){ # single-list queries that use `filter()` don't have a `lists` slot if(is.null(lists_slot)){ result_df <- result |> - make_nulls_safe() |> - tibble::as_tibble() + tidy_list_columns() # but some queries do }else{ result_df <- lists_slot |> diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 8a2aca2c..4a5d998b 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -108,14 +108,17 @@ lookup_select_columns <- function(type) { "url"), "lists" = c("species_list_uid", "list_name", + "title", # ALA v2 "description", "list_type", "item_count", + "row_count", # ALA v2 "is_authoritative", "is_threatened"), "lists-unnest" = c("scientific_name", "vernacular_name", - "taxon_concept_id"), + "taxon_concept_id", + "taxon_id"), # ALA v2 "media" = c("media_id", "occurrence_id", "creator", diff --git a/tests/testthat/test-request_metadata_select.R b/tests/testthat/test-request_metadata_select.R index 8c7cd8de..060f4715 100644 --- a/tests/testthat/test-request_metadata_select.R +++ b/tests/testthat/test-request_metadata_select.R @@ -158,9 +158,18 @@ test_that("`request_metadata()` works with `select()` for remote APIs *with* def result_everything |> ncol() |> expect_gt(expected_n) - result_everything |> - colnames() |> - expect_contains(expected_columns) + + # lists contains some entries for different atlases; so not all are present together + if(a == "lists"){ + which(colnames(result_everything) %in% expected_columns) |> + length() |> + expect_equal(7) + }else{ + result_everything |> + colnames() |> + expect_contains(expected_columns) + } + result_cached <- retrieve_cache(a) expect_equal(colnames(result_everything), colnames(result_cached)) From 60d761626a0bfb5fad6a80e62dfcb28981e3c1a2 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Thu, 4 Jun 2026 13:14:35 +0200 Subject: [PATCH 41/70] Update tests to work adequately with new specieslist API (#300) --- R/collapse_metadata.R | 1 + R/collect_metadata.R | 7 ++-- tests/testthat/test-request_metadata_select.R | 36 ++++++++++++------- tests/testthat/test-search_all.R | 16 +++++++++ tests/testthat/test-show_all.R | 26 ++++---------- tests/testthat/test-show_values.R | 22 +----------- 6 files changed, 54 insertions(+), 54 deletions(-) diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index 0994421d..b34e9ec2 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -9,6 +9,7 @@ collapse_lists <- function(.query){ as_query(.query) }else if(inherits(.query$url, "tbl_df")){ as_query(.query) + # in non-Australian atlases, ending in a digit means that pageSize or similar is set already }else if(stringr::str_detect(.query$url, "[:digit:]+$") & .query$atlas != "Australia"){ as_query(.query) # set up 'new' lists code (May 2026) diff --git a/R/collect_metadata.R b/R/collect_metadata.R index e4fdbcfe..997a3ffb 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -378,10 +378,12 @@ collect_lists <- function(.query){ # single-list queries that use `filter()` don't have a `lists` slot if(is.null(lists_slot)){ result_df <- result |> - tidy_list_columns() + purrr::map(tidy_list_columns) |> + dplyr::bind_rows() # but some queries do }else{ result_df <- lists_slot |> + purrr::map(tidy_list_columns) |> dplyr::bind_rows() should_update_cache <- TRUE } @@ -395,7 +397,8 @@ collect_lists <- function(.query){ update_attributes(type = "lists", atlas = .query$atlas) - if(should_update_cache){ + if(should_update_cache & + potions::pour("package", "caching", .pkg = "galah")){ update_cache(lists = result_df) } } diff --git a/tests/testthat/test-request_metadata_select.R b/tests/testthat/test-request_metadata_select.R index 060f4715..7b451cb0 100644 --- a/tests/testthat/test-request_metadata_select.R +++ b/tests/testthat/test-request_metadata_select.R @@ -106,7 +106,6 @@ test_that("`request_metadata()` works with `select()` for remote APIs *with* def type_list <- c("assertions", "fields", "licences", - "lists", "profiles", "reasons") reset_cache() @@ -158,17 +157,9 @@ test_that("`request_metadata()` works with `select()` for remote APIs *with* def result_everything |> ncol() |> expect_gt(expected_n) - - # lists contains some entries for different atlases; so not all are present together - if(a == "lists"){ - which(colnames(result_everything) %in% expected_columns) |> - length() |> - expect_equal(7) - }else{ - result_everything |> - colnames() |> - expect_contains(expected_columns) - } + result_everything |> + colnames() |> + expect_contains(expected_columns) result_cached <- retrieve_cache(a) expect_equal(colnames(result_everything), @@ -182,6 +173,27 @@ test_that("`request_metadata()` works with `select()` for remote APIs *with* def }) }) +# implement a smaller test for lists, given how slow it is +test_that("`request_metdata()` works with `select()` for `type = 'lists'`", { + skip_if_offline(); skip_on_ci() + x <- request_metadata(type = "lists") |> + slice_head(n = 10) |> + collect() + + # lists contains some entries for different atlases; so not all are present together + expected_columns <- lookup_select_columns("lists") + which(colnames(x) %in% expected_columns) |> + length() |> + expect_equal(7) + + y <- request_metadata(type = "lists") |> + slice_head(n = 10) |> + select(everything()) |> + collect() + + expect_gt(ncol(y), ncol(x)) +}) + test_that("`request_metdata()` works with `select()` for `type = 'taxa'`", { skip_if_offline(); skip_on_ci() diff --git a/tests/testthat/test-search_all.R b/tests/testthat/test-search_all.R index e8370c33..f7407c13 100644 --- a/tests/testthat/test-search_all.R +++ b/tests/testthat/test-search_all.R @@ -163,8 +163,24 @@ test_that("`search_licenses()` returns a filtered result", { expect_true() }) +# implement a smaller test for search_lists() to make tests faster +# this relies on a 'fake' query to `show_all_lists()` for testing purposes +# this is bad practice in production, but useful here test_that("`search_lists()` returns a filtered result", { skip_if_offline(); skip_on_ci() + + # manually override cached lists with something (much) smaller, + # but still contains the term 'threatened'. + galah_config(caching = FALSE) + x <- request_metadata("lists") |> + slice_head(n = 10) |> + collapse() + x$url <- "https://api.ala.org.au/specieslist/v2/speciesList?isAuthoritative=true&pageSize=10" + y <- collect(x) + galah_config(caching = TRUE) + update_cache(lists = y) + + # now resume earlier code search_string <- "threatened" quiet_lists_search <- purrr::quietly(search_lists) result <- quiet_lists_search(search_string) |> diff --git a/tests/testthat/test-show_all.R b/tests/testthat/test-show_all.R index 8ea4d0b6..b6320607 100644 --- a/tests/testthat/test-show_all.R +++ b/tests/testthat/test-show_all.R @@ -51,23 +51,11 @@ test_that("`show_all_lists()` functions work correctly", { list_fun(...) |> purrr::pluck("result") } - syntax1 <- quiet_lists() - limit_test <- quiet_lists(limit = 3) - expect_s3_class(syntax1, c("tbl_df", "tbl", "data.frame")) - expect_equal(attributes(syntax1)$call, "lists") - expect_equal(attributes(syntax1)$region, "Australia") - expect_equal(nrow(limit_test), 3) -}) - -test_that("show_all(lists) returns additional metadata columns", { - skip_if_offline(); skip_on_ci() - quiet_lists <- function(...){ - list_fun <- purrr::quietly(show_all_lists) - list_fun(...) |> - purrr::pluck("result") - } - syntax1 <- quiet_lists() - limit_test <- quiet_lists(limit = 3) + result <- quiet_lists(limit = 3) + expect_s3_class(result, c("tbl_df", "tbl", "data.frame")) + expect_equal(attributes(result)$call, "lists") + expect_equal(attributes(result)$region, "Australia") + expect_equal(nrow(result), 3) expected_cols <- c("is_authoritative", "is_threatened") - expect_in(expected_cols, colnames(limit_test)) -}) + expect_in(expected_cols, colnames(result)) +}) \ No newline at end of file diff --git a/tests/testthat/test-show_values.R b/tests/testthat/test-show_values.R index 710c2ab3..8f708d0e 100644 --- a/tests/testthat/test-show_values.R +++ b/tests/testthat/test-show_values.R @@ -36,13 +36,7 @@ test_that("`show_values()` accepts search & show_all inputs from profiles", { expect_gt(nrow(values_search), 0) }) -test_that("`show_values()` accepts search & show_all inputs from lists", { - skip_if_offline(); skip_on_ci() - values_search <- search_all(lists, "dr650") |> - quiet_values() - expect_s3_class(values_search, c("tbl_df", "tbl", "data.frame")) - expect_gt(nrow(values_search), 0) -}) +## NOTE: search_lists() tests removed 2026-06-04 because v2 specieslist API has become too slow to test adequately test_that("`search_values()` returns helpful error when missing query", { skip_if_offline(); skip_on_ci() @@ -78,20 +72,6 @@ test_that("`search_values()` returns filtered results for profiles", { expect_true(search_result_check) }) -test_that("`search_values()` returns filtered results for lists", { - skip_if_offline(); skip_on_ci() - base_df <- search_all(lists, "dr650") - values_search <- base_df |> quiet_search("frog") - values_show <- base_df |> quiet_values() - search_result_check <- all(grepl(pattern = "frog", - paste(values_search$vernacular_name, values_search$scientific_name), - ignore.case = TRUE)) - expect_s3_class(values_search, c("tbl_df", "tbl", "data.frame")) - expect_equal(names(values_search), names(values_show)) - expect_lt(nrow(values_search), nrow(values_show)) - expect_true(search_result_check) -}) - test_that("`show_values()` & `search_values()` return number of matched fields", { skip_if_offline(); skip_on_ci() search1 <- search_fields("year") From 498857573b6405b72dad9f40dbedfb7ea5d82c47 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Mon, 29 Jun 2026 20:51:55 +0100 Subject: [PATCH 42/70] Minor changes to tests, news --- NEWS.md | 14 ++- tests/testthat/_snaps/print.new.md | 116 ++++++++++++++++++ .../test-international-GBIF-predicates.R | 5 +- tests/testthat/test-international-Portugal.R | 8 +- 4 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 tests/testthat/_snaps/print.new.md diff --git a/NEWS.md b/NEWS.md index fca1533e..76214b31 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,15 +1,19 @@ # galah 2.3.0 ### New functions and options -* `request_data()` and friends gain a `from` argument, allowing in-pipe atlas specification (#297). -* `authenticate()` gains arguments to allow in-pipe email- or JWT-based authentication. Support added for JWT-based authentication on VBP (#296). * New function `describe()` to check what fields are available (#291). +* `authenticate()` gains arguments to allow in-pipe email- or JWT-based authentication. Support added for JWT-based authentication on VBP (#296). +* `request_data()` and friends gain a `from` argument, allowing in-pipe atlas specification (#297). * New names for spatial functions: `geolocate_polygon()`, `geolocate_bbox()` and `geolocate_radius()`. -* Now possible to call `select()` on GBIF occurrence queries. +* Now possible to call `select()` on GBIF occurrence queries, though they are applied post-hoc. -### Bug fixes +### Bug fixes & performance improvements * `isAuthoritative` and `isThreatened` returned by default by `search_all()` (#293). -* Calling functions using `pkg::fun()` syntax work without first calling `library(galah)` (#294). +* Calling functions using `pkg::fun()` syntax work without first calling `library(galah)` (#294, #298). +* `query_API()` now uses `cli::cli_progress_step()`, `httr2::req_retry()` and `httr::req_perform_parallel()` for improved performance. Timeout is now 60 seconds, rather than 20. +* Misc bug fixes for `atlas_occurrences()`, `glimpse()` and `show_values()` for GBIF (#299). +* Upgraded to v2 of specieslist API for ALA (#300). +* `galah` now auto-populates the `q` parameter for living atlas API calls for server-side performance reasons (#301). ### Lifecycle * `galah_` prefix functions for piping formally superseded by their `dplyr` alternatives. diff --git a/tests/testthat/_snaps/print.new.md b/tests/testthat/_snaps/print.new.md new file mode 100644 index 00000000..e58d1678 --- /dev/null +++ b/tests/testthat/_snaps/print.new.md @@ -0,0 +1,116 @@ +# object of class `data-request` formats correctly + + Code + request_data() + Message + Object of type data_request containing: + * type occurrences + * atlas Australia + +# populated `data_request()` prints correctly + + Code + filter(identify(galah_call(), "crinia"), year == 2025) + Message + Object of type data_request containing: + * type occurrences + * atlas Australia + * identify search_term: crinia + * filter Object of class `data_filter`: year == 2025 + +# object of class `metadata-request` formats correctly + + Code + request_data() + Message + Object of type data_request containing: + * type occurrences + * atlas Australia + +# object of class `metadata-request` formats correctly with `filter() + + Code + filter(request_metadata(), list == "dr650") + Message + Object of type metadata_request containing: + * type lists + * atlas Australia + * filter Object of class `metadata_filter` with type `list` + +# object of class `metadata-request` formats correctly with `identify() + + Code + identify(request_metadata(), "Crinia") + Message + Object of type metadata_request containing: + * type taxa + * atlas Australia + * identify search_term: Crinia + +# object of class `metadata-request` formats correctly with `identify() |> unnest() + + Code + unnest(identify(request_metadata(), "Crinia")) + Message + Object of type metadata_request containing: + * type taxa-unnest + * atlas Australia + * identify search_term: Crinia + +# object of class `query` formats correctly + + Code + capture(identify(request_metadata(), "Crinia")) + Message + Object of class query with type metadata/taxa-single + * atlas: Australia + * url: https://api.ala.org.au/namematching/api/search?q=Crinia + +# object of class `computed_query` formats correctly + + Code + x + Message + Object of class computed query with type metadata/taxa-single + * atlas: Australia + * url: https://api.ala.org.au/namematching/api/search?q=Crinia + +# object of class `query_set` formats correctly + + Code + compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), + basisOfRecord == "HUMAN_OBSERVATION")) + Message + Object of class query_set containing 5 queries: + * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig + * metadata/fields url: https://api.ala.org.au/occurrences/index/fields + * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes + * metadata/reasons url: https://api.ala.org.au/logger/service/logger/reasons + * data/occurrences url: + https://api.ala.org.au/occurrences/occurrences/offline/... + +# `galah_config()` formats correctly + + Code + galah_config() + Message + `galah` package configuration + + Atlas + Atlas of Living Australia (ALA), Australia + + User + x authentication + username [Provided] + email ala4r@ala.org.au + password [Provided] + download_reason_id 10 + + Package + x verbose + v run_checks + x send_email + v caching + i directory: something + + diff --git a/tests/testthat/test-international-GBIF-predicates.R b/tests/testthat/test-international-GBIF-predicates.R index 99cbfaee..b5fb020d 100644 --- a/tests/testthat/test-international-GBIF-predicates.R +++ b/tests/testthat/test-international-GBIF-predicates.R @@ -131,7 +131,10 @@ test_that("`filter()` handles `OR` and `%in%` for GBIF", { filter(basisOfRecord %in% c("HUMAN_OBSERVATION", "PRESERVED_SPECIMEN")) |> count() |> collect() - expect_equal(y$count, z$count) + + # check difference is <1% + # sometimes get slight bugs with code like this due to server-side estimation error + expect_lte(abs(y$count - z$count) / y$count, 0.01) }) test_that("`filter()` handles multiple queries including != for GBIF", { diff --git a/tests/testthat/test-international-Portugal.R b/tests/testthat/test-international-Portugal.R index 17a8bd2b..44c5b44d 100644 --- a/tests/testthat/test-international-Portugal.R +++ b/tests/testthat/test-international-Portugal.R @@ -100,7 +100,7 @@ test_that("show_values works for fields for Portugal", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -test_that("atlas_counts works for Portugal", { +test_that("`atlas_counts()` works for Portugal", { skip_if_offline(); skip_on_ci() x <- atlas_counts() |> dplyr::pull(count) |> @@ -109,7 +109,7 @@ test_that("atlas_counts works for Portugal", { expect_gt(x, 0) }) -test_that("atlas_counts works with type = 'species' for Portugal", { +test_that("`atlas_counts()` works with type = 'species' for Portugal", { skip_if_offline(); skip_on_ci() x <- atlas_counts(type = "species") |> dplyr::pull(count) |> @@ -118,7 +118,7 @@ test_that("atlas_counts works with type = 'species' for Portugal", { expect_gt(x, 0) }) -test_that("`atlas_counts()` works with galah_identify for Portugal", { +test_that("`count()` works with `identify()` for Portugal", { skip_if_offline(); skip_on_ci() result <- galah_call() |> identify("Mammalia") |> @@ -138,7 +138,7 @@ test_that("`atlas_counts()` works with galah_identify for Portugal", { 0.1) # i.e. <1% margin of error }) -test_that("`atlas_counts()` works with group_by for Portugal", { +test_that("`count()` works with `group_by()` for Portugal", { skip_if_offline(); skip_on_ci() result <- galah_call() |> filter(year >= 2000) |> From 3406aec8981d8eaa38231442f8c75c76d5a10e47 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:33:24 +1000 Subject: [PATCH 43/70] Fix v2 list output and pagination, fix `show_values()` #300 * Fixes pagination for v2 lists * Trims number of required queries for `show_all_lists()` by increasing pageSize from 100 to 1000 * Fixes `show_values()` and adds lots of unnesting * Adds new message about duplicated rows in `show_values()` --- R/capture_metadata.R | 6 +- R/capture_metadata_unnest.R | 41 ++++++++++-- R/collapse_metadata.R | 4 +- R/collect_metadata_unnest.R | 123 +++++++++++++++++++++++++++++++++--- R/utilities_internal.R | 2 +- 5 files changed, 155 insertions(+), 21 deletions(-) diff --git a/R/capture_metadata.R b/R/capture_metadata.R index f3676588..611b9970 100644 --- a/R/capture_metadata.R +++ b/R/capture_metadata.R @@ -195,8 +195,8 @@ capture_lists <- function(x, headers = build_headers()) } }else{ - cli::cli_abort(c("`filter()` arguments to `lists` only accept a data resource number", - i = "e.g. request_metadata() |> filter(lists == 'dr656')"), + cli::cli_abort(c("`filter()` arguments to `lists` only accept a data resource number.", + i = "e.g. {.code request_metadata() |> filter(lists == 'dr656')}"), call = error_call) } # if filter isn't supplied, check cache etc @@ -208,7 +208,7 @@ capture_lists <- function(x, # note: page size is set in `collapse()` rather than here # as it allows more sensible assessment of pagination requirements url$query <- switch(x$atlas, - "Australia" = {list(pageSize = 1)}, + "Australia" = {list(pageSize = 1)}, # placeholder list(max = 10000)) url <- httr2::url_build(url) } diff --git a/R/capture_metadata_unnest.R b/R/capture_metadata_unnest.R index aa4c9d58..708b85d8 100644 --- a/R/capture_metadata_unnest.R +++ b/R/capture_metadata_unnest.R @@ -30,14 +30,45 @@ capture_lists_unnest <- function(.query){ url <- url_lookup(.query, list_id = .query$filter$value[1]) |> httr2::url_parse() + browser() # set a default query - url$query <- list(max = -1, # remove max limit - includeKVP = TRUE) # add name & status columns + url$query <- list( + pageSize = 30000, # 30000 is the max, a few lists have >30000 items + includeKVP = TRUE # add name & status columns + ) + # create object - list(type = .query$type, + x <- list(type = .query$type, atlas = .query$atlas, - url = httr2::url_build(url)) |> - as_query() + url = httr2::url_build(url)) + + # return lists longer than max pageSize + lists_gt_max_page <- retrieve_cache("lists") |> + dplyr::filter(row_count > 30000) |> + dplyr::pull(species_list_uid) + + # if list is long, add more queries to match required page number for list row_count + if(.query$filter$value %in% lists_gt_max_page) { + # browser() + row_count <- retrieve_cache("lists") |> + dplyr::filter(species_list_uid == .query$filter$value) |> + dplyr::pull(row_count) + n_pages <- ceiling(row_count * (1/30000)) # (i.e. 1/1000) + + # add additional urls to reach required number of pages to return all items + x$url <- tibble::tibble(url = glue::glue("{x$url}&page={seq_len(n_pages)}")) + } + # browser() + # create query + x |> as_query() + + # I thought this would work but it doesn't + + # create object + # list(type = .query$type, + # atlas = .query$atlas, + # url = httr2::url_build(url)) |> + # as_query() } #' Internal function to run `capture()` for diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index b34e9ec2..f5430dba 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -28,12 +28,12 @@ collapse_lists <- function(.query){ if(is.null(n_lists)){ as_query(.query) }else{ - n_pages <- ceiling(n_lists * 0.01) # (i.e. 1/100) + n_pages <- ceiling(n_lists * 0.001) # (i.e. 1/1000) Note that 1000 is the maximum base_url <- stringr::str_remove(.query$url, "pageSize=1$") if(n_pages < 2){ .query$url <- tibble::tibble(url = glue::glue("{base_url}pageSize={n_lists}")) }else{ - .query$url <- tibble::tibble(url = glue::glue("{base_url}page={seq_len(n_pages)}&pageSize=100")) + .query$url <- tibble::tibble(url = glue::glue("{base_url}page={seq_len(n_pages)}&pageSize=1000")) } as_query(.query) } diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index bf258bb7..f371c714 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -89,21 +89,124 @@ collect_lists_unnest <- function(.query){ } df } - + # get data result <- query_API(.query) + + # browser() + + if(stringr::str_detect(.query$url, "/v1/")) { # version 1 API + # old + # process + result |> + purrr::list_transpose() |> + tibble::as_tibble() |> + clean_common_names() |> + clean_kvp_values() |> + dplyr::rename_with(camel_to_snake_case) |> + parse_rename(.query) |> + parse_select(.query) + } else { + # new + # process + + # first, add `classification` which captures ALA-matched information + x <- result |> + purrr::list_transpose() |> + tibble::as_tibble() |> + clean_common_names() |> + parse_classification() |> + dplyr::rename_with(camel_to_snake_case) |> + parse_rename(.query) + + # second, `properties` contains status information and other raw fields + raw_columns <- x |> + select(taxon_concept_id, supplied_name, scientific_name, properties) |> + + tidyr::unnest(cols = properties) |> + tidyr::unnest_wider(properties) |> + dplyr::mutate(key = camel_to_snake_case(key)) |> + dplyr::mutate(key = dplyr::if_else(key %in% colnames(x), glue::glue("{key}_raw"), key)) |> # rename prior to pivot to avoid name conflicts + tidyr::pivot_wider(names_from = "key", + values_from = "value", + names_repair = "minimal", + values_fn = list) |> + tidyr::unnest(cols = everything()) + + # merge + result_final <- x |> + dplyr::left_join(raw_columns, + dplyr::join_by(taxon_concept_id, supplied_name, scientific_name)) |> + dplyr::select(-properties) |> + parse_rename(.query) + # parse_select(.query) # this is limiting output + + # inform user about duplicated taxon_concept_ids + duplicate_taxa <- result_final |> filter(dplyr::n() > 1, .by = taxon_concept_id) |> distinct(taxon_concept_id) |> nrow() + bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", + "i" = "This happens because {.field taxon_concept_id} can match to multiple {.field supplied_name} values.", + "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") + cli::cli_warn(bullets) + + return(result_final) + } - # process - result |> - purrr::list_transpose() |> - tibble::as_tibble() |> - clean_common_names() |> - clean_kvp_values() |> - dplyr::rename_with(camel_to_snake_case) |> - parse_rename(.query) |> - parse_select(.query) } +clean_raw_fields <- function(df){ + + if(any(colnames(df) == "properties")){ + if(any(lengths(df$properties) > 0)){ + df <- df |> + tidyr::unnest(cols = properties) |> + tidyr::unnest_wider(properties) |> + tidyr::pivot_wider(names_from = "key", + values_from = "value", + names_repair = "minimal") + } + } + df + +} + +parse_classification <- function(df){ + + if(any(colnames(df) == "classification")){ + df <- df |> + tidyr::unnest_wider(classification, + names_repair = "minimal", + names_sep = "_") |> + # select ALA-matched columns + select(speciesListID, + classification_taxonConceptID, + suppliedName, + # scientific_name, + classification_scientificName, # ALA-matched name + classification_scientificNameAuthorship, + classification_vernacularName, + classification_rank, + classification_kingdom, + classification_phylum, + classification_class, + classification_order, + classification_family, + classification_genus, + classification_species, + properties # keep properties for raw fields + ) |> + # remove prefix + dplyr::rename_with( + ~ stringr::str_remove(., "classification_"), + classification_taxonConceptID:classification_species + ) + } + df + + +} + + + #' Internal function to run `compute()` for #' `request_metadata(type = "profiles") |> unnest()` #' @noRd diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 4a5d998b..6c0872ba 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -34,7 +34,7 @@ parse_select <- function(df, } #' equivalent to `parse_select()` but for filter -#' mainly called for delayed filter arugments on APIs that don't support `q` +#' mainly called for delayed filter arguments on APIs that don't support `q` #' @noRd #' @keywords Internal parse_filter <- function(df, query){ From cb9c536ee50460f879a12749b06ee6c7cc4d90e2 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:24:54 +1000 Subject: [PATCH 44/70] Re-add `species_list_uid` to `show_values()` result #300 --- R/capture_metadata_unnest.R | 6 ++---- R/collect_metadata_unnest.R | 33 +++++++++------------------------ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/R/capture_metadata_unnest.R b/R/capture_metadata_unnest.R index 708b85d8..115ea486 100644 --- a/R/capture_metadata_unnest.R +++ b/R/capture_metadata_unnest.R @@ -30,7 +30,7 @@ capture_lists_unnest <- function(.query){ url <- url_lookup(.query, list_id = .query$filter$value[1]) |> httr2::url_parse() - browser() + # set a default query url$query <- list( pageSize = 30000, # 30000 is the max, a few lists have >30000 items @@ -49,7 +49,6 @@ capture_lists_unnest <- function(.query){ # if list is long, add more queries to match required page number for list row_count if(.query$filter$value %in% lists_gt_max_page) { - # browser() row_count <- retrieve_cache("lists") |> dplyr::filter(species_list_uid == .query$filter$value) |> dplyr::pull(row_count) @@ -58,12 +57,11 @@ capture_lists_unnest <- function(.query){ # add additional urls to reach required number of pages to return all items x$url <- tibble::tibble(url = glue::glue("{x$url}&page={seq_len(n_pages)}")) } - # browser() # create query x |> as_query() - # I thought this would work but it doesn't + # old code # create object # list(type = .query$type, # atlas = .query$atlas, diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index f371c714..794377d3 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -93,11 +93,8 @@ collect_lists_unnest <- function(.query){ # get data result <- query_API(.query) - # browser() - if(stringr::str_detect(.query$url, "/v1/")) { # version 1 API # old - # process result |> purrr::list_transpose() |> tibble::as_tibble() |> @@ -108,13 +105,14 @@ collect_lists_unnest <- function(.query){ parse_select(.query) } else { # new - # process - # first, add `classification` which captures ALA-matched information x <- result |> purrr::list_transpose() |> tibble::as_tibble() |> clean_common_names() |> + # species_list_uid is no longer retained in v2 API results. + # this adds info again as column (which matches old v1 output) + dplyr::mutate(species_list_uid = .query$request$filter$value) |> parse_classification() |> dplyr::rename_with(camel_to_snake_case) |> parse_rename(.query) @@ -122,7 +120,6 @@ collect_lists_unnest <- function(.query){ # second, `properties` contains status information and other raw fields raw_columns <- x |> select(taxon_concept_id, supplied_name, scientific_name, properties) |> - tidyr::unnest(cols = properties) |> tidyr::unnest_wider(properties) |> dplyr::mutate(key = camel_to_snake_case(key)) |> @@ -139,7 +136,7 @@ collect_lists_unnest <- function(.query){ dplyr::join_by(taxon_concept_id, supplied_name, scientific_name)) |> dplyr::select(-properties) |> parse_rename(.query) - # parse_select(.query) # this is limiting output + # parse_select(.query) # FIXME: this is limiting output in an unexpected way # inform user about duplicated taxon_concept_ids duplicate_taxa <- result_final |> filter(dplyr::n() > 1, .by = taxon_concept_id) |> distinct(taxon_concept_id) |> nrow() @@ -153,22 +150,10 @@ collect_lists_unnest <- function(.query){ } -clean_raw_fields <- function(df){ - - if(any(colnames(df) == "properties")){ - if(any(lengths(df$properties) > 0)){ - df <- df |> - tidyr::unnest(cols = properties) |> - tidyr::unnest_wider(properties) |> - tidyr::pivot_wider(names_from = "key", - values_from = "value", - names_repair = "minimal") - } - } - df - -} - +#' Internal function to parse ALA-matched taxonomic information of species lists +#' via `show_values()` / `request_metadata(type = "lists") |> unnest()` +#' @noRd +#' @keywords Internal parse_classification <- function(df){ if(any(colnames(df) == "classification")){ @@ -177,7 +162,7 @@ parse_classification <- function(df){ names_repair = "minimal", names_sep = "_") |> # select ALA-matched columns - select(speciesListID, + select(species_list_uid, classification_taxonConceptID, suppliedName, # scientific_name, From d97400bbc04a745852f934426e8a344c37f63c72 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:33:06 +1000 Subject: [PATCH 45/70] Fix queries that use `geolocate()` #301 * follows auto-populating `q` format --- R/build_query.R | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/R/build_query.R b/R/build_query.R index 3f53a6e8..55ceacdc 100644 --- a/R/build_query.R +++ b/R/build_query.R @@ -37,19 +37,25 @@ build_query <- function(identify = NULL, filter_query <- unique(filter$query) } } - + # merge combined_query <- c(filter_query, taxa_query) n_queries <- length(combined_query) if(n_queries < 1L){ query <- list(q = "*:*") - }else if(n_queries == 1L){ + }else if(n_queries == 1L & is.null(geolocate)){ query <- list(q = combined_query) - }else{ + }else if(n_queries > 1L & is.null(geolocate)){ + query <- list(q = combined_query[1], + fq = glue::glue_collapse(combined_query[-1], sep = " AND ")) + } else if(n_queries == 1L & !is.null(geolocate)) { + # single taxa queries must be formatted using fq format if geolocate is present + query <- list(q = glue::glue_collapse(combined_query[1], sep = " AND ")) + } else { query <- list(q = combined_query[1], fq = glue::glue_collapse(combined_query[-1], sep = " AND ")) } - + # geographic stuff if (!is.null(geolocate)) { # if `geolocate` is for a point radius vs polygon/bbox From 6e8e48ead21a15b4b1f17c2792cd898bd1e59d70 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:08:23 +1000 Subject: [PATCH 46/70] Add CRS warning to `galah_polygon()` #302 --- R/geolocate_polygon.R | 12 ++++++++++++ tests/testthat/test-geolocate_polygon.R | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/R/geolocate_polygon.R b/R/geolocate_polygon.R index 9a0a7ca8..e85bb35b 100644 --- a/R/geolocate_polygon.R +++ b/R/geolocate_polygon.R @@ -91,6 +91,18 @@ parse_polygon <- function(query, cli::cli_abort(call = error_call) } + # check whether crs of polygon is epsg:4326 + crs <- st_crs(query)$epsg + if(crs != 4326) { + selected_atlas <- potions::pour("atlas")$acronym + c("Spatial object CRS is not WGS 84 (EPSG:4326).", + i = "Results of this query may be incorrect because polygon uses different CRS to data.", + x = "Occurrence data in {selected_atlas} uses EPSG:4326, not EPSG:{crs}." # NOTE: All supported LAs use EPSG:4326 + ) |> + cli::cli_warn() + } + # browser() + # currently a bug where the ALA doesn't accept some polygons # to avoid any issues, any polygons are converted to multipolygons if(inherits(query, "sf") || inherits(query, "sfc")) { diff --git a/tests/testthat/test-geolocate_polygon.R b/tests/testthat/test-geolocate_polygon.R index 3a6cae83..12f87b08 100644 --- a/tests/testthat/test-geolocate_polygon.R +++ b/tests/testthat/test-geolocate_polygon.R @@ -88,5 +88,12 @@ test_that("`geolocate_polygon()` counts n vertices correctly", { expect_equal(n_points(sf_wkt), 4) }) +test_that("`geolocate_polygon()` warns when CRS isn't EPSG:4326", { + wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" + obj_sf_gda94 <- obj_sf |> st_transform(crs = 4283) # wrong GDA94 + + expect_warning(geolocate(obj_sf_gda94), "Polygon CRS") +}) + # Future: test that geolocate_polygon accepts nothing as an input and ignores \ No newline at end of file From 35013998a9ee0015663093f3291d246412418999 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:26:06 +1000 Subject: [PATCH 47/70] Add CRS test to `geolocate_bbox()` #302 * Create `check_crs()` function in utilities_internal.R * Allows consistent check in both polygon & bbox functions --- R/geolocate_bbox.R | 5 +++++ R/geolocate_polygon.R | 12 +----------- R/utilities_internal.R | 17 +++++++++++++++++ tests/testthat/test-geolocate_bbox.R | 6 ++++++ tests/testthat/test-geolocate_polygon.R | 2 +- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/R/geolocate_bbox.R b/R/geolocate_bbox.R index 8ad188d1..88c38343 100644 --- a/R/geolocate_bbox.R +++ b/R/geolocate_bbox.R @@ -66,10 +66,15 @@ geolocate_bbox <- function(...) { log <<- cnd "" }) + check_crs(query) # check whether crs is epsg:4326 } else { valid <- query |> sf::st_is_valid() + + check_crs(query) # check whether crs is epsg:4326 + + } if (valid != TRUE) { diff --git a/R/geolocate_polygon.R b/R/geolocate_polygon.R index e85bb35b..aad486c6 100644 --- a/R/geolocate_polygon.R +++ b/R/geolocate_polygon.R @@ -91,17 +91,7 @@ parse_polygon <- function(query, cli::cli_abort(call = error_call) } - # check whether crs of polygon is epsg:4326 - crs <- st_crs(query)$epsg - if(crs != 4326) { - selected_atlas <- potions::pour("atlas")$acronym - c("Spatial object CRS is not WGS 84 (EPSG:4326).", - i = "Results of this query may be incorrect because polygon uses different CRS to data.", - x = "Occurrence data in {selected_atlas} uses EPSG:4326, not EPSG:{crs}." # NOTE: All supported LAs use EPSG:4326 - ) |> - cli::cli_warn() - } - # browser() + check_crs(query) # check whether crs is epsg:4326 # currently a bug where the ALA doesn't accept some polygons # to avoid any issues, any polygons are converted to multipolygons diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 6c0872ba..e4f9d84a 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -577,4 +577,21 @@ media_supported <- function(atlas = NULL){ if(atlas %in% unsupported_atlases){ cli::cli_abort("`atlas_media` is not supported for atlas = {atlas}") } +} + +#' Internal function for checking whether spatial objects passed to +#' `geolocate_polygon()` and `geolocate_bbox()` use correct CRS (EPSG:4326) +#' for Living Atlas data +#' @noRd +#' @keywords Internal +check_crs <- function(query) { + crs <- st_crs(query)$epsg + if(crs != 4326) { + selected_atlas <- potions::pour("atlas")$acronym + c("Spatial object CRS is not WGS 84 (EPSG:4326).", + i = "Results of this query may be incorrect because geolocate object uses different CRS to data.", + x = "Occurrence data in {selected_atlas} uses EPSG:4326, not EPSG:{crs}." # NOTE: All supported LAs use EPSG:4326 + ) |> + cli::cli_warn() + } } \ No newline at end of file diff --git a/tests/testthat/test-geolocate_bbox.R b/tests/testthat/test-geolocate_bbox.R index 7e8e26ce..89f509cc 100644 --- a/tests/testthat/test-geolocate_bbox.R +++ b/tests/testthat/test-geolocate_bbox.R @@ -117,6 +117,12 @@ test_that("`geolocate_bbox()` converts to multipolygon", { expect_match(quiet_bbox(sf_wkt), "MULTIPOLYGON") }) +test_that("`geolocate_bbox()` warns when CRS isn't EPSG:4326", { # FIXME: not backwards compatible with bbox coords? + bbox <- sf::st_bbox(c(xmin = 143, xmax = 148, ymin = -29, ymax = -28), + crs = sf::st_crs(4283)) + expect_warning(suppressMessages(geolocate_bbox(bbox)), "Spatial object CRS") +}) + # TODO: test that geolocate_bbox unnests sf object & shapefiles correctly # after converting from dots diff --git a/tests/testthat/test-geolocate_polygon.R b/tests/testthat/test-geolocate_polygon.R index 12f87b08..65e4642b 100644 --- a/tests/testthat/test-geolocate_polygon.R +++ b/tests/testthat/test-geolocate_polygon.R @@ -92,7 +92,7 @@ test_that("`geolocate_polygon()` warns when CRS isn't EPSG:4326", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" obj_sf_gda94 <- obj_sf |> st_transform(crs = 4283) # wrong GDA94 - expect_warning(geolocate(obj_sf_gda94), "Polygon CRS") + expect_warning(geolocate(obj_sf_gda94), "Spatial object CRS") }) From 430add8af785620331140ed60144352c1c17474f Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:34:30 +1000 Subject: [PATCH 48/70] Fix query parsing #303 * Fixes some issues with how taxonomic placeholders are handled in queries * Formats queries using `URLencode(reserved = TRUE)` to match ALA API query parsing format --- R/capture_taxa.R | 20 ++++++++++++++++++-- R/check.R | 25 +++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/R/capture_taxa.R b/R/capture_taxa.R index bf2773f9..44211b37 100644 --- a/R/capture_taxa.R +++ b/R/capture_taxa.R @@ -23,9 +23,18 @@ capture_taxa <- function(.query){ capture_taxa_single <- function(.query){ .query$type <- "metadata/taxa-single" terms <- .query$identify$search_term + + # Match parsing of search term to ALA API formatting + # Formats RFC 3986 reserved characters (eg. `(`, `)`, `+`) + # Spaces are reformatted to "+" + terms_formatted <- terms |> + utils::URLencode(reserved = TRUE) |> + gsub("%20", "+", x = _, fixed = TRUE) + + # build object and return list(type = .query$type, atlas = .query$atlas, - url = tibble::tibble(url = url_lookup(.query, name = terms), + url = tibble::tibble(url = url_lookup(.query, name = terms_formatted), search_term = terms), headers = build_headers()) } @@ -58,11 +67,18 @@ capture_taxa_multiple <- function(.query){ function(a){glue::glue_collapse(a, sep = "_")}) |> unlist() + # Match parsing of search term to ALA API formatting + # Formats all RFC 3986 reserved characters (eg. `(`, `)`, `+`) + # Spaces are reformatted to "+" + terms_formatted <- search_terms |> + utils::URLencode(reserved = TRUE) |> + gsub("%20", "+", x = _, fixed = TRUE) + # build object and return list(type = .query$type, atlas = .query$atlas, url = tibble::tibble(url = urls, - search_term = search_terms), + search_term = terms_formatted), headers = build_headers()) } diff --git a/R/check.R b/R/check.R index 049a94e8..e749fce2 100644 --- a/R/check.R +++ b/R/check.R @@ -396,8 +396,10 @@ check_identifiers_la <- function(.query, url <- httr2::url_parse(.query$url[1]) } queries <- url$query - if(!is.null(queries$fq)){ - if(grepl("(`TAXON_PLACEHOLDER`)", queries$fq)){ + + # either q or fq can have taxonomic queries + if(!is.null(queries$fq) | !is.null(queries$q)){ + if(has_taxon(queries$fq) | has_taxon(queries$q)){ metadata_lookup <- grepl("^metadata/taxa", names(.query)) if(any(metadata_lookup)){ identifiers <- .query[[which(metadata_lookup)[1]]] @@ -410,9 +412,19 @@ check_identifiers_la <- function(.query, taxa_ids <- build_taxa_query(ids = identifiers$taxon_concept_id, atlas = .query$atlas) + + # q and fq format taxonomic placeholder slightly differently + if(has_taxon(queries$fq)) { queries$fq <- stringr::str_replace_all(queries$fq, "\\(`TAXON_PLACEHOLDER`\\)", taxa_ids) + } + if(has_taxon(queries$q)) { + queries$q <- stringr::str_replace_all(queries$q, + "`TAXON_PLACEHOLDER`", + taxa_ids) + } + url$query <- queries .query$url[1] <- httr2::url_build(url) }else{ @@ -797,3 +809,12 @@ check_type_valid <- function(type, cli::cli_abort(call = error_call) } } + +#' Check whether query contains a taxonomic placeholder to pass to namematching +#' (eg `collect_taxa_namematching()`) in order to construct complete query +#' @noRd +#' @keywords Internal +has_taxon <- function(x, pattern = "`TAXON_PLACEHOLDER`") { + if (is.null(x)) return(FALSE) + any(grepl(pattern, x)) +} From acc395e7496c53ce54c21f2d4b3ed16286e0b829 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Mon, 31 Aug 2026 15:37:51 +1000 Subject: [PATCH 49/70] First pass at resolving #298 Test solution is to have a catcher to detect whether `potions` has been set up, and if not, apply some defaults. This is placed in `galah_config()`, `onLoad` and `onAttach`. Can only be tested when installed, so pushing this now to allow installation from dev --- R/galah_config.R | 4 +--- R/onload.R | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/R/galah_config.R b/R/galah_config.R index c3ecd05a..93eaf459 100644 --- a/R/galah_config.R +++ b/R/galah_config.R @@ -87,9 +87,7 @@ galah_config <- function(...) { dots <- list(...) # set defaults, if this has not happened already - if(length(potions::pour()) == 0) { - potions::brew(default_config()) - } + set_up_potions() # defined in `.onLoad()` # add user-provided information if(length(dots) > 0){ diff --git a/R/onload.R b/R/onload.R index 3e4e471d..e6566344 100644 --- a/R/onload.R +++ b/R/onload.R @@ -3,10 +3,20 @@ #' @keywords Internal .onLoad <- function(libname, pkgname) { if (pkgname == "galah") { + set_up_potions() + } +} + +#' Internal function to set up potions storage. +#' Called by `.onLoad`, but also by `galah_config()` when `galah` not previously called by `library()` +#' (Issue 298) +#' @noRd +#' @keywords Internal +set_up_potions <- function(){ + stored_options <- getOption("potions-pkg") + if(is.null(stored_options)){ # set up storage of standard information via {potions} - potions::brew(.pkg = "galah") # set up caching of behaviour - quiet_config <- purrr::quietly(galah_config) - config_info <- quiet_config() # to cache defaults without raising a message + potions::brew(default_config(), .pkg = "galah") # set up caching of behaviour } } @@ -23,6 +33,8 @@ .onAttach <- function(libname, pkgname) { if (pkgname == "galah") { + set_up_potions() + # get information to display to the user ## get the galah version, if we can galah_version <- "version unknown" From cb78bd442270a38147cd422e70a3c2c0fcbee539 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Mon, 31 Aug 2026 17:12:55 +1000 Subject: [PATCH 50/70] Allow `NA` crs vaues in `check_crs()` (#302) This allows users to continue supplying WKT strings without a CRS. If that is undesirable this fix will need to change. Minor fixes to tests --- R/utilities_internal.R | 11 +++++++++-- tests/testthat/test-atlas_species.R | 7 ++++--- tests/testthat/test-geolocate_polygon.R | 3 +-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/R/utilities_internal.R b/R/utilities_internal.R index e4f9d84a..21550cb5 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -585,8 +585,15 @@ media_supported <- function(atlas = NULL){ #' @noRd #' @keywords Internal check_crs <- function(query) { - crs <- st_crs(query)$epsg - if(crs != 4326) { + crs <- sf::st_crs(query)$epsg + crs_check <- if(is.na(crs)){ + FALSE # this allows situations where crs is not set at all, e.g. from wkt strings + }else if(is.integer(crs)){ # I'm assuming here that valid crs are classed as `integer` and not `numeric` + crs != 4326L + }else{ + TRUE + } + if(crs_check) { selected_atlas <- potions::pour("atlas")$acronym c("Spatial object CRS is not WGS 84 (EPSG:4326).", i = "Results of this query may be incorrect because geolocate object uses different CRS to data.", diff --git a/tests/testthat/test-atlas_species.R b/tests/testthat/test-atlas_species.R index d06167ba..732c0f60 100644 --- a/tests/testthat/test-atlas_species.R +++ b/tests/testthat/test-atlas_species.R @@ -73,7 +73,7 @@ test_that("`atlas_species()` returns correct results when piped", { expect_s3_class(species, c("tbl_df", "tbl", "data.frame")) }) -test_that("`atlas_species()` returns correct results filtered by galah_geolocate", { +test_that("`atlas_species()` returns correct results filtered by `geolocate()`", { skip_if_offline(); skip_on_ci() galah_config(run_checks = TRUE) wkt <- "POLYGON ((146.5425 -42.63203, 146.8312 -43.13203, 147.4085 -43.13203, @@ -131,9 +131,10 @@ test_that("collapse works when no `filter()` is supplied", { # but `group_by` wasn't being called, so checks weren't constructed properly skip_if_offline(); skip_on_ci() wkt <- "POLYGON((73.0 -53, 95.6 -11.5, 105.6 -10.1, 123 -12.1, 130.7 -9.5, 142.2 -9.8, 168.1 -29.05, 159.1 -54.9, 73.0 -53))" - expect_no_error({x <- galah_call(type = "species") |> + x <- galah_call(type = "species") |> st_crop(wkt) |> - collapse()}) + collapse() |> + expect_no_error() expect_s3_class(x, "query") }) diff --git a/tests/testthat/test-geolocate_polygon.R b/tests/testthat/test-geolocate_polygon.R index 65e4642b..42118872 100644 --- a/tests/testthat/test-geolocate_polygon.R +++ b/tests/testthat/test-geolocate_polygon.R @@ -90,8 +90,7 @@ test_that("`geolocate_polygon()` counts n vertices correctly", { test_that("`geolocate_polygon()` warns when CRS isn't EPSG:4326", { wkt <- "POLYGON((142.36228 -29.00703,142.74131 -29.00703,142.74131 -29.39064,142.36228 -29.39064,142.36228 -29.00703))" - obj_sf_gda94 <- obj_sf |> st_transform(crs = 4283) # wrong GDA94 - + obj_sf_gda94 <- sf::st_as_sfc(wkt, crs = sf::st_crs(4283)) # wrong GDA94 expect_warning(geolocate(obj_sf_gda94), "Spatial object CRS") }) From c9d49fd2a54767479efa8b79ef65a5fcd10478b2 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 1 Sep 2026 15:19:18 +1000 Subject: [PATCH 51/70] revise `check_crs()` to inform when NA is given (#302) also remove call to `potions::pour("atlas")$acronym` in `check_crs()` as this isn't always indicative of target for current query. --- R/utilities_internal.R | 26 +++++++++++++------------- tests/testthat/test-geolocate_bbox.R | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 21550cb5..54c4a7f3 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -586,19 +586,19 @@ media_supported <- function(atlas = NULL){ #' @keywords Internal check_crs <- function(query) { crs <- sf::st_crs(query)$epsg - crs_check <- if(is.na(crs)){ - FALSE # this allows situations where crs is not set at all, e.g. from wkt strings - }else if(is.integer(crs)){ # I'm assuming here that valid crs are classed as `integer` and not `numeric` - crs != 4326L + # allow situations where crs is not set at all, e.g. from wkt strings + if(is.na(crs)){ + c("No CRS given, assuming WGS84 (EPSG:4326)") |> + cli::cli_inform() + # otherwise set error when CRS is set to anything other than 4326, or is not an integer }else{ - TRUE - } - if(crs_check) { - selected_atlas <- potions::pour("atlas")$acronym - c("Spatial object CRS is not WGS 84 (EPSG:4326).", - i = "Results of this query may be incorrect because geolocate object uses different CRS to data.", - x = "Occurrence data in {selected_atlas} uses EPSG:4326, not EPSG:{crs}." # NOTE: All supported LAs use EPSG:4326 - ) |> - cli::cli_warn() + crs_check <- ifelse(is.integer(crs), crs != 4326L, TRUE) + if(crs_check) { + c("Spatial object CRS is not WGS 84 (EPSG:4326).", + i = "Results of this query may be incorrect because geolocate object uses different CRS to data.", + x = "Occurrence data in GBIF nodes uses EPSG:4326, not EPSG:{crs}." # NOTE: All supported LAs use EPSG:4326 + ) |> + cli::cli_warn() + } } } \ No newline at end of file diff --git a/tests/testthat/test-geolocate_bbox.R b/tests/testthat/test-geolocate_bbox.R index 89f509cc..3aaa46b7 100644 --- a/tests/testthat/test-geolocate_bbox.R +++ b/tests/testthat/test-geolocate_bbox.R @@ -82,7 +82,9 @@ test_that("`geolocate_bbox()` checks number of inputs, uses first argument", { # result <- purrr_bbox(wkt_1, wkt_2) grepl("More than 1 spatial area provided", result$warnings) |> expect_true() - grepl("Data returned for bounding box:", result$messages) |> + grepl("No CRS given, assuming WGS84", result$messages[1]) |> + expect_true() + grepl("Data returned for bounding box:", result$messages[2]) |> expect_true() expect_identical(as.character(result$result), as.character(quiet_bbox(wkt_1)[1]), From 41587761f112acdbbc7eff75d9fdd2b0da356363 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Tue, 1 Sep 2026 16:38:33 +1000 Subject: [PATCH 52/70] Integrate list-unnest lookup with `compound()` workflow (#300) Previous version looked up list length using `retrieve_cache()`, but this is unstable because there is no way to know whether lists have already been queried. More consistent approach is to add query at the `compound()` stage, then use `collapse()` code to run checks to determine whether pagination is needed. --- R/capture_metadata_unnest.R | 32 ++++---------------------------- R/collapse_checks.R | 5 +++-- R/collapse_metadata.R | 17 +++++++++++++++++ R/collapse_query_set.R | 2 ++ R/collect_metadata.R | 7 ++++--- R/compound.R | 10 +++++++++- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/R/capture_metadata_unnest.R b/R/capture_metadata_unnest.R index 115ea486..9dd31b28 100644 --- a/R/capture_metadata_unnest.R +++ b/R/capture_metadata_unnest.R @@ -38,35 +38,11 @@ capture_lists_unnest <- function(.query){ ) # create object - x <- list(type = .query$type, + list(type = .query$type, atlas = .query$atlas, - url = httr2::url_build(url)) - - # return lists longer than max pageSize - lists_gt_max_page <- retrieve_cache("lists") |> - dplyr::filter(row_count > 30000) |> - dplyr::pull(species_list_uid) - - # if list is long, add more queries to match required page number for list row_count - if(.query$filter$value %in% lists_gt_max_page) { - row_count <- retrieve_cache("lists") |> - dplyr::filter(species_list_uid == .query$filter$value) |> - dplyr::pull(row_count) - n_pages <- ceiling(row_count * (1/30000)) # (i.e. 1/1000) - - # add additional urls to reach required number of pages to return all items - x$url <- tibble::tibble(url = glue::glue("{x$url}&page={seq_len(n_pages)}")) - } - # create query - x |> as_query() - - - # old code - # create object - # list(type = .query$type, - # atlas = .query$atlas, - # url = httr2::url_build(url)) |> - # as_query() + url = httr2::url_build(url)) |> + as_prequery() + # note: class prequery is necessary to ensure checking of list length } #' Internal function to run `capture()` for diff --git a/R/collapse_checks.R b/R/collapse_checks.R index 424a2ac6..bf8aaff1 100644 --- a/R/collapse_checks.R +++ b/R/collapse_checks.R @@ -4,7 +4,6 @@ #' @keywords Internal collapse_build_checks <- function(.query){ # get basic description of `query_set` object - n <- length(.query) names_vec <- purrr::map(.query, \(a){purrr::pluck(a, "type")}) |> unlist() @@ -19,6 +18,7 @@ collapse_build_checks <- function(.query){ .query[[which(data_lookup)]] |> collapse_add_metadata(metadata_results) }else if(any(names_vec %in% c("metadata/fields-unnest", + "metadata/lists-unnest", "metadata/profiles-unnest", "metadata/taxa-unnest"))){ # this code accounts for `unnest` functions that require lookups @@ -144,7 +144,8 @@ collapse_add_metadata <- function(query, meta){ #' @noRd #' @keywords Internal collapse_remove_metadata <- function(.query){ - names_lookup <- stringr::str_detect( names(.query), "^metadata/") + names_lookup <- stringr::str_detect(names(.query), "^metadata/") & + names(.query) != "metadata/lists" # exception to retain metadata/lists until collapse() stage if(any(names_lookup)){ as_query(.query[!names_lookup]) }else{ diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index f5430dba..d12e84fe 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -90,6 +90,23 @@ get_max_n <- function(.query){ n } +#' Internal function to ensure that list values are paginated properly +#' @noRd +#' @keywords Internal +collapse_lists_unnest <- function(.query, error_call){ + # get row length + n_rows <- .query$`metadata/lists`$row_count[1] + # if >30000, paginate + if(n_rows > 30000){ + n_pages <- ceiling(row_count * (1/30000)) + # add additional urls to reach required number of pages to return all items + initial_url <- .query$url + url_tibble <- tibble::tibble(url = glue::glue("{initial_url}&page={seq_len(n_pages)}")) + .query$url <- url_tibble + } + # return cleaned object + as_query(.query[names(.query) != "metadata/lists"]) +} #' Internal function to call `collapse` for `request_metadata(type = "profiles-unnest")` #' @noRd diff --git a/R/collapse_query_set.R b/R/collapse_query_set.R index 6e42b8f3..33f23b8f 100644 --- a/R/collapse_query_set.R +++ b/R/collapse_query_set.R @@ -45,6 +45,8 @@ collapse_query_set <- function(x, "metadata/profiles-unnest" = collapse_profile_values(x, error_call = error_call) |> add_request(x$request), + "metadata/lists-unnest" = collapse_lists_unnest(x, + error_call = error_call), # some "metadata/" functions require pagination under some circumstances "metadata/lists" = collapse_lists(x), # always paginates x # remaining "metadata/" functions and "data/occurrences-doi" are passed as-is diff --git a/R/collect_metadata.R b/R/collect_metadata.R index 997a3ffb..d16de3ee 100644 --- a/R/collect_metadata.R +++ b/R/collect_metadata.R @@ -40,7 +40,9 @@ tidy_list_columns <- function(x){ list_names <- names(x)[list_check] list_tibbles <- purrr::map(list_names, \(a){ - tibble::tibble({{a}} == list(x[[a]])) + aa <- tibble::tibble(list(x[[a]])) + colnames(aa) <- a + aa }) }else{ list_tibbles <- NULL @@ -378,8 +380,7 @@ collect_lists <- function(.query){ # single-list queries that use `filter()` don't have a `lists` slot if(is.null(lists_slot)){ result_df <- result |> - purrr::map(tidy_list_columns) |> - dplyr::bind_rows() + tidy_list_columns() # but some queries do }else{ result_df <- lists_slot |> diff --git a/R/compound.R b/R/compound.R index 87105722..fa7a372d 100644 --- a/R/compound.R +++ b/R/compound.R @@ -114,10 +114,11 @@ as_query_set <- function(x){ } #' Internal function to build a `query_set` object -#' for object of class `data_request` +#' for object of class `metadata_request` #' @noRd #' @keywords Internal build_query_set_metadata <- function(x){ + # create an empty object to store results result <- list() @@ -155,6 +156,13 @@ build_query_set_metadata <- function(x){ if(is.null(x$request$identify) & is.null(x$request$filter)){ cli::cli_abort("Requests of type `taxa-unnest` must also supply one of `filter()` or `identify()`.") } + # lists-unnest requires a single-row lists query first, to determine how many rows are expected in the output + # (and paginate accordingly) + }else if(x$request$type == "lists-unnest"){ + result <- append(result, + list(request_metadata(from = x$atlas) |> + filter(list == x$request$filter$value[1]) |> + capture())) }else if(is.null(x$request$filter)){ current_type <- x$request$type cli::cli_abort("Requests of type `{current_type}` must supply `filter()`.") From ddd630de56927cbc2d30710124d1c97a29728f30 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 2 Sep 2026 16:17:44 +1000 Subject: [PATCH 53/70] Ensure `collect()` works for type `lists-unnest` (#300) - change default `select()` behaviour for type `lists-unnest` to `everything()` - minor bug fix to ensure parsing works properly for lists regardless of how many entries are given in the `properties` field - update tests for above --- R/capture.R | 10 +- R/collapse_metadata.R | 11 +- R/collect_metadata_unnest.R | 105 ++++++++++-------- R/utilities_internal.R | 4 - tests/testthat/test-request_metadata_unnest.R | 3 +- 5 files changed, 78 insertions(+), 55 deletions(-) diff --git a/R/capture.R b/R/capture.R index 072158f6..47d1e33f 100644 --- a/R/capture.R +++ b/R/capture.R @@ -419,12 +419,14 @@ enforce_select_query_metadata <- function(x){ purrr::pluck("type") |> stringr::str_remove("^metadata/") # see whether `lookup_select_columns()` returns anything - chosen_columns <- lookup_select_columns(specific_type) + chosen_columns <- lookup_select_columns(specific_type) # some `unnest` queries internally rename the lead column to the name of the supplied field if(is.null(chosen_columns) & - stringr::str_detect(specific_type, "-unnest$")){ - chosen_columns <- x$filter |> - purrr::pluck("value") + stringr::str_detect(specific_type, "-unnest$") & + specific_type != "lists-unnest" + ){ + chosen_columns <- x$filter |> + purrr::pluck("value") } # if we have, after 2 attempts, found some chosen_columns, use them if(!is.null(chosen_columns)){ diff --git a/R/collapse_metadata.R b/R/collapse_metadata.R index d12e84fe..2e2098cb 100644 --- a/R/collapse_metadata.R +++ b/R/collapse_metadata.R @@ -95,10 +95,17 @@ get_max_n <- function(.query){ #' @keywords Internal collapse_lists_unnest <- function(.query, error_call){ # get row length - n_rows <- .query$`metadata/lists`$row_count[1] + list_metadata <- .query$`metadata/lists` + if(any(colnames(list_metadata) == "row_count")){ # ALA lists API v2 + n_rows <- list_metadata$row_count[1] + }else if(any(colnames(list_metadata) == "item_count")){ # legacy API + n_rows <- list_metadata$item_count[1] + }else{ + n_rows <- 1 # placeholder so code doesn't break. Suppresses pagination. + } # if >30000, paginate if(n_rows > 30000){ - n_pages <- ceiling(row_count * (1/30000)) + n_pages <- ceiling(n_rows * (1/30000)) # add additional urls to reach required number of pages to return all items initial_url <- .query$url url_tibble <- tibble::tibble(url = glue::glue("{initial_url}&page={seq_len(n_pages)}")) diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 794377d3..4d60f177 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -66,34 +66,10 @@ check_missing_fields <- function(x, call){ #' @keywords Internal collect_lists_unnest <- function(.query){ - clean_common_names <- function(df){ - if(any(colnames(df) == "commonName")){ - df$commonName <- as.character(df$commonName) - if(any(df$commonName == "NULL")){ - df$commonName[df$commonName == "NULL"] <- NA - } - } - df - } - - # extract additional raw fields columns - clean_kvp_values <- function(df){ - if(any(colnames(df) == "kvpValues")){ - if(any(lengths(df$kvpValues) > 0)){ - df <- df |> - tidyr::unnest(cols = "kvpValues") |> - tidyr::unnest_wider("kvpValues") |> - tidyr::pivot_wider(names_from = "key", - values_from = "value") - } - } - df - } - # get data result <- query_API(.query) - if(stringr::str_detect(.query$url, "/v1/")) { # version 1 API + if(stringr::str_detect(.query$url, "/v1/") | .query$atlas != "Australia") { # version 1 API # old result |> purrr::list_transpose() |> @@ -117,29 +93,46 @@ collect_lists_unnest <- function(.query){ dplyr::rename_with(camel_to_snake_case) |> parse_rename(.query) - # second, `properties` contains status information and other raw fields - raw_columns <- x |> - select(taxon_concept_id, supplied_name, scientific_name, properties) |> - tidyr::unnest(cols = properties) |> - tidyr::unnest_wider(properties) |> - dplyr::mutate(key = camel_to_snake_case(key)) |> - dplyr::mutate(key = dplyr::if_else(key %in% colnames(x), glue::glue("{key}_raw"), key)) |> # rename prior to pivot to avoid name conflicts - tidyr::pivot_wider(names_from = "key", - values_from = "value", - names_repair = "minimal", - values_fn = list) |> - tidyr::unnest(cols = everything()) + # second, `properties` contains status information and other raw fields. + simple_columns <- x |> + select(taxon_concept_id, supplied_name, scientific_name, properties) + + # where `properties` contains multipe key:value pairs per row, we have to unnest + # or the code breaks. + single_properties_check <- purrr::map(x$properties, + \(a){length(a) == 2 & all(names(a)[1:2] == c("key", "value"))}) |> + unlist() |> + all() + if(!single_properties_check){ + raw_columns <- simple_columns |> tidyr::unnest(cols = .data$properties) + }else{ + raw_columns <- simple_columns + } + + # resume pipe + raw_columns <- raw_columns |> + tidyr::unnest_wider(properties, names_sep = "_") |> + dplyr::mutate(key = camel_to_snake_case(properties_key)) |> + dplyr::mutate(key = dplyr::if_else(key %in% colnames(x), glue::glue("{key}_raw"), key)) |> # rename prior to pivot to avoid name conflicts + tidyr::pivot_wider(names_from = "key", + values_from = "properties_value", + names_repair = "minimal", + values_fn = list) |> + tidyr::unnest(cols = everything()) # merge result_final <- x |> dplyr::left_join(raw_columns, dplyr::join_by(taxon_concept_id, supplied_name, scientific_name)) |> dplyr::select(-properties) |> - parse_rename(.query) - # parse_select(.query) # FIXME: this is limiting output in an unexpected way + parse_rename(.query) |> + parse_select(.query) # inform user about duplicated taxon_concept_ids - duplicate_taxa <- result_final |> filter(dplyr::n() > 1, .by = taxon_concept_id) |> distinct(taxon_concept_id) |> nrow() + duplicate_taxa <- result_final |> + filter(dplyr::n() > 1, .by = taxon_concept_id) |> + distinct(taxon_concept_id) |> + nrow() bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", "i" = "This happens because {.field taxon_concept_id} can match to multiple {.field supplied_name} values.", "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") @@ -147,7 +140,35 @@ collect_lists_unnest <- function(.query){ return(result_final) } +} + +#' Internal function for cleaning common names +#' @noRd +#' @keywords Internal +clean_common_names <- function(df){ + if(any(colnames(df) == "commonName")){ + df$commonName <- as.character(df$commonName) + if(any(df$commonName == "NULL")){ + df$commonName[df$commonName == "NULL"] <- NA + } + } + df +} +#' Internal function to extract additional raw fields columns +#' @noRd +#' @keywords Internal +clean_kvp_values <- function(df){ + if(any(colnames(df) == "kvpValues")){ + if(any(lengths(df$kvpValues) > 0)){ + df <- df |> + tidyr::unnest(cols = "kvpValues") |> + tidyr::unnest_wider("kvpValues") |> + tidyr::pivot_wider(names_from = "key", + values_from = "value") + } + } + df } #' Internal function to parse ALA-matched taxonomic information of species lists @@ -186,12 +207,8 @@ parse_classification <- function(df){ ) } df - - } - - #' Internal function to run `compute()` for #' `request_metadata(type = "profiles") |> unnest()` #' @noRd diff --git a/R/utilities_internal.R b/R/utilities_internal.R index 54c4a7f3..d582626c 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -115,10 +115,6 @@ lookup_select_columns <- function(type) { "row_count", # ALA v2 "is_authoritative", "is_threatened"), - "lists-unnest" = c("scientific_name", - "vernacular_name", - "taxon_concept_id", - "taxon_id"), # ALA v2 "media" = c("media_id", "occurrence_id", "creator", diff --git a/tests/testthat/test-request_metadata_unnest.R b/tests/testthat/test-request_metadata_unnest.R index e69f6ef7..257e6f04 100644 --- a/tests/testthat/test-request_metadata_unnest.R +++ b/tests/testthat/test-request_metadata_unnest.R @@ -70,6 +70,7 @@ test_that("request_metadata() |> unnest() works for type = 'lists'", { x <- request_metadata() |> filter(list == "dr947") |> unnest() |> + select(taxon_concept_id, supplied_name, rank) |> collapse() expect_s3_class(x, "query") expect_equal(x$type, "metadata/lists-unnest") @@ -80,7 +81,7 @@ test_that("request_metadata() |> unnest() works for type = 'lists'", { z <- collect(y) expect_s3_class(z, c("tbl_df", "tbl", "data.frame")) expect_gte(nrow(z), 10) - expect_gte(ncol(z), 3) + expect_equal(ncol(z), 3) # now check `everything()` xx <- request_metadata() |> filter(list == "dr947") |> From 5a76734be910f44d05985f9951144944e9584b66 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 4 Sep 2026 11:17:34 +1000 Subject: [PATCH 54/70] Misc improvements such that R CMD Check passes while offline - `geolocate_polygon()` checks now transform shapefiles to 4326 to avoid errors - `parse_properties()` and `parse_classification()` use consistent syntax, incorporate `.data` to ensure passing R CMD, and refer to `lookup_select_columns()` for consistency with other functions --- R/collect_metadata_unnest.R | 129 ++++++++++++------------ R/utilities_internal.R | 17 ++++ tests/testthat/test-geolocate_polygon.R | 6 +- 3 files changed, 85 insertions(+), 67 deletions(-) diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 4d60f177..2ec8e7f7 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -91,54 +91,21 @@ collect_lists_unnest <- function(.query){ dplyr::mutate(species_list_uid = .query$request$filter$value) |> parse_classification() |> dplyr::rename_with(camel_to_snake_case) |> - parse_rename(.query) - - # second, `properties` contains status information and other raw fields. - simple_columns <- x |> - select(taxon_concept_id, supplied_name, scientific_name, properties) - - # where `properties` contains multipe key:value pairs per row, we have to unnest - # or the code breaks. - single_properties_check <- purrr::map(x$properties, - \(a){length(a) == 2 & all(names(a)[1:2] == c("key", "value"))}) |> - unlist() |> - all() - if(!single_properties_check){ - raw_columns <- simple_columns |> tidyr::unnest(cols = .data$properties) - }else{ - raw_columns <- simple_columns - } - - # resume pipe - raw_columns <- raw_columns |> - tidyr::unnest_wider(properties, names_sep = "_") |> - dplyr::mutate(key = camel_to_snake_case(properties_key)) |> - dplyr::mutate(key = dplyr::if_else(key %in% colnames(x), glue::glue("{key}_raw"), key)) |> # rename prior to pivot to avoid name conflicts - tidyr::pivot_wider(names_from = "key", - values_from = "properties_value", - names_repair = "minimal", - values_fn = list) |> - tidyr::unnest(cols = everything()) - - # merge - result_final <- x |> - dplyr::left_join(raw_columns, - dplyr::join_by(taxon_concept_id, supplied_name, scientific_name)) |> - dplyr::select(-properties) |> parse_rename(.query) |> + parse_properties(.query) |> parse_select(.query) # inform user about duplicated taxon_concept_ids - duplicate_taxa <- result_final |> - filter(dplyr::n() > 1, .by = taxon_concept_id) |> - distinct(taxon_concept_id) |> + duplicate_taxa <- x |> + filter(dplyr::n() > 1, .by = .data$taxon_concept_id) |> + distinct(.data$taxon_concept_id) |> nrow() bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", "i" = "This happens because {.field taxon_concept_id} can match to multiple {.field supplied_name} values.", "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") cli::cli_warn(bullets) - return(result_final) + return(x) } } @@ -171,6 +138,54 @@ clean_kvp_values <- function(df){ df } +#' Internal function to parse user-supplied information on species lists +#' via `show_values()` / `request_metadata(type = "lists") |> unnest()` +#' @noRd +#' @keywords Internal +parse_properties <- function(df, .query){ + + if(any(colnames(df) == "properties")){ + # second, `properties` contains status information and other raw fields. + simple_columns <- df |> + dplyr::select(tidyselect::any_of( + c("taxon_concept_id", "supplied_name", "scientific_name", "properties"))) + + # where `properties` contains multipe key:value pairs per row, we have to unnest + # or the code breaks. + single_properties_check <- purrr::map(df$properties, + \(a){length(a) == 2 & all(names(a)[1:2] == c("key", "value"))}) |> + unlist() |> + all() + if(!single_properties_check){ + raw_columns <- simple_columns |> tidyr::unnest(cols = .data$properties) + }else{ + raw_columns <- simple_columns + } + + # resume pipe + raw_columns <- raw_columns |> + tidyr::unnest_wider(.data$properties, names_sep = "_") |> + dplyr::mutate(key = camel_to_snake_case(.data$properties_key)) |> + dplyr::mutate(key = dplyr::if_else(.data$key %in% colnames(df), glue::glue("{key}_raw"), .data$key)) |> # rename prior to pivot to avoid name conflicts + tidyr::pivot_wider(names_from = "key", + values_from = "properties_value", + names_repair = "minimal", + values_fn = list) |> + tidyr::unnest(cols = tidyselect::everything()) + + # merge + result_final <- df |> + dplyr::left_join(raw_columns, + dplyr::join_by("taxon_concept_id", "supplied_name", "scientific_name")) |> + dplyr::select(-tidyselect::starts_with("properties")) |> # also excludes `properties_key` + parse_rename(.query) + + return(result_final) + }else{ + return(df) + } +} + #' Internal function to parse ALA-matched taxonomic information of species lists #' via `show_values()` / `request_metadata(type = "lists") |> unnest()` #' @noRd @@ -178,33 +193,17 @@ clean_kvp_values <- function(df){ parse_classification <- function(df){ if(any(colnames(df) == "classification")){ - df <- df |> - tidyr::unnest_wider(classification, - names_repair = "minimal", - names_sep = "_") |> - # select ALA-matched columns - select(species_list_uid, - classification_taxonConceptID, - suppliedName, - # scientific_name, - classification_scientificName, # ALA-matched name - classification_scientificNameAuthorship, - classification_vernacularName, - classification_rank, - classification_kingdom, - classification_phylum, - classification_class, - classification_order, - classification_family, - classification_genus, - classification_species, - properties # keep properties for raw fields - ) |> - # remove prefix - dplyr::rename_with( - ~ stringr::str_remove(., "classification_"), - classification_taxonConceptID:classification_species - ) + df <- df |> + tidyr::unnest_wider(.data$classification, + names_repair = "minimal", + names_sep = "_") |> + # select ALA-matched columns + select(dplyr::any_of(lookup_select_columns("classification"))) |> + # remove prefix + dplyr::rename_with( + ~ stringr::str_remove(., "classification_"), + tidyselect::starts_with("classification_") + ) } df } diff --git a/R/utilities_internal.R b/R/utilities_internal.R index d582626c..bf176e70 100644 --- a/R/utilities_internal.R +++ b/R/utilities_internal.R @@ -98,6 +98,23 @@ lookup_select_columns <- function(type) { "description", "category", "type"), + "classification" = { + classification_columns <- c("taxonConceptID", + "scientificName", + "scientificNameAuthorship", + "vernacularName", + "rank", + "kindom", + "phylum", + "class", + "order", + "family", + "genus", + "species") + c("species_list_uid", + "suppliedName", + glue::glue("classification_{classification_columns}"), + "properties")}, "fields" = c("id", "description", "type"), diff --git a/tests/testthat/test-geolocate_polygon.R b/tests/testthat/test-geolocate_polygon.R index 42118872..c658a702 100644 --- a/tests/testthat/test-geolocate_polygon.R +++ b/tests/testthat/test-geolocate_polygon.R @@ -71,7 +71,8 @@ test_that("`geolocate_polygon()` counts vertices correctly", { test_that("`geolocate_polygon()` checks for simple polygons only", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") - shapefile_complex <- sf::st_read(poly_path, quiet = TRUE) + shapefile_complex <- sf::st_read(poly_path, quiet = TRUE) |> + sf::st_transform(crs = sf::st_crs(4326)) shapefile_simple <- sf::st_simplify(shapefile_complex, dTolerance = 1000) geolocate_polygon(shapefile_complex) |> expect_error(label = "Polygon must have 500 or fewer vertices") @@ -83,7 +84,8 @@ test_that("`geolocate_polygon()` counts n vertices correctly", { sf_wkt <- "POLYGON((143.32 -18.78,145.30 -20.52,141.52 -21.50,143.32 -18.78))" |> sf::st_as_sfc() poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") - shapefile_complex <- sf::st_read(poly_path, quiet = TRUE) + shapefile_complex <- sf::st_read(poly_path, quiet = TRUE) |> + sf::st_transform(crs = sf::st_crs(4326)) expect_equal(n_points(shapefile_complex), 2787) expect_equal(n_points(sf_wkt), 4) }) From 58db1682bcd9f3a8fa8230e2aa3358450f813bbd Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 4 Sep 2026 16:09:04 +1000 Subject: [PATCH 55/70] Misc bug fixes to code and tests - prevent failure of `atlas_media()` when updating `fq` to supply only records that contain media - prevent loss of taxonomic information in `fq` for occurrence downloads - change `glimpse()` code to use same codebase as lists for parsing nested fields - make tests for efficient for greater speed --- R/atlas_media.R | 11 +++- R/check.R | 12 ++-- R/collect_occurrences.R | 24 ++++---- tests/testthat/test-atlas_media.R | 44 +++++++++++--- tests/testthat/test-international-GBIF.R | 60 ++++++++++++-------- tests/testthat/test-international-Portugal.R | 1 - 6 files changed, 101 insertions(+), 51 deletions(-) diff --git a/R/atlas_media.R b/R/atlas_media.R index 752fb25b..f36b3f86 100644 --- a/R/atlas_media.R +++ b/R/atlas_media.R @@ -66,7 +66,16 @@ atlas_media <- function(request = NULL, media_fq <- glue::glue("({glue::glue_collapse(media_fq, ' OR ')})") } url <- httr2::url_parse(query_collapse$url) - url$query$fq <- glue::glue("{url$query$fq}AND{media_fq}") + # NOTE: since v 2.3.0, we need to adapt to whether there are `q` or `fq filters` + if(is.null(url$query$q)){ + url$query$fq <- media_fq + }else{ + if(is.null(url$query$fq)){ + url$query$fq <- media_fq + }else{ + url$query$fq <- glue::glue("{url$query$fq}AND{media_fq}") + } + } query_collapse$url <- httr2::url_build(url) } diff --git a/R/check.R b/R/check.R index e749fce2..73fd6d7c 100644 --- a/R/check.R +++ b/R/check.R @@ -415,14 +415,14 @@ check_identifiers_la <- function(.query, # q and fq format taxonomic placeholder slightly differently if(has_taxon(queries$fq)) { - queries$fq <- stringr::str_replace_all(queries$fq, - "\\(`TAXON_PLACEHOLDER`\\)", - taxa_ids) + queries$fq <- stringr::str_replace_all(queries$fq, + "`TAXON_PLACEHOLDER`", + taxa_ids) } if(has_taxon(queries$q)) { - queries$q <- stringr::str_replace_all(queries$q, - "`TAXON_PLACEHOLDER`", - taxa_ids) + queries$q <- stringr::str_replace_all(queries$q, + "`TAXON_PLACEHOLDER`", + taxa_ids) } url$query <- queries diff --git a/R/collect_occurrences.R b/R/collect_occurrences.R index 519c21da..26b72177 100644 --- a/R/collect_occurrences.R +++ b/R/collect_occurrences.R @@ -159,17 +159,21 @@ collect_occurrences_glimpse_la <- function(.query){ # pull required info from API into a tibble df <- result |> purrr::pluck("occurrences") |> - # non-standard fields are nested within `otherProperties` - # extract these - purrr::map(\(a){ - if(any(names(a) == "otherProperties")){ - c(a[names(a) != "otherProperties"], - a[["otherProperties"]]) - }else{ - a - } - }) |> + purrr::map(build_tibble_from_nested_list) |> dplyr::bind_rows() + + ## OLD CODE + ## non-standard fields are nested within `otherProperties` + ## extract these + # purrr::map(\(a){ + # if(any(names(a) == "otherProperties")){ + # c(a[names(a) != "otherProperties"], + # a[["otherProperties"]]) + # }else{ + # a + # } + # }) + attr(df, "total_n") <- result$totalRecords # assign new object for bespoke printing diff --git a/tests/testthat/test-atlas_media.R b/tests/testthat/test-atlas_media.R index 2ab5ae17..14e582c5 100644 --- a/tests/testthat/test-atlas_media.R +++ b/tests/testthat/test-atlas_media.R @@ -81,12 +81,16 @@ test_that("`collapse()` and `collect()` work for `type = 'media'`", { ## PART 1: request occurrence data capture_config <- purrr_config(email = "ala4r@ala.org.au") - occ_collect <- request_data() |> + occ_collect <- request_data(from = "ALA") |> identify("Litoria peronii") |> filter(year == 2010, !is.na(images)) |> - select(group = "media") |> + select(species, group = "media") |> + glimpse() |> quiet_collect(wait = TRUE) + # basic test to ensure taxon matching is working + expect_all_equal(occ_collect$species, "Litoria peronii") # test + ## PART 2: request media metadata # collapse media_collapse <- request_metadata() |> @@ -190,20 +194,42 @@ test_that("atlas_media gives a warning when old arguments are used", { test_that("collect_media handles different file formats", { skip_if_offline(); skip_on_ci() - + # setup media_dir <- "test_media" galah_config(email = "ala4r@ala.org.au", directory = media_dir) - media_data <- galah_call() |> + + # 'safe' way to query data that might be large: use `glimpse()` rather than + # `atlas_media()` + sound_data <- galah_call() |> identify("Regent Honeyeater") |> - filter(multimedia %in% c("Sound", "Image"), year == 2024) |> - quiet_media() + filter(multimedia == "Sound") |> + select(occurrenceID, species, taxonConceptID, multimedia, images, sounds) |> + glimpse() |> + collect() + + # reproduce media unnesting + sound_data <- sound_data |> + dplyr::mutate(media_id = purrr::map2(sound_data$images, + sound_data$sounds, \(a, b){c(a, b)})) |> + tidyr::unnest_longer(col = media_id) + + # get metadata + media_metadata <- request_metadata() |> + filter(media == sound_data$media_id) |> + collect() + + # join + media_data <- dplyr::left_join(sound_data, + media_metadata, + by = "media_id") + # sample one of each multimedia type to shorten testing time media_summary <- media_data |> - dplyr::group_by(media_type) |> + dplyr::group_by(mime_type) |> dplyr::sample_n(size = 2) - expect_equal(sort(unique(media_data$multimedia)), - c("Image", "Image | Sound")) + + # now run test result <- purrr_collect_media(media_summary, thumbnail = TRUE) downloads <- list.files(path = media_dir) expect_true(any(grepl(".mpg$", downloads))) # sounds diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index 112783e8..deae4981 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -1,6 +1,11 @@ # set verbose to off galah_config(verbose = FALSE, run_checks = FALSE) +# This script includes a delay between successive GBIF occurrence queries. +# This is needed because GBIF appears to limit the number of queries allowed +# per unit time. The delay (in seconds) is set here: +pause_time <- 2 + test_that("swapping to atlas = GBIF works", { expect_message(galah_config(atlas = "GBIF", username = "atlasoflivingaustralia", @@ -195,6 +200,7 @@ galah_config(run_checks = TRUE) test_that("`count()` works with 2 `group_by` args for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- galah_call() |> filter(year >= 2020) |> group_by(year, basisOfRecord) |> @@ -235,6 +241,7 @@ test_that("`count()` works with 2 `group_by` args for GBIF", { test_that("`count()` works with `identify` for GBIF when `run_checks` = TRUE", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) galah_config(run_checks = TRUE) # collapse x <- request_data() |> @@ -261,6 +268,7 @@ test_that("`count()` works with `identify` for GBIF when `run_checks` = TRUE", { test_that("`glimpse()` works for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) galah_config(atlas = "GBIF", username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", @@ -283,6 +291,7 @@ test_that("`glimpse()` works for GBIF", { test_that("`glimpse()` works with `identify()` for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) galah_config(atlas = "GBIF", username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", @@ -308,6 +317,7 @@ test_that("`glimpse()` works with `identify()` for GBIF", { test_that("`atlas_occurrences()` works for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", @@ -320,6 +330,7 @@ test_that("`atlas_occurrences()` works for GBIF", { test_that("`atlas_occurrences()` works with `select()` for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", @@ -331,9 +342,9 @@ test_that("`atlas_occurrences()` works with `select()` for GBIF", { expect_equal(ncol(x), 3) }) -# FIXME: test not implemented yet test_that("invalid fields are caught from `select()` for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", @@ -343,40 +354,36 @@ test_that("invalid fields are caught from `select()` for GBIF", { expect_error() }) -test_that("`atlas_occurrences()` works with `geolocate_polygon()` for GBIF", { +test_that("`count()` works with `geolocate_()` functions for GBIF", { + # NOTE: `atlas_occurrences()` call removed due to high time taken skip_if_offline(); skip_on_ci() - wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" - base_query <- galah_call() |> + Sys.sleep(pause_time) + count_total <- request_data(from = "GBIF") |> identify("Mammalia") |> - geolocate_polygon(wkt) - count <- base_query |> count() |> collect() - result <- base_query |> collect() - expect_s3_class(result, c("tbl_df", "tbl", "data.frame")) - expect_gt(ncol(result), 30) - expect_equal(nrow(result), count$count) -}) - -test_that("`atlas_occurences()` works with `geolocate_radius()` for GBIF", { - skip_if_offline(); skip_on_ci() - base_query <- galah_call() |> + Sys.sleep(pause_time) + count_radius <- request_data(from = "GBIF") |> identify("Mammalia") |> geolocate_radius(lat = -33.7, - lon = 151.3, - radius = 0.5) - count <- base_query |> + lon = 151.3, + radius = 0.5) |> count() |> collect() - result <- base_query |> + Sys.sleep(pause_time) + wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" + count_polygon <- request_data(from = "GBIF") |> + identify("Mammalia") |> + geolocate_polygon(wkt) |> + count() |> collect() - expect_s3_class(result, c("tbl_df", "tbl", "data.frame")) - expect_gt(ncol(result), 30) - expect_equal(nrow(result), count$count) + expect_lt(count_radius$count, count_total$count) + expect_lt(count_polygon$count, count_total$count) }) test_that("`atlas_species()` works for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- request_data(type = "species", from = "GBIF") |> authenticate(username = "atlasoflivingaustralia", @@ -400,6 +407,7 @@ test_that("`atlas_species()` works for GBIF", { test_that("`count()` queries work with `authenticate()` for GBIF", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- request_data(from = "GBIF") |> authenticate(username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", @@ -413,7 +421,8 @@ test_that("`count()` queries work with `authenticate()` for GBIF", { }) test_that("`distinct()` queries accept `select()` for GBIF", { - skip_if_offline(); skip_on_ci() + skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) x <- request_data(from = "GBIF") |> authenticate(username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", @@ -438,6 +447,7 @@ test_that("`atlas_media()` fails for GBIF", { test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences-doi'` ", { skip_if_offline(); skip_on_ci() + Sys.sleep(pause_time) # collapse base_query <- request_data() |> identify("Vulpes vulpes") |> @@ -445,6 +455,7 @@ test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences count <- base_query |> count() |> collect() + Sys.sleep(pause_time) x <- base_query |> collapse() # NOTE: the above query should return 72 records (tested 2025-06-10) @@ -466,6 +477,7 @@ test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences expect_true(!is.null(attributes(z)$doi)) # FIXME: need DOI search test + Sys.sleep(pause_time) recent_doi <- attributes(z)$doi a <- galah_call() |> filter(doi == recent_doi) |> @@ -476,4 +488,4 @@ test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences quiet_config <- purrr::quietly(galah_config) quiet_config(atlas = "Australia") -rm(quiet_config) \ No newline at end of file +rm(quiet_config, pause_time) \ No newline at end of file diff --git a/tests/testthat/test-international-Portugal.R b/tests/testthat/test-international-Portugal.R index 44c5b44d..12bf5e33 100644 --- a/tests/testthat/test-international-Portugal.R +++ b/tests/testthat/test-international-Portugal.R @@ -32,7 +32,6 @@ test_that("show_all(datasets) works for Portugal", { expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) }) -## FIXME: No data returned test_that("show_all(providers) works for Portugal", { skip_if_offline(); skip_on_ci() x <- show_all(providers, limit = 10) |> From b0b3e889fcfa963000cd02c2fae9045290051131 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 4 Sep 2026 16:31:51 +1000 Subject: [PATCH 56/70] Use brighter color for `galah_green()` (#304) --- R/print.R | 2 +- tests/testthat/_snaps/print.md | 3 +- tests/testthat/_snaps/print.new.md | 116 ----------------------------- 3 files changed, 3 insertions(+), 118 deletions(-) delete mode 100644 tests/testthat/_snaps/print.new.md diff --git a/R/print.R b/R/print.R index 01743723..f976553c 100644 --- a/R/print.R +++ b/R/print.R @@ -371,7 +371,7 @@ galah_pink <- crayon::make_style("#bf2a6d") #' Green for printing secondary text (e.g. object types) to the console #' @noRd #' @keywords Internal -galah_green <- crayon::make_style("#176666") +galah_green <- crayon::make_style("#31a6a6") #' Green for printing non-emphasized text to the console #' @noRd diff --git a/tests/testthat/_snaps/print.md b/tests/testthat/_snaps/print.md index 2122c122..e58d1678 100644 --- a/tests/testthat/_snaps/print.md +++ b/tests/testthat/_snaps/print.md @@ -81,10 +81,11 @@ compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), basisOfRecord == "HUMAN_OBSERVATION")) Message - Object of class query_set containing 4 queries: + Object of class query_set containing 5 queries: * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig * metadata/fields url: https://api.ala.org.au/occurrences/index/fields * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes + * metadata/reasons url: https://api.ala.org.au/logger/service/logger/reasons * data/occurrences url: https://api.ala.org.au/occurrences/occurrences/offline/... diff --git a/tests/testthat/_snaps/print.new.md b/tests/testthat/_snaps/print.new.md deleted file mode 100644 index e58d1678..00000000 --- a/tests/testthat/_snaps/print.new.md +++ /dev/null @@ -1,116 +0,0 @@ -# object of class `data-request` formats correctly - - Code - request_data() - Message - Object of type data_request containing: - * type occurrences - * atlas Australia - -# populated `data_request()` prints correctly - - Code - filter(identify(galah_call(), "crinia"), year == 2025) - Message - Object of type data_request containing: - * type occurrences - * atlas Australia - * identify search_term: crinia - * filter Object of class `data_filter`: year == 2025 - -# object of class `metadata-request` formats correctly - - Code - request_data() - Message - Object of type data_request containing: - * type occurrences - * atlas Australia - -# object of class `metadata-request` formats correctly with `filter() - - Code - filter(request_metadata(), list == "dr650") - Message - Object of type metadata_request containing: - * type lists - * atlas Australia - * filter Object of class `metadata_filter` with type `list` - -# object of class `metadata-request` formats correctly with `identify() - - Code - identify(request_metadata(), "Crinia") - Message - Object of type metadata_request containing: - * type taxa - * atlas Australia - * identify search_term: Crinia - -# object of class `metadata-request` formats correctly with `identify() |> unnest() - - Code - unnest(identify(request_metadata(), "Crinia")) - Message - Object of type metadata_request containing: - * type taxa-unnest - * atlas Australia - * identify search_term: Crinia - -# object of class `query` formats correctly - - Code - capture(identify(request_metadata(), "Crinia")) - Message - Object of class query with type metadata/taxa-single - * atlas: Australia - * url: https://api.ala.org.au/namematching/api/search?q=Crinia - -# object of class `computed_query` formats correctly - - Code - x - Message - Object of class computed query with type metadata/taxa-single - * atlas: Australia - * url: https://api.ala.org.au/namematching/api/search?q=Crinia - -# object of class `query_set` formats correctly - - Code - compound(filter(authenticate(galah_call(), email = "ala4r@ala.org.au"), - basisOfRecord == "HUMAN_OBSERVATION")) - Message - Object of class query_set containing 5 queries: - * metadata/config url: https://api.ala.org.au/common/api/getAuthConfig - * metadata/fields url: https://api.ala.org.au/occurrences/index/fields - * metadata/assertions url: https://api.ala.org.au/occurrences/assertions/codes - * metadata/reasons url: https://api.ala.org.au/logger/service/logger/reasons - * data/occurrences url: - https://api.ala.org.au/occurrences/occurrences/offline/... - -# `galah_config()` formats correctly - - Code - galah_config() - Message - `galah` package configuration - - Atlas - Atlas of Living Australia (ALA), Australia - - User - x authentication - username [Provided] - email ala4r@ala.org.au - password [Provided] - download_reason_id 10 - - Package - x verbose - v run_checks - x send_email - v caching - i directory: something - - From 70a4bdd7b300ba754c4f03cf58d71f93a4e8e655 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 4 Sep 2026 17:04:40 +1000 Subject: [PATCH 57/70] Minor test improvements - prevent error from wrong crs in bbox testing - remove duplucated GBIF tests - allow for minor discrepancies in repeated count queries for GBIF --- tests/testthat/test-geolocate_bbox.R | 3 +- .../test-international-GBIF-predicates.R | 36 ++++++++++--------- tests/testthat/test-international-GBIF.R | 27 -------------- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/tests/testthat/test-geolocate_bbox.R b/tests/testthat/test-geolocate_bbox.R index 3aaa46b7..786efefb 100644 --- a/tests/testthat/test-geolocate_bbox.R +++ b/tests/testthat/test-geolocate_bbox.R @@ -19,7 +19,8 @@ test_that("`geolocate_bbox()` returns bbox for sf", { test_that("`geolocate_bbox()` returns bbox for shapefile", { poly_path <- test_path("testdata", "act_state_polygon_shp", "ACT_STATE_POLYGON_shp.shp") - shapefile <- sf::st_read(poly_path, quiet = TRUE) + shapefile <- sf::st_read(poly_path, quiet = TRUE) |> + sf::st_transform(sf::st_crs(4326)) shapefile_bbox <- quiet_bbox(shapefile) expected_bbox <- attributes(shapefile_bbox)$bbox expect_message(geolocate_bbox(shapefile), "Data returned for bounding box:") diff --git a/tests/testthat/test-international-GBIF-predicates.R b/tests/testthat/test-international-GBIF-predicates.R index b5fb020d..7726082d 100644 --- a/tests/testthat/test-international-GBIF-predicates.R +++ b/tests/testthat/test-international-GBIF-predicates.R @@ -124,7 +124,7 @@ test_that("`filter()` handles `OR` and `%in%` for GBIF", { filter(basisOfRecord == "HUMAN_OBSERVATION" | basisOfRecord == "PRESERVED_SPECIMEN") |> count() |> collect() - expect_equal(sum(x$count), y$count) + expect_lte((sum(x$count) - y$count) / y$count, 0.01) # check that %in% gives the same result z <- request_data() |> @@ -173,7 +173,7 @@ test_that("`filter()` handles `between()` for GBIF", { expect_true() }) -test_that("filter() handles !() for GBIF", { +test_that("`filter()` handles `!()` for GBIF", { skip_if_offline(); skip_on_ci() # exclude some levels of basisOfRecord @@ -199,7 +199,7 @@ test_that("filter() handles !() for GBIF", { sort(missing_categories)) }) -test_that("filter() handles `is.na()` for GBIF", { +test_that("`filter()` handles `is.na()` for GBIF", { skip_if_offline(); skip_on_ci() # missing values @@ -224,10 +224,11 @@ test_that("filter() handles `is.na()` for GBIF", { z <- galah_call() |> count() |> collect() - expect_equal(x$count + y$count, z$count) + + expect_lte(((x$count + y$count) - z$count) / z$count, 0.01) }) -test_that("filter() handles c() for GBIF", { +test_that("`filter()` handles `c()` for GBIF", { skip_if_offline(); skip_on_ci() # effectively parses this as 'in' as per GBIF instructions @@ -256,13 +257,14 @@ test_that("filter() handles c() for GBIF", { expect_contains(y$country, country_vector) # direct comparison - expect_identical(x, y) + expect_all_true(sort(x$country) == sort(y$country)) + expect_all_true((x$count - y$count) / y$count < 0.01) }) -test_that("`count()` works with `galah_polygon()` for GBIF", { +test_that("`count()` works with `geolocate_polygon()` for GBIF", { skip_if_offline(); skip_on_ci() # errors when points given clockwise - # FIXME: This has been disableed at some point + # FIXME: This has been disabled at some point # `st_sfc()` has a `check_ring_dir` argument that might help # wkt <- "POLYGON((142.36 -29.01,142.74 -29.01,142.74 -29.39,142.36 -29.39,142.36 -29.01))" # expect_error({galah_call() |> @@ -273,7 +275,7 @@ test_that("`count()` works with `galah_polygon()` for GBIF", { wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" result <- galah_call() |> identify("Mammalia") |> - galah_polygon(wkt) |> + geolocate_polygon(wkt) |> count() |> collect() # compare against a taxonomic query in the same place @@ -283,27 +285,27 @@ test_that("`count()` works with `galah_polygon()` for GBIF", { collect() # compare against a purely spatial query result_space <- galah_call() |> - galah_polygon(wkt) |> + geolocate_polygon(wkt) |> count() |> collect() expect_lt(result$count, result_taxa$count) expect_lt(result$count, result_space$count) }) -test_that("`count()` works with `galah_radius()` for GBIF", { +test_that("`count()` works with `geolocate_radius()` for GBIF", { skip_if_offline(); skip_on_ci() # ditto for a point and radius result <- galah_call() |> identify("Mammalia") |> - galah_radius(lat = -33.7, - lon = 151.3, - radius = 5) |> + geolocate_radius(lat = -33.7, + lon = 151.3, + radius = 5) |> count() |> collect() result_space <- galah_call() |> - galah_radius(lat = -33.7, - lon = 151.3, - radius = 5) |> + geolocate_radius(lat = -33.7, + lon = 151.3, + radius = 5) |> count() |> collect() result_taxa <- galah_call() |> diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index deae4981..626ca7b6 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -354,33 +354,6 @@ test_that("invalid fields are caught from `select()` for GBIF", { expect_error() }) -test_that("`count()` works with `geolocate_()` functions for GBIF", { - # NOTE: `atlas_occurrences()` call removed due to high time taken - skip_if_offline(); skip_on_ci() - Sys.sleep(pause_time) - count_total <- request_data(from = "GBIF") |> - identify("Mammalia") |> - count() |> - collect() - Sys.sleep(pause_time) - count_radius <- request_data(from = "GBIF") |> - identify("Mammalia") |> - geolocate_radius(lat = -33.7, - lon = 151.3, - radius = 0.5) |> - count() |> - collect() - Sys.sleep(pause_time) - wkt <- "POLYGON((142.36 -29.01,142.36 -29.39,142.74 -29.39,142.74 -29.01,142.36 -29.01))" - count_polygon <- request_data(from = "GBIF") |> - identify("Mammalia") |> - geolocate_polygon(wkt) |> - count() |> - collect() - expect_lt(count_radius$count, count_total$count) - expect_lt(count_polygon$count, count_total$count) -}) - test_that("`atlas_species()` works for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) From bb4666be3b09997bae30109db078006b74839e64 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:36:24 +1000 Subject: [PATCH 58/70] Use cli in startup message --- R/onload.R | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/R/onload.R b/R/onload.R index e6566344..6d7409e9 100644 --- a/R/onload.R +++ b/R/onload.R @@ -54,17 +54,20 @@ set_up_potions <- function(){ # NOTE: This message *must* have the following classes to enable them # to be controlled programmatically. # see ?packageStartupMessage (required by `check()`) - c( - glue::glue("galah version {galah_version}"), - "\n", # note: glue wipes newlines, so have to be outside - cli::col_magenta(glue::glue('This package is currently configured to query {current_node} ({current_url}).\n')), - "\n", - i = cli::col_magenta('- You can change this setting globally using using e.g. `galah_config(atlas = \"GBIF\")`.'), - "\n", - i = cli::col_magenta('- Or for a single query by opening your pipe with e.g. `request_data(from = "Spain")`.'), - "\n", - i = cli::col_magenta('- To see *all* supported organisations, run `show_all(atlases)`.') - ) |> + startup_message <- function() { + lines <- cli::cli_fmt({ + cli::cli_text("galah version {galah_version}") + cli::cli_bullets(c( + "*" = cli::col_magenta("This package is currently configured to query {current_node} ({current_url})."), + "i" = cli::col_magenta('Change this setting globally by using e.g. `galah_config(atlas = \"GBIF\")`.'), + " " = cli::col_magenta('Or for a single query by opening your pipe with e.g. `galah_call(from = "Spain")`.'), + "i" = cli::col_magenta('See {.strong all} supported organisations with `show_all(atlases)`.') + )) + }) + paste(lines, collapse = "\n") + } + + startup_message() |> packageStartupMessage() } } From 1a4a65e6ba140a6823b593f62501c57c0e6b97aa Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:36:49 +1000 Subject: [PATCH 59/70] Add `from` example to `galah_call()` --- R/galah_call.R | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/R/galah_call.R b/R/galah_call.R index 2f923bbb..08510d8b 100644 --- a/R/galah_call.R +++ b/R/galah_call.R @@ -73,6 +73,13 @@ #' identify("Eolophus") |> #' filter(year > 2000 & year < 2005) |> #' collect() +#' +#' # Use `from` argument to change query organisation +#' galah_call(from = "Spain") |> +#' identify("Aves") |> +#' filter(year > 2000 & year < 2005) |> +#' count() |> +#' collect() #' } #' @export galah_call <- function(type = c("occurrences", From 28deb6d44bd33b26a37a39236d8812df5c98a961 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:37:18 +1000 Subject: [PATCH 60/70] add example to `authenticate()` docs --- R/authenticate.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/R/authenticate.R b/R/authenticate.R index 5210b416..7b2d819c 100644 --- a/R/authenticate.R +++ b/R/authenticate.R @@ -20,6 +20,12 @@ #' @returns An object of the same class as supplied, but with an added #' `authenticate` slot. #' @examples \dontrun{ +#' # Authenticate occurrence queries within a pipe without using `galah_config()` +#' galah_call() |> +#' authenticate(email = "your-email@email.com") |> +#' identify("Wollemia nobilis") |> +#' collect() +#' #' # use `galah_config()` to set for all occurrence queries #' galah_config(authenticate = TRUE) #' From a8558834dd8262cf0bd628b278783808db483e71 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:31:54 +1000 Subject: [PATCH 61/70] Update docs on `all_fields`, remove from code --- R/show_values.R | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/R/show_values.R b/R/show_values.R index cbbbcb70..2a84dbbd 100644 --- a/R/show_values.R +++ b/R/show_values.R @@ -28,18 +28,10 @@ #' #' @param df A search result from [search_fields()], [search_profiles()] or #' [search_lists()]. -#' @param all_fields `r lifecycle::badge("experimental")` If `TRUE`, -#' `show_values()` also returns all columns available from the API, rather -#' than the 'default' columns traditionally provided via galah. -#' -#' For lists, this will include 'raw' columns; columns included prior to the -#' dataset's ingestion into the ALA, and will often include raw scientific -#' names and vernacular names. For conservation lists like the EPBC list, this -#' also includes columns containing each species' conservation status -#' information. -#' -#' For other forms of metadata, setting this to `TRUE` may return more -#' information than you want or need. Default is set to `FALSE`. +#' @param all_fields `r lifecycle::badge("deprecated")` +#' By default all columns available from the API are returned as of v2.3.0. +#' Use `request_metadata()` with `select()` to refine which columns are +#' returned in your query instead. #' @return A `tibble` of values for a specified field, profile or list. #' @examples \dontrun{ #' # Show values in field 'cl22' @@ -60,6 +52,14 @@ #' # See items within species list "dr19257" #' search_lists("dr19257") |> #' show_values() +#' +#' # Refine which columns are returned with `request_metadata()` and `select()`. +#' # This example is synonymous to the previous example, but with `select()`: +#' request_metadata() |> +#' filter(list == "dr19257") |> +#' unnest() |> +#' select(taxon_concept_id, supplied_name, scientific_name) |> +#' collect() #' } #' @export show_values <- function(df, @@ -96,18 +96,10 @@ show_values <- function(df, } } - if(isTRUE(all_fields)){ - request_metadata() |> - filter({{type}} == {{match_name}}) |> - select(tidyselect::everything()) |> - unnest() |> - collect() - }else{ - request_metadata() |> - filter({{type}} == {{match_name}}) |> - unnest() |> - collect() - } + request_metadata() |> + filter({{type}} == {{match_name}}) |> + unnest() |> + collect() } #' @param query A string specifying a search term. Not case sensitive. From 8f8b462bface4054245c5100fb2277ba1888fe72 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 8 Sep 2026 11:58:16 +1000 Subject: [PATCH 62/70] Show values for lists with empty `properties` #300 * Only supply message about duplicated taxon_concept_ids when they exist * Add tests for duplicated taxon_concept_id in lists messages --- R/collect_metadata_unnest.R | 64 +++++++++++++++++++------------ tests/testthat/test-show_values.R | 29 ++++++++++++++ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 2ec8e7f7..2025f47c 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -100,10 +100,13 @@ collect_lists_unnest <- function(.query){ filter(dplyr::n() > 1, .by = .data$taxon_concept_id) |> distinct(.data$taxon_concept_id) |> nrow() - bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", - "i" = "This happens because {.field taxon_concept_id} can match to multiple {.field supplied_name} values.", - "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") - cli::cli_warn(bullets) + + if(duplicate_taxa > 0) { + bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", + "i" = "This happens because {.field taxon_concept_id} can match multiple {.field supplied_name} values with unique metadata", + "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") + cli::cli_warn(bullets) + } return(x) } @@ -143,14 +146,15 @@ clean_kvp_values <- function(df){ #' @noRd #' @keywords Internal parse_properties <- function(df, .query){ - + + # second, `properties` contains status information and other raw fields. if(any(colnames(df) == "properties")){ - # second, `properties` contains status information and other raw fields. + simple_columns <- df |> dplyr::select(tidyselect::any_of( c("taxon_concept_id", "supplied_name", "scientific_name", "properties"))) - # where `properties` contains multipe key:value pairs per row, we have to unnest + # where `properties` contains multiple key:value pairs per row, we have to unnest # or the code breaks. single_properties_check <- purrr::map(df$properties, \(a){length(a) == 2 & all(names(a)[1:2] == c("key", "value"))}) |> @@ -161,26 +165,36 @@ parse_properties <- function(df, .query){ }else{ raw_columns <- simple_columns } - - # resume pipe - raw_columns <- raw_columns |> - tidyr::unnest_wider(.data$properties, names_sep = "_") |> - dplyr::mutate(key = camel_to_snake_case(.data$properties_key)) |> - dplyr::mutate(key = dplyr::if_else(.data$key %in% colnames(df), glue::glue("{key}_raw"), .data$key)) |> # rename prior to pivot to avoid name conflicts - tidyr::pivot_wider(names_from = "key", - values_from = "properties_value", - names_repair = "minimal", - values_fn = list) |> - tidyr::unnest(cols = tidyselect::everything()) - # merge - result_final <- df |> - dplyr::left_join(raw_columns, - dplyr::join_by("taxon_concept_id", "supplied_name", "scientific_name")) |> - dplyr::select(-tidyselect::starts_with("properties")) |> # also excludes `properties_key` - parse_rename(.query) - + # if unnested properties column is not empty, reformat + if(nrow(raw_columns) > 0) { + # resume pipe + raw_columns <- raw_columns |> + tidyr::unnest_wider(.data$properties, names_sep = "_") |> + dplyr::mutate(key = camel_to_snake_case(.data$properties_key)) |> + dplyr::mutate(key = dplyr::if_else(.data$key %in% colnames(df), glue::glue("{key}_raw"), .data$key)) |> # rename prior to pivot to avoid name conflicts + tidyr::pivot_wider(names_from = "key", + values_from = "properties_value", + names_repair = "minimal", + values_fn = list) |> + tidyr::unnest(cols = tidyselect::everything()) + + # merge + result_final <- df |> + dplyr::left_join(raw_columns, + dplyr::join_by("taxon_concept_id", "supplied_name", "scientific_name")) |> + dplyr::select(-tidyselect::starts_with("properties")) |> # also excludes `properties_key` + parse_rename(.query) + + # if unnested properties column is empty, remove it + } else { + result_final <- simple_columns |> + dplyr::select(-tidyselect::starts_with("properties")) |> # also excludes `properties_key` + parse_rename(.query) + } + return(result_final) + }else{ return(df) } diff --git a/tests/testthat/test-show_values.R b/tests/testthat/test-show_values.R index 8f708d0e..9ac260b2 100644 --- a/tests/testthat/test-show_values.R +++ b/tests/testthat/test-show_values.R @@ -124,4 +124,33 @@ test_that("`show_values()` all_fields = TRUE works for lists", { expect_true() }) +test_that("`show_values()` returns message when there are duplicated taxon concept ids", { + skip_if_offline(); skip_on_ci() + # simple, fake version for testing `show_values()` + df <- tibble::tibble(species_list_uid = "dr650") + attr(df, "call") <- "lists" + show_values_query <- purrr_values(df) + # NOTE: above is same as following code, but much faster + # search <- search_all(lists, "dr650") |> + # show_values(all_fields = TRUE) + + # warning expected + expect_match(show_values_query$warnings, + "^List contains") +}) + +test_that("`show_values()` doesn't return message when there are no duplicated taxon concept ids", { + skip_if_offline(); skip_on_ci() + # simple, fake version for testing `show_values()` + df <- tibble::tibble(species_list_uid = "dr30561") + attr(df, "call") <- "lists" + show_values_query <- purrr_values(df) + # NOTE: above is same as following code, but much faster + # search <- search_all(lists, "dr650") |> + # show_values(all_fields = TRUE) + + # warning not expected + expect_length(show_values_query$warnings, 0) +}) + rm(purrr_values, quiet_values, purrr_search, quiet_search) \ No newline at end of file From ea748111788abd285062b4105a675096afc095a7 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:19:57 +1000 Subject: [PATCH 63/70] Add complex string test #303 --- tests/testthat/test-search_taxa.R | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/testthat/test-search_taxa.R b/tests/testthat/test-search_taxa.R index 412f9c1a..89d1e6e8 100644 --- a/tests/testthat/test-search_taxa.R +++ b/tests/testthat/test-search_taxa.R @@ -85,6 +85,14 @@ test_that("`search_taxa()` errors nicely when piped in `galah_call()`", { expect_error(galah_call() |> search_taxa("perameles"), "Can't pipe `search_taxa()") }) +test_that("`search_taxa()` handles complex strings", { + skip_if_offline(); skip_on_ci() + taxa <- search_taxa("Commelina sp. (Lakefield NP P.I.Forster+ PIF12943)") + expect_s3_class(taxa, c("tbl_df", "tbl", "data.frame")) + expect_equal(taxa$scientific_name, "Commelina sp. Lakefield NP (P.I.Forster+ PIF12943)") + expect_equal(nrow(taxa), 1) +}) + test_that("`search_identifiers()` searches using identifier", { skip_if_offline(); skip_on_ci() # check different types of id From 3b05899f14ae8d80404e5b9a7da314e874a01536 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:21:31 +1000 Subject: [PATCH 64/70] Remove `all_fields` from `show_values()` tests --- tests/testthat/test-show_values.R | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/testthat/test-show_values.R b/tests/testthat/test-show_values.R index 9ac260b2..4a678582 100644 --- a/tests/testthat/test-show_values.R +++ b/tests/testthat/test-show_values.R @@ -102,23 +102,25 @@ test_that("`show_values()` returns unformatted names", { expected) }) -test_that("`show_values()` all_fields = TRUE works for lists", { +test_that("`show_values()` returns nested columns within 'properties'", { skip_if_offline(); skip_on_ci() # simple, fake version for testing `show_values()` df <- tibble::tibble(species_list_uid = "dr650") attr(df, "call") <- "lists" - show_values_query <- quiet_values(df, all_fields = TRUE) + show_values_query <- quiet_values(df) # NOTE: above is same as following code, but much faster # search <- search_all(lists, "dr650") |> - # show_values(all_fields = TRUE) + # show_values() + extra_cols <- c("raw_scientificName", "status", "sourceStatus", "IUCN_equivalent_status") expect_s3_class(show_values_query, c("tbl_df", "tbl", "data.frame")) expect_gt(nrow(show_values_query), 0) expect_true(any(colnames(show_values_query) %in% extra_cols)) expect_gt(ncol(show_values_query), 6) # adds additional columns + # doesn't work for fields x <- search_all(fields, "cl22") |> - purrr_values(all_fields = TRUE) + purrr_values() stringr::str_detect(x$messages, "cl22") |> any() |> expect_true() @@ -132,7 +134,7 @@ test_that("`show_values()` returns message when there are duplicated taxon conce show_values_query <- purrr_values(df) # NOTE: above is same as following code, but much faster # search <- search_all(lists, "dr650") |> - # show_values(all_fields = TRUE) + # show_values() # warning expected expect_match(show_values_query$warnings, @@ -147,7 +149,7 @@ test_that("`show_values()` doesn't return message when there are no duplicated t show_values_query <- purrr_values(df) # NOTE: above is same as following code, but much faster # search <- search_all(lists, "dr650") |> - # show_values(all_fields = TRUE) + # show_values() # warning not expected expect_length(show_values_query$warnings, 0) From 74b443698c409bbe231bbb93d4f54467e8ac6d10 Mon Sep 17 00:00:00 2001 From: Dax Kellie <43838326+daxkellie@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:22:24 +1000 Subject: [PATCH 65/70] Update NEWS with fixes * Add `all_fields`, `geolocate()` fix and complex string parsing fix --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 76214b31..f0287ddd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,10 +14,13 @@ * Misc bug fixes for `atlas_occurrences()`, `glimpse()` and `show_values()` for GBIF (#299). * Upgraded to v2 of specieslist API for ALA (#300). * `galah` now auto-populates the `q` parameter for living atlas API calls for server-side performance reasons (#301). +* Added check that object has correct Coordinate Reference System when using `geolocate()` (#302) +* Improved query parsing for more complex text strings (#303) ### Lifecycle * `galah_` prefix functions for piping formally superseded by their `dplyr` alternatives. * Using `filter`, `select` etc as arguments to the `atlas_` family formally superseded in favor of piping. +* `all_fields` argument in `show_values()` used to return all columns in a specified list superseded to supplying all columns by default. # galah 2.2.0 From 41c8b7900139c66fea04b22c66e27d86e6f69709 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 16 Sep 2026 18:58:46 +1000 Subject: [PATCH 66/70] reinstate `galah_call()` in place of `request_data()` in QSG --- vignettes/quick_start_guide.Rmd | 128 +++++++++++++++------------ vignettes/quick_start_guide.Rmd.orig | 30 ++++--- 2 files changed, 89 insertions(+), 69 deletions(-) diff --git a/vignettes/quick_start_guide.Rmd b/vignettes/quick_start_guide.Rmd index 7a09b7cc..5cfd25d1 100644 --- a/vignettes/quick_start_guide.Rmd +++ b/vignettes/quick_start_guide.Rmd @@ -1,7 +1,7 @@ --- title: "Quick start guide" author: "Martin Westgate & Dax Kellie" -date: '2026-05-02' +date: '2026-09-16' output: rmarkdown::html_vignette vignette: > @@ -42,14 +42,14 @@ library(galah) a `tibble` in your workspace, you amend a query, which is then sent to your chosen organisation. These pipes differ from traditional syntax in two ways: -- they begin with a function - usually `request_data()` - instead of a `tibble` +- they begin with a function - usually `galah_call()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` So an example query might be to find the number of records: ``` r -request_data() |> # open a pipe +galah_call() |> # open a pipe count() |> # count the number of rows collect() # retrieve query from the server ``` @@ -58,7 +58,7 @@ request_data() |> # open a pipe ## # A tibble: 1 × 1 ## count ## -## 1 179728084 +## 1 184796858 ``` By default, queries are sent to the Atlas of Living Australia, but this can @@ -66,16 +66,16 @@ be changed using the `from` argument: ``` r -request_data(from = "Sweden") |> +galah_call(from = "Spain") |> count() |> collect() ``` ``` ## # A tibble: 1 × 1 -## count -## -## 1 173135256 +## count +## +## 1 59852882 ``` The `from` argument accepts the `acronym`, `region`, or `institution` fields from @@ -92,7 +92,7 @@ You can pass taxonomic filters to your query with `identify()`: ``` r -request_data() |> +galah_call() |> identify("Eolophus roseicapilla") |> count() |> collect() @@ -102,7 +102,7 @@ request_data() |> ## # A tibble: 1 × 1 ## count ## -## 1 1594059 +## 1 1593848 ``` To choose records using non-taxonomic criteria, you'll need to use `filter()`. @@ -115,13 +115,13 @@ You can answer the first question with `describe()`: ``` r -request_data() |> +galah_call() |> describe() |> collect() ``` ``` -## # A tibble: 581 × 3 +## # A tibble: 582 × 3 ## id description data_type ## ## 1 acceptedNameUsage Accepted name string @@ -134,7 +134,7 @@ request_data() |> ## 8 assertionsCount int ## 9 associatedMedia Associated Media string ## 10 associatedOccurrences Associated Occurrences string -## # ℹ 571 more rows +## # ℹ 572 more rows ``` Once you find a variable you are interested in, see what values it contains @@ -142,7 +142,7 @@ using `distinct()`: ``` r -request_data() |> +galah_call() |> distinct(basisOfRecord) |> collect() ``` @@ -154,8 +154,8 @@ request_data() |> ## 1 HUMAN_OBSERVATION ## 2 PRESERVED_SPECIMEN ## 3 OCCURRENCE -## 4 MACHINE_OBSERVATION -## 5 OBSERVATION +## 4 OBSERVATION +## 5 MACHINE_OBSERVATION ## 6 MATERIAL_SAMPLE ## 7 LIVING_SPECIMEN ## 8 FOSSIL_SPECIMEN @@ -166,7 +166,7 @@ Then you can combine these within a `filter()` query: ``` r -request_data() |> +galah_call() |> filter(basisOfRecord == "PRESERVED_SPECIMEN") |> count() |> collect() @@ -176,26 +176,32 @@ request_data() |> ## # A tibble: 1 × 1 ## count ## -## 1 18189911 +## 1 18419249 ``` These functions can be combined in a number of ways, for example to -learn how many species are present within the genus 'Crinia': +learn how many species from the genus 'Crinia' have records from +each country with records in the ALA: ``` r -request_data() |> +galah_call() |> identify("Crinia") |> # filters by taxonomic names - distinct(speciesID) |> # keep only unique values + group_by(country) |> # grouping variable + distinct(species) |> # keep only unique values count() |> collect() ``` ``` -## # A tibble: 1 × 1 -## count -## -## 1 17 +## # A tibble: 5 × 2 +## country count +## +## 1 Australia 17 +## 2 Papua New Guinea 2 +## 3 New Zealand 1 +## 4 Indonesia 0 +## 5 Solomon Islands 1 ``` You can `glimpse()` a data download before you run it, to check all the data @@ -203,7 +209,7 @@ you need is included: ``` r -request_data() |> +galah_call() |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> glimpse() |> @@ -211,16 +217,16 @@ request_data() |> ``` ``` -## Rows: 27,556 +## Rows: 26,430 ## Columns: 8 -## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/a586dca0-4717-451a-90da-b65518a52ee3", "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/a38ac5ac-3049-4fde-8391-faf8bf7aeb7f" -## $ eventDate 1.267402e+12, 1.272586e+12, 1.282781e+12 -## $ scientificName "Eolophus roseicapilla roseicapilla", "Eolophus roseicapilla", "Eolophus roseicapilla albiceps" -## $ decimalLatitude -30.91667, -36.42500, -20.52361 -## $ decimalLongitude 150.2500, 144.6583, 145.3958 +## $ taxonConceptID "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea", "https://biodiversity.org.au/afd/taxa/a38ac5ac-3049-4fde-8391-faf8bf7aeb7f" +## $ eventDate 1.278833e+12, 1.286170e+12, 1.292803e+12 +## $ scientificName "Eolophus roseicapilla", "Eolophus roseicapilla", "Eolophus roseicapilla albiceps" +## $ decimalLatitude -27.57463, -16.06593, -35.88444 +## $ decimalLongitude 153.4350, 136.3067, 145.9089 ## $ basisOfRecord "HUMAN_OBSERVATION", "HUMAN_OBSERVATION", "HUMAN_OBSERVATION" -## $ dataResourceName "New South Wales Bird Atlassers", "Victorian Biodiversity Atlas", "BirdLife Australia, Birdata" -## $ occurrenceStatus "PRESENT", "PRESENT", "PRESENT" +## $ dataResourceName "eBird Australia", "eBird Australia", "BirdLife Australia, Birdata" +## $ otherProperties ["PRESENT"], ["PRESENT"], ["PRESENT"] ``` And, once satisfied that your parameters are correct, download the records @@ -229,7 +235,7 @@ themselves: ``` r -request_data() |> +galah_call() |> authenticate(email = "registered_email@wherever.com") |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> @@ -239,7 +245,7 @@ request_data() |> ``` -## Request for 27556 occurrences placed in queue. +## Request for 26430 occurrences placed in queue. ``` ``` @@ -247,24 +253,32 @@ request_data() |> ``` ``` -## --- +## -- ``` ``` -## # A tibble: 27,556 × 3 +## ℹ Downloading +``` + +``` +## ✔ Downloading [253ms] +``` + +``` +## # A tibble: 26,430 × 3 ## eventDate decimalLatitude species ## -## 1 NA -36.3 Eolophus roseicapilla -## 2 NA -38.2 Eolophus roseicapilla -## 3 NA -37.0 Eolophus roseicapilla -## 4 NA -37.7 Eolophus roseicapilla -## 5 NA -35.6 Eolophus roseicapilla -## 6 NA -31.1 Eolophus roseicapilla -## 7 NA -38.2 Eolophus roseicapilla -## 8 NA -36.8 Eolophus roseicapilla -## 9 NA -38.2 Eolophus roseicapilla -## 10 NA -38.2 Eolophus roseicapilla -## # ℹ 27,546 more rows +## 1 NA -37.1 Eolophus roseicapilla +## 2 NA -37.0 Eolophus roseicapilla +## 3 NA -37.1 Eolophus roseicapilla +## 4 NA -37.1 Eolophus roseicapilla +## 5 NA -37.2 Eolophus roseicapilla +## 6 NA -37.0 Eolophus roseicapilla +## 7 NA -37.0 Eolophus roseicapilla +## 8 NA -37.0 Eolophus roseicapilla +## 9 NA -37.0 Eolophus roseicapilla +## 10 NA -37.1 Eolophus roseicapilla +## # ℹ 26,420 more rows ``` # Architecture @@ -318,9 +332,9 @@ galah_call() |> ``` ## # A tibble: 1 × 1 -## count -## -## 1 9913135 +## count +## +## 1 10781991 ``` Or occurrences: @@ -336,7 +350,11 @@ galah_call() |> ``` ``` -## --# A tibble: 2,318 × 9 +## ----- +``` + +``` +## # A tibble: 2,318 × 9 ## recordID scientificName taxonConceptID decimalLatitude decimalLongitude eventDate basisOfRecord occurrenceStatus dataResourceName ## ## 1 0026d29f-b6ab-4a1d-9c57-6ee12cfde3a0 Eolophus roseicapilla https://biodiversity.org.au/afd/taxa/9b4ad548-8bb3-486a-ab0a-905506c463ea -35.4 149. 2000-08-07 00:00:00 HUMAN_OBSERVATION PRESENT Garden Bird Surveys @@ -361,7 +379,7 @@ show_all(fields) ``` ``` -## # A tibble: 581 × 3 +## # A tibble: 582 × 3 ## id description type ## ## 1 acceptedNameUsage Accepted name fields @@ -374,7 +392,7 @@ show_all(fields) ## 8 assertionsCount fields ## 9 associatedMedia Associated Media fields ## 10 associatedOccurrences Associated Occurrences fields -## # ℹ 571 more rows +## # ℹ 572 more rows ``` You can check the diff --git a/vignettes/quick_start_guide.Rmd.orig b/vignettes/quick_start_guide.Rmd.orig index d3330e85..46a4400a 100644 --- a/vignettes/quick_start_guide.Rmd.orig +++ b/vignettes/quick_start_guide.Rmd.orig @@ -39,13 +39,13 @@ library(galah) a `tibble` in your workspace, you amend a query, which is then sent to your chosen organisation. These pipes differ from traditional syntax in two ways: -- they begin with a function - usually `request_data()` - instead of a `tibble` +- they begin with a function - usually `galah_call()` - instead of a `tibble` - they end with one of `dplyr`'s evaluation functions, usually `collect()` So an example query might be to find the number of records: ```{r, count-example} -request_data() |> # open a pipe +galah_call() |> # open a pipe count() |> # count the number of rows collect() # retrieve query from the server ``` @@ -53,8 +53,8 @@ request_data() |> # open a pipe By default, queries are sent to the Atlas of Living Australia, but this can be changed using the `from` argument: -```{r, sbdi-example} -request_data(from = "Sweden") |> +```{r, spain-example} +galah_call(from = "Spain") |> count() |> collect() ``` @@ -71,7 +71,7 @@ knitr::include_graphics('../man/figures/atlases_plot.png') You can pass taxonomic filters to your query with `identify()`: ```{r, identify-example} -request_data() |> +galah_call() |> identify("Eolophus roseicapilla") |> count() |> collect() @@ -86,7 +86,7 @@ This reqires that you find out: You can answer the first question with `describe()`: ```{r, describe-example} -request_data() |> +galah_call() |> describe() |> collect() ``` @@ -95,7 +95,7 @@ Once you find a variable you are interested in, see what values it contains using `distinct()`: ```{r, distinct-example} -request_data() |> +galah_call() |> distinct(basisOfRecord) |> collect() ``` @@ -103,19 +103,21 @@ request_data() |> Then you can combine these within a `filter()` query: ```{r, count-filter-example} -request_data() |> +galah_call() |> filter(basisOfRecord == "PRESERVED_SPECIMEN") |> count() |> collect() ``` These functions can be combined in a number of ways, for example to -learn how many species are present within the genus 'Crinia': +learn how many species from the genus 'Crinia' have records from +each country with records in the ALA: ```{r, species-count-example} -request_data() |> +galah_call() |> identify("Crinia") |> # filters by taxonomic names - distinct(speciesID) |> # keep only unique values + group_by(country) |> # grouping variable + distinct(species) |> # keep only unique values count() |> collect() ``` @@ -124,7 +126,7 @@ You can `glimpse()` a data download before you run it, to check all the data you need is included: ```{r, glimpse-example} -request_data() |> +galah_call() |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> glimpse() |> @@ -136,7 +138,7 @@ themselves: ```{r, occurrence-example, eval = FALSE} -request_data() |> +galah_call() |> authenticate(email = "registered_email@wherever.com") |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> @@ -145,7 +147,7 @@ request_data() |> ``` ```{r, cache-email, echo = FALSE} -request_data() |> +galah_call() |> authenticate(email = "ala4r@ala.org.au") |> identify("Eolophus roseicapilla") |> filter(year == 2010) |> From ebe4f26ea14864e04fb4a4bc54f9deddad5ce0ba Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 16 Sep 2026 19:00:25 +1000 Subject: [PATCH 67/70] render recent changes to help files --- man/authenticate.Rd | 6 ++++++ man/galah_call.Rd | 7 +++++++ man/show_values.Rd | 24 ++++++++++++------------ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/man/authenticate.Rd b/man/authenticate.Rd index a170a05d..df837362 100644 --- a/man/authenticate.Rd +++ b/man/authenticate.Rd @@ -49,6 +49,12 @@ pass cached information stored via \code{\link[=galah_config]{galah_config()}} t } \examples{ \dontrun{ +# Authenticate occurrence queries within a pipe without using `galah_config()` +galah_call() |> + authenticate(email = "your-email@email.com") |> + identify("Wollemia nobilis") |> + collect() + # use `galah_config()` to set for all occurrence queries galah_config(authenticate = TRUE) diff --git a/man/galah_call.Rd b/man/galah_call.Rd index 3bce59c1..cad472e1 100644 --- a/man/galah_call.Rd +++ b/man/galah_call.Rd @@ -92,6 +92,13 @@ galah_call() |> identify("Eolophus") |> filter(year > 2000 & year < 2005) |> collect() + +# Use `from` argument to change query organisation +galah_call(from = "Spain") |> + identify("Aves") |> + filter(year > 2000 & year < 2005) |> + count() |> + collect() } } \seealso{ diff --git a/man/show_values.Rd b/man/show_values.Rd index 77a98534..454958a2 100644 --- a/man/show_values.Rd +++ b/man/show_values.Rd @@ -13,18 +13,10 @@ search_values(df, query) \item{df}{A search result from \code{\link[=search_fields]{search_fields()}}, \code{\link[=search_profiles]{search_profiles()}} or \code{\link[=search_lists]{search_lists()}}.} -\item{all_fields}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} If \code{TRUE}, -\code{show_values()} also returns all columns available from the API, rather -than the 'default' columns traditionally provided via galah. - -For lists, this will include 'raw' columns; columns included prior to the -dataset's ingestion into the ALA, and will often include raw scientific -names and vernacular names. For conservation lists like the EPBC list, this -also includes columns containing each species' conservation status -information. - -For other forms of metadata, setting this to \code{TRUE} may return more -information than you want or need. Default is set to \code{FALSE}.} +\item{all_fields}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} +By default all columns available from the API are returned as of v2.3.0. +Use \code{request_metadata()} with \code{select()} to refine which columns are +returned in your query instead.} \item{query}{A string specifying a search term. Not case sensitive.} } @@ -83,5 +75,13 @@ search_fields("cl22") |> # See items within species list "dr19257" search_lists("dr19257") |> show_values() + +# Refine which columns are returned with `request_metadata()` and `select()`. +# This example is synonymous to the previous example, but with `select()`: +request_metadata() |> + filter(list == "dr19257") |> + unnest() |> + select(taxon_concept_id, supplied_name, scientific_name) |> + collect() } } From a091f0ff646839fbff3734d90a9213414788c24c Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Wed, 16 Sep 2026 19:05:07 +1000 Subject: [PATCH 68/70] Fix bug preventing `.onLoad()` from working --- R/onload.R | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/R/onload.R b/R/onload.R index 6d7409e9..358f8464 100644 --- a/R/onload.R +++ b/R/onload.R @@ -17,6 +17,12 @@ set_up_potions <- function(){ if(is.null(stored_options)){ # set up storage of standard information via {potions} potions::brew(default_config(), .pkg = "galah") # set up caching of behaviour + }else if(length(stored_options$packages$galah) < 1L){ + # this is necessary because `drain_package()` often leaves the *name* + # of the package, but not the *content*. In which case we need to replace it. + # This particularly hits the test suite, which calls + # `pkgload:::run_pkg_hook(package, "attach")` internally + potions::brew(default_config(), .pkg = "galah") } } @@ -49,26 +55,27 @@ set_up_potions <- function(){ dplyr::filter(.data$acronym == current_node) |> dplyr::pull("url") |> stringr::str_replace("^https://", "") - - # display a message - # NOTE: This message *must* have the following classes to enable them - # to be controlled programmatically. - # see ?packageStartupMessage (required by `check()`) - startup_message <- function() { - lines <- cli::cli_fmt({ - cli::cli_text("galah version {galah_version}") - cli::cli_bullets(c( - "*" = cli::col_magenta("This package is currently configured to query {current_node} ({current_url})."), - "i" = cli::col_magenta('Change this setting globally by using e.g. `galah_config(atlas = \"GBIF\")`.'), - " " = cli::col_magenta('Or for a single query by opening your pipe with e.g. `galah_call(from = "Spain")`.'), - "i" = cli::col_magenta('See {.strong all} supported organisations with `show_all(atlases)`.') - )) - }) - paste(lines, collapse = "\n") - } - startup_message() |> - packageStartupMessage() + # run start-up message + startup_message(node = current_node, + url = current_url, + version = galah_version) |> + packageStartupMessage() # Mandatory: see `?packageStartupMessage` (required by `check()`) } } - \ No newline at end of file + +#' Display a startup message +#' @noRd +#' @keywords Internal +startup_message <- function(node, url, version) { + lines <- cli::cli_fmt({ + cli::cli_text("galah version {version}") + cli::cli_bullets(c( + "*" = cli::col_magenta("This package is currently configured to query {node} ({url})."), + "i" = cli::col_magenta('Change this setting globally by using e.g. `galah_config(atlas = \"GBIF\")`.'), + " " = cli::col_magenta('Or for a single query by opening your pipe with e.g. `galah_call(from = "Spain")`.'), + "i" = cli::col_magenta('See {.strong all} supported organisations with `show_all(atlases)`.') + )) + }) + paste(lines, collapse = "\n") +} From 814989a8776cdc21ab546c4f8317ba9996749e9d Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 18 Sep 2026 16:01:52 +1000 Subject: [PATCH 69/70] Fix minor bugs in list unnesting --- R/collect_metadata_unnest.R | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/R/collect_metadata_unnest.R b/R/collect_metadata_unnest.R index 2025f47c..8ae2beda 100644 --- a/R/collect_metadata_unnest.R +++ b/R/collect_metadata_unnest.R @@ -94,20 +94,23 @@ collect_lists_unnest <- function(.query){ parse_rename(.query) |> parse_properties(.query) |> parse_select(.query) - - # inform user about duplicated taxon_concept_ids - duplicate_taxa <- x |> - filter(dplyr::n() > 1, .by = .data$taxon_concept_id) |> - distinct(.data$taxon_concept_id) |> - nrow() - - if(duplicate_taxa > 0) { - bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", - "i" = "This happens because {.field taxon_concept_id} can match multiple {.field supplied_name} values with unique metadata", - "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") - cli::cli_warn(bullets) + + verbose <- potions::pour("package", "verbose", .pkg = "galah") + if(verbose){ + # inform user about duplicated taxon_concept_ids + duplicate_taxa <- x |> + filter(dplyr::n() > 1, .by = "taxon_concept_id") |> + distinct(.data$taxon_concept_id) |> + nrow() + + if(duplicate_taxa > 0) { + bullets <- c("List contains {duplicate_taxa} taxon_concept_id(s) with > 1 row.", + "i" = "This happens because {.field taxon_concept_id} can match multiple {.field supplied_name} values with unique metadata", + "i" = "To see duplicated rows, save list as object then run: {.code {{your_object}} |> dplyr::filter(dplyr::n() > 1, .by = taxon_concept_id)}") + cli::cli_inform(bullets) + } } - + return(x) } } @@ -161,7 +164,7 @@ parse_properties <- function(df, .query){ unlist() |> all() if(!single_properties_check){ - raw_columns <- simple_columns |> tidyr::unnest(cols = .data$properties) + raw_columns <- simple_columns |> tidyr::unnest(cols = "properties") }else{ raw_columns <- simple_columns } @@ -170,7 +173,7 @@ parse_properties <- function(df, .query){ if(nrow(raw_columns) > 0) { # resume pipe raw_columns <- raw_columns |> - tidyr::unnest_wider(.data$properties, names_sep = "_") |> + tidyr::unnest_wider("properties", names_sep = "_") |> dplyr::mutate(key = camel_to_snake_case(.data$properties_key)) |> dplyr::mutate(key = dplyr::if_else(.data$key %in% colnames(df), glue::glue("{key}_raw"), .data$key)) |> # rename prior to pivot to avoid name conflicts tidyr::pivot_wider(names_from = "key", @@ -208,7 +211,7 @@ parse_classification <- function(df){ if(any(colnames(df) == "classification")){ df <- df |> - tidyr::unnest_wider(.data$classification, + tidyr::unnest_wider("classification", names_repair = "minimal", names_sep = "_") |> # select ALA-matched columns From 16272c51bc1f478d4dfc77c2820d646fff5bc596 Mon Sep 17 00:00:00 2001 From: Martin Westgate Date: Fri, 18 Sep 2026 16:05:20 +1000 Subject: [PATCH 70/70] Minor fixes to tests - force 10-sec delay and `wait = FALSE` for non-ALA queries to prevent very long delays on test suite - fix failed tests resulting from `run_checks` being set to `FALSE` and affecting visibility of text or errors --- tests/testthat/_snaps/print.md | 2 +- tests/testthat/test-describe.R | 2 +- tests/testthat/test-international-Austria.R | 18 +++--- tests/testthat/test-international-Brazil.R | 5 +- .../test-international-GBIF-predicates.R | 4 ++ tests/testthat/test-international-GBIF.R | 58 ++++++++++++++----- tests/testthat/test-international-Kew.R | 9 ++- tests/testthat/test-international-Spain.R | 9 ++- tests/testthat/test-international-Sweden.R | 6 ++ tests/testthat/test-international-UK.R | 22 +++---- tests/testthat/test-print.R | 3 + tests/testthat/test-request_metadata_unnest.R | 2 + tests/testthat/test-show_values.R | 13 +++-- 13 files changed, 108 insertions(+), 45 deletions(-) diff --git a/tests/testthat/_snaps/print.md b/tests/testthat/_snaps/print.md index e58d1678..d30aaae7 100644 --- a/tests/testthat/_snaps/print.md +++ b/tests/testthat/_snaps/print.md @@ -102,7 +102,7 @@ User x authentication username [Provided] - email ala4r@ala.org.au + email random_email_for_testing@email.com password [Provided] download_reason_id 10 diff --git a/tests/testthat/test-describe.R b/tests/testthat/test-describe.R index f71d8fb0..b4341113 100644 --- a/tests/testthat/test-describe.R +++ b/tests/testthat/test-describe.R @@ -33,7 +33,7 @@ test_that("`describe()` adheres to user-provided `select()`", { describe() |> collect() y <- default_columns() - expect_true(all(x$name %in% y)) + expect_true(all(x$id %in% y)) expect_s3_class(x, c("tbl", "tbl_df", "data.frame")) }) diff --git a/tests/testthat/test-international-Austria.R b/tests/testthat/test-international-Austria.R index baa15118..020335d2 100644 --- a/tests/testthat/test-international-Austria.R +++ b/tests/testthat/test-international-Austria.R @@ -187,7 +187,7 @@ test_that("`count()` works with group_by for Austria", { expect_equal(names(result), c("year", "count")) }) -test_that("atlas_species works for Austria", { +test_that("`atlas_species()` works for Austria", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Austria", @@ -206,7 +206,7 @@ test_that("atlas_species works for Austria", { }) ## FIXME: Test only works when run_checks = TRUE -test_that("atlas_occurrences works for Austria", { +test_that("`atlas_occurrences()` works for Austria", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Austria", @@ -231,19 +231,23 @@ test_that("atlas_occurrences works for Austria", { c("type", "atlas", "url", "headers", "request")) expect_equal(occ_collapse$type, "data/occurrences") # compute - occ_compute <- compute(occ_collapse) + occ_compute <- compute(occ_collapse) |> + try(silent = TRUE) + skip_if(inherits(occ_collapse, "try-error"), message = "API not available") expect_s3_class(occ_compute, "computed_query") - # collect - occ <- collect(occ_compute) |> + # collect with pause of 20 seconds and `wait = FALSE` + # This is clumsy, but useful beacuse previously we were getting wait times of ~10 minutes + Sys.sleep(20) + occ <- collect(occ_compute, wait = FALSE) |> try(silent = TRUE) - skip_if(inherits(occ_compute, "try-error"), message = "API not available") + skip_if(inherits(occ, c("try-error", "computed_query")), message = "API not available") expect_equal(nrow(occ), counts$count[1]) expect_s3_class(occ, c("tbl_df", "tbl", "data.frame")) expect_equal(ncol(occ), 2) unlink("temp", recursive = TRUE) }) -test_that("atlas_media() works for Austria", { +test_that("`atlas_media()` works for Austria", { skip_if_offline(); skip_on_ci() galah_config( atlas = "Austria", diff --git a/tests/testthat/test-international-Brazil.R b/tests/testthat/test-international-Brazil.R index d017e4e8..8f131233 100644 --- a/tests/testthat/test-international-Brazil.R +++ b/tests/testthat/test-international-Brazil.R @@ -199,10 +199,11 @@ test_that("`atlas_occurrences()` works for Brazil", { try(silent = TRUE) skip_if(inherits(occ_compute, "try-error"), message = "API not available") expect_s3_class(occ_compute, "computed_query") + Sys.sleep(20) occ <- occ_compute |> - collect(wait = TRUE) |> + collect(wait = FALSE) |> try(silent = TRUE) - skip_if(inherits(occ, "try-error"), message = "API not available") + skip_if(inherits(occ, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(occ), 0) expect_equal(ncol(occ), 9) expect_true(inherits(occ, c("tbl_df", "tbl", "data.frame"))) diff --git a/tests/testthat/test-international-GBIF-predicates.R b/tests/testthat/test-international-GBIF-predicates.R index 7726082d..fe90ffbe 100644 --- a/tests/testthat/test-international-GBIF-predicates.R +++ b/tests/testthat/test-international-GBIF-predicates.R @@ -74,6 +74,8 @@ test_that("`filter()` handles multiple (`AND`) queries for GBIF", { test_that("`count()` errors when real but non-indexed fields are requested", { skip_if_offline(); skip_on_ci() + galah_config(run_checks = TRUE) + # invalid fields galah_call() |> filter(something == 9) |> @@ -88,6 +90,8 @@ test_that("`count()` errors when real but non-indexed fields are requested", { count() |> collapse() |> expect_error() + + galah_config(run_checks = FALSE) }) test_that("`count()` works with `identify()` for GBIF", { diff --git a/tests/testthat/test-international-GBIF.R b/tests/testthat/test-international-GBIF.R index 626ca7b6..364e5148 100644 --- a/tests/testthat/test-international-GBIF.R +++ b/tests/testthat/test-international-GBIF.R @@ -66,11 +66,13 @@ test_that("`show_values()` works for GBIF fields", { test_that("`show_values()` fails for unsearchable GBIF fields", { skip_if_offline(); skip_on_ci() - request_metadata(from = "GBIF") |> + galah_config(run_checks = TRUE) + x <- request_metadata(from = "GBIF") |> filter(field == "accessRights") |> unnest() |> collapse() |> expect_error() + galah_config(run_checks = FALSE) }) test_that("`show_all(collections)` works for GBIF", { @@ -215,6 +217,7 @@ test_that("`count()` works with 2 `group_by` args for GBIF", { y <- compute(x) expect_s3_class(y, "computed_query") # collect + Sys.sleep(pause_time * 5) z <- collect(y) expect_s3_class(z, c("tbl_df", "tbl", "data.frame")) expect_gt(nrow(z), 1) @@ -260,7 +263,10 @@ test_that("`count()` works with `identify` for GBIF when `run_checks` = TRUE", { y <- compute(x) expect_s3_class(y, "computed_query") # collect - z <- collect(y) + Sys.sleep(pause_time * 5) + z <- collect(y, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(z, c("try-error", "computed_query")), message = "API not available") expect_s3_class(z, c("tbl_df", "tbl", "data.frame")) expect_gt(z$count, 1) expect_equal(nrow(z), 1) @@ -318,11 +324,15 @@ test_that("`glimpse()` works with `identify()` for GBIF", { test_that("`atlas_occurrences()` works for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) - x <- galah_call(from = "GBIF") |> + x_compute <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", country == "AU") |> - collect() + compute() + Sys.sleep(pause_time * 5) + x <- collect(x_compute, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(x, c("try-error", "computed_query")), message = "API not available") expect_s3_class(x, c("tbl_df", "tbl", "data.frame")) expect_gt(nrow(x), 10) expect_gt(ncol(x), 10) @@ -331,12 +341,16 @@ test_that("`atlas_occurrences()` works for GBIF", { test_that("`atlas_occurrences()` works with `select()` for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) - x <- galah_call(from = "GBIF") |> + x_compute <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", country == "AU") |> select(countryCode, species, eventDate) |> - collect() + compute() + Sys.sleep(pause_time * 5) + x <- collect(x_compute, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(x, c("try-error", "computed_query")), message = "API not available") expect_s3_class(x, c("tbl_df", "tbl", "data.frame")) expect_gt(nrow(x), 10) expect_equal(ncol(x), 3) @@ -345,12 +359,14 @@ test_that("`atlas_occurrences()` works with `select()` for GBIF", { test_that("invalid fields are caught from `select()` for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) - galah_call(from = "GBIF") |> + x <- galah_call(from = "GBIF") |> filter(year == 1890, classKey == "359", country == "AU") |> # this uses `check_fields_gbif_predicates()` select(country, species, eventDate) |> # this uses `check_fields_gbif_counts()` - collect() |> + compute() + Sys.sleep(pause_time * 5) + collect(x, wait = FALSE) |> expect_error() }) @@ -372,7 +388,10 @@ test_that("`atlas_species()` works for GBIF", { expect_equal(x$type, "data/species") y <- compute(x) expect_s3_class(y, "computed_query") - z <- collect(y) + Sys.sleep(pause_time * 5) + z <- collect(y, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(z, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(z), 10) # n = 14 expect_gt(ncol(z), 5) # n = 22 expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) @@ -381,13 +400,17 @@ test_that("`atlas_species()` works for GBIF", { test_that("`count()` queries work with `authenticate()` for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) - x <- request_data(from = "GBIF") |> + x_compute <- request_data(from = "GBIF") |> authenticate(username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", password = "galah-gbif-test-login") |> filter(year == 2010) |> count() |> - collect() + compute() + Sys.sleep(pause_time * 5) + x <- collect(x_compute, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(x, c("try-error", "computed_query")), message = "API not available") expect_equal(nrow(x), 1) expect_equal(ncol(x), 1) expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) @@ -396,7 +419,7 @@ test_that("`count()` queries work with `authenticate()` for GBIF", { test_that("`distinct()` queries accept `select()` for GBIF", { skip_if_offline(); skip_on_ci() Sys.sleep(pause_time) - x <- request_data(from = "GBIF") |> + x_compute <- request_data(from = "GBIF") |> authenticate(username = "atlasoflivingaustralia", email = "ala4r@ala.org.au", password = "galah-gbif-test-login") |> @@ -404,7 +427,11 @@ test_that("`distinct()` queries accept `select()` for GBIF", { distinct(species_guid, .keep_all = TRUE) |> identify("Crinia") |> select(scientificName, speciesKey) |> - collect() + compute() + Sys.sleep(pause_time * 5) + x <- collect(x_compute, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(x, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(x), 10) # expect_equal(ncol(x), 2) expect_true(inherits(x, c("tbl_df", "tbl", "data.frame"))) @@ -442,7 +469,10 @@ test_that("`collect()` works for GBIF with `type = 'occurrences' or 'occurrences expect_true(y$type == "data/occurrences") expect_true(any(names(y) == "status")) # collect - z <- collect(y) + Sys.sleep(pause_time * 5) + z <- collect(y, wait = FALSE) |> + try(silent = TRUE) + skip_if(inherits(z, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(z), 0) expect_gt(ncol(z), 0) expect_true(inherits(z, c("tbl_df", "tbl", "data.frame"))) diff --git a/tests/testthat/test-international-Kew.R b/tests/testthat/test-international-Kew.R index f0149d21..81467105 100644 --- a/tests/testthat/test-international-Kew.R +++ b/tests/testthat/test-international-Kew.R @@ -201,13 +201,16 @@ test_that("`atlas_occurrences()` works for Kew", { email = "ala4r@ala.org.au", download_reason_id = 10, send_email = FALSE) - occ <- galah_call() |> + occ_compute <- galah_call() |> identify("Acer") |> filter(year >= 1999) |> select(species, year) |> - atlas_occurrences() |> + compute() + + Sys.sleep(20) + occ <- collect(occ_compute, wait = FALSE) |> try(silent = TRUE) - skip_if(inherits(occ, "try-error"), message = "API not available") + skip_if(inherits(occ, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(occ), 0) expect_equal(ncol(occ), 2) expect_false(any(occ$year < 1999)) diff --git a/tests/testthat/test-international-Spain.R b/tests/testthat/test-international-Spain.R index f9249333..8b0d1528 100644 --- a/tests/testthat/test-international-Spain.R +++ b/tests/testthat/test-international-Spain.R @@ -264,13 +264,16 @@ test_that("`atlas_occurrences()` works for Spain", { email = "test@ala.org.au", download_reason_id = 10, send_email = FALSE) - occ <- galah_call() |> + occ_compute <- galah_call() |> identify("Mammalia") |> filter(year <= 1800) |> select(species, year) |> - atlas_occurrences() |> + compute() + + Sys.sleep(20) + occ <- collect(occ_compute, wait = FALSE) |> try(silent = TRUE) - skip_if(inherits(occ, "try-error"), message = "API not available") + skip_if(inherits(occ, c("try-error", "computed_query")), message = "API not available") expect_gt(nrow(occ), 0) expect_equal(ncol(occ), 2) expect_true(inherits(occ, c("tbl_df", "tbl", "data.frame"))) diff --git a/tests/testthat/test-international-Sweden.R b/tests/testthat/test-international-Sweden.R index bf1449bb..e6737d34 100644 --- a/tests/testthat/test-international-Sweden.R +++ b/tests/testthat/test-international-Sweden.R @@ -79,6 +79,7 @@ test_that("show_all(assertions) works for Sweden", { }) test_that("show_all(profiles) works for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() x <- show_all(profiles) |> try(silent = TRUE) @@ -229,6 +230,7 @@ test_that("atlas_counts works with group_by for Sweden", { }) test_that("atlas_counts works with apply_profile for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() without_profile <- galah_call() |> count() |> @@ -243,6 +245,7 @@ test_that("atlas_counts works with apply_profile for Sweden", { }) test_that("atlas_species works for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() galah_config( atlas = "Sweden", @@ -259,6 +262,7 @@ test_that("atlas_species works for Sweden", { }) test_that("atlas_occurrences works for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() galah_config( atlas = "Sweden", @@ -287,6 +291,7 @@ test_that("atlas_occurrences works for Sweden", { }) test_that("atlas_media() works for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() galah_config( atlas = "Sweden", @@ -306,6 +311,7 @@ test_that("atlas_media() works for Sweden", { }) test_that("collect_media() works for Sweden", { + skip("API not available") skip_if_offline(); skip_on_ci() galah_config( atlas = "Sweden", diff --git a/tests/testthat/test-international-UK.R b/tests/testthat/test-international-UK.R index fa8359f7..0687c88e 100644 --- a/tests/testthat/test-international-UK.R +++ b/tests/testthat/test-international-UK.R @@ -128,7 +128,7 @@ test_that("show_values works for UK", { expect_gt(nrow(x), 1) }) -test_that("atlas_counts works with type = 'occurrences' for United Kingdom", { +test_that("`atlas_counts()` works with type = 'occurrences' for United Kingdom", { skip_if_offline(); skip_on_ci() x <- atlas_counts() |> dplyr::pull(count) |> @@ -137,7 +137,7 @@ test_that("atlas_counts works with type = 'occurrences' for United Kingdom", { expect_gt(x, 0) }) -test_that("atlas_counts works with type = 'species' for United Kingdom", { +test_that("`atlas_counts()` works with type = 'species' for United Kingdom", { skip_if_offline(); skip_on_ci() x <- atlas_counts(type = "species") |> dplyr::pull(count) |> @@ -146,7 +146,7 @@ test_that("atlas_counts works with type = 'species' for United Kingdom", { expect_gt(x, 0) }) -test_that("atlas_counts works with `identify()` for United Kingdom", { +test_that("`count()` works with `identify()` for United Kingdom", { skip_if_offline(); skip_on_ci() result <- galah_call() |> identify("Vulpes") |> @@ -168,19 +168,20 @@ test_that("atlas_counts works with `identify()` for United Kingdom", { # Note: canonical example is to use class == Mammalia, but that fails, # possibly because name-matching is going wrong somewhere -test_that("atlas_counts works with group_by for United Kingdom", { +test_that("`count()` works with group_by for United Kingdom", { skip_if_offline(); skip_on_ci() result <- galah_call() |> filter(year >= 2020) |> group_by(year) |> - atlas_counts() |> + count() |> + collect() |> try(silent = TRUE) skip_if(inherits(result, "try-error"), message = "API not available") expect_gt(nrow(result), 1) expect_equal(names(result), c("year", "count")) }) -test_that("atlas_species works for United Kingdom", { +test_that("`atlas_species()` works for United Kingdom", { skip_if_offline(); skip_on_ci() galah_config( atlas = "United Kingdom", @@ -196,7 +197,7 @@ test_that("atlas_species works for United Kingdom", { expect_gte(ncol(x), 1) }) -test_that("atlas_occurrences works for United Kingdom", { +test_that("`atlas_occurrences()` works for United Kingdom", { skip_if_offline(); skip_on_ci() galah_config( atlas = "United Kingdom", @@ -232,9 +233,10 @@ test_that("atlas_occurrences works for United Kingdom", { occ_compute <- compute(occ_collapse) expect_s3_class(occ_compute, "computed_query") # collect - occ <- collect(occ_compute) |> + Sys.sleep(20) + occ <- collect(occ_compute, wait = FALSE) |> try(silent = TRUE) - skip_if(inherits(occ_compute, "try-error"), message = "API not available") + skip_if(inherits(occ, c("try-error", "computed_query")), message = "API not available") expect_equal(nrow(occ), counts$count[1]) expect_s3_class(occ, c("tbl_df", "tbl", "data.frame")) expect_equal(ncol(occ), length(default_columns())) @@ -243,7 +245,7 @@ test_that("atlas_occurrences works for United Kingdom", { unlink("temp", recursive = TRUE) }) -test_that("atlas_media() works for UK", { +test_that("`atlas_media()` works for UK", { skip_if_offline(); skip_on_ci() galah_config( atlas = "United Kingdom", diff --git a/tests/testthat/test-print.R b/tests/testthat/test-print.R index 8dd5a8c4..7c6a5e5f 100644 --- a/tests/testthat/test-print.R +++ b/tests/testthat/test-print.R @@ -51,6 +51,8 @@ test_that("object of class `computed_query` formats correctly", { test_that("object of class `query_set` formats correctly", { # prevent earlier caching (or lack thereof) affecting the result reset_cache() + galah_config(run_checks = TRUE) # necessary status affects whether `logger` is run, + # which affects the test below galah_call() |> authenticate(email = "ala4r@ala.org.au") |> filter(basisOfRecord == "HUMAN_OBSERVATION") |> @@ -60,6 +62,7 @@ test_that("object of class `query_set` formats correctly", { test_that("`galah_config()` formats correctly", { galah_config(atlas = "ALA", + email = "random_email_for_testing@email.com", directory = "something", verbose = FALSE, caching = TRUE, diff --git a/tests/testthat/test-request_metadata_unnest.R b/tests/testthat/test-request_metadata_unnest.R index 257e6f04..f656b144 100644 --- a/tests/testthat/test-request_metadata_unnest.R +++ b/tests/testthat/test-request_metadata_unnest.R @@ -61,6 +61,8 @@ test_that("request_metadata() |> select() |> unnest() works for type = 'fields'" test_that("request_metadata() |> unnest() works for type = 'lists'", { skip_if_offline(); skip_on_ci() + galah_config(verbose = FALSE) + x1 <- request_metadata() |> filter(list == "dr947") |> collect() diff --git a/tests/testthat/test-show_values.R b/tests/testthat/test-show_values.R index 4a678582..a2ebea30 100644 --- a/tests/testthat/test-show_values.R +++ b/tests/testthat/test-show_values.R @@ -130,15 +130,18 @@ test_that("`show_values()` returns message when there are duplicated taxon conce skip_if_offline(); skip_on_ci() # simple, fake version for testing `show_values()` df <- tibble::tibble(species_list_uid = "dr650") + galah_config(verbose = TRUE) attr(df, "call") <- "lists" show_values_query <- purrr_values(df) # NOTE: above is same as following code, but much faster # search <- search_all(lists, "dr650") |> # show_values() - # warning expected - expect_match(show_values_query$warnings, - "^List contains") + # message expected + grepl("^List contains", show_values_query$messages) |> + any() |> + expect_true() + galah_config(verbose = FALSE) }) test_that("`show_values()` doesn't return message when there are no duplicated taxon concept ids", { @@ -146,13 +149,15 @@ test_that("`show_values()` doesn't return message when there are no duplicated t # simple, fake version for testing `show_values()` df <- tibble::tibble(species_list_uid = "dr30561") attr(df, "call") <- "lists" + galah_config(verbose = TRUE) show_values_query <- purrr_values(df) # NOTE: above is same as following code, but much faster # search <- search_all(lists, "dr650") |> # show_values() - # warning not expected + # message not expected expect_length(show_values_query$warnings, 0) + galah_config(verbose = FALSE) }) rm(purrr_values, quiet_values, purrr_search, quiet_search) \ No newline at end of file