Skip to content

Commit

Permalink
Merge pull request #4 from christopherDT/main
Browse files Browse the repository at this point in the history
updated a bit in the description, included readme updates
  • Loading branch information
lswainemoore authored Nov 28, 2023
2 parents 0e0dedc + f19bb7e commit 002b2bc
Show file tree
Hide file tree
Showing 18 changed files with 224 additions and 220 deletions.
7 changes: 7 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
^.*\.Rproj$
^\.Rproj\.user$
^tests/testthat\.R$
^interactive_testing\.Rmd$
^LICENSE\.md$
^\.github
^\.github$
1 change: 1 addition & 0 deletions .github/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.html
49 changes: 49 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

name: R-CMD-check

jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}

name: ${{ matrix.config.os }} (${{ matrix.config.r }})

strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes

steps:
- uses: actions/checkout@v3

- uses: r-lib/actions/setup-pandoc@v2

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check

- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
28 changes: 14 additions & 14 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
Package: QuantAQAPIClient
Title: Bindings for QuantAQ API Client
Version: 0.0.0.9000
Title: QuantAQ API Client
Version: 0.1.0.0
Authors@R:
person("Christopher", "Dudas-Thomas", , "[email protected]", role = c("aut", "cre"))
person("David", "Hagan", , "[email protected]", role = c("aut", "cre"))
Description: This package offers wrappers for the QuantAQ API (https://docs.quant-aq.com/api). It gives convenience functions for connecting to the API, as well as querying account information, devices, data, logs, and calibration models.
Description: Wrappers and convenience functions for the Quant AQ API <https://api.quant-aq.com/>. These include functions for connecting to the API, as well as querying account information, devices, data, logs, and calibration models. See <https://docs.quant-aq.com/api> for more details.
License: Apache License (>= 2)
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
Imports:
httr,
XML,
stringr,
dplyr,
rstudioapi,
lubridate,
jsonlite,
magrittr,
tidyr,
wrapr
Imports:
rlang,
stringr,
dplyr,
rstudioapi,
lubridate,
magrittr,
tidyr,
wrapr,
httr2
Suggests:
httptest,
testthat (>= 3.0.0)
Config/testthat/edition: 3
6 changes: 1 addition & 5 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export(get_models)
export(get_teams)
export(setup_client)
export(whoami)
import(httr)
importFrom(XML,getRelativeURL)
importFrom(dplyr,across)
importFrom(dplyr,any_of)
importFrom(dplyr,case_when)
Expand All @@ -24,9 +22,7 @@ importFrom(dplyr,rename_with)
importFrom(dplyr,select)
importFrom(dplyr,starts_with)
importFrom(dplyr,where)
importFrom(httr,GET)
importFrom(httr,modify_url)
importFrom(jsonlite,fromJSON)
importFrom(httr2,resp_body_json)
importFrom(lubridate,parse_date_time)
importFrom(magrittr,`%>%`)
importFrom(stringr,str_split)
Expand Down
102 changes: 27 additions & 75 deletions R/api_calls.R
Original file line number Diff line number Diff line change
@@ -1,97 +1,52 @@
#' Build and return URL
#'
#' A helper function for [request()] to build and return the URL given an endpoint and any query parameters.
#'
#' @importFrom XML getRelativeURL
#' @importFrom httr modify_url
#'
#' @param endpoint A character vector of an endpoint. Can be a full or relative URL
#'
#' @param qs_params A list of query string parameters to format and attach to the URL (e.g. list(per_page = 10))
#'
#' @returns A character vector containing the final built URL.
#'
build_api_url <- function(endpoint, qs_params = NULL){
client <- access_client()
url_string <- paste(client$base_url, client$version, "", sep="/")
combined_url_string <- XML::getRelativeURL(endpoint, url_string) # combined url string and path/endpoint


final_url <- httr::modify_url(combined_url_string, query = qs_params)

return(final_url)
}

#' Complete an API request
#'
#' Generate an API request given endpoint, http verb, and any relevant query parameters.
#'
#' @importFrom httr GET
#' @importFrom jsonlite fromJSON
#' Generate an API request given an endpoint and any relevant query parameters.
#'
#' @param endpoint A character string of the API endpoint to request.
#' @param verb The httr function corresponding to the HTTP verb. Defaults to GET.
#' @param endpoint A character string of the API endpoint to request. Can be a full url, or a relative endpoint for the QuantAQ API
#' @param qs_params Query string parameters.
#'
#' @examples
#' \dontrun{r <- request("account")}
#' \dontrun{r <- quantaq_request("account")}
#'
#' # include named arguments for query params, e.g. to limit the response to the first 5 results:
#' \dontrun{r <- request("devices/MOD-PM-00808/data", limit = 5)}
#' # include a named list for query params, e.g. to limit the response to the first 5 results:
#' \dontrun{r <- quantaq_request("devices/MOD-PM-00808/data", list(limit = 5))}
#'
#' @returns The parsed JSON from the API, including both data and metadata.
#'
request <- function(endpoint, verb = httr::GET, qs_params = NULL){
quantaq_request <- function(endpoint, qs_params = NULL){
client <- access_client()
this_url <- build_api_url(endpoint, qs_params)

resp <- verb(this_url,
authenticate(client$api_key, ""),
add_headers(user_agent = client$ua))

# because API doesn't return json upon 400 and 500 errors, check that first
if (http_error(resp)) {
stop(
sprintf(
"[%s] -- QuantAQ API request failed \n%s\npath: <%s>",
status_code(resp),
content(resp, as = "text", encoding = "UTF-8"),
this_url
),
call. = FALSE
)
}

# then, if we don't err out and it's JSON, parse it!
if (http_type(resp) != "application/json"){
stop("API did not return JSON", call. = FALSE)
}
is_full_url <- stringr::str_detect(endpoint, "http")

parsed <- jsonlite::fromJSON(content(resp, as = "text", encoding = "UTF-8"), simplifyVector = FALSE)
if(is_full_url){ # if it's a full url
req <- httr2::request(endpoint)
} else{ # otherwise, build the url from the relative path
req <- httr2::request(client$base_url) %>%
httr2::req_url_path_append(client$version) %>%
httr2::req_url_path_append(endpoint)
}

return(parsed)
req %>% httr2::req_url_query(!!!qs_params) %>%
httr2::req_auth_basic(client$api_key, "") %>%
httr2::req_user_agent(client$ua) %>%
httr2::req_perform()
}

#' Deal with paginated data.
#'
#' Iterates over and collated all pages of the data.
#'
#' @importFrom httr GET
#'
#' @param response_content Content from a \code{\link{request}()}
#' @param verb The httr function corresponding to the HTTP verb. Defaults to GET.
#' @param response_content JSON content from a [quantaq_request()] response
#'
#' @returns All collated pages of the data.
#'
paginate <- function(response_content, verb = httr::GET){
paginate <- function(response_content){
all_data <- response_content$data
next_url <- response_content$meta$next_url

#keep getting the next page while a new page exists, and append every new page's data
while(!is.null(next_url)){
this_query <- parse_url(next_url)$query

r <- do.call(request, list(next_url, qs_params = this_query))
r <- quantaq_request(next_url) %>% httr2::resp_body_json()

next_url <- r$meta$next_url

Expand All @@ -103,15 +58,16 @@ paginate <- function(response_content, verb = httr::GET){

#' Handle requests params
#'
#' A helper function to format the parameters that can be passed to \code{requests()}.
#' A helper function to format the parameters that can be passed to [requests()].
#'
#' @importFrom stringr str_split
#' @param ... Parameters to be translated to query string and passed to the request.
#' @returns A named list of query params.
#'
format_params <- function(...){
kwargs <- list(...)
filter <- stringr::str_split(kwargs$filter, ";")
filter <- stringr::str_split(kwargs$filter, ";", simplify = TRUE)
filter <- filter[filter != ""] # drop empty string from filter

# TODO: set default per_page to 100?
# TODO: give some warning when passed params are not recognized?
Expand All @@ -138,19 +94,15 @@ format_params <- function(...){

#' Request that handles pagination
#'
#' @importFrom httr GET
#'
#' @importFrom httr2 resp_body_json
#' @param endpoint A character string of the API endpoint to request.
#'
#' @param verb The httr function corresponding to the HTTP verb. Defaults to GET.
#'
#' @param ... Params to be translated to query string.
#'
#' @returns Parsed data from the API.
#'
requests <- function(endpoint, verb = httr::GET, ...){
requests <- function(endpoint, ...){
kwargs <- format_params(...)
r <- request(endpoint, verb = verb, qs_params = kwargs)
r <- quantaq_request(endpoint, qs_params = kwargs) %>% httr2::resp_body_json()

this_data <- r

Expand Down
29 changes: 13 additions & 16 deletions R/setup.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#'
#' @export
setup_client <- function(api_type = 'prod', version = 'v1', api_key = NULL, verbose = FALSE) {
ua <- user_agent("https://github.com/quant-aq/r-quantaq")
ua <- "https://github.com/quant-aq/r-quantaq"

base_url <- dplyr::case_when(
api_type == 'prod' ~ 'https://api.quant-aq.com/device-api',
Expand Down Expand Up @@ -72,27 +72,24 @@ access_client <- function(){
#' A helper function for [setup_client()] to determine that the user-provided
#' API details yields a proper connection with the API.
#'
#' @import httr
#' @param verbose (Optional) Default FALSE. Whether you want a message printed upon API connection.
#'
#' @returns No return value. `verbose = TRUE` causes a message to print to console upon successful API connection.
#'
authenticate_client <- function(verbose = FALSE){
client <- access_client()
path <- paste0(client$version,'/account') # just use "account" endpoint to authenticate
url <- modify_url(client$base_url, path=path)
code <- status_code(GET(
url,
authenticate(client$api_key, ""),
add_headers(user_agent = client$ua)
))
if (code == 401){
stop(paste0(http_status(code)$message, " -- Invalid API Key!"))
} else if (code != 200){
stop(http_status(code)$message)
} else {
if(verbose == TRUE){
cat("Successfully connected to QuantAQ API!")

withCallingHandlers(
quantaq_request('account'),
httr2_http_401 = function(cnd) {
rlang::abort("Invalid API Key!", parent = cnd)
},
httr2_http_404 = function(cnd) {
rlang::abort("Endpoint not found!", parent = cnd)
}
)

if(verbose == TRUE){
cat("Successfully connected to QuantAQ API!")
}
}
11 changes: 4 additions & 7 deletions R/wrappers.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ unnest_all <- function(df) {
#' @export
whoami <- function(){
structure(
request("account"),
quantaq_request("account"),
class = "account"
)
}
Expand Down Expand Up @@ -76,7 +76,6 @@ as.data.frame.teams <- function(x, ...){

#' Get the user's devices
#'
#' @importFrom httr GET
#' @importFrom wrapr stop_if_dot_args
#'
#' @param sn A device serial number
Expand All @@ -87,7 +86,7 @@ as.data.frame.teams <- function(x, ...){
#' @export
get_devices <- function(sn = NULL, limit = NULL, sort = NULL){
structure(
requests(paste("devices", sn, sep = "/"), verb = httr::GET, limit = limit, sort = sort),
requests(paste("devices", sn, sep = "/"), limit = limit, sort = sort),
class = "devices"
)
}
Expand Down Expand Up @@ -130,8 +129,6 @@ get_device_metadata <- function(sn){
#'
#' Get data according to provided serial number and other parameters.
#'
#' @importFrom httr GET
#'
#' @param sn A device serial number
#' @param limit (optional) Default = 1000. The number of data points to return.
#' @param start (optional) The earliest date to retrieve data from. Should be a timestamp string of the form "YYYY-MM-DD HH:MM:SS"
Expand Down Expand Up @@ -168,7 +165,7 @@ get_data <- function(sn, limit = 1000, start = NULL, stop = NULL, filter = NULL,
}

structure(
requests(endpoint, httr::GET,
requests(endpoint,
limit = limit,
start = start,
stop = stop,
Expand Down Expand Up @@ -200,7 +197,7 @@ get_data_by_date <- function(sn, date, raw = FALSE){
endpoint <- paste(endpoint, date, sep = "/")

structure(
requests(endpoint, httr::GET),
requests(endpoint),
class = "device_data"
)
}
Expand Down
Loading

0 comments on commit 002b2bc

Please sign in to comment.