Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updated a bit in the description, included readme updates #4

Merged
merged 7 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading