diff --git a/DESCRIPTION b/DESCRIPTION index 7d10577..ec2e2ea 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: zen4R -Version: 0.9 -Date: 2023-09-20 +Version: 1.0 +Date: 2024-03-11 Title: Interface to 'Zenodo' REST API Authors@R: c( person("Emmanuel", "Blondel", role = c("aut", "cre"), email = "emmanuel.blondel1@gmail.com", comment = c(ORCID = "0000-0002-5870-5762")), @@ -11,7 +11,7 @@ Authors@R: c( person("Jemma", "Stachelek", role = c("ctb"), comment = c(ORCID = "0000-0002-5924-2464"))) Maintainer: Emmanuel Blondel Depends: R (>= 3.3.0), methods -Imports: R6, httr, jsonlite, XML, xml2, keyring, tools, atom4R, utf8 +Imports: R6, httr, jsonlite, XML, xml2, keyring, tools, atom4R, utf8, plyr Suggests: testthat, parallel, knitr, markdown Description: Provides an Interface to 'Zenodo' () REST API, including management of depositions, attribution of DOIs by 'Zenodo' and diff --git a/NAMESPACE b/NAMESPACE index 31ac534..bbd4de9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,7 @@ import(keyring) import(methods) import(xml2) importFrom(R6,R6Class) +importFrom(plyr,rbind.fill) importFrom(tools,file_path_as_absolute) importFrom(tools,md5sum) importFrom(utf8,utf8_encode) diff --git a/R/ZenodoManager.R b/R/ZenodoManager.R index a4894d1..c49af85 100644 --- a/R/ZenodoManager.R +++ b/R/ZenodoManager.R @@ -87,7 +87,7 @@ ZenodoManager <- R6Class("ZenodoManager", initialize = function(url = "https://zenodo.org/api", token = zenodo_pat(), sandbox = FALSE, logger = NULL, keyring_backend = 'env'){ super$initialize(logger = logger) - if(sandbox) url = "https://sandbox.zenodo.org/api" + if(sandbox) url = "https://zenodo-rdm-qa.web.cern.ch/api" private$url = url if(url == "https://sandbox.zenodo.org/api") self$sandbox = TRUE if(!is.null(token)) if(nzchar(token)){ @@ -125,7 +125,59 @@ ZenodoManager <- R6Class("ZenodoManager", return(token) }, - #Licenses + #Vocabulary/Languages + #------------------------------------------------------------------------------------------ + + #' @description Get Languages supported by Zenodo. + #' @param pretty Prettify the output. By default the argument \code{pretty} is set to + #' \code{TRUE} which will returns the list of languages as \code{data.frame}. + #' Set \code{pretty = FALSE} to get the raw list of languages + #' @return list of languages as \code{data.frame} or \code{list} + getLanguages = function(pretty = TRUE){ + zenReq <- ZenodoRequest$new(private$url, "GET", "vocabularies/languages?q=&size=1000", + token= self$getToken(), + logger = self$loggerType) + zenReq$execute() + out <- zenReq$getResponse() + if(zenReq$getStatus() == 200){ + out = out$hits$hits + if(pretty){ + out = do.call("rbind", lapply(out,function(x){ + rec = data.frame( + id = x$id, + title = x$title[[1]], + revision_id = x$revision_id, + created = x$created, + updated = x$updated + ) + return(rec) + })) + } + self$INFO("Successfully fetched list of languages") + }else{ + self$ERROR(sprintf("Error while fetching languages: %s", out$message)) + } + return(out) + }, + + #' @description Get language by Id. + #' @param id license id + #' @return the license + getLanguageById = function(id){ + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("vocabularies/languages/%s",id), + token= self$getToken(), + logger = self$loggerType) + zenReq$execute() + out <- zenReq$getResponse() + if(zenReq$getStatus() == 200){ + self$INFO(sprintf("Successfully fetched language '%s'",id)) + }else{ + self$ERROR(sprintf("Error while fetching language '%s': %s", id, out$message)) + } + return(out) + }, + + #Vocabulary/Licenses #------------------------------------------------------------------------------------------ #' @description Get Licenses supported by Zenodo. @@ -134,22 +186,26 @@ ZenodoManager <- R6Class("ZenodoManager", #' Set \code{pretty = FALSE} to get the raw list of licenses. #' @return list of licenses as \code{data.frame} or \code{list} getLicenses = function(pretty = TRUE){ - zenReq <- ZenodoRequest$new(private$url, "GET", "licenses/?q=&size=1000", + zenReq <- ZenodoRequest$new(private$url, "GET", "vocabularies/licenses?q=&size=1000", token= self$getToken(), logger = self$loggerType) zenReq$execute() out <- zenReq$getResponse() if(zenReq$getStatus() == 200){ - out <- out$hits$hits + out = out$hits$hits if(pretty){ out = do.call("rbind", lapply(out,function(x){ - rec = x$metadata - rec$`$schema` <- NULL - rec$is_generic <- NULL - rec$suggest <- NULL - rec <- as.data.frame(rec) - rec <- rec[,c("id", "title", "url", "domain_content", "domain_data", "domain_software", "family", - "maintainer", "od_conformance", "osd_conformance", "status")] + rec = data.frame( + id = x$id, + title = x$title[[1]], + description = if(!is.null(x$description)) x$description[[1]] else NA, + url = x$props$url, + schema = x$props$scheme, + osi_approved = x$props$osi_approved, + revision_id = x$revision_id, + created = x$created, + updated = x$updated + ) return(rec) })) } @@ -164,7 +220,7 @@ ZenodoManager <- R6Class("ZenodoManager", #' @param id license id #' @return the license getLicenseById = function(id){ - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("licenses/%s",id), + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("vocabularies/licenses/%s",id), token= self$getToken(), logger = self$loggerType) zenReq$execute() @@ -184,34 +240,80 @@ ZenodoManager <- R6Class("ZenodoManager", #' @param pretty Prettify the output. By default the argument \code{pretty} is set to #' \code{TRUE} which will returns the list of communities as \code{data.frame}. #' Set \code{pretty = FALSE} to get the raw list of communities + #' @param q an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. + #' Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, + #' not all communities can be listed from Zenodo, a query has to be specified. + #' @param size number of communities to be returned. By default equal to 500 #' @return list of communities as \code{data.frame} or \code{list} - getCommunities = function(pretty = TRUE){ - zenReq <- ZenodoRequest$new(private$url, "GET", "communities/?q=&size=10000", - token= self$getToken(), + getCommunities = function(pretty = TRUE, q = "", size = 500){ + page <- 1 + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("communities?q=%s&size=%s&page=%s", URLencode(q), size, page), + token = self$getToken(), logger = self$loggerType) zenReq$execute() - out <- zenReq$getResponse() + out <- NULL if(zenReq$getStatus() == 200){ - out <- out$hits$hits - if(pretty){ - out = do.call("rbind", lapply(out,function(x){ - rec = data.frame( - id = x$id, - title = x$title, - description = x$description, - curation_policy = x$curation_policy, - url = x$links$html, - created = x$created, - updated = x$updated, - stringsAsFactors = FALSE - ) - return(rec) - })) + resp <- zenReq$getResponse() + communities <- resp$hits$hits + total <- resp$hits$total + if(total > 10000){ + self$WARN(sprintf("Total of %s records found: the Zenodo API limits to a maximum of 10,000 records!", total)) + } + total_remaining <- total + hasCommunities <- length(communities)>0 + while(hasCommunities){ + out <- c(out, communities) + self$INFO(sprintf("Successfully fetched list of communities - page %s", page)) + total_remaining <- total_remaining-length(communities) + if(total_remaining <= size) size = total_remaining + if(total_remaining == 0){ + break + } + + #next page + page <- page+1 + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("communities?q=%s&size=%s&page=%s", URLencode(q), size, page), + token = self$getToken(), + logger = self$loggerType) + zenReq$execute() + if(zenReq$getStatus() == 200){ + resp <- zenReq$getResponse() + communities <- resp$hits$hits + hasCommunities <- length(communities)>0 + }else{ + self$WARN(sprintf("Maximum allowed size for list of communities at page %s", page)) + break + } } - self$INFO("Successfully fetched list of communities") + self$INFO("Successfully fetched list of communities!") }else{ + out <- zenReq$getResponse() self$ERROR(sprintf("Error while fetching communities: %s", out$message)) + for(error in out$errors){ + self$ERROR(sprintf("Error: %s - %s", error$field, error$message)) + } } + + if(pretty){ + out = do.call("rbind", lapply(out,function(x){ + rec = data.frame( + id = x$id, + title = x$metadata$title, + description = if(!is.null(x$metadata$description)) x$metadata$description else NA, + website = if(!is.null(x$metadata$website)) x$metadata$website else NA, + visibility = x$access$visibility, + member_policy = x$access$member_policy, + record_policy = x$access$record_policy, + review_policy = x$access$review_policy, + url = x$links$self_html, + created = x$created, + updated = x$updated, + stringsAsFactors = FALSE + ) + return(rec) + })) + } + return(out) }, @@ -233,63 +335,69 @@ ZenodoManager <- R6Class("ZenodoManager", return(out) }, - #Grants + #Special vocabulary/Awards (former Grants) #------------------------------------------------------------------------------------------ - #' @description Get Grants supported by Zenodo. + #' @description Get Grants supported by Zenodo. DEPRECATED: replaced by \code{getAwards} #' @param pretty Prettify the output. By default the argument \code{pretty} is set to #' \code{TRUE} which will returns the list of grants as \code{data.frame}. #' Set \code{pretty = FALSE} to get the raw list of grants #' @param q an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. - #' Note that the Zenodo API restrains a maximum number of 10,000 grants to be retrieved. Consequently, + #' Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, #' not all grants can be listed from Zenodo, a query has to be specified. - #' @param size number of grants to be returned. By default equal to 1000. + #' @param size number of grants to be returned. By default equal to 500. #' @return list of grants as \code{data.frame} or \code{list} - getGrants = function(q = "", pretty = TRUE, size = 1000){ - if(q=="") size = 10000 + getGrants = function(q = "", pretty = TRUE, size = 500){ + self$WARN("Method 'getGrants' is deprecated, please use 'getAwards' instead!") + return(self$getAwards(q = q, pretty = pretty, size = size)) + }, + + #' @description Get Awards supported by Zenodo. + #' @param pretty Prettify the output. By default the argument \code{pretty} is set to + #' \code{TRUE} which will returns the list of awards as \code{data.frame}. + #' Set \code{pretty = FALSE} to get the raw list of awards + #' @param q an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. + #' Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, + #' not all awards can be listed from Zenodo, a query has to be specified. + #' @param size number of awards to be returned. By default equal to 500. + #' @return list of awards as \code{data.frame} or \code{list} + getAwards = function(q = "", pretty = TRUE, size = 500){ page <- 1 - lastPage <- FALSE - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("grants/?q=%s&size=%s&page=%s", URLencode(q), size, page), + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("awards?q=%s&size=%s&page=%s", URLencode(q), size, page), token = self$getToken(), logger = self$loggerType) zenReq$execute() out <- NULL if(zenReq$getStatus() == 200){ resp <- zenReq$getResponse() - grants <- resp$hits$hits + awards <- resp$hits$hits total <- resp$hits$total if(total > 10000){ self$WARN(sprintf("Total of %s records found: the Zenodo API limits to a maximum of 10,000 records!", total)) } total_remaining <- total - hasGrants <- length(grants)>0 - while(hasGrants){ - out <- c(out, grants) - if(!is.null(grants)){ - self$INFO(sprintf("Successfully fetched list of grants - page %s", page)) - if(q!=""){ - page <- page+1 #next - total_remaining <- total_remaining-length(grants) - }else{ - break; - } - }else{ - lastPage <- TRUE + hasAwards <- length(awards)>0 + while(hasAwards){ + out <- c(out, awards) + total_remaining <- total_remaining-length(awards) + if(total_remaining <= size) size = total_remaining + if(total_remaining == 0){ + break } - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("grants/?q=%s&size=%s&page=%s", URLencode(q), size, page), + + #next page + page = page+1 + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("awards?q=%s&size=%s&page=%s", URLencode(q), size, page), token = self$getToken(), logger = self$loggerType) zenReq$execute() if(zenReq$getStatus() == 200){ resp <- zenReq$getResponse() - grants <- resp$hits$hits - hasGrants <- length(grants)>0 - if(lastPage) break; + awards <- resp$hits$hits + hasAwards <- length(awards)>0 }else{ - self$WARN(sprintf("Maximum allowed size for list of grants - page %s - attempt to decrease size", page)) - size <- size-1 - hasGrants <- TRUE - grants <- NULL + self$WARN(sprintf("Maximum allowed size for list of awars at page %s", page)) + break } } self$INFO("Successfully fetched list of grants!") @@ -304,24 +412,14 @@ ZenodoManager <- R6Class("ZenodoManager", if(pretty){ out = do.call("rbind", lapply(out,function(x){ rec = data.frame( - id = x$metadata$internal_id, - code = x$metadata$code, - title = x$metadata$title, - startdate = x$metadata$startdate, - enddate = x$metadata$enddate, - url = x$metadata$url, + id = x$id, + number = x$number, + title = x$title[[1]], created = x$created, updated = x$updated, - funder_country = x$metadata$funder$country, - funder_doi = x$metadata$funder$doi, - funder_name = x$metadata$funder$name, - funder_type = x$metadata$funder$type, - funder_subtype = x$metadata$funder$subtype, - funder_parent_country = if(length(x$metadata$funder$parent)>0) x$metadata$funder$parent$country else NA, - funder_parent_doi = if(length(x$metadata$funder$parent)>0) x$metadata$funder$parent$doi else NA, - funder_parent_name = if(length(x$metadata$funder$parent)>0) x$metadata$funder$parent$name else NA, - funder_parent_type = if(length(x$metadata$funder$parent)>0) x$metadata$funder$parent$type else NA, - funder_parent_subtype = if(length(x$metadata$funder$parent)>0) x$metadata$funder$parent$subtype else NA, + funder_id = x$funder$id, + funder_name = x$funder$name, + program = if(!is.null(x$program)) x$program else NA, stringsAsFactors = FALSE ) return(rec) @@ -330,30 +428,49 @@ ZenodoManager <- R6Class("ZenodoManager", return(out) }, - #' @description Get grants by name. + #' @description Get grants by name. DEPRECATED: replaced by \code{getAwardByName} #' @param name name #' @param pretty Prettify the output. By default the argument \code{pretty} is set to #' \code{TRUE} which will returns the list of grants as \code{data.frame}. #' Set \code{pretty = FALSE} to get the raw list of grants #' @return list of grants as \code{data.frame} or \code{list} getGrantsByName = function(name, pretty = TRUE){ - query = sprintf("title:%s", URLencode(paste0("\"",name,"\""))) - self$getGrants(q = query, pretty = pretty) + self$WARN("Method 'getGrantsByName' is deprecated, please use 'getAwardsByName' instead!") + return(self$getAwardsByName(name = name, pretty = pretty)) }, - #' @description Get grant by Id. + #' @description Get awards by name. + #' @param name name + #' @param pretty Prettify the output. By default the argument \code{pretty} is set to + #' \code{TRUE} which will returns the list of awards as \code{data.frame}. + #' Set \code{pretty = FALSE} to get the raw list of awards + #' @return list of awards as \code{data.frame} or \code{list} + getAwardsByName = function(name, pretty = TRUE){ + query = sprintf("title.en:%s", URLencode(paste0("\"",name,"\""))) + self$getAwards(q = query, pretty = pretty) + }, + + #' @description Get grant by Id.DEPRECATED: replaced by \code{getAwardById} #' @param id grant id #' @return the grant getGrantById = function(id){ - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("grants/%s",id), + self$WARN("Method 'getGrantById' is deprecated, please use 'getAwardById' instead!") + return(self$getAwardById(id)) + }, + + #' @description Get award by Id. + #' @param id award id + #' @return the award + getAwardById = function(id){ + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("awards/%s",id), token= self$getToken(), logger = self$loggerType) zenReq$execute() out <- zenReq$getResponse() if(zenReq$getStatus() == 200){ - self$INFO(sprintf("Successfully fetched grant '%s'",id)) + self$INFO(sprintf("Successfully fetched award '%s'",id)) }else{ - self$ERROR(sprintf("Error while fetching grant '%s': %s", id, out$message)) + self$ERROR(sprintf("Error while fetching award '%s': %s", id, out$message)) out <- NULL } return(out) @@ -367,15 +484,13 @@ ZenodoManager <- R6Class("ZenodoManager", #' \code{TRUE} which will returns the list of funders as \code{data.frame}. #' Set \code{pretty = FALSE} to get the raw list of funders #' @param q an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. - #' Note that the Zenodo API restrains a maximum number of 10,000 funders to be retrieved. Consequently, + #' Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, #' not all funders can be listed from Zenodo, a query has to be specified. - #' @param size number of funders to be returned. By default equal to 1000. + #' @param size number of funders to be returned. By default equal to 500 #' @return list of funders as \code{data.frame} or \code{list} - getFunders = function(q = "", pretty = TRUE, size = 1000){ - if(q=="") size = 10000 + getFunders = function(q = "", pretty = TRUE, size = 500){ page <- 1 - lastPage <- FALSE - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("funders/?q=%s&size=%s&page=%s", URLencode(q), size, page), + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("funders?q=%s&size=%s&page=%s", URLencode(q), size, page), token = self$getToken(), logger = self$loggerType) zenReq$execute() @@ -391,18 +506,16 @@ ZenodoManager <- R6Class("ZenodoManager", hasFunders <- length(funders)>0 while(hasFunders){ out <- c(out, funders) - if(!is.null(funders)){ - self$INFO(sprintf("Successfully fetched list of funders - page %s", page)) - if(q != ""){ - page <- page+1 #next - total_remaining <- total_remaining-length(funders) - }else{ - break; - } - }else{ - lastPage <- TRUE + self$INFO(sprintf("Successfully fetched list of funders - page %s", page)) + total_remaining <- total_remaining-length(funders) + if(total_remaining <= size) size = total_remaining + if(total_remaining == 0){ + break } - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("funders/?q=%s&size=%s&page=%s", URLencode(q), size, page), + + #next page + page <- page+1 + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("funders?q=%s&size=%s&page=%s", URLencode(q), size, page), token = self$getToken(), logger = self$loggerType) zenReq$execute() @@ -410,12 +523,9 @@ ZenodoManager <- R6Class("ZenodoManager", resp <- zenReq$getResponse() funders <- resp$hits$hits hasFunders <- length(funders)>0 - if(lastPage) break; }else{ - self$WARN(sprintf("Maximum allowed size for list of funders - page %s - attempt to decrease size", page)) - size <- size-1 - hasFunders <- TRUE - funders <- NULL + self$WARN(sprintf("Maximum allowed size for list of communities reached at page %s", page)) + break } } self$INFO("Successfully fetched list of funders!") @@ -428,14 +538,19 @@ ZenodoManager <- R6Class("ZenodoManager", } if(pretty){ - out = do.call("rbind", lapply(out,function(x){ + out = do.call("rbind.fill", lapply(out,function(x){ + + identifiers = do.call("cbind", lapply(x$identifiers, function(identifier){ + out_id = data.frame(scheme = identifier$identifier, stringsAsFactors = F) + names(out_id) = identifier$scheme + return(out_id) + })) + rec = data.frame( - id = x$metadata$doi, - doi = x$metadata$doi, - country = x$metadata$country, - name = x$metadata$name, - type = x$metadata$type, - subtype = x$metadata$subtype, + id = x$id, + country = x$country, + name = x$name, + identifiers, created = x$created, updated = x$updated, stringsAsFactors = FALSE @@ -478,14 +593,13 @@ ZenodoManager <- R6Class("ZenodoManager", #Depositions #------------------------------------------------------------------------------------------ - #' @description Get the list of Zenodo records deposited in your Zenodo workspace. By defaut - #' the list of depositions will be returned by page with a size of 10 results per - #' page (default size of the Zenodo API). The parameter \code{q} allows to specify - #' an ElasticSearch-compliant query to filter depositions (default query is empty - #' to retrieve all records). The argument \code{all_versions}, if set to TRUE allows - #' to get all versions of records as part of the depositions list. The argument \code{exact} - #' specifies that an exact matching is wished, in which case paginated search will be - #' disabled (only the first search page will be returned). + #' @description Get the list of Zenodo records deposited in your Zenodo workspace (user records). By default + #' the list of depositions will be returned by page with a size of 10 results per page (default size of + #' the Zenodo API). The parameter \code{q} allows to specify an ElasticSearch-compliant query to filter + #' depositions (default query is empty to retrieve all records). The argument \code{all_versions}, if set + #' to TRUE allows to get all versions of records as part of the depositions list. The argument \code{exact} + #' specifies that an exact matching is wished, in which case paginated search will be disabled (only the first + #' search page will be returned). #' Examples of ElasticSearch queries for Zenodo can be found at \href{https://help.zenodo.org/guides/search/}{https://help.zenodo.org/guides/search/}. #' @param q Elastic-Search-compliant query, as object of class \code{character}. Default is "" #' @param size number of depositions to be retrieved per request (paginated). Default is 10 @@ -496,13 +610,13 @@ ZenodoManager <- R6Class("ZenodoManager", getDepositions = function(q = "", size = 10, all_versions = FALSE, exact = TRUE, quiet = FALSE){ page <- 1 - baseUrl <- "deposit/depositions" + baseUrl <- "user/records" #set in #72, now re-deactivated through #76 (due to Zenodo server-side changes) #if(!private$sandbox) baseUrl <- paste0(baseUrl, "/") req <- sprintf("%s?q=%s&size=%s&page=%s", baseUrl, URLencode(q), size, page) - if(all_versions) req <- paste0(req, "&all_versions=1") + if(all_versions) req <- paste0(req, "&allversions=1") zenReq <- ZenodoRequest$new(private$url, "GET", req, token = self$getToken(), logger = if(quiet) NULL else self$loggerType) @@ -510,10 +624,21 @@ ZenodoManager <- R6Class("ZenodoManager", out <- NULL if(zenReq$getStatus() == 200){ resp <- zenReq$getResponse() - hasRecords <- length(resp)>0 + records <- resp$hits$hits + total <- resp$hits$total + if(total > 10000){ + self$WARN(sprintf("Total of %s records found: the Zenodo API limits to a maximum of 10,000 records!", total)) + } + total_remaining <- total + hasRecords <- length(records)>0 while(hasRecords){ - out <- c(out, lapply(resp, ZenodoRecord$new)) - if(!quiet) self$INFO(sprintf("Successfully fetched list of depositions - page %s", page)) + out <- c(out, lapply(records, ZenodoRecord$new)) + if(!quiet) self$INFO(sprintf("Successfully fetched list of depositions (user records) - page %s", page)) + total_remaining <- total_remaining-length(records) + if(total_remaining <= size) size = total_remaining + if(total_remaining == 0){ + break + } if(exact){ hasRecords <- FALSE @@ -521,19 +646,26 @@ ZenodoManager <- R6Class("ZenodoManager", #next page <- page+1 nextreq <- sprintf("%s?q=%s&size=%s&page=%s", baseUrl, q, size, page) - if(all_versions) nextreq <- paste0(nextreq, "&all_versions=1") + if(all_versions) nextreq <- paste0(nextreq, "&allversions=1") zenReq <- ZenodoRequest$new(private$url, "GET", nextreq, token = self$getToken(), logger = if(quiet) NULL else self$loggerType) zenReq$execute() resp <- zenReq$getResponse() - hasRecords <- length(resp)>0 + if(zenReq$getStatus() == 200){ + resp <- zenReq$getResponse() + records <- resp$hits$hits + hasRecords <- length(records)>0 + }else{ + if(!quiet) self$WARN(sprintf("Maximum allowed size for list of depositions (user records) at page %s", page)) + break + } } } - if(!quiet) self$INFO("Successfully fetched list of depositions!") + if(!quiet) self$INFO("Successfully fetched list of depositions (user records)!") }else{ out <- zenReq$getResponse() - if(!quiet) self$ERROR(sprintf("Error while fetching depositions: %s", out$message)) + if(!quiet) self$ERROR(sprintf("Error while fetching depositions (user records): %s", out$message)) for(error in out$errors){ self$ERROR(sprintf("Error: %s - %s", error$field, error$message)) } @@ -657,8 +789,8 @@ ZenodoManager <- R6Class("ZenodoManager", depositRecord = function(record, publish = FALSE){ data <- record type <- ifelse(is.null(record$id), "POST", "PUT") - request <- ifelse(is.null(record$id), "deposit/depositions", - sprintf("deposit/depositions/%s", record$id)) + request <- ifelse(is.null(record$id), "records", + sprintf("records/%s/draft", record$id)) zenReq <- ZenodoRequest$new(private$url, type, request, data = data, token = self$getToken(), logger = self$loggerType) @@ -754,8 +886,8 @@ ZenodoManager <- R6Class("ZenodoManager", #' @param recordId the ID of the record to be deleted #' @return \code{TRUE} if deleted, \code{FALSE} otherwise deleteRecord = function(recordId){ - zenReq <- ZenodoRequest$new(private$url, "DELETE", "deposit/depositions", - data = recordId, token = self$getToken(), + zenReq <- ZenodoRequest$new(private$url, "DELETE", sprintf("records/%s/draft", recordId), + token = self$getToken(), logger = self$loggerType) zenReq$execute() out <- FALSE @@ -870,11 +1002,14 @@ ZenodoManager <- R6Class("ZenodoManager", return(out) }, + #File management + #------------------------------------------------------------------------------------------ + #' @description Get list of files attached to a Zenodo record. #' @param recordId the ID of the record. #' @return list of files getFiles = function(recordId){ - zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("deposit/depositions/%s/files", recordId), + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("records/%s/draft/files", recordId), token = self$getToken(), logger = self$loggerType) zenReq$execute() @@ -889,11 +1024,83 @@ ZenodoManager <- R6Class("ZenodoManager", return(out) }, - #' @description Uploads a file to a Zenodo record + #' @description Get a file record metadata. + #' @param recordId the ID of the record. + #' @param filename filename + #' @return the file metadata + getFile = function(recordId, filename){ + zenReq <- ZenodoRequest$new(private$url, "GET", sprintf("records/%s/draft/files/%s", recordId, filename), + token = self$getToken(), + logger = self$loggerType) + zenReq$execute() + out <- zenReq$getResponse() + if(zenReq$getStatus() == 200){ + self$INFO(sprintf("Successful fetched file metadata for record '%s' - filename '%s'", recordId, filename)) + }else{ + self$ERROR(sprintf("Error while fetching file(s) for record '%s': %s", recordId, out$message)) + } + return(out) + }, + + #' @description Start a file upload. The method will create a key for the file to be uploaded + #' This method is essentially for internal purpose, and is called directly in \code{uploadFile} + #' for user convenience and for backward compatibility with the legacy Zenodo API. + #' @param path Local path of the file + #' @param recordId ID of the record + startFileUpload = function(path, recordId){ + self$INFO(sprintf("Start upload procedure for file '%s'", path)) + fileparts <- unlist(strsplit(path,"/")) + filename <- fileparts[length(fileparts)] + + zenReq <- ZenodoRequest$new(private$url, "POST", sprintf("records/%s/draft/files", recordId), + data = list(list(key = filename)), + token = self$getToken(), + logger = self$loggerType) + zenReq$execute() + out <- FALSE + if(zenReq$getStatus() == 201){ + self$INFO(sprintf("Successfully started upload procedure for file '%s'", path)) + out <- TRUE + }else{ + self$ERROR(sprintf("Error while starting upload procedure for file '%s' in record %s: %s", + path, recordId, out$message)) + } + return(out) + }, + + #' @description Completes a file upload. The method will complete a file upload through a commit operation + #' This method is essentially for internal purpose, and is called directly in \code{uploadFile} + #' for user convenience and for backward compatibility with the legacy Zenodo API. + #' @param path Local path of the file + #' @param recordId ID of the record + completeFileUpload = function(path, recordId){ + self$INFO(sprintf("Complete upload procedure for file '%s'", path)) + fileparts <- unlist(strsplit(path,"/")) + filename <- fileparts[length(fileparts)] + + zenReq <- ZenodoRequest$new(private$url, "POST", sprintf("records/%s/draft/files/%s/commit", recordId, filename), + token = self$getToken(), + logger = self$loggerType) + zenReq$execute() + out <- FALSE + if(zenReq$getStatus() == 200){ + self$INFO(sprintf("Successfully completed upload procedure for file '%s'", path)) + out <- TRUE + }else{ + self$ERROR(sprintf("Error while completing upload procedure for file '%s' in record %s: %s", + path, recordId, out$message)) + } + return(out) + }, + + #' @description Uploads a file to a Zenodo record. With the new Zenodo Invenio RDM API, this method + #' internally calls \code{startFileUpload} to create a file record (with a filename key) at start, followed + #' by the actual file content upload. At this stage, the file upload is in "pending" status. At the end, + #' the function calls \code{completeFileUpload} to commit the file which status becomes "completed". #' @param path Local path of the file - #' @param record object of class \code{ZenodoRecord} #' @param recordId ID of the record. Deprecated, use \code{record} instead to take advantage of the new Zenodo bucket upload API. - uploadFile = function(path, record = NULL, recordId = NULL){ + #' @param record object of class \code{ZenodoRecord} + uploadFile = function(path, recordId = NULL, record = NULL){ newapi = TRUE if(!is.null(recordId)){ self$WARN("'recordId' argument is deprecated, please consider using 'record' argument giving an object of class 'ZenodoRecord'") @@ -907,9 +1114,20 @@ ZenodoManager <- R6Class("ZenodoManager", self$WARN(sprintf("No bucket link for record id = %s. Revert to old file upload API", recordId)) newapi <- FALSE } + + #start upload (needed with new Invenio RDM API) + if(newapi){ + started = self$startFileUpload(path = path, recordId = recordId) + if(!started){ + return(NULL) + } + } + + #proceed with upload method <- if(newapi) "PUT" else "POST" - if(newapi) self$INFO(sprintf("Using new file upload API with bucket: %s", record$links$bucket)) - method_url <- if(newapi) sprintf("%s/%s", unlist(strsplit(record$links$bucket, "api/"))[2], URLencode(filename)) else sprintf("deposit/depositions/%s/files", recordId) + if(newapi) self$INFO("Using new file upload API with bucket") + method_url <- if(newapi) sprintf("records/%s/draft/files/%s/content", recordId, URLencode(filename)) else sprintf("deposit/depositions/%s/files", recordId) + print(method_url) zenReq <- if(newapi){ ZenodoRequest$new( private$url, method, method_url, @@ -930,21 +1148,37 @@ ZenodoManager <- R6Class("ZenodoManager", zenReq$execute() out <- NULL if(zenReq$getStatus() == 201){ - out <- ZenodoRecord$new(obj = zenReq$getResponse()) self$INFO(sprintf("Successful uploaded file to record '%s'", recordId)) + rec_files = self$getFiles(recordId = recordId) + out = rec_files$entries[sapply(rec_files$entries, function(x){x$key == filename})][[1]] + return(out) }else{ out <- zenReq$getResponse() self$ERROR(sprintf("Error while uploading file to record '%s': %s", recordId, out$message)) } + + #complete upload (needed with new Invenio RDM API) + if(newapi){ + completed = self$completeFileUpload(path = path, recordId = recordId) + if(!completed){ + self$WARN("File upload procedure completion failed, file is uploaded but remains in 'pending' status!") + }else{ + out$status = "completed" + } + } + return(out) }, - #' @description Deletes a file for a record + #' @description Deletes a file for a record. With the new Zenodo Invenio RDM API, if a file is + #' deleted although its status was pending, only the upload content is deleted, and the file upload + #' record (identified by a filename key) is kept. If the status was completed (with a file commit), + #' the file record is deleted. #' @param recordId ID of the record - #' @param fileId ID of the file to delete - deleteFile = function(recordId, fileId){ - zenReq <- ZenodoRequest$new(private$url, "DELETE", sprintf("deposit/depositions/%s/files", recordId), - data = fileId, token = self$getToken(), + #' @param filename name of the file to be deleted + deleteFile = function(recordId, filename){ + zenReq <- ZenodoRequest$new(private$url, "DELETE", sprintf("records/%s/draft/files", recordId), + data = filename, token = self$getToken(), logger = self$loggerType) zenReq$execute() out <- FALSE @@ -976,10 +1210,25 @@ ZenodoManager <- R6Class("ZenodoManager", #' @param exact object of class \code{logical} indicating if exact matching has to be applied. Default is \code{TRUE} #' @param quiet object of class \code{logical} indicating if logs have to skipped. Default is \code{FALSE} #' @return a list of \code{ZenodoRecord} - getRecords = function(q = "", size = 10, all_versions = FALSE, exact = FALSE){ + getRecords = function(q = "", size = 10, all_versions = FALSE, exact = TRUE){ page <- 1 - req <- sprintf("records/?q=%s&size=%s&page=%s", URLencode(q), size, page) - if(all_versions) req <- paste0(req, "&all_versions=1") + req <- sprintf("records?q=%s&size=10page=%s", URLencode(q), page) + if(all_versions) req <- paste0(req, "&allversions=1") + zenReq <- ZenodoRequest$new(private$url, "GET", req, + token = self$getToken(), + logger = NULL) + zenReq$execute() + total = 0 + if(zenReq$getStatus() == 200){ + resp <- zenReq$getResponse() + total <- resp$hits$total + if(total > 10000){ + self$WARN(sprintf("Total of %s records found: the Zenodo API limits to a maximum of 10,000 records!", total)) + } + } + + req <- sprintf("records?q=%s&size=%s&page=%s", URLencode(q), size, page) + if(all_versions) req <- paste0(req, "&allversions=1") zenReq <- ZenodoRequest$new(private$url, "GET_WITH_CURL", req, token = self$getToken(), logger = self$loggerType) @@ -987,24 +1236,37 @@ ZenodoManager <- R6Class("ZenodoManager", out <- NULL if(zenReq$getStatus() == 200){ resp <- zenReq$getResponse() - hasRecords <- length(resp)>0 + total_remaining <- total + records = resp + hasRecords <- length(records)>0 while(hasRecords){ - out <- c(out, lapply(resp, ZenodoRecord$new)) + out <- c(out, lapply(records, ZenodoRecord$new)) self$INFO(sprintf("Successfully fetched list of published records - page %s", page)) + total_remaining <- total_remaining-length(records) + if(total_remaining <= size) size = total_remaining + if(total_remaining == 0){ + break + } if(exact){ hasRecords <- FALSE }else{ #next page <- page+1 - nextreq <- sprintf("records/?q=%s&size=%s&page=%s", URLencode(q), size, page) - if(all_versions) nextreq <- paste0(nextreq, "&all_versions=1") + nextreq <- sprintf("records?q=%s&size=%s&page=%s", URLencode(q), size, page) + if(all_versions) nextreq <- paste0(nextreq, "&allversions=1") zenReq <- ZenodoRequest$new(private$url, "GET_WITH_CURL", nextreq, token = self$getToken(), logger = self$loggerType) zenReq$execute() - resp <- zenReq$getResponse() - hasRecords <- length(resp)>0 + if(zenReq$getStatus() == 200){ + resp <- zenReq$getResponse() + records <- resp$hits$hits + hasRecords <- length(records)>0 + }else{ + self$WARN(sprintf("Maximum allowed size for list of published records at page %s", page)) + break + } } } self$INFO("Successfully fetched list of published records!") diff --git a/R/ZenodoRecord.R b/R/ZenodoRecord.R index 3d048b9..12de1fd 100644 --- a/R/ZenodoRecord.R +++ b/R/ZenodoRecord.R @@ -36,6 +36,7 @@ ZenodoRecord <- R6Class("ZenodoRecord", ) }, fromList = function(obj){ + self$access = obj$access self$conceptdoi = obj$conceptdoi self$conceptrecid = obj$conceptrecid self$created = obj$created @@ -50,19 +51,24 @@ ZenodoRecord <- R6Class("ZenodoRecord", ) }) self$id = obj$id + self$recid = obj$recid self$links = obj$links self$metadata = obj$metadata self$modified = obj$modified - self$owner = obj$owner - self$record_id = obj$record_id + self$owners = obj$owners + self$status = obj$status self$state = obj$state self$submitted = obj$submitted - self$title = obj$title - self$version = obj$version + self$revision = obj$revision if(!is.null(obj$stats)) self$stats = data.frame(obj$stats) } ), public = list( + #' @field access access policies + access = list( + record = "public", + files = "public" + ), #' @field conceptdoi record Concept DOI (common to all record versions) conceptdoi = NULL, #' @field conceptrecid record concept id @@ -83,18 +89,18 @@ ZenodoRecord <- R6Class("ZenodoRecord", metadata = list(), #' @field modified record modification date modified = NULL, - #' @field owner record owner - owner = NULL, - #' @field record_id record_id - record_id = NULL, + #' @field owners record owners + owners = NULL, + #' @field recid recid + recid = NULL, + #' @field status record status + status = NULL, #' @field state record state state = NULL, #' @field submitted record submission status submitted = FALSE, - #' @field title record title - title = NULL, - #' @field version record version - version = NULL, + #' @field revision record revision + revision = NULL, #' @field stats stats stats = NULL, @@ -104,10 +110,35 @@ ZenodoRecord <- R6Class("ZenodoRecord", #' or "DEBUG" (for complete curl http calls logs) initialize = function(obj = NULL, logger = "INFO"){ super$initialize(logger = logger) - self$prereserveDOI(TRUE) if(!is.null(obj)) private$fromList(obj) }, + #Invenio RDM API new methods + #--------------------------------------------------------------------------- + #'@description Set the access policy for record, among values "public" (default) or "restricted" + #'@param access access policy ('public' or 'restricted') + setAccessPolicyRecord = function(access = c("public","resticted")){ + self$access$record = access + }, + + #'@description Set the access policy for files, among values "public" (default) or "restricted" + #'@param access access policy ('public' or 'restricted') + setAccessPolicyFiles = function(access = c("public","resticted")){ + self$access$files = access + }, + + #'@description Set access policy embargo options + #'@param active whether embargo is active or not. Default is \code{FALSE} + #'@param until embargo date, object of class \code{Date}. Default is \code{NULL}. Must be provided if embargo is active + #'@param reason embargo reason, object of class \code{character}. Default is an empty string + setAccessPolicyEmbargo = function(active = FALSE, until = NULL, reason = ""){ + if(!is.null(until)) if(!is(until, "Date")) stop("Argument 'until' should be of class 'Date'") + self$access$embargo = list(active = active, until = until, reason = reason) + }, + + #legacy REST API methods (to be evaluated under Zenodo Invenio RDM migration) + #---------------------------------------------------------------------------- + #' @description Set prereserve_doi if \code{TRUE}, \code{FALSE} otherwise to create a record without #' prereserved DOI by Zenodo. By default, this method will be called to prereserve a DOI assuming #' the record created doesn't yet handle a DOI. To avoid prereserving a DOI call \code{$prereserveDOI(FALSE)} @@ -971,18 +1002,18 @@ ZenodoRecord <- R6Class("ZenodoRecord", #' @return the writen file name (with extension) exportAs = function(format, filename, append_format = TRUE){ zenodo_url <- self$links$record_html - if(is.null(zenodo_url)) zenodo_url <- self$links$latest_html + if(is.null(zenodo_url)) zenodo_url <- self$links$self_html if(is.null(zenodo_url)){ stop("Ups, this record seems a draft, can't export metadata until it is published!") } metadata_export_url <- switch(format, - "BibTeX" = paste0(zenodo_url,"/export/hx"), + "BibTeX" = paste0(zenodo_url,"/export/bibtex"), "CSL" = paste0(zenodo_url,"/export/csl"), - "DataCite" = paste0(zenodo_url,"/export/dcite4"), - "DublinCore" = paste0(zenodo_url,"/export/xd"), - "DCAT" = paste0(zenodo_url,"/export/dcat"), + "DataCite" = paste0(zenodo_url,"/export/datacite-xml"), + "DublinCore" = paste0(zenodo_url,"/export/dublincore"), + "DCAT" = paste0(zenodo_url,"/export/dcat-ap"), "JSON" = paste0(zenodo_url,"/export/json"), - "JSON-LD" = paste0(zenodo_url,"/export/schemaorg_jsonld"), + "JSON-LD" = paste0(zenodo_url,"/export/json-ld"), "GeoJSON" = paste0(zenodo_url,"/export/geojson"), "MARCXML" = paste0(zenodo_url,"/export/xm"), NULL @@ -992,19 +1023,12 @@ ZenodoRecord <- R6Class("ZenodoRecord", } fileext <- private$getExportFormatExtension(format) - - html <- xml2::read_html(metadata_export_url) - reference <- xml2::xml_find_all(html, ".//pre") - reference <- reference[1] - reference <- gsub("","",reference) - reference <- gsub("","",reference) - if(fileext %in% c("xml", "rdf")){ - reference <- gsub("<", "<", reference) - reference <- gsub(">", ">", reference) - } - destfile <- paste(paste0(filename, ifelse(append_format,paste0("_", format),"")), fileext, sep = ".") - writeChar(reference, destfile, eos = NULL) + + req <- httr::GET( + url = metadata_export_url, + httr::write_disk(path = destfile, overwrite = TRUE) + ) return(destfile) }, diff --git a/R/ZenodoRequest.R b/R/ZenodoRequest.R index 5dc8b8b..9f4d384 100644 --- a/R/ZenodoRequest.R +++ b/R/ZenodoRequest.R @@ -47,8 +47,10 @@ ZenodoRequest <- R6Class("ZenodoRequest", data <- data[!sapply(data, is.null)] }else if(is(data, "list")){ meta <- data$metadata - if(!is.null(meta$prereserve_doi)) meta$prereserve_doi <- NULL - data <- list(metadata = meta) + if(!is.null(meta)){ + if(!is.null(meta$prereserve_doi)) meta$prereserve_doi <- NULL + data <- list(metadata = meta) + } } data <- as(toJSON(data, pretty=T, auto_unbox=T), "character") @@ -134,12 +136,12 @@ ZenodoRequest <- R6Class("ZenodoRequest", PUT = function(url, request, data, progress){ req <- paste(url, request, sep="/") - if(regexpr("api/files", req)<0) data <- private$prepareData(data) + if(regexpr("draft/files", req)<0) data <- private$prepareData(data) #headers headers <- c( "User-Agent" = private$agent, - "Content-Type" = if(regexpr("api/files", req)>0) "application/octet-stream" else "application/json", + "Content-Type" = if(regexpr("draft/files", req)>0) "application/octet-stream" else "application/json", "Authorization" = paste("Bearer",private$token) ) @@ -167,7 +169,8 @@ ZenodoRequest <- R6Class("ZenodoRequest", }, DELETE = function(url, request, data){ - req <- paste(url, request, data, sep="/") + req <- paste(url, request, sep="/") + if(!is.null(data)) req <- paste(req, data, sep = "/") #headers headers <- c( "User-Agent" = private$agent, diff --git a/R/zen4R.R b/R/zen4R.R index f7c39e3..f592f22 100644 --- a/R/zen4R.R +++ b/R/zen4R.R @@ -14,6 +14,7 @@ #' @importFrom tools md5sum #' @import atom4R #' @importFrom utf8 utf8_encode +#' @importFrom plyr rbind.fill #' #' @title Interface to 'Zenodo' REST API #' @description Provides an Interface to 'Zenodo' () REST API, diff --git a/man/ZenodoManager.Rd b/man/ZenodoManager.Rd index 6aa39ef..8f20ee5 100644 --- a/man/ZenodoManager.Rd +++ b/man/ZenodoManager.Rd @@ -92,13 +92,18 @@ Emmanuel Blondel \itemize{ \item \href{#method-ZenodoManager-new}{\code{ZenodoManager$new()}} \item \href{#method-ZenodoManager-getToken}{\code{ZenodoManager$getToken()}} +\item \href{#method-ZenodoManager-getLanguages}{\code{ZenodoManager$getLanguages()}} +\item \href{#method-ZenodoManager-getLanguageById}{\code{ZenodoManager$getLanguageById()}} \item \href{#method-ZenodoManager-getLicenses}{\code{ZenodoManager$getLicenses()}} \item \href{#method-ZenodoManager-getLicenseById}{\code{ZenodoManager$getLicenseById()}} \item \href{#method-ZenodoManager-getCommunities}{\code{ZenodoManager$getCommunities()}} \item \href{#method-ZenodoManager-getCommunityById}{\code{ZenodoManager$getCommunityById()}} \item \href{#method-ZenodoManager-getGrants}{\code{ZenodoManager$getGrants()}} +\item \href{#method-ZenodoManager-getAwards}{\code{ZenodoManager$getAwards()}} \item \href{#method-ZenodoManager-getGrantsByName}{\code{ZenodoManager$getGrantsByName()}} +\item \href{#method-ZenodoManager-getAwardsByName}{\code{ZenodoManager$getAwardsByName()}} \item \href{#method-ZenodoManager-getGrantById}{\code{ZenodoManager$getGrantById()}} +\item \href{#method-ZenodoManager-getAwardById}{\code{ZenodoManager$getAwardById()}} \item \href{#method-ZenodoManager-getFunders}{\code{ZenodoManager$getFunders()}} \item \href{#method-ZenodoManager-getFundersByName}{\code{ZenodoManager$getFundersByName()}} \item \href{#method-ZenodoManager-getFunderById}{\code{ZenodoManager$getFunderById()}} @@ -117,6 +122,9 @@ Emmanuel Blondel \item \href{#method-ZenodoManager-discardChanges}{\code{ZenodoManager$discardChanges()}} \item \href{#method-ZenodoManager-publishRecord}{\code{ZenodoManager$publishRecord()}} \item \href{#method-ZenodoManager-getFiles}{\code{ZenodoManager$getFiles()}} +\item \href{#method-ZenodoManager-getFile}{\code{ZenodoManager$getFile()}} +\item \href{#method-ZenodoManager-startFileUpload}{\code{ZenodoManager$startFileUpload()}} +\item \href{#method-ZenodoManager-completeFileUpload}{\code{ZenodoManager$completeFileUpload()}} \item \href{#method-ZenodoManager-uploadFile}{\code{ZenodoManager$uploadFile()}} \item \href{#method-ZenodoManager-deleteFile}{\code{ZenodoManager$deleteFile()}} \item \href{#method-ZenodoManager-getRecords}{\code{ZenodoManager$getRecords()}} @@ -187,6 +195,48 @@ the token, object of class \code{character} } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getLanguages}{}}} +\subsection{Method \code{getLanguages()}}{ +Get Languages supported by Zenodo. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getLanguages(pretty = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to +\code{TRUE} which will returns the list of languages as \code{data.frame}. +Set \code{pretty = FALSE} to get the raw list of languages} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +list of languages as \code{data.frame} or \code{list} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getLanguageById}{}}} +\subsection{Method \code{getLanguageById()}}{ +Get language by Id. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getLanguageById(id)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{id}}{license id} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +the license +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getLicenses}{}}} \subsection{Method \code{getLicenses()}}{ @@ -234,7 +284,7 @@ the license \subsection{Method \code{getCommunities()}}{ Get Communities supported by Zenodo. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$getCommunities(pretty = TRUE)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$getCommunities(pretty = TRUE, q = "", size = 500)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -243,6 +293,12 @@ Get Communities supported by Zenodo. \item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to \code{TRUE} which will returns the list of communities as \code{data.frame}. Set \code{pretty = FALSE} to get the raw list of communities} + +\item{\code{q}}{an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. +Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, +not all communities can be listed from Zenodo, a query has to be specified.} + +\item{\code{size}}{number of communities to be returned. By default equal to 500} } \if{html}{\out{}} } @@ -274,23 +330,23 @@ the community \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getGrants}{}}} \subsection{Method \code{getGrants()}}{ -Get Grants supported by Zenodo. +Get Grants supported by Zenodo. DEPRECATED: replaced by \code{getAwards} \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$getGrants(q = "", pretty = TRUE, size = 1000)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$getGrants(q = "", pretty = TRUE, size = 500)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{q}}{an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. -Note that the Zenodo API restrains a maximum number of 10,000 grants to be retrieved. Consequently, +Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, not all grants can be listed from Zenodo, a query has to be specified.} \item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to \code{TRUE} which will returns the list of grants as \code{data.frame}. Set \code{pretty = FALSE} to get the raw list of grants} -\item{\code{size}}{number of grants to be returned. By default equal to 1000.} +\item{\code{size}}{number of grants to be returned. By default equal to 500.} } \if{html}{\out{
}} } @@ -299,10 +355,38 @@ list of grants as \code{data.frame} or \code{list} } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getAwards}{}}} +\subsection{Method \code{getAwards()}}{ +Get Awards supported by Zenodo. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getAwards(q = "", pretty = TRUE, size = 500)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{q}}{an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. +Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, +not all awards can be listed from Zenodo, a query has to be specified.} + +\item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to +\code{TRUE} which will returns the list of awards as \code{data.frame}. +Set \code{pretty = FALSE} to get the raw list of awards} + +\item{\code{size}}{number of awards to be returned. By default equal to 500.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +list of awards as \code{data.frame} or \code{list} +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getGrantsByName}{}}} \subsection{Method \code{getGrantsByName()}}{ -Get grants by name. +Get grants by name. DEPRECATED: replaced by \code{getAwardByName} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{ZenodoManager$getGrantsByName(name, pretty = TRUE)}\if{html}{\out{
}} } @@ -323,10 +407,34 @@ list of grants as \code{data.frame} or \code{list} } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getAwardsByName}{}}} +\subsection{Method \code{getAwardsByName()}}{ +Get awards by name. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getAwardsByName(name, pretty = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{name}}{name} + +\item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to +\code{TRUE} which will returns the list of awards as \code{data.frame}. +Set \code{pretty = FALSE} to get the raw list of awards} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +list of awards as \code{data.frame} or \code{list} +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getGrantById}{}}} \subsection{Method \code{getGrantById()}}{ -Get grant by Id. +Get grant by Id.DEPRECATED: replaced by \code{getAwardById} \subsection{Usage}{ \if{html}{\out{
}}\preformatted{ZenodoManager$getGrantById(id)}\if{html}{\out{
}} } @@ -343,26 +451,46 @@ the grant } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getAwardById}{}}} +\subsection{Method \code{getAwardById()}}{ +Get award by Id. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getAwardById(id)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{id}}{award id} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +the award +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getFunders}{}}} \subsection{Method \code{getFunders()}}{ Get Funders supported by Zenodo based on a query. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$getFunders(q = "", pretty = TRUE, size = 1000)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$getFunders(q = "", pretty = TRUE, size = 500)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{q}}{an ElasticSearch compliant query, object of class \code{character}. Default is emtpy. -Note that the Zenodo API restrains a maximum number of 10,000 funders to be retrieved. Consequently, +Note that the Zenodo API restrains a maximum number of 10,000 records to be retrieved. Consequently, not all funders can be listed from Zenodo, a query has to be specified.} \item{\code{pretty}}{Prettify the output. By default the argument \code{pretty} is set to \code{TRUE} which will returns the list of funders as \code{data.frame}. Set \code{pretty = FALSE} to get the raw list of funders} -\item{\code{size}}{number of funders to be returned. By default equal to 1000.} +\item{\code{size}}{number of funders to be returned. By default equal to 500} } \if{html}{\out{
}} } @@ -418,14 +546,13 @@ the funder \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-getDepositions}{}}} \subsection{Method \code{getDepositions()}}{ -Get the list of Zenodo records deposited in your Zenodo workspace. By defaut - the list of depositions will be returned by page with a size of 10 results per - page (default size of the Zenodo API). The parameter \code{q} allows to specify - an ElasticSearch-compliant query to filter depositions (default query is empty - to retrieve all records). The argument \code{all_versions}, if set to TRUE allows - to get all versions of records as part of the depositions list. The argument \code{exact} - specifies that an exact matching is wished, in which case paginated search will be - disabled (only the first search page will be returned). +Get the list of Zenodo records deposited in your Zenodo workspace (user records). By default + the list of depositions will be returned by page with a size of 10 results per page (default size of + the Zenodo API). The parameter \code{q} allows to specify an ElasticSearch-compliant query to filter + depositions (default query is empty to retrieve all records). The argument \code{all_versions}, if set + to TRUE allows to get all versions of records as part of the depositions list. The argument \code{exact} + specifies that an exact matching is wished, in which case paginated search will be disabled (only the first + search page will be returned). Examples of ElasticSearch queries for Zenodo can be found at \href{https://help.zenodo.org/guides/search/}{https://help.zenodo.org/guides/search/}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{ZenodoManager$getDepositions( @@ -752,12 +879,79 @@ list of files } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-getFile}{}}} +\subsection{Method \code{getFile()}}{ +Get a file record metadata. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$getFile(recordId, filename)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{recordId}}{the ID of the record.} + +\item{\code{filename}}{filename} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +the file metadata +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-startFileUpload}{}}} +\subsection{Method \code{startFileUpload()}}{ +Start a file upload. The method will create a key for the file to be uploaded +This method is essentially for internal purpose, and is called directly in \code{uploadFile} +for user convenience and for backward compatibility with the legacy Zenodo API. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$startFileUpload(path, recordId)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{path}}{Local path of the file} + +\item{\code{recordId}}{ID of the record} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoManager-completeFileUpload}{}}} +\subsection{Method \code{completeFileUpload()}}{ +Completes a file upload. The method will complete a file upload through a commit operation +This method is essentially for internal purpose, and is called directly in \code{uploadFile} +for user convenience and for backward compatibility with the legacy Zenodo API. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoManager$completeFileUpload(path, recordId)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{path}}{Local path of the file} + +\item{\code{recordId}}{ID of the record} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-uploadFile}{}}} \subsection{Method \code{uploadFile()}}{ -Uploads a file to a Zenodo record +Uploads a file to a Zenodo record. With the new Zenodo Invenio RDM API, this method +internally calls \code{startFileUpload} to create a file record (with a filename key) at start, followed +by the actual file content upload. At this stage, the file upload is in "pending" status. At the end, +the function calls \code{completeFileUpload} to commit the file which status becomes "completed". \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$uploadFile(path, record = NULL, recordId = NULL)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$uploadFile(path, recordId = NULL, record = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -765,9 +959,9 @@ Uploads a file to a Zenodo record \describe{ \item{\code{path}}{Local path of the file} -\item{\code{record}}{object of class \code{ZenodoRecord}} - \item{\code{recordId}}{ID of the record. Deprecated, use \code{record} instead to take advantage of the new Zenodo bucket upload API.} + +\item{\code{record}}{object of class \code{ZenodoRecord}} } \if{html}{\out{
}} } @@ -776,9 +970,12 @@ Uploads a file to a Zenodo record \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoManager-deleteFile}{}}} \subsection{Method \code{deleteFile()}}{ -Deletes a file for a record +Deletes a file for a record. With the new Zenodo Invenio RDM API, if a file is +deleted although its status was pending, only the upload content is deleted, and the file upload +record (identified by a filename key) is kept. If the status was completed (with a file commit), +the file record is deleted. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$deleteFile(recordId, fileId)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$deleteFile(recordId, filename)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -786,7 +983,7 @@ Deletes a file for a record \describe{ \item{\code{recordId}}{ID of the record} -\item{\code{fileId}}{ID of the file to delete} +\item{\code{filename}}{name of the file to be deleted} } \if{html}{\out{}} } @@ -804,12 +1001,7 @@ Get the list of Zenodo records. By defaut the list of records will be returned b paginated search will be disabled (only the first search page will be returned). Examples of ElasticSearch queries for Zenodo can be found at \href{https://help.zenodo.org/guides/search/}{https://help.zenodo.org/guides/search/}. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{ZenodoManager$getRecords( - q = "", - size = 10, - all_versions = FALSE, - exact = FALSE -)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{ZenodoManager$getRecords(q = "", size = 10, all_versions = FALSE, exact = TRUE)}\if{html}{\out{
}} } \subsection{Arguments}{ diff --git a/man/ZenodoRecord.Rd b/man/ZenodoRecord.Rd index 93dedb3..b33d981 100644 --- a/man/ZenodoRecord.Rd +++ b/man/ZenodoRecord.Rd @@ -29,6 +29,8 @@ Emmanuel Blondel \section{Public fields}{ \if{html}{\out{
}} \describe{ +\item{\code{access}}{access policies} + \item{\code{conceptdoi}}{record Concept DOI (common to all record versions)} \item{\code{conceptrecid}}{record concept id} @@ -49,17 +51,17 @@ Emmanuel Blondel \item{\code{modified}}{record modification date} -\item{\code{owner}}{record owner} +\item{\code{owners}}{record owners} + +\item{\code{recid}}{recid} -\item{\code{record_id}}{record_id} +\item{\code{status}}{record status} \item{\code{state}}{record state} \item{\code{submitted}}{record submission status} -\item{\code{title}}{record title} - -\item{\code{version}}{record version} +\item{\code{revision}}{record revision} \item{\code{stats}}{stats} } @@ -69,6 +71,9 @@ Emmanuel Blondel \subsection{Public methods}{ \itemize{ \item \href{#method-ZenodoRecord-new}{\code{ZenodoRecord$new()}} +\item \href{#method-ZenodoRecord-setAccessPolicyRecord}{\code{ZenodoRecord$setAccessPolicyRecord()}} +\item \href{#method-ZenodoRecord-setAccessPolicyFiles}{\code{ZenodoRecord$setAccessPolicyFiles()}} +\item \href{#method-ZenodoRecord-setAccessPolicyEmbargo}{\code{ZenodoRecord$setAccessPolicyEmbargo()}} \item \href{#method-ZenodoRecord-prereserveDOI}{\code{ZenodoRecord$prereserveDOI()}} \item \href{#method-ZenodoRecord-setDOI}{\code{ZenodoRecord$setDOI()}} \item \href{#method-ZenodoRecord-getConceptDOI}{\code{ZenodoRecord$getConceptDOI()}} @@ -195,6 +200,61 @@ or "DEBUG" (for complete curl http calls logs)} } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoRecord-setAccessPolicyRecord}{}}} +\subsection{Method \code{setAccessPolicyRecord()}}{ +Set the access policy for record, among values "public" (default) or "restricted" +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoRecord$setAccessPolicyRecord(access = c("public", "resticted"))}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{access}}{access policy ('public' or 'restricted')} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoRecord-setAccessPolicyFiles}{}}} +\subsection{Method \code{setAccessPolicyFiles()}}{ +Set the access policy for files, among values "public" (default) or "restricted" +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoRecord$setAccessPolicyFiles(access = c("public", "resticted"))}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{access}}{access policy ('public' or 'restricted')} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-ZenodoRecord-setAccessPolicyEmbargo}{}}} +\subsection{Method \code{setAccessPolicyEmbargo()}}{ +Set access policy embargo options +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{ZenodoRecord$setAccessPolicyEmbargo(active = FALSE, until = NULL, reason = "")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{active}}{whether embargo is active or not. Default is \code{FALSE}} + +\item{\code{until}}{embargo date, object of class \code{Date}. Default is \code{NULL}. Must be provided if embargo is active} + +\item{\code{reason}}{embargo reason, object of class \code{character}. Default is an empty string} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-ZenodoRecord-prereserveDOI}{}}} \subsection{Method \code{prereserveDOI()}}{ diff --git a/tests/testthat/test_awards.R b/tests/testthat/test_awards.R new file mode 100644 index 0000000..3229108 --- /dev/null +++ b/tests/testthat/test_awards.R @@ -0,0 +1,24 @@ +# test_awards.R +# Author: Emmanuel Blondel +# +# Description: Unit tests for Zenodo awards operations +#======================= +require(zen4R, quietly = TRUE) +require(testthat) + +context("awards") + +test_that("awards are retrieved",{ + zenodo <- ZenodoManager$new(logger = "INFO") + zen_awards_df <- zenodo$getAwards() + expect_is(zen_awards_df, "data.frame") + zen_awards_raw <- zenodo$getAwards(pretty = FALSE) + expect_is(zen_awards_raw, "list") +}) + +test_that("grant is retrieved by id",{ + zenodo <- ZenodoManager$new(logger = "INFO") + zen_award <- zenodo$getAwardById("001aqnf71::2444954") + expect_equal(zen_award$id, "001aqnf71::2444954") + Sys.sleep(2) +}) \ No newline at end of file diff --git a/tests/testthat/test_grants.R b/tests/testthat/test_grants.R deleted file mode 100644 index d6b9ce8..0000000 --- a/tests/testthat/test_grants.R +++ /dev/null @@ -1,25 +0,0 @@ -# test_grants.R -# Author: Emmanuel Blondel -# -# Description: Unit tests for Zenodo grants operations -#======================= -require(zen4R, quietly = TRUE) -require(testthat) - -context("grants") - -#as of 2020-04-14 this method doesn't respond, to liaise with Zenodo team if it persists -test_that("grants are retrieved",{ - #zenodo <- ZenodoManager$new(logger = "INFO") - #zen_grants_df <- zenodo$getGrants() - #expect_is(zen_grants_df, "data.frame") - #zen_grants_raw <- zenodo$getGrants(pretty = FALSE) - #expect_is(zen_grants_raw, "list") -}) - -test_that("grant is retrieved by id",{ - zenodo <- ZenodoManager$new(logger = "INFO") - zen_grant <- zenodo$getGrantById("10.13039/100000002::5R01DK049482-03") - expect_equal(zen_grant$metadata$internal_id, "10.13039/100000002::5R01DK049482-03") - Sys.sleep(2) -}) \ No newline at end of file diff --git a/tests/testthat/test_languages.R b/tests/testthat/test_languages.R new file mode 100644 index 0000000..5417c03 --- /dev/null +++ b/tests/testthat/test_languages.R @@ -0,0 +1,27 @@ +# test_languages.R +# Author: Emmanuel Blondel +# +# Description: Unit tests for Zenodo supported languages +#======================= +require(zen4R, quietly = TRUE) +require(testthat) + +context("languages") + +test_that("languages are retrieved",{ + zenodo <- ZenodoManager$new(logger = "INFO") + zen_languages_df <- zenodo$getLanguages() + Sys.sleep(2) + expect_is(zen_languages_df, "data.frame") + zen_languages_raw <- zenodo$getLanguages(pretty = FALSE) + expect_is(zen_languages_raw, "list") + Sys.sleep(2) +}) + +test_that("language is retrieved by id",{ + zenodo <- ZenodoManager$new(logger = "INFO") + zen_license <- zenodo$getLanguageById("eng") + expect_equal(zen_license$id, "eng") + expect_equal(zen_license$title[[1]], "English") + Sys.sleep(2) +}) \ No newline at end of file diff --git a/tests/testthat/test_licenses.R b/tests/testthat/test_licenses.R index 31888de..162b31c 100644 --- a/tests/testthat/test_licenses.R +++ b/tests/testthat/test_licenses.R @@ -21,7 +21,7 @@ test_that("licenses are retrieved",{ test_that("license is retrieved by id",{ zenodo <- ZenodoManager$new(logger = "INFO") zen_license <- zenodo$getLicenseById("mit") - expect_equal(zen_license$metadata$id, "MIT") - expect_equal(zen_license$metadata$title, "MIT License") + expect_equal(zen_license$id, "mit") + expect_equal(zen_license$title[[1]], "MIT License") Sys.sleep(2) }) \ No newline at end of file