Skip to content

Commit

Permalink
Merge pull request #18 from avantcredit/no_more_logouts
Browse files Browse the repository at this point in the history
caching access tokens
  • Loading branch information
abelcastilloavant committed Jun 6, 2016
2 parents 68fec04 + 80a8c47 commit 1a3dac5
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 112 deletions.
5 changes: 4 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ LazyData: true
Imports:
httr,
jsonlite,
cacher,
checkr (>= 0.0.4.9007)
Suggests:
testthat,
withr
Remotes: peterhurford/[email protected]
Remotes:
peterhurford/[email protected],
kirillseva/[email protected]
Roxygen: list(wrap = FALSE)
RoxygenNote: 5.0.0
7 changes: 4 additions & 3 deletions R/api_calls.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ logout_api_call <- function(base_url, token) {
httr::add_headers(Authorization = paste0("token ", token)))
}

query_api_call <- function(base_url, token, model, view, fields, filters,
limit = 1000){
query_url <- paste0(base_url, "api/3.0/queries/run/csv")
query_api_call <- function(base_url, model, view, fields, filters,
limit = 1000) {
token <- token_cache$get("token")$token
query_url <- paste0(base_url, "api/3.0/queries/run/csv")
query_body <- list(model = model, view = view, fields = I(fields),
filters = filters, limit = limit)
if (length(filters) == 0) { query_body$filters <- NULL }
Expand Down
62 changes: 33 additions & 29 deletions R/looker3.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,46 @@
#' @return a data.frame containing the data returned by the query
#'
#' @export
looker3 <- function(model, view, fields,
filters = list(), limit = 1000) {
looker3 <- checkr::ensure(pre = list( # model, view, and fields are
model %is% simple_string, # required to form a query.
view %is% simple_string,
fields %is% character,
filters %is% simple_string ||
filters %is% list && (filters %contains_only% simple_string || filters %is% empty),
limit %is% numeric && limit > 0 && limit %% 1 == 0
),

env_var_descriptions <- list(
LOOKER_URL = "API url",
LOOKER_ID = "client id",
LOOKER_SECRET = "client secret"
)
function(model, view, fields,
filters = list(), limit = 1000) {

looker_setup <- lapply(names(env_var_descriptions), function(name) {
env_var <- Sys.getenv(name)
if (env_var == "") {
stop(paste0("Your environment variables are not set correctly. ",
"please place your Looker 3.0 ", env_var_descriptions[[name]],
" in the environment variable ", name, "."
))
}
env_var
})
env_var_descriptions <- list(
LOOKER_URL = "API url",
LOOKER_ID = "client id",
LOOKER_SECRET = "client secret"
)

names(looker_setup) <- names(env_var_descriptions)
looker_setup <- lapply(names(env_var_descriptions), function(name) {
env_var <- Sys.getenv(name)
if (env_var == "") {
stop(paste0("Your environment variables are not set correctly. ",
"please place your Looker 3.0 ", env_var_descriptions[[name]],
" in the environment variable ", name, "."
))
}
env_var
})

# model, view, and fields are required to perform a query
checkr::validate(model %is% simple_string, view %is% simple_string,
fields %is% character)
names(looker_setup) <- names(env_var_descriptions)

# if user-specified filters as a character vector, reformat to a list
if (!missing(filters) && is.character(filters)) {
filters <- colon_split_to_list(filters)
}

# if user-specified filters as a character vector, reformat to a list
if (!missing(filters) && is.character(filters)) {
filters <- colon_split_to_list(filters)
run_inline_query(looker_setup$LOOKER_URL, looker_setup$LOOKER_ID, looker_setup$LOOKER_SECRET,
model, view, fields, filters, limit)
}

run_inline_query(looker_setup$LOOKER_URL, looker_setup$LOOKER_ID, looker_setup$LOOKER_SECRET,
model, view, fields, filters, limit)
}

)

colon_split_to_list <- function(string) {
colon_split <- strsplit(string, ": ")
Expand Down
16 changes: 13 additions & 3 deletions R/response_handlers.R
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
cached_token_is_invalid <- function() {
if (!token_cache$exists("token")) { return(TRUE) }
expiration_date <- token_cache$get("token")$expires_in
is.null(expiration_date) || methods::is(expiration_date, "POSIXt") || i
(expiration_date < Sys.time())
}

is.successful_response <- function(response) {
httr::status_code(response) %in% c("200", "201", "202", "204")
}


validate_response <- function(response) {
if (is.successful_response(response)) { return(TRUE) }

Expand All @@ -18,9 +24,13 @@ validate_response <- function(response) {
)
}

extract_login_token <- function(login_response) {
put_new_token_in_cache <- function(login_response) {
validate_response(login_response)
httr::content(login_response)$access_token
token_cache$set("token", list(
token = httr::content(login_response)$access_token,
# avoid token expiration during code execution
expires_in = Sys.time() + httr::content(login_response)$expires_in - 1
))
}


Expand Down
13 changes: 5 additions & 8 deletions R/run_inline_query.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,13 @@ run_inline_query <- function(base_url, client_id, client_secret,

# The API requires you to "log in" and obtain a session token
# TODO: find a way to cache the session token, perhaps using memoise package?
login_response <- login_api_call(base_url, client_id, client_secret)
session_token <- extract_login_token(login_response)

on.exit({
logout_response <- logout_api_call(base_url, session_token)
tryCatch(handle_logout_response(logout_response),
error = function(e) warning(e))
})
if (cached_token_is_invalid()) {
login_response <- login_api_call(base_url, client_id, client_secret)
put_new_token_in_cache(login_response)
}

inline_query_response <- query_api_call(base_url, session_token,
inline_query_response <- query_api_call(base_url,
model, view, fields, filters, limit)

extract_query_result(inline_query_response)
Expand Down
3 changes: 3 additions & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
token_cache <- cacher::LRUcache(1)


ensure_line_coverage <- function() {
TRUE
}
5 changes: 3 additions & 2 deletions tests/test-all.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
library("testthat")
library("withr")
library("avant-looker3")
library("checkr")
library("looker3")


test_check("avant-looker3")
test_check("looker3")
21 changes: 4 additions & 17 deletions tests/testthat/test-api_calls.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,17 @@ with_mock(
})

with_mock(
`httr::DELETE` = function(url, header) {
list(url = url, header = header)
}, {
test_that("logout_api_call passes url to httr::DELETE", {
expect_identical(
logout_api_call("https://fake.looker.com:111/", "FAKE_TOKEN")$url,
"https://fake.looker.com:111/api/3.0/logout")
})
test_that("logout_api_call passes token to httr::DELETE", {
expect_identical(
logout_api_call("https://fake.looker.com:111/", "FAKE_TOKEN")$header,
"Authorization is token FAKE_TOKEN")
})
})

with_mock(
`looker3:::cached_token_is_invalid` = function(...) { FALSE },
`httr::POST` = function(url, header, body, encode) {
list(url = url, header = header, body = body)
}, {
args <- list(base_url = "https://fake.looker.com:111/", token = "FAKE_TOKEN",
args <- list(base_url = "https://fake.looker.com:111/",
model = "look", view = "items",
fields = c("category.name", "products.count"),
filters = list(c("category.name", "socks")))

token_cache$set("token", list(token = "FAKE_TOKEN"))

test_that("query_api_call passes url to httr::POST", {
expect_identical(
do.call(query_api_call, args)$url,
Expand Down
4 changes: 0 additions & 4 deletions tests/testthat/test-response_handlers.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ describe("processing successful responses", {
with_mock(
`httr::status_code` = function(response) { response$status },
`httr::content` = function(response) { response$body }, {
test_that("extract_login_token returns the access token", {
expect_equal(extract_login_token(fake_login_response),
"FAKE_TOKEN")
})
test_that("handle_logout_response returns TRUE", {
expect_true(handle_logout_response(fake_logout_response))
})
Expand Down
71 changes: 26 additions & 45 deletions tests/testthat/test-run_inline_query.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,45 @@ describe("run_inline_query helpers called with the corresponding inputs", {
with_mock(
`looker3:::login_api_call` = function(...) NULL,
`looker3:::logout_api_call` = function(...) NULL,
`looker3:::cached_token_is_invalid` = function(...) FALSE,
`looker3:::query_api_call` = function(...) NULL,
`looker3:::extract_login_token` = function(...) NULL,
`looker3:::handle_logout_response` = function(...) NULL,
`looker3:::extract_query_result` = function(...) NULL, {

test_that("login_api_call receives the base_url, client_id, and secret", {
with_mock(`looker3:::login_api_call` = function(base_url, client_id, client_secret) {
stop(paste("login_api_call:", base_url, client_id, client_secret))
}, {
expect_error(do.call(run_inline_query, args),
"login_api_call: fake.looker.com/ fake_client fake_secret")
test_that("login_api_call called if cached token is invalid", {
with_mock(
`looker3:::login_api_call` = function(base_url, client_id, client_secret) {
stop(paste("login_api_call:", base_url, client_id, client_secret))
},
`looker3:::cached_token_is_invalid` = function(...) { TRUE }, {
expect_error(do.call(run_inline_query, args),
"login_api_call: fake.looker.com/ fake_client fake_secret")
})
})

test_that("extract_login_token receives the output of login_api_call", {
test_that("login procedure bypassed if cached token is valid", {
with_mock(
`looker3:::login_api_call` = function(...) "response received",
`looker3:::extract_login_token` = function(login_response) {
stop(paste0("extract_login_token called: ", login_response))
`looker3:::login_api_call` = function(...) {
stop("stopped by login_api_call")
},
`looker3:::put_new_token_in_cache` = function(...) {
stop("stopped by put_new_token_in_cache")
},
`looker3:::query_api_call` = function(...) {
stop("bypassed token caching")
}, {
expect_error(do.call(run_inline_query, args),
"extract_login_token called: response received")
})
"bypassed token caching")
})
})

test_that("query_api_call receives its args, including token", {

with_mock(
`looker3:::extract_login_token` = function(...) "fake_token",
`looker3:::query_api_call` = function(base_url, token, model, view, fields, filters, limit) {
actual_inputs <- list(base_url = base_url, token = token, model = model, view = view, fields = fields,
`looker3:::query_api_call` = function(base_url, model, view, fields, filters, limit) {
actual_inputs <- list(base_url = base_url, model = model, view = view, fields = fields,
filters = filters, limit = limit)
expected_inputs <- list(base_url = args$base_url, token = "fake_token", model = args$model, view = args$view,
expected_inputs <- list(base_url = args$base_url, model = args$model, view = args$view,
fields = args$fields, filters = args$filters, limit = 1000)
if (identical(actual_inputs, expected_inputs)) {
stop("query_api_call called correctly")
Expand All @@ -57,11 +63,10 @@ describe("run_inline_query helpers called with the corresponding inputs", {
})

with_mock(
`looker3:::extract_login_token` = function(...) "fake_token",
`looker3:::query_api_call` = function(base_url, token, model, view, fields, filters, limit) {
actual_inputs <- list(base_url = base_url, token = token, model = model, view = view, fields = fields,
`looker3:::query_api_call` = function(base_url, model, view, fields, filters, limit) {
actual_inputs <- list(base_url = base_url, model = model, view = view, fields = fields,
filters = filters, limit = limit)
expected_inputs <- list(base_url = args$base_url, token = "fake_token", model = args$model, view = args$view,
expected_inputs <- list(base_url = args$base_url, model = args$model, view = args$view,
fields = args$fields, filters = args$filters, limit = 20)
if (identical(actual_inputs, expected_inputs)) {
stop("query_api_call called correctly")
Expand All @@ -72,8 +77,6 @@ describe("run_inline_query helpers called with the corresponding inputs", {
})
})



test_that("extract_query_result receives the output of query_api_call", {
with_mock(
`looker3:::query_api_call` = function(...) "response received",
Expand All @@ -84,28 +87,6 @@ describe("run_inline_query helpers called with the corresponding inputs", {
"extract_query_result called: response received")
})
})

test_that("logout_api_call receives the base_url and the token", {
with_mock(
`looker3:::extract_login_token` = function(...) "fake_token",
`looker3:::logout_api_call` = function(base_url, session_token) {
stop(paste("logout_api_call:", base_url, session_token))
}, {
expect_error(do.call(run_inline_query, args),
"logout_api_call: fake.looker.com/ fake_token")
})
})

test_that("handle_logout_response recieves the output of logout_api_call", {
with_mock(
`looker3:::logout_api_call` = function(...) "response received",
`looker3:::handle_logout_response` = function(logout_response) {
stop(paste0("handle_logout_response called: ", logout_response))
}, {
expect_error(do.call(run_inline_query, args),
"handle_logout_response called: response received")
})
})
})
})

Expand Down

0 comments on commit 1a3dac5

Please sign in to comment.