diff --git a/.Rbuildignore b/.Rbuildignore index e388bbb..6eafe18 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -5,12 +5,11 @@ ^_pkgdown\.yml$ ^docs$ ^pkgdown$ -^english-ewt-ud-2.5-191206.udpipe$ ^jhudsl_chapter_info.tsv$ ^jhudsl-repos.json$ ^docker$ ^.local$ ^.rstudio$ -^english-ewt-ud-2.5-191206.udpipe$ +^.*udpipe$ ^\.secrets$ ^/home/rstudio/cow/\.secrets$ diff --git a/DESCRIPTION b/DESCRIPTION index b16a478..3f2a8f1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,13 +1,13 @@ Package: cow -Title: Helps Manage Git Course Repos +Title: Course Organizer and Wrangler Version: 0.0.0.9000 Authors@R: person(given = "Candace", family = "Savonen", role = c("aut", "cre"), email = "cansav09@gmail.com") -Description: Accesses GitHub API from R and performs some course - management functions, including retrieving chapter names, learning objectives, +Description: Performs some course management functions through interactions with + GitHub API, including retrieving chapter names, learning objectives, and keywords for courses hosted on GitHub repositories. License: GPL-3 Imports: @@ -24,6 +24,8 @@ Imports: textrank, udpipe, xml2, + knitr, + igraph, Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) diff --git a/NAMESPACE b/NAMESPACE index 4c01306..5c90e63 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(borrow_chapter) export(check_git_repo) export(extract_entries) export(get_chapters) @@ -16,8 +17,10 @@ importFrom(gitcreds,gitcreds_get) importFrom(httr,GET) importFrom(httr,accept_json) importFrom(httr,authenticate) +importFrom(knitr,knit_child) importFrom(magrittr,"%>%") importFrom(textrank,textrank_keywords) importFrom(udpipe,udpipe_annotate) importFrom(udpipe,udpipe_download_model) importFrom(udpipe,udpipe_load_model) +importFrom(utils,download.file) diff --git a/R/auth_arg.R b/R/auth_arg.R index f3486d8..edcd61c 100644 --- a/R/auth_arg.R +++ b/R/auth_arg.R @@ -6,42 +6,35 @@ #' access token needs to be supplied. If none is supplied, then this will attempt to #' grab from a git pat set in the environment with usethis::create_github_token(). #' @param git_username Optional, can include username for credentials. -#' +#' @param quiet Use TRUE if you don't want the warning about no GitHub credentials. +#' #' @return Authorization argument to supply to curl OR a blank string if no #' authorization is found or supplied. #' #' @export #' -get_git_auth <- function(git_pat = NULL, git_username = "PersonalAccessToken") { - - auth_arg <- NULL +get_git_auth <- function(git_pat = NULL, git_username = "PersonalAccessToken", quiet = FALSE) { - # If either username or git pat is not provided, try to get credentials with gitcreds - if (is.null(git_pat) || is.null(git_username)) { + auth_arg <- NULL + + # If git pat is not provided, try to get credentials with gitcreds + if (is.null(git_pat)) { # Try getting credentials - creds <- try(gitcreds::gitcreds_get(), silent = TRUE) + auth_arg <- try(gitcreds::gitcreds_get(), silent = TRUE) - if (grepl("Could not find any credentials", creds[1])) { - message("Could not find git credentials, please set by running usethis::create_github_token(), - or directly providing a personal access token here.") + if (grepl("Could not find any credentials", auth_arg[1])) { + # Only if we're running this interactively if (interactive()) { # Set credentials if null auth_arg <- gitcreds::gitcreds_set() + } else { + message("Could not find git credentials, please set by running usethis::create_github_token(), + or directly providing a personal access token using the git_pat argument") } } - - git_username <- auth_arg$username - git_pat <- auth_arg$password - - if (is.null(git_pat)) { - warning("No github credentials found or provided. - Only public repositories will be retrieved. Set GitHub token using - usethis::create_github_token() if you would like private repos to be included.") - } - } - if (!is.null(git_pat)) { + } else { # If git_pat is given, use it. # Set to Renviron file temporarily Sys.setenv(GITHUB_PAT = git_pat) @@ -56,5 +49,15 @@ get_git_auth <- function(git_pat = NULL, git_username = "PersonalAccessToken") { auth_arg$host <- "github.com" auth_arg$username <- git_username } + + # Check if we have authentication + git_pat <- try(auth_arg$password, silent = TRUE) + + if (grepl("Error", git_pat[1])) { + if (!quiet) { + message("No github credentials found or provided; only public repositories will be successful.") + } + } + return(auth_arg) } diff --git a/R/borrow_chapter.R b/R/borrow_chapter.R new file mode 100644 index 0000000..9ee2213 --- /dev/null +++ b/R/borrow_chapter.R @@ -0,0 +1,82 @@ +#' Borrow/link a chapter from another bookdown course +#' +#' @param doc_path A file path of markdown or R Markdown +#' document of the chapter in the repository you are retrieving it from that +#' you would like to include in the current document. e.g "docs/intro.md" or "intro.md" +#' @param repo_name A character vector indicating the repo name of where you are +#' borrowing from. e.g. "jhudsl/DaSL_Course_Template_Bookdown/". +#' For a Wiki of a repo, use "wiki/jhudsl/DaSL_Course_Template_Bookdown/" +#' If nothing is provided, will look for local file. +#' @param branch Default is to pull from main branch, but need to declare if other branch is needed. +#' @param git_pat A personal access token from GitHub. Only necessary if the +#' repository being checked is a private repository. +#' @param base_url it's assumed this is coming from github so it is by default 'https://raw.githubusercontent.com/' +#' @param dest_dir A file path where the file should be stored upon arrival to +#' the current repository. +#' +#' @return An Rmarkdown or markdown is knitted into the document from another repository +#' +#' @importFrom knitr knit_child +#' @importFrom utils download.file +#' @export +#' +#' @examples \dontrun{ +#' +#' # In an Rmarkdown document: +#' +#' # For a file in another repository: +#' # ```{r, echo=FALSE, results='asis'} +#' borrow_chapter( +#' doc_path = "docs/02-chapter_of_course.md", +#' repo_name = "jhudsl/DaSL_Course_Template_Bookdown" +#' ) +#' # ``` +#' +#' # For a local file: +#' # ```{r, echo=FALSE, results='asis'} +#' borrow_chapter(doc_path = "02-chapter_of_course.Rmd") +#' # ``` +#' } +borrow_chapter <- function(doc_path, + repo_name = NULL, + branch = "main", + git_pat = NULL, + base_url = "https://raw.githubusercontent.com", + dest_dir = file.path("resources", "other_chapters")) { + + + # Declare file names + doc_path <- file.path(doc_path) + doc_name <- basename(doc_path) + + if (is.null(repo_name)) { + exists <- check_git_repo( + repo_name = repo_name, + git_pat = git_pat, + verbose = FALSE, + silent = TRUE + ) + + # Create folder if it doesn't exist + if (!dir.exists(dest_dir)) { + dir.create(dest_dir, recursive = TRUE) + } + + dest_file <- file.path(dest_dir, doc_name) + + full_url <- file.path(base_url, repo_name, branch, doc_path) + + # Progress message + message(full_url) + + # Download it + download.file(full_url, destfile = dest_file) + } else { + # If the file is local we don't need to download anything + dest_file <- doc_path + } + + # Knit it in + result <- knitr::knit_child(dest_file, quiet = TRUE) + cat(result, sep = "\n") +} diff --git a/R/check_git_repo.R b/R/check_git_repo.R index b805e00..979995c 100644 --- a/R/check_git_repo.R +++ b/R/check_git_repo.R @@ -24,24 +24,19 @@ check_git_repo <- function(repo_name, silent = TRUE, return_repo = FALSE, verbose = TRUE) { - if (verbose) { message(paste("Checking for remote git repository:", repo_name)) } # If silent = TRUE don't print out the warning message from the 'try' report <- ifelse(silent, suppressWarnings, message) - if (is.null(git_pat)) { - git_pat <- gitcreds::gitcreds_get()$password - if (is.null(git_pat)) { - warning("No github credentials found or provided. - Only public repositories will be retrieved. Set GitHub token using - usethis::create_github_token() - if you would like private repos to be included.") - } - } + # Try to get credentials + auth_arg <- get_git_auth(git_pat = git_pat, quiet = !verbose) - if (!is.null(git_pat)) { + git_pat <- try(auth_arg$password, silent = TRUE) + + # Run git ls-remote + if (!grepl("Error", git_pat[1])) { # If git_pat is supplied, use it test_repo <- report( try(system(paste0("git ls-remote https://", git_pat, "@github.com/", repo_name), @@ -49,13 +44,11 @@ check_git_repo <- function(repo_name, )) ) } else { - # Try to git ls-remote the repo_name given - test_repo <- report( - try(system(paste0("git ls-remote https://github.com/", repo_name), - intern = TRUE, ignore.stderr = TRUE - )) - ) + test_repo <- report + try(system(paste0("git ls-remote https://github.com/", repo_name), + intern = TRUE, ignore.stderr = TRUE + )) } # If 128 is returned as a status attribute it means it failed exists <- ifelse(is.null(attr(test_repo, "status")), TRUE, FALSE) diff --git a/R/get_chapters.R b/R/get_chapters.R index d5bdf1e..c71b4ca 100644 --- a/R/get_chapters.R +++ b/R/get_chapters.R @@ -28,9 +28,13 @@ utils::globalVariables(c( #' #' @export #' -#' @examples +#' @examples \dontrun{ #' +#' usethis::create_github_token() +#' #' get_chapters("jhudsl/Documentation_and_Usability") +#' +#' } get_chapters <- function(repo_name, git_pat = NULL, retrieve_learning_obj = FALSE, diff --git a/R/get_learning_obj.R b/R/get_learning_obj.R index 376beeb..454650a 100644 --- a/R/get_learning_obj.R +++ b/R/get_learning_obj.R @@ -17,12 +17,13 @@ #' #' @export #' -#' @examples +#' @examples \dontrun{ #' #' # Declare chapter URL #' url <- "https://jhudatascience.org/Documentation_and_Usability/other-helpful-features.html" #' #' get_learning_obj(url) +#' } get_learning_obj <- function(url, prompt = "This chapter will demonstrate how to\\:") { # Try chapter url diff --git a/R/get_pages_url.R b/R/get_pages_url.R index 0cefeee..061c42c 100644 --- a/R/get_pages_url.R +++ b/R/get_pages_url.R @@ -19,24 +19,34 @@ #' #' @export #' -#' @examples +#' @examples \dontrun{ #' -#' pages_url <- get_pages_url("jhudsl/DaSL_Course_Template_Bookdown") +#' usethis::create_github_token() +#' +#' get_chapters("jhudsl/Documentation_and_Usability") +#' +#' } get_pages_url <- function(repo_name, git_pat = NULL, verbose = FALSE, keep_json = FALSE) { page_url <- NA - # Build auth argument - auth_arg <- get_git_auth(git_pat = git_pat) + # Try to get credentials other way + auth_arg <- get_git_auth(git_pat = git_pat, quiet = !verbose) + + git_pat <- try(auth_arg$password, silent = TRUE) + + if (grepl("Error", git_pat[1])) { + warning("Cannot retrieve page info without GitHub credentials. Passing an NA.") + } # We can only retrieve pages if we have the credentials - if (!is.null(auth_arg$password)) { + if (!grepl("Error", git_pat[1])) { exists <- check_git_repo( repo_name = repo_name, git_pat = git_pat, - verbose = verbose + verbose = FALSE ) if (exists) { @@ -67,8 +77,6 @@ get_pages_url <- function(repo_name, page_url <- page_info$html_url } } - } else { - warning("Cannot retrieve page info without GitHub credentials. Passing an NA.") } return(page_url) } diff --git a/R/get_release_info.R b/R/get_release_info.R index 0cd446c..49ba166 100644 --- a/R/get_release_info.R +++ b/R/get_release_info.R @@ -10,7 +10,6 @@ #' grab from a git pat set in the environment with usethis::create_github_token(). #' Authorization handled by \link[cow]{get_git_auth} #' @param verbose TRUE/FALSE do you want more progress messages? -#' @param keep_json verbose TRUE/FALSE keep the json file locally? #' #' @return a data frame with the repository's release information: tag_name and tag_date. #' NAs are returned in these columns if there are no releases. @@ -20,42 +19,47 @@ #' #' @export #' -#' @examples +#' @examples \dontrun{ #' #' release_info <- get_release_info("jhudsl/DaSL_Course_Template_Bookdown") +#' } get_release_info <- function(repo_name, git_pat = NULL, - verbose = TRUE, - keep_json = FALSE) { + verbose = TRUE) { releases <- NA - # Build auth argument - auth_arg <- get_git_auth(git_pat = git_pat) - - exists <- check_git_repo( + # Get repo info + repo_info <- get_repo_info( repo_name = repo_name, - git_pat = git_pat, - verbose = verbose + git_pat = git_pat ) - if (exists) { - # Get repo info - repo_info <- get_repo_info( - repo_name = repo_name, - git_pat = git_pat - ) - - # Declare URL - url <- gsub("{/id}", "", repo_info$releases_url, - fixed = TRUE - ) - - # Github api get - response <- httr::GET( - url, - httr::add_headers(Authorization = paste0("token ", auth_arg$password)), - httr::accept_json() - ) + # Declare URL + url <- gsub("{/id}", "", repo_info$releases_url, + fixed = TRUE + ) + + # Try to get credentials other way + auth_arg <- get_git_auth(git_pat = git_pat, quiet = TRUE) + + git_pat <- try(auth_arg$password, silent = TRUE) + + if (grepl("Error", git_pat[1])) { + + # Github api get without authorization + response <- httr::GET( + url, + httr::accept_json() + ) + + } else { + # Github api get + response <- httr::GET( + url, + httr::add_headers(Authorization = paste0("token ", git_pat)), + httr::accept_json() + ) + } if (httr::http_error(response)) { warning(paste0("url: ", url, " failed")) @@ -81,8 +85,5 @@ get_release_info <- function(repo_name, ) %>% dplyr::mutate(tag_date = as.Date(tag_date)) } - } else { - warning(paste0(repo_name, " could not be found with the given credentials.")) - } return(releases) } diff --git a/R/get_repo_info.R b/R/get_repo_info.R index 0b589e1..2468a8b 100644 --- a/R/get_repo_info.R +++ b/R/get_repo_info.R @@ -10,7 +10,6 @@ #' grab from a git pat set in the environment with usethis::create_github_token(). #' Authorization handled by \link[cow]{get_git_auth} #' @param verbose TRUE/FALSE do you want more progress messages? -#' @param keep_json verbose TRUE/FALSE keep the json file locally? #' #' @return a data frame with the repository with the following columns: #' data_level, data_path, chapt_name, url, repository name @@ -28,13 +27,9 @@ #' repo_info <- get_repo_info("jhudsl/Documentation_and_Usability") get_repo_info <- function(repo_name, git_pat = NULL, - verbose = FALSE, - keep_json = FALSE) { + verbose = FALSE) { repo_info <- NA - # Build auth argument - auth_arg <- get_git_auth(git_pat = git_pat) - exists <- check_git_repo( repo_name = repo_name, git_pat = git_pat, @@ -46,7 +41,12 @@ get_repo_info <- function(repo_name, # Declare URL url <- paste0("https://api.github.com/repos/", repo_name) - if (is.null(auth_arg$password)){ + # Try to get credentials other way + auth_arg <- get_git_auth(git_pat = git_pat) + + git_pat <- try(auth_arg$password, silent = TRUE) + + if (grepl("Error", git_pat[1])) { # Github api get without authorization response <- httr::GET( url, @@ -56,7 +56,7 @@ get_repo_info <- function(repo_name, # Github api get response <- httr::GET( url, - httr::add_headers(Authorization = paste0("token ", auth_arg$password)), + httr::add_headers(Authorization = paste0("token ", git_pat)), httr::accept_json() ) } diff --git a/R/retrieve_org_chapters.R b/R/retrieve_org_chapters.R index 7c18d49..814a8ae 100644 --- a/R/retrieve_org_chapters.R +++ b/R/retrieve_org_chapters.R @@ -19,8 +19,7 @@ #' #' @export #' -#' @examples -#' \dontrun{ +#' @examples \dontrun{ #' retrieve_org_chapters( #' org_name = "jhudsl", #' output_file = "jhudsl_chapter_info.tsv" diff --git a/R/retrieve_org_repos.R b/R/retrieve_org_repos.R index 7c49e81..01c9e54 100644 --- a/R/retrieve_org_repos.R +++ b/R/retrieve_org_repos.R @@ -23,17 +23,22 @@ #' org_name = "jhudsl", #' output_file = "jhudsl_repos.tsv" #' ) +#' retrieve_org_repos <- function(org_name = NULL, output_file = "org_repos.tsv", git_pat = NULL, verbose = TRUE) { - # Build auth argument - auth_arg <- get_git_auth(git_pat = git_pat) + + # Try to get credentials other way + auth_arg <- get_git_auth(git_pat = git_pat, quiet = TRUE) + + git_pat <- try(auth_arg$password, silent = TRUE) # Declare URL url <- paste0("https://api.github.com/orgs/", org_name, "/repos?per_page=1000000") - if (is.null(auth_arg$password)){ + if (grepl("Error", git_pat[1])) { + # Github api get without authorization response <- httr::GET( url, @@ -43,7 +48,7 @@ retrieve_org_repos <- function(org_name = NULL, # Github api get response <- httr::GET( url, - httr::add_headers(Authorization = paste0("token ", auth_arg$password)), + httr::add_headers(Authorization = paste0("token ", git_pat)), httr::accept_json() ) } diff --git a/man/borrow_chapter.Rd b/man/borrow_chapter.Rd new file mode 100644 index 0000000..eef15de --- /dev/null +++ b/man/borrow_chapter.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/borrow_chapter.R +\name{borrow_chapter} +\alias{borrow_chapter} +\title{Borrow/link a chapter from another bookdown course} +\usage{ +borrow_chapter( + doc_path, + repo_name = NULL, + branch = "main", + git_pat = NULL, + base_url = "https://raw.githubusercontent.com", + dest_dir = file.path("resources", "other_chapters") +) +} +\arguments{ +\item{doc_path}{A file path of markdown or R Markdown +document of the chapter in the repository you are retrieving it from that +you would like to include in the current document. e.g "docs/intro.md" or "intro.md"} + +\item{repo_name}{A character vector indicating the repo name of where you are +borrowing from. e.g. "jhudsl/DaSL_Course_Template_Bookdown/". +For a Wiki of a repo, use "wiki/jhudsl/DaSL_Course_Template_Bookdown/" +If nothing is provided, will look for local file.} + +\item{branch}{Default is to pull from main branch, but need to declare if other branch is needed.} + +\item{git_pat}{A personal access token from GitHub. Only necessary if the +repository being checked is a private repository.} + +\item{base_url}{it's assumed this is coming from github so it is by default 'https://raw.githubusercontent.com/'} + +\item{dest_dir}{A file path where the file should be stored upon arrival to +the current repository.} +} +\value{ +An Rmarkdown or markdown is knitted into the document from another repository +} +\description{ +Borrow/link a chapter from another bookdown course +} +\examples{ +\dontrun{ + +# In an Rmarkdown document: + +# For a file in another repository: +# ```{r, echo=FALSE, results='asis'} +borrow_chapter( + doc_path = "docs/02-chapter_of_course.md", + repo_name = "jhudsl/DaSL_Course_Template_Bookdown" +) +# ``` + +# For a local file: +# ```{r, echo=FALSE, results='asis'} +borrow_chapter(doc_path = "02-chapter_of_course.Rmd") +# ``` +} +} diff --git a/man/get_chapters.Rd b/man/get_chapters.Rd index b66fd7a..01e053c 100644 --- a/man/get_chapters.Rd +++ b/man/get_chapters.Rd @@ -41,6 +41,11 @@ information for the Github page if it exists. Currently only public repositories are supported. } \examples{ +\dontrun{ + +usethis::create_github_token() get_chapters("jhudsl/Documentation_and_Usability") + +} } diff --git a/man/get_git_auth.Rd b/man/get_git_auth.Rd index b814ff8..e2ee5f8 100644 --- a/man/get_git_auth.Rd +++ b/man/get_git_auth.Rd @@ -4,7 +4,11 @@ \alias{get_git_auth} \title{Handle GitHub PAT authorization} \usage{ -get_git_auth(git_pat = NULL, git_username = "PersonalAccessToken") +get_git_auth( + git_pat = NULL, + git_username = "PersonalAccessToken", + quiet = FALSE +) } \arguments{ \item{git_pat}{If private repositories are to be retrieved, a github personal @@ -12,6 +16,8 @@ access token needs to be supplied. If none is supplied, then this will attempt t grab from a git pat set in the environment with usethis::create_github_token().} \item{git_username}{Optional, can include username for credentials.} + +\item{quiet}{Use TRUE if you don't want the warning about no GitHub credentials.} } \value{ Authorization argument to supply to curl OR a blank string if no diff --git a/man/get_learning_obj.Rd b/man/get_learning_obj.Rd index d2b81c8..f5f6ae3 100644 --- a/man/get_learning_obj.Rd +++ b/man/get_learning_obj.Rd @@ -21,9 +21,11 @@ information for the Github page if it exists. Currently only public repositories are supported. } \examples{ +\dontrun{ # Declare chapter URL url <- "https://jhudatascience.org/Documentation_and_Usability/other-helpful-features.html" get_learning_obj(url) } +} diff --git a/man/get_pages_url.Rd b/man/get_pages_url.Rd index f95d567..92aef11 100644 --- a/man/get_pages_url.Rd +++ b/man/get_pages_url.Rd @@ -27,6 +27,11 @@ data_level, data_path, chapt_name, url, repository name Given an repository on GitHub, retrieve the pages URL for it. } \examples{ +\dontrun{ -pages_url <- get_pages_url("jhudsl/DaSL_Course_Template_Bookdown") +usethis::create_github_token() + +get_chapters("jhudsl/Documentation_and_Usability") + +} } diff --git a/man/get_release_info.Rd b/man/get_release_info.Rd index 5e79767..29b9447 100644 --- a/man/get_release_info.Rd +++ b/man/get_release_info.Rd @@ -4,7 +4,7 @@ \alias{get_release_info} \title{Retrieve information about a github repo} \usage{ -get_release_info(repo_name, git_pat = NULL, verbose = TRUE, keep_json = FALSE) +get_release_info(repo_name, git_pat = NULL, verbose = TRUE) } \arguments{ \item{repo_name}{The full name of the repo to get bookdown chapters from. @@ -16,8 +16,6 @@ grab from a git pat set in the environment with usethis::create_github_token(). Authorization handled by \link[cow]{get_git_auth}} \item{verbose}{TRUE/FALSE do you want more progress messages?} - -\item{keep_json}{verbose TRUE/FALSE keep the json file locally?} } \value{ a data frame with the repository's release information: tag_name and tag_date. @@ -28,6 +26,8 @@ Given an repository on GitHub, retrieve the information about it from the GitHub API and read it into R. } \examples{ +\dontrun{ release_info <- get_release_info("jhudsl/DaSL_Course_Template_Bookdown") } +} diff --git a/man/get_repo_info.Rd b/man/get_repo_info.Rd index a6fe81a..24657ef 100644 --- a/man/get_repo_info.Rd +++ b/man/get_repo_info.Rd @@ -4,7 +4,7 @@ \alias{get_repo_info} \title{Retrieve information about a github repo} \usage{ -get_repo_info(repo_name, git_pat = NULL, verbose = FALSE, keep_json = FALSE) +get_repo_info(repo_name, git_pat = NULL, verbose = FALSE) } \arguments{ \item{repo_name}{The full name of the repo to get bookdown chapters from. @@ -16,8 +16,6 @@ grab from a git pat set in the environment with usethis::create_github_token(). Authorization handled by \link[cow]{get_git_auth}} \item{verbose}{TRUE/FALSE do you want more progress messages?} - -\item{keep_json}{verbose TRUE/FALSE keep the json file locally?} } \value{ a data frame with the repository with the following columns: diff --git a/man/retrieve_org_repos.Rd b/man/retrieve_org_repos.Rd index a0d083e..ff9d1ae 100644 --- a/man/retrieve_org_repos.Rd +++ b/man/retrieve_org_repos.Rd @@ -38,4 +38,5 @@ retrieve_org_repos( org_name = "jhudsl", output_file = "jhudsl_repos.tsv" ) + }