From 81ec2b723ec2573b3678904921243dd4e7e5a070 Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Wed, 12 Aug 2026 23:15:34 +0200 Subject: [PATCH 1/9] Add support to write back and load individual metrics in online --- ...onsApi_2.9.3.pdf => imotionsApi_2.9.4.pdf} | Bin 130003 -> 129922 bytes imotionsApi/DESCRIPTION | 2 +- imotionsApi/NAMESPACE | 2 + imotionsApi/R/imotionsApi.R | 263 +++++--- imotionsApi/man/getAoiMetricsUrl.Rd | 19 + .../man/getAoiMetricsUrl.imRespondent.Rd | 19 + imotionsApi/man/getFile.Rd | 4 +- imotionsApi/man/getUploadAoiMetricsUrl.Rd | 7 +- .../getUploadAoiMetricsUrl.imRespondent.Rd | 19 + .../man/privateGetAoiRespondentMetricsPath.Rd | 19 + .../privateUploadAoiMetrics.imRespondent.Rd | 4 +- .../man/privateUploadToPresignedUrl.Rd | 35 + .../testthat/test-getAOIRespondentMetrics.R | 140 +++- imotionsApi/tests/testthat/test-getUrls.R | 54 +- .../tests/testthat/test-httrRequests.R | 635 +++++++++--------- .../tests/testthat/test-uploadAOIMetrics.R | 112 ++- 16 files changed, 847 insertions(+), 487 deletions(-) rename documentation/{imotionsApi_2.9.3.pdf => imotionsApi_2.9.4.pdf} (81%) create mode 100644 imotionsApi/man/getAoiMetricsUrl.Rd create mode 100644 imotionsApi/man/getAoiMetricsUrl.imRespondent.Rd create mode 100644 imotionsApi/man/getUploadAoiMetricsUrl.imRespondent.Rd create mode 100644 imotionsApi/man/privateGetAoiRespondentMetricsPath.Rd create mode 100644 imotionsApi/man/privateUploadToPresignedUrl.Rd diff --git a/documentation/imotionsApi_2.9.3.pdf b/documentation/imotionsApi_2.9.4.pdf similarity index 81% rename from documentation/imotionsApi_2.9.3.pdf rename to documentation/imotionsApi_2.9.4.pdf index c643f0496668bc2421844b98f1b18ad0dcc688d4..d1364278ea25b0ef7fe3fa65547f06027a426440 100644 GIT binary patch delta 24562 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

ho$L?-a~_Xw@{re_#0hseA0zWM>wU=Sxw=` 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 E01QAO4gdfE diff --git a/imotionsApi/DESCRIPTION b/imotionsApi/DESCRIPTION index 288bd3e..f00f6cf 100644 --- a/imotionsApi/DESCRIPTION +++ b/imotionsApi/DESCRIPTION @@ -1,7 +1,7 @@ Package: imotionsApi Type: Package Title: iMotions R library -Version: 2.9.3-0000000000 +Version: 2.9.4-0000000000 Date: 2026-07-08 Authors@R: c( person("Amandine", "Grappe", role = "aut"), diff --git a/imotionsApi/NAMESPACE b/imotionsApi/NAMESPACE index c23e5b6..cf1338b 100644 --- a/imotionsApi/NAMESPACE +++ b/imotionsApi/NAMESPACE @@ -4,8 +4,10 @@ S3method("[",imObject) S3method("[",imObjectList) S3method(getAoiDetailsUrl,imAOI) S3method(getAoiDetailsUrl,imStimulus) +S3method(getAoiMetricsUrl,imRespondent) S3method(getSensorsUrl,imRespondent) S3method(getSensorsUrl,imSegment) +S3method(getUploadAoiMetricsUrl,imRespondent) S3method(getUploadAoiMetricsUrl,imSegment) S3method(getUploadEventsUrl,imRespondent) S3method(getUploadMetricsUrl,imRespondent) diff --git a/imotionsApi/R/imotionsApi.R b/imotionsApi/R/imotionsApi.R index 2e0423c..0932aae 100644 --- a/imotionsApi/R/imotionsApi.R +++ b/imotionsApi/R/imotionsApi.R @@ -945,6 +945,7 @@ getSensors <- function(study, target, stimulus = NULL) { type <- tolower(gsub("^im|List", "", class(target)[1])) endpoint <- paste0(type, ": ", target$name) + if (!is.null(stimulus)) { endpoint <- paste0(endpoint, ", stimulus: ", stimulus$name) } @@ -1642,6 +1643,21 @@ getAoiRespondentData <- function(study, AOI, respondent) { } +#' Return the local cache path for AOI respondent metrics. +#' +#' @param study An imStudy object as returned from \code{\link{imStudy}}. +#' @param AOI An imAOI object as returned from \code{\link{getAois}}. +#' @param respondent An imRespondent object as returned from \code{\link{getRespondents}}. +#' +#' @keywords internal +privateGetAoiRespondentMetricsPath <- function(study, AOI, respondent) { + if (is.null(study$connection$localPath)) return(NULL) + + return(file.path(study$connection$localPath, sub("^.*/aoiDefinition/", "", AOI$fileId), + paste0(respondent$id, "metrics.csv"))) +} + + #' Get the metrics for a specific AOI/respondent combination. #' #' @param study An imStudy object as returned from imStudy() @@ -1669,6 +1685,7 @@ getAoiRespondentMetrics <- function(study, AOI, respondent) { assertClass(respondent, "imRespondent", "`respondent` argument is not an imRespondent object") endpoint <- paste0("AOI: ", AOI$name, ", Respondent: ", respondent$name) + metricsFile <- NULL if (study$connection$localIM) { AOIDetails <- privateGetAoiDetails(study, AOI, respondent) @@ -1678,31 +1695,36 @@ getAoiRespondentMetrics <- function(study, AOI, respondent) { return(NULL) } - if (is.na(AOIDetails$resultId)) { - warning(paste0("No metrics found for ", endpoint)) - return(NULL) - } - - metrics <- fread(AOIDetails$resultId) + metricsFile <- AOIDetails$resultId } else { - # For online we load metrics written locally - if (is.null(study$connection$localPath)) { - warning("No localPath set when calling imConnection(), not possible to read metrics locally.") - return(NULL) - } - - tmp_dir <- study$connection$localPath - file_name <- file.path(tmp_dir, sub("^.*/aoiDefinition/", "", AOI$fileId), paste0(respondent$id, "metrics.csv")) + metricsFile <- privateGetAoiRespondentMetricsPath(study, AOI, respondent) + + # Retrieve the S3 location only when the metrics are not already cached locally. + if (is.null(metricsFile) || !file.exists(metricsFile)) { + stats <- tryCatch({ + getJSON(study$connection, getAoiMetricsUrl(study, respondent, AOI), + message = paste("Retrieving AOI respondent metrics for", endpoint)) + }, error = function(err) { + if (grepl("Resource not found", err$message, fixed = TRUE)) return(NULL) + stop(err) + }) - if (file.exists(file_name)) { - metrics <- fread(file_name) - } else { - warning(paste0("No metrics found for ", endpoint)) - return(NULL) + if (!is.null(stats)) { + metricsFile <- getFile(study$connection, stats$aoiRespondentStatsUrl, + message = paste("Downloading AOI respondent metrics for", endpoint), + localFilePath = metricsFile)$file_path + } else { + metricsFile <- NULL + } } } - metrics <- checkDataFormat(metrics) + if (is.null(metricsFile) || is.na(metricsFile)) { + warning(paste0("No metrics found for ", endpoint)) + return(NULL) + } + + metrics <- checkDataFormat(fread(metricsFile)) return(metrics) } @@ -1907,6 +1929,24 @@ uploadAoiMetrics <- function(study, AOI, target, metrics) { } +#' Get upload credentials and upload a file to the returned presigned URL. +#' +#' @param connection An imConnection object as returned from \code{\link{imConnection}}. +#' @param uploadUrl The URL used to request upload credentials. +#' @param postData The JSON body used to request upload credentials. +#' @param fileName The path of the file to upload. +#' @param credentialsMsg Message used if requesting credentials fails. +#' @param uploadMsg Message used if uploading the file fails. +#' +#' @return The upload credentials returned by the API. +#' @keywords internal +privateUploadToPresignedUrl <- function(connection, uploadUrl, postData, fileName, credentialsMsg, uploadMsg) { + fileInfos <- postJSON(connection, uploadUrl, postData, message = credentialsMsg) + putHttr(connection, fileInfos$presignedUrl, fileName, message = uploadMsg) + return(fileInfos) +} + + #' Generic privateUploadAoiMetrics function that takes as parameter a study object, a respondent/segment object, an AOI #' and some metrics to upload. #' @@ -1923,9 +1963,9 @@ privateUploadAoiMetrics <- function(study, obj, AOI, metrics) { } -#' Writes back metrics locally for a respondent of interest. +#' Write back or upload metrics for a respondent of interest. #' -#' S3 method to write back metrics locally for a specific respondent. +#' S3 method to write back metrics locally or upload them for a specific respondent. #' #' @inheritParams privateUploadAoiMetrics #' @@ -1942,19 +1982,29 @@ privateUploadAoiMetrics.imRespondent <- function(study, obj, AOI, metrics) { if (study$connection$localIM) { dataFileName <- paste0(tools::file_path_sans_ext(AOIDetails$fileId), "metrics.csv") } else { - if (is.null(study$connection$localPath)) { - warning("Please set a localPath when calling imConnection() to write back metrics locally.") - return() - } - - # For online we write back metrics locally - tmp_dir <- study$connection$localPath - dataFileName <- file.path(tmp_dir, sub("^.*/aoiDefinition/", "", AOI$fileId), paste0(obj$id, "metrics.csv")) - - dir.create(dirname(dataFileName), showWarnings = FALSE, recursive = TRUE) + dataFileName <- tempfile(fileext = ".csv") } fwrite(x = metrics, file = dataFileName, col.names = TRUE, row.names = FALSE) + + if (!study$connection$localIM) { + uploadUrl <- getUploadAoiMetricsUrl(study, obj, AOI) + postData <- toJSON(list(name = AOI$name, fileName = dataFileName), null = "null") + endpoint <- paste0("respondent: ", obj$name, ", AOI: ", AOI$name) + uploadMessage <- paste("Uploading AOI metrics for", endpoint) + + fileInfos <- privateUploadToPresignedUrl(study$connection, uploadUrl, postData, dataFileName, + "Getting presignedUrl to upload AOI metrics", uploadMessage) + + putHttr(study$connection, uploadUrl, reqBody = toJSON(fileInfos, null = "null"), + message = paste("Upload of AOI metrics for", endpoint, "confirmed")) + + # Check if some locally saved metrics needs to be invalidated + cachedFile <- privateGetAoiRespondentMetricsPath(study, AOI, obj) + if (!is.null(cachedFile)) unlink(cachedFile) + + unlink(dataFileName) + } } @@ -2220,16 +2270,15 @@ privateUpload <- function(params, study, data, target, sampleName, scriptName, m endpoint <- paste0(endpoint, ", stimulus: ", stimulus$name) } + uploadMessage <- paste("Uploading", endpoint_data, "for", endpoint) + if (study$connection$localIM) { - fileInfos <- postJSON(study$connection, uploadUrl, postData, - message = paste("Uploading", endpoint_data, "for", endpoint)) + fileInfos <- postJSON(study$connection, uploadUrl, postData, message = uploadMessage) } else { assertValid(exists("reportRunId", params), "Required `reportRunId` field in params for remote connection") uploadUrl <- str_replace(uploadUrl, "placeholder_reportId", params$reportRunId) - fileInfos <- postJSON(study$connection, uploadUrl, postData, message = "Getting presignedUrl to upload data") - - putHttr(study$connection, fileInfos$presignedUrl, tempFileName, - message = paste("Uploading", endpoint_data, "for", endpoint)) + fileInfos <- privateUploadToPresignedUrl(study$connection, uploadUrl, postData, tempFileName, + "Getting presignedUrl to upload data", uploadMessage) if (inherits(data, "imExport")) { confirmUrl <- uploadUrl @@ -2784,7 +2833,6 @@ getUploadMetricsUrl <- function(study, imObject) { UseMethod("getUploadMetricsUrl", object = imObject) } - #' getUploadMetricsUrl method to get the path/url to upload metrics to this respondent object. #' #' @inheritParams getUploadMetricsUrl @@ -2795,49 +2843,6 @@ getUploadMetricsUrl.imRespondent <- function(study, imObject) { file.path(getStudyBaseUrl(study), "rmetrics", study$id, "respondent", imObject$id, "data") } - -#' getUploadAoiMetadataUrl function that takes as parameter a study object. -#' -#' Return the path/url to upload AOI metadata for a specific study. -#' -#' @param study An imStudy object as returned from \code{\link{imStudy}}. -#' -#' @keywords internal -getUploadAoiMetadataUrl <- function(study) { - if (!study$connection$localIM) { - file.path(getStudyBaseUrl(study), "aoi", "sets", study$aoiSet$id, "metadata") - } -} - - -#' Generic getUploadAoiMetricsUrl function that takes as parameter a study object, a respondent/segment object and -#' an AOI -#' . -#' Return the path/url to upload AOI metrics to this respondent/segment object. -#' -#' @param study An imStudy object as returned from \code{\link{imStudy}}. -#' @param imObject An imRespondent or imSegment object of interest. -#' @param AOI An imAOI object as returned from \code{\link{getAois}}. -#' -#' @keywords internal -getUploadAoiMetricsUrl <- function(study, imObject, AOI) { - UseMethod("getUploadAoiMetricsUrl", object = imObject) -} - - -#' getUploadAoiMetricsUrl method to get the path/url to upload AOI metrics to this segment object. -#' -#' @inheritParams getUploadAoiMetricsUrl -#' -#' @keywords internal -#' @exportS3Method getUploadAoiMetricsUrl imSegment -getUploadAoiMetricsUrl.imSegment <- function(study, imObject, AOI) { - if (!study$connection$localIM) { - file.path(getAoisUrl(study), AOI$id, "segments", imObject$id, "stats") - } -} - - #' getAoiUrl function that takes as parameter a study object and optionally a respondent or a stimulus id. #' #' Return the path/url to the AOIs for this study/respondent/stimulus. @@ -2923,6 +2928,86 @@ getAoiDetailsUrl.imStimulus <- function(study, imObject, respondent = NULL) { } +#' Generic getAoiMetricsUrl function that takes as parameter a study, respondent object and AOI. +#' +#' Return the path/url to retrieve AOI metrics for the given object. +#' +#' @param study An imStudy object as returned from \code{\link{imStudy}}. +#' @param imObject An imRespondent object of interest. +#' @param AOI An imAOI object as returned from \code{\link{getAois}}. +#' +#' @keywords internal +getAoiMetricsUrl <- function(study, imObject, AOI) { + UseMethod("getAoiMetricsUrl", object = imObject) +} + + +#' getAoiMetricsUrl method to return the path/url to retrieve AOI metrics for a respondent. +#' +#' @inheritParams getAoiMetricsUrl +#' +#' @keywords internal +#' @exportS3Method getAoiMetricsUrl imRespondent +getAoiMetricsUrl.imRespondent <- function(study, imObject, AOI) { + if (!study$connection$localIM) { + file.path(getStudyBaseUrl(study), "aoi", "definitions", AOI$id, "respondents", imObject$id, "stats") + } +} + +#' getUploadAoiMetadataUrl function that takes as parameter a study object. +#' +#' Return the path/url to upload AOI metadata for a specific study. +#' +#' @param study An imStudy object as returned from \code{\link{imStudy}}. +#' +#' @keywords internal +getUploadAoiMetadataUrl <- function(study) { + if (!study$connection$localIM) { + file.path(getStudyBaseUrl(study), "aoi", "sets", study$aoiSet$id, "metadata") + } +} + +#' Generic getUploadAoiMetricsUrl function that takes as parameter a study object, a respondent/segment object and +#' an AOI. +#' +#' Return the path/url to upload AOI metrics to this respondent/segment object. +#' +#' @param study An imStudy object as returned from \code{\link{imStudy}}. +#' @param imObject An imRespondent or imSegment object of interest. +#' @param AOI An imAOI object as returned from \code{\link{getAois}}. +#' +#' @keywords internal +getUploadAoiMetricsUrl <- function(study, imObject, AOI) { + UseMethod("getUploadAoiMetricsUrl", object = imObject) +} + + +#' getUploadAoiMetricsUrl method to get the path/url to upload AOI metrics to this respondent object. +#' +#' @inheritParams getUploadAoiMetricsUrl +#' +#' @keywords internal +#' @exportS3Method getUploadAoiMetricsUrl imRespondent +getUploadAoiMetricsUrl.imRespondent <- function(study, imObject, AOI) { + if (!study$connection$localIM) { + file.path(getStudyBaseUrl(study), "aoi", "definitions", AOI$id, "respondents", imObject$id, "upload") + } +} + + +#' getUploadAoiMetricsUrl method to get the path/url to upload AOI metrics to this segment object. +#' +#' @inheritParams getUploadAoiMetricsUrl +#' +#' @keywords internal +#' @exportS3Method getUploadAoiMetricsUrl imSegment +getUploadAoiMetricsUrl.imSegment <- function(study, imObject, AOI) { + if (!study$connection$localIM) { + file.path(getAoisUrl(study), AOI$id, "segments", imObject$id, "stats") + } +} + + #' Return the path/url to the scenes of a specific respondent. #' #' @param study An imStudy object as returned from \code{\link{imStudy}}. @@ -2975,24 +3060,28 @@ getJSON <- function(connection, url, message = NULL, ...) { #' error message. #' #' @param fileName Optional - In case of a zip folder, the name of the file to look for. +#' @param localFilePath Optional - Exact local path where the downloaded file should be cached. #' #' @return A list with the temporary folder path and the downloaded file name. #' @keywords internal -getFile <- function(connection, url, message = NULL, fileName = NULL) { +getFile <- function(connection, url, message = NULL, fileName = NULL, localFilePath = NULL) { response <- getHttr(connection, url, message) - # Use temporary directory to download data - if (!is.null(connection$localPath)) { + # Use the requested file path, or fall back to the connection/temporary download directory. + if (!is.null(localFilePath)) { + tmp_dir <- dirname(localFilePath) + file_path <- localFilePath + } else if (!is.null(connection$localPath)) { tmp_dir <- connection$localPath file_path <- file.path(tmp_dir, sub("^.*/StudyUpload/", "", url)) - dir.create(dirname(file_path), showWarnings = FALSE, recursive = TRUE) } else { tmp_dir <- tempdir(check = TRUE) file_path <- file.path(tmp_dir, basename(url)) } + # Reuse files already downloaded to the selected path. if (!file.exists(file_path)) { - # We only download the file if it's not there yet + dir.create(dirname(file_path), showWarnings = FALSE, recursive = TRUE) download.file(response$url, file_path, method = "auto", mode = "wb") } else { message("Retrieving local data for ", file_path) diff --git a/imotionsApi/man/getAoiMetricsUrl.Rd b/imotionsApi/man/getAoiMetricsUrl.Rd new file mode 100644 index 0000000..a70e133 --- /dev/null +++ b/imotionsApi/man/getAoiMetricsUrl.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/imotionsApi.R +\name{getAoiMetricsUrl} +\alias{getAoiMetricsUrl} +\title{Generic getAoiMetricsUrl function that takes as parameter a study, respondent object and AOI.} +\usage{ +getAoiMetricsUrl(study, imObject, AOI) +} +\arguments{ +\item{study}{An imStudy object as returned from \code{\link{imStudy}}.} + +\item{imObject}{An imRespondent object of interest.} + +\item{AOI}{An imAOI object as returned from \code{\link{getAois}}.} +} +\description{ +Return the path/url to retrieve AOI metrics for the given object. +} +\keyword{internal} diff --git a/imotionsApi/man/getAoiMetricsUrl.imRespondent.Rd b/imotionsApi/man/getAoiMetricsUrl.imRespondent.Rd new file mode 100644 index 0000000..5db3584 --- /dev/null +++ b/imotionsApi/man/getAoiMetricsUrl.imRespondent.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/imotionsApi.R +\name{getAoiMetricsUrl.imRespondent} +\alias{getAoiMetricsUrl.imRespondent} +\title{getAoiMetricsUrl method to return the path/url to retrieve AOI metrics for a respondent.} +\usage{ +\method{getAoiMetricsUrl}{imRespondent}(study, imObject, AOI) +} +\arguments{ +\item{study}{An imStudy object as returned from \code{\link{imStudy}}.} + +\item{imObject}{An imRespondent object of interest.} + +\item{AOI}{An imAOI object as returned from \code{\link{getAois}}.} +} +\description{ +getAoiMetricsUrl method to return the path/url to retrieve AOI metrics for a respondent. +} +\keyword{internal} diff --git a/imotionsApi/man/getFile.Rd b/imotionsApi/man/getFile.Rd index 8c6142b..9ad9eaa 100644 --- a/imotionsApi/man/getFile.Rd +++ b/imotionsApi/man/getFile.Rd @@ -5,7 +5,7 @@ \title{Retrieve a zip/csv file from the indicated path/url - optionally allow the user to add more information to know when an error occurred.} \usage{ -getFile(connection, url, message = NULL, fileName = NULL) +getFile(connection, url, message = NULL, fileName = NULL, localFilePath = NULL) } \arguments{ \item{connection}{An imConnection object as returned from \code{\link{imConnection}}.} @@ -16,6 +16,8 @@ getFile(connection, url, message = NULL, fileName = NULL) error message.} \item{fileName}{Optional - In case of a zip folder, the name of the file to look for.} + +\item{localFilePath}{Optional - Exact local path where the downloaded file should be cached.} } \value{ A list with the temporary folder path and the downloaded file name. diff --git a/imotionsApi/man/getUploadAoiMetricsUrl.Rd b/imotionsApi/man/getUploadAoiMetricsUrl.Rd index f1d0518..f9c1a96 100644 --- a/imotionsApi/man/getUploadAoiMetricsUrl.Rd +++ b/imotionsApi/man/getUploadAoiMetricsUrl.Rd @@ -3,9 +3,7 @@ \name{getUploadAoiMetricsUrl} \alias{getUploadAoiMetricsUrl} \title{Generic getUploadAoiMetricsUrl function that takes as parameter a study object, a respondent/segment object and -an AOI -. -Return the path/url to upload AOI metrics to this respondent/segment object.} +an AOI.} \usage{ getUploadAoiMetricsUrl(study, imObject, AOI) } @@ -17,9 +15,6 @@ getUploadAoiMetricsUrl(study, imObject, AOI) \item{AOI}{An imAOI object as returned from \code{\link{getAois}}.} } \description{ -Generic getUploadAoiMetricsUrl function that takes as parameter a study object, a respondent/segment object and -an AOI -. Return the path/url to upload AOI metrics to this respondent/segment object. } \keyword{internal} diff --git a/imotionsApi/man/getUploadAoiMetricsUrl.imRespondent.Rd b/imotionsApi/man/getUploadAoiMetricsUrl.imRespondent.Rd new file mode 100644 index 0000000..037c4bc --- /dev/null +++ b/imotionsApi/man/getUploadAoiMetricsUrl.imRespondent.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/imotionsApi.R +\name{getUploadAoiMetricsUrl.imRespondent} +\alias{getUploadAoiMetricsUrl.imRespondent} +\title{getUploadAoiMetricsUrl method to get the path/url to upload AOI metrics to this respondent object.} +\usage{ +\method{getUploadAoiMetricsUrl}{imRespondent}(study, imObject, AOI) +} +\arguments{ +\item{study}{An imStudy object as returned from \code{\link{imStudy}}.} + +\item{imObject}{An imRespondent or imSegment object of interest.} + +\item{AOI}{An imAOI object as returned from \code{\link{getAois}}.} +} +\description{ +getUploadAoiMetricsUrl method to get the path/url to upload AOI metrics to this respondent object. +} +\keyword{internal} diff --git a/imotionsApi/man/privateGetAoiRespondentMetricsPath.Rd b/imotionsApi/man/privateGetAoiRespondentMetricsPath.Rd new file mode 100644 index 0000000..b7a7c29 --- /dev/null +++ b/imotionsApi/man/privateGetAoiRespondentMetricsPath.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/imotionsApi.R +\name{privateGetAoiRespondentMetricsPath} +\alias{privateGetAoiRespondentMetricsPath} +\title{Return the local cache path for AOI respondent metrics.} +\usage{ +privateGetAoiRespondentMetricsPath(study, AOI, respondent) +} +\arguments{ +\item{study}{An imStudy object as returned from \code{\link{imStudy}}.} + +\item{AOI}{An imAOI object as returned from \code{\link{getAois}}.} + +\item{respondent}{An imRespondent object as returned from \code{\link{getRespondents}}.} +} +\description{ +Return the local cache path for AOI respondent metrics. +} +\keyword{internal} diff --git a/imotionsApi/man/privateUploadAoiMetrics.imRespondent.Rd b/imotionsApi/man/privateUploadAoiMetrics.imRespondent.Rd index 00b17bd..5faa006 100644 --- a/imotionsApi/man/privateUploadAoiMetrics.imRespondent.Rd +++ b/imotionsApi/man/privateUploadAoiMetrics.imRespondent.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/imotionsApi.R \name{privateUploadAoiMetrics.imRespondent} \alias{privateUploadAoiMetrics.imRespondent} -\title{Writes back metrics locally for a respondent of interest.} +\title{Write back or upload metrics for a respondent of interest.} \usage{ \method{privateUploadAoiMetrics}{imRespondent}(study, obj, AOI, metrics) } @@ -16,6 +16,6 @@ \item{metrics}{A data.table containing the metrics to upload.} } \description{ -S3 method to write back metrics locally for a specific respondent. +S3 method to write back metrics locally or upload them for a specific respondent. } \keyword{internal} diff --git a/imotionsApi/man/privateUploadToPresignedUrl.Rd b/imotionsApi/man/privateUploadToPresignedUrl.Rd new file mode 100644 index 0000000..03b0bcd --- /dev/null +++ b/imotionsApi/man/privateUploadToPresignedUrl.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/imotionsApi.R +\name{privateUploadToPresignedUrl} +\alias{privateUploadToPresignedUrl} +\title{Get upload credentials and upload a file to the returned presigned URL.} +\usage{ +privateUploadToPresignedUrl( + connection, + uploadUrl, + postData, + fileName, + credentialsMsg, + uploadMsg +) +} +\arguments{ +\item{connection}{An imConnection object as returned from \code{\link{imConnection}}.} + +\item{uploadUrl}{The URL used to request upload credentials.} + +\item{postData}{The JSON body used to request upload credentials.} + +\item{fileName}{The path of the file to upload.} + +\item{credentialsMsg}{Message used if requesting credentials fails.} + +\item{uploadMsg}{Message used if uploading the file fails.} +} +\value{ +The upload credentials returned by the API. +} +\description{ +Get upload credentials and upload a file to the returned presigned URL. +} +\keyword{internal} diff --git a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R index 134f08b..11f968d 100644 --- a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R +++ b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R @@ -1,5 +1,5 @@ -# getAoiRespondentMetrics ============================================================================================= -context("getAoiRespondentMetrics()") +# privateGetAoiRespondentMetricsPath ================================================================================== +context("privateGetAoiRespondentMetricsPath()") library(mockery) @@ -12,31 +12,70 @@ study_cloud_local <- jsonlite::unserializeJSON(readLines("../data/imStudy_cloud_ AOI <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imAOI.json"))) AOI_cloud <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imAOI_cloud.json"))) AOIDetailsRespondentPath <- "../data/AOIDetailsRespondent.json" -AOIDetailsFile <- jsonlite::fromJSON(AOIDetailsRespondentPath) +AOIDetails <- jsonlite::fromJSON(AOIDetailsRespondentPath) metrics_output <- fread("../data/AOImetrics.csv") # Load respondent respondent <- getRespondents(study)[1, ] -mockedGetAoiRespondentMetrics <- function(study, AOI, respondent, AOIDetailsFile = NULL, expectedFilePath = NULL, - expectCallsDetails = 0, expectCallsFread = 0, fail = FALSE) { +expectedTmpPath <- paste0("myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/dd8a9342-f5c0-4a02-bf27-de68fc13f2bc/", + respondent$id, "metrics.csv") + +test_that("remote check - should return the local AOI respondent metrics path", { + expect_identical(privateGetAoiRespondentMetricsPath(study_cloud_local, AOI_cloud, respondent), expectedTmpPath) +}) + +# getAoiRespondentMetrics ============================================================================================= +context("getAoiRespondentMetrics()") + +mockGetAoiRespondentMetrics <- function(study, AOI, respondent, AOIDetailsFile = NULL, stats = NULL, + expectedFilePath = NULL, fileExists = FALSE, expectCallsDetails = 0, + expectCallsFileExists = 0, expectCallsFread = 0, expectCallsGetJSON = 0, + expectCallsGetFile = 0) { + privateGetAoiDetails_Stub <- mock(AOIDetailsFile) - file.exists_Stub <- mock(!fail) + file.exists_Stub <- mock(fileExists) fread_Stub <- mock(metrics_output) + getJSON_Stub <- mock(stats) + getFile_Stub <- mock(list(file_path = expectedFilePath)) metrics <- mockr::with_mock(privateGetAoiDetails = privateGetAoiDetails_Stub, file.exists = file.exists_Stub, + getJSON = getJSON_Stub, + getFile = getFile_Stub, fread = fread_Stub, { getAoiRespondentMetrics(study, AOI, respondent) }) expect_called(privateGetAoiDetails_Stub, expectCallsDetails) + expect_called(file.exists_Stub, expectCallsFileExists) + expect_called(getJSON_Stub, expectCallsGetJSON) + expect_called(getFile_Stub, expectCallsGetFile) + expect_called(fread_Stub, expectCallsFread) if (expectCallsDetails > 0) { expect_args(privateGetAoiDetails_Stub, 1, study = study, imObject = AOI, respondent = respondent) } - expect_called(fread_Stub, expectCallsFread) + if (!study$connection$localIM) { + endpoint <- paste0("AOI: ", AOI$name, ", Respondent: ", respondent$name) + + if (expectCallsFileExists > 0) { + expect_args(file.exists_Stub, 1, expectedFilePath) + } + + if (expectCallsGetJSON > 0) { + expect_args(getJSON_Stub, 1, study$connection, getAoiMetricsUrl(study, respondent, AOI), + message = paste("Retrieving AOI respondent metrics for", endpoint)) + } + + if (expectCallsGetFile > 0) { + localFilePath <- if (!is.null(study$connection$localPath)) expectedFilePath else NULL + expect_args(getFile_Stub, 1, study$connection, stats$aoiRespondentStatsUrl, + message = paste("Downloading AOI respondent metrics for", endpoint), + localFilePath = localFilePath) + } + } if (expectCallsFread > 0) { expect_args(fread_Stub, 1, expectedFilePath) @@ -73,31 +112,30 @@ test_that("error - arguments are missing or not from the good class", { info = "respondent not being an imRespondent object should throw an error") }) -test_that("warning - AOI has not been defined for this respondent", { - AOIDetailsFile <- jsonlite::fromJSON("../data/no_scenes_annotations_aoidetails.json") - expect_warning(metrics <- mockedGetAoiRespondentMetrics(study, AOI, respondent, AOIDetailsFile, - expectCallsDetails = 1), +test_that("local warning - AOI has not been defined for this respondent", { + AOIDetails <- jsonlite::fromJSON("../data/no_scenes_annotations_aoidetails.json") + expect_warning(metrics <- mockGetAoiRespondentMetrics(study, AOI, respondent, AOIDetails, expectCallsDetails = 1), "AOI New Aoi was not found for respondent Wendy", info = "no AOI defined for this respondent should throw a warning") expect_null(metrics, info = "result should be null") }) -test_that("warning - no metrics have been found for this respondent", { - expect_warning(metrics <- mockedGetAoiRespondentMetrics(study, AOI, respondent, AOIDetailsFile, - expectCallsDetails = 1), +test_that("local warning - no metrics have been found for this respondent", { + expect_warning(metrics <- mockGetAoiRespondentMetrics(study, AOI, respondent, AOIDetails, expectCallsDetails = 1), "No metrics found for AOI: New Aoi, Respondent: Wendy", info = "no metrics found should throw an warning") expect_null(metrics, "result should be null") }) -# Modify AOIDetailsFile so it fit test data -AOIDetailsFile$resultId <- "../data/AOImetrics.csv" +# Modify AOIDetails so it fit test data +AOIDetails$resultId <- "../data/AOImetrics.csv" -test_that("local return - metrics for this AOI/respondent pair", { - metrics <- mockedGetAoiRespondentMetrics(study, AOI, respondent, AOIDetailsFile, expectCallsDetails = 1, - expectCallsFread = 1, expectedFilePath = "../data/AOImetrics.csv") +test_that("local check - should call privateGetAoiDetails and fread for a specific respondent", { + metrics <- mockGetAoiRespondentMetrics(study, AOI, respondent, AOIDetails, + expectedFilePath = "../data/AOImetrics.csv", expectCallsDetails = 1, + expectCallsFread = 1) # Check dimensions and class of metrics expect_equal(nrow(metrics), 1, infos = "metrics should always only have one row") @@ -105,31 +143,69 @@ test_that("local return - metrics for this AOI/respondent pair", { expect_s3_class(metrics, "imAOIMetrics") }) -test_that("remote warning - in case no local path is set, should send a warning and return NULL", { - expect_warning(metrics <- mockedGetAoiRespondentMetrics(study_cloud, AOI_cloud, respondent), - "No localPath set when calling imConnection(), not possible to read metrics locally.", - fixed = TRUE, info = "no local path, should throw an warning") +test_that("remote warning - no AOI respondent stats have been returned", { + expect_warning(metrics <- mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, fileExists = FALSE, + expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, + expectCallsGetJSON = 1), + "No metrics found for AOI: El Manuel Area, Respondent: Wendy", + info = "no metrics found should throw an warning") expect_null(metrics, "result should be null") }) -test_that("remote warning - in case no metrics path found", { - expect_warning(metrics <- mockedGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, fail = TRUE), +test_that("remote warning - no metrics have been uploaded for this respondent", { + expect_warning(metrics <- mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, + stats = stop("Resource not found"), + expectedFilePath = expectedTmpPath, + expectCallsFileExists = 1, expectCallsGetJSON = 1), "No metrics found for AOI: El Manuel Area, Respondent: Wendy", - info = "no metrics found should throw an warning") + fixed = TRUE + ) - expect_null(metrics, "result should be null") + expect_null(metrics) }) -test_that("remote return - metrics for this AOI/respondent pair", { - expectedDirectory <- "myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/dd8a9342-f5c0-4a02-bf27-de68fc13f2bc" - expectedFilepath <- paste0(expectedDirectory, "/", respondent$id, "metrics.csv") +test_that("remote error - getJSON errors other than resource not found should be rethrown", { + expect_error(mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, stats = stop("Server error"), + expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, + expectCallsGetJSON = 1), + "Server error", fixed = TRUE) +}) + +test_that("remote check - should call getJSON, getFile and fread when no local path is set", { + stats <- list(aoiRespondentStatsUrl = "https://s3.test/respondent-aoi-metrics.csv") - metrics <- mockedGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, expectCallsFread = 1, - expectedFilePath = expectedFilepath) + metrics <- mockGetAoiRespondentMetrics(study_cloud, AOI_cloud, respondent, stats = stats, + expectedFilePath = "../data/AOImetrics.csv", expectCallsFread = 1, + expectCallsGetJSON = 1, expectCallsGetFile = 1) # Check dimensions and class of metrics expect_equal(nrow(metrics), 1, infos = "metrics should always only have one row") expect_equal(ncol(metrics), 37, infos = "no column should be lost") expect_s3_class(metrics, "imAOIMetrics") }) + + + +test_that("remote check - should only call fread when metrics are cached locally", { + metrics <- mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, expectedFilePath = expectedTmpPath, + fileExists = TRUE, expectCallsFileExists = 1, expectCallsFread = 1) + + expect_equal(nrow(metrics), 1, infos = "metrics should always only have one row") + expect_equal(ncol(metrics), 37, infos = "no column should be lost") + expect_s3_class(metrics, "imAOIMetrics") +}) + +test_that("remote check - should call getJSON, getFile and fread when metrics are not cached locally", { + stats <- list(aoiRespondentStatsUrl = "https://s3.test/respondent-aoi-metrics.csv") + + metrics <- mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, stats = stats, + expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, + expectCallsFread = 1, expectCallsGetJSON = 1, expectCallsGetFile = 1) + + expect_equal(nrow(metrics), 1, infos = "metrics should always only have one row") + expect_equal(ncol(metrics), 37, infos = "no column should be lost") + expect_s3_class(metrics, "imAOIMetrics") +}) + + diff --git a/imotionsApi/tests/testthat/test-getUrls.R b/imotionsApi/tests/testthat/test-getUrls.R index 386a42a..3e84131 100644 --- a/imotionsApi/tests/testthat/test-getUrls.R +++ b/imotionsApi/tests/testthat/test-getUrls.R @@ -57,6 +57,16 @@ test_that("local - all getUrl function should work as expected", { expect_identical(getUploadSensorDataUrl(study, respondent, stimulus), paste0(sensorStimulusUrl, "/data")) expect_identical(getUploadSensorDataUrl(study, segment, stimulus), paste0(sensorsSegmentUrl, "/data")) + #getUploadEventsUrl + eventUrl <- paste0(baseUrl, "/revents/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1") + expect_identical(getUploadEventsUrl(study, respondent), + paste0(eventUrl, "/respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63/data")) + + #getUploadMetricsUrl + metricsUrl <- paste0(baseUrl, "/rmetrics/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1") + expect_identical(getUploadMetricsUrl(study, respondent), + paste0(metricsUrl, "/respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63/data")) + #getAoisUrl AOIUrl <- paste0(baseUrl, "/aois/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1") expect_identical(getAoisUrl(study), AOIUrl) @@ -68,26 +78,16 @@ test_that("local - all getUrl function should work as expected", { "Please provide either stimulusId or respondentId, not both.", info = "too many params not handled properly") - #getUploadEventsUrl - eventUrl <- paste0(baseUrl, "/revents/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1") - expect_identical(getUploadEventsUrl(study, respondent), - paste0(eventUrl, "/respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63/data")) - - #getUploadMetricsUrl - metricsUrl <- paste0(baseUrl, "/rmetrics/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1") - expect_identical(getUploadMetricsUrl(study, respondent), - paste0(metricsUrl, "/respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63/data")) + #getAoiDetailsUrl (AOI) + expect_identical(getAoiDetailsUrl(study, AOI), paste0(AOIUrl, "/stimuli/", AOI$stimulusId, "/", AOI$id)) + expect_identical(getAoiDetailsUrl(study, AOI, respondent), + paste0(AOIUrl, "/stimuli/", AOI$stimulusId, "/respondent/", respondentId, "/", AOI$id)) - #getAoiDetailsForStimulusUrl + #getAoiDetailsUrl (Stimulus) expect_identical(getAoiDetailsUrl(study, stimulus), paste0(AOIUrl, "/stimuli/", stimulusId, "/*")) expect_identical(getAoiDetailsUrl(study, stimulus, respondent), paste0(AOIUrl, "/stimuli/", stimulusId, "/respondent/", respondentId, "/*")) - #getAoiDetailsUrl - expect_identical(getAoiDetailsUrl(study, AOI), paste0(AOIUrl, "/stimuli/", AOI$stimulusId, "/", AOI$id)) - expect_identical(getAoiDetailsUrl(study, AOI, respondent), - paste0(AOIUrl, "/stimuli/", AOI$stimulusId, "/respondent/", respondentId, "/", AOI$id)) - #getRespondentScenesUrl sceneUrl <- paste0(baseUrl, "/scenes/af8c2165-4389-4cc3-8b1e-b2d3a4bd8be1/", "respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63") @@ -99,6 +99,11 @@ test_that("local - all getUrl function should work as expected", { "respondent/09bd22e6-29b6-4a8a-8cc1-4780a5163e63") expect_identical(getRespondentAnnotationsUrl(study, respondent), annotationsUrl) + + # List of functions that have no support locally + #getAoiMetricsUrl + #getUploadAoiMetadataUrl + #getUploadAoiMetricsUrl }) # Also checking remote study path @@ -159,14 +164,27 @@ test_that("remote - all getUrl function should work as expected", { "Please provide either stimulusId or respondentId, not both.", info = "too many params not handled properly") - #getUploadAoiMetricsUrl - uploadAOIUrl <- paste0(AOIUrl, "/09a234fc-00ad-4257-b7e1-da5754986c9d/segments/1010/stats") - expect_identical(getUploadAoiMetricsUrl(study_cloud, segment, AOI), uploadAOIUrl) + #getAoiMetricsUrl + aoiMetricsUrl <- paste0(baseUrl, "/aoi/definitions/09a234fc-00ad-4257-b7e1-da5754986c9d/respondents/", + "09bd22e6-29b6-4a8a-8cc1-4780a5163e63/stats") + + expect_identical(getAoiMetricsUrl(study_cloud, respondent, AOI), aoiMetricsUrl) #getUploadAoiMetadataUrl uploadMetadataUrl <- paste0(baseUrl, "/aoi/sets/fd7891cf-3f51-4fdd-bf48-0ee2f75d53b9/metadata") expect_identical(getUploadAoiMetadataUrl(study_cloud), uploadMetadataUrl) + #getUploadAoiMetricsUrl + respondentMetricAoiUrl <- paste0(baseUrl, "/aoi/definitions/09a234fc-00ad-4257-b7e1-da5754986c9d/respondents/", + "09bd22e6-29b6-4a8a-8cc1-4780a5163e63/upload") + + expect_identical(getUploadAoiMetricsUrl(study_cloud, respondent, AOI), respondentMetricAoiUrl) + + segmentMetricAoiUrl <- paste0(AOIUrl, "/09a234fc-00ad-4257-b7e1-da5754986c9d/segments/1010/stats") + expect_identical(getUploadAoiMetricsUrl(study_cloud, segment, AOI), segmentMetricAoiUrl) + + # List of functions that have no support in online + #getAoiDetailsUrl #getUploadEventsUrl #getUploadMetricsUrl #getRespondentScenesUrl diff --git a/imotionsApi/tests/testthat/test-httrRequests.R b/imotionsApi/tests/testthat/test-httrRequests.R index d5992d4..4c13fbb 100644 --- a/imotionsApi/tests/testthat/test-httrRequests.R +++ b/imotionsApi/tests/testthat/test-httrRequests.R @@ -1,304 +1,331 @@ -context("Testing `httrRequests` functions") -# stopOnHttpError ===================================================================================================== -context("stopOnHttpError()") - -library(mockery) - -mockResponse <- list(status_code = 200) - -mockedStopOnHttpError <- function(response) { - status_code_Stub <- mock(response) - - mockr::with_mock(status_code = status_code_Stub, { - stopOnHttpError(mockResponse, "Test API") - }) -} - -test_that("error - token is not authorized", { - expect_error(mockedStopOnHttpError(response = 401), "Test API - Token not authorized to access requested resource", - info = "error 401 is not handled properly") -}) - -test_that("error - resource is not found", { - expect_error(mockedStopOnHttpError(response = 404), "Test API - Resource not found", - info = "error 404 is not handled properly") -}) - -test_that("error - something unexpected happened", { - expect_error(mockedStopOnHttpError(response = 135), "Test API - unexpected response status (135)", - fixed = TRUE, info = "unexpected error is not handled properly") -}) - -test_that("remote/local check - should work correctly", { - # Local connection - error <- mockedStopOnHttpError(response = 200) - expect_null(error, info = "should not produce an error") - - # Remote connection - error <- mockedStopOnHttpError(response = 204) - expect_null(error, info = "should not produce an error") -}) - -# retryHttr =========================================================================================================== -context("retryHttr()") - -connection <- jsonlite::unserializeJSON(readLines("../data/imConnection.json")) -connection_cloud <- jsonlite::unserializeJSON(readLines("../data/imConnection_cloud.json")) -terminate_on <- 404 -config <- tokenHeaders(connection$token) - - -mockedRetryHttr <- function(message, url, mockResponse) { - if (mockResponse == "error") { - RETRY_Stub <- mock(stop("Error")) - } else { - RETRY_Stub <- mock(mockResponse) - } - - response <- mockr::with_mock(RETRY = RETRY_Stub, { - retryHttr(message, "GET", url, config, terminate_on) - }) - - expect_args(RETRY_Stub, 1, "GET", url, config, terminate_on, 3, 2, 60, 1, FALSE) - return(response) -} - -test_that("return - a valid response in case of a valid request", { - mockResponse$status_code <- 200 - class(mockResponse) <- "response" - response <- mockedRetryHttr("Test Message", "testurl", mockResponse) - expect_identical(response, mockResponse, "response should not have been modified") - -}) - -test_that("error - non existing url should throw an error", { - mockResponse <- "error" - - expect_error(mockedRetryHttr("Test Message", "testurl", mockResponse), - "Test Message - Cannot access: testurl", - info = "should handle error from the RETRY function") -}) - -test_that("error - invalid response", { - mockResponse$status_code <- 404 - class(mockResponse) <- "response" - - expect_error(mockedRetryHttr("Test Message", "testurl", mockResponse), - "Test Message - url: testurl - Resource not found", - info = "should handle error from the stopOnHttpError function") -}) - -# getHttr ============================================================================================================= -context("getHttr()") - -mockedGetHttr <- function(connection, url, mockResponse, terminate_on) { - config <- tokenHeaders(connection$token) - retryHttr_Stub <- mock(mockResponse) - - response <- mockr::with_mock(retryHttr = retryHttr_Stub, { - getHttr(connection, url, "Test API") - }) - - expect_args(retryHttr_Stub, 1, "Test API", "GET", url, config, terminate_on) - return(response) -} - -test_that("remote/local check - should work correctly", { - # Local connection - mockResponse$status_code <- 200 - class(mockResponse) <- "response" - response <- mockedGetHttr(connection, "url", mockResponse, terminate_on) - expect_identical(response, mockResponse, "response should not have been modified") - - # Remote connection - mockResponse$status_code <- 204 - class(mockResponse) <- "response" - response <- mockedGetHttr(connection_cloud, "url", mockResponse, terminate_on = NULL) - expect_identical(response, mockResponse, "response should not have been modified") -}) - -# getJSON ============================================================================================================= -context("getJSON()") - -mockedGetJSON <- function(connection, url, mockResponse) { - getHttr_Stub <- mock(mockResponse) - - data <- mockr::with_mock(getHttr = getHttr_Stub, { - getJSON(connection, url, "Test API") - }) - - expect_args(getHttr_Stub, 1, connection, url, "Test API") - return(data) -} - -test_that("return - JSON file if a good request has been sent", { - mockData <- '{"userId":1,"id":1,"title":"delectus aut autem"}' - mockResponse <- list(status_code = 200, content = charToRaw(mockData)) - class(mockResponse) <- "response" - - expectedReturn <- list(userId = 1, id = 1, title = "delectus aut autem") - - #in case of good request - send back a JSON file - data <- mockedGetJSON(connection, "url", mockResponse) - expect_equal(data, expectedReturn, info = "wrong object returned") -}) - -# getFile ============================================================================================================= -context("getFile()") - -# Get the sensors through the cloud -sensors_cloud <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imSensorList_cloud.json"))) - -mockedGetFile <- function(connection, url, mockResponse, fileName) { - getHttr_Stub <- mock(mockResponse) - - fileInfos <- mockr::with_mock(getHttr = getHttr_Stub, { - getFile(connection, url, "Test API", fileName) - }) - - expect_args(getHttr_Stub, 1, connection, url, "Test API") - return(fileInfos) -} - -test_that("remote return - correct paths to the file", { - # Case with eyetracking data (zip file) - eyetracking_fileName <- sensors_cloud[2, ]$fileName - url <- "file://../data/example_data_cloud.zip" - mockResponse <- list(headers = list("content-type" = "application/zip"), url = url) - class(mockResponse) <- "response" - - fileInfos <- mockedGetFile(connection_cloud, url, mockResponse, eyetracking_fileName) - - expected_path <- paste0("ProgramData/iMotions/Lab_NG/Data/RRRock The R/Signals/", - "20e73a6c-f2ae-4146-90f7-1430bbc9857b/ET_Eyetracker.csv") - - expect_identical(fileInfos$file_path, file.path(gsub("\\\\", "/", fileInfos$tmp_dir), expected_path), - "wrong file found") - - expect_true(file.exists(fileInfos$file_path), info = "file should exists") - unlink(c(fileInfos$file_path, file.path(fileInfos$tmp_dir, "ProgramData")), recursive = TRUE) - expect_false(file.exists(fileInfos$file_path), info = "file should have been deleted") - - # Case with slideEvents data (csv file) - events_fileName <- sensors_cloud[2, ]$fileName - url <- "file://../data/Native_SlideEvents_cloud.csv" - mockResponse <- list(headers = list("content-type" = "application/octet-stream"), url = url) - class(mockResponse) <- "response" - - fileInfos <- mockedGetFile(connection_cloud, url, mockResponse, events_fileName) - - expected_path <- file.path(fileInfos$tmp_dir, "Native_SlideEvents_cloud.csv") - expect_identical(fileInfos$file_path, expected_path, "wrong file found") - - expect_true(file.exists(fileInfos$file_path), info = "file should exists") - unlink(c(fileInfos$file_path, file.path(fileInfos$tmp_dir, "ProgramData")), recursive = TRUE) - expect_false(file.exists(fileInfos$file_path), info = "file should have been deleted") -}) - -# postHttr ============================================================================================================ -context("postHttr()") - -body <- '{"flowName":"flowName","sampleName":"TestSensor","fileName":"../data/testFile.csv"}' -class(body) <- "json" - -mockedPostHttr <- function(connection, url, mockResponse, body) { - config <- tokenHeaders(connection$token) - retryHttr_Stub <- mock(mockResponse) - - response <- mockr::with_mock(retryHttr = retryHttr_Stub, { - postHttr(connection, url, body, "Test API") - }) - - expect_args(retryHttr_Stub, 1, "Test API", "POST", url, body, config, jsonHeaders(), "json") - return(response) -} - -test_that("remote/local check - should work correctly", { - # Local connection - mockResponse$status_code <- 200 - class(mockResponse) <- "response" - response <- mockedPostHttr(connection, "url", mockResponse, body) - expect_identical(response, mockResponse, "response should not have been modified") - - # Remote connection - mockResponse$status_code <- 204 - class(mockResponse) <- "response" - response <- mockedPostHttr(connection_cloud, "url", mockResponse, body) - expect_identical(response, mockResponse, "response should not have been modified") -}) - -# postJSON ============================================================================================================ -context("postJSON()") - -mockedPostJSON <- function(connection, url, body, mockResponse = NULL) { - postHttr_Stub <- mock(mockResponse) - - fileInfos <- mockr::with_mock(postHttr = postHttr_Stub, { - postJSON(connection, url, body, "Test Upload API") - }) - - expect_args(postHttr_Stub, 1, connection, url, body, "Test Upload API") - return(fileInfos) -} - -test_that("local check - should upload a JSON file if a good request has been sent", { - # prepare mock results - mockData <- '{"filePath":"C:\\\\Users\\\\exampleUrls"}' - mockResponse <- list(status_code = 200, content = charToRaw(mockData)) - class(mockResponse) <- "response" - - # in case of good request - send back the url - expectedReturn <- list(filePath = "C:\\Users\\exampleUrls") - fileInfos <- mockedPostJSON(connection, "url", body, mockResponse) - expect_identical(fileInfos, expectedReturn, info = "wrong object returned") -}) - - -test_that("remote check - should get back correct information if asking for credential", { - # prepare mock results - expectedReturn <- fromJSON("../data/uploadCredential_cloud.json") - mockData <- as.character(toJSON(expectedReturn)) - mockResponse <- list(status_code = 200, content = charToRaw(mockData)) - class(mockResponse) <- "response" - - # in case of good request - send back the whole table - fileInfos <- mockedPostJSON(connection_cloud, "url", body, mockResponse) - expect_identical(fileInfos, expectedReturn, info = "wrong object returned") -}) - -# putHttr ============================================================================================================= -context("putHttr()") - -mockedPutHttr <- function(connection, url, mockResponse, fileName = NULL, reqBody = NULL, expectedBody = NULL, - expectedConfig = NULL) { - - retryHttr_Stub <- mock(mockResponse) - - mockr::with_mock(retryHttr = retryHttr_Stub, { - putHttr(connection, url, fileName, reqBody, "Test Put API") - }) - - expect_args(retryHttr_Stub, 1, "Test Put API", "PUT", url, body = expectedBody, config = expectedConfig) -} - -test_that("remote check - should work correctly", { - # With a file pushed - mockResponse$status_code <- 200 - class(mockResponse) <- "response" - config <- csvHeaders() - fileName <- "../data/testFile.csv" - expectedBody <- httr::upload_file(fileName) - mockedPutHttr(connection_cloud, "url", mockResponse, fileName, expectedBody = expectedBody, expectedConfig = config) - - # With no file pushed - mockResponse$status_code <- 204 - class(mockResponse) <- "response" - config <- tokenHeaders(connection_cloud$token) - mockedPutHttr(connection_cloud, "url", mockResponse, expectedConfig = config) - - # With a json object pushed - config <- c(tokenHeaders(connection_cloud$token), jsonHeaders()) - expectedBody <- "test" - mockedPutHttr(connection_cloud, "url", mockResponse, reqBody = "test", expectedBody = expectedBody, - expectedConfig = config) -}) +context("Testing `httrRequests` functions") +# stopOnHttpError ===================================================================================================== +context("stopOnHttpError()") + +library(mockery) + +mockResponse <- list(status_code = 200) + +mockedStopOnHttpError <- function(response) { + status_code_Stub <- mock(response) + + mockr::with_mock(status_code = status_code_Stub, { + stopOnHttpError(mockResponse, "Test API") + }) +} + +test_that("error - token is not authorized", { + expect_error(mockedStopOnHttpError(response = 401), "Test API - Token not authorized to access requested resource", + info = "error 401 is not handled properly") +}) + +test_that("error - resource is not found", { + expect_error(mockedStopOnHttpError(response = 404), "Test API - Resource not found", + info = "error 404 is not handled properly") +}) + +test_that("error - something unexpected happened", { + expect_error(mockedStopOnHttpError(response = 135), "Test API - unexpected response status (135)", + fixed = TRUE, info = "unexpected error is not handled properly") +}) + +test_that("remote/local check - should work correctly", { + # Local connection + error <- mockedStopOnHttpError(response = 200) + expect_null(error, info = "should not produce an error") + + # Remote connection + error <- mockedStopOnHttpError(response = 204) + expect_null(error, info = "should not produce an error") +}) + +# retryHttr =========================================================================================================== +context("retryHttr()") + +connection <- jsonlite::unserializeJSON(readLines("../data/imConnection.json")) +connection_cloud <- jsonlite::unserializeJSON(readLines("../data/imConnection_cloud.json")) +terminate_on <- 404 +config <- tokenHeaders(connection$token) + + +mockedRetryHttr <- function(message, url, mockResponse) { + if (mockResponse == "error") { + RETRY_Stub <- mock(stop("Error")) + } else { + RETRY_Stub <- mock(mockResponse) + } + + response <- mockr::with_mock(RETRY = RETRY_Stub, { + retryHttr(message, "GET", url, config, terminate_on) + }) + + expect_args(RETRY_Stub, 1, "GET", url, config, terminate_on, 3, 2, 60, 1, FALSE) + return(response) +} + +test_that("return - a valid response in case of a valid request", { + mockResponse$status_code <- 200 + class(mockResponse) <- "response" + response <- mockedRetryHttr("Test Message", "testurl", mockResponse) + expect_identical(response, mockResponse, "response should not have been modified") + +}) + +test_that("error - non existing url should throw an error", { + mockResponse <- "error" + + expect_error(mockedRetryHttr("Test Message", "testurl", mockResponse), + "Test Message - Cannot access: testurl", + info = "should handle error from the RETRY function") +}) + +test_that("error - invalid response", { + mockResponse$status_code <- 404 + class(mockResponse) <- "response" + + expect_error(mockedRetryHttr("Test Message", "testurl", mockResponse), + "Test Message - url: testurl - Resource not found", + info = "should handle error from the stopOnHttpError function") +}) + +# getHttr ============================================================================================================= +context("getHttr()") + +mockedGetHttr <- function(connection, url, mockResponse, terminate_on) { + config <- tokenHeaders(connection$token) + retryHttr_Stub <- mock(mockResponse) + + response <- mockr::with_mock(retryHttr = retryHttr_Stub, { + getHttr(connection, url, "Test API") + }) + + expect_args(retryHttr_Stub, 1, "Test API", "GET", url, config, terminate_on) + return(response) +} + +test_that("remote/local check - should work correctly", { + # Local connection + mockResponse$status_code <- 200 + class(mockResponse) <- "response" + response <- mockedGetHttr(connection, "url", mockResponse, terminate_on) + expect_identical(response, mockResponse, "response should not have been modified") + + # Remote connection + mockResponse$status_code <- 204 + class(mockResponse) <- "response" + response <- mockedGetHttr(connection_cloud, "url", mockResponse, terminate_on = NULL) + expect_identical(response, mockResponse, "response should not have been modified") +}) + +# getJSON ============================================================================================================= +context("getJSON()") + +mockedGetJSON <- function(connection, url, mockResponse) { + getHttr_Stub <- mock(mockResponse) + + data <- mockr::with_mock(getHttr = getHttr_Stub, { + getJSON(connection, url, "Test API") + }) + + expect_args(getHttr_Stub, 1, connection, url, "Test API") + return(data) +} + +test_that("return - JSON file if a good request has been sent", { + mockData <- '{"userId":1,"id":1,"title":"delectus aut autem"}' + mockResponse <- list(status_code = 200, content = charToRaw(mockData)) + class(mockResponse) <- "response" + + expectedReturn <- list(userId = 1, id = 1, title = "delectus aut autem") + + #in case of good request - send back a JSON file + data <- mockedGetJSON(connection, "url", mockResponse) + expect_equal(data, expectedReturn, info = "wrong object returned") +}) + +# getFile ============================================================================================================= +context("getFile()") + +# Get the sensors through the cloud +sensors_cloud <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imSensorList_cloud.json"))) + +mockGetFile <- function(connection, url, mockResponse, fileName = NULL, localFilePath = NULL) { + getHttr_Stub <- mock(mockResponse) + + fileInfos <- mockr::with_mock(getHttr = getHttr_Stub, { + getFile(connection, url, "Test API", fileName, localFilePath) + }) + + expect_args(getHttr_Stub, 1, connection, url, "Test API") + return(fileInfos) +} + +test_that("remote check - should call getHttr and return the correct file paths", { + # Case with eyetracking data (zip file) + eyetracking_fileName <- sensors_cloud[2, ]$fileName + url <- "file://../data/example_data_cloud.zip" + mockResponse <- list(headers = list("content-type" = "application/zip"), url = url) + class(mockResponse) <- "response" + + fileInfos <- mockGetFile(connection_cloud, url, mockResponse, eyetracking_fileName) + + expected_path <- paste0("ProgramData/iMotions/Lab_NG/Data/RRRock The R/Signals/", + "20e73a6c-f2ae-4146-90f7-1430bbc9857b/ET_Eyetracker.csv") + + expect_identical(fileInfos$file_path, file.path(gsub("\\\\", "/", fileInfos$tmp_dir), expected_path), + "wrong file found") + + expect_true(file.exists(fileInfos$file_path), info = "file should exists") + unlink(c(fileInfos$file_path, file.path(fileInfos$tmp_dir, "ProgramData")), recursive = TRUE) + expect_false(file.exists(fileInfos$file_path), info = "file should have been deleted") + + # Case with slideEvents data (csv file) + events_fileName <- sensors_cloud[2, ]$fileName + url <- "file://../data/Native_SlideEvents_cloud.csv" + mockResponse <- list(headers = list("content-type" = "application/octet-stream"), url = url) + class(mockResponse) <- "response" + + fileInfos <- mockGetFile(connection_cloud, url, mockResponse, events_fileName) + + expected_path <- file.path(fileInfos$tmp_dir, "Native_SlideEvents_cloud.csv") + expect_identical(fileInfos$file_path, expected_path, "wrong file found") + + expect_true(file.exists(fileInfos$file_path), info = "file should exists") + unlink(c(fileInfos$file_path, file.path(fileInfos$tmp_dir, "ProgramData")), recursive = TRUE) + expect_false(file.exists(fileInfos$file_path), info = "file should have been deleted") +}) + +test_that("remote check - should call getHttr and download a file to an explicit local path", { + url <- "https://s3.test/respondent-aoi-metrics.csv" + expected_path <- tempfile("aoi-metrics-cache-", fileext = ".csv") + mockResponse <- list(headers = list("content-type" = "text/csv"), url = "file://../data/AOImetrics.csv") + class(mockResponse) <- "response" + + fileInfos <- mockGetFile(connection_cloud, url, mockResponse, localFilePath = expected_path) + + expect_identical(fileInfos$file_path, expected_path, "wrong file found") + + expect_true(file.exists(expected_path)) + unlink(expected_path) + expect_false(file.exists(expected_path)) +}) + +test_that("remote check - should call getHttr and reuse a file from an explicit local path", { + url <- "https://s3.test/respondent-aoi-metrics.csv" + expected_path <- "../data/AOImetrics.csv" + mockResponse <- list(headers = list("content-type" = "text/csv")) + class(mockResponse) <- "response" + + fileInfos <- mockGetFile(connection_cloud, url, mockResponse, localFilePath = expected_path) + + expect_identical(fileInfos$file_path, expected_path) + expect_true(file.exists(expected_path)) +}) + +# postHttr ============================================================================================================ +context("postHttr()") + +body <- '{"flowName":"flowName","sampleName":"TestSensor","fileName":"../data/testFile.csv"}' +class(body) <- "json" + +mockedPostHttr <- function(connection, url, mockResponse, body) { + config <- tokenHeaders(connection$token) + retryHttr_Stub <- mock(mockResponse) + + response <- mockr::with_mock(retryHttr = retryHttr_Stub, { + postHttr(connection, url, body, "Test API") + }) + + expect_args(retryHttr_Stub, 1, "Test API", "POST", url, body, config, jsonHeaders(), "json") + return(response) +} + +test_that("remote/local check - should work correctly", { + # Local connection + mockResponse$status_code <- 200 + class(mockResponse) <- "response" + response <- mockedPostHttr(connection, "url", mockResponse, body) + expect_identical(response, mockResponse, "response should not have been modified") + + # Remote connection + mockResponse$status_code <- 204 + class(mockResponse) <- "response" + response <- mockedPostHttr(connection_cloud, "url", mockResponse, body) + expect_identical(response, mockResponse, "response should not have been modified") +}) + +# postJSON ============================================================================================================ +context("postJSON()") + +mockedPostJSON <- function(connection, url, body, mockResponse = NULL) { + postHttr_Stub <- mock(mockResponse) + + fileInfos <- mockr::with_mock(postHttr = postHttr_Stub, { + postJSON(connection, url, body, "Test Upload API") + }) + + expect_args(postHttr_Stub, 1, connection, url, body, "Test Upload API") + return(fileInfos) +} + +test_that("local check - should upload a JSON file if a good request has been sent", { + # prepare mock results + mockData <- '{"filePath":"C:\\\\Users\\\\exampleUrls"}' + mockResponse <- list(status_code = 200, content = charToRaw(mockData)) + class(mockResponse) <- "response" + + # in case of good request - send back the url + expectedReturn <- list(filePath = "C:\\Users\\exampleUrls") + fileInfos <- mockedPostJSON(connection, "url", body, mockResponse) + expect_identical(fileInfos, expectedReturn, info = "wrong object returned") +}) + + +test_that("remote check - should get back correct information if asking for credential", { + # prepare mock results + expectedReturn <- fromJSON("../data/uploadCredential_cloud.json") + mockData <- as.character(toJSON(expectedReturn)) + mockResponse <- list(status_code = 200, content = charToRaw(mockData)) + class(mockResponse) <- "response" + + # in case of good request - send back the whole table + fileInfos <- mockedPostJSON(connection_cloud, "url", body, mockResponse) + expect_identical(fileInfos, expectedReturn, info = "wrong object returned") +}) + +# putHttr ============================================================================================================= +context("putHttr()") + +mockedPutHttr <- function(connection, url, mockResponse, fileName = NULL, reqBody = NULL, expectedBody = NULL, + expectedConfig = NULL) { + + retryHttr_Stub <- mock(mockResponse) + + mockr::with_mock(retryHttr = retryHttr_Stub, { + putHttr(connection, url, fileName, reqBody, "Test Put API") + }) + + expect_args(retryHttr_Stub, 1, "Test Put API", "PUT", url, body = expectedBody, config = expectedConfig) +} + +test_that("remote check - should work correctly", { + # With a file pushed + mockResponse$status_code <- 200 + class(mockResponse) <- "response" + config <- csvHeaders() + fileName <- "../data/testFile.csv" + expectedBody <- httr::upload_file(fileName) + mockedPutHttr(connection_cloud, "url", mockResponse, fileName, expectedBody = expectedBody, expectedConfig = config) + + # With no file pushed + mockResponse$status_code <- 204 + class(mockResponse) <- "response" + config <- tokenHeaders(connection_cloud$token) + mockedPutHttr(connection_cloud, "url", mockResponse, expectedConfig = config) + + # With a json object pushed + config <- c(tokenHeaders(connection_cloud$token), jsonHeaders()) + expectedBody <- "test" + mockedPutHttr(connection_cloud, "url", mockResponse, reqBody = "test", expectedBody = expectedBody, + expectedConfig = config) +}) diff --git a/imotionsApi/tests/testthat/test-uploadAOIMetrics.R b/imotionsApi/tests/testthat/test-uploadAOIMetrics.R index 274eac8..70f0423 100644 --- a/imotionsApi/tests/testthat/test-uploadAOIMetrics.R +++ b/imotionsApi/tests/testthat/test-uploadAOIMetrics.R @@ -1,5 +1,5 @@ -# privateUploadAoiMetrics ========================================================================================== -context("privateUploadAoiMetrics()") +# privateUploadToPresignedUrl ========================================================================================= +context("privateUploadToPresignedUrl()") library(mockery) @@ -8,7 +8,30 @@ study <- jsonlite::unserializeJSON(readLines("../data/imStudy.json")) study_cloud <- jsonlite::unserializeJSON(readLines("../data/imStudy_cloud.json")) study_cloud_local <- jsonlite::unserializeJSON(readLines("../data/imStudy_cloud_local.json")) +test_that("remote check - presigned uploads request should work as expected and upload metrics", { + uploadUrl <- "myurl" + fileName <- "metrics.csv" + postData <- '{"name":"AOI","fileName":"metrics.csv"}' + fileInfos <- list(presignedUrl = "presignedUrl") + postJSON_Stub <- mock(fileInfos) + putHttr_Stub <- mock() + + result <- mockr::with_mock(postJSON = postJSON_Stub, + putHttr = putHttr_Stub, { + privateUploadToPresignedUrl(study_cloud$connection, uploadUrl, postData, fileName, + "Getting upload credentials", "Uploading") + }) + + expect_identical(result, fileInfos) + expect_args(postJSON_Stub, 1, study_cloud$connection, uploadUrl, postData, message = "Getting upload credentials") + expect_args(putHttr_Stub, 1, study_cloud$connection, fileInfos$presignedUrl, fileName, message = "Uploading") +}) + +# privateUploadAoiMetrics ========================================================================================== +context("privateUploadAoiMetrics()") + respondent <- getRespondent(study, "09bd22e6-29b6-4a8a-8cc1-4780a5163e63") +respondent_cloud <- getRespondent(study_cloud, "6c74637b-6250-4d13-bbbc-c999d9c8a74b") AOI <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imAOI.json"))) AOI_cloud <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imAOI_cloud.json"))) @@ -18,44 +41,58 @@ AOIDetailsFile <- jsonlite::fromJSON("../data/AOIDetailsRespondent.json") metrics <- data.frame("metric1" = 2, "metric2" = 234, "metric3" = 1234) mockPrivateUploadAoiMetrics <- function(study, obj, AOI, metrics, AOIDetailsFile = NULL, expectedFilepath = NULL, - expectedDirectory = NULL, expectedEndpoint = NULL, expectedBody = NULL, - expectCallDetails = 0, expectCallsfwrite = 0, expectCallsDir = 0, - expectCallsPut = 0) { + expectedEndpoint = NULL, expectedBody = NULL, expectCallsfwrite = 0, + expectCallsPut = 0, expectCallsPresigned = 0, expectFileRemoved = FALSE) { privateGetAoiDetails_Stub <- mock(AOIDetailsFile) fwrite_Stub <- mock() - dir.create_Stub <- mock() + fwriteMock <- if (expectFileRemoved) function(...) data.table::fwrite(...) else fwrite_Stub getUploadAoiMetricsUrl_Stub <- mock("myurl") + fileInfos <- list(assetId = "assetId", presignedUrl = "presignedUrl") + privateUploadToPresignedUrl_Stub <- mock(fileInfos) putHttr_Stub <- mock() mockr::with_mock(privateGetAoiDetails = privateGetAoiDetails_Stub, - fwrite = fwrite_Stub, - dir.create = dir.create_Stub, + fwrite = fwriteMock, getUploadAoiMetricsUrl = getUploadAoiMetricsUrl_Stub, + privateUploadToPresignedUrl = privateUploadToPresignedUrl_Stub, putHttr = putHttr_Stub, { privateUploadAoiMetrics(study, obj, AOI, metrics) }) - expect_called(privateGetAoiDetails_Stub, expectCallDetails) + expect_called(privateGetAoiDetails_Stub, as.integer(inherits(obj, "imRespondent"))) expect_called(fwrite_Stub, expectCallsfwrite) - expect_called(dir.create_Stub, expectCallsDir) expect_called(getUploadAoiMetricsUrl_Stub, expectCallsPut) + expect_called(privateUploadToPresignedUrl_Stub, expectCallsPresigned) expect_called(putHttr_Stub, expectCallsPut) - if (expectCallDetails > 0) { - expect_args(privateGetAoiDetails_Stub, 1, study = study, imObject = AOI, respondent = obj) + if (!study$connection$localIM) { + expect_args(getUploadAoiMetricsUrl_Stub, 1, study, obj, AOI) } - if (expectCallsfwrite > 0) { - expect_args(fwrite_Stub, 1, x = metrics, file = expectedFilepath, col.names = TRUE, row.names = FALSE) - } + if (inherits(obj, "imRespondent")) { + expect_args(privateGetAoiDetails_Stub, 1, study = study, imObject = AOI, respondent = obj) - if (expectCallsDir > 0) { - expect_args(dir.create_Stub, 1, expectedDirectory, showWarnings = FALSE, recursive = TRUE) - } + if (expectCallsPresigned > 0) { + expectedFilepath <- mock_args(privateUploadToPresignedUrl_Stub)[[1]][[4]] + postData <- toJSON(list(name = AOI$name, fileName = expectedFilepath), null = "null") + endpoint <- paste0("respondent: ", obj$name, ", AOI: ", AOI$name) - if (expectCallsPut > 0) { - expect_args(getUploadAoiMetricsUrl_Stub, 1, study, obj, AOI) + expect_args(privateUploadToPresignedUrl_Stub, 1, study$connection, "myurl", postData, expectedFilepath, + "Getting presignedUrl to upload AOI metrics", paste("Uploading AOI metrics for", endpoint)) + + expect_args(putHttr_Stub, 1, study$connection, "myurl", reqBody = toJSON(fileInfos, null = "null"), + message = paste("Upload of AOI metrics for", endpoint, "confirmed")) + + if (expectFileRemoved) { + expect_false(file.exists(expectedFilepath), info = "temporary metrics file should have been deleted") + } + } + + if (expectCallsfwrite > 0) { + expect_args(fwrite_Stub, 1, x = metrics, file = expectedFilepath, col.names = TRUE, row.names = FALSE) + } + } else if (inherits(obj, "imSegment") && expectCallsPut > 0) { expect_args(putHttr_Stub, 1, study$connection, "myurl", expectedBody, expectedEndpoint) } } @@ -63,7 +100,7 @@ mockPrivateUploadAoiMetrics <- function(study, obj, AOI, metrics, AOIDetailsFile test_that("warning - AOI is not found for a specific respondent", { AOIDetailsFile <- jsonlite::fromJSON("../data/no_scenes_annotations_aoidetails.json") - expect_warning(mockPrivateUploadAoiMetrics(study, respondent, AOI, metrics, AOIDetailsFile, expectCallDetails = 1), + expect_warning(mockPrivateUploadAoiMetrics(study, respondent, AOI, metrics, AOIDetailsFile), "AOI New Aoi was not found for respondent Wendy", info = "no AOI defined for this respondent should throw a warning") }) @@ -72,33 +109,36 @@ test_that("local check - should call privateGetAoiDetails and fwrite for a speci expectedFilepath <- paste0(tools::file_path_sans_ext(AOIDetailsFile$fileId), "metrics.csv") mockPrivateUploadAoiMetrics(study, respondent, AOI, metrics, AOIDetailsFile, expectedFilepath, - expectCallDetails = 1, expectCallsfwrite = 1) + expectCallsfwrite = 1) }) -test_that("remote warning - should throw a warning and exit if no local path was set for a specific respondent", { - expect_warning(mockPrivateUploadAoiMetrics(study_cloud, respondent, AOI_cloud, metrics, AOIDetailsFile, - expectCallDetails = 1), fixed = TRUE, - "Please set a localPath when calling imConnection() to write back metrics locally.", - info = "no local path should throw a warning") - +test_that("remote check - should call privateUploadToPresignedUrl and putHttr for a specific respondent", { + mockPrivateUploadAoiMetrics(study_cloud, respondent_cloud, AOI_cloud, metrics, AOIDetailsFile = AOI_cloud, + expectCallsPut = 1, expectCallsPresigned = 1, expectFileRemoved = TRUE) }) -segment <- getSegments(study_cloud) +test_that("remote check - should remove cached metrics after uploading for a specific respondent", { + expectedTmpPath <- paste0("myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/", + "dd8a9342-f5c0-4a02-bf27-de68fc13f2bc/", respondent_cloud$id, "metrics.csv") -test_that("remote check - should call privateGetAoiDetails, dir.create and fwrite for a specific respondent", { - expectedDirectory <- "myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/dd8a9342-f5c0-4a02-bf27-de68fc13f2bc" - expectedFilepath <- paste0(expectedDirectory, "/", respondent$id, "metrics.csv") + dir.create(dirname(expectedTmpPath), recursive = TRUE) + file.create(expectedTmpPath) - mockPrivateUploadAoiMetrics(study_cloud_local, respondent, AOI_cloud, metrics, AOIDetailsFile, expectedFilepath, - expectedDirectory, expectCallDetails = 1, expectCallsfwrite = 1, expectCallsDir = 1) + mockPrivateUploadAoiMetrics(study_cloud_local, respondent_cloud, AOI_cloud, metrics, AOIDetailsFile = AOI_cloud, + expectCallsPut = 1, expectCallsPresigned = 1, expectFileRemoved = TRUE) + + expect_false(file.exists(expectedTmpPath), info = "cached metrics file should have been deleted") + unlink(study_cloud_local$connection$localPath, recursive = TRUE) }) +segment <- getSegments(study_cloud) + test_that("remote check - should call getUploadAoiMetricsUrl and putHttr for a specific segment", { expectedEndpoint <- "Updating AOI metrics for segment: All Respondents, AOI: El Manuel Area" expectedBody <- toJSON(fromJSON("../data/AOImetrics_cloud.json")) - mockPrivateUploadAoiMetrics(study_cloud, segment, AOI_cloud, metrics, expectedUrl, - expectedEndpoint = expectedEndpoint, expectedBody = expectedBody, expectCallsPut = 1) + mockPrivateUploadAoiMetrics(study_cloud, segment, AOI_cloud, metrics, expectedEndpoint = expectedEndpoint, + expectedBody = expectedBody, expectCallsPut = 1) }) # uploadAoiMetrics ========================================================================================== From cb7f7c0f2d10bd8ca54574b00eacab6f85a12fca Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Thu, 13 Aug 2026 12:44:34 +0200 Subject: [PATCH 2/9] Fix for sampleDescription missing in some cases --- imotionsApi/R/imotionsApi.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imotionsApi/R/imotionsApi.R b/imotionsApi/R/imotionsApi.R index 0932aae..c7ae2d2 100644 --- a/imotionsApi/R/imotionsApi.R +++ b/imotionsApi/R/imotionsApi.R @@ -969,7 +969,7 @@ getSensors <- function(study, target, stimulus = NULL) { } } else { for (i in seq_along(sensors)) { - signals[[i]] <- sensors[[i]]$sampleDescription$signals + signals[i] <- list(sensors[[i]]$sampleDescription$signals) sensors[[i]][c("id", "respondent", "sampleDescription")] <- NULL sensors[[i]]$sensorSpecific <- list() } From 0d2daef0e62b85ee7260f480b697edf59e3c4f02 Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Thu, 13 Aug 2026 14:09:05 +0200 Subject: [PATCH 3/9] Add sampleDescription to the upload signal request --- imotionsApi/R/imotionsApi.R | 6 ++++-- imotionsApi/man/privateCreatePostRequest.Rd | 11 ++++++++++- imotionsApi/tests/testthat/test-uploadSensorData.R | 4 +++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/imotionsApi/R/imotionsApi.R b/imotionsApi/R/imotionsApi.R index c7ae2d2..a4bd59d 100644 --- a/imotionsApi/R/imotionsApi.R +++ b/imotionsApi/R/imotionsApi.R @@ -2247,7 +2247,7 @@ privateUpload <- function(params, study, data, target, sampleName, scriptName, m # Prepare the http request if (inherits(data, "imSignals")) { uploadUrl <- getUploadSensorDataUrl(study, target, stimulus) - postData <- privateCreatePostRequest(params, study, sampleName, tempFileName, overwrite) + postData <- privateCreatePostRequest(params, study, sampleName, tempFileName, overwrite, names(data)) endpoint_data <- "sensor data" } else if (inherits(data, "imEvents")) { uploadUrl <- getUploadEventsUrl(study, target) @@ -2356,9 +2356,10 @@ privateSaveToFile <- function(params, study, data, sampleName, scriptName, metad #' Create headers specific to the data format (signals, events, exports). #' #' @inheritParams privateUpload +#' @param data_names Optional names of the data columns included in an online sensor upload. #' #' @keywords internal -privateCreatePostRequest <- function(params, study, sampleName, fileName, overwrite = TRUE) { +privateCreatePostRequest <- function(params, study, sampleName, fileName, overwrite = TRUE, data_names = NULL) { postRequest <- list(params$flowName, sampleName, fileName) if (study$connection$localIM) { @@ -2366,6 +2367,7 @@ privateCreatePostRequest <- function(params, study, sampleName, fileName, overwr names(postRequest) <- c("flowName", "sampleName", "fileName", "overwrite") } else { names(postRequest) <- c("instance", "name", "fileName") + if (!is.null(data_names)) postRequest$sampleDescription <- data_names } postRequest <- toJSON(postRequest, null = "null") diff --git a/imotionsApi/man/privateCreatePostRequest.Rd b/imotionsApi/man/privateCreatePostRequest.Rd index b2f5736..2cc600b 100644 --- a/imotionsApi/man/privateCreatePostRequest.Rd +++ b/imotionsApi/man/privateCreatePostRequest.Rd @@ -4,7 +4,14 @@ \alias{privateCreatePostRequest} \title{Create headers specific to the data format (signals, events, exports).} \usage{ -privateCreatePostRequest(params, study, sampleName, fileName, overwrite = TRUE) +privateCreatePostRequest( + params, + study, + sampleName, + fileName, + overwrite = TRUE, + data_names = NULL +) } \arguments{ \item{params}{The list of parameters provided to the script - specific parameters value will be stored as metadata.} @@ -16,6 +23,8 @@ this should correspond to the name of the file created (a path can also be provi \item{overwrite}{Optional - a boolean indicating if the new sensor/metrics/events/export should overwrite sensor/metrics/events/export generated for the same flowName. By default, they are overwritten.} + +\item{data_names}{Optional names of the data columns included in an online sensor upload.} } \description{ Create headers specific to the data format (signals, events, exports). diff --git a/imotionsApi/tests/testthat/test-uploadSensorData.R b/imotionsApi/tests/testthat/test-uploadSensorData.R index 100daf8..00425b9 100644 --- a/imotionsApi/tests/testthat/test-uploadSensorData.R +++ b/imotionsApi/tests/testthat/test-uploadSensorData.R @@ -435,7 +435,9 @@ test_that("remote check - upload signals to a given respondent/segment", { # upload to a respondent data <- checkDataFormat(data) params$reportRunId <- "1234" - expectedBody <- '{"instance":"flowName","name":"TestSensor","fileName":"../data/testFile.csv"}' + expectedBody <- paste0('{"instance":"flowName","name":"TestSensor","fileName":"../data/testFile.csv",', + '"sampleDescription":["Timestamp","Thresholded value"]}') + expectedEndpoint <- "Getting presignedUrl to upload data" res <- mockedPrivateUpload(params, study_cloud, data, respondent, expectedBody, expectedEndpoint, sampleName, From a7915093d3592f55c34b5ab0b1614b94e16c545f Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Mon, 24 Aug 2026 15:40:03 +0200 Subject: [PATCH 4/9] Add automation for documentation --- .github/workflows/workflow.yml | 90 +++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index f69fe11..1b9742e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -32,6 +32,9 @@ jobs: r-version: '4.4.0' use-public-rspm: true + - uses: r-lib/actions/setup-tinytex@v2 + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + - uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::rcmdcheck, any::lintr @@ -68,6 +71,22 @@ jobs: sed -i "/^Version:/c\Version: ${{ steps.get_version.outputs.VERSION }}" imotionsApi/DESCRIPTION shell: bash + - name: Generate PDF manual + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + run: | + R CMD Rd2pdf . --output=../documentation/imotionsApi_${{ steps.get_version.outputs.VERSION }}.pdf --force --no-preview + shell: bash + working-directory: './imotionsApi' + + - name: Upload PDF manual for documentation branch + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + uses: actions/upload-artifact@v4 + with: + name: release-documentation-${{ github.ref_name }} + path: ./documentation/imotionsApi_${{ steps.get_version.outputs.VERSION }}.pdf + if-no-files-found: error + retention-days: 1 + - uses: r-lib/actions/check-r-package@v2 with: error-on: '"error"' @@ -93,9 +112,78 @@ jobs: files: | LICENSE ./imotionsApi/check/*.tar.gz + ./documentation/imotionsApi_${{ steps.get_version.outputs.VERSION }}.pdf - name: Publish test results uses: EnricoMi/publish-unit-test-result-action/windows@v2 if: always() with: - files: './imotionsApi/check/imotionsApi.Rcheck/tests/test-results-tests.xml' \ No newline at end of file + files: './imotionsApi/check/imotionsApi.Rcheck/tests/test-results-tests.xml' + + update-tagged-branch: + needs: R-test-lint-build-release + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + runs-on: windows-latest + concurrency: + group: tagged-branch-documentation + cancel-in-progress: false + + steps: + - name: Find branch tagged at this commit + id: target_branch + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $headers = @{ + Accept = 'application/vnd.github+json' + Authorization = "Bearer $env:GH_TOKEN" + 'X-GitHub-Api-Version' = '2022-11-28' + } + $uri = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/commits/$env:GITHUB_SHA/branches-where-head" + $response = Invoke-RestMethod -Uri $uri -Headers $headers + $defaultBranch = '${{ github.event.repository.default_branch }}' + $branches = @($response | Where-Object { $_.name -ne $defaultBranch } | ForEach-Object { $_.name }) + if ($branches.Count -eq 0) { + throw "No non-default branch has tagged commit $env:GITHUB_SHA as its head." + } + if ($branches.Count -gt 1) { + throw "Multiple non-default branches have tagged commit $env:GITHUB_SHA as their head: $($branches -join ', ')" + } + "branch=$($branches[0])" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.target_branch.outputs.branch }} + fetch-depth: 0 + + - name: Download PDF manual + uses: actions/download-artifact@v4 + with: + name: release-documentation-${{ github.ref_name }} + path: ./release-documentation + + - name: Prepare latest release documentation + shell: pwsh + run: | + $version = '${{ github.ref_name }}'.Substring(1) + Get-ChildItem -LiteralPath './documentation' -Filter 'imotionsApi_*.pdf' -File | Remove-Item -Force + Move-Item -LiteralPath "./release-documentation/imotionsApi_$version.pdf" -Destination './documentation/' + $description = Get-Content -Raw -LiteralPath './imotionsApi/DESCRIPTION' + $description = $description -replace '(?m)^Version:.*$', "Version: $version-0000000000" + Set-Content -NoNewline -LiteralPath './imotionsApi/DESCRIPTION' -Value $description + + - name: Commit release documentation to tagged branch + shell: pwsh + run: | + $branch = '${{ steps.target_branch.outputs.branch }}' + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add --all -- documentation imotionsApi/DESCRIPTION + git diff --cached --quiet + if ($LASTEXITCODE -eq 0) { + Write-Output 'Release documentation is already up to date.' + exit 0 + } + git commit -m "Update release documentation for ${{ github.ref_name }}" + git push origin "HEAD:$branch" From 99f5bcc452428bd0e0c109ad68ee408fe6b79e25 Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Mon, 24 Aug 2026 15:54:42 +0200 Subject: [PATCH 5/9] lintr --- imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R index 11f968d..b874a0b 100644 --- a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R +++ b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R @@ -207,5 +207,3 @@ test_that("remote check - should call getJSON, getFile and fread when metrics ar expect_equal(ncol(metrics), 37, infos = "no column should be lost") expect_s3_class(metrics, "imAOIMetrics") }) - - From 177553978e6ed462e088ee78e02aac0864d32ee6 Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Mon, 24 Aug 2026 16:03:07 +0200 Subject: [PATCH 6/9] lintr fix --- imotionsApi/R/imotionsApi.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/imotionsApi/R/imotionsApi.R b/imotionsApi/R/imotionsApi.R index a4bd59d..9657a4b 100644 --- a/imotionsApi/R/imotionsApi.R +++ b/imotionsApi/R/imotionsApi.R @@ -1399,11 +1399,11 @@ convertRecordingTsToIntervals <- function(recordingTs, intervals, keepTs = FALSE } # Converting timestamps to intervals fragments ranges - invisible(lapply(seq_len(nrow(intervals)), function(x) { + for (x in seq_len(nrow(intervals))) { isIn <- timestamps %inrange% intervals[x, c("fragments.start", "fragments.end")] - timestamps[isIn] <<- timestamps[isIn] - intervals[x, ]$fragments.start + + timestamps[isIn] <- timestamps[isIn] - intervals[x, ]$fragments.start + sum(intervals[0:(x - 1), ]$fragments.duration) - })) + } if (inherits(recordingTs, "data.frame")) { recordingTs$Timestamp <- timestamps From 7701267df7574b24b079ddd4933fe4cf12f1148d Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Mon, 24 Aug 2026 16:23:35 +0200 Subject: [PATCH 7/9] Try to fix pdf generation --- .github/workflows/workflow.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 1b9742e..d3d1d9a 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -35,6 +35,12 @@ jobs: - uses: r-lib/actions/setup-tinytex@v2 if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + - name: Install PDF manual dependencies + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} + run: | + tlmgr update --self + tlmgr install makeindex + - uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::rcmdcheck, any::lintr From f1eefd23443bdde65b01a741bbae51637e5b863e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:30:48 +0000 Subject: [PATCH 8/9] Update release documentation for v2.9.4 --- documentation/imotionsApi_2.9.4.pdf | Bin 129922 -> 128940 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/imotionsApi_2.9.4.pdf b/documentation/imotionsApi_2.9.4.pdf index d1364278ea25b0ef7fe3fa65547f06027a426440..9525d0885511a53152ae1a6ee569894c90eb7904 100644 GIT binary patch delta 42584 zcmV)sK$yRR_Xn)+2M8rlL`E$!E;o?~90WKsG$4}!Mks&fR$XtKFcf|7uRz+vh*oZl z!F){Gq<&7B+NnxuU%E-7;8L#yka4o*-_MRonxrIw=c*5OliYLeJ@?qY$jA(2d^!xY*GqV7WK&h z6SHC;2fu%!?-D9BY#TyCu!8as*vK_3>_T9N8tG)3O>3b+uzTEW5cU+nDz8OSA%q_G z6p&?m9@v2|H$D&z6b*dWoL6)^E?kRyun0mbd9qA?4+)a!1Of*y@$JB5BJV)JuC*4r zn6X}R_Q+#C@50!Kd@n^tCgrunI!uZo1h+>$=dpk2P>Sk=g?tMIeV}Qba8`*4or#WZEKh91eDmDQZH>{;~aYj^Z3me8W^ zAoTSp(j|P-%TI)G-W;y0(b>h1+lwBw7e!L0tjG@Y-bI&I(8HAHw4y~?9|j&T>mcas z7rlRX!388guzOT8np7nh9r*s#Wsx&M-_qI;K%i)k6^bEmpAf05V{tKQmS-V&Tc8X6 zYarT;Nn;U%h#&J-N?C)uGc@MX^zxqk+Y(7Y-jX{b1xx3xnL2GjUXy8+w+GpKa-2}{ zT&8vFch!{hRzL)JCtxkObYyE7*;6skW9fh6*5sW-Ub0rbUUSs#G|On+#Zp54OgzWp zV5J+DYDAxu$xBPI`Kt z&C7l_zS@XE;L8;mT>jf^#|p^}-Uu&`V^x114wpj(B;{mpyUb(#br*^@Pb>*1b69`p z9DZmXl+8kVnC*Vx_$JE=Dg-^9vOJ~yubb~YIOLtn>a@zblGpZ#^tvNWOaLQWi@f>7Ttr9z0SICY3zG!|DFHK+5fLeq zSOgG%0|GjooprZKCY@&5jop)*g960S1OtmiTYvjX2q(7J?Ly-A5?w3_izn uMj9 zptE$Kb3S=9pPZc0#7RIxF?Hs*4uTMnfI1O{ASBqCXU@XK-u3+5$yr3VIxc*IF-ly% zswA&UF|D~@v-F-VbKQFa`yuw^lL*9N?D#l;(wB_*w8@#wPrubwBR!u4$OWDsLTHQa zB1jTdY@>zh7lPPnbkkI?&L>JUh!E%odwor*etd)hjzhzKnagR#mGmgKoCoTlZw*&P zI6@Uyxu~l$%S-u|N#^+!y*$MXB#V~6kN#n70dEWD}(34n*S3FJ|4_^i?V&i9?{x| zX+Sn@nKGHr__Ab$c(dM%h|w1jO&CTMCoUQm18d7dc@XPAj^s`7ut->2YwFW~ia#ET zLKs=dzYMbZ0vBNWb{vTsjX-_+MTZwna51uHpl7`lF&+Wl8u+oyos6Jv!@18weK@jp zaz^8=3@-m6g$sj#Mh5>yndRU5yAO^KKm?*#T`&n{yGPp7`Q+aT>iVOH3^WoY07B}d ztI6UTI+^V-3#{PUlg!|gP;V0#e=K|llK5Q;#Ig3Sr~ z|5DkaXeR>>#Q}8rrMXLggNN>wDW)GUOhw7@Fm&&khoHOEKSt)^B?k$hC>*n7lff4s z$lxO=0!jaLyv;@0@Vbk~{6NJ!Nq|bbC;=Gu33^krmbBn~t^ss!tHxA+SeoWS@N#LI zX6MxPXsVT|$-CTCc2T`&H>R=?wfpynvfyQZV_h(*MqB}th`4KydbT(QkVKosO2*ak z;|UA>B-DTe5?5CDov!p`bo(<5;ceBHnQmfNhqd%&W{M_Pmeb!UbP(nbFAQB_`d*pq zQ*}aG3*O7CwQY4Zy?9-J-mFdchA;1=uTZD=9n`MRX1Is)Q5JT2e^p~vYwkQFQmXHD zFREMlRWF7$xmpJ5byAs9NY==Mnp{_H^QhB7 zn|_#WB`w@nYJq6WKDs_T&-0?2=xfo|I*IhJ&65n$YW3#N{P_%m zpU>c<-sZ8Y=(T#uL|f=D54y3~8|M0;PLxCX4!6zL=abP0M*%jIk)JBFLkW}xf5czb ztZt{N=Wkc0y>a|zGgUAu`+c>XPjiTd$@}4x$S(5x`tclkJgdF`VOD$h+*$3~SxlSO z#10{*Wyu&M=c(;c+}!ii$^UUwRqIxQHw_{ zo^SDdi|5V2YV_oc4pvJFw#(??e~=AEAI@ku{;UdEdUQ-%4kk)v{b3ID0vymL<+dC& z$w2y5Ir!wH*F7B+76w0{gMR@I(O|RM>fn=-Ne`@W;RQGZC6(Li;6ub@AJ8Gb00#=v zyPgi-BQ^v1kjCVorMAL5k}mXQAcv_rAcI!pbFfyL)RO@_igE)oXgxj$f1{;RJsk`x z6&ljPjL$(u8B9+H9S|G+svIbJOwFDSim+@B>0rm_5S`?m>^lca2FvD<4sLu7LCfH` zI#5)YnnOC!_#6n;bL{EhBVwapmxBvZr=AYvF*k>F@Z)o^C>nK72ZtKn59tub=U|M) zny}@7lE#H09pd;LG#Al5e;v4_N%~bexD;e>sivl)8IocAEz99B8N!x>%}_CE#@{uaOH%cUNgS0iGlz5-f5&nbMU&kgmmOC$ zaY%>pcPwX|bp3olMWcs`Ni+VA<iY)`~7rpLz3q4 z>Hg@>{|8c0JVcYx2osa~5EcS5HG|cdwys{A+rO|H5R$CG_$I-d}iKTPGIu7`NFfR&PBOe$b}U%v$`r9UVq

dgLz@h&;C4Ww#uKuTR31t=sQLbd0@+9{@jo9M(2#qm!2P^Xu!?2 zlh^^zz!*5h?Y+)M;0W;@d1h^Sk%_!e|O zRq1ifiA-pR(e^jS+}d)$%!W2@Yec6Mw^kcoHUq7~PAjdvZB0Dpn3q-%n4^2B6apD( zgbN?vFH0L3B3;~*iMK5&`UVyl#VHI^?d z4}8^g9T<0u-bsss0uwfFc=N!yc0sB~(JM@yQy>JUywu)%5Jl15r}0j?ecNbFT1uEx7M--Dr|7skjoJs-jm#N5st1?@$%To9Qw((+_l(|RMoZf4 zXB)1`HHa}75Y)`z31u@<5c&-m(y|X6VeFdoEG+;bAfeu=41bP7fN6J_#$q!Us}cbo zSVU%Z^EcEfnx}fN=1p6dn8~=h*=Z-h4?Kh=Ur!H}e7v#Z&77CAsz0FOCq1ZCs7Vl- zX*cOj!D;;u%|UD$q@vEXS|>w_jT+6e3^GV(#j#MB?48zYfW(oiIJTyX z2Sin|xf-Uq>whUzpPqdZs#+i->({_JnUA;n;LwBZv^yQ)DTJD;X-FcE#=)u=ZW@+e z_tJO+Uiu4{+p5eVh*#G|lhL(6={D@WuK4L>Kf_al5_@K>CeH&s<| zh3Rpk(pWt&%w)JG}`uzpx`#BW+CJb$npv#TPoHLN1AQMHW}Ij2P@ zaZp8$J##O1JkVUOtG3W{tQMXd?@J5qwq>@4D$jGkWT=-I6pAY6q*YsJLbQO>H0>t6 z!ZQhc*Sxx=eG<`mcbvUKQr{yvvIfvszy}-WpE0jz=ctO~4UvOLX1 zn)k}i!MdIJ^t}Cw_m`EpU{=dt-@ZT)I|;)E(Nzs1E|i8-Qfk`AV>~24!dd?za19Nk z8fy$3no?`X+)-StoAAvWYCJwy4NdB{ycm25CjaA0 z@bTla>uJVpyU*g|JPRlnbD~0)Szo|Wmb;TmBD>v?nQqCnIzNeg>0BQZQ14;+l-`44 zp;VVztaIo~TdEe~8HUtnJTlW-_Y_(>dhe+Avg?PQ7lc)3EKBRT^u}_YL>_$H{0kFy z{sHWh(FhZhZWk8;Gm`;ED1Vh$OLN;c5WeeIaFiaXnUDtGl&3kkj?+meO{TGOQhcCE zNMeVt5~S_?`|e@^iqtD{JQ;w%VzJo$@a;m(*c#0EbN0t-cJ(1#8cTZ_x}mY!7!G5$ z=Z8iVGCS~GW0e_e(_cKS{<``Qg(E=^1Y+L@HBuyPdD8K_m!@tzlz*NvwZ+v3KQ>}m z7W%l%@_bjWv)l-E(dxYFc>9zTVqpc0nZH}q=4IXGbtP_^{Q7#!yRTd{b(Qg|`=}Ru zX8{vkpZ$1COWHw1tQ|G>074EL1qNBJV=tYUwtcg(JkK*hGj$SbD*j9zk#DP{p#F2- z?Wot~B^O;%HiEwMihtTRskWTDYxb5pX`9cC|sC0R@sU2+rb_{3XqRa&=MUTs%mRZqP|DwJ;q&;NqA-jKjum7$)` zb*Eh3S9;0pZZ-RF=D#eWTAvuPK8VAczVnNNAfGaE7Fn-CCA0^LrU2fc)vv@ZIxqCNqc#FV_e zK-8_0l54r5px&bzA?pFi8klHXK(^d~~5fxt8n*d7RM z00J9;z!VVJ9tgC0ANnEe+DkTNj!CF2^s^h-$Vk$c`8810h?a_X~4$U9vMm= z8l}DI!GD{iO-dlZ6FyO?WIUrTAI9b6lDhAjzI&gyJnicC$4Qw@UhvQ0(36w=8j^0+0MWPJ8vr5AusRu z*Q0*?7vin1gR-*aUEfyP!cAM3N;~}7vC(b~-TiuG+j_FaC8NyOoKfQH@)0oVtK)qRmoIgZFAyJ)yI>_2k3zU!CW?oDpMo^16EIBBQ$ zY^pPpoQINbWIxJRxzHycINnah6H>uz&@(X8&yVs&W?3wj-PkiNY;x$&JyZ z0)x>VPckAL_@ML*GM$1BjFr8lsYvY_(fL3I$O-cOGmSynt==GXZASa41PLSBsfXlF z$@&$T^QKARGU>XLIC-&)al!_x8*G7ofe0)P@Ftc!HGG)^nXajjDJs3s4^XIa8~7yi$==CIaQ_}w91^{@@(xq z9BmMf5~X~Kqkj2NN7mY4(B-N3zdg`2Qv95{@2Hutxaj8U;XnUdrbK5dBKUu(%(;f= z3R*H1cBjHEZ%|nOl%STJ^Uz`OtA7;n<5<FtcvVvw8xKz-@+Il!mFM z*`xWsZ~r>etGq!XyD!OSH*}{rH5GiSpMKLzt{$dG`ZTThZaak++#OB{&l|cGl-2w2 zWv1GL*&;s>sMK|l#FbnQ$pd`oir*>$~KpXUw$T`lxs z)i^`U?=gT1hR95BgYOyG~WH~5{sR{GO0Sl~kLcc20cVy8a`kJ|C zq36Ya_S{&DR@Vwc9oYRpP;5-d^Py=;(WM2X#5-f4WAU9B_@=G&&$56<$Pc?nmX>{k zj?0aSBWk2)E4_*h*%MgLtTllyeZkt&V3qV=d0|il+u5a|M5fb&gF~C3x48LlC-v;C zY{|P4*toWL*+}f6(_{>edLT9_#cGk!TD9MQ!v^TgLu)}A!o{1i=sRB*EY%avA4Uh?^to}AF?!wyV!?I-g$J=B?H@=#Fa zFwK&3ajl^w$$ZG*Aj#)fg;dEgCT1##5$9)G+~T`K4CbnKAEa*z@UA$$Lgu?v_Eflk zJ;MEvF~SQy=T%=vvAxq*qvWi*r?rxQMRsK`mAObXp4emtN>(5}D|L}HkVe-d)0po~ zXDNsj0LF1`2Gw9R|E|W?y2uveDtlm51Dws_@drbZF!lqvaM>3pk1Vh8_eY=>8YE4Gktp8 zkh-S`2x|w%!LLRj)%QA<@Y%tdf)x8)OlwXH>A5&DtpqlGD}jRwVK0vDb2)X+FR#z^ zJzeP)K5lvvvNV80q2oX+;rb4kH##;A8#ZDG!JUgXEA^$(V#}zCW4Kv5J|vnUDN7C7 zr^nJspzBEt5A-eCT+#MBCZ5fI=~uH;y>a*?qp>u5{Y9sSQK?&9J{w>y0r=GDZ z^_NF4<_6J!Ky>psskY+TiNY77T(?4!{s*xqE^rEEZe(+Ga%Ev{3T1AS-UtShwj&h- zF*Y}o0Y)g3_9G8}hyvfFr#+~(>`^DFr?%3I;sZ@W7FrTi3DA!F>$`Y}BK5Q;$)VFq z2QGlcw+}3K7sz-tka0cuX*L*Lg^7{aN$7^gY+*PE*`6O7QHboobB$SU%+1icpZzkr zioy=#eCQ$1L*^sq)x_BJK#VMp~nuBX&z+doOqwSntK=9pR_ADN`QlRM;~diVopT+u00-x~9#rZ`ul zMO~G>jlu_iYa0)#2uI@hb{Kft)SKyX1;f6AV=D+u*mI7G3LpMyqB1#q}U&}+bN(QuCpZuJL143^rqn- ztA@#OXy$OgY@R+8dVML)hlNy)r%WQ#S{Yw*U_ZKREk%N0|^;#(n8TQ%L98gfa0sP zAe?4@%Y7zDv&9v*fe*}{E>vXEN`2RE0dnIO8Hd^}DpG45!+N*WP7uA@+XJB3as%_p zasm_U-CzpYOupZOT8D!WqcL!~;xAShoQTs%nfkkxxMo^3_?$?M5}me$dlIuCj4SGy z@Fh?#Qo(zj{*6c7Fcv(7E~>grIjN|cSO`ad2|^to5Nw4r@);pFQ()R*qfQEud|6dK z5ZZwfk<~pn@lOk^^@>tT6~adZ|4*dZ=0JY^SC~_XbS&En1y-B%eF?hXg?VKBlm)rEPO`2&V3oprdG= zTD7O+QpAt#n9Uu%c+3FNzMZt;WVhFUk+swK9niyto9gJ|r(^F{G`0AN&_^{^8Aj^F zamn^sAk)mQ4cf4Go8|7_c)eF-g|uLY_O6%rX{=SA&|J8qdu!Sza55YGHE?7T;YOvTOO3)es|Z& z==NdPxlw5QUVM)0)XOX0^YU_im|MH(60@u(E1A^JZ{5DyF}r1MpHoiN^sj#4MaKi? zn>_NO|IQ=N{r{2o?|kI##2{n`qkjNIW}Bmv(FhZhyC@a`G?GRsf3;WLbDTC1f4{$i zN%{aYMi$@TX&-27d)#s3sqN(D;s@@K@6>|0f{5e)zN-&75px%3()ea9tyrygf7<;m zU~6Mx>vHky{o?c@iLDG}u^(IakCulq3c}b*VvM4|x9&@8X>SP68`?P!_kWyT#F?QU z;us~d55}ZoB2*Swe_IFL)X;SYW!F!{qT2Go$$Y!0dNSE+m@tp?QrPSvFH(DR#|`|% zE(=~DUaTsj2?h(3FB&E-P9|}3Qij~ItD>Pe_*cXp)Ar-P{s*tcu2a7~AQ5yp z)G&!q02TZVvC5bxL^2u+VuU?U#q>Mr)-B!1!v1#ly(El4RyeMgc_TbhcCP#7Z@3r29oxJx_M;v+%Uz zwC=0Eo8w+(5&=DfUsp_B%DU)?nz3fMNy+p2CaE4v@)2X7ky2lUthn9^WS&HhP$UNf0_hCXa*XP*dWl8IjBY$P0kT9%xasx z@HP4ykxT_ASL~+e7qlYNb1w4hyYtzNpGdoE#C?6KEtTDw5C>v82)k~Nc`K52)7O%v zkHLQc_Q>o>U%VJ)6W)k?css~-Ug#dBgdyi8XLM!~jcL~crzq?2L#fqntYLcl!nft* ze~4pFX-YZ}qqv0>4Q~==i5-qINFd4^U9ZY{SJ#&j`jBNmE_x^fbqxLA05H=1C^r!4{MZPf<-jvfLDxB7NQe?{q=QWFd=$wsVq+orF|IhI*zMjxWnz{@Tq zCKY9>wP~5_#zBhpcDUE^V_|=(M}c)maQ1mNu-(xl3;juwiPq_=Ay>nQq7Y0Jg*>7t z1rb!yGI-jl6`5;twri>QtokJW0VFy+2`KdP(VhY77|;x|pe8@k7@DP8cr*GB1Y2Fp z>XXq36O*nj6#_LllMxXqf6W+OZ`(%f-M>Oe3rqqcS*LFY>qE9SMTVipT5o$u^B_wn z`!-op_0cr@^&^ibSxRL$X@Ox4(Btv=O&*W3t!*`1d%gH+y;xoaD{W=10w>VcnZ|6} za=kzc1KaXlM_Z@bjUE}d>tB{vVK5?eeaG?vw25TP+jqRF$|B`Oe~aV`Tj>A=(TLCN z8BHfdB(cBbZ7lL;!Z;Ezu`tq17Fi%va$fJNvTkEgYOQsW?vw z>?%dQ;{+t-zUtbz6=gw)GNW~C_SnDdJIOn0T@-GfpJc|Dz4)_ zZ+T7IEiZT-xBR+z+qG9BN1G9!^xPXxD=*?rj*-C42ivzdWxIo&ztRW83{BcL4?XW z@Z593b3%TwZ6{X}?nIs+Wa8R;u2g1atwhDij!-H!gvQk3+U~?`2Kpc@M{b)ATVauG zM4^g=5CxQNA*%uRq5$3dp1{Gtp|i4v>U1C*ties3w1P4Y|4G(m%D+S^;v${1xG&OW zl8fYT$bsWJf74n;geV}`uI=hr997)z6d|Tat~=gv0t;D$clkYSM*((Rrm~^9DIExH z6m!#-HIbrDVgcErCR9?^DzIqTj5mkyM7~~b{^CgshI=f~uQNmjP!$kcijByzT%cN< zl{a_*d}L$qCGaRa;i#3hhEOVM5H7t0ij89J8fiU1e;ajzcbGa8{8^D!V`C!l9rh=0 z+~95`EA+$5QXE?1%eajnw#9K*dk}-HD-xCVAc811Ess;$VEH;h2(@LZyikVWNcI{8 zRE99$fvc~v&`FS5p>?z(>GHUi&I1OV&bpi=3#B%R8zm3!Lo2FW?GB9&spDFqQ*O?s z6KyGEe?)zU?xy3r%2dEK(8CDT-#fILh58j<9U^zY**!G$o~}y;Z_a5qZg)sY=DlVF?(-#h3C?~M&&?ELg zj1}0Gcl;Zkx66VwioJ^ZucA>#(A6Lq+jt5ke`d9K9k8(y{!U!;7t)HR6zTAPeiDtQR-%e9bUQSw7^3%Yi5NyaAR0B z&FX8evMS3DflFC>Qoc&0Yz4jV_j4v5c#hcyz4nAr;p2@;c_w7H^7~0b5-I~Lo`_5& zy}1vjxNo=k{DDNddof0e8>WbEzbEETe{gSR==*L5#}n1a#Zd|V=WfnUdw4LO_KpU? z^_UER>p5Qy0Md??;Qj@%J`4aJ-KupolLO_8F;X0ePesdhU5XY0GFk{I2LKwPg&Ufl zk&u={wB&9sT6i#-jU{`b1j59h@J6Xpb0C89;Z_Ey;pl%J<8x^vb8X9JuCg@he?Nb} z{P-Fb_Ik1njRMqhQq95q-^9=Z}Q+mYRfa%X@iD}6M&r<@=i1LiS(wbe_^(VXd z6DMfH<(C!mrzuy*G!`Hvn=b-5Dl1euIkJaLN#!{|btzvJXtzCn%zM`tuhxr?3nnLL zHvG;2Uj&xtg<7&-+}zq)3Wp%ve{xrmb|*>qng^p;=(zyr+WWVUk!~% z!eP6X3zj}19P}GbH1it{3j>Qqe*X(glhmT>+u2jcP$TpvUtG%f@$7&IH~C`&0vkn< zm0gjJX3?j7M)~$MS*aKb}YlhFth zlSVWZ0x>p|0fQ-j?N>{4qec+E>sL_T9H1&1&42*mN)Afa-rB9Lq}JBv;`o4JER%>q zfx%w=`*hC?Pb1lz@-fn3-u>?W8ftAVYJHgfvY9O|!@!D26u5!4xwjal#9Ibd7*OJS zuC+<68+*&;6|eS%NV$+7h2-U<^;%VnPqTUm1k%SO^l*6Cr)cqpCkx}m)m&fN8>o#of+ zg<2GMfAK`>d0gqS=4f9GCt%b^rV>~=M z%(po)G11pxj(+keVZK)34<~XVd}==F>yaK~A9udH8VTNa;Gfi|X)I$R3P&lu_5J7?}wYX4S9 zJt=E(j)YY}P!cr(0V)a5J1c~~_tRXDSzh2~wccmv=UM)1dkt-K_Rajxlx-%+VlHK% z%5j>ztdiII7LrW*433gKoD@>!%8=NP`l1o%hb+Bex5pTCPCO;|pq?qwE5r5$qOYad zk~Fe^_r!WGzspDjz90DfLZnEe@A~G+>|!(fXU0_BVQsx$hAZOI&`R>z%`LT3I0WLv zi&oZ?x|&;1lu5YsV7{}iXJ3wnr2>Uvl?7`AOJ*$r6jmNP3JL}gdI27W12Qg&Rt$^_ zqi?eL1DB&j#Hboosx0TdWK?|GB+v4O`Db{4*d_XXQJPY&hi&%AP30&ZFSJ|P$qB1^ zK7oY)4x=jL4QfP7i!#DIPiy_2S4o-eRnGp061aKDzye+n*p2oYk3TEZc|mA5=Sh+8 ziwf}zchR7@TY*LEUACwaE_k(wg(zgKV%bpbV?1%@K|t+GtnO%2yijt8eXfb>qz-(4 z8-m@7;EvcG+BAaL_WdUL6;lT^p+gROD=xX7RP}yeloEwj`{o>l$59mc>mv5E9ufsV*D4z7>dchLvuljS zcC~IpvcsqOgjmhtOP2e;!+cGLxq1e8Clz(Wfug6{vA5H92;OhkR;xHY?boiIjv^D+ zZnV-c1v{f6gGZ`KhNE$9(Wk1nX63|RCE+v|T zMm86znWc3Zo1PJud%zSSn+Kg4;YfWA0YR|gRMir3({eOT2#=EoJ!>dw`0Sx4K@U-T z-G@{C&5`T6qf?SJmQ|wYN0u=MTEqlZ(^gMgPfg)cx4K z>)zl2WIi$_Pq4a3)teh@4Zd0svHgV$B^Am#3rQ^gb0A;$1I)wZ^f3xYC$X@fkiulD*N2u*H&~xfUc=|w{DFS^!$3sK!8Hn-@tI~jdZ?Y6ZLC@cZE4ivTaaUy5L3App^pmGCy z6;v`awoKZOFJdVLBZM6)GzL>?y&{!$;AQMJCnb3EgUy4H+{NnS&a^TU8($ zeV?S))bFa~;LWUNAKSK=vsF_ zN$c&{D2ptANXnwE+o&UuYI@3F3JGLyP%uAVAz@b~c3~ThKxDV~*^Ax6$KW&N2MdYW z==}0*w8^3do(K;r5w?ITIESO%*ITAo^CQEMu5mG;>s(B%0xPyLb0b&+!ceYT0NWKc($T~a;N+KaIf`mX}F40FaFLOKdma$)D-YT#LMzE-DU=gU38zm>g zk;l3Pa4f~Ix;|WqW%6{JrQiJEE%^@diae2cc~OLvtCkimZ{q+)-VcxD>jO9==cQ7f z@g`4J=Uz^#KH$_7w2@6A_4VOXZ;KIOIX{W^i^@`d5+3+PW%K#TqBcp3%=)S}NeHA_ z=#inRXnSZ{#(p(4tpZz8aOdXmUT^|9q&Bc!S@CjF@E@v4_>+^YKqvw+IFk_(DSx$B z$#Ua15WVXwSehKjsv!j~;Nr?0l*D#<$`w~enz<-GP$DED!!2CA{Cxm+Q=%k_BiF?C z0YtmE*J!+kiLz0May$BMKAPOPff5*jYq`pNt(b%uw&NtAA3ej|c`45&Vvwj)uDBNg0@5zl2F1vqQsk>*U7q z6<-*;PLr~)=w@Iz)B#@~K4>~6MEx_X;G?M1E9qDm>n1S*8i3*|OH;P0c$R|RuR5$e z$)vEH=KSi4C%^C1{w9=+U+`xvc4nX3vZ^C4pq%#^^Rk+OmUOx51~}{Tb$>8P>F)K= z*+)>sbbjH{!hAdl;6s3-b9U#$93HZaR$|!Rta)0o;wg+Tc6rwt>|7&;PI?OyWd~R+ z(+Et&g2#W$@c6Sd#@ba|*4Bh4b<8oJU7EufY};-y$n}jFq{%S`DSO1Vdd(SpeS&2? zYcj0M@mZ7z^E!=Wq;9jfCV!*r`RLz~DJ+R%?rt`vp5YQlStX;zBT*vpN8#MC17CS= zR1?K9O~Z3+@f|C((O+jTVnZKJ_i~$}cCKS8)D8@fx)*SR=XJ~%x??+dbQTq0G}}|V ztto};LUX8^C9n%&fg3`pY78J3j3dy$1vwC;-$Rg2KqLbJ!arEKDt~z1P)Q$y9h0nJ zEQ<)n0_$6rvVyO~$-C}xu^QRBPLZZ;*HbIaRJUP;oAA445|yE5h-)4eVZuZwq8JvW z5&4-d{}C(!KEgCYVS^xy^f*}k| zckMu(mLNoJxuby^7Jm%HF;4+U1p6vO1=yB&8{~EIJ(Xhwj!skA{8By7%91Z*)^Q@% zyvSr~Pds9vw#=$6=D6;IC3qq+VB{T+E_raaDLNe4;cFRg*yS_pYzLeYYm=7Lw+~Z8 zM(Ihqtr5_`C!B#_@9%rHrw@0RDV7Y6bWj=Z!FR^;J&m{q%0>pV z5<~^w#joKj#YSmAznmnuI0!M66!P2|kn&hO!za#BYYf7)JpuCpDFCw9Vd-LTo9zAI zi_0*>`TX-0uz&aMJ_z2mehePD>z%PrGhY6Gz}xFP2;R8>{H#mw?|b-=^3N85HnV<^ zmIeZNt`Yc_aAOH>GQrKhLN;|}g>2CeQ`jxDEM{Q}8=gjd6;?dmfX?%liIGykANTJ- z*gt6&RrgFOMddcD;|Oe*3}i4K^Qw<_<-p);QFPC1-ha5_pd>+gE(};H4mFElC?3mD zl4!+jwgaNx?M&rBm1f)KK+#+C5wW#QAgtpG1mG*K8CYcK<3{rwRWkfX*u&#nYV2H% zH$_(G6D5<@MJEI~IW{1Z z0Y)uQ8nc%~?lcW26hwn-*Ew9|z4LOD3L0JX-ZUb6iAO4hRx1VhpxNe){0 z-u_8`B4~~Xy6Alz_fF0|7X>(QO&5)4h!TP=+eMyBuw$8M9HU8p&dO_MP^Uj%va~d& z<1Z)YZlFoogksMZDr-2U64E1JL|}alI{@&1!1Uyv5;?mMm#c&o8-iWq$+_(#UkJN) z?Xu8CeeC$4>+M@(;1JUPnw4sjSLvx#EQAM?;J^tKFv`-DMJ3Nth5Im2BVT56vly-T z=_y}+YGJ>KL*CrO-y5-_yVMqC6>||vx6XtY<+Vadm62|sQ{}^6C#AZFO=a&D5Zrlx z$Kwf|b{pW!D&c!fNDe5Fv{Hb%O=UhPV>M5T1KI%9tqyz}eY-rMYLxNaYD|g)>TlBG zf&neUBmsV~VwI)w+GP%eC>LyJ78IPI} zWbvxv=uy1i(qFHJ`X--H08NiroZV4>vi6GFX5~?W1oay+0*{Xx1(3_qRudBtf)oCN0OuL_`uTdy^?avG`Fa$5!B@yBfMgHWf{wZHak;u6y4*d%wD4 zwe^0IiZr=4MV2i_H_$rP_EVsLb&j5=iB1NtkWytfoto-$sHy&o5Ho@`GXx3T@ZEjEzXZ1M7 zXZqCkpq)t?IHu|KM^GVn6R1UY^IJR$fUi)?oRw8BFKXbl;PW%9*O)k5xvn7PE$3q2 zcQ;-Rm+}5MEg9!{0Tkq95J5ImW4U-tU$ya&RX|l)ldMWWGU0#Z28hW90`ik{CJ)7} z)Wh&o%Kg&_O!2v>*nb_vb?LTU*9#~)8HZ(vOLu9il!UPadFv7s- z4q$|!=Q9v61gS^BC<0>#6ZdBHeg7n2ElZDjkoiQ#scojRx|CU&7e`?gYV!fIf0w8m zc*}3tI1Wjm4~_frQ<%;YS#~_veeR$59B}2#{5oe2hL}UdJriqh&mB^Pob$Xg55+t? z(1Dfn5F;q4Fou&n%jBer^UT>5$@;mQ7eP;You*s_{O zu_|SK%p@+WOJk)&PTCiAL-Lo9f6p6})Zw1H!9E#7o6g5L8snR_Im>A(vuab)^@NR; zzNfSoZVIrrBpr-z z5Qx-wDmL^42amO73Ve)V;Ct#h0hfaxYuB(wkKf*Hta`wF3@B$%2(N_x?Jq?qw&Aw7g{{IWl1ik$nU>)}O8f7)Ob-X^l1V12IW zR_$mOXVX4xmo~EQ(+)fNE*eSxKaCXsjz-oB7VM~C#8WFQ@ZXg%qElt`^&fXjBIaum zJIdW5b{<{qWJw-cn$YNJFjEw+7bu#aEEy5D$%^butJV|QiY)`p9Q72B!Utk||$v2CYf%&20|*tTukwry6dleN}+c5A!s zKQO+Hr}cj0>SP9ho|QLALPXAjjB5%X1gKX3Y83^k?0D`;`b(?ih6}E2EZaTCP58^^ z<|S3q;@u;Um$@yk7quk4iO!A85;t{~E+d>b0fGmP$%xh1d&b10oe1*I@_H6)RP#hS zf&`^$1OMK`e+!-;pmA8{)c>!1l!?8O6)ccY9t(`|f9;0>ZpipSJzovq{PT;vA{!hGAZ$7-r&ve}-V=)Em=%URt_Ase4<(S?J5I7KYX+! zz)nVkap=t(cx6-V0~RZht}H5>yy?5vn(EcI$>R8(5@_8N#6;31Wasjv67!XBIS8oA z+D^FdpWVYdjt;@Y@!94!S>ovn;%Y`X`)TUnMy<}CDdI$n8lG3}lk>_>J0<}?Rs_Od zvs_j2lOHmU!${gl&7^dO|B%><^&K_svC$wJ7}AqK!5c62A&sz3FktC)qfjO1#m>)O z&RZ+XGVeZMR5yubfr#ti9kNO`r`mfR{Y7nyrj_Smg;9SNUxMa&6-){@Pru&nF*WmH zNWk!%gsWRo8+tXlXeyy`Om_mv>>4B8V-k2kF*4|synptbYZUZbG@Fm|kLjL^x)4(n zEy*65y%$Vf?U&}?>nY7T)n&99NDmPYtv&`JE{m<1wC44fw9A_e6}GJ&>}lseVm~7` zfAsVztqxgixm`tb=n*>X5U^;*;_sA&7wH7L5&vj<`B^PftCmQ^mPiIr+{GPaZ5qr} zUOt(Dt0R*xH^N5!qe{yT&V2<>5Xl?ys`TaD<{+rouaEJh*eLbKDJb?+XesaKl{EHr z_gYt!s?{Qs35hpTo+cL8f!41(gxSTX40ifGVvMa|Ga-pH<{1N!n`7BG9U0VA*#788 zd9?f>yU0Mj`?K5{2*DA+aQ=qdC;lA!;XaEnk}IhJrh6p^X#`jJa=RNeQqv0#td`vv zg@A2ilh8RNO2C`b4U-_l><4GQstF{4nz^>4CrzEc7W2QUQMU&-l1-K#Omhs-BD|wi zFhzYdj5-}RNIa7l*0O>OvDb+$v|Qz5sJM!iNn+4;8^ZbakVR#{gLD}Fl2#OAKK!Eh zEc0(IH*_r~7gt5EGXn;h-m^8Vf&@IwJ;CU6l0{3GEXtAmblumn18=SucEh^kTGuUC zpY6KL5e;+TVw5e}4Jl*uhL{Pj^xVWvPKxmKUmq(Sg-_0u7pIbIq_Lfl9^>HiU^R3E z#D~l_n0~!41f3qhn1W7BlqI1AXbjs5zo3N3i4w5j*8?UWq1Hl1Tkfhg29Zz`to49U zv^`|b8!a9g{%j!n^Qwv z`n0z5(I(T;t*HF977htdrRJAt@bbH!WSXs;eWiyQ2E=3ljcnqLaRfVw^_)1yhXQO^ zw~pQ!u16`_`a7=vGSKKzAJZG@^owWQ1X~^IC*W$eXjb`=dqw zlou(HTkl%i`6`PrLa@DetSk2n+_i*0BA30#on0N)YCd4aqQb0lc5}J9D&|(*aK#IQ zIfdT5rG)|@4k5sl_@{8UHL?eNi!Ho@q)vBg@6?(v8gGVBIUZXb$I`J(oa>OlF8FdHsA%8>8xv)g*V8k&F*7`5kR4@kBb${__?I$-e0&rJ1bnDQ;VO7 zV9tI$t+$>=c$pr=(1s_g1Su0B;?Ud!;9Vg(=PL&Bex+ffCsub?ClgPeX}`y7ZMWHu z8*t|{u*kmz%c;W3BxwLYCDW0gg|5-Tk}k&+g+G~LZCRwPxjE-~cy8!HFcEWltf#UL zNXK2{Mwv~&`1TXmsH?i?G{%!h#OA0q#FnWsXe%D=HC^%xsN@oRRI#m4uqxKR$!S_Dq?7z?i&$k1pvlWNW;86NMq2Da?C(^Bf4Ol}?hiAgvziR-?WIDgO#Ps#o9(c?-XLT zffuQOD|tK4IQzuGw)^FWPV(U>WXx{tm~NA_XgSG&rQ~aj^jw|b3t&ck1Fbohi!Xbg zb^l!#&`qali}od4^ruYzUNqn-yYMm8k(;RtMY! z47kI(g_)lLyw4sq7o_q2{1R6EF2@|ezjh=>f{}@ZD7w;KO;7*w)d3pIBc51l%(`!#_8V+nqy?iicp zNDUg8Kj7c4!DRn<-V$DE_GPd*>BoZ>VItm4yw#*8|Fwl`E2S?Z$RS?C-r$QTO7Ub| zmDM@8gg{PdD;5giXg1?&x=$O;wL38_qOr}`ozPr9%`R;@T+EfLaUG{i!jq1qsD;nG zG_*NiK{ffsij^kfVtQf9D}Y#ea7Hfle111)aXp=PBPvO3D?0FR2YGYbYVt>a-Nn~5 zD>nZJEB5>P{0>XBy^n~l^Od3H4L5=X;6|A*cz*e(D)JUmw$3pxa9W-E{{|c2mmn;_ zAf8|(H6BzDm5lFF$Tm}!CCkic{JX-hNJf@_C~`;|{pEv&BwZjn8fCrC^5b@G)86g& z5;q^A@1*}CdGKQMEjD*03dhu}M6fUm_5u;~ zO5lBbV)p1kdF`MA@@@g`XOR2?@i(QF?6C;s!Vdh=!NKK}ae8g!3r!v1jSxIw&Q-Rg?slcMdcu%2}6~u zuX&g&96Kyl#3=c3tM$m1zC{lFyj%_}f&=&W(7NaW?>viARe76a0%DZ6s>@{%AZXS8 z`elY=;`s<9hBPWS&RVwBHXb2+z#-BS?-5+~EfmlTsz&i{!2>#^Hkod#YSv-%@NUlc z?&#&x$P;&8J=*)Z_xgw`8dnTpny^kcu%5@r$>WMRgn?gk+z-NsbUKAd2wvK7*Fonv z3r; z3!`F*>$|7p@ceX`-i2IpLe*4F6*x0373==n3?znD}=sXB+#aAg!G^*_Jp)-0iK3Fe#d#9~z z-3K*T>2OL zePv4v@?dql@tl}_S86r~pDICkV$0tOMoQBoWWS$2(&4fpu=3#mZ(i$yy^2~!k%yrS zLeVBNw_@~#z++#n$rLPCRdcF@zIs2uNU+>CgqK`piFFfYiBiTZ^{{#Ech1Y;-p1dU z1WN;?=Y7|8h+1Ivw(QNk7}jwFs%y!0PbI+|GDb^RS$F?TQ>D22C$6hyIcydve+6G% zsJ^MdPC6{SnS`_etfx52gjlUq5Q-(Q0lg_jB8X>cU#rc^JtHgQ4V7e(x<|EGmfFXz zC+XoNPEzA&6X_JeJL+a`x-Ny(pGbL~0P)-GPYI*4n`{FY+~MlQ3)gSc`qX*n$tL+c zq>uRI3#`j4neG3(;IMEq5it_k1Lbs3Kw1A=;%-#?f2}#;KmOAQ;jGGZq_o}CxDvYs zB_YOPBZLK6n(b!rgS3oK!fDNvgrdWW*!*~eB(k<9KJUNyaB}s;C{#Nuh{Mtxk2)kd zcos4lL~#<{u1{)htlPBk?S6mwM-%NC?~Bc?hbqmd6`X|Sq8cYV)20lVV==Lj1edyA z$>VFjF&Ye-Keo9d@mfzPK=pR;?#R-E^l`Bx!Dx-n{*ep`Q7Jw^kCC> zCyuwfYf8O7p4Km1UewQD%rvDW7h%ya0_KXc>e@}fE&JOhqN^dj zX(PHDhxAob2b&L9TOUPkK*>uaigaK?7=!Bm)xGDW9(%fnf4~8V8_Ub-r+TZByJ>g3 z7YmL3@S^uKkaH!N?VCG3yWs2{(T+Qm%#O0H*MQAfgVPM;78%~*-4sK}_i zic1`qE7if824d1z_PAtn-jM5RG}jC0(~IzZhU}||6T|Az-td*qejHiE?C)AUfSc&o zv=XjEz|HHlSa(&aJ$UEt87>6>YWIOO>pJZ;-J?-c*IKl6 zBWE$SdHFw%fHr1OItBIAEGQZ+Pt}0olX}VdDOE2L%{7PbUHOlnJPOg(1(?M!<{xAs zCU5j_hDY!f`j-KbL_zc$K6{Ug%!qDFt$%2i8d^|XO+Ii2Qc6S( z@I*Hsd-I}3{jao2%Uv}sFtNr07tdFO8FtN<~jvipF-^L zb^Hb)=`yD?7~N@Va_5U$f@Q5@P3N9(?94427Xv{twxK6@Rd_NrG$V&HE@}@HTc+UlsnjdwGr=wpC;k? zv89b-MJJyKptuJt(Kae!`$fo^N^JRiBT}Z|h(8jE4y(*kF4yxIocdI-2s6|yL2$eJ z$N!NT{aJk2F50KqG~8;mY*!zrU~5!Cy;VHk;5v!z!2W_vDhS>AUq8cu12{)T4g6_{ zixI#X(Ik#(SMfs}pE~xC^keg*kv1+e~hl+o*@A}!>{1*Wy|j5Nu%p?#P0TV#2&Jz$j00~`)$t;$k-VieH*m^ zyyInO%3@*lazV^X?0*MHRuA7d4~z`r4Nsm%Pgh6lSBo7kz;E(Kkn7CC8#@P=<&QML zcDIslisw(^X$g=bk4d=p>9buPbi!YwMEg5tpHJ?t2WP%6ZCQfx3Ogfo3s%&&jLlntq19o*&b!Sw5Uv zI$6G-tEx>ug>q5@Kz25aKo1Y#{b{ggm06drVefqFIJGi-T6hK4bLkYpIDa(A))L<_ zAD$RarW%`e$t4xn*GFv9489~1gZQWQSk?wyIj|0iUaA$!ZUG#QJAT0jj3F%`m*-g7 zhL$Rm9ce9r-TZiz0T!E(qFm3;BK&9@m3u27jc4N~X(I%#n|uoik0C&GQzF5*%(n__ z{~@Po!F<&m$F*UuY!rjVvqXFI(pq%S9%nstJH7+QtOObQ*+uAcv8MHXA5W(A>P+*J zthWA(|9s?+DK}yq{)$JZ?}+vy64gW3A+W;~7XY_S)EJX-0Qxe7gt#ET1c4h!G)Y1{ zP8T}m89|hebDXsr%|pn$sJO5A2nUBhGOf5@;7$A1%fdfZR9y2Q!#Vlh4ZQCp!9K99UZiz$ z1?&kqhUcM+8&`>#B>;#fJpC`;Q<|wD@7W7?C~BB`sGs~7S+yA0ahS2s!P_BqExkZQ z;JYQR!0LrJD_sJ@l;HU4Ce)W%^b@_ZG}^dp6Wlye>CxmBSVyae)nSSoaDUH19RmDU z;U=akch(_SF~#oROcykwP7qhC?czSp0PL&R6g9hWpQN%_oauvV>-;_NkC%{eRxfF- zXml`-wfv0gQRHsny+r5n&vAvBD{iE3&)EuNh?*JE6pqPnK2l$V|Dp- zisDh~p1blgew}rj6aO~)dMOp3f4xZ!f01ka;#JO5dZg{q;MEiiOVa8RajJ2u)cR%C z8r^AxnPk!D1@d#i!Y0#TsFrRHO6{OK-L9J5uwEb5uFY=?I%#(~fuE6d@i0NXJh^!1-4R6 z=Jz@ya@VoCg)q`1JbV1MnWsNkrt6x#j4%T=6s1}~xNV^?`rS3HW;`hn3TFO*NNjDn z`R!61nGOVk5Gr}UcU2DCZzUR>5@erC5P7W%%a75o>6+UhBOpxZPHD4%_TKmX<$@xV z%@K)&Co-i{4e_sz2}q#}#ZSw+BmjC-Mr<}r;`uR|zFEr|^_AANT?e$NEZ5DNMk+n} zx*Z;@HYH`zp@@Zde&b4loZ}P%iz%EC&!aIlRaN(lW_;LXBqn6mVi4x~!Tn+i|DJGY zSMvb3cT%>Y0y!#8T5lHSD78SvucU+bi~~aMTK$=jIiz(G(|IA+$UME3WT`PIIZdCws+l>5B(05I$=+3lC6ZDWzwb& zd<5?}L`5*5XD#PYwNW|{W-kU#NJ3-3QXPkY(7cU#TsO%1dB=4=Kg0tGd2p85LyPON zYl9#Oz`9~=uv&j2h>K_C-jU=uOa#6l6`jF}N_HRIRHv+D*o#TVwSBxPM7Kx_dK-RR!lFqg@Di}rs4!vN*v^vXI5iM*wZ zZXF|y`314j&RRy=(T!UrdyG0JUU8HL8>*vVg#7tU2l>e>&olU*qbwFS`-r&hx|Bm=eIA4CmagLlsHUt&v+Ruf9cj# zDes7E>#jcqd}t$9%^ zyti{R6hLkqj{_*>+#+2k&rU-^<%jYn=dyfHC6pm}zN7FLW6XHVqCo`}<8S9{va};} zUfg9It&}jDUSGV-lMKH@uXDNQi`95ovhGJWjPLNqY?~3crG~05`ae@;)$q2n=(@UO zC-=^9Ss<4jRDU7e?Qu#>i=-d$LboF1cJC#wmm6U z44U7e)HW&Y`c4xc2QaDBo~Yn`QHLc@hU#n7IJp!>+OvJ(~Ow}Z2(3Oq^CF+{|uMNCqAc^x$6jM3q1 zOM+{F{?nCbLxa|DL8b@h??}Hv5-!jhLipttY(xPR{3*&3Rwpv|n%gPUk9L0^)&674 zWT(#V2e#0e(M~>y(XjSkx71rkSPn>PmU|v(v~)(S)MTd^Q#7R1zgzRX2^>1zUAt~^ zT5|c4C8~3rO6M94JPoTAW6tBZladWRN=gHOR6jY^pQU-AffI&riiM)4KSc}qg9q+0 z$glH^l7^cL|80{Ri%EAgV%IPQqBgI75Rbbk8>-xKY98`V-z9LAOv3R`(qz)>*S?g% z?B>kJS0DFe+hXvf*IWvg!>9r#{HTy6*IqY zoZHaa!xYkr=#=p>P8CQ#It5p-2R4FpOL71W7Ie)n$1Cvvy0}}c5(pm+SExjEpboRu zY}kymqqzGsXP63$vZyvRZvNEX+-EHZ=ws%uKR{X68J~>lgL&Eck~4JimC} z^)GG$k6&5^#asWzwU#Ka@`iZrI1ptvM{32{@8C342!K!q) z=aqR{zBFmRaYeg^q%ahfc^yG(}6Vw0^L!7S9X}7Z2#?3WPW>e`|R-FL`sJ`z7ZB$u?6ka ztKM}N?$y;(S>s|X4k%3{n?}?_=|6R!PmnNUN$3LcWKg%DV9$a(;I=@F+R^L$>igLT zTkP1CaBxbZdrD#3?^2#$kiYFqmBtG=6?PR*;w#q$r@;h6jD z|io_i3o{wlbV;z9yPo;EOIT> z7-iajI@nky?61z%_Rp#g9VKufYic`6sB2Xit+Se(xMO-hMJX;yohmr<1`_nM^U7XU zo)9YSY`D8@Sxe9MtyS`Th=<9%TZ&sNcxqYRF>OVZd++Wc*gW z&@ha@`~lBHX$?~xBv8|BpqvnSZmp~&Mc}K3Q10G+!wVd)#`A?Lq`ecdw9#>Dhs?)e zb5){qLHkqjyTWttJVi~t%+@xZwNOGXKBNqqr14JkITyeL$#9ZmTHXeey}wMA)Vsrg z%HQCB7WUF^;UV?!@+_uhD*UE{at6DG0-|jO%#p*v^HHtUWRL=K_;7{u4f0AM16TzN72*_TYozMxoMaLX(ofwW)tV|XX=MUf$~@f+fDZ8;Q=Sl zuEi?<-jQuEEL#6T3N!-=>#3=7?0cC@O@q|e;~i%+2LFJ{Xn*-o;MC+FbLLk3oDaim zIa|$QG0>v_GAxZX#-agVwxVcaQ-=VnKa{urHcd$$!W;VE)UW6l!Z6TShSJLbpA;~s z71m>awbCWL;s@#C=-`U*k1T5^RcOXam7x*9S7uMmcT^b?s~Lyq%2txno(8y4IQJq= z(^yS4{{WdcVXb$Zk@8wO@U@h7T!CMEVl5#Neqi+1ujJs?*x|cdF&!wL0Dv~}0#IYW zMB7671g5(UUsU5&+M!+L5798deU_>2z*OCoQmQeI|Cp~|e z{vu%wYw2BJZG@(I$5+6c3jh;BH5tAa0o+(q6%u&a2Sd!_kpzBiK+%6Ro+i~dJQ^E( z=udz^>q3t(J}P9(5Mgi}i&R`yuc^K+%wCF(wjFiqALW0>JJYnW>pKLPiLoVCROFs? zV6cR;r4$JZvfji1OXyek5g%(Dwl*U;r#2;@M|Hk;2Zr)NER!#H4>8S%)|wJ}>RG7G z@CiRv{jwaxQ#E}qs$%wYW%)NRpzmf1qgFj`NY(D{{^QIV?_ z>4H@@%D?EBOLs}H(%;aIKe+@6EXvm=!H*tS0t zaMlRiy4HV6Mv$8Oxi;tfYeUBM?la=ZDe{$}=&2_#Tc7Fuy)Vde=mTT^1>VFT97{mK zV>7gu=OX2;Zg%pIHP7i+h@KCe=YEzb%7fxUMMc&sh-5#JZGG3A47AR&D)hmV2=qcP zADBcn@7eVZByDonj=Ap_BOi=%e#V_F?zys}*8xU)@AcRh=n;l={(t8H;XjyQ?Eg{i zr~zv0(cjqwpC@%_c|x)rBX<;P%ULXrO$eA7%uCO=y$f*lJX?&mmNt|9?rX&Yn^6uM zzlI<1x7-@{vYDf9rL4AdCmVc7wo3r%LK&GZSTMpBmo( zJ^E0I)2j35)1lfmkJG09D!Wi$98+mht_EZpdi8xcJx)goqVXJ9WT2wo_eh0*H>-H4 zWqq9tXreQceJyt(f^rxQW~MSjRtcfJinsX#h;StY8BswUh4F}_Z6S0^GrcX^S$|Is z2!+yQh+>F>x_+sM9im+LDrG5fx&Cagzy7W#r7fu;!eu4W$jJ~ED9=xg8HVU4{Q;zxL*rYNqwKxG_qbO;XR-(}g zOnDO*dUh-k!)>&{txy=HByh5$bvAH3hIP;aWsih*2G?P=q;$}Y%70WlS14CmLfRm8 zm@{MErh2rgSJQFLLBzloG&*m5XaFfRnh}YI&4IYC=nh_-Wtpa^g*Wjyzj5@gg5G|w zzMd>`lDrQbQh(`vh@%s`ZoHs;Wjk+k&4SM_=ZMllb6xepzZ<-hvmv2{h3hUiQ}=ACy&f)PKL}KVo>MDebOMumDY-{QYt5HwN!dF-5;inx^8#-Gn!pT;EDvRMUf5>nG*O zSIr|Wje-%pGTKK1nfnEW6H`+FC7pils?F)?nRT*WCos8G(a?;Av_(;8Ex4Hdm9jD1 z{2ZjOvWdk}r&7P2WQ#%O>nhJWGwZDzU>Ps27%muJCo9g*u8$l(V*wItJcb=SC!>Co zQENj)yu>R{rM9f-RER52CAKj>TY}hN;4gwH6xnyrl3iI`@zuqicMmm}F{8`j-!czG&x$a?v#%AhNP{jc?tay= zuU{BT^IjEyi=h^*v;mHI0M%aIg;$s6Q&=kZX4(ae=@pHb;yh-lio|JZgw4M`y&693 znP)Hd$S2fXjaKb#yw!80h614%7+5ZtL7^e+3ww3{GTldoFUd`ca_pw}JC zG~3x&NLvGMD}pi@OxC;geNcj~+G(!C=PKw;b7_m1Vvo&kR_W7%_6pb*d8N*pu&`5t zC+0mW7@R~E;ywPX?j%B2^Q)$ZjNq6Ov3-HDsY!x-KOWziVouKgt?HL*GQf1s|4_V3 zGWY~lXC#UAey0a=+!oX&N|;`7aXB0KIZEO7fQwA(UX|7++4k#v8$I-j1C$$>tioWMe4m4?Q1UhI->sw$uvG zS6PvX!|qp)vf>~xK{L1w1-qyhGoTS0$?;OIN30$l8-i3$BkVMt45raCT7(rD;D=?p ze+PGMgRTggL9kr;{js}{weu^qT&*Gz#i8A;t#Cly1t?z6bG{9v;OFb4M%x!WbkW5( zSTtDW#$i%i-0UL2YR*C_v+cG0$TXO#ErkYz2@R@L`{VbDo`4v)`FC<>*4Fdc%rL4> z@6FWfuy+3z{X*2;R{_NUs(7T883K~JxDZ`9m%A2c**iO!VxYKq<@)IMLaE|eOqB}w(A_2X*!!hN>qehf>=ux%fmWs zA!U*-0R-jD*ErJLYd;wG@@>{ZF=U@e3~bhfs;>>M6?INX_WHAb6s*PX|9cFx|7i;D z5eUBwv^z!Zq!Z_bn>U0d?9&0r_u7WYzyYD=0B8vcY)-1h6mC(r(GOtO8v1i57P=zm z(h;xEx@P1w4q1oIlvE>Y$Q9l-D*aDI9xRf*WZh8 z;wku3Rm;f>f$Q%Vjm)xD-2EeSr(yxj9j<9qv~@-i%w~=&qUop-HiGkV*I!Vl--;xx zVW#bv;il=@PZRi}vxH$Xt>Wk@9hpw`{6Dt+4}0?fT;2wtzul?F8F=a!i6v=T=Q`T}BO0%ys=;s47BcT3Sr? zY(8tafo06txq&v8ioY0)M?<~1ORW!vw@*3OH`eda@wJ2#&iQ_twOH-GsIs=abx1l9 z5`F6OEr`5tVx3yUO*@quT)ro(!f(NSW`KMv9fx<;#E}Ea>3f!qeD~j_P5?MzcZ%^+ z$#vUnbwi8xp{!L-G0PKAspkaF-F6C=Yh1D1@eb_$TV+;F`f{~0C)8*kjiMDvx)5?Z zybvmWPn+SVl+HtXH*Md{b9av{-9K^>3D(@BDoUl|Hn%F9JISPuicis4d+<}(^&I6c z7UROK^b9m6`{ZFUl^js1V}SO&KZd|6ud;l6aC5v+McnvZs=m2}E}%y=tu2WRT07>A zRwKJo_T7)}N@WjP+Kmy6da15Xj24RpIj?3T13mhYfL_7_!JA$3_=Pg&ZpxRcGL_B8 zCtM55DIL=cx(OA{aM|^(7H7RqtiNZu+BX~8{5n;njAm&-D5~blhJRj^~B@}93k|3c)>X$y3 z(8chh`2KPXsQKK16(GY%tuJhrSWAh+U2k!0L7rj2V=L~*goIoR`%EDJK~WgshP#p(#>zqN`6_#W&%qOw!h8m^RgY14=~VJBzvx*2 zY%?7{HcY+J@|SRu9DS+S`l6}*wp=Cb9OBo_%)``K@^uV+2Ov$yag6G%Ab)E>jA{Ai zXvV>xUEz!q6PPrvoW!%S@0A`59m8PoUjND<}Ij9LT?fN}IHz1^bA?yIGf) zgy1E04}|Ze$k3fv4YJHhonguhJH*n#Ke*n2C_ZI%N1n<9^)(IN8E8Cwe??${rz2k9 z@qEyGO(QOzrHY)vdny(a4D%ButEEOUxGNrhNRdv;GO4--N19AAIC%~73;Yq{vtmWk zXStMoZ3?c!Pk8l1N|L1jhIRRbG52o#>;6mvTDGI;v!!N+XmN1Wv34^-Uy>easfaIY zhcm~vVkQ?Q&`V|gNzJD89}h?o7F_xz(am|xkn&Ep2(if!{F8F|A2CyNfq({@jP5M5g@7#p;Duryta#!nA0Hq-3Als`H$9x=*wPCsO$4*Y zquG*+{Sl6%MtRSNetO4mKj4YhTJ9gym^49LKi0T@lMqS%WvTYGE7VT0@fQovYw z26mX6YBt6kpb;*z%O08ja^{tiKSyJk)CphJaqy-Sb2RbQXZbK`wuO|R!lU{pmaiib z^7!*`$i2m5ubsR%q8^Fk7&!9y`nZ$x=ba&o%jbh78!(l9aajJA1@tI*M&Nt%(5BI^ zZXt6Ec1738v{(PC;DrWSd;A`hklGngGQgNTy8>D#I>5yJ37~6qO>3+bMmpn#H_+GS zC>JP370_X;5M2CO1J*U1Yc8gA@oe~rpuxll9`DZJ(+o2|)42U`PE@P;ig_fk~woEK#WAiyt9iaRN2&S^@f@NJJS%yqUr6&J(cb{FA zhRhy4AB)Q%F%HfQHS5%1=DOH~P+-`8A6*+iLoVzllHWxe| z9KZ;`K!Z>WuPNfPHEzF^nq3)+&_U|1{ZG;f5@QVh=3=gbnNG8eAn&&6QyP->5B@bd zEs}KI(O+Ms70eD>&D=Mx({3{aLfW;K!m*1}aqp_O=tM+0LNhK~75eAY{g_4v+B=CR z4N-;&t7v5?F*b0j2TpE{aDkNL%M~R|BY@P0>2C0x`z@m0zqnCjqcRsdfWsMGu?#u; z_4a~mm7RchPm&S01nh9rZD&pPLm4tbh#4OKr}c^@ZU#pX&a04!~`fYd`piEEIt z;4C*bda|JB@_i%L>a=QUJsRi2M83L8ywXl=3vv9asGfz~<#8jS&?!=Zq52}59$-_pw!0e=_ z4`!Nw!#s5mU*0$?mjfDxE+63FuFbMJ%@TYTe!E>6*K-o%Q3O4rRv?lZM`{^64KH=triahgfALGt1Oy1Cv8e2q z^V5QjoDm%lCSarouBG%dTG?o zW#yK0@tL14-px7#oe}K06f|Fvw=0{E%XsmYnx{@r<_n}1W7G>meskN0hHj=IGE&LO z#YNu$N`r(=atOU?vLT7*i^vIU#EBj)`@as{#e0fK?%U_hijzXlSimZrl*7D2@y=vY zHO9}ROd;1sUe|WG3y)zh^gqUsR@bipb_plDgNduFzWs9P|5z5{RQ1MTO<(~3sD+l- zlN=D<_*S)qxDN80yis^|BC`R4ZuhJTE0abcWPM{C(zj>q#^WI+*S zXrHm6;pnb#`o!+wcc*DlH+TRBk;ab$zgf9c{cMdNbkykU&eXwvtW7Zvj(RuYupt41 z>4tyoK~0LlejgKAGT<^%YgwJ#_42IK@C3=0#Zni+URcNPOJC6zasR1-(sz@;X^e77 zE=7nficedoOUOyOmiXEO&v2-h8kdz<4CH4_E_oPY?80kJXZlDnq$>sJA{bXo6JQU~ zzI3c;@!1yd!XN$foPijE3KZUw%+g|%efV{1&*9WE9GeVE%cVhW)?`mLoVKMHGE@tp zJO7hdToJI5y0z1-$N9uOMt%XAA1k&Ne@uwjG#1C~63{BnfZg5`&Rb2AX;51GCp@xG zfMQny#7kTzv&leyR>2*hYfu)xu)@WXOSHIQc?geNOXJ31W=GN`^X{&XN!B?OFyLk= zqzTj68{nJpb2aAfwRJr&SXLU;Brq~iL)cbhK@kB0s- zid@gq{aXar+S)S1d+GL}S2ja~+nD=RCYxulbjQc(*;AvC1xZFgG~vsV@-BMh=Kgx2 zC%3>0$%?8>t2OW++5yEQ+_C9R2p`x=g7G1L#ombpwOas8)iw`{muZ4FC3d2c{8LFJ zK_c2SH|0$yr=f^fSK#`Mnu4HX?mM)B2wVN+V3W~sdN6+lu1-aRJmZw0@8ANYS-*eo z8mV7t0D;ye1SuUr`m90x7wSj+>D!oZHtjQlLh@_R1?BmgzRd?+`=oEcZ`|k`VaJZ7 zUtCAQB5dD8d^fnpYsP*>ODbHJJAAQjX_E(d5ns0mWpr0VbFb9aXkP1weQGs(bie}sAJ`9ZP4cOOZnI7R!j$w@+qa$rH^uFBe7{5M z+v-dI+iF_2_h_GF$L_ATBnSdA`n#7aZRRQ3M(=hJH#>R3PZ5o9j6}ZFK@(O~6h!%_ zP_HNK^X`c5pzqs?hM1dtO9n?K3o!DKl(F(*kgoe%sk@%FHmwHRh(8_<%R#@oi`Rsg zg)0l7g3~Iggk_hUt~eJ$(Gb&IQLKE|?C&l~>D9x;Jy9%u<6;-Dur0qphs0PS{yU0I z#Rvm7zjtVOz>|LN?v>(bO8fH6PWRR<5+=mzR|ly!`Oi0hFlw3T z3>p1mEEI?vQfNPr(0Z59`{9G9hl4$lItR>85*So-pKC?ocSh+958-!C_zag_n+~G1 zt*Wn%XGR6jc*B&}Q4+`uHM4Ho^a_nyEq1`3PZ4t^6Y&uqC)s7`RK*ot%DJ4V`?SiC z442{sD1602*?_(%Q`sk5VQq3)Mf|)bG6QdK*LAA_r1BRkYRw#SszHmx%dz-vQPP(z zbs+&7D)ch+qWBu*W=-mkS~m?d3}<=+*=YEUa{s7ua0VHj&D6}!hN6-mU*y%@yJY|) ztDFnBAAN071A`Yd4ajJ&;B)`tuBY`%c22^@h)^#o6d_%EaOs~=>CkiMxKxpil& za>rV-*GsKK+}F>~yCk^ktd=8!w4BQM%b_%NSsKX<6jkX+6+uO@^=7|i{JCAg5}$bEVuGzj@a(>^xh+S)QZX8rbkcD1LRx2s;hF3LtLE(O(vO z8}W46q>-;$2417VR*@iH2LEN0^8xD@(vxFv9-#Jv+4?7PaucG~;6!o)u)U`WayhM_ z0AycsU5cVUXk-6vr$lk-6TL31PGcuxivjs*;fHd&6=uo#q){Y`HF=}X!pDOr%U}!Y z=wpv1u6U&}^*|Lz8s)&gfE6#d8%uAJD9m3Sa%O8CW;_k(jk(3ki~o!ybPSM@plCt% zbjfLUXzHt&<$E?iDu-SKsC&cR|3IIPrRY=-ZJ0^~S4WaGeZ}%Rx5V~pvL~XnAM^Q} z8uUkPe{TxJP*HRDydm97{V5#g;6fD0LY>&zw2Px`oGeEa9LIzn!f$m1}Pk_^! zk|Sd*6#pr7`KWKd3T3QrjADB1ZdBeNZ`Gbqeu!zECl*Wr@OyH~6fnr>N?K3v^+D$9 z`_AWsz)i9eDnyRw*^gO^B9R);-Zx83Xj&rMQ^Op+l$Jlf5Sy8yR;zSr2+l}u=W%~J zk2CJr=8^g5J}@kB$4WzBS>LW|^54ydweAN`6nw}27s%bA+n@iwHhu};(Bc2lA~*l< zzS^6*#ebwb847BpJl@M*sS9~-+qJO&Xm>=Y@iXy3!m(=a*Xb*@jvsxy+*>|1!-#Se<+TF%r z8831Bbs+xXh)_lR6mdsvf@2v~2)8*W5TU zNzty2z$>u~s+E^Wm)|P6y{L#h{3}HVAtvkG-Z7!4Pb(L|f*~Y9^#uz9*CjRG$77!F z$;sE4zb!n=neUCSuSg@}%WSd9)qpb`U(Z3g5{z;bQ&%ub5K%*iw*M&l9tE+*f21^} z*_#avQo;WclPPnG&@8^7u7wruj2;hF_fp}p%m(T=wPY=MMS6a9(FQ?y$6`c3kR!|E ziI=PC`ab~l(6x<9AS~KOhz^y51@n6yz`6kBK<3F4T5eHI5Fc%jo-sk;Qtm8$q?8C= zhLg{R;+MGgi-9+)T$O~|S7NW^fFfB6Y-zp_`><5mR zwqZt@W%ye&taFdPle!dh%+!nZU~B|X*i?8m;k$rPtXQErYwc#Xm?F8KjWDItm2xdm zv>}+JQzvj8Cp>tUmjU-lX8TO|!24q4ph`v2+Nl5*_TUb8APSce_isN{N<2ku z`gJUDVYTEDa#S;fi?YdF(;<(FNN*{!`RzYo+ctGsguX95_ddVESs=|Wn$cX&PDTIv z&(;9qnRj2s1ONJ=;R!LH{f@N0#Mn+CCR%{Rz*H3)d|5(QN&#vn_!Srh>dqSVE08fv zYbxmmHJL0I)|AB~-!UB$t7%2jFT^Al=|;TZ%5c%5;WGzMm zo^e`sTD5EM|5+<&x*2I5 zPOWjM$);2$EI0HaDRNW(8;+-m-F~xPpl4@)%@y)m&oW zmhv6DK+EQ5;@8ZQU~2*d8o?n`_j3zXXn*A{`H|*1u=#O#()g!p$47$_Ld1Db4TvwQ z3JuZhC$bRp)#gz{{c%R_nu9?EGVGKkID3Y*+Er)vuj`F?+0O}muCVnxgKw4W{GN8t zHMa+bOU+d`tN1=)uX=v<%?CH)gB>shF>ie|6drTzLW~LX)pyVcR^HRbVf0hI_{Tv< zHbez%3|CdN_0A$6?b3&8pq^m@xt`tCy^7@wHN@i(Vrcx`#VyMQ!V`x0`?3x6<4FF9dzL?8J(*fa6Q%eIR2b6uNDYE~hM7faC? z3n)`G>-m}02qrSC`1(xh`&t7wg?4CJNG{g1bZ!mGam%MAWg|3Yg)-^KSch;m(`Ri@ zJfDWP@7gVwmg>5F*Qu;pExR&z(xv78j{!A)-1YA8$miqjxgO^_>)Yc;=hx3#15YkL zX8mrJJ}3ve@;nfXI@im?Hom|PV7{k#U+uXMzC5R~AoC0Ur#Ey&e=IKVrDfE;?!_)M zM?DHAb*6FVu+4Se?+gh3Zh{R7HZjln3iq4gSS9khN+wGYv+22$w(GWRl6+!wPw}pk zEbld|b_=n**sWU{$u-xvz{K-h-vk}UW!8bl`Jl_!o<_KLBbw`XC-$Y$*@L3{Tro$C3$p);0L|t<}{=)XkohgAus)xO8gV8Yv)u|K9NBes*69%SL#JG2^m<0`@e^RrNK#8fHjoV9*fm zisxPdf+_j$+{4Z@<4M)L^CPn>c6Wns-g$}Q=d5cAU*N&rh9QbQPHMS6bHumm*n(c^ zZ%W_9GOk4Oeu8Z+f4U-h>~Xs%ZXbm4*``ISmv`!U+2NBljc00h6$^QHD++w!+&x1# zn2q2p>%O&bMVaQC(BW6NboNI%@?MO~id;9~D)xd#ZdDz}(Js1T4?)k!);^Mg{@v^t zWb-5P3jXV8N`DteJV4NQ^2l~(XZnNZHTaA^_v;nC)4TdwMBI7;G;HTkY#ww}`|G89 z+8K4env_(h*#&>{(o$iL{p8Q5!w$voHBpYZgSA7-!AZdf)R^jY4yEyO@ zW}|bwy^?5R?N!W40HWd*B)I9KNp|%$k(ovLCqOOm5ejXr;zyYl6!4rZdChUZUt}W6 z6FyD+l9Y$)CeTIOve7V1a;We|{#e!yPP7Q&DE#9L#hP3|m6lWhrR2zocon6+hLF;%abu=YsW%2Y!0M)WF0U%1bw8dWhEY zC-zMY5~4RWN+|oC3K%j8F(tWH>B2S|wTme!3M+HD~L~$D7TvnR!Zy#`pOz%qC9qUfHPa<(3{aO z+pS0_8MSVrRKBRs_nu*@R6h$qfmdL`&RLZ=t+B*LnxP?3KpJk&d=q9-{}!94Q)(3( z4h!ZH331=*xx=t=&=^iEn{Tv{je(Qg^?bMS_{(Tb>Il^MmIc)Khj0I<#tNMfF>CN3f zvxpz@RoqBK?ys$izCsB3@6*?YQ2dTSIGhf%y!$YH;|rbZ^LgKat}33un(zGc!_-C< z9CowC>=fsBbr6Bn?sL0cwe%d>M^oV<*2R{*-Y)dP;OETQ#0qZZO8djw zUGiU%HKZd-ig->~mt22hTDF@u$Cra%FQ)Ji2^V;fsJ9Yv7ldBqryb(h7@T%Hy zg$(N|wOX}+Nwc@h{d*>wHzWJcvs^A@4*@MVZH42vaHZTLS8$j~Tz5xDb94Bwop+@? zjD~j`Zf#*-*Czeed&KaVF3jcdonx(z$Xr;s-CAVYYqa#Zo9w0O?w(nvj2Ptcs!t0w$_wr zyt1P}xMhKrr?ECCo$D!&#fQnhXcb}cVj6%lRq6;DOU z%`x~2U@i;KNwa_~1iv({)pz;R{`_!!WK4Ws{)7YhHYE_6T8SgOK&&b31_zer=&i=A zQ173dpRDXn7N@p*+RI4XKN~f-U|r{98HRa^B+xFzt@j2Umam5+Y zG>N$TZl3jTef|%@n5IPp=6iTHzg0bCncT+}_mAM_+pe_Q%YKViYvhhEYG0xxnT^kcy z+Q~ai2|CHT#wH^?nn*k0#YC8;&8S$`GkTInZ(lc5%5~yF^H|RjX%`#pzT4zl1;w$` zC(y|OWiCvw88$znComYo!{7;p-8mR8DzmfnF5A<`w&Ne0K=c@0jpN^YV|#2gBfPY~ z%X-F;>0@5drHC(sp;C=Qmf@*>W{jW+{TJIfcEfm12+ zHCkEY_;b{6Fe`bIO+Y756Au%0iCdonle`z%%O>YIa`-VRhhAex#3qNA08vyaLHJ$J zV0i<7xS3E0^BYw13usE#4v3WHK`ru#(OI9|WrJ7e@B`suapYS6}-N^3^e+KxH47iE%b#{y>gvd20 zc!ss4BTPM)$pz^pcFE0c;@3l4?G3v)y7$wiUydGkFzKPu)+=yiP)*vG@WH)VcKtPq zA*?*WuVLs&Kgo=VXE{ErEqk_El86TcQwn*xdMGKcO_l;Y3@^7N{+`ssbc1YLMjzi% zenPK_Zdy2Z26MI4^Vo?DvI_uxZMkf+b`&B}*5f3LBBZGj2uGE9Q;9E!%|kMwHZbof zAF&!*F=g_AL+=xC&?4DMEHr?D5AK*-pqn+YK?d<@_=0LA%X^0LrFL{1C`dxlooUiO zk5HXj^M|pM!xLR7S`)~@!?iQ)OB4fWj8qQFtC67#wUk}i%R zQ0Oo>rvUoNJr~vl0mESgBX0HaL=62IMY)FK@CV1Q1qKhz_}va)BUKjywpPR5>QrsI z=N?5`(pQ7Z%^iLL1tQ*O7|_oO@`c9p=!Hs`lY*?q;kTS)y{nvK*7U7g3v{hp6jH;S zKicZway2w~Iy$Ewae5RD*;#ntN>w;C2y3NM@a;$o6K-LTR!fzSf5}-KKsz9TwJXk*R9bn_I$1{CmV}0#Nwp7LR-Ai>5ntWuSN<;l z#OdP5?tI6Tny-DL4-h(fl~mEWUd7OE<9X9iA3f#{x|CS?`aNobI2K7 z8c31x($4`qBZBq~25gcWJE^{|G`#}iSq)#xkFQ(^mmM$HRhE5=e=|Ne9fYK(=ar4h z&Bexz&Q!D?X3Araj8`WJYrB54y0_ z-uGiD74H{PK1OBFe8!eIz1rYy-Y69H?6Sx$_%cLraMta>XfJG$S9;b%703~&LulQ4R&m4q~3hj1~LrigW40 z@BXf>yEmDj&G?e<7Oql7x&l5tD<4!OYpq-suZyp^B;OO26@hQCm@eFHlToJttp7d0 zc6D>a{{z4#%Z$x)`#qV>=W0y~PaZ310s=C+nQ;}bXfw-<{TD<Rq%NZVTsY zM||W4((C=i=Z+IDpTB*5nQ=Kr{I_^1^Iw3?A}}6#&)~3;8)-@@+PW>89hJYUTvv>$#{Ei2t(&lo4B<;W)rqY%Tum5T`w$u%n!kn8?&2I&q9uFkYT#9GGzR*4oaU<0}#T-xvBp?eQP4>DZ~u{m*f&L^Vkc0tX@ z1-wn98!x?Ye;E_i3GdE@?u?{clO%1Ra(xra z=jrJARNHtAy&X^%y|MQ4HZh{-Pm=ixn>auHdB{{6?ZB~o=atB|Dibm+K^Ej?`5BIn zaL29r4v$^T#}*`b9#iiJopo%=^0h4bhpODoPP`$WRAS0S$_kq$2 z8HazvVAfJMSUO}Y#N6T-f|4S1Kzi4#!2fsFBeywD_7g6pN1z0IA6j_mmvqw z3LMvqwaG68AJ|^MpNxyht?mvDpoD)M8@Z9iSoy<-IE|u~@i2tbSLk?>*fe86RONEe zDlBmaO7ZjK6UAB-a2EB!*++>&Br0t#k?mNw^`gU5YxNh8J+899Hm=eu-Xm%giR~74 zY{4kR`xjyDIWm1@S5^P#Z92(+5nj?1NV^X3hC19iSHxY9owRAv!z*KUOPUv7-5YO( z8{h8-ht0mLV;!6Q?KAn5PV*qfdx! z3MqpC{gLkgZvY>2eMVzEg9}L)NZ;fpek17Fiw1l_l8|MmV17*}9rtF3<^|+k-C-0x zBDFvmC&f!4M%gBXn>)&Le>Scl&q+bBN`4(}LGB5AJ!@-QD&7+G0igr;6LD53SWyaY zBA3kf%aOtg)!Xxp*Xo?kI?1}$I`*leMdU0**ilOFJH;6N46x%Pu-W_xXz@Vg9ejB> zwRpgDs*6pj{8||Odc;U@EpI?@cDREu10q9~i5z8K9_wBKzg!xuUik;%KSAOXgqC6) zZm2l3&@udgc{TUdh^YMVn>Qz$UH7+ne)V85aDSwrzHB>cexsO)*QhLX%}vL1p^zrfGJ6g0^>R z`cD7Szb1~0Yrk6b=mFY#-hRG(d%Pwqs$_Y;8MptV`MYbxlMl(S2I+g){)7G{Tz${< z9==wI>@mGn^9&JATQ~^-ja-zQu1byZUW`4Sa_+V#6nB=Xw&qRJ1_)hjK=X&|W`*z% za?5&OT|wS@oPOK3O3HOrixV7yy z!L~0`Wp)ALUIF_Nx?Z;f#*O!$izh20@C?>h-*rmQm&SRm+!_{q`JNpX@D|SR|CFUv z;IugQjnZDF&#I=c&B(p`uIsSCTh@=aEPk!@lZkt&2KK798N;+%B}%)Nmz5txP>A?B zxIaZ6k$}n!-vEsooeH`LG)MX@`B~F8Q&L?*otRDuS0;>&?;2guU{-C~NJF~15YEI) z;K)kJ>)iNFatf76xd&M>jS~49o%|sCy7trgpQFkVMuNRYcIi}pEp@Iy%Wa<3P-qPP zMKCX}rRU%WoD2}G6Ee5mndCc3X0(JLy#2YB%a_ZZeS1K4L_fbsp^>!J9FNI-6fB7w z{+>C7%CgdjVCp2%gLZh3*K+trExZMz4AqI;(wmB>_n6;2dif~$lrjY-usLEAU1B?$ z!kg4)UO|%GKbW9e6KHD)4p%hULkjh5RaWmP z2S^_?tv=ayE7YfKF&bL!G8#_G*U!%>)c-wXG(3Ef4i8@Bm$=Wp@U!EK(^^fU6R084 zDVLkcaQEE+X@KHDk^__Bp^0Msw`3;;*V`k;H#-0r8)6vcqy|xCO~FrlrN{97+)!GI+)rGiro%5 zr7JTa6SvCFSDgOPCn`oj+e5W&PW!ju#|YoC%=ySQYlIu6qtD0IiLd+ipUS!-MbjJ| zsE$wVdyU)bEolWSF9n1G;RL!L*0a=;KkBmkePp5B%=)I$%eo$GiddpxJfCKrH{3H2 z;p(PjnUPylbBh3t=c^V(ApAiUOx4#}mGt*cjBr{o4`#5hrZ@T9vK3=?*N)2IlV$|( z+_X98)!iSQ`DHUDhyt~bn^NVo{}Tf(&`8cfvvc*lHIV+S)MEVR7G8nKTukePMSkr~ z3OxM#9p$h?M72~3wcYI`0+XC^@ugF_S+T|i3am)X=~F&MG+{ID+mGFYg|&YUQbI24 z=3+pct>_~x^phIfHy!??l5+Q5kRqRh?b!l!ipCb$$I>$KW6)eP?Fv2jPo&jhO)g`6C zFP{cW@k4llAQ?e^9x%^ah=72sG)M*rk$DRdqZ9cbml$JmD_OhPdfL%}_(1q$kiewYle39{`%FEOhm@T3vw`` ze8%za`}Nk+LMr$p1S?FfiQ*m!aQSob{=7zz#U48=l zmmR^u9F3n#B{4tB0d>>$n!-araN*OMOR7iML#X5O&XJ4uNy<5?i@pv;dM=2Z(-msM z13yS=)mxO0n39fi@>@que~$2bjIJk#ON*l zaXIJcMfIemE?fuQ5}fKoG~trv0$Au40{OSA_(k)imd@iJHkZ@5)xVLh;2H=dg_F~|?HIdD#+>MQP2BzZ~1RRUR=qQ-oo03&3}Rl-K>LJ!3w%-@13 z2w#j!u{@C=lMEN`wJNU@;mP_?l;NN}jAux%3C-MxW^HnaJ-Br6yTn7)K$Fk!<8qZ6*%$;gnWw8({c$_B_qN7S6YFoMDP&cEnPx&= zp5Q&J$-YJo(I>_fIO0^`jiCM6$BZa$PxT)MR&xrOUxc!EVq7=akq0m`h-g`q+vBG` z<9Tx3dYtWSjtyiVVCU^vjSQq!y0LASRBrQ5e)sq)6!r{)Ghxf1Ob2RQFA`sULLEm> zli=A!yoAJvPN*6nNV4-PGRYu$(#|9DvZ(8L?(nvKo8o0@fFmjY49K}{zyhve_R510 z$-zBlDmAVLMmEOk2!;A?_`vR{?m#weY9um1NRc20xj>IS6jdo`Eo1u`KCLc+M!eI7 z0>otw?2X*M!EQu$33lIpQ-6S1vx=F5bfNBs^Cs|M6;%(hp%ctD+w`LHuE3hs{e#05o_}8*3GSI4^;>mEzAK zyF}c`_QeH@F=kA-Y15lbiVmG(2krJoAP`C>OwU?StcW`a`KQvsT0zU{S#jgryNz3oE#e)H6Wj%==u+Q z&K?~n6jdreIXQA@RXIGiin7*f6+9n_EQORCG%jHmA7WNUP>eH;>VQ-iv`9w2zHc*gckTr-Q^(BQx4f zNqFZ7kH5Nb_~-e;!AuDFDg{>w4PK4j%nzjz#Peqdz2bHHXUQ(ECt7oe6S6T?B%brf z%iJ*u;e1L7^1Q=*yk`{%{JJpU2L&&Je{9`|BjSjerb;&Mfw%SZQ+;0kYzA7^K{4k} zviEMW=#KaEq`KUwX@IuL(}C-|59gL2ZQ+SNPjOF+cre7Hk~)l5^x81^D>Nbr9`*Mn z_CO@Apdm9-J0vl*+L@W6BT7AlMgBhs0$s2FrDxpb5qG@Z2gT0T_aAgRDaZM53G~nw zMNYHIwrbjTHy27<581g}28SwU5n<`t^;6m=ZeMkcoCj}{Pll!fo<$zfLYXTT|BwE| W$#(YuJS+i_01qY;lZ>h?=Klf|IQ#hk delta 43602 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 From 138ac26fa8fd63b82ffe82ff0d2f0191d840ad23 Mon Sep 17 00:00:00 2001 From: Amandine Grappe Date: Wed, 26 Aug 2026 16:18:58 +0200 Subject: [PATCH 9/9] Answer comments and optimize the path when a file is already there --- imotionsApi/NAMESPACE | 1 - imotionsApi/R/imotionsApi.R | 56 +++++++++------- imotionsApi/man/getHttr.Rd | 4 +- .../testthat/test-getAOIRespondentMetrics.R | 36 ++++++++-- imotionsApi/tests/testthat/test-getSensors.R | 13 ++++ .../tests/testthat/test-httrRequests.R | 65 ++++++++++++++++--- .../tests/testthat/test-uploadAOIMetrics.R | 44 ++++++++++--- 7 files changed, 169 insertions(+), 50 deletions(-) diff --git a/imotionsApi/NAMESPACE b/imotionsApi/NAMESPACE index cf1338b..553dc10 100644 --- a/imotionsApi/NAMESPACE +++ b/imotionsApi/NAMESPACE @@ -61,7 +61,6 @@ importFrom(dplyr,mutate_at) importFrom(purrr,map_chr) importFrom(rlang,enquo) importFrom(tidyr,unnest) -importFrom(utils,download.file) importFrom(utils,tail) importFrom(utils,type.convert) importFrom(utils,unzip) diff --git a/imotionsApi/R/imotionsApi.R b/imotionsApi/R/imotionsApi.R index 9657a4b..eac828b 100644 --- a/imotionsApi/R/imotionsApi.R +++ b/imotionsApi/R/imotionsApi.R @@ -964,7 +964,7 @@ getSensors <- function(study, target, stimulus = NULL) { if (study$connection$localIM == TRUE) { for (i in seq_along(sensors)) { suppressWarnings(signalsMetaData[[i]] <- rbindlist(sensors[[i]]$signalsMetaData, fill = TRUE)) - signals[[i]] <- sensors[[i]]$signals + signals[i] <- list(sensors[[i]]$signals) sensors[[i]][c("signals", "signalsMetaData")] <- NULL } } else { @@ -1478,7 +1478,7 @@ getSensorData <- function(study, sensor, signalsName = NULL, intervals = NULL) { #' #' @importFrom arrow read_parquet set_cpu_count #' @importFrom rlang enquo -#' @importFrom utils download.file unzip +#' @importFrom utils unzip #' @return A data.table with all signals (or specified signals) from the sensor of interest. #' @keywords internal privateDownloadData <- function(study, sensor, signalsName = NULL) { @@ -1651,7 +1651,7 @@ getAoiRespondentData <- function(study, AOI, respondent) { #' #' @keywords internal privateGetAoiRespondentMetricsPath <- function(study, AOI, respondent) { - if (is.null(study$connection$localPath)) return(NULL) + if (is.null(study$connection$localPath) || !length(AOI$fileId) || is.na(AOI$fileId)) return(NULL) return(file.path(study$connection$localPath, sub("^.*/aoiDefinition/", "", AOI$fileId), paste0(respondent$id, "metrics.csv"))) @@ -1701,21 +1701,21 @@ getAoiRespondentMetrics <- function(study, AOI, respondent) { # Retrieve the S3 location only when the metrics are not already cached locally. if (is.null(metricsFile) || !file.exists(metricsFile)) { - stats <- tryCatch({ - getJSON(study$connection, getAoiMetricsUrl(study, respondent, AOI), - message = paste("Retrieving AOI respondent metrics for", endpoint)) + metricsFile <- tryCatch({ + stats <- getJSON(study$connection, getAoiMetricsUrl(study, respondent, AOI), + message = paste("Retrieving AOI respondent metrics for", endpoint)) + + if (is.null(stats)) { + NULL + } else { + getFile(study$connection, stats$aoiRespondentStatsUrl, + message = paste("Downloading AOI respondent metrics for", endpoint), + localFilePath = metricsFile)$file_path + } }, error = function(err) { if (grepl("Resource not found", err$message, fixed = TRUE)) return(NULL) stop(err) }) - - if (!is.null(stats)) { - metricsFile <- getFile(study$connection, stats$aoiRespondentStatsUrl, - message = paste("Downloading AOI respondent metrics for", endpoint), - localFilePath = metricsFile)$file_path - } else { - metricsFile <- NULL - } } } @@ -1983,6 +1983,7 @@ privateUploadAoiMetrics.imRespondent <- function(study, obj, AOI, metrics) { dataFileName <- paste0(tools::file_path_sans_ext(AOIDetails$fileId), "metrics.csv") } else { dataFileName <- tempfile(fileext = ".csv") + on.exit(unlink(dataFileName), add = TRUE) } fwrite(x = metrics, file = dataFileName, col.names = TRUE, row.names = FALSE) @@ -1996,14 +1997,12 @@ privateUploadAoiMetrics.imRespondent <- function(study, obj, AOI, metrics) { fileInfos <- privateUploadToPresignedUrl(study$connection, uploadUrl, postData, dataFileName, "Getting presignedUrl to upload AOI metrics", uploadMessage) - putHttr(study$connection, uploadUrl, reqBody = toJSON(fileInfos, null = "null"), - message = paste("Upload of AOI metrics for", endpoint, "confirmed")) - - # Check if some locally saved metrics needs to be invalidated + # The uploaded file makes any locally cached metrics stale, even if confirmation fails. cachedFile <- privateGetAoiRespondentMetricsPath(study, AOI, obj) if (!is.null(cachedFile)) unlink(cachedFile) - unlink(dataFileName) + putHttr(study$connection, uploadUrl, reqBody = toJSON(fileInfos, null = "null"), + message = paste("Upload of AOI metrics for", endpoint, "confirmed")) } } @@ -3067,8 +3066,6 @@ getJSON <- function(connection, url, message = NULL, ...) { #' @return A list with the temporary folder path and the downloaded file name. #' @keywords internal getFile <- function(connection, url, message = NULL, fileName = NULL, localFilePath = NULL) { - response <- getHttr(connection, url, message) - # Use the requested file path, or fall back to the connection/temporary download directory. if (!is.null(localFilePath)) { tmp_dir <- dirname(localFilePath) @@ -3084,12 +3081,18 @@ getFile <- function(connection, url, message = NULL, fileName = NULL, localFileP # Reuse files already downloaded to the selected path. if (!file.exists(file_path)) { dir.create(dirname(file_path), showWarnings = FALSE, recursive = TRUE) - download.file(response$url, file_path, method = "auto", mode = "wb") + tmp_path <- paste0(file_path, ".part") + + getHttr(connection, url, message, writePath = tmp_path) + file.rename(tmp_path, file_path) } else { message("Retrieving local data for ", file_path) } - if (grepl("zip", response$headers$`content-type`)) { + # Check the retrieved/downloaded file for a zip signature directly + is_zip <- identical(readBin(file_path, "raw", n = 2), charToRaw("PK")) + + if (is_zip) { files_in_zip <- unzip(file_path, exdir = tmp_dir) assertValid(!is.null(fileName), "You need to provide a fileName for zip file extraction.") file_path <- str_subset(files_in_zip, fileName) @@ -3145,10 +3148,11 @@ jsonHeaders <- function() httr::add_headers("Content-Type" = "application/json") #' @param url The url/path where the file is located. #' @param message Optional - a short message indicating which steps are getting performed to get a more indicative #' error message. +#' @param writePath Optional - A local path where the response body should be streamed. #' #' @return The last response. #' @keywords internal -getHttr <- function(connection, url, message = NULL) { +getHttr <- function(connection, url, message = NULL, writePath = NULL) { if (connection$localIM) { # Locally there is no point to retry request if we get a 404 not found error terminate_on <- 404 @@ -3156,7 +3160,9 @@ getHttr <- function(connection, url, message = NULL) { terminate_on <- NULL } - response <- retryHttr(message, "GET", url, tokenHeaders(connection$token), terminate_on = terminate_on) + output <- if (is.null(writePath)) httr::write_memory() else httr::write_disk(writePath, overwrite = TRUE) + response <- retryHttr(message, "GET", url, tokenHeaders(connection$token), output, terminate_on = terminate_on) + return(response) } diff --git a/imotionsApi/man/getHttr.Rd b/imotionsApi/man/getHttr.Rd index f2e188f..f6f6f78 100644 --- a/imotionsApi/man/getHttr.Rd +++ b/imotionsApi/man/getHttr.Rd @@ -4,7 +4,7 @@ \alias{getHttr} \title{Perform a GET HTTP request with authentication.} \usage{ -getHttr(connection, url, message = NULL) +getHttr(connection, url, message = NULL, writePath = NULL) } \arguments{ \item{connection}{An imConnection object as returned from \code{\link{imConnection}}.} @@ -13,6 +13,8 @@ getHttr(connection, url, message = NULL) \item{message}{Optional - a short message indicating which steps are getting performed to get a more indicative error message.} + +\item{writePath}{Optional - A local path where the response body should be streamed.} } \value{ The last response. diff --git a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R index b874a0b..e669d77 100644 --- a/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R +++ b/imotionsApi/tests/testthat/test-getAOIRespondentMetrics.R @@ -25,19 +25,27 @@ test_that("remote check - should return the local AOI respondent metrics path", expect_identical(privateGetAoiRespondentMetricsPath(study_cloud_local, AOI_cloud, respondent), expectedTmpPath) }) +test_that("remote check - should not return a local path when the AOI file ID is unavailable", { + AOI_cloud$fileId <- NULL + expect_null(privateGetAoiRespondentMetricsPath(study_cloud_local, AOI_cloud, respondent)) + + AOI_cloud$fileId <- NA_character_ + expect_null(privateGetAoiRespondentMetricsPath(study_cloud_local, AOI_cloud, respondent)) +}) + # getAoiRespondentMetrics ============================================================================================= context("getAoiRespondentMetrics()") mockGetAoiRespondentMetrics <- function(study, AOI, respondent, AOIDetailsFile = NULL, stats = NULL, expectedFilePath = NULL, fileExists = FALSE, expectCallsDetails = 0, expectCallsFileExists = 0, expectCallsFread = 0, expectCallsGetJSON = 0, - expectCallsGetFile = 0) { + expectCallsGetFile = 0, fail_getFile = NULL) { privateGetAoiDetails_Stub <- mock(AOIDetailsFile) file.exists_Stub <- mock(fileExists) fread_Stub <- mock(metrics_output) getJSON_Stub <- mock(stats) - getFile_Stub <- mock(list(file_path = expectedFilePath)) + getFile_Stub <- if (!is.null(fail_getFile)) mock(stop(fail_getFile)) else mock(list(file_path = expectedFilePath)) metrics <- mockr::with_mock(privateGetAoiDetails = privateGetAoiDetails_Stub, file.exists = file.exists_Stub, @@ -158,9 +166,7 @@ test_that("remote warning - no metrics have been uploaded for this respondent", stats = stop("Resource not found"), expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, expectCallsGetJSON = 1), - "No metrics found for AOI: El Manuel Area, Respondent: Wendy", - fixed = TRUE - ) + "No metrics found for AOI: El Manuel Area, Respondent: Wendy", fixed = TRUE) expect_null(metrics) }) @@ -172,6 +178,26 @@ test_that("remote error - getJSON errors other than resource not found should be "Server error", fixed = TRUE) }) +stats <- list(aoiRespondentStatsUrl = "https://s3.test/respondent-aoi-metrics.csv") + +test_that("remote warning - missing AOI respondent metrics file should return no metrics", { + expect_warning(metrics <- mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, stats = stats, + expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, + expectCallsGetJSON = 1, expectCallsGetFile = 1, + fail_getFile = "Resource not found"), + "No metrics found for AOI: El Manuel Area, Respondent: Wendy", fixed = TRUE) + + expect_null(metrics) +}) + +test_that("remote error - getFile errors other than resource not found should be rethrown", { + expect_error(mockGetAoiRespondentMetrics(study_cloud_local, AOI_cloud, respondent, stats = stats, + expectedFilePath = expectedTmpPath, expectCallsFileExists = 1, + expectCallsGetJSON = 1, expectCallsGetFile = 1, + fail_getFile = "Server error"), + "Server error", fixed = TRUE) +}) + test_that("remote check - should call getJSON, getFile and fread when no local path is set", { stats <- list(aoiRespondentStatsUrl = "https://s3.test/respondent-aoi-metrics.csv") diff --git a/imotionsApi/tests/testthat/test-getSensors.R b/imotionsApi/tests/testthat/test-getSensors.R index e8ee894..8d1ea09 100644 --- a/imotionsApi/tests/testthat/test-getSensors.R +++ b/imotionsApi/tests/testthat/test-getSensors.R @@ -95,6 +95,19 @@ test_that("local return - imSensorList object", { expect_s3_class(sensors, c("data.table", "data.frame"), exact = TRUE) }) +test_that("local return - should preserve a NULL signals entry for the last sensor", { + sensors_with_null <- jsonlite::fromJSON(sensorsRespondentPath, simplifyDataFrame = FALSE) + sensors_with_null[[4]]["signals"] <- list(NULL) + + sensors <- mockr::with_mock(getJSON = mock(sensors_with_null), { + getSensors(study, respondent) + }) + + expect_equal(nrow(sensors), 4) + expect_length(sensors$signals, 4) + expect_null(sensors$signals[[4]]) +}) + test_that("remote return - imSensorList object", { respondent <- getRespondents(study_cloud)[1, ] expectedEndpoint <- "Retrieving sensors for respondent: bab55356-43fc-4c25-a39d-a1d513965614" diff --git a/imotionsApi/tests/testthat/test-httrRequests.R b/imotionsApi/tests/testthat/test-httrRequests.R index 4c13fbb..c0f5ae7 100644 --- a/imotionsApi/tests/testthat/test-httrRequests.R +++ b/imotionsApi/tests/testthat/test-httrRequests.R @@ -91,15 +91,17 @@ test_that("error - invalid response", { # getHttr ============================================================================================================= context("getHttr()") -mockedGetHttr <- function(connection, url, mockResponse, terminate_on) { +mockedGetHttr <- function(connection, url, mockResponse, terminate_on, writePath = NULL) { config <- tokenHeaders(connection$token) + output <- if (is.null(writePath)) httr::write_memory() else httr::write_disk(writePath, overwrite = TRUE) retryHttr_Stub <- mock(mockResponse) response <- mockr::with_mock(retryHttr = retryHttr_Stub, { - getHttr(connection, url, "Test API") + getHttr(connection, url, "Test API", writePath = writePath) }) - expect_args(retryHttr_Stub, 1, "Test API", "GET", url, config, terminate_on) + expect_args(retryHttr_Stub, 1, "Test API", "GET", url, config, output, terminate_on) + return(response) } @@ -117,6 +119,14 @@ test_that("remote/local check - should work correctly", { expect_identical(response, mockResponse, "response should not have been modified") }) +test_that("remote check - should stream a GET response to the requested file", { + mockResponse$status_code <- 200 + class(mockResponse) <- "response" + + response <- mockedGetHttr(connection_cloud, "url", mockResponse, terminate_on = NULL, writePath = "download.part") + expect_identical(response, mockResponse, "response should not have been modified") +}) + # getJSON ============================================================================================================= context("getJSON()") @@ -150,13 +160,16 @@ context("getFile()") sensors_cloud <- suppressWarnings(jsonlite::unserializeJSON(readLines("../data/imSensorList_cloud.json"))) mockGetFile <- function(connection, url, mockResponse, fileName = NULL, localFilePath = NULL) { - getHttr_Stub <- mock(mockResponse) + getHttr_Stub <- function(connection, url, message, writePath = NULL) { + # Mimic write_disk() by copying the fixture to the requested path. + file.copy(sub("^file://", "", mockResponse$url), writePath, overwrite = TRUE) + return(mockResponse) + } fileInfos <- mockr::with_mock(getHttr = getHttr_Stub, { getFile(connection, url, "Test API", fileName, localFilePath) }) - expect_args(getHttr_Stub, 1, connection, url, "Test API") return(fileInfos) } @@ -213,15 +226,51 @@ test_that("remote check - should call getHttr and download a file to an explicit test_that("remote check - should call getHttr and reuse a file from an explicit local path", { url <- "https://s3.test/respondent-aoi-metrics.csv" expected_path <- "../data/AOImetrics.csv" - mockResponse <- list(headers = list("content-type" = "text/csv")) - class(mockResponse) <- "response" + getHttr_Stub <- mock() - fileInfos <- mockGetFile(connection_cloud, url, mockResponse, localFilePath = expected_path) + fileInfos <- mockr::with_mock(getHttr = getHttr_Stub, { + getFile(connection_cloud, url, "Test API", localFilePath = expected_path) + }) + expect_called(getHttr_Stub, 0) expect_identical(fileInfos$file_path, expected_path) expect_true(file.exists(expected_path)) }) +test_that("remote check - should reuse a cached ZIP without a file extension", { + connection_cloud$localPath <- "../data" + file.copy("../data/example_data_cloud.zip", "../data/example_data_cloud", overwrite = TRUE) + getHttr_Stub <- mock() + + fileInfos <- mockr::with_mock(getHttr = getHttr_Stub, { + getFile(connection_cloud, "api/content/StudyUpload/example_data_cloud", "Test API", + sensors_cloud[2, ]$fileName) + }) + + expect_called(getHttr_Stub, 0) + expect_true(file.exists(fileInfos$file_path)) + unlink(c("../data/example_data_cloud", "../data/ProgramData"), recursive = TRUE) +}) + +test_that("remote error - should not promote a partial download", { + expected_path <- file.path(tempdir(), "aoi-metrics-cache.csv") + on.exit(unlink(paste0(expected_path, ".part")), add = TRUE) + + expect_error( + mockr::with_mock(getHttr = function(connection, url, message, writePath = NULL) { + writeBin(charToRaw("partial"), writePath) + stop("Network error") + }, { + getFile(connection_cloud, "https://s3.test/respondent-aoi-metrics.csv", "Test API", + localFilePath = expected_path) + }), + "Network error", + fixed = TRUE + ) + + expect_false(file.exists(expected_path)) +}) + # postHttr ============================================================================================================ context("postHttr()") diff --git a/imotionsApi/tests/testthat/test-uploadAOIMetrics.R b/imotionsApi/tests/testthat/test-uploadAOIMetrics.R index 70f0423..cba57eb 100644 --- a/imotionsApi/tests/testthat/test-uploadAOIMetrics.R +++ b/imotionsApi/tests/testthat/test-uploadAOIMetrics.R @@ -42,15 +42,23 @@ metrics <- data.frame("metric1" = 2, "metric2" = 234, "metric3" = 1234) mockPrivateUploadAoiMetrics <- function(study, obj, AOI, metrics, AOIDetailsFile = NULL, expectedFilepath = NULL, expectedEndpoint = NULL, expectedBody = NULL, expectCallsfwrite = 0, - expectCallsPut = 0, expectCallsPresigned = 0, expectFileRemoved = FALSE) { + expectCallsPut = 0, expectCallsPresigned = 0, uploadStub = NULL, + confirmationStub = NULL, expectFileRemoved = FALSE) { privateGetAoiDetails_Stub <- mock(AOIDetailsFile) fwrite_Stub <- mock() fwriteMock <- if (expectFileRemoved) function(...) data.table::fwrite(...) else fwrite_Stub getUploadAoiMetricsUrl_Stub <- mock("myurl") fileInfos <- list(assetId = "assetId", presignedUrl = "presignedUrl") - privateUploadToPresignedUrl_Stub <- mock(fileInfos) - putHttr_Stub <- mock() + privateUploadToPresignedUrl_Stub <- if (is.null(uploadStub)) mock(fileInfos) else mock(stop(uploadStub)) + putHttr_Stub <- if (is.null(confirmationStub)) mock() else mock(stop(confirmationStub)) + + if (expectFileRemoved) { + on.exit({ + uploadedFile <- mock_args(privateUploadToPresignedUrl_Stub)[[1]][[4]] + expect_false(file.exists(uploadedFile), info = "temporary metrics file should have been deleted") + }, add = TRUE) + } mockr::with_mock(privateGetAoiDetails = privateGetAoiDetails_Stub, fwrite = fwriteMock, @@ -83,10 +91,6 @@ mockPrivateUploadAoiMetrics <- function(study, obj, AOI, metrics, AOIDetailsFile expect_args(putHttr_Stub, 1, study$connection, "myurl", reqBody = toJSON(fileInfos, null = "null"), message = paste("Upload of AOI metrics for", endpoint, "confirmed")) - - if (expectFileRemoved) { - expect_false(file.exists(expectedFilepath), info = "temporary metrics file should have been deleted") - } } if (expectCallsfwrite > 0) { @@ -117,10 +121,17 @@ test_that("remote check - should call privateUploadToPresignedUrl and putHttr fo expectCallsPut = 1, expectCallsPresigned = 1, expectFileRemoved = TRUE) }) -test_that("remote check - should remove cached metrics after uploading for a specific respondent", { - expectedTmpPath <- paste0("myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/", - "dd8a9342-f5c0-4a02-bf27-de68fc13f2bc/", respondent_cloud$id, "metrics.csv") +test_that("remote error - should remove the temporary metrics file when upload fails", { + expect_error(mockPrivateUploadAoiMetrics(study_cloud, respondent_cloud, AOI_cloud, metrics, + AOIDetailsFile = AOI_cloud, uploadStub = "Upload failed", + expectFileRemoved = TRUE), + "Upload failed", fixed = TRUE) +}) + +expectedTmpPath <- paste0("myLocalPath/93fbaae0-8b6f-45b6-b5dd-9a5d4216d7fd/", + "dd8a9342-f5c0-4a02-bf27-de68fc13f2bc/", respondent_cloud$id, "metrics.csv") +test_that("remote check - should remove cached metrics after uploading for a specific respondent", { dir.create(dirname(expectedTmpPath), recursive = TRUE) file.create(expectedTmpPath) @@ -131,6 +142,19 @@ test_that("remote check - should remove cached metrics after uploading for a spe unlink(study_cloud_local$connection$localPath, recursive = TRUE) }) +test_that("remote error - should invalidate cached metrics before upload confirmation", { + dir.create(dirname(expectedTmpPath), recursive = TRUE) + file.create(expectedTmpPath) + + expect_error(mockPrivateUploadAoiMetrics(study_cloud_local, respondent_cloud, AOI_cloud, metrics, + AOIDetailsFile = AOI_cloud, confirmationStub = "Confirmation failed", + expectFileRemoved = TRUE), + "Confirmation failed", fixed = TRUE) + + expect_false(file.exists(expectedTmpPath)) + unlink(study_cloud_local$connection$localPath, recursive = TRUE) +}) + segment <- getSegments(study_cloud) test_that("remote check - should call getUploadAoiMetricsUrl and putHttr for a specific segment", {