Merge branch 'main' into js-sdk-upgrade

This commit is contained in:
Jim Myers 2023-05-15 23:35:21 -04:00
commit 2eb8abecb1
16 changed files with 357 additions and 127 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ examples/python/scratch/*
r/shroomDK_0.1.0.tar.gz
python-sdk-example.py
r/shroomDK/api_key.txt
r/shroomDK/test_of_page2_issue.R

View File

@ -2,8 +2,8 @@
"name": "@flipsidecrypto/sdk",
"version": "1.1.1",
"description": "The official Flipside Crypto SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/flipsidecrypto/sdk.git"

View File

@ -1,12 +1,11 @@
Package: shroomDK
Type: Package
Title: Accessing the Flipside Crypto ShroomDK REST API
Version: 0.1.1
Title: Accessing the Flipside Crypto ShroomDK API
Version: 0.2.0
Author: Carlos Mercado
Maintainer: Carlos Mercado <carlos.mercado@flipsidecrypto.com>
Description: Programmatic access to Flipside Crypto data via the REST API: <https://sdk.flipsidecrypto.xyz/shroomdk>. As simple as auto_paginate_query() but with core functions as needed for troubleshooting.
Description: Programmatic access to Flipside Crypto data via the Compass RPC API: <https://api-docs.flipsidecrypto.xyz/>. As simple as auto_paginate_query() but with core functions as needed for troubleshooting. Note, 0.1.1 support deprecated 2023-05-31.
Imports: jsonlite, httr
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: false
RoxygenNote: 7.2.1

View File

@ -1,8 +1,10 @@
# Generated by roxygen2: do not edit by hand
export(auto_paginate_query)
export(cancel_query)
export(clean_query)
export(create_query_token)
export(get_query_from_token)
export(get_query_status)
import(httr)
import(jsonlite)

View File

@ -1,57 +1,55 @@
library(jsonlite)
library(httr)
#' Auto Paginate Queries
#'
#' @description Grabs up to maxrows in a query by going through each page 100k rows at a time.
#' @description Grabs up to maxrows in a query by going through each page to download one at a time.
#'
#' @param query The SQL query to pass to ShroomDK
#' @param api_key ShroomDK API key.
#' @param maxrows Max rows allowed in ShroomDK, 1M at time of writing.
#'
#' @return data frame of up to 1M rows, see ?clean_query for more details on column classes.
#' @param page_size Default 1000. May return error if page_size is tool large and data to exceed 30MB.
#' @param page_count Default 1. How many pages, of page_size rows each, to read.
#' @param api_url default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.
#' @return data frame of up to `page_size * page_count` rows, see ?clean_query for more details on column classes.
#' @import jsonlite httr
#' @export
#' @examples
#' \dontrun{
#' pull_data <- auto_paginate_query("
#' SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000",
#' api_key = readLines("api_key.txt"))
#' SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10001",
#' api_key = readLines("api_key.txt"),
#' page_count = 10)
#' }
auto_paginate_query <- function(query, api_key, maxrows = 1000000){
auto_paginate_query <- function(query, api_key, page_size = 1000,
page_count = 1,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"){
qtoken <- shroomDK::create_query_token(query = query, api_key = api_key)
res <- shroomDK::get_query_from_token(qtoken$token, api_key = api_key)
df <- shroomDK::clean_query(res)
qtoken <- create_query_token(query = query,
api_key = api_key,
ttl = 1,
mam = 10,
api_url = api_url)
# Handle Pagination via ShroomDK
# up to 1M rows max
# get 100,000 rows at a time
# stop when the most recent page < 100,000 items.
# otherwise stop at 1M total rows.
# NOTE: in the future, if we allow > 1M rows, will need to update this.
res <- lapply(1:page_count, function(i){
temp_page <- get_query_from_token(qtoken$result$queryRequest$queryRunId,
api_key = api_key,
page_number = i,
page_size = page_size,
result_format = "csv",
api_url = api_url)
maxpages = ceiling(maxrows/100000)
if(nrow(df) == 100000){
warning("Checking for additional pages of data...")
for(i in 2:maxpages){
temp_page <- clean_query(
shroomDK::get_query_from_token(qtoken$token,
api_key = api_key,
page_number = i)
)
df <- rbind.data.frame(df, temp_page)
if(nrow(temp_page) < 100000 | i == maxpages){
# done
return(df)
} else {
# continue
if(length(temp_page$result$rows) < 1){
df <- data.frame()
} else {
df <- clean_query(temp_page)
}
}
} else {
return(df)
}
})
res <- res[unlist(lapply(res, nrow)) > 0]
}
df <- do.call(rbind.data.frame, res)
return(df)
}

View File

@ -0,0 +1,60 @@
library(jsonlite)
library(httr)
#' Cancel Query
#'
#' Uses Flipside ShroomDK to CANCEL a query run id from `create_query_token()`, as the new API uses warehouse-seconds to charge users above the free tier,
#' the ability to cancel is critical for cost management.
#' @param query_run_id queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`
#' @param api_key Flipside Crypto ShroomDK API Key
#' @param api_url default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.
#' @return returns a list of the status_canceled (TRUE or FALSE) and the cancel object (which includes related details).
#' @import jsonlite httr
#' @export
#'
#' @examples
#' \dontrun{
#' query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000000", api_key)
#' query_status <- get_query_status(query$result$queryRequest$queryRunId, api_key)
#' canceled <- cancel_query(query$result$queryRequest$queryRunId, api_key)
#' }
cancel_query <- function(query_run_id, api_key,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"){
headers = c(
"Content-Type" = 'application/json',
"x-api-key" = api_key
)
# get status of a run id
request_cancel_body <- as.character(
jsonlite::toJSON(pretty = TRUE,
list(
"jsonrpc" = "2.0",
"method" = "cancelQueryRun",
"params" = list(
list( "queryRunId" = query_run_id
)
),
"id" = 1
),
auto_unbox = TRUE
)
)
canceled <- content(
httr::POST(
api_url,
config = httr::add_headers(.headers = headers),
body = request_cancel_body)
)
statecheck = canceled$result$canceledQueryRun$state == "QUERY_STATE_CANCELED"
return(
list(
status_canceled = statecheck,
cancellation_details = canceled
)
)
}

View File

@ -9,7 +9,7 @@
#' @param try_simplify because requests can return JSON and may not have the same length
#' across values, they may not be data frame compliant (all columns having the same number of rows).
#' A key example would be TX_JSON in EVM FACT_TRANSACTION tables which include 50+
#' extra details from transaction logs. But other examples like NULLs TO_ADDRESS can have similar
#' extra details from transaction logs. But other examples like NULLs in TO_ADDRESS can have similar
#' issues. Default TRUE.
#'
#' @return A data frame. If `try_simplify` is FALSE OR if `try_simplify` TRUE fails:
@ -20,9 +20,10 @@
#'
#' @examples
#' \dontrun{
#' query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
#' request = get_query_from_token(query$token, api_key, 1, 10000)
#' clean_query(request, try_simplify = FALSE)
#' query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000", api_key)
#' request <- get_query_from_token(query$result$queryRequest$queryRunId, api_key)
#' df1 <- clean_query(request, try_simplify = TRUE) # warning b/c of tx_json
#' df2 <- clean_query(request, try_simplify = FALSE) # silently returns columns of lists
#' }
clean_query <- function(request, try_simplify = TRUE){
@ -44,13 +45,13 @@ clean_query <- function(request, try_simplify = TRUE){
# start data reformat
# this is a matrix/array
data <- t(list2DF(request$results))
colnames(data) <- request$columnLabels
data <- t(list2DF(request$result$rows))
colnames(data) <- request$result$columnNames
rownames(data) <- NULL
# Protects NULL values
for(i in 1:ncol(data)){
data[, i] <- fill_null(data[, i])
for(j in 1:ncol(data)){
data[, j] <- fill_null(data[, j])
}
# data frame of Lists

View File

@ -3,69 +3,91 @@ library(httr)
#' Get Query From Token
#'
#' Uses Flipside ShroomDK to access a Query Token. Query tokens are cached up to `ttl` minutes
#' for each `query`. This function is for pagination and multiple requests
#' while . Note: To reduce payload it returns
#' a list of outputs (separating column names from rows).
#' Uses Flipside ShroomDK to access a Query Token (Run ID). This function is for pagination and multiple requests.
#' Note: To reduce payload it returns a list of outputs (separating column names from rows). Use `clean_query()` to
#'
#' @param query_token token from `create_query_token()`
#' @param query_run_id queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`
#' @param api_key Flipside Crypto ShroomDK API Key
#' @param page_number Query tokens are cached and 100k rows max. Get up to 1M rows by going through pages.
#' @param page_size Default 100,000. Paginate via page_number.
#' @return returns a request of length 8: `results`, `columnLabels`,
#' `columnTypes`, `startedAt`, `endedAt`, `pageNumber`, `pageSize`, `status`
#' @param page_number Results are cached, max 30MB of data per page.
#' @param page_size Default 1000. Paginate via page_number. May return error if page_size causes data to exceed 30MB.
#' @param result_format Default to csv. Options: csv and json.
#' @param api_url default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.
#' @return returns a list of jsonrpc, id, and result. Within result are:
#' columnNames, columnTypes, rows, page, sql, format, originalQueryRun, redirectedToQueryRun
#' use `clean_query()` to transform this into a data frame.
#' @import jsonlite httr
#' @export
#'
#' @examples
#' \dontrun{
#' query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
#' get_query_from_token(query$token, api_key, 1, 10000)
#' query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000", api_key)
#' fact_transactions <- get_query_from_token(query$result$queryRequest$queryRunId, api_key, 1, 1000)
#' }
get_query_from_token <- function(query_token, api_key, page_number = 1, page_size = 100000){
get_query_from_token <- function(query_run_id, api_key,
page_number = 1,
page_size = 1000,
result_format = "csv",
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"){
headers = c(
"Accept" = 'application/json',
"Content-Type" = 'application/json',
"x-api-key" = api_key
)
url = paste0(
"https://node-api.flipsidecrypto.com/queries/",
query_token,"?",
"pageNumber=", page_number, "&",
"pageSize=", format(page_size, scientific = FALSE) # just in case user's R settings force 100,000 -> 1e+05 which breaks API.
)
query_status <- get_query_status(query_run_id = query_run_id, api_key = api_key, api_url = api_url)
query_state <- query_status$result$queryRun$state
req <- httr::GET(
url = url,
config = httr::add_headers(.headers = headers)
)
# implicit else for "QUERY_STATUS_SUCCESS"
if(query_state == "QUERY_STATE_FAILED"){
stop(query_status$result$queryRun$errorMessage)
} else if(query_state == "QUERY_STATE_CANCELED"){
stop("This query was canceled, typically by cancel_query()")
} else if(query_state != "QUERY_STATE_SUCCESS"){
warning("Query in process, checking again in 5 seconds")
Sys.sleep(5)
# run it back
return(
get_query_from_token(query_run_id = query_run_id,
api_key = api_key,
page_number = page_number,
page_size = page_size,
result_format = result_format,
api_url = api_url
)
)
} else {
request <- content(req, as = 'parsed')
if(is.null(request$status) & !is.null(request$errors)){
stop(request$errors)
}
# if running give it a few seconds
# this won't count as a re-request as long as cache intact
if(request$status == 'running'){
Sys.sleep(5)
warning("Query is still running! Trying again shortly")
return(
get_query_from_token(query_token, api_key, page_number, page_size)
headers = c(
"Content-Type" = 'application/json',
"x-api-key" = api_key
)
} else if(request$status == 'finished') {
return(request)
request_data <- as.character(
jsonlite::toJSON(pretty = TRUE,
list(
"jsonrpc" = "2.0",
"method" = "getQueryRunResults",
"params" = list(
list(
"queryRunId" = query_run_id,
"format" = result_format,
"page" = list(
"number" = page_number,
"size" = page_size
)
)
),
"id" = 1
),
auto_unbox = TRUE
)
)
} else {
return(
paste0("Request not running nor finished, see status code: ",
request$status))
}
return(request)
return(
content(
httr::POST(
api_url,
config = httr::add_headers(.headers = headers),
body = request_data)
)
)
}

View File

@ -0,0 +1,52 @@
library(jsonlite)
library(httr)
#' Get Query ID Status
#'
#' Uses Flipside ShroomDK to access the status of a query run id from `create_query_token()`
#' @param query_run_id queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`
#' @param api_key Flipside Crypto ShroomDK API Key
#' @param api_url default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.
#' @return returns request content; for content `x`, use `x$result$queryRun$state` and `x$result$queryRun$errorMessage`. Expect one of
#' QUERY_STATE_READY, QUERY_STATE_RUNNING, QUERY_STATE_STREAMING_RESULTS, QUERY_STATE_SUCCESS, QUERY_STATE_FAILED, QUERY_STATE_CANCELED
#' @import jsonlite httr
#' @export
#'
#' @examples
#' \dontrun{
#' query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
#' get_query_status(query$result$queryRequest$queryRunId, api_key)
#' }
get_query_status <- function(query_run_id, api_key,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"){
headers = c(
"Content-Type" = 'application/json',
"x-api-key" = api_key
)
# get status of a run id
request_status_body <- as.character(
jsonlite::toJSON(pretty = TRUE,
list(
"jsonrpc" = "2.0",
"method" = "getQueryRun",
"params" = list(
list( "queryRunId" = query_run_id
)
),
"id" = 1
),
auto_unbox = TRUE
)
)
return(
content(
httr::POST(
api_url,
config = httr::add_headers(.headers = headers),
body = request_status_body)
)
)
}

View File

@ -4,25 +4,36 @@
\alias{auto_paginate_query}
\title{Auto Paginate Queries}
\usage{
auto_paginate_query(query, api_key, maxrows = 1e+06)
auto_paginate_query(
query,
api_key,
page_size = 1000,
page_count = 1,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"
)
}
\arguments{
\item{query}{The SQL query to pass to ShroomDK}
\item{api_key}{ShroomDK API key.}
\item{maxrows}{Max rows allowed in ShroomDK, 1M at time of writing.}
\item{page_size}{Default 1000. May return error if page_size is tool large and data to exceed 30MB.}
\item{page_count}{Default 1. How many pages, of page_size rows each, to read.}
\item{api_url}{default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.}
}
\value{
data frame of up to 1M rows, see ?clean_query for more details on column classes.
data frame of up to `page_size * page_count` rows, see ?clean_query for more details on column classes.
}
\description{
Grabs up to maxrows in a query by going through each page 100k rows at a time.
Grabs up to maxrows in a query by going through each page to download one at a time.
}
\examples{
\dontrun{
pull_data <- auto_paginate_query("
SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000",
api_key = readLines("api_key.txt"))
SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10001",
api_key = readLines("api_key.txt"),
page_count = 10)
}
}

View File

@ -0,0 +1,33 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/cancel_query.R
\name{cancel_query}
\alias{cancel_query}
\title{Cancel Query}
\usage{
cancel_query(
query_run_id,
api_key,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"
)
}
\arguments{
\item{query_run_id}{queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`}
\item{api_key}{Flipside Crypto ShroomDK API Key}
\item{api_url}{default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.}
}
\value{
returns a list of the status_canceled (TRUE or FALSE) and the cancel object (which includes related details).
}
\description{
Uses Flipside ShroomDK to CANCEL a query run id from `create_query_token()`, as the new API uses warehouse-seconds to charge users above the free tier,
the ability to cancel is critical for cost management.
}
\examples{
\dontrun{
query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000000", api_key)
query_status <- get_query_status(query$result$queryRequest$queryRunId, api_key)
canceled <- cancel_query(query$result$queryRequest$queryRunId, api_key)
}
}

View File

@ -12,7 +12,7 @@ clean_query(request, try_simplify = TRUE)
\item{try_simplify}{because requests can return JSON and may not have the same length
across values, they may not be data frame compliant (all columns having the same number of rows).
A key example would be TX_JSON in EVM FACT_TRANSACTION tables which include 50+
extra details from transaction logs. But other examples like NULLs TO_ADDRESS can have similar
extra details from transaction logs. But other examples like NULLs in TO_ADDRESS can have similar
issues. Default TRUE.}
}
\value{
@ -26,8 +26,9 @@ intelligently.
}
\examples{
\dontrun{
query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
request = get_query_from_token(query$token, api_key, 1, 10000)
clean_query(request, try_simplify = FALSE)
query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000", api_key)
request <- get_query_from_token(query$result$queryRequest$queryRunId, api_key)
df1 <- clean_query(request, try_simplify = TRUE) # warning b/c of tx_json
df2 <- clean_query(request, try_simplify = FALSE) # silently returns columns of lists
}
}

View File

@ -4,16 +4,24 @@
\alias{create_query_token}
\title{Create Query Token}
\usage{
create_query_token(query, api_key, ttl = 10, cache = TRUE)
create_query_token(
query,
api_key,
ttl = 1,
mam = 10,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"
)
}
\arguments{
\item{query}{Flipside Crypto Snowflake SQL compatible query as a string.}
\item{api_key}{Flipside Crypto ShroomDK API Key}
\item{ttl}{time (in minutes) to keep query in cache.}
\item{ttl}{time-to-live (in hours) to keep query results available. Default 1 hour.}
\item{cache}{Use cached results; set as FALSE to re-execute.}
\item{mam}{max-age-minutes, lifespan of cache. set to 0 to always re-execute. Default 10 minutes.}
\item{api_url}{default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.}
}
\value{
list of `token` and `cached` use `token` in `get_query_from_token()`
@ -28,7 +36,7 @@ allowing for pagination and multiple requests before expending more daily reques
create_query_token(
query = "SELECT * FROM ethereum.core.fact_transactions LIMIT 1",
api_key = readLines("api_key.txt"),
ttl = 15,
cache = TRUE)
ttl = 1,
mam = 5)
}
}

View File

@ -4,30 +4,40 @@
\alias{get_query_from_token}
\title{Get Query From Token}
\usage{
get_query_from_token(query_token, api_key, page_number = 1, page_size = 1e+05)
get_query_from_token(
query_run_id,
api_key,
page_number = 1,
page_size = 1000,
result_format = "csv",
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"
)
}
\arguments{
\item{query_token}{token from `create_query_token()`}
\item{query_run_id}{queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`}
\item{api_key}{Flipside Crypto ShroomDK API Key}
\item{page_number}{Query tokens are cached and 100k rows max. Get up to 1M rows by going through pages.}
\item{page_number}{Results are cached, max 30MB of data per page.}
\item{page_size}{Default 100,000. Paginate via page_number.}
\item{page_size}{Default 1000. Paginate via page_number. May return error if page_size causes data to exceed 30MB.}
\item{result_format}{Default to csv. Options: csv and json.}
\item{api_url}{default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.}
}
\value{
returns a request of length 8: `results`, `columnLabels`,
`columnTypes`, `startedAt`, `endedAt`, `pageNumber`, `pageSize`, `status`
returns a list of jsonrpc, id, and result. Within result are:
columnNames, columnTypes, rows, page, sql, format, originalQueryRun, redirectedToQueryRun
use `clean_query()` to transform this into a data frame.
}
\description{
Uses Flipside ShroomDK to access a Query Token. Query tokens are cached up to `ttl` minutes
for each `query`. This function is for pagination and multiple requests
while . Note: To reduce payload it returns
a list of outputs (separating column names from rows).
Uses Flipside ShroomDK to access a Query Token (Run ID). This function is for pagination and multiple requests.
Note: To reduce payload it returns a list of outputs (separating column names from rows). Use `clean_query()` to
}
\examples{
\dontrun{
query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
get_query_from_token(query$token, api_key, 1, 10000)
query <- create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 1000", api_key)
fact_transactions <- get_query_from_token(query$result$queryRequest$queryRunId, api_key, 1, 1000)
}
}

View File

@ -0,0 +1,32 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/get_query_status.R
\name{get_query_status}
\alias{get_query_status}
\title{Get Query ID Status}
\usage{
get_query_status(
query_run_id,
api_key,
api_url = "https://api-v2.flipsidecrypto.xyz/json-rpc"
)
}
\arguments{
\item{query_run_id}{queryRunId from `create_query_token()`, for token stored as `x`, use `x$result$queryRequest$queryRunId`}
\item{api_key}{Flipside Crypto ShroomDK API Key}
\item{api_url}{default to https://api-v2.flipsidecrypto.xyz/json-rpc but upgradeable for user.}
}
\value{
returns request content; for content `x`, use `x$result$queryRun$state` and `x$result$queryRun$errorMessage`. Expect one of
QUERY_STATE_READY, QUERY_STATE_RUNNING, QUERY_STATE_STREAMING_RESULTS, QUERY_STATE_SUCCESS, QUERY_STATE_FAILED, QUERY_STATE_CANCELED
}
\description{
Uses Flipside ShroomDK to access the status of a query run id from `create_query_token()`
}
\examples{
\dontrun{
query = create_query_token("SELECT * FROM ETHEREUM.CORE.FACT_TRANSACTIONS LIMIT 10000", api_key)
get_query_status(query$result$queryRequest$queryRunId, api_key)
}
}

BIN
r/shroomDK_0.2.0.tar.gz Normal file

Binary file not shown.